@@ -7819,11 +7819,24 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
78197819 time .Duration (expirySeconds ) * time .Second ,
78207820 )
78217821
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+
78227835 rpcSpecifier := marshalAssetSpecifier (specifier )
78237836
78247837 resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
78257838 AssetSpecifier : & rpcSpecifier ,
7826- AssetMaxAmt : req . AssetAmount ,
7839+ AssetMaxAmt : maxUnits ,
78277840 Expiry : uint64 (expiryTimestamp .Unix ()),
78287841 PeerPubKey : peerPubKey [:],
78297842 TimeoutSeconds : uint32 (
@@ -7853,35 +7866,17 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
78537866 return nil , fmt .Errorf ("unexpected response type: %T" , r )
78547867 }
78557868
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 ,
78737874 )
78747875 if err != nil {
7875- return nil , fmt .Errorf ("error unmarshalling ask asset rate : %w" ,
7876+ return nil , fmt .Errorf ("error validating invoice amount : %w" ,
78767877 err )
78777878 }
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 )
78857880
78867881 // The last step is to create a hop hint that includes the fake SCID of
78877882 // the quote, alongside the channel's routing policy. We need to choose
@@ -7984,6 +7979,147 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
79847979 }, nil
79857980}
79867981
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+
79878123// DeclareScriptKey declares a new script key to the wallet. This is useful
79888124// when the script key contains scripts, which would mean it wouldn't be
79898125// recognized by the wallet automatically. Declaring a script key will make any
0 commit comments