Skip to content

Commit aae0e9d

Browse files
committed
loopout: add asset loop out payment flow
1 parent 3962111 commit aae0e9d

File tree

5 files changed

+275
-13
lines changed

5 files changed

+275
-13
lines changed

client.go

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,31 @@ func (s *Client) resumeSwaps(ctx context.Context,
506506
func (s *Client) LoopOut(globalCtx context.Context,
507507
request *OutRequest) (*LoopOutSwapInfo, error) {
508508

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

513535
if err := s.waitForInitialized(globalCtx); err != nil {
514536
return nil, err
@@ -529,7 +551,10 @@ func (s *Client) LoopOut(globalCtx context.Context,
529551
}
530552

531553
// Create a new swap object for this swap.
532-
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server, s.assetClient)
554+
swapCfg := newSwapConfig(
555+
s.lndServices, s.Store, s.Server, s.assetClient,
556+
)
557+
533558
initResult, err := newLoopOutSwap(
534559
globalCtx, swapCfg, initiationHeight, request,
535560
)
@@ -574,6 +599,16 @@ func (s *Client) getExpiry(height int32, terms *LoopOutTerms,
574599
func (s *Client) LoopOutQuote(ctx context.Context,
575600
request *LoopOutQuoteRequest) (*LoopOutQuote, error) {
576601

602+
if request.AssetId != nil && request.AssetEdgeNode == nil {
603+
return nil, errors.New("asset edge node must be set " +
604+
"when using an asset id")
605+
}
606+
607+
if request.AssetEdgeNode != nil && request.AssetId == nil {
608+
return nil, errors.New("asset id must be set " +
609+
"when using an asset edge node")
610+
}
611+
577612
terms, err := s.Server.GetLoopOutTerms(ctx, request.Initiator)
578613
if err != nil {
579614
return nil, err
@@ -608,12 +643,55 @@ func (s *Client) LoopOutQuote(ctx context.Context,
608643
return nil, err
609644
}
610645

611-
return &LoopOutQuote{
646+
loopOutQuote := &LoopOutQuote{
612647
SwapFee: quote.SwapFee,
613648
MinerFee: minerFee,
614649
PrepayAmount: quote.PrepayAmount,
615650
SwapPaymentDest: quote.SwapPaymentDest,
616-
}, nil
651+
}
652+
653+
// If we use an Asset we'll rfq to get the asset amounts to use for
654+
// the swap.
655+
if request.AssetId != nil {
656+
// First we'll get the prepay prepayRfq.
657+
prepayRfq, err := s.assetClient.GetRfqForAsset(
658+
ctx, quote.PrepayAmount, request.AssetId,
659+
request.AssetEdgeNode,
660+
)
661+
if err != nil {
662+
return nil, err
663+
}
664+
log.Infof("Prepay RFQ: %v", prepayRfq)
665+
666+
invoiceAmt := request.Amount + quote.SwapFee -
667+
quote.PrepayAmount
668+
669+
swapRfq, err := s.assetClient.GetRfqForAsset(
670+
ctx, invoiceAmt, request.AssetId, request.AssetEdgeNode,
671+
)
672+
if err != nil {
673+
return nil, err
674+
}
675+
log.Infof("Swap RFQ: %v", swapRfq)
676+
677+
// We'll also want the asset name to verify for the client.
678+
assetName, err := s.assetClient.GetAssetName(
679+
ctx, request.AssetId,
680+
)
681+
if err != nil {
682+
return nil, err
683+
}
684+
685+
loopOutQuote.LoopOutRfq = &LoopOutRfq{
686+
PrepayRfqId: prepayRfq.Id,
687+
PrepayAssetAmt: btcutil.Amount(prepayRfq.AssetAmount),
688+
SwapRfqId: swapRfq.Id,
689+
SwapAssetAmt: btcutil.Amount(swapRfq.AssetAmount),
690+
AssetName: assetName,
691+
}
692+
}
693+
694+
return loopOutQuote, nil
617695
}
618696

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

interface.go

Lines changed: 46 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,12 @@ 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+
// AssetId is the asset that we'll quote for.
162+
AssetId []byte
163+
164+
// AssetEdgeNode is the pubkey of the peer that we'll quote for.
165+
AssetEdgeNode []byte
148166
}
149167

150168
// LoopOutTerms are the server terms on which it executes swaps.
@@ -181,6 +199,31 @@ type LoopOutQuote struct {
181199
// SwapPaymentDest is the node pubkey where to swap payment needs to be
182200
// sent to.
183201
SwapPaymentDest [33]byte
202+
203+
// LoopOutRfq is the RFQ that can be used in the actual loop out to
204+
// commit to an asset exchange rate.
205+
LoopOutRfq *LoopOutRfq
206+
}
207+
208+
// LoopOutRfq contains the details of an asset request for quote for a loop out
209+
// swap.
210+
type LoopOutRfq struct {
211+
// PrepayRfqId is the ID of the prepay RFQ.
212+
PrepayRfqId []byte
213+
214+
// PrepayAssetAmt is the amount of the asset that will be used to pay
215+
// for the prepay invoice.
216+
PrepayAssetAmt btcutil.Amount
217+
218+
// SwapRfqId is the ID of the swap RFQ.
219+
SwapRfqId []byte
220+
221+
// SwapAssetAmt is the amount of the asset that will be used to pay for
222+
// the swap invoice.
223+
SwapAssetAmt btcutil.Amount
224+
225+
// AssetName is the human readable name of the asset.
226+
AssetName string
184227
}
185228

186229
// LoopInRequest contains the required parameters for the swap.
@@ -430,6 +473,9 @@ type SwapInfo struct {
430473
// channels that may be used to loop out. On a loop in this field
431474
// is nil.
432475
OutgoingChanSet loopdb.ChannelSet
476+
477+
// AssetSwapInfo contains the asset information for the swap.
478+
AssetSwapInfo *loopdb.LoopOutAssetSwap
433479
}
434480

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

loopd/swapclient_server.go

Lines changed: 71 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 in.AssetInfo.AssetId == nil &&
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 in.AssetRfqInfo.PrepayRfqId == nil &&
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 in.AssetRfqInfo.SwapRfqId == nil &&
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,49 @@ 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+
759+
loopOutQuoteReq.AssetId = req.AssetInfo.AssetId
760+
loopOutQuoteReq.AssetEdgeNode = req.AssetInfo.AssetEdgeNode
761+
}
762+
763+
quote, err := s.impl.LoopOutQuote(ctx, loopOutQuoteReq)
718764
if err != nil {
719765
return nil, err
720766
}
721767

722-
return &looprpc.OutQuoteResponse{
768+
response := &looprpc.OutQuoteResponse{
723769
HtlcSweepFeeSat: int64(quote.MinerFee),
724770
PrepayAmtSat: int64(quote.PrepayAmount),
725771
SwapFeeSat: int64(quote.SwapFee),
726772
SwapPaymentDest: quote.SwapPaymentDest[:],
727773
ConfTarget: confTarget,
728-
}, nil
774+
}
775+
776+
if quote.LoopOutRfq != nil {
777+
response.AssetRfqInfo = &looprpc.AssetRfqInfo{
778+
PrepayRfqId: quote.LoopOutRfq.PrepayRfqId,
779+
PrepayAssetAmt: uint64(quote.LoopOutRfq.PrepayAssetAmt),
780+
SwapRfqId: quote.LoopOutRfq.SwapRfqId,
781+
SwapAssetAmt: uint64(quote.LoopOutRfq.SwapAssetAmt),
782+
AssetName: quote.LoopOutRfq.AssetName,
783+
}
784+
}
785+
786+
return response, nil
729787
}
730788

731789
// GetLoopInTerms returns the terms that the server enforces for swaps.
@@ -2025,6 +2083,15 @@ func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient,
20252083
return 0, errInvalidAddress
20262084
}
20272085

2086+
// If this is an asset payment, we'll check that we have the necessary
2087+
// outbound asset capacaity to fulfill the request.
2088+
if req.AssetInfo != nil {
2089+
// Todo(sputn1ck) actually check outbound capacity.
2090+
return validateConfTarget(
2091+
req.SweepConfTarget, loop.DefaultSweepConfTarget,
2092+
)
2093+
}
2094+
20282095
// Check that the label is valid.
20292096
if err := labels.Validate(req.Label); err != nil {
20302097
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 btcutil.Amount
87+
88+
// SwapPaidAmt is the asset amount that was paid for the swap invoice.
89+
SwapPaidAmt btcutil.Amount
6890
}
6991

7092
// ChannelSet stores a set of channels.

0 commit comments

Comments
 (0)