@@ -8011,6 +8011,160 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
80118011 }, nil
80128012}
80138013
8014+ // calculateAssetMaxAmount calculates the max units to be placed in the invoice
8015+ // RFQ quote order. When adding invoices based on asset units, that value is
8016+ // directly returned. If using the value/value_msat fields of the invoice then
8017+ // a price oracle query will take place to calculate the max units of the quote.
8018+ func calculateAssetMaxAmount (ctx context.Context , priceOracle rfq.PriceOracle ,
8019+ specifier asset.Specifier , requestAssetAmount uint64 ,
8020+ inv * lnrpc.Invoice , deviationPPM uint64 ) (uint64 , error ) {
8021+
8022+ // Let's unmarshall the satoshi related fields to see if an amount was
8023+ // set based on those.
8024+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
8025+ if err != nil {
8026+ return 0 , err
8027+ }
8028+
8029+ // Let's make sure that only one type of amount is set, in order to
8030+ // avoid ambiguous behavior. This field dictates the actual value of the
8031+ // invoice so let's be strict and only allow one possible value to be
8032+ // set.
8033+ if requestAssetAmount > 0 && amtMsat != 0 {
8034+ return 0 , fmt .Errorf ("cannot set both asset amount and sats " +
8035+ "amount" )
8036+ }
8037+
8038+ // If the invoice is being added based on asset units, there's nothing
8039+ // to do so return the amount directly.
8040+ if amtMsat == 0 {
8041+ return requestAssetAmount , nil
8042+ }
8043+
8044+ // If the invoice defines the desired amount in satoshis, we need to
8045+ // query our oracle first to get an estimation on the asset rate. This
8046+ // will help us establish a quote with the correct amount of asset
8047+ // units.
8048+ oracleRes , err := priceOracle .QueryBidPrice (
8049+ ctx , specifier , fn .None [uint64 ](), fn .Some (amtMsat ),
8050+ fn .None [rfqmsg.AssetRate ](),
8051+ )
8052+ if err != nil {
8053+ return 0 , err
8054+ }
8055+
8056+ if oracleRes .Err != nil {
8057+ return 0 , fmt .Errorf ("cannot query oracle: %v" ,
8058+ oracleRes .Err .Error ())
8059+ }
8060+
8061+ assetUnits := rfqmath .MilliSatoshiToUnits (
8062+ amtMsat , oracleRes .AssetRate .Rate ,
8063+ )
8064+
8065+ maxUnits := assetUnits .ToUint64 ()
8066+
8067+ maxMathUnits := rfqmath .NewBigIntFromUint64 (maxUnits )
8068+
8069+ // Since we used a different oracle price query above calculate the max
8070+ // amount of units, we want to add some breathing room to account for
8071+ // price fluctuations caused by the small time delay, plus the fact that
8072+ // the agreed upon quote may be different. If we don't add this safety
8073+ // window the peer may allow a routable amount that evaluates to less
8074+ // than what we ask for.
8075+ tolerance := rfqmath .NewBigIntFromUint64 (deviationPPM )
8076+
8077+ // Calculate the tolerance margin.
8078+ toleranceUnits := maxMathUnits .Mul (tolerance ).Div (
8079+ rfqmath .NewBigIntFromUint64 (1_000_000 ),
8080+ )
8081+
8082+ // Apply the tolerance margin twice. Once due to the ask/bid price
8083+ // deviation that may occur during rfq negotiation, and once for the
8084+ // price movement that may occur between querying the oracle and
8085+ // acquiring the quote. We don't really care about this margin being too
8086+ // big, this only affects the max units our peer agrees to route.
8087+ maxMathUnits = maxMathUnits .Add (toleranceUnits ).Add (toleranceUnits )
8088+
8089+ return maxMathUnits .ToUint64 (), nil
8090+ }
8091+
8092+ // validateInvoiceAmount validates the quote against the invoice we're trying to
8093+ // add. It returns the value in msat that should be included in the invoice.
8094+ func validateInvoiceAmount (acceptedQuote * rfqrpc.PeerAcceptedBuyQuote ,
8095+ requestAssetAmount uint64 , inv * lnrpc.Invoice ) (int64 , error ) {
8096+
8097+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
8098+ if err != nil {
8099+ return 0 , err
8100+ }
8101+
8102+ invUnits := requestAssetAmount
8103+
8104+ // Now that we have the accepted quote, we know the amount in Satoshi
8105+ // that we need to pay. We can now update the invoice with this amount.
8106+ //
8107+ // First, un-marshall the ask asset rate from the accepted quote.
8108+ askAssetRate , err := rfqrpc .UnmarshalFixedPoint (
8109+ acceptedQuote .AskAssetRate ,
8110+ )
8111+ if err != nil {
8112+ return 0 , fmt .Errorf ("error unmarshalling ask asset rate: %w" ,
8113+ err )
8114+ }
8115+
8116+ var invoiceValueMsat int64
8117+ switch {
8118+ case amtMsat != 0 :
8119+ // If the invoice was created with a satoshi amount, we need to
8120+ // calculate the units.
8121+ invUnits = rfqmath .MilliSatoshiToUnits (
8122+ amtMsat , * askAssetRate ,
8123+ ).ScaleTo (0 ).ToUint64 ()
8124+
8125+ // Now let's see if the negotiated quote can actually route the
8126+ // amount we need in msat.
8127+ maxFixedUnits := rfqmath .NewBigIntFixedPoint (
8128+ acceptedQuote .AssetMaxAmount , 0 ,
8129+ )
8130+ maxRoutableMsat := rfqmath .UnitsToMilliSatoshi (
8131+ maxFixedUnits , * askAssetRate ,
8132+ )
8133+
8134+ if maxRoutableMsat <= amtMsat {
8135+ return 0 , fmt .Errorf ("cannot create invoice for %v " +
8136+ "msat, max routable amount is %v msat" , amtMsat ,
8137+ maxRoutableMsat )
8138+ }
8139+
8140+ invoiceValueMsat = int64 (amtMsat )
8141+ default :
8142+ // Convert the asset amount into a fixed-point.
8143+ assetAmount := rfqmath .NewBigIntFixedPoint (
8144+ requestAssetAmount , 0 ,
8145+ )
8146+
8147+ // Calculate the invoice amount in msat.
8148+ valMsat := rfqmath .UnitsToMilliSatoshi (
8149+ assetAmount , * askAssetRate ,
8150+ )
8151+ invoiceValueMsat = int64 (valMsat )
8152+ }
8153+
8154+ // If the invoice is for an asset unit amount smaller than the minimal
8155+ // transportable amount, we'll return an error, as it wouldn't be
8156+ // payable by the network.
8157+ if acceptedQuote .MinTransportableUnits > invUnits {
8158+ return 0 , fmt .Errorf ("cannot create invoice of %d asset " +
8159+ "units, as the minimal transportable amount is %d " +
8160+ "units with the current rate of %v units/BTC" ,
8161+ invUnits , acceptedQuote .MinTransportableUnits ,
8162+ acceptedQuote .AskAssetRate )
8163+ }
8164+
8165+ return invoiceValueMsat , nil
8166+ }
8167+
80148168// DeclareScriptKey declares a new script key to the wallet. This is useful
80158169// when the script key contains scripts, which would mean it wouldn't be
80168170// recognized by the wallet automatically. Declaring a script key will make any
0 commit comments