@@ -13,7 +13,9 @@ import (
1313 "github.com/lightninglabs/taproot-assets/rfqmath"
1414 "github.com/lightninglabs/taproot-assets/rfqmsg"
1515 "github.com/lightninglabs/taproot-assets/taprpc"
16+ "github.com/lightningnetwork/lnd/lnrpc"
1617 "github.com/lightningnetwork/lnd/lnwire"
18+ "github.com/lightningnetwork/lnd/routing/route"
1719)
1820
1921// InvoiceHtlcModifier is an interface that abstracts the invoice HTLC
@@ -28,6 +30,32 @@ type InvoiceHtlcModifier interface {
2830 handler lndclient.InvoiceHtlcModifyHandler ) error
2931}
3032
33+ // RfqManager is an interface that abstracts the functionalities of the rfq
34+ // manager that are needed by AuxInvoiceManager.
35+ type RfqManager interface {
36+ // PeerAcceptedBuyQuotes returns buy quotes that were requested by our
37+ // node and have been accepted by our peers. These quotes are
38+ // exclusively available to our node for the acquisition of assets.
39+ PeerAcceptedBuyQuotes () rfq.BuyAcceptMap
40+
41+ // LocalAcceptedSellQuotes returns sell quotes that were accepted by our
42+ // node and have been requested by our peers. These quotes are
43+ // exclusively available to our node for the sale of assets.
44+ LocalAcceptedSellQuotes () rfq.SellAcceptMap
45+ }
46+
47+ // A compile time assertion to ensure that the rfq.Manager meets the expected
48+ // tapchannel.RfqManager interface.
49+ var _ RfqManager = (* rfq .Manager )(nil )
50+
51+ // RfqLookup is an interface that abstracts away the process of performing
52+ // a lookup to the current set of existing RFQs.
53+ type RfqLookup interface {
54+ // RfqPeerFromScid retrieves the peer associated with the RFQ id that
55+ // is mapped to the provided scid, if it exists.
56+ RfqPeerFromScid (scid uint64 ) (route.Vertex , error )
57+ }
58+
3159// InvoiceManagerConfig defines the configuration for the auxiliary invoice
3260// manager.
3361type InvoiceManagerConfig struct {
@@ -42,7 +70,7 @@ type InvoiceManagerConfig struct {
4270 // RfqManager is the RFQ manager that will be used to retrieve the
4371 // accepted quotes for determining the incoming value of invoice related
4472 // HTLCs.
45- RfqManager * rfq. Manager
73+ RfqManager RfqManager
4674}
4775
4876// AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be
@@ -110,6 +138,10 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
110138 AmtPaid : req .ExitHtlcAmt ,
111139 }
112140
141+ if req .Invoice == nil {
142+ return nil , fmt .Errorf ("cannot handle empty invoice" )
143+ }
144+
113145 jsonBytes , err := taprpc .ProtoJSONMarshalOpts .Marshal (req .Invoice )
114146 if err != nil {
115147 return nil , fmt .Errorf ("unable to decode response: %w" , err )
@@ -121,6 +153,18 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
121153
122154 // No custom record on the HTLC, so we have nothing to do.
123155 if len (req .WireCustomRecords ) == 0 {
156+ // If there's no wire custom records and the invoice is an asset
157+ // invoice do not settle the invoice. Since we are asking for
158+ // assets in the invoice, we may not let this HTLC go through
159+ // as it is not carrying assets. This could lead to undesired
160+ // behavior where the asset invoice may be settled by accepting
161+ // sats instead of assets.
162+ //
163+ // TODO(george): Strict-forwarding could be configurable?
164+ if isAssetInvoice (req .Invoice , s ) {
165+ resp .CancelSet = true
166+ }
167+
124168 return resp , nil
125169 }
126170
@@ -216,12 +260,15 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) (
216260 sellQuote , isSell := acceptedSellQuotes [rfqID .Scid ()]
217261
218262 switch {
263+ // This is a normal invoice payment with multiple hops, so we expect to
264+ // find a buy quote.
219265 case isBuy :
220- log .Debugf ("Found buy quote for ID %x / SCID %d: %#v" ,
221- rfqID [:], rfqID .Scid (), buyQuote )
266+ log .Debugf ("Found buy quote for ID %x / SCID %d: %#v" , rfqID [:],
267+ rfqID .Scid (), buyQuote )
222268
223269 return & buyQuote .AssetRate , nil
224270
271+ // This is a direct peer payment, so we expect to find a sell quote.
225272 case isSell :
226273 log .Debugf ("Found sell quote for ID %x / SCID %d: %#v" ,
227274 rfqID [:], rfqID .Scid (), sellQuote )
@@ -234,6 +281,54 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) (
234281 }
235282}
236283
284+ // RfqPeerFromScid attempts to match the provided scid with a negotiated quote,
285+ // then it returns the RFQ peer's node id.
286+ func (s * AuxInvoiceManager ) RfqPeerFromScid (scid uint64 ) (route.Vertex , error ) {
287+ acceptedBuyQuotes := s .cfg .RfqManager .PeerAcceptedBuyQuotes ()
288+
289+ buyQuote , isBuy := acceptedBuyQuotes [rfqmsg .SerialisedScid (scid )]
290+
291+ if ! isBuy {
292+ return route.Vertex {}, fmt .Errorf ("no peer found for RFQ " +
293+ "SCID %d" , scid )
294+ }
295+
296+ return buyQuote .Peer , nil
297+ }
298+
299+ // isAssetInvoice checks whether the provided invoice is an asset invoice. This
300+ // method checks whether the routing hints of the invoice match those created
301+ // when generating an asset invoice, and if that's the case we then check that
302+ // the scid matches an existing quote.
303+ func isAssetInvoice (invoice * lnrpc.Invoice , rfqLookup RfqLookup ) bool {
304+ hints := invoice .RouteHints
305+
306+ for _ , hint := range hints {
307+ for _ , h := range hint .HopHints {
308+ scid := h .ChanId
309+ nodeId := h .NodeId
310+
311+ // Check if for this hop hint we can retrieve a valid
312+ // rfq quote.
313+ peer , err := rfqLookup .RfqPeerFromScid (scid )
314+ if err != nil {
315+ log .Debugf ("invoice hop hint scid %v does not " +
316+ "correspond to a valid RFQ quote" , scid )
317+
318+ continue
319+ }
320+
321+ // If we also have a nodeId match, we're safe to assume
322+ // this is an asset invoice.
323+ if peer .String () == nodeId {
324+ return true
325+ }
326+ }
327+ }
328+
329+ return false
330+ }
331+
237332// Stop signals for an aux invoice manager to gracefully exit.
238333func (s * AuxInvoiceManager ) Stop () error {
239334 var stopErr error
0 commit comments