Skip to content

Commit e488107

Browse files
GeorgeTsagkguggero
andcommitted
rpcserver: further break apart AddInvoice into helpers
We extract some more of the AddInvoice logic into individual helper functions. We use one to calculate the max units to use in the rfq quote and another helper to validate and derive the final value to encode in the invoice itself. Co-authored-by: Oliver Gugger <[email protected]>
1 parent b1f52d8 commit e488107

File tree

1 file changed

+153
-121
lines changed

1 file changed

+153
-121
lines changed

rpcserver.go

Lines changed: 153 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)