@@ -20,9 +20,11 @@ import (
2020 "github.com/lightninglabs/loop/sweep"
2121 "github.com/lightninglabs/loop/sweepbatcher"
2222 "github.com/lightninglabs/loop/utils"
23+ "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
2324 "github.com/lightningnetwork/lnd/chainntnfs"
2425 "github.com/lightningnetwork/lnd/channeldb"
2526 "github.com/lightningnetwork/lnd/lnrpc"
27+ "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
2628 "github.com/lightningnetwork/lnd/lntypes"
2729)
2830
@@ -87,8 +89,9 @@ type loopOutSwap struct {
8789 // to calculate the total cost of the swap.
8890 prepayAmount btcutil.Amount
8991
90- swapPaymentChan chan paymentResult
91- prePaymentChan chan paymentResult
92+ swapPaymentChan chan paymentResult
93+ prePaymentChan chan paymentResult
94+ waitForRfqCompletedChan chan interface {}
9295
9396 wg sync.WaitGroup
9497}
@@ -140,6 +143,15 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
140143 log .Infof ("Initiating swap request at height %v: amt=%v, expiry=%v" ,
141144 currentHeight , request .Amount , request .Expiry )
142145
146+ // If we have an asset id, we'll add that to the user agent.
147+ if request .AssetId != nil {
148+ if request .AssetEdgeNode == nil {
149+ return nil , errors .New ("asset edge node must be set" )
150+ }
151+
152+ request .Initiator += " asset_out"
153+ }
154+
143155 // The swap deadline will be given to the server for it to use as the
144156 // latest swap publication time.
145157 swapResp , err := cfg .server .NewLoopOutSwap (
@@ -634,13 +646,28 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) {
634646 s .log .Infof ("Server recommended routing plugin: %v" , pluginType )
635647 }
636648
649+ if s .AssetId != nil {
650+ s .waitForRfqCompletedChan = make (chan interface {})
651+ }
652+
637653 // Use the recommended routing plugin.
638654 s .swapPaymentChan = s .payInvoice (
639655 ctx , s .SwapInvoice , s .MaxSwapRoutingFee ,
640656 s .LoopOutContract .OutgoingChanSet ,
641657 s .LoopOutContract .PaymentTimeout , pluginType , true ,
642658 )
643659
660+ // We'll need to wait for the RFQ to be completed before we can start
661+ // paying the prepay invoice. This is due to a timing bug in tapd that
662+ // on simultaneous RFQs return the same quote for both RFQs.
663+ if s .AssetId != nil {
664+ select {
665+ case <- ctx .Done ():
666+ return
667+ case <- s .waitForRfqCompletedChan :
668+ }
669+ }
670+
644671 // Pay the prepay invoice. Won't use the routing plugin here as the
645672 // prepay is trivially small and shouldn't normally need any help. We
646673 // are sending it over the same channel as the loop out payment.
@@ -696,6 +723,11 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
696723 )
697724 if err != nil {
698725 result .err = err
726+ // On error, we close the asset channel to signal that
727+ // we are done and to not block the payment loop.
728+ if s .waitForRfqCompletedChan != nil {
729+ close (s .waitForRfqCompletedChan )
730+ }
699731 sendResult (result )
700732 return
701733 }
@@ -717,13 +749,80 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
717749 return resultChan
718750}
719751
752+ func (s * loopOutSwap ) payInvoiceWithAssetClient (ctx context.Context ,
753+ invoice string ) (* lndclient.PaymentStatus , error ) {
754+
755+ totalPaymentTimeout := s .executeConfig .totalPaymentTimeout
756+
757+ sendReq := & routerrpc.SendPaymentRequest {
758+ PaymentRequest : invoice ,
759+ TimeoutSeconds : int32 (totalPaymentTimeout .Seconds ()),
760+ FeeLimitMsat : 1_000_000 ,
761+ }
762+
763+ paymentStream , err := s .assets .SendPayment (
764+ ctx , & tapchannelrpc.SendPaymentRequest {
765+ AssetId : s .AssetId ,
766+ PeerPubkey : s .AssetEdgeNode ,
767+ PaymentRequest : sendReq ,
768+ })
769+ if err != nil {
770+ return nil , err
771+ }
772+
773+ // We want to receive the accepted quote message first, so we know how
774+ // many assets we're going to pay.
775+ for {
776+ select {
777+ case <- ctx .Done ():
778+ return nil , ctx .Err ()
779+ default :
780+ msg , err := paymentStream .Recv ()
781+ if err != nil {
782+ return nil , err
783+ }
784+
785+ switch msg .GetResult ().(type ) {
786+ case * tapchannelrpc.SendPaymentResponse_AcceptedSellOrder :
787+ quote := msg .GetAcceptedSellOrder ()
788+ log .Infof ("Accepted quote: %v" ,
789+ quote .AssetAmount )
790+
791+ if s .waitForRfqCompletedChan != nil {
792+ close (s .waitForRfqCompletedChan )
793+ s .waitForRfqCompletedChan = nil
794+ }
795+
796+ case * tapchannelrpc.SendPaymentResponse_PaymentResult :
797+ payRes := msg .GetPaymentResult ()
798+ if payRes .Status == lnrpc .Payment_SUCCEEDED ||
799+ payRes .Status == lnrpc .Payment_FAILED {
800+
801+ s .log .Infof ("Asset Payment result: %v" ,
802+ payRes .Status )
803+
804+ return & lndclient.PaymentStatus {
805+ State : payRes .Status ,
806+ }, nil
807+ }
808+ }
809+ }
810+ }
811+ }
812+
720813// payInvoiceAsync is the asynchronously executed part of paying an invoice.
721814func (s * loopOutSwap ) payInvoiceAsync (ctx context.Context ,
722815 invoice string , maxFee btcutil.Amount ,
723816 outgoingChanIds loopdb.ChannelSet , paymentTimeout time.Duration ,
724817 pluginType RoutingPluginType , reportPluginResult bool ) (
725818 * lndclient.PaymentStatus , error ) {
726819
820+ // If we want to use an asset for the payment, we need to use the asset
821+ // client.
822+ if s .AssetId != nil {
823+ return s .payInvoiceWithAssetClient (ctx , invoice )
824+ }
825+
727826 // Extract hash from payment request. Unfortunately the request
728827 // components aren't available directly.
729828 chainParams := s .lnd .ChainParams
0 commit comments