Skip to content

Commit 874d55d

Browse files
committed
staticaddr: fractional loop-in amount
1 parent 4d4bcf4 commit 874d55d

File tree

5 files changed

+131
-12
lines changed

5 files changed

+131
-12
lines changed

interface.go

+2
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ type StaticAddressLoopInRequest struct {
338338
// swap payment. If the timeout is reached the swap will be aborted and
339339
// the client can retry the swap if desired with different parameters.
340340
PaymentTimeoutSeconds uint32
341+
342+
SelectedAmount btcutil.Amount
341343
}
342344

343345
// LoopInTerms are the server terms on which it executes loop in swaps.

staticaddr/loopin/actions.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,14 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
6868
}
6969

7070
// Calculate the swap invoice amount. The server needs to pay us the
71-
// sum of all deposits minus the fees that the server charges for the
72-
// swap.
73-
swapInvoiceAmt := f.loopIn.TotalDepositAmount() - f.loopIn.QuotedSwapFee
71+
// swap amount minus the fees that the server charges for the swap. The
72+
// swap amount is either the total value of the selected deposits, or
73+
// the selected amount if a specific amount was requested.
74+
swapAmount := f.loopIn.TotalDepositAmount()
75+
if f.loopIn.SelectedAmount > 0 {
76+
swapAmount = f.loopIn.SelectedAmount
77+
}
78+
swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee
7479

7580
// Generate random preimage.
7681
var swapPreimage lntypes.Preimage

staticaddr/loopin/loopin.go

+38-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package loopin
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -93,6 +94,8 @@ type StaticAddressLoopIn struct {
9394
// swap.
9495
DepositOutpoints []string
9596

97+
SelectedAmount btcutil.Amount
98+
9699
// state is the current state of the swap.
97100
state fsm.StateType
98101

@@ -287,10 +290,20 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
287290
weight := l.htlcWeight()
288291
fee := feeRate.FeeForWeight(weight)
289292

290-
// Check if the server breaches our fee limits.
291-
amt := float64(l.TotalDepositAmount())
292-
feeLimit := btcutil.Amount(amt * maxFeePercentage)
293+
// Determine the swap amount. If the user selected a specific amount, we
294+
// use that and use the difference to the total deposit amount as the
295+
// change.
296+
var (
297+
swapAmt = float64(l.TotalDepositAmount())
298+
changeAmount btcutil.Amount
299+
)
300+
if l.SelectedAmount > 0 {
301+
swapAmt = float64(l.SelectedAmount)
302+
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
303+
}
293304

305+
// Check if the server breaches our fee limits.
306+
feeLimit := btcutil.Amount(swapAmt * maxFeePercentage)
294307
if fee > feeLimit {
295308
return nil, fmt.Errorf("htlc tx fee %v exceeds max fee %v",
296309
fee, feeLimit)
@@ -314,6 +327,14 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
314327

315328
msgTx.AddTxOut(sweepOutput)
316329

330+
// We expect change to be sent back to our static address output script.
331+
if changeAmount > 0 {
332+
msgTx.AddTxOut(&wire.TxOut{
333+
Value: int64(changeAmount),
334+
PkScript: l.AddressParams.PkScript,
335+
})
336+
}
337+
317338
return msgTx, nil
318339
}
319340

@@ -373,11 +394,24 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
373394
return nil, err
374395
}
375396

397+
// Check if the htlc tx has a change output. If so we need to select the
398+
// non-change output index to construct the sweep with.
399+
htlcInputIndex := uint32(0)
400+
if len(htlcTx.TxOut) == 2 {
401+
// If the first htlc tx output matches our static address
402+
// script we need to select the second output to sweep from.
403+
if bytes.Equal(
404+
htlcTx.TxOut[0].PkScript, l.AddressParams.PkScript,
405+
) {
406+
htlcInputIndex = 1
407+
}
408+
}
409+
376410
// Add the htlc input.
377411
sweepTx.AddTxIn(&wire.TxIn{
378412
PreviousOutPoint: wire.OutPoint{
379413
Hash: htlcTx.TxHash(),
380-
Index: 0,
414+
Index: htlcInputIndex,
381415
},
382416
SignatureScript: htlc.SigScript,
383417
Sequence: htlc.SuccessSequence(),

staticaddr/loopin/manager.go

+81-5
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"sort"
78
"sync/atomic"
89
"time"
910

1011
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
12+
"github.com/btcsuite/btcd/btcutil"
1113
"github.com/btcsuite/btcd/btcutil/psbt"
1214
"github.com/btcsuite/btcd/chaincfg"
1315
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -20,7 +22,9 @@ import (
2022
"github.com/lightninglabs/loop/staticaddr/deposit"
2123
"github.com/lightninglabs/loop/swapserverrpc"
2224
looprpc "github.com/lightninglabs/loop/swapserverrpc"
25+
"github.com/lightningnetwork/lnd/input"
2326
"github.com/lightningnetwork/lnd/lntypes"
27+
"github.com/lightningnetwork/lnd/lnwallet"
2428
"github.com/lightningnetwork/lnd/routing/route"
2529
)
2630

@@ -205,8 +209,8 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
205209
case request.respChan <- resp:
206210

207211
case <-ctx.Done():
208-
// Noify subroutines that the main loop has been
209-
// canceled.
212+
// Notify subroutines that the main loop has
213+
// been canceled.
210214
close(m.exitChan)
211215

212216
return ctx.Err()
@@ -529,14 +533,30 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
529533
req *loop.StaticAddressLoopInRequest) (*StaticAddressLoopIn, error) {
530534

531535
// Validate the loop-in request.
536+
if len(req.DepositOutpoints) == 0 && req.SelectedAmount == 0 {
537+
return nil, fmt.Errorf("no deposit outpoints provided and no " +
538+
"amount selected")
539+
}
540+
541+
var (
542+
err error
543+
selectedOutpoints = req.DepositOutpoints
544+
)
545+
// If there's only an amount selected by the user, we need to find
546+
// deposits that cover this amount.
532547
if len(req.DepositOutpoints) == 0 {
533-
return nil, fmt.Errorf("no deposit outpoints provided")
548+
selectedOutpoints, err = m.selectDeposits(
549+
ctx, req.SelectedAmount,
550+
)
551+
if err != nil {
552+
return nil, err
553+
}
534554
}
535555

536556
// Retrieve all deposits referenced by the outpoints and ensure that
537557
// they are in state Deposited.
538558
deposits, active := m.cfg.DepositManager.AllStringOutpointsActiveDeposits( //nolint:lll
539-
req.DepositOutpoints, deposit.Deposited,
559+
selectedOutpoints, deposit.Deposited,
540560
)
541561
if !active {
542562
return nil, fmt.Errorf("one or more deposits are not in "+
@@ -549,8 +569,17 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
549569
}
550570
totalDepositAmount := tmp.TotalDepositAmount()
551571

572+
// If the selected amount would leave a dust change output or exceeds
573+
// the total deposits value, we return an error.
574+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
575+
if totalDepositAmount-req.SelectedAmount < dustLimit {
576+
return nil, fmt.Errorf("selected amount %v leaves "+
577+
"dust or exceeds total deposit value %v",
578+
req.SelectedAmount, totalDepositAmount)
579+
}
580+
552581
// Check that the label is valid.
553-
err := labels.Validate(req.Label)
582+
err = labels.Validate(req.Label)
554583
if err != nil {
555584
return nil, fmt.Errorf("invalid label: %w", err)
556585
}
@@ -616,6 +645,7 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
616645
}
617646

618647
swap := &StaticAddressLoopIn{
648+
SelectedAmount: req.SelectedAmount,
619649
DepositOutpoints: req.DepositOutpoints,
620650
Deposits: deposits,
621651
Label: req.Label,
@@ -635,6 +665,52 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
635665
return m.startLoopInFsm(ctx, swap)
636666
}
637667

668+
// selectDeposits finds a set of deposits that are ready to be used for a
669+
// loop-in and cover a given amount. It returns the outpoints of the selected
670+
// deposits.
671+
func (m *Manager) selectDeposits(ctx context.Context,
672+
amount btcutil.Amount) ([]string, error) {
673+
674+
// TODO(hieblmi): provide sql query to get all deposits in given state.
675+
allDeposits, err := m.cfg.DepositManager.GetAllDeposits(ctx)
676+
if err != nil {
677+
return nil, err
678+
}
679+
680+
deposits := make([]*deposit.Deposit, 0)
681+
for _, d := range allDeposits {
682+
if d.IsInState(deposit.Deposited) {
683+
deposits = append(deposits, d)
684+
}
685+
}
686+
687+
// Sort deposits by confirmation block in descending order first to pick
688+
// the oldest deposits, then sort by deposit amount in descending order.
689+
sort.Slice(deposits, func(i, j int) bool {
690+
if deposits[i].ConfirmationHeight !=
691+
deposits[j].ConfirmationHeight {
692+
693+
return deposits[i].ConfirmationHeight >
694+
deposits[j].ConfirmationHeight
695+
}
696+
697+
return deposits[i].Value > deposits[j].Value
698+
})
699+
700+
// Now select deposits from the front of the sorted slice until the sum
701+
// satisfies the required amount.
702+
selectedDeposits := make([]string, 0)
703+
for _, d := range deposits {
704+
amount -= d.Value
705+
selectedDeposits = append(selectedDeposits, d.OutPoint.String())
706+
if amount <= 0 {
707+
return selectedDeposits, nil
708+
}
709+
}
710+
711+
return nil, fmt.Errorf("not enough deposits to cover amount")
712+
}
713+
638714
// startLoopInFsm initiates a loop-in state machine based on the user-provided
639715
// swap information, sends that info to the server and waits for the server to
640716
// return htlc signature information. It then creates the loop-in object in the

staticaddr/loopin/sql_store.go

+2
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
230230
HtlcTimeoutSweepAddress: loopIn.HtlcTimeoutSweepAddress.String(),
231231
HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate),
232232
DepositOutpoints: joinedOutpoints,
233+
SelectedAmount: int64(loopIn.SelectedAmount),
233234
PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds),
234235
}
235236

@@ -378,6 +379,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
378379
LastHop: row.LastHop,
379380
QuotedSwapFee: btcutil.Amount(row.QuotedSwapFeeSatoshis),
380381
DepositOutpoints: depositOutpoints,
382+
SelectedAmount: btcutil.Amount(row.SelectedAmount),
381383
HtlcTxFeeRate: chainfee.SatPerKWeight(
382384
row.HtlcTxFeeRateSatKw,
383385
),

0 commit comments

Comments
 (0)