Skip to content

Commit 0c548de

Browse files
committed
rpcserver: add AddInvoice helpers for calculating & validating fields
1 parent 3c761c0 commit 0c548de

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

rpcserver.go

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

Comments
 (0)