@@ -11,6 +11,7 @@ import (
1111 "io"
1212 "math"
1313 "net/http"
14+ "sort"
1415 "strings"
1516 "sync"
1617 "sync/atomic"
@@ -47,6 +48,7 @@ import (
4748 "github.com/lightninglabs/taproot-assets/taprpc"
4849 wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
4950 "github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
51+ "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
5052 "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
5153 tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
5254 "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
@@ -102,6 +104,10 @@ const (
102104 // proofTypeReceive is an alias for the proof type used for receiving
103105 // assets.
104106 proofTypeReceive = tapdevrpc .ProofTransferType_PROOF_TRANSFER_TYPE_RECEIVE
107+
108+ // maxRfqHopHints is the maximum number of RFQ quotes that may be
109+ // encoded as hop hints in a bolt11 invoice.
110+ maxRfqHopHints = 20
105111)
106112
107113type (
@@ -7968,6 +7974,15 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
79687974 return nil , fmt .Errorf ("invoice request must be specified" )
79697975 }
79707976 iReq := req .InvoiceRequest
7977+ existingQuotes := iReq .RouteHints != nil
7978+
7979+ if existingQuotes && ! tapchannel .IsAssetInvoice (
7980+ iReq , r .cfg .AuxInvoiceManager ,
7981+ ) {
7982+
7983+ return nil , fmt .Errorf ("existing route hints should only " +
7984+ "contain valid accepted quotes" )
7985+ }
79717986
79727987 assetID , groupKey , err := parseAssetSpecifier (
79737988 req .AssetId , "" , req .GroupKey , "" ,
@@ -8003,16 +8018,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
80038018 err )
80048019 }
80058020
8006- // TODO(george): this is temporary just for the commit to compile.
8007- var firstChan rfq.ChannelWithSpecifier
8008- for _ , v := range chanMap {
8009- firstChan = v [0 ]
8010- }
8011-
8012- // Even if the user didn't specify the peer public key before, we
8013- // definitely know it now. So let's make sure it's always set.
8014- peerPubKey = & firstChan .ChannelInfo .PubKeyBytes
8015-
80168021 expirySeconds := iReq .Expiry
80178022 if expirySeconds == 0 {
80188023 expirySeconds = int64 (rfq .DefaultInvoiceExpiry .Seconds ())
@@ -8036,63 +8041,93 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
80368041
80378042 rpcSpecifier := marshalAssetSpecifier (specifier )
80388043
8039- resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
8040- AssetSpecifier : & rpcSpecifier ,
8041- AssetMaxAmt : maxUnits ,
8042- Expiry : uint64 (expiryTimestamp .Unix ()),
8043- PeerPubKey : peerPubKey [:],
8044- TimeoutSeconds : uint32 (
8045- rfq .DefaultTimeout .Seconds (),
8046- ),
8047- })
8048- if err != nil {
8049- return nil , fmt .Errorf ("error adding buy order: %w" , err )
8044+ type quoteWithInfo struct {
8045+ quote * rfqrpc.PeerAcceptedBuyQuote
8046+ rate * rfqmath.BigIntFixedPoint
8047+ channel rfq.ChannelWithSpecifier
80508048 }
80518049
8052- var acceptedQuote * rfqrpc.PeerAcceptedBuyQuote
8053- switch r := resp .Response .(type ) {
8054- case * rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote :
8055- acceptedQuote = r .AcceptedQuote
8050+ var acquiredQuotes []quoteWithInfo
80568051
8057- case * rfqrpc. AddAssetBuyOrderResponse_InvalidQuote :
8058- return nil , fmt . Errorf ( "peer %v sent back an invalid quote, " +
8059- "status: %v" , r . InvalidQuote . Peer ,
8060- r . InvalidQuote . Status . String ())
8052+ for peer , channels := range chanMap {
8053+ if existingQuotes {
8054+ break
8055+ }
80618056
8062- case * rfqrpc.AddAssetBuyOrderResponse_RejectedQuote :
8063- return nil , fmt .Errorf ("peer %v rejected the quote, code: %v, " +
8064- "error message: %v" , r .RejectedQuote .Peer ,
8065- r .RejectedQuote .ErrorCode , r .RejectedQuote .ErrorMessage )
8057+ quote , err := r .acquireBuyOrder (
8058+ ctx , & rpcSpecifier , maxUnits , expiryTimestamp ,
8059+ & peer ,
8060+ )
8061+ if err != nil {
8062+ rpcsLog .Errorf ("error while trying to acquire a buy " +
8063+ "order for invoice: %v" , err )
8064+ continue
8065+ }
80668066
8067- default :
8068- return nil , fmt .Errorf ("unexpected response type: %T" , r )
8067+ rate , err := rpcutils .UnmarshalFixedPoint (
8068+ & priceoraclerpc.FixedPoint {
8069+ Coefficient : quote .AskAssetRate .Coefficient ,
8070+ Scale : quote .AskAssetRate .Scale ,
8071+ },
8072+ )
8073+ if err != nil {
8074+ return nil , err
8075+ }
8076+
8077+ acquiredQuotes = append (acquiredQuotes , quoteWithInfo {
8078+ quote : quote ,
8079+ rate : rate ,
8080+ // Since the channels are sorted, we know the value with
8081+ // the greatest remote balance is at index 0.
8082+ channel : channels [0 ],
8083+ })
8084+ }
8085+
8086+ // Let's sort the ask rate of the quotes in ascending order.
8087+ sort .Slice (acquiredQuotes , func (i , j int ) bool {
8088+ return acquiredQuotes [i ].rate .ToUint64 () <
8089+ acquiredQuotes [j ].rate .ToUint64 ()
8090+ })
8091+
8092+ // If we failed to get any quotes, we need to return an error. If the
8093+ // user has already defined quotes in the request we don't return an
8094+ // error.
8095+ if len (acquiredQuotes ) == 0 && ! existingQuotes {
8096+ return nil , fmt .Errorf ("could not create any quotes for the " +
8097+ "invoice" )
8098+ }
8099+
8100+ // We need to trim any extra quotes that cannot make it into the bolt11
8101+ // invoice due to size limitations.
8102+ if len (acquiredQuotes ) > maxRfqHopHints {
8103+ acquiredQuotes = acquiredQuotes [:maxRfqHopHints ]
8104+ }
8105+
8106+ // TODO(george): We want to cancel back quotes that didn't make it into
8107+ // the set. Need to add CancelOrder endpoints to RFQ manager.
8108+
8109+ // We grab the most expensive rate to use as reference for the total
8110+ // invoice amount. Since peers have varying prices for the assets, we
8111+ // pick the most expensive rate in order to allow for any combination of
8112+ // MPP shards through our set of chosen peers.
8113+ var expensiveQuote * rfqrpc.PeerAcceptedBuyQuote
8114+ if ! existingQuotes {
8115+ expensiveQuote = acquiredQuotes [0 ].quote
80698116 }
80708117
8118+ // replace with above
80718119 // Now that we have the accepted quote, we know the amount in (milli)
80728120 // Satoshi that we need to pay. We can now update the invoice with this
80738121 // amount.
80748122 invoiceAmtMsat , err := validateInvoiceAmount (
8075- acceptedQuote , req .AssetAmount , iReq ,
8123+ expensiveQuote , req .AssetAmount , iReq ,
80768124 )
80778125 if err != nil {
80788126 return nil , fmt .Errorf ("error validating invoice amount: %w" ,
80798127 err )
80808128 }
80818129 iReq .ValueMsat = int64 (invoiceAmtMsat )
80828130
8083- // The last step is to create a hop hint that includes the fake SCID of
8084- // the quote, alongside the channel's routing policy. We need to choose
8085- // the policy that points towards us, as the payment will be flowing in.
8086- // So we get the policy that's being set by the remote peer.
8087- channelID := firstChan .ChannelInfo .ChannelID
8088- inboundPolicy , err := r .getInboundPolicy (
8089- ctx , channelID , peerPubKey .String (),
8090- )
8091- if err != nil {
8092- return nil , fmt .Errorf ("unable to get inbound channel policy " +
8093- "for channel with ID %d: %w" , channelID , err )
8094- }
8095-
80968131 // If this is a hodl invoice, then we'll copy over the relevant fields,
80978132 // then route this through the invoicerpc instead.
80988133 if req .HodlInvoice != nil {
@@ -8102,24 +8137,21 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81028137 "hash: %w" , err )
81038138 }
81048139
8105- peerPub , err := btcec .ParsePubKey (peerPubKey [:])
8106- if err != nil {
8107- return nil , fmt .Errorf ("error parsing peer " +
8108- "pubkey: %w" , err )
8109- }
8140+ routeHints := make ([][]zpay32.HopHint , 0 )
8141+ for _ , v := range acquiredQuotes {
8142+ hopHint , err := r .cfg .RfqManager .RfqToHopHint (
8143+ ctx , r .getInboundPolicy ,
8144+ v .channel .ChannelInfo .ChannelID ,
8145+ v .channel .ChannelInfo .PubKeyBytes , v .quote ,
8146+ true ,
8147+ )
8148+ if err != nil {
8149+ return nil , err
8150+ }
81108151
8111- hopHint := []zpay32.HopHint {
8112- {
8113- NodeID : peerPub ,
8114- ChannelID : acceptedQuote .Scid ,
8115- FeeBaseMSat : uint32 (inboundPolicy .FeeBaseMsat ),
8116- FeeProportionalMillionths : uint32 (
8117- inboundPolicy .FeeRateMilliMsat ,
8118- ),
8119- CLTVExpiryDelta : uint16 (
8120- inboundPolicy .TimeLockDelta ,
8121- ),
8122- },
8152+ routeHints = append (
8153+ routeHints , []zpay32.HopHint {* hopHint },
8154+ )
81238155 }
81248156
81258157 payReq , err := r .cfg .Lnd .Invoices .AddHoldInvoice (
@@ -8135,7 +8167,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81358167 // add any hop hints other than this one.
81368168 Private : false ,
81378169 HodlInvoice : true ,
8138- RouteHints : [][]zpay32. HopHint { hopHint } ,
8170+ RouteHints : routeHints ,
81398171 },
81408172 )
81418173 if err != nil {
@@ -8144,29 +8176,39 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81448176 }
81458177
81468178 return & tchrpc.AddInvoiceResponse {
8147- AcceptedBuyQuote : acceptedQuote ,
8179+ // TODO(george): For now we just return the expensive
8180+ // quote
8181+ AcceptedBuyQuote : expensiveQuote ,
81488182 InvoiceResult : & lnrpc.AddInvoiceResponse {
81498183 PaymentRequest : payReq ,
81508184 },
81518185 }, nil
81528186 }
81538187
8154- // Otherwise, we'll make this into a normal invoice.
8155- hopHint := & lnrpc.HopHint {
8156- NodeId : peerPubKey .String (),
8157- ChanId : acceptedQuote .Scid ,
8158- FeeBaseMsat : uint32 (inboundPolicy .FeeBaseMsat ),
8159- FeeProportionalMillionths : uint32 (
8160- inboundPolicy .FeeRateMilliMsat ,
8161- ),
8162- CltvExpiryDelta : inboundPolicy .TimeLockDelta ,
8163- }
8164- iReq .RouteHints = []* lnrpc.RouteHint {
8165- {
8188+ routeHints := make ([]* lnrpc.RouteHint , 0 )
8189+ for _ , v := range acquiredQuotes {
8190+ hopHint , err := r .cfg .RfqManager .RfqToHopHint (
8191+ ctx , r .getInboundPolicy ,
8192+ v .channel .ChannelInfo .ChannelID ,
8193+ v .channel .ChannelInfo .PubKeyBytes , v .quote , false ,
8194+ )
8195+ if err != nil {
8196+ return nil , err
8197+ }
8198+
8199+ lnrpcHopHint := rfq .Zpay32HopHintToLnrpc (hopHint )
8200+
8201+ routeHints = append (routeHints , & lnrpc.RouteHint {
81668202 HopHints : []* lnrpc.HopHint {
8167- hopHint ,
8203+ lnrpcHopHint ,
81688204 },
8169- },
8205+ })
8206+ }
8207+
8208+ // Only replace the route hints of the invoice request if the user has
8209+ // not already set them.
8210+ if ! existingQuotes {
8211+ iReq .RouteHints = routeHints
81708212 }
81718213
81728214 rpcCtx , _ , rawClient := r .cfg .Lnd .Client .RawClientWithMacAuth (ctx )
@@ -8176,7 +8218,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81768218 }
81778219
81788220 return & tchrpc.AddInvoiceResponse {
8179- AcceptedBuyQuote : acceptedQuote ,
8221+ AcceptedBuyQuote : expensiveQuote ,
81808222 InvoiceResult : invoiceResp ,
81818223 }, nil
81828224}
0 commit comments