@@ -7746,11 +7746,24 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
77467746 time .Duration (expirySeconds ) * time .Second ,
77477747 )
77487748
7749+ // We now want to calculate the upper bound of the RFQ order, which
7750+ // either is the asset amount specified by the user, or the converted
7751+ // satoshi amount of the invoice, expressed in asset units, using the
7752+ // local price oracle's conversion rate.
7753+ maxUnits , err := calculateAssetMaxAmount (
7754+ ctx , r .cfg .PriceOracle , specifier , req .AssetAmount , iReq ,
7755+ r .cfg .RfqManager .GetPriceDeviationPpm (),
7756+ )
7757+ if err != nil {
7758+ return nil , fmt .Errorf ("error calculating asset max " +
7759+ "amount: %w" , err )
7760+ }
7761+
77497762 rpcSpecifier := marshalAssetSpecifier (specifier )
77507763
77517764 resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
77527765 AssetSpecifier : & rpcSpecifier ,
7753- AssetMaxAmt : req . AssetAmount ,
7766+ AssetMaxAmt : maxUnits ,
77547767 Expiry : uint64 (expiryTimestamp .Unix ()),
77557768 PeerPubKey : peerPubKey [:],
77567769 TimeoutSeconds : uint32 (
@@ -7784,7 +7797,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
77847797 // Satoshi that we need to pay. We can now update the invoice with this
77857798 // amount.
77867799 invoiceAmtMsat , err := validateInvoiceAmount (
7787- acceptedQuote , req .AssetAmount ,
7800+ acceptedQuote , req .AssetAmount , iReq ,
77887801 )
77897802 if err != nil {
77907803 return nil , fmt .Errorf ("error validating invoice amount: %w" ,
@@ -7893,10 +7906,78 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
78937906 }, nil
78947907}
78957908
7909+ // calculateAssetMaxAmount calculates the max units to be placed in the invoice
7910+ // RFQ quote order. When adding invoices based on asset units, that value is
7911+ // directly returned. If using the value/value_msat fields of the invoice then
7912+ // a price oracle query will take place to calculate the max units of the quote.
7913+ func calculateAssetMaxAmount (ctx context.Context , priceOracle rfq.PriceOracle ,
7914+ specifier asset.Specifier , requestAssetAmount uint64 ,
7915+ inv * lnrpc.Invoice , deviationPPM uint64 ) (uint64 , error ) {
7916+
7917+ // Let's unmarshall the satoshi related fields to see if an amount was
7918+ // set based on those.
7919+ amtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
7920+ if err != nil {
7921+ return 0 , err
7922+ }
7923+
7924+ // Let's make sure that only one type of amount is set, in order to
7925+ // avoid ambiguous behavior. This field dictates the actual value of the
7926+ // invoice so let's be strict and only allow one possible value to be
7927+ // set.
7928+ if requestAssetAmount > 0 && amtMsat != 0 {
7929+ return 0 , fmt .Errorf ("cannot set both asset amount and sats " +
7930+ "amount" )
7931+ }
7932+
7933+ // If the invoice is being added based on asset units, there's nothing
7934+ // to do so return the amount directly.
7935+ if amtMsat == 0 {
7936+ return requestAssetAmount , nil
7937+ }
7938+
7939+ // If the invoice defines the desired amount in satoshis, we need to
7940+ // query our oracle first to get an estimation on the asset rate. This
7941+ // will help us establish a quote with the correct amount of asset
7942+ // units.
7943+ maxUnits , err := rfq .EstimateAssetUnits (
7944+ ctx , priceOracle , specifier , amtMsat ,
7945+ )
7946+ if err != nil {
7947+ return 0 , err
7948+ }
7949+
7950+ maxMathUnits := rfqmath .NewBigIntFromUint64 (maxUnits )
7951+
7952+ // Since we used a different oracle price query above calculate the max
7953+ // amount of units, we want to add some breathing room to account for
7954+ // price fluctuations caused by the small-time delay, plus the fact that
7955+ // the agreed upon quote may be different. If we don't add this safety
7956+ // window the peer may allow a routable amount that evaluates to less
7957+ // than what we ask for.
7958+ // Apply the tolerance margin twice. Once due to the ask/bid price
7959+ // deviation that may occur during rfq negotiation, and once for the
7960+ // price movement that may occur between querying the oracle and
7961+ // acquiring the quote. We don't really care about this margin being too
7962+ // big, this only affects the max units our peer agrees to route.
7963+ tolerance := rfqmath .NewBigIntFromUint64 (deviationPPM )
7964+
7965+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7966+ maxMathUnits = rfqmath .AddTolerance (maxMathUnits , tolerance )
7967+
7968+ return maxMathUnits .ToUint64 (), nil
7969+ }
7970+
78967971// validateInvoiceAmount validates the quote against the invoice we're trying to
78977972// add. It returns the value in msat that should be included in the invoice.
78987973func validateInvoiceAmount (acceptedQuote * rfqrpc.PeerAcceptedBuyQuote ,
7899- requestAssetAmount uint64 ) (lnwire.MilliSatoshi , error ) {
7974+ requestAssetAmount uint64 , inv * lnrpc.Invoice ) (lnwire.MilliSatoshi ,
7975+ error ) {
7976+
7977+ invoiceAmtMsat , err := lnrpc .UnmarshallAmt (inv .Value , inv .ValueMsat )
7978+ if err != nil {
7979+ return 0 , err
7980+ }
79007981
79017982 // Now that we have the accepted quote, we know the amount in Satoshi
79027983 // that we need to pay. We can now update the invoice with this amount.
@@ -7910,22 +7991,56 @@ func validateInvoiceAmount(acceptedQuote *rfqrpc.PeerAcceptedBuyQuote,
79107991 err )
79117992 }
79127993
7913- // Convert the asset amount into a fixed-point.
7914- assetAmount := rfqmath .NewBigIntFixedPoint (requestAssetAmount , 0 )
7915-
7916- // Calculate the invoice amount in msat.
7917- newInvoiceAmtMsat := rfqmath .UnitsToMilliSatoshi (
7918- assetAmount , * askAssetRate ,
7994+ // We either have a requested amount in milli satoshi that we want to
7995+ // validate against the quote's max amount (in which case we overwrite
7996+ // the invoiceUnits), or we have a requested amount in asset units that
7997+ // we want to convert into milli satoshis (and overwrite
7998+ // newInvoiceAmtMsat).
7999+ var (
8000+ newInvoiceAmtMsat = invoiceAmtMsat
8001+ invoiceUnits = requestAssetAmount
79198002 )
8003+ switch {
8004+ case invoiceAmtMsat != 0 :
8005+ // If the invoice was created with a satoshi amount, we need to
8006+ // calculate the units.
8007+ invoiceUnits = rfqmath .MilliSatoshiToUnits (
8008+ invoiceAmtMsat , * askAssetRate ,
8009+ ).ScaleTo (0 ).ToUint64 ()
8010+
8011+ // Now let's see if the negotiated quote can actually route the
8012+ // amount we need in msat.
8013+ maxFixedUnits := rfqmath .NewBigIntFixedPoint (
8014+ acceptedQuote .AssetMaxAmount , 0 ,
8015+ )
8016+ maxRoutableMsat := rfqmath .UnitsToMilliSatoshi (
8017+ maxFixedUnits , * askAssetRate ,
8018+ )
8019+
8020+ if maxRoutableMsat <= invoiceAmtMsat {
8021+ return 0 , fmt .Errorf ("cannot create invoice for %v " +
8022+ "msat, max routable amount is %v msat" ,
8023+ invoiceAmtMsat , maxRoutableMsat )
8024+ }
8025+
8026+ default :
8027+ // Convert the asset amount into a fixed-point.
8028+ assetAmount := rfqmath .NewBigIntFixedPoint (invoiceUnits , 0 )
8029+
8030+ // Calculate the invoice amount in msat.
8031+ newInvoiceAmtMsat = rfqmath .UnitsToMilliSatoshi (
8032+ assetAmount , * askAssetRate ,
8033+ )
8034+ }
79208035
79218036 // If the invoice is for an asset unit amount smaller than the minimal
79228037 // transportable amount, we'll return an error, as it wouldn't be
79238038 // payable by the network.
7924- if acceptedQuote .MinTransportableUnits > requestAssetAmount {
8039+ if acceptedQuote .MinTransportableUnits > invoiceUnits {
79258040 return 0 , fmt .Errorf ("cannot create invoice for %d asset " +
79268041 "units, as the minimal transportable amount is %d " +
79278042 "units with the current rate of %v units/BTC" ,
7928- requestAssetAmount , acceptedQuote .MinTransportableUnits ,
8043+ invoiceUnits , acceptedQuote .MinTransportableUnits ,
79298044 acceptedQuote .AskAssetRate )
79308045 }
79318046
0 commit comments