-
Notifications
You must be signed in to change notification settings - Fork 136
Open
Labels
P3RFQWork relating to TAP channel Request For Quote (RFQ).Work relating to TAP channel Request For Quote (RFQ).enhancementNew feature or requestNew feature or requestinvoices
Description
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 (
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
Labels
P3RFQWork relating to TAP channel Request For Quote (RFQ).Work relating to TAP channel Request For Quote (RFQ).enhancementNew feature or requestNew feature or requestinvoices