Skip to content

[feature]: AddInvoice: allow specifying an rfq_id #1442

Open
@ZZiigguurraatt

Description

@ZZiigguurraatt

For SendPayment we have the ability to manually pass an rfq_id from AddAssetSellOrder.

In AddInvoice we do not have the ability to manually pass an rfq_id from AddAssetBuyOrder . This issue requests that capability to be added.

Motivation for the issue can be found in #1440 .

Here we have a test case that does manually create the asset invoice and uses a manually passed rfq_id: https://github.com/lightninglabs/lightning-terminal/blob/f082579252e4b6d1f9dd84e74646ec3e9b7e8e33/itest/litd_custom_channels_test.go#L2112-L2166 . That test needs to be updated once the capability is added in to the AddInvoice function (

taproot-assets/rpcserver.go

Lines 7623 to 7845 in 331ac78

// AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset
// specific parameters. It allows RPC users to create invoices that correspond
// to the specified asset amount.
func (r *rpcServer) AddInvoice(ctx context.Context,
req *tchrpc.AddInvoiceRequest) (*tchrpc.AddInvoiceResponse, error) {
if req.InvoiceRequest == nil {
return nil, fmt.Errorf("invoice request must be specified")
}
iReq := req.InvoiceRequest
// Do some preliminary checks on the asset ID and make sure we have any
// balance for that asset.
if len(req.AssetId) != sha256.Size {
return nil, fmt.Errorf("asset ID must be 32 bytes")
}
var assetID asset.ID
copy(assetID[:], req.AssetId)
// The peer public key is optional if there is only a single asset
// channel.
var peerPubKey *route.Vertex
if len(req.PeerPubkey) > 0 {
parsedKey, err := route.NewVertexFromBytes(req.PeerPubkey)
if err != nil {
return nil, fmt.Errorf("error parsing peer pubkey: %w",
err)
}
peerPubKey = &parsedKey
}
specifier := asset.NewSpecifierFromId(assetID)
// We can now query the asset channels we have.
assetChan, err := r.rfqChannel(
ctx, specifier, peerPubKey, ReceiveIntention,
)
if err != nil {
return nil, fmt.Errorf("error finding asset channel to use: %w",
err)
}
// Even if the user didn't specify the peer public key before, we
// definitely know it now. So let's make sure it's always set.
peerPubKey = &assetChan.channelInfo.PubKeyBytes
expirySeconds := iReq.Expiry
if expirySeconds == 0 {
expirySeconds = int64(rfq.DefaultInvoiceExpiry.Seconds())
}
expiryTimestamp := time.Now().Add(
time.Duration(expirySeconds) * time.Second,
)
resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{
AssetSpecifier: &rfqrpc.AssetSpecifier{
Id: &rfqrpc.AssetSpecifier_AssetId{
AssetId: assetID[:],
},
},
AssetMaxAmt: req.AssetAmount,
Expiry: uint64(expiryTimestamp.Unix()),
PeerPubKey: peerPubKey[:],
TimeoutSeconds: uint32(
rfq.DefaultTimeout.Seconds(),
),
})
if err != nil {
return nil, fmt.Errorf("error adding buy order: %w", err)
}
var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote
switch r := resp.Response.(type) {
case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote:
acceptedQuote = r.AcceptedQuote
case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote:
return nil, fmt.Errorf("peer %v sent back an invalid quote, "+
"status: %v", r.InvalidQuote.Peer,
r.InvalidQuote.Status.String())
case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote:
return nil, fmt.Errorf("peer %v rejected the quote, code: %v, "+
"error message: %v", r.RejectedQuote.Peer,
r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage)
default:
return nil, fmt.Errorf("unexpected response type: %T", r)
}
// If the invoice is for an asset unit amount smaller than the minimal
// transportable amount, we'll return an error, as it wouldn't be
// payable by the network.
if acceptedQuote.MinTransportableUnits > req.AssetAmount {
return nil, fmt.Errorf("cannot create invoice over %d asset "+
"units, as the minimal transportable amount is %d "+
"units with the current rate of %v units/BTC",
req.AssetAmount, acceptedQuote.MinTransportableUnits,
acceptedQuote.AskAssetRate)
}
// Now that we have the accepted quote, we know the amount in Satoshi
// that we need to pay. We can now update the invoice with this amount.
//
// First, un-marshall the ask asset rate from the accepted quote.
askAssetRate, err := rfqrpc.UnmarshalFixedPoint(
acceptedQuote.AskAssetRate,
)
if err != nil {
return nil, fmt.Errorf("error unmarshalling ask asset rate: %w",
err)
}
// Convert the asset amount into a fixed-point.
assetAmount := rfqmath.NewBigIntFixedPoint(req.AssetAmount, 0)
// Calculate the invoice amount in msat.
valMsat := rfqmath.UnitsToMilliSatoshi(assetAmount, *askAssetRate)
iReq.ValueMsat = int64(valMsat)
// The last step is to create a hop hint that includes the fake SCID of
// the quote, alongside the channel's routing policy. We need to choose
// the policy that points towards us, as the payment will be flowing in.
// So we get the policy that's being set by the remote peer.
channelID := assetChan.channelInfo.ChannelID
inboundPolicy, err := r.getInboundPolicy(
ctx, channelID, peerPubKey.String(),
)
if err != nil {
return nil, fmt.Errorf("unable to get inbound channel policy "+
"for channel with ID %d: %w", channelID, err)
}
// If this is a hodl invoice, then we'll copy over the relevant fields,
// then route this through the invoicerpc instead.
if req.HodlInvoice != nil {
payHash, err := lntypes.MakeHash(req.HodlInvoice.PaymentHash)
if err != nil {
return nil, fmt.Errorf("error creating payment "+
"hash: %w", err)
}
peerPub, err := btcec.ParsePubKey(peerPubKey[:])
if err != nil {
return nil, fmt.Errorf("error parsing peer "+
"pubkey: %w", err)
}
hopHint := []zpay32.HopHint{
{
NodeID: peerPub,
ChannelID: acceptedQuote.Scid,
FeeBaseMSat: uint32(inboundPolicy.FeeBaseMsat),
FeeProportionalMillionths: uint32(
inboundPolicy.FeeRateMilliMsat,
),
CLTVExpiryDelta: uint16(
inboundPolicy.TimeLockDelta,
),
},
}
payReq, err := r.cfg.Lnd.Invoices.AddHoldInvoice(
ctx, &invoicesrpc.AddInvoiceData{
Memo: iReq.Memo,
Value: lnwire.MilliSatoshi(
iReq.ValueMsat,
),
Hash: &payHash,
DescriptionHash: iReq.DescriptionHash,
Expiry: iReq.Expiry,
// We set private to false as we don't want to
// add any hop hints other than this one.
Private: false,
HodlInvoice: true,
RouteHints: [][]zpay32.HopHint{hopHint},
},
)
if err != nil {
return nil, fmt.Errorf("error creating hodl invoice: "+
"%w", err)
}
return &tchrpc.AddInvoiceResponse{
AcceptedBuyQuote: acceptedQuote,
InvoiceResult: &lnrpc.AddInvoiceResponse{
PaymentRequest: payReq,
},
}, nil
}
// Otherwise, we'll make this into a normal invoice.
hopHint := &lnrpc.HopHint{
NodeId: peerPubKey.String(),
ChanId: acceptedQuote.Scid,
FeeBaseMsat: uint32(inboundPolicy.FeeBaseMsat),
FeeProportionalMillionths: uint32(
inboundPolicy.FeeRateMilliMsat,
),
CltvExpiryDelta: inboundPolicy.TimeLockDelta,
}
iReq.RouteHints = []*lnrpc.RouteHint{
{
HopHints: []*lnrpc.HopHint{
hopHint,
},
},
}
rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx)
invoiceResp, err := rawClient.AddInvoice(rpcCtx, iReq)
if err != nil {
return nil, fmt.Errorf("error creating invoice: %w", err)
}
return &tchrpc.AddInvoiceResponse{
AcceptedBuyQuote: acceptedQuote,
InvoiceResult: invoiceResp,
}, nil
}
).

Also, when fixing this issue, need to keep in mind #1428

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions