Skip to content

Commit

Permalink
rpc: implement DecodeAssetPayReq
Browse files Browse the repository at this point in the history
In this commit, we implement the `DecodeAssetPayReq` command. This
command allows a caller to decode a normal LN invoice, adding the asset
specific information along the way. This includes the corresponding
asset unit amount, asset group information, and also the decimal display

information.
Fixes lightninglabs#1238
  • Loading branch information
Roasbeef committed Dec 18, 2024
1 parent 3dd5d23 commit 25289f2
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 0 deletions.
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ var (
Entity: "channels",
Action: "write",
}},
"/tapchannelrpc.TaprootAssetChannels/DecodeAssetPayReq": {{
Entity: "channels",
Action: "read",
}},
"/tapchannelrpc.TaprootAssetChannels/EncodeCustomRecords": {
// This RPC is completely stateless and doesn't require
// any permissions to use.
Expand Down
131 changes: 131 additions & 0 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7729,3 +7729,134 @@ func (r *rpcServer) getInboundPolicy(ctx context.Context, chanID uint64,

return policy, nil
}

// assetInvoiceAmt calculates the amount of asset units to pay for an invoice
// which is expressed in sats.
func (r *rpcServer) assetInvoiceAmt(ctx context.Context, assetID asset.ID,
invoiceAmt lnwire.MilliSatoshi, peerPubKey *route.Vertex,
expiryTimestamp time.Time) (uint64, error) {

acceptedQuote, err := r.fetchSendRfqQuote(
ctx, assetID, invoiceAmt, peerPubKey, expiryTimestamp,
)
if err != nil {
return 0, fmt.Errorf("error sending RFQ quote: %w", err)
}

return acceptedQuote.AssetAmount, nil
}

// DecodeAssetPayReq decodes an incoming invoice, then uses the RFQ system to
// map the BTC amount to the amount of asset units for the specified asset ID.
func (r *rpcServer) DecodeAssetPayReq(ctx context.Context,
payReq *tchrpc.AssetPayReq) (*tchrpc.AssetPayReqResponse, error) {

// First, we'll perform some basic input validation.
switch {
case len(payReq.AssetId) == 0:
return nil, fmt.Errorf("asset ID must be specified")

case len(payReq.AssetId) != 32:
return nil, fmt.Errorf("asset ID must be 32 bytes, "+
"was %d", len(payReq.AssetId))

case len(payReq.PayReqString) == 0:
return nil, fmt.Errorf("payment request must be specified")
}

var (
resp tchrpc.AssetPayReqResponse
assetID asset.ID
)

copy(assetID[:], payReq.AssetId)

// With the inputs validated, we'll first call out to lnd to decode the
// payment request.
rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx)
payReqInfo, err := rawClient.DecodePayReq(rpcCtx, &lnrpc.PayReqString{
PayReq: payReq.PayReqString,
})
if err != nil {
return nil, fmt.Errorf("unable to fetch channel: %w", err)
}

resp.PayReq = payReqInfo

// TODO(roasbeef): add dry run mode?
// * obtains quote, but doesn't actually treat as standing order

// Now that we have the basic invoice information, we'll query the RFQ
// system to obtain a quote to send this amount of BTC. Note that this
// doesn't factor in the fee limit, so this attempts just to map the
// sats amount to an asset unit.
timestamp := time.Unix(payReqInfo.Timestamp, 0)
expiryTimestamp := timestamp.Add(time.Duration(payReqInfo.Expiry))
numMsat := lnwire.NewMSatFromSatoshis(
btcutil.Amount(payReqInfo.NumSatoshis),
)
invoiceAmt, err := r.assetInvoiceAmt(
ctx, assetID, numMsat, nil,
expiryTimestamp,
)
if err != nil {
return nil, fmt.Errorf("error deriving asset amount: %w", err)
}

resp.AssetAmount = invoiceAmt

// Next, we'll fetch the information for this asset ID through the addr
// book. This'll automatically fetch the asset if needed.
assetGroup, err := r.cfg.AddrBook.QueryAssetInfo(ctx, assetID)
if err != nil {
return nil, fmt.Errorf("unable to fetch asset info for "+
"asset_id=%x: %w", assetID[:], err)
}

resp.GenesisInfo = &taprpc.GenesisInfo{
GenesisPoint: assetGroup.FirstPrevOut.String(),
AssetType: taprpc.AssetType(assetGroup.Type),
Name: assetGroup.Tag,
MetaHash: assetGroup.MetaHash[:],
AssetId: assetID[:],
}

// If this asset ID belongs to an asset group, then we'll display thiat
// information as well.
//
// nolint:lll
if assetGroup.GroupKey != nil {
groupInfo := assetGroup.GroupKey
resp.AssetGroup = &taprpc.AssetGroup{
RawGroupKey: groupInfo.RawKey.PubKey.SerializeCompressed(),
TweakedGroupKey: groupInfo.GroupPubKey.SerializeCompressed(),
TapscriptRoot: groupInfo.TapscriptRoot,
}

if len(groupInfo.Witness) != 0 {
resp.AssetGroup.AssetWitness, err = asset.SerializeGroupWitness(
groupInfo.Witness,
)
if err != nil {
return nil, err
}
}
}

// The final piece of information we need is the decimal display
// information for this asset ID.
decDisplay, err := r.DecDisplayForAssetID(ctx, assetID)
if err != nil {
return nil, err
}

resp.DecimalDisplay = fn.MapOptionZ(
decDisplay, func(d uint32) *taprpc.DecimalDisplay {
return &taprpc.DecimalDisplay{
DecimalDisplay: d,
}
},
)

return &resp, nil
}

0 comments on commit 25289f2

Please sign in to comment.