Skip to content

Commit 66be6de

Browse files
committed
multi: add AddInvoice RPC method
1 parent db31799 commit 66be6de

File tree

10 files changed

+1031
-46
lines changed

10 files changed

+1031
-46
lines changed

perms/perms.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ var (
272272
Entity: "channels",
273273
Action: "write",
274274
}},
275+
"/tapchannelrpc.TaprootAssetChannels/AddInvoice": {{
276+
Entity: "channels",
277+
Action: "write",
278+
}},
275279
"/tapchannelrpc.TaprootAssetChannels/EncodeCustomRecords": {
276280
// This RPC is completely stateless and doesn't require
277281
// any permissions to use.

rfq/manager.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const (
2121
// DefaultTimeout is the default timeout used for context operations.
2222
DefaultTimeout = 30 * time.Second
2323

24+
// DefaultInvoiceExpiry is the default expiry time for asset invoices.
25+
// The current value corresponds to 5 minutes.
26+
DefaultInvoiceExpiry = time.Second * 300
27+
2428
// CacheCleanupInterval is the interval at which local runtime caches
2529
// are cleaned up.
2630
CacheCleanupInterval = 30 * time.Second

rpcserver.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6946,6 +6946,140 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
69466946
}
69476947
}
69486948

6949+
// AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset
6950+
// specific parameters. It allows RPC users to create invoices that correspond
6951+
// to the specified asset amount.
6952+
func (r *rpcServer) AddInvoice(ctx context.Context,
6953+
req *tchrpc.AddInvoiceRequest) (*tchrpc.AddInvoiceResponse, error) {
6954+
6955+
if req.InvoiceRequest == nil {
6956+
return nil, fmt.Errorf("invoice request must be specified")
6957+
}
6958+
iReq := req.InvoiceRequest
6959+
6960+
// Do some preliminary checks on the asset ID and make sure we have any
6961+
// balance for that asset.
6962+
if len(req.AssetId) != sha256.Size {
6963+
return nil, fmt.Errorf("asset ID must be 32 bytes")
6964+
}
6965+
var assetID asset.ID
6966+
copy(assetID[:], req.AssetId)
6967+
6968+
// The peer public key is optional if there is only a single asset
6969+
// channel.
6970+
var peerPubKey *route.Vertex
6971+
if len(req.PeerPubkey) > 0 {
6972+
parsedKey, err := route.NewVertexFromBytes(req.PeerPubkey)
6973+
if err != nil {
6974+
return nil, fmt.Errorf("error parsing peer pubkey: %w",
6975+
err)
6976+
}
6977+
6978+
peerPubKey = &parsedKey
6979+
}
6980+
6981+
// We can now query the asset channels we have.
6982+
assetChan, err := r.rfqChannel(ctx, assetID, peerPubKey)
6983+
if err != nil {
6984+
return nil, fmt.Errorf("error finding asset channel to use: %w",
6985+
err)
6986+
}
6987+
6988+
// Even if the user didn't specify the peer public key before, we
6989+
// definitely know it now. So let's make sure it's always set.
6990+
peerPubKey = &assetChan.channelInfo.PubKeyBytes
6991+
6992+
expirySeconds := iReq.Expiry
6993+
if expirySeconds == 0 {
6994+
expirySeconds = int64(rfq.DefaultInvoiceExpiry.Seconds())
6995+
}
6996+
expiryTimestamp := time.Now().Add(
6997+
time.Duration(expirySeconds) * time.Second,
6998+
)
6999+
7000+
resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{
7001+
AssetSpecifier: &rfqrpc.AssetSpecifier{
7002+
Id: &rfqrpc.AssetSpecifier_AssetId{
7003+
AssetId: assetID[:],
7004+
},
7005+
},
7006+
MinAssetAmount: req.AssetAmount,
7007+
Expiry: uint64(expiryTimestamp.Unix()),
7008+
PeerPubKey: peerPubKey[:],
7009+
TimeoutSeconds: uint32(
7010+
rfq.DefaultTimeout.Seconds(),
7011+
),
7012+
})
7013+
if err != nil {
7014+
return nil, fmt.Errorf("error adding buy order: %w", err)
7015+
}
7016+
7017+
var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote
7018+
switch r := resp.Response.(type) {
7019+
case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote:
7020+
acceptedQuote = r.AcceptedQuote
7021+
7022+
case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote:
7023+
return nil, fmt.Errorf("peer %v sent back an invalid quote, "+
7024+
"status: %v", r.InvalidQuote.Peer,
7025+
r.InvalidQuote.Status.String())
7026+
7027+
case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote:
7028+
return nil, fmt.Errorf("peer %v rejected the quote, code: %v, "+
7029+
"error message: %v", r.RejectedQuote.Peer,
7030+
r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage)
7031+
7032+
default:
7033+
return nil, fmt.Errorf("unexpected response type: %T", r)
7034+
}
7035+
7036+
// Now that we have the accepted quote, we know the amount in Satoshi
7037+
// that we need to pay. We can now update the invoice with this amount.
7038+
mSatPerUnit := acceptedQuote.AskPrice
7039+
iReq.ValueMsat = int64(req.AssetAmount * mSatPerUnit)
7040+
7041+
// The last step is to create a hop hint that includes the fake SCID of
7042+
// the quote, alongside the channel's routing policy. We need to choose
7043+
// the policy that points towards us, as the payment will be flowing in.
7044+
// So we get the policy that's being set by the remote peer.
7045+
channelID := assetChan.channelInfo.ChannelID
7046+
inboundPolicy, err := r.getInboundPolicy(
7047+
ctx, channelID, peerPubKey.String(),
7048+
)
7049+
if err != nil {
7050+
return nil, fmt.Errorf("unable to get inbound channel policy "+
7051+
"for channel with ID %d: %w", channelID, err)
7052+
}
7053+
7054+
hopHint := &lnrpc.HopHint{
7055+
NodeId: peerPubKey.String(),
7056+
ChanId: acceptedQuote.Scid,
7057+
FeeBaseMsat: uint32(inboundPolicy.FeeBaseMsat),
7058+
FeeProportionalMillionths: uint32(
7059+
inboundPolicy.FeeRateMilliMsat,
7060+
),
7061+
CltvExpiryDelta: inboundPolicy.TimeLockDelta,
7062+
}
7063+
iReq.RouteHints = []*lnrpc.RouteHint{
7064+
{
7065+
HopHints: []*lnrpc.HopHint{
7066+
hopHint,
7067+
},
7068+
},
7069+
}
7070+
7071+
rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx)
7072+
invoiceResp, err := rawClient.AddInvoice(rpcCtx, iReq)
7073+
if err != nil {
7074+
return nil, fmt.Errorf("error creating invoice: %w", err)
7075+
}
7076+
7077+
return &tchrpc.AddInvoiceResponse{
7078+
AcceptedBuyQuote: acceptedQuote,
7079+
InvoiceResult: invoiceResp,
7080+
}, nil
7081+
}
7082+
69497083
// DeclareScriptKey declares a new script key to the wallet. This is useful
69507084
// when the script key contains scripts, which would mean it wouldn't be
69517085
// recognized by the wallet automatically. Declaring a script key will make any
@@ -7166,3 +7300,24 @@ func (r *rpcServer) computeChannelAssetBalance(
71667300

71677301
return channelsByID, nil
71687302
}
7303+
7304+
// getInboundPolicy returns the policy of the given channel that points towards
7305+
// our node, so it's the policy set by the remote peer.
7306+
func (r *rpcServer) getInboundPolicy(ctx context.Context, chanID uint64,
7307+
remotePubStr string) (*lnrpc.RoutingPolicy, error) {
7308+
7309+
rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx)
7310+
edge, err := rawClient.GetChanInfo(rpcCtx, &lnrpc.ChanInfoRequest{
7311+
ChanId: chanID,
7312+
})
7313+
if err != nil {
7314+
return nil, fmt.Errorf("unable to fetch channel: %w", err)
7315+
}
7316+
7317+
policy := edge.Node2Policy
7318+
if edge.Node2Pub == remotePubStr {
7319+
policy = edge.Node1Policy
7320+
}
7321+
7322+
return policy, nil
7323+
}

0 commit comments

Comments
 (0)