Skip to content

Commit 1440b6b

Browse files
committed
loopout: add asset loop out payment flow
1 parent 0a0cda4 commit 1440b6b

File tree

6 files changed

+317
-14
lines changed

6 files changed

+317
-14
lines changed

client.go

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ var (
7979
// quote call as the miner fee if the fee estimation in lnd's wallet
8080
// failed because of insufficient funds.
8181
MinerFeeEstimationFailed btcutil.Amount = -1
82+
83+
// defaultRFQExpiry is the default expiry time for RFQs.
84+
defaultRFQExpiry = 5 * time.Minute
85+
86+
// defaultRFQMaxLimitMultiplier is the default maximum fee multiplier for
87+
// RFQs.
88+
defaultRFQMaxLimitMultiplier = 1.2
8289
)
8390

8491
// Client performs the client side part of swaps. This interface exists to be
@@ -506,9 +513,30 @@ func (s *Client) resumeSwaps(ctx context.Context,
506513
func (s *Client) LoopOut(globalCtx context.Context,
507514
request *OutRequest) (*LoopOutSwapInfo, error) {
508515

509-
log.Infof("LoopOut %v to %v (channels: %v)",
510-
request.Amount, request.DestAddr, request.OutgoingChanSet,
511-
)
516+
if request.AssetId != nil {
517+
if request.AssetPrepayRfqId == nil ||
518+
request.AssetSwapRfqId == nil {
519+
520+
return nil, errors.New("asset prepay and swap rfq ids " +
521+
"must be set when using an asset id")
522+
}
523+
524+
// Verify that if we have an asset id set, we have a valid asset
525+
// client to use.
526+
if s.assetClient == nil {
527+
return nil, errors.New("asset client must be set " +
528+
"when using an asset id")
529+
}
530+
531+
log.Infof("LoopOut %v sats to %v with asset %x",
532+
request.Amount, request.DestAddr, request.AssetId,
533+
)
534+
} else {
535+
log.Infof("LoopOut %v to %v (channels: %v)",
536+
request.Amount, request.DestAddr,
537+
request.OutgoingChanSet,
538+
)
539+
}
512540

513541
if err := s.waitForInitialized(globalCtx); err != nil {
514542
return nil, err
@@ -529,7 +557,10 @@ func (s *Client) LoopOut(globalCtx context.Context,
529557
}
530558

531559
// Create a new swap object for this swap.
532-
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server, s.assetClient)
560+
swapCfg := newSwapConfig(
561+
s.lndServices, s.Store, s.Server, s.assetClient,
562+
)
563+
533564
initResult, err := newLoopOutSwap(
534565
globalCtx, swapCfg, initiationHeight, request,
535566
)
@@ -574,6 +605,14 @@ func (s *Client) getExpiry(height int32, terms *LoopOutTerms,
574605
func (s *Client) LoopOutQuote(ctx context.Context,
575606
request *LoopOutQuoteRequest) (*LoopOutQuote, error) {
576607

608+
if request.AssetRFQRequest != nil {
609+
rfqReq := request.AssetRFQRequest
610+
if rfqReq.AssetId == nil || rfqReq.AssetEdgeNode == nil {
611+
return nil, errors.New("both asset edge node and " +
612+
"asset id must be set")
613+
}
614+
}
615+
577616
terms, err := s.Server.GetLoopOutTerms(ctx, request.Initiator)
578617
if err != nil {
579618
return nil, err
@@ -608,12 +647,67 @@ func (s *Client) LoopOutQuote(ctx context.Context,
608647
return nil, err
609648
}
610649

611-
return &LoopOutQuote{
650+
loopOutQuote := &LoopOutQuote{
612651
SwapFee: quote.SwapFee,
613652
MinerFee: minerFee,
614653
PrepayAmount: quote.PrepayAmount,
615654
SwapPaymentDest: quote.SwapPaymentDest,
616-
}, nil
655+
}
656+
657+
// If we use an Asset we'll rfq to get the asset amounts to use for
658+
// the swap.
659+
if request.AssetRFQRequest != nil {
660+
rfqReq := request.AssetRFQRequest
661+
if rfqReq.Expiry == 0 {
662+
rfqReq.Expiry = time.Now().Add(defaultRFQExpiry).Unix()
663+
}
664+
665+
if rfqReq.MaxLimitMultiplier == 0 {
666+
rfqReq.MaxLimitMultiplier = defaultRFQMaxLimitMultiplier
667+
}
668+
669+
// First we'll get the prepay rfq.
670+
prepayRfq, err := s.assetClient.GetRfqForAsset(
671+
ctx, quote.PrepayAmount, rfqReq.AssetId,
672+
rfqReq.AssetEdgeNode, rfqReq.Expiry,
673+
rfqReq.MaxLimitMultiplier,
674+
)
675+
if err != nil {
676+
return nil, err
677+
}
678+
679+
// The actual invoice swap amount is the requested amount plus
680+
// the swap fee minus the prepay amount.
681+
invoiceAmt := request.Amount + quote.SwapFee -
682+
quote.PrepayAmount
683+
684+
swapRfq, err := s.assetClient.GetRfqForAsset(
685+
ctx, invoiceAmt, rfqReq.AssetId,
686+
rfqReq.AssetEdgeNode, rfqReq.Expiry,
687+
rfqReq.MaxLimitMultiplier,
688+
)
689+
if err != nil {
690+
return nil, err
691+
}
692+
693+
// We'll also want the asset name to verify for the client.
694+
assetName, err := s.assetClient.GetAssetName(
695+
ctx, rfqReq.AssetId,
696+
)
697+
if err != nil {
698+
return nil, err
699+
}
700+
701+
loopOutQuote.LoopOutRfq = &LoopOutRfq{
702+
PrepayRfqId: prepayRfq.Id,
703+
PrepayAssetAmt: prepayRfq.AssetAmount,
704+
SwapRfqId: swapRfq.Id,
705+
SwapAssetAmt: swapRfq.AssetAmount,
706+
AssetName: assetName,
707+
}
708+
}
709+
710+
return loopOutQuote, nil
617711
}
618712

619713
// getLoopOutSweepFee is a helper method to estimate the loop out htlc sweep

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/lightningnetwork/lnd/clock v1.1.1
3030
github.com/lightningnetwork/lnd/queue v1.1.1
3131
github.com/lightningnetwork/lnd/ticker v1.1.1
32+
github.com/lightningnetwork/lnd/tlv v1.2.6
3233
github.com/lightningnetwork/lnd/tor v1.1.2
3334
github.com/ory/dockertest/v3 v3.10.0
3435
github.com/stretchr/testify v1.9.0
@@ -123,7 +124,6 @@ require (
123124
github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect
124125
github.com/lightningnetwork/lnd/kvdb v1.4.10 // indirect
125126
github.com/lightningnetwork/lnd/sqldb v1.0.4 // indirect
126-
github.com/lightningnetwork/lnd/tlv v1.2.6 // indirect
127127
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
128128
github.com/mattn/go-isatty v0.0.20 // indirect
129129
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect

interface.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ type OutRequest struct {
9898
// the configured maximum payment timeout) the total time spent may be
9999
// a multiple of this value.
100100
PaymentTimeout time.Duration
101+
102+
// AssetId is an optional asset id that can be used to specify the asset
103+
// that will be used to pay for the swap. If this is set, a connection
104+
// to a tapd server is required to pay for the asset.
105+
AssetId []byte
106+
107+
// AssetPrepayRfqId is the rfq id that is used to pay the prepay
108+
// invoice.
109+
AssetPrepayRfqId []byte
110+
111+
// AssetSwapRfqId is the rfq id that is used to pay the swap invoice.
112+
AssetSwapRfqId []byte
101113
}
102114

103115
// Out contains the full details of a loop out request. This includes things
@@ -145,6 +157,25 @@ type LoopOutQuoteRequest struct {
145157
// initiated the swap (loop CLI, autolooper, LiT UI and so on) and is
146158
// appended to the user agent string.
147159
Initiator string
160+
161+
// AssetRFQRequest is the optional RFQ request that can be used to quote
162+
// for asset rfqs using the asset client
163+
AssetRFQRequest *AssetRFQRequest
164+
}
165+
166+
type AssetRFQRequest struct {
167+
// AssetId is the asset that we'll quote for.
168+
AssetId []byte
169+
170+
// AssetEdgeNode is the pubkey of the peer that we'll quote for.
171+
AssetEdgeNode []byte
172+
173+
// Expiry is the unix timestamp when the rfq will expire.
174+
Expiry int64
175+
176+
// MaxLimitMultiplier is the multiplier that we'll use to calculate the
177+
// max limit we'll quote for.
178+
MaxLimitMultiplier float64
148179
}
149180

150181
// LoopOutTerms are the server terms on which it executes swaps.
@@ -181,6 +212,31 @@ type LoopOutQuote struct {
181212
// SwapPaymentDest is the node pubkey where to swap payment needs to be
182213
// sent to.
183214
SwapPaymentDest [33]byte
215+
216+
// LoopOutRfq is the RFQ that can be used in the actual loop out to
217+
// commit to an asset exchange rate.
218+
LoopOutRfq *LoopOutRfq
219+
}
220+
221+
// LoopOutRfq contains the details of an asset request for quote for a loop out
222+
// swap.
223+
type LoopOutRfq struct {
224+
// PrepayRfqId is the ID of the prepay RFQ.
225+
PrepayRfqId []byte
226+
227+
// PrepayAssetAmt is the amount of the asset that will be used to pay
228+
// for the prepay invoice.
229+
PrepayAssetAmt uint64
230+
231+
// SwapRfqId is the ID of the swap RFQ.
232+
SwapRfqId []byte
233+
234+
// SwapAssetAmt is the amount of the asset that will be used to pay for
235+
// the swap invoice.
236+
SwapAssetAmt uint64
237+
238+
// AssetName is the human readable name of the asset.
239+
AssetName string
184240
}
185241

186242
// LoopInRequest contains the required parameters for the swap.
@@ -430,6 +486,9 @@ type SwapInfo struct {
430486
// channels that may be used to loop out. On a loop in this field
431487
// is nil.
432488
OutgoingChanSet loopdb.ChannelSet
489+
490+
// AssetSwapInfo contains the asset information for the swap.
491+
AssetSwapInfo *loopdb.LoopOutAssetSwap
433492
}
434493

435494
// LastUpdate returns the last update time of the swap.

loopd/swapclient_server.go

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,38 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
210210
PaymentTimeout: paymentTimeout,
211211
}
212212

213+
// If the asset id is set, we need to set the asset amount and asset id
214+
// in the request.
215+
if in.AssetInfo != nil {
216+
if len(in.AssetInfo.AssetId) != 0 &&
217+
len(in.AssetInfo.AssetId) != 32 {
218+
219+
return nil, fmt.Errorf(
220+
"asset id must be set to a 32 byte value",
221+
)
222+
}
223+
224+
if len(in.AssetRfqInfo.PrepayRfqId) != 0 &&
225+
len(in.AssetRfqInfo.PrepayRfqId) != 32 {
226+
227+
return nil, fmt.Errorf(
228+
"prepay rfq id must be set to a 32 byte value",
229+
)
230+
}
231+
232+
if len(in.AssetRfqInfo.SwapRfqId) != 0 &&
233+
len(in.AssetRfqInfo.SwapRfqId) != 32 {
234+
235+
return nil, fmt.Errorf(
236+
"swap rfq id must be set to a 32 byte value",
237+
)
238+
}
239+
240+
req.AssetId = in.AssetInfo.AssetId
241+
req.AssetPrepayRfqId = in.AssetRfqInfo.PrepayRfqId
242+
req.AssetSwapRfqId = in.AssetRfqInfo.SwapRfqId
243+
}
244+
213245
switch {
214246
case in.LoopOutChannel != 0 && len(in.OutgoingChanSet) > 0: // nolint:staticcheck
215247
return nil, errors.New("loop_out_channel and outgoing_" +
@@ -709,23 +741,52 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
709741
req.SwapPublicationDeadline,
710742
)
711743

712-
quote, err := s.impl.LoopOutQuote(ctx, &loop.LoopOutQuoteRequest{
744+
loopOutQuoteReq := &loop.LoopOutQuoteRequest{
713745
Amount: btcutil.Amount(req.Amt),
714746
SweepConfTarget: confTarget,
715747
SwapPublicationDeadline: publicactionDeadline,
716748
Initiator: defaultLoopdInitiator,
717-
})
749+
}
750+
751+
if req.AssetInfo != nil {
752+
if req.AssetInfo.AssetId == nil ||
753+
req.AssetInfo.AssetEdgeNode == nil {
754+
755+
return nil, fmt.Errorf(
756+
"asset id and edge node must both be set")
757+
}
758+
loopOutQuoteReq.AssetRFQRequest = &loop.AssetRFQRequest{
759+
AssetId: req.AssetInfo.AssetId,
760+
AssetEdgeNode: req.AssetInfo.AssetEdgeNode,
761+
Expiry: req.AssetInfo.Expiry,
762+
MaxLimitMultiplier: req.AssetInfo.MaxLimitMultiplier,
763+
}
764+
}
765+
766+
quote, err := s.impl.LoopOutQuote(ctx, loopOutQuoteReq)
718767
if err != nil {
719768
return nil, err
720769
}
721770

722-
return &looprpc.OutQuoteResponse{
771+
response := &looprpc.OutQuoteResponse{
723772
HtlcSweepFeeSat: int64(quote.MinerFee),
724773
PrepayAmtSat: int64(quote.PrepayAmount),
725774
SwapFeeSat: int64(quote.SwapFee),
726775
SwapPaymentDest: quote.SwapPaymentDest[:],
727776
ConfTarget: confTarget,
728-
}, nil
777+
}
778+
779+
if quote.LoopOutRfq != nil {
780+
response.AssetRfqInfo = &looprpc.AssetRfqInfo{
781+
PrepayRfqId: quote.LoopOutRfq.PrepayRfqId,
782+
PrepayAssetAmt: quote.LoopOutRfq.PrepayAssetAmt,
783+
SwapRfqId: quote.LoopOutRfq.SwapRfqId,
784+
SwapAssetAmt: quote.LoopOutRfq.SwapAssetAmt,
785+
AssetName: quote.LoopOutRfq.AssetName,
786+
}
787+
}
788+
789+
return response, nil
729790
}
730791

731792
// GetLoopInTerms returns the terms that the server enforces for swaps.
@@ -2025,6 +2086,15 @@ func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient,
20252086
return 0, errInvalidAddress
20262087
}
20272088

2089+
// If this is an asset payment, we'll check that we have the necessary
2090+
// outbound asset capacaity to fulfill the request.
2091+
if req.AssetInfo != nil {
2092+
// Todo(sputn1ck) actually check outbound capacity.
2093+
return validateConfTarget(
2094+
req.SweepConfTarget, loop.DefaultSweepConfTarget,
2095+
)
2096+
}
2097+
20282098
// Check that the label is valid.
20292099
if err := labels.Validate(req.Label); err != nil {
20302100
return 0, err

loopdb/loopout.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ type LoopOutContract struct {
6565
// PaymentTimeout is the timeout for any individual off-chain payment
6666
// attempt.
6767
PaymentTimeout time.Duration
68+
69+
// AssetSwapInfo contains information, should the loop out swpa be
70+
// paid via an asset channel.
71+
AssetSwapInfo *LoopOutAssetSwap
72+
}
73+
74+
type LoopOutAssetSwap struct {
75+
// AssetId is the optional asset id that is used to pay the swap invoice.
76+
AssetId []byte
77+
78+
// PrepayRfqId is the rfq id that is used to pay the prepay invoice.
79+
PrepayRfqId []byte
80+
81+
// SwapRfqId is the rfq id that is used to pay the swap invoice.
82+
SwapRfqId []byte
83+
84+
// PrepayPaidAmt is the asset amount that was paid for the prepay
85+
// invoice.
86+
PrepayPaidAmt uint64
87+
88+
// SwapPaidAmt is the asset amount that was paid for the swap invoice.
89+
SwapPaidAmt uint64
6890
}
6991

7092
// ChannelSet stores a set of channels.

0 commit comments

Comments
 (0)