From 1924f0853de024371e1152bfc53d74d15c5fd975 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 13 Dec 2024 13:19:17 +0100 Subject: [PATCH] rpc: implement DecodeAssetPayReq 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 https://github.com/lightninglabs/taproot-assets/issues/1238 --- rpcserver.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index cb965e8ee..ad19c088e 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -7729,3 +7729,132 @@ 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.AssetPayReqString) (*tchrpc.AssetPayReq, 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.PayReq) == 0: + return nil, fmt.Errorf("payment request must be specified") + } + + var ( + resp tchrpc.AssetPayReq + 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.PayReq, + }) + 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(int64(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. + 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 +}