diff --git a/perms/perms.go b/perms/perms.go index 91bff0d6f..0b755adc0 100644 --- a/perms/perms.go +++ b/perms/perms.go @@ -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. diff --git a/rpcserver.go b/rpcserver.go index cb965e8ee..8a54d2db7 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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 +}