-
Notifications
You must be signed in to change notification settings - Fork 132
Open
Labels
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