@@ -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