@@ -7621,6 +7621,147 @@ func checkOverpayment(quote *rfqrpc.PeerAcceptedSellQuote,
76217621 return nil
76227622}
76237623
7624+ // calculateAssetMaxAmount calculates the max units to be placed in the invoice
7625+ // RFQ quote order. When adding invoices based on asset units, that value is
7626+ // directly returned. If using the value/value_msat fields of the invoice then
7627+ // a price oracle query will take place to calculate the max units of the quote.
7628+ func calculateAssetMaxAmount (ctx context.Context , priceOracle rfq.PriceOracle ,
7629+ specifier asset.Specifier , requestAssetAmount uint64 ,
7630+ inv * lnrpc.Invoice , deviationPPM uint64 ) (uint64 , error ) {
7631+
7632+ // Let's unmarshall the satoshi related fields to see if an amount was
7633+ // set based on those.
7634+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
7635+ if err != nil {
7636+ return 0 , err
7637+ }
7638+
7639+ // Let's make sure that only one type of amount is set, in order to
7640+ // avoid ambiguous behavior. This field dictates the actual value of the
7641+ // invoice so let's be strict and only allow one possible value to be
7642+ // set.
7643+ if requestAssetAmount > 0 && amtMsat != 0 {
7644+ return 0 , fmt .Errorf ("cannot set both asset amount and sats" +
7645+ "amount" )
7646+ }
7647+
7648+ // If the invoice is being added based on asset units, there's nothing
7649+ // to do so return the amount directly.
7650+ if amtMsat == 0 {
7651+ return requestAssetAmount , nil
7652+ }
7653+
7654+ // If the invoice defines the desired amount in satoshis, we need to
7655+ // query our oracle first to get an estimation on the asset rate. This
7656+ // will help us establish a quote with the correct amount of asset
7657+ // units.
7658+ maxUnits , err := rfq .EstimateAssetUnits (
7659+ ctx , priceOracle , specifier , amtMsat ,
7660+ )
7661+ if err != nil {
7662+ return 0 , err
7663+ }
7664+
7665+ maxMathUnits := rfqmath .NewBigIntFromUint64 (maxUnits )
7666+ tolerance := rfqmath .NewBigIntFromUint64 (deviationPPM )
7667+
7668+ // Since we used a different oracle price query above to
7669+ // calculate the max amount of units, we want to add some
7670+ // breathing room to account for price fluctuations caused by
7671+ // the small time delay, plus the fact that the agreed upon
7672+ // quote may be different. If we don't add this safety window
7673+ // the peer may allow a routable amount that evaluates to less
7674+ // than what we ask for. This is also checked below, after
7675+ // acquiring the quote.
7676+
7677+ // Apply the tolerance margin twice. Once due to the ask/bid
7678+ // price deviation that may occur during rfq negotiation, and
7679+ // once for the price movement that may occur between querying
7680+ // the oracle and acquiring the quote. We don't really care
7681+ // about this margin being too big, this only affects the max
7682+ // units our peer agrees to route.
7683+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7684+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7685+
7686+ return maxMathUnits .ToUint64 (), nil
7687+ }
7688+
7689+ // validateInvoiceAmount validates the quote against the invoice we're trying to
7690+ // add. It returns the value in msat that should be included in the invoice.
7691+ func validateInvoiceAmount (acceptedQuote * rfqrpc.PeerAcceptedBuyQuote ,
7692+ requestAssetAmount uint64 , inv * lnrpc.Invoice ) (int64 , error ) {
7693+
7694+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
7695+ if err != nil {
7696+ return 0 , err
7697+ }
7698+
7699+ invUnits := requestAssetAmount
7700+
7701+ // Now that we have the accepted quote, we know the amount in Satoshi
7702+ // that we need to pay. We can now update the invoice with this amount.
7703+ //
7704+ // First, un-marshall the ask asset rate from the accepted quote.
7705+ askAssetRate , err := rfqrpc .UnmarshalFixedPoint (
7706+ acceptedQuote .AskAssetRate ,
7707+ )
7708+ if err != nil {
7709+ return 0 , fmt .Errorf ("error unmarshalling ask asset rate: %w" ,
7710+ err )
7711+ }
7712+
7713+ var invoiceValueMsat int64
7714+ switch {
7715+ case amtMsat != 0 :
7716+ // If the invoice was created over a satoshi amount, we need to
7717+ // calculate the units.
7718+ invUnits = rfqmath .MilliSatoshiToUnits (
7719+ amtMsat , * askAssetRate ,
7720+ ).ScaleTo (0 ).ToUint64 ()
7721+
7722+ // Now let's see if the negotiated quote can actually route the
7723+ // amount we need in msat.
7724+ maxFixedUnits := rfqmath .NewBigIntFixedPoint (
7725+ acceptedQuote .AssetMaxAmount , 0 ,
7726+ )
7727+ maxRoutableMsat := rfqmath .UnitsToMilliSatoshi (
7728+ maxFixedUnits , * askAssetRate ,
7729+ )
7730+
7731+ if maxRoutableMsat <= amtMsat {
7732+ return 0 , fmt .Errorf ("cannot create invoice for %v " +
7733+ "msat, max routable amount is %v msat" , amtMsat ,
7734+ maxRoutableMsat )
7735+ }
7736+
7737+ invoiceValueMsat = int64 (amtMsat )
7738+ default :
7739+ // Convert the asset amount into a fixed-point.
7740+ assetAmount := rfqmath .NewBigIntFixedPoint (
7741+ requestAssetAmount , 0 ,
7742+ )
7743+
7744+ // Calculate the invoice amount in msat.
7745+ valMsat := rfqmath .UnitsToMilliSatoshi (
7746+ assetAmount , * askAssetRate ,
7747+ )
7748+ invoiceValueMsat = int64 (valMsat )
7749+ }
7750+
7751+ // If the invoice is for an asset unit amount smaller than the minimal
7752+ // transportable amount, we'll return an error, as it wouldn't be
7753+ // payable by the network.
7754+ if acceptedQuote .MinTransportableUnits > invUnits {
7755+ return 0 , fmt .Errorf ("cannot create invoice over %d asset " +
7756+ "units, as the minimal transportable amount is %d " +
7757+ "units with the current rate of %v units/BTC" ,
7758+ invUnits , acceptedQuote .MinTransportableUnits ,
7759+ acceptedQuote .AskAssetRate )
7760+ }
7761+
7762+ return invoiceValueMsat , nil
7763+ }
7764+
76247765// AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset
76257766// specific parameters. It allows RPC users to create invoices that correspond
76267767// to the specified asset amount.
@@ -7638,29 +7779,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
76387779 return nil , fmt .Errorf ("asset ID must be 32 bytes" )
76397780 }
76407781
7641- amtMsat , err := lnrpc .UnmarshallAmt (
7642- req .InvoiceRequest .Value , req .InvoiceRequest .ValueMsat ,
7643- )
7644- if err != nil {
7645- return nil , err
7646- }
7647-
7648- // Let's make sure that only one type of amount is set, in order to
7649- // avoid ambiguous behavior. This field dictates the actual value of the
7650- // invoice so let's be strict and only allow one possible value to be
7651- // set.
7652- if req .AssetAmount > 0 && amtMsat != 0 {
7653- return nil , fmt .Errorf ("cannot set both asset amount and sats" +
7654- "amount" )
7655- }
7656-
7657- // In order to avoid repeating the following check let's assign it to a
7658- // boolean for easier access.
7659- var satsMode bool
7660- if amtMsat != 0 {
7661- satsMode = true
7662- }
7663-
76647782 var assetID asset.ID
76657783 copy (assetID [:], req .AssetId )
76667784
@@ -7700,45 +7818,13 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
77007818 time .Duration (expirySeconds ) * time .Second ,
77017819 )
77027820
7703- maxUnits := req .AssetAmount
7704-
7705- // If the invoice defines the desired amount in satoshis, we need to
7706- // query our oracle first to get an estimation on the asset rate. This
7707- // will help us establish a quote with the correct amount of asset
7708- // units.
7709- if satsMode {
7710- maxUnits , err = rfq .EstimateAssetUnits (
7711- ctx , r .cfg .PriceOracle , specifier , amtMsat ,
7712- )
7713- if err != nil {
7714- return nil , err
7715- }
7716-
7717- maxMathUnits := rfqmath .NewBigIntFromUint64 (maxUnits )
7718-
7719- tolerance := rfqmath .NewBigIntFromUint64 (
7720- r .cfg .RfqManager .GetPriceDeviationPpm (),
7721- )
7722-
7723- // Since we used a different oracle price query above to
7724- // calculate the max amount of units, we want to add some
7725- // breathing room to account for price fluctuations caused by
7726- // the small time delay, plus the fact that the agreed upon
7727- // quote may be different. If we don't add this safety window
7728- // the peer may allow a routable amount that evaluates to less
7729- // than what we ask for. This is also checked below, after
7730- // acquiring the quote.
7731-
7732- // Apply the tolerance margin twice. Once due to the ask/bid
7733- // price deviation that may occur during rfq negotiation, and
7734- // once for the price movement that may occur between querying
7735- // the oracle and acquiring the quote. We don't really care
7736- // about this margin being too big, this only affects the max
7737- // units our peer agrees to route.
7738- maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7739- maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7740-
7741- maxUnits = maxMathUnits .ToUint64 ()
7821+ maxUnits , err := calculateAssetMaxAmount (
7822+ ctx , r .cfg .PriceOracle , specifier , req .AssetAmount , iReq ,
7823+ r .cfg .RfqManager .GetPriceDeviationPpm (),
7824+ )
7825+ if err != nil {
7826+ return nil , fmt .Errorf ("error calculating asset max " +
7827+ "amount: %w" , err )
77427828 }
77437829
77447830 resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
@@ -7777,68 +7863,14 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
77777863 return nil , fmt .Errorf ("unexpected response type: %T" , r )
77787864 }
77797865
7780- // Now that we have the accepted quote, we know the amount in Satoshi
7781- // that we need to pay. We can now update the invoice with this amount.
7782- //
7783- // First, un-marshall the ask asset rate from the accepted quote.
7784- askAssetRate , err := rfqrpc .UnmarshalFixedPoint (
7785- acceptedQuote .AskAssetRate ,
7866+ invoiceAmtMsat , err := validateInvoiceAmount (
7867+ acceptedQuote , req .AssetAmount , iReq ,
77867868 )
77877869 if err != nil {
7788- return nil , fmt .Errorf ("error unmarshalling ask asset rate: %w" ,
7789- err )
7790- }
7791-
7792- invUnits := req .AssetAmount
7793-
7794- if satsMode {
7795- // If the invoice was created over a satoshi amount, we need to
7796- // calculate the units.
7797- invUnits = rfqmath .MilliSatoshiToUnits (
7798- amtMsat , * askAssetRate ,
7799- ).ScaleTo (0 ).ToUint64 ()
7800-
7801- // Now let's see if the negotiated quote can actually route the
7802- // amount we need in msat.
7803- maxFixedUnits := rfqmath .NewBigIntFixedPoint (
7804- acceptedQuote .AssetMaxAmount , 0 ,
7805- )
7806- maxRoutableMsat := rfqmath .UnitsToMilliSatoshi (
7807- maxFixedUnits , * askAssetRate ,
7808- )
7809-
7810- if maxRoutableMsat <= amtMsat {
7811- return nil , fmt .Errorf ("cannot create invoice for %v " +
7812- "msat, max routable amount is %v msat" , amtMsat ,
7813- maxRoutableMsat )
7814- }
7815- }
7816-
7817- // If the invoice is for an asset unit amount smaller than the minimal
7818- // transportable amount, we'll return an error, as it wouldn't be
7819- // payable by the network.
7820- if acceptedQuote .MinTransportableUnits > invUnits {
7821- return nil , fmt .Errorf ("cannot create invoice over %d asset " +
7822- "units, as the minimal transportable amount is %d " +
7823- "units with the current rate of %v units/BTC" ,
7824- invUnits , acceptedQuote .MinTransportableUnits ,
7825- acceptedQuote .AskAssetRate )
7826- }
7827-
7828- switch {
7829- case satsMode :
7830- iReq .ValueMsat = int64 (amtMsat )
7831-
7832- default :
7833- // Convert the asset amount into a fixed-point.
7834- assetAmount := rfqmath .NewBigIntFixedPoint (req .AssetAmount , 0 )
7835-
7836- // Calculate the invoice amount in msat.
7837- valMsat := rfqmath .UnitsToMilliSatoshi (
7838- assetAmount , * askAssetRate ,
7839- )
7840- iReq .ValueMsat = int64 (valMsat )
7870+ return nil , fmt .Errorf ("error validating invoice " +
7871+ "amount: %w" , err )
78417872 }
7873+ iReq .ValueMsat = invoiceAmtMsat
78427874
78437875 // The last step is to create a hop hint that includes the fake SCID of
78447876 // the quote, alongside the channel's routing policy. We need to choose
0 commit comments