@@ -11,6 +11,7 @@ import (
11
11
"io"
12
12
"math"
13
13
"net/http"
14
+ "sort"
14
15
"strings"
15
16
"sync"
16
17
"sync/atomic"
@@ -47,6 +48,7 @@ import (
47
48
"github.com/lightninglabs/taproot-assets/taprpc"
48
49
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
49
50
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
51
+ "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
50
52
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
51
53
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
52
54
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
@@ -102,6 +104,10 @@ const (
102
104
// proofTypeReceive is an alias for the proof type used for receiving
103
105
// assets.
104
106
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
105
111
)
106
112
107
113
type (
@@ -7968,6 +7974,15 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
7968
7974
return nil , fmt .Errorf ("invoice request must be specified" )
7969
7975
}
7970
7976
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
+ }
7971
7986
7972
7987
assetID , groupKey , err := parseAssetSpecifier (
7973
7988
req .AssetId , "" , req .GroupKey , "" ,
@@ -8003,16 +8018,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8003
8018
err )
8004
8019
}
8005
8020
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
-
8016
8021
expirySeconds := iReq .Expiry
8017
8022
if expirySeconds == 0 {
8018
8023
expirySeconds = int64 (rfq .DefaultInvoiceExpiry .Seconds ())
@@ -8036,63 +8041,93 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8036
8041
8037
8042
rpcSpecifier := marshalAssetSpecifier (specifier )
8038
8043
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
8050
8048
}
8051
8049
8052
- var acceptedQuote * rfqrpc.PeerAcceptedBuyQuote
8053
- switch r := resp .Response .(type ) {
8054
- case * rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote :
8055
- acceptedQuote = r .AcceptedQuote
8050
+ var acquiredQuotes []quoteWithInfo
8056
8051
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
+ }
8061
8056
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
+ }
8066
8066
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
8069
8116
}
8070
8117
8118
+ // replace with above
8071
8119
// Now that we have the accepted quote, we know the amount in (milli)
8072
8120
// Satoshi that we need to pay. We can now update the invoice with this
8073
8121
// amount.
8074
8122
invoiceAmtMsat , err := validateInvoiceAmount (
8075
- acceptedQuote , req .AssetAmount , iReq ,
8123
+ expensiveQuote , req .AssetAmount , iReq ,
8076
8124
)
8077
8125
if err != nil {
8078
8126
return nil , fmt .Errorf ("error validating invoice amount: %w" ,
8079
8127
err )
8080
8128
}
8081
8129
iReq .ValueMsat = int64 (invoiceAmtMsat )
8082
8130
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
-
8096
8131
// If this is a hodl invoice, then we'll copy over the relevant fields,
8097
8132
// then route this through the invoicerpc instead.
8098
8133
if req .HodlInvoice != nil {
@@ -8102,24 +8137,21 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8102
8137
"hash: %w" , err )
8103
8138
}
8104
8139
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
+ }
8110
8151
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
+ )
8123
8155
}
8124
8156
8125
8157
payReq , err := r .cfg .Lnd .Invoices .AddHoldInvoice (
@@ -8135,7 +8167,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8135
8167
// add any hop hints other than this one.
8136
8168
Private : false ,
8137
8169
HodlInvoice : true ,
8138
- RouteHints : [][]zpay32. HopHint { hopHint } ,
8170
+ RouteHints : routeHints ,
8139
8171
},
8140
8172
)
8141
8173
if err != nil {
@@ -8144,29 +8176,39 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8144
8176
}
8145
8177
8146
8178
return & tchrpc.AddInvoiceResponse {
8147
- AcceptedBuyQuote : acceptedQuote ,
8179
+ // TODO(george): For now we just return the expensive
8180
+ // quote
8181
+ AcceptedBuyQuote : expensiveQuote ,
8148
8182
InvoiceResult : & lnrpc.AddInvoiceResponse {
8149
8183
PaymentRequest : payReq ,
8150
8184
},
8151
8185
}, nil
8152
8186
}
8153
8187
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 {
8166
8202
HopHints : []* lnrpc.HopHint {
8167
- hopHint ,
8203
+ lnrpcHopHint ,
8168
8204
},
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
8170
8212
}
8171
8213
8172
8214
rpcCtx , _ , rawClient := r .cfg .Lnd .Client .RawClientWithMacAuth (ctx )
@@ -8176,7 +8218,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
8176
8218
}
8177
8219
8178
8220
return & tchrpc.AddInvoiceResponse {
8179
- AcceptedBuyQuote : acceptedQuote ,
8221
+ AcceptedBuyQuote : expensiveQuote ,
8180
8222
InvoiceResult : invoiceResp ,
8181
8223
}, nil
8182
8224
}
0 commit comments