@@ -7819,11 +7819,24 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
7819
7819
time .Duration (expirySeconds ) * time .Second ,
7820
7820
)
7821
7821
7822
+ // We now want to calculate the upper bound of the RFQ order, which
7823
+ // either is the asset amount specified by the user, or the converted
7824
+ // satoshi amount of the invoice, expressed in asset units, using the
7825
+ // local price oracle's conversion rate.
7826
+ maxUnits , err := calculateAssetMaxAmount (
7827
+ ctx , r .cfg .PriceOracle , specifier , req .AssetAmount , iReq ,
7828
+ r .cfg .RfqManager .GetPriceDeviationPpm (),
7829
+ )
7830
+ if err != nil {
7831
+ return nil , fmt .Errorf ("error calculating asset max " +
7832
+ "amount: %w" , err )
7833
+ }
7834
+
7822
7835
rpcSpecifier := marshalAssetSpecifier (specifier )
7823
7836
7824
7837
resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
7825
7838
AssetSpecifier : & rpcSpecifier ,
7826
- AssetMaxAmt : req . AssetAmount ,
7839
+ AssetMaxAmt : maxUnits ,
7827
7840
Expiry : uint64 (expiryTimestamp .Unix ()),
7828
7841
PeerPubKey : peerPubKey [:],
7829
7842
TimeoutSeconds : uint32 (
@@ -7853,35 +7866,17 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
7853
7866
return nil , fmt .Errorf ("unexpected response type: %T" , r )
7854
7867
}
7855
7868
7856
- // If the invoice is for an asset unit amount smaller than the minimal
7857
- // transportable amount, we'll return an error, as it wouldn't be
7858
- // payable by the network.
7859
- if acceptedQuote .MinTransportableUnits > req .AssetAmount {
7860
- return nil , fmt .Errorf ("cannot create invoice over %d asset " +
7861
- "units, as the minimal transportable amount is %d " +
7862
- "units with the current rate of %v units/BTC" ,
7863
- req .AssetAmount , acceptedQuote .MinTransportableUnits ,
7864
- acceptedQuote .AskAssetRate )
7865
- }
7866
-
7867
- // Now that we have the accepted quote, we know the amount in Satoshi
7868
- // that we need to pay. We can now update the invoice with this amount.
7869
- //
7870
- // First, un-marshall the ask asset rate from the accepted quote.
7871
- askAssetRate , err := rfqrpc .UnmarshalFixedPoint (
7872
- acceptedQuote .AskAssetRate ,
7869
+ // Now that we have the accepted quote, we know the amount in (milli)
7870
+ // Satoshi that we need to pay. We can now update the invoice with this
7871
+ // amount.
7872
+ invoiceAmtMsat , err := validateInvoiceAmount (
7873
+ acceptedQuote , req .AssetAmount , iReq ,
7873
7874
)
7874
7875
if err != nil {
7875
- return nil , fmt .Errorf ("error unmarshalling ask asset rate : %w" ,
7876
+ return nil , fmt .Errorf ("error validating invoice amount : %w" ,
7876
7877
err )
7877
7878
}
7878
-
7879
- // Convert the asset amount into a fixed-point.
7880
- assetAmount := rfqmath .NewBigIntFixedPoint (req .AssetAmount , 0 )
7881
-
7882
- // Calculate the invoice amount in msat.
7883
- valMsat := rfqmath .UnitsToMilliSatoshi (assetAmount , * askAssetRate )
7884
- iReq .ValueMsat = int64 (valMsat )
7879
+ iReq .ValueMsat = int64 (invoiceAmtMsat )
7885
7880
7886
7881
// The last step is to create a hop hint that includes the fake SCID of
7887
7882
// the quote, alongside the channel's routing policy. We need to choose
@@ -7984,6 +7979,147 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
7984
7979
}, nil
7985
7980
}
7986
7981
7982
+ // calculateAssetMaxAmount calculates the max units to be placed in the invoice
7983
+ // RFQ quote order. When adding invoices based on asset units, that value is
7984
+ // directly returned. If using the value/value_msat fields of the invoice then
7985
+ // a price oracle query will take place to calculate the max units of the quote.
7986
+ func calculateAssetMaxAmount (ctx context.Context , priceOracle rfq.PriceOracle ,
7987
+ specifier asset.Specifier , requestAssetAmount uint64 ,
7988
+ inv * lnrpc.Invoice , deviationPPM uint64 ) (uint64 , error ) {
7989
+
7990
+ // Let's unmarshall the satoshi related fields to see if an amount was
7991
+ // set based on those.
7992
+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
7993
+ if err != nil {
7994
+ return 0 , err
7995
+ }
7996
+
7997
+ // Let's make sure that only one type of amount is set, in order to
7998
+ // avoid ambiguous behavior. This field dictates the actual value of the
7999
+ // invoice so let's be strict and only allow one possible value to be
8000
+ // set.
8001
+ if requestAssetAmount > 0 && amtMsat != 0 {
8002
+ return 0 , fmt .Errorf ("cannot set both asset amount and sats " +
8003
+ "amount" )
8004
+ }
8005
+
8006
+ // If the invoice is being added based on asset units, there's nothing
8007
+ // to do so return the amount directly.
8008
+ if amtMsat == 0 {
8009
+ return requestAssetAmount , nil
8010
+ }
8011
+
8012
+ // If the invoice defines the desired amount in satoshis, we need to
8013
+ // query our oracle first to get an estimation on the asset rate. This
8014
+ // will help us establish a quote with the correct amount of asset
8015
+ // units.
8016
+ maxUnits , err := rfq .EstimateAssetUnits (
8017
+ ctx , priceOracle , specifier , amtMsat ,
8018
+ )
8019
+ if err != nil {
8020
+ return 0 , err
8021
+ }
8022
+
8023
+ maxMathUnits := rfqmath .NewBigIntFromUint64 (maxUnits )
8024
+
8025
+ // Since we used a different oracle price query above calculate the max
8026
+ // amount of units, we want to add some breathing room to account for
8027
+ // price fluctuations caused by the small-time delay, plus the fact that
8028
+ // the agreed upon quote may be different. If we don't add this safety
8029
+ // window the peer may allow a routable amount that evaluates to less
8030
+ // than what we ask for.
8031
+ // Apply the tolerance margin twice. Once due to the ask/bid price
8032
+ // deviation that may occur during rfq negotiation, and once for the
8033
+ // price movement that may occur between querying the oracle and
8034
+ // acquiring the quote. We don't really care about this margin being too
8035
+ // big, this only affects the max units our peer agrees to route.
8036
+ tolerance := rfqmath .NewBigIntFromUint64 (deviationPPM )
8037
+
8038
+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
8039
+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
8040
+
8041
+ return maxMathUnits .ToUint64 (), nil
8042
+ }
8043
+
8044
+ // validateInvoiceAmount validates the quote against the invoice we're trying to
8045
+ // add. It returns the value in msat that should be included in the invoice.
8046
+ func validateInvoiceAmount (acceptedQuote * rfqrpc.PeerAcceptedBuyQuote ,
8047
+ requestAssetAmount uint64 , inv * lnrpc.Invoice ) (lnwire.MilliSatoshi ,
8048
+ error ) {
8049
+
8050
+ invoiceAmtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
8051
+ if err != nil {
8052
+ return 0 , err
8053
+ }
8054
+
8055
+ // Now that we have the accepted quote, we know the amount in Satoshi
8056
+ // that we need to pay. We can now update the invoice with this amount.
8057
+ //
8058
+ // First, un-marshall the ask asset rate from the accepted quote.
8059
+ askAssetRate , err := rfqrpc .UnmarshalFixedPoint (
8060
+ acceptedQuote .AskAssetRate ,
8061
+ )
8062
+ if err != nil {
8063
+ return 0 , fmt .Errorf ("error unmarshalling ask asset rate: %w" ,
8064
+ err )
8065
+ }
8066
+
8067
+ // We either have a requested amount in milli satoshi that we want to
8068
+ // validate against the quote's max amount (in which case we overwrite
8069
+ // the invoiceUnits), or we have a requested amount in asset units that
8070
+ // we want to convert into milli satoshis (and overwrite
8071
+ // newInvoiceAmtMsat).
8072
+ var (
8073
+ newInvoiceAmtMsat = invoiceAmtMsat
8074
+ invoiceUnits = requestAssetAmount
8075
+ )
8076
+ switch {
8077
+ case invoiceAmtMsat != 0 :
8078
+ // If the invoice was created with a satoshi amount, we need to
8079
+ // calculate the units.
8080
+ invoiceUnits = rfqmath .MilliSatoshiToUnits (
8081
+ invoiceAmtMsat , * askAssetRate ,
8082
+ ).ScaleTo (0 ).ToUint64 ()
8083
+
8084
+ // Now let's see if the negotiated quote can actually route the
8085
+ // amount we need in msat.
8086
+ maxFixedUnits := rfqmath .NewBigIntFixedPoint (
8087
+ acceptedQuote .AssetMaxAmount , 0 ,
8088
+ )
8089
+ maxRoutableMsat := rfqmath .UnitsToMilliSatoshi (
8090
+ maxFixedUnits , * askAssetRate ,
8091
+ )
8092
+
8093
+ if maxRoutableMsat <= invoiceAmtMsat {
8094
+ return 0 , fmt .Errorf ("cannot create invoice for %v " +
8095
+ "msat, max routable amount is %v msat" ,
8096
+ invoiceAmtMsat , maxRoutableMsat )
8097
+ }
8098
+
8099
+ default :
8100
+ // Convert the asset amount into a fixed-point.
8101
+ assetAmount := rfqmath .NewBigIntFixedPoint (invoiceUnits , 0 )
8102
+
8103
+ // Calculate the invoice amount in msat.
8104
+ newInvoiceAmtMsat = rfqmath .UnitsToMilliSatoshi (
8105
+ assetAmount , * askAssetRate ,
8106
+ )
8107
+ }
8108
+
8109
+ // If the invoice is for an asset unit amount smaller than the minimal
8110
+ // transportable amount, we'll return an error, as it wouldn't be
8111
+ // payable by the network.
8112
+ if acceptedQuote .MinTransportableUnits > invoiceUnits {
8113
+ return 0 , fmt .Errorf ("cannot create invoice for %d asset " +
8114
+ "units, as the minimal transportable amount is %d " +
8115
+ "units with the current rate of %v units/BTC" ,
8116
+ invoiceUnits , acceptedQuote .MinTransportableUnits ,
8117
+ acceptedQuote .AskAssetRate )
8118
+ }
8119
+
8120
+ return newInvoiceAmtMsat , nil
8121
+ }
8122
+
7987
8123
// DeclareScriptKey declares a new script key to the wallet. This is useful
7988
8124
// when the script key contains scripts, which would mean it wouldn't be
7989
8125
// recognized by the wallet automatically. Declaring a script key will make any
0 commit comments