diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 072ade7d4..45561d7f1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -375,6 +375,7 @@ jobs: working-directory: ./lightning-terminal run: | go mod edit -replace=github.com/lightninglabs/taproot-assets=../ + go mod edit -replace=github.com/lightninglabs/taproot-assets/taprpc=../taprpc go mod tidy - name: Install yarn diff --git a/address/book.go b/address/book.go index d10711406..95197a8eb 100644 --- a/address/book.go +++ b/address/book.go @@ -82,8 +82,8 @@ type QueryParams struct { // known to universe servers in our federation. type AssetSyncer interface { // SyncAssetInfo queries the universes in our federation for genesis - // and asset group information about the given asset ID. - SyncAssetInfo(ctx context.Context, assetID *asset.ID) error + // and asset group information about the given asset. + SyncAssetInfo(ctx context.Context, specifier asset.Specifier) error // EnableAssetSync updates the sync config for the given asset so that // we sync future issuance proofs. @@ -232,7 +232,7 @@ func (b *Book) QueryAssetInfo(ctx context.Context, log.Debugf("Asset %v is unknown, attempting to bootstrap", id.String()) // Use the AssetSyncer to query our universe federation for the asset. - err = b.cfg.Syncer.SyncAssetInfo(ctx, &id) + err = b.cfg.Syncer.SyncAssetInfo(ctx, asset.NewSpecifierFromId(id)) if err != nil { return nil, err } @@ -264,6 +264,27 @@ func (b *Book) QueryAssetInfo(ctx context.Context, return assetGroup, nil } +// SyncAssetGroup attempts to enable asset sync for the given asset group, then +// perform an initial sync with the federation for that group. +func (b *Book) SyncAssetGroup(ctx context.Context, + groupKey btcec.PublicKey) error { + + groupInfo := &asset.AssetGroup{ + GroupKey: &asset.GroupKey{ + GroupPubKey: groupKey, + }, + } + err := b.cfg.Syncer.EnableAssetSync(ctx, groupInfo) + if err != nil { + return fmt.Errorf("unable to enable asset sync: %w", err) + } + + // Use the AssetSyncer to query our universe federation for the asset. + return b.cfg.Syncer.SyncAssetInfo( + ctx, asset.NewSpecifierFromGroupKey(groupKey), + ) +} + // FetchAssetMetaByHash attempts to fetch an asset meta based on an asset hash. func (b *Book) FetchAssetMetaByHash(ctx context.Context, metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { @@ -296,7 +317,7 @@ func (b *Book) FetchAssetMetaForAsset(ctx context.Context, assetID.String()) // Use the AssetSyncer to query our universe federation for the asset. - err = b.cfg.Syncer.SyncAssetInfo(ctx, &assetID) + err = b.cfg.Syncer.SyncAssetInfo(ctx, asset.NewSpecifierFromId(assetID)) if err != nil { return nil, err } diff --git a/itest/channels_test.go b/itest/channels_test.go index 3cb423c52..9a3a91c23 100644 --- a/itest/channels_test.go +++ b/itest/channels_test.go @@ -61,7 +61,10 @@ func testChannelRPCs(t *harnessTest) { require.NoError(t.t, err) _, err = stream.Recv() - require.ErrorContains(t.t, err, "invalid vertex length of 0, want 33") + require.ErrorContains( + t.t, err, "destination node must be specified for keysend "+ + "payment", + ) // Now let's also try the invoice path, which should fail because we // don't have any asset channels with peers that we could ask for a diff --git a/rfq/manager.go b/rfq/manager.go index 7a257a58d..125b0230f 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -579,7 +579,7 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier, continue } - match, err := m.ChannelCompatible( + match, err := m.ChannelMatchesFully( ctxb, assetData, assetSpecifier, ) if err != nil { @@ -1019,11 +1019,11 @@ func (m *Manager) GetPriceDeviationPpm() uint64 { return m.cfg.AcceptPriceDeviationPpm } -// ChannelCompatible checks a channel's assets against an asset specifier. If -// the specifier is an asset ID, then all assets must be of that specific ID, -// if the specifier is a group key, then all assets in the channel must belong -// to that group. -func (m *Manager) ChannelCompatible(ctx context.Context, +// ChannelMatchesFully checks a channel's assets against an asset specifier. If +// the specifier is an asset ID, then all asset UTXOs must be of that specific +// ID, if the specifier is a group key, then all assets in the channel must +// belong to that group. +func (m *Manager) ChannelMatchesFully(ctx context.Context, jsonChannel rfqmsg.JsonAssetChannel, specifier asset.Specifier) (bool, error) { diff --git a/rpcserver.go b/rpcserver.go index 79267977e..b15c5ee74 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -7743,8 +7743,26 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest, "for keysend payment") } - var balances []*rfqmsg.AssetBalance + if len(req.PaymentRequest.Dest) == 0 { + return fmt.Errorf("destination node must be " + + "specified for keysend payment") + } + + dest, err := route.NewVertexFromBytes(req.PaymentRequest.Dest) + if err != nil { + return fmt.Errorf("error parsing destination node "+ + "pubkey: %w", err) + } + + // We check that we have the asset amount available in the + // channel. + _, err = r.rfqChannel(ctx, specifier, &dest, SendIntention) + if err != nil { + return fmt.Errorf("error finding asset channel to "+ + "use: %w", err) + } + var balances []*rfqmsg.AssetBalance switch { case specifier.HasId(): balances = []*rfqmsg.AssetBalance{ @@ -8395,12 +8413,29 @@ func (r *rpcServer) rfqChannel(ctx context.Context, specifier asset.Specifier, peerPubKey *route.Vertex, intention chanIntention) (*channelWithSpecifier, error) { - balances, err := r.computeChannelAssetBalance(ctx, specifier) + balances, haveGroupChans, err := r.computeCompatibleChannelAssetBalance( + ctx, specifier, + ) if err != nil { return nil, fmt.Errorf("error computing available asset "+ "channel balance: %w", err) } + // If the user uses the asset ID to specify what asset to use, that will + // not work for asset channels that have multiple UTXOs of grouped + // assets. The result wouldn't be deterministic anyway (meaning, it's + // not guaranteed that a specific asset ID is moved within a channel + // when an HTLC is sent, the allocation logic decides which actual UTXO + // is used). So we tell the user to use the group key instead, at least + // for channels that have multiple UTXOs of grouped assets. + if specifier.HasId() && len(balances) == 0 && haveGroupChans { + return nil, fmt.Errorf("no compatible asset channel found for "+ + "%s, make sure to use group key for grouped asset "+ + "channels", &specifier) + } + + // If the above special case didn't apply, it just means we don't have + // the specific asset in a channel that can be used. if len(balances) == 0 { return nil, fmt.Errorf("no asset channel balance found for %s", &specifier) @@ -8484,17 +8519,27 @@ type channelWithSpecifier struct { assetInfo rfqmsg.JsonAssetChannel } -// computeChannelAssetBalance computes the total local and remote balance for -// each asset channel that matches the provided asset specifier. -func (r *rpcServer) computeChannelAssetBalance(ctx context.Context, - specifier asset.Specifier) ([]channelWithSpecifier, error) { +// computeCompatibleChannelAssetBalance computes the total local and remote +// balance for each asset channel that matches the provided asset specifier. +// For a channel to match an asset specifier, all asset UTXOs in the channel +// must match the specifier. That means a channel is seen as _not_ compatible if +// it contains multiple asset IDs but the specifier only specifies one of them. +// The user should use the group key in that case instead. To help inform the +// user about this, the returned boolean value indicates if there are any active +// channels that have grouped assets. +func (r *rpcServer) computeCompatibleChannelAssetBalance(ctx context.Context, + specifier asset.Specifier) ([]channelWithSpecifier, bool, error) { activeChannels, err := r.cfg.Lnd.Client.ListChannels(ctx, true, false) if err != nil { - return nil, fmt.Errorf("unable to fetch channels: %w", err) + return nil, false, fmt.Errorf("unable to fetch channels: %w", + err) } - channels := make([]channelWithSpecifier, 0) + var ( + channels = make([]channelWithSpecifier, 0) + haveGroupedAssetChannels bool + ) for chanIdx := range activeChannels { openChan := activeChannels[chanIdx] if len(openChan.CustomChannelData) == 0 { @@ -8504,17 +8549,21 @@ func (r *rpcServer) computeChannelAssetBalance(ctx context.Context, var assetData rfqmsg.JsonAssetChannel err = json.Unmarshal(openChan.CustomChannelData, &assetData) if err != nil { - return nil, fmt.Errorf("unable to unmarshal asset "+ - "data: %w", err) + return nil, false, fmt.Errorf("unable to unmarshal "+ + "asset data: %w", err) + } + + if len(assetData.GroupKey) > 0 { + haveGroupedAssetChannels = true } // Check if the assets of this channel match the provided // specifier. - pass, err := r.cfg.RfqManager.ChannelCompatible( + pass, err := r.cfg.RfqManager.ChannelMatchesFully( ctx, assetData, specifier, ) if err != nil { - return nil, err + return nil, false, err } if pass { @@ -8526,7 +8575,7 @@ func (r *rpcServer) computeChannelAssetBalance(ctx context.Context, } } - return channels, nil + return channels, haveGroupedAssetChannels, nil } // getInboundPolicy returns the policy of the given channel that points towards @@ -8584,29 +8633,73 @@ func (r *rpcServer) assetInvoiceAmt(ctx context.Context, func (r *rpcServer) DecodeAssetPayReq(ctx context.Context, payReq *tchrpc.AssetPayReq) (*tchrpc.AssetPayReqResponse, error) { + tapdLog.Debugf("Decoding asset pay req, asset_id=%x, group_key=%x,"+ + "pay_req=%v", payReq.AssetId, payReq.GroupKey, + payReq.PayReqString) + if r.cfg.PriceOracle == nil { return nil, fmt.Errorf("price oracle is not set") } // First, we'll perform some basic input validation. + var assetID asset.ID switch { - case len(payReq.AssetId) == 0: - return nil, fmt.Errorf("asset ID must be specified") + case len(payReq.AssetId) == 0 && len(payReq.GroupKey) == 0: + return nil, fmt.Errorf("either asset ID or group key must be " + + "specified") + + case len(payReq.AssetId) != 0 && len(payReq.GroupKey) != 0: + return nil, fmt.Errorf("cannot set both asset ID and group key") - case len(payReq.AssetId) != 32: + case len(payReq.AssetId) != 0 && len(payReq.AssetId) != 32: return nil, fmt.Errorf("asset ID must be 32 bytes, "+ "was %d", len(payReq.AssetId)) + case len(payReq.GroupKey) != 0 && len(payReq.GroupKey) != 33 && + len(payReq.GroupKey) != 32: + + return nil, fmt.Errorf("group key must be 32 or 33 bytes, "+ + "was %d", len(payReq.GroupKey)) + case len(payReq.PayReqString) == 0: return nil, fmt.Errorf("payment request must be specified") } - var ( - resp tchrpc.AssetPayReqResponse - assetID asset.ID - ) + // We made sure that only one is set, so let's now use the asset ID + // or group key. + switch { + // The asset ID is easy, we can just copy the bytes. + case len(payReq.AssetId) != 0: + copy(assetID[:], payReq.AssetId) + + // The group key is a bit more involved. We first need to sync the asset + // group, then fetch the leaves that are associated with this group key. + // From there, we can look up the asset ID of one of the group's + // tranches. + case len(payReq.GroupKey) != 0: + var ( + groupKey *btcec.PublicKey + err error + ) + if len(payReq.GroupKey) == 32 { + groupKey, err = schnorr.ParsePubKey(payReq.GroupKey) + } else { + groupKey, err = btcec.ParsePubKey(payReq.GroupKey) + } + if err != nil { + return nil, fmt.Errorf("error parsing group "+ + "key: %w", err) + } + + assetID, err = r.syncAssetGroup(ctx, *groupKey) + if err != nil { + return nil, fmt.Errorf("error syncing asset group: %w", + err) + } - copy(assetID[:], payReq.AssetId) + tapdLog.Debugf("Resolved asset ID %v for group key %x", + assetID.String(), groupKey.SerializeCompressed()) + } // With the inputs validated, we'll first call out to lnd to decode the // payment request. @@ -8618,7 +8711,9 @@ func (r *rpcServer) DecodeAssetPayReq(ctx context.Context, return nil, fmt.Errorf("unable to fetch channel: %w", err) } - resp.PayReq = payReqInfo + resp := tchrpc.AssetPayReqResponse{ + PayReq: payReqInfo, + } // Next, we'll fetch the information for this asset ID through the addr // book. This'll automatically fetch the asset if needed. @@ -8628,12 +8723,17 @@ func (r *rpcServer) DecodeAssetPayReq(ctx context.Context, "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[:], + // The genesis info makes no sense in the case where we have decoded the + // invoice for a group key, since we just pick the first asset ID we + // found for a group key. + if len(payReq.GroupKey) == 0 { + 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 that @@ -8693,12 +8793,103 @@ func (r *rpcServer) DecodeAssetPayReq(ctx context.Context, return &resp, nil } +// syncAssetGroup checks if we already know any asset leaves associated with +// this group key. If not, it will sync the asset group and return the asset +// ID of the first leaf found. If there are no leaves found after syncing, an +// error is returned. +func (r *rpcServer) syncAssetGroup(ctx context.Context, + groupKey btcec.PublicKey) (asset.ID, error) { + + // We first check if we already know any asset leaves associated with + // this group key. + leaf := fn.NewRight[asset.ID](groupKey) + leaves, err := r.cfg.Multiverse.FetchLeaves( + ctx, []universe.MultiverseLeafDesc{leaf}, + universe.ProofTypeIssuance, + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error fetching leaves: %w", err) + } + + tapdLog.Tracef("Found %d leaves for group key %x", + len(leaves), groupKey.SerializeCompressed()) + + // If there are no leaves, then we need to sync the asset group. + if len(leaves) == 0 { + tapdLog.Debugf("No leaves found for group key %x, "+ + "syncing asset group", groupKey.SerializeCompressed()) + err = r.cfg.AddrBook.SyncAssetGroup(ctx, groupKey) + if err != nil { + return asset.ID{}, fmt.Errorf("error syncing asset "+ + "group: %w", err) + } + + // Now we can try again. + leaves, err = r.cfg.Multiverse.FetchLeaves( + ctx, []universe.MultiverseLeafDesc{leaf}, + universe.ProofTypeIssuance, + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error fetching leaves: "+ + "%w", err) + } + + tapdLog.Tracef("Found %d leaves for group key %x", + len(leaves), groupKey.SerializeCompressed()) + + if len(leaves) == 0 { + return asset.ID{}, fmt.Errorf("no asset leaves found "+ + "for group %x after sync", + groupKey.SerializeCompressed()) + } + } + + // Since we know we have at least one leaf, we can just take leaf ID and + // query the keys with it. We just need one so we can fetch the actual + // proof to find out the asset ID. We don't really care about the order, + // so we just use the natural database order. + leafID := leaves[0] + leafKeys, err := r.cfg.Multiverse.UniverseLeafKeys( + ctx, universe.UniverseLeafKeysQuery{ + Id: leafID.ID, + Limit: 1, + }, + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error fetching leaf keys: %w", + err) + } + + // We know we have a leaf, so this shouldn't happen. + if len(leafKeys) != 1 { + return asset.ID{}, fmt.Errorf("expected 1 leaf key, got %d", + len(leafKeys)) + } + + proofs, err := r.cfg.Multiverse.FetchProofLeaf( + ctx, leafID.ID, leafKeys[0], + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error fetching proof leaf: %w", + err) + } + + // We should have a proof for the asset ID now. + if len(proofs) != 1 { + return asset.ID{}, fmt.Errorf("expected 1 proof, got %d", + len(proofs)) + } + + // We can now extract the asset ID from the proof. + return proofs[0].Leaf.ID(), nil +} + // RegisterTransfer informs the daemon about a new inbound transfer that has // happened. This is used for interactive transfers where no TAP address is // involved and the recipient is aware of the transfer through an out-of-band // protocol but the daemon hasn't been informed about the completion of the // transfer. For this to work, the proof must already be in the recipient's -// local universe (e.g. through the use of the universerpc.ImportProof RPC or +// local universe (e.g. through the use of the universerpc.InsertProof RPC or // the universe proof courier and universe sync mechanisms) and this call // simply instructs the daemon to detect the transfer as an asset it owns. func (r *rpcServer) RegisterTransfer(ctx context.Context, diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index e2fc9830e..3cac1f2be 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -417,7 +417,7 @@ type pendingAssetFunding struct { inputProofs []*proof.Proof - feeRate chainfee.SatPerVByte + feeRate chainfee.SatPerKWeight lockedInputs []wire.OutPoint @@ -1280,7 +1280,7 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, // This'll add yet another output (lnd's change output) to the // template. finalFundedPsbt, err := f.fundPsbt( - ctx, fundingPsbt, fundingState.feeRate.FeePerKWeight(), + ctx, fundingPsbt, fundingState.feeRate, ) if err != nil { return nil, fmt.Errorf("unable to fund PSBT: %w", err) @@ -1619,7 +1619,15 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, return fmt.Errorf("unable to establish min_relay_fee: %w", err) } - if fundReq.FeeRate.FeePerKWeight() < minRelayFee { + + // If the user specifies 1 sat/vByte, that result in 253 sat/kw, which + // is exactly 3 sat/kw lower than the default min relay fee. So we need + // to make sure we don't allow that. + feeRate := fundReq.FeeRate.FeePerKWeight() + if feeRate == chainfee.AbsoluteFeePerKwFloor { + feeRate = chainfee.FeePerKwFloor + } + if feeRate < minRelayFee { return fmt.Errorf("fee rate %v too low, min_relay_fee: %v", fundReq.FeeRate.FeePerKWeight(), minRelayFee) } @@ -1638,7 +1646,7 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, initiator: true, amt: fundReq.AssetAmount, pushAmt: fundReq.PushAmount, - feeRate: fundReq.FeeRate, + feeRate: feeRate, fundingAckChan: make(chan bool, 1), fundingFinalizedSignal: make(chan struct{}), } diff --git a/tapgarden/mock.go b/tapgarden/mock.go index 495d941e4..1326eec33 100644 --- a/tapgarden/mock.go +++ b/tapgarden/mock.go @@ -824,13 +824,13 @@ func (m *MockAssetSyncer) FetchAsset(id asset.ID) (*asset.AssetGroup, error) { } func (m *MockAssetSyncer) SyncAssetInfo(_ context.Context, - id *asset.ID) error { + s asset.Specifier) error { - if id == nil { + if !s.HasId() { return fmt.Errorf("no asset ID provided") } - _, err := m.FetchAsset(*id) + _, err := m.FetchAsset(*s.UnwrapIdToPtr()) return err } diff --git a/taprpc/tapchannelrpc/tapchannel.pb.go b/taprpc/tapchannelrpc/tapchannel.pb.go index cdf914a31..dc0298309 100644 --- a/taprpc/tapchannelrpc/tapchannel.pb.go +++ b/taprpc/tapchannelrpc/tapchannel.pb.go @@ -791,10 +791,14 @@ type AssetPayReq struct { unknownFields protoimpl.UnknownFields // The asset ID that will be used to resolve the invoice's satoshi amount. + // Mutually exclusive to group_key. AssetId []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // The normal LN invoice that whose amount will be mapped to units of the // asset ID. PayReqString string `protobuf:"bytes,2,opt,name=pay_req_string,json=payReqString,proto3" json:"pay_req_string,omitempty"` + // The group key that will be used to resolve the invoice's satoshi amount. + // Mutually exclusive to asset_id. + GroupKey []byte `protobuf:"bytes,3,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` } func (x *AssetPayReq) Reset() { @@ -843,6 +847,13 @@ func (x *AssetPayReq) GetPayReqString() string { return "" } +func (x *AssetPayReq) GetGroupKey() []byte { + if x != nil { + return x.GroupKey + } + return nil +} + type AssetPayReqResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -855,7 +866,9 @@ type AssetPayReqResponse struct { // The group the asset ID belong to, if applicable. AssetGroup *taprpc.AssetGroup `protobuf:"bytes,3,opt,name=asset_group,json=assetGroup,proto3" json:"asset_group,omitempty"` // Genesis information for the asset ID which includes the meta hash, and - // asset ID. + // asset ID. This is only set if the payment request was decoded with an + // asset ID and not with a group key (since a group can contain assets from + // different minting events or genesis infos). GenesisInfo *taprpc.GenesisInfo `protobuf:"bytes,4,opt,name=genesis_info,json=genesisInfo,proto3" json:"genesis_info,omitempty"` // The normal decoded payment request. PayReq *lnrpc.PayReq `protobuf:"bytes,5,opt,name=pay_req,json=payReq,proto3" json:"pay_req,omitempty"` @@ -1049,63 +1062,65 @@ var file_tapchannelrpc_tapchannel_proto_rawDesc = []byte{ 0x63, 0x65, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x4e, 0x0a, 0x0b, 0x41, 0x73, 0x73, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x6b, 0x0a, 0x0b, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x61, 0x79, - 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x8e, 0x02, 0x0a, 0x13, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x5f, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x33, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x74, 0x61, 0x70, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x0c, 0x67, 0x65, - 0x6e, 0x65, 0x73, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, - 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x26, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x32, 0xda, 0x03, 0x0a, 0x14, 0x54, - 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x73, 0x12, 0x54, 0x0a, 0x0b, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x12, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, + 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x22, 0x8e, 0x02, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x61, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x12, 0x33, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x61, 0x73, 0x73, + 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x36, 0x0a, 0x0c, 0x67, 0x65, 0x6e, 0x65, 0x73, + 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x0b, 0x67, 0x65, 0x6e, 0x65, 0x73, 0x69, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x26, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, + 0x06, 0x70, 0x61, 0x79, 0x52, 0x65, 0x71, 0x32, 0xda, 0x03, 0x0a, 0x14, 0x54, 0x61, 0x70, 0x72, + 0x6f, 0x6f, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, + 0x12, 0x54, 0x0a, 0x0b, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, + 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, + 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x45, 0x6e, 0x63, - 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, - 0x12, 0x29, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, - 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x61, - 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x63, 0x6f, - 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x29, 0x2e, + 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, + 0x63, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, - 0x51, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x20, 0x2e, - 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, - 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x21, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x53, 0x0a, 0x11, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x1a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, - 0x52, 0x65, 0x71, 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, - 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x0a, + 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x20, 0x2e, 0x74, 0x61, 0x70, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x74, + 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, + 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x53, 0x0a, 0x11, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, + 0x79, 0x52, 0x65, 0x71, 0x12, 0x1a, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, + 0x1a, 0x22, 0x2e, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, + 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, + 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x61, 0x70, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/taprpc/tapchannelrpc/tapchannel.proto b/taprpc/tapchannelrpc/tapchannel.proto index 210488059..d665afa47 100644 --- a/taprpc/tapchannelrpc/tapchannel.proto +++ b/taprpc/tapchannelrpc/tapchannel.proto @@ -10,7 +10,7 @@ import "routerrpc/router.proto"; import "taprootassets.proto"; service TaprootAssetChannels { - /* + /* litcli: `ln fundchannel` FundChannel initiates the channel funding negotiation with a peer for the creation of a channel that contains a specified amount of a given asset. */ @@ -26,7 +26,7 @@ service TaprootAssetChannels { rpc EncodeCustomRecords (EncodeCustomRecordsRequest) returns (EncodeCustomRecordsResponse); - /* + /* litcli: `ln sendpayment` SendPayment is a wrapper around lnd's routerrpc.SendPaymentV2 RPC method with asset specific parameters. It allows RPC users to send asset keysend payments (direct payments) or payments to an invoice with a specified asset @@ -34,17 +34,17 @@ service TaprootAssetChannels { */ rpc SendPayment (SendPaymentRequest) returns (stream SendPaymentResponse); - /* + /* litcli: `ln addinvoice` 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. */ rpc AddInvoice (AddInvoiceRequest) returns (AddInvoiceResponse); - /* + /* litcli: `ln decodeassetinvoice` DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an - asset ID and returns the invoice amount expressed in asset units along side - the normal information. + asset ID or group key and returns the invoice amount expressed in asset + units along side the normal information. */ rpc DecodeAssetPayReq (AssetPayReq) returns (AssetPayReqResponse); } @@ -220,11 +220,16 @@ message AddInvoiceResponse { message AssetPayReq { // The asset ID that will be used to resolve the invoice's satoshi amount. + // Mutually exclusive to group_key. bytes asset_id = 1; // The normal LN invoice that whose amount will be mapped to units of the // asset ID. string pay_req_string = 2; + + // The group key that will be used to resolve the invoice's satoshi amount. + // Mutually exclusive to asset_id. + bytes group_key = 3; } message AssetPayReqResponse { @@ -238,7 +243,9 @@ message AssetPayReqResponse { taprpc.AssetGroup asset_group = 3; // Genesis information for the asset ID which includes the meta hash, and - // asset ID. + // asset ID. This is only set if the payment request was decoded with an + // asset ID and not with a group key (since a group can contain assets from + // different minting events or genesis infos). taprpc.GenesisInfo genesis_info = 4; // The normal decoded payment request. diff --git a/taprpc/tapchannelrpc/tapchannel.swagger.json b/taprpc/tapchannelrpc/tapchannel.swagger.json index 405385e01..fb5f4ed59 100644 --- a/taprpc/tapchannelrpc/tapchannel.swagger.json +++ b/taprpc/tapchannelrpc/tapchannel.swagger.json @@ -51,7 +51,7 @@ }, "/v1/taproot-assets/channels/fund": { "post": { - "summary": "FundChannel initiates the channel funding negotiation with a peer for the\ncreation of a channel that contains a specified amount of a given asset.", + "summary": "litcli: `ln fundchannel`\nFundChannel initiates the channel funding negotiation with a peer for the\ncreation of a channel that contains a specified amount of a given asset.", "operationId": "TaprootAssetChannels_FundChannel", "responses": { "200": { @@ -84,7 +84,7 @@ }, "/v1/taproot-assets/channels/invoice": { "post": { - "summary": "AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset\nspecific parameters. It allows RPC users to create invoices that correspond\nto the specified asset amount.", + "summary": "litcli: `ln addinvoice`\nAddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset\nspecific parameters. It allows RPC users to create invoices that correspond\nto the specified asset amount.", "operationId": "TaprootAssetChannels_AddInvoice", "responses": { "200": { @@ -117,7 +117,7 @@ }, "/v1/taproot-assets/channels/invoice/decode": { "post": { - "summary": "DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an\nasset ID and returns the invoice amount expressed in asset units along side\nthe normal information.", + "summary": "litcli: `ln decodeassetinvoice`\nDecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an\nasset ID or group key and returns the invoice amount expressed in asset\nunits along side the normal information.", "operationId": "TaprootAssetChannels_DecodeAssetPayReq", "responses": { "200": { @@ -150,7 +150,7 @@ }, "/v1/taproot-assets/channels/send-payment": { "post": { - "summary": "SendPayment is a wrapper around lnd's routerrpc.SendPaymentV2 RPC method\nwith asset specific parameters. It allows RPC users to send asset keysend\npayments (direct payments) or payments to an invoice with a specified asset\namount.", + "summary": "litcli: `ln sendpayment`\nSendPayment is a wrapper around lnd's routerrpc.SendPaymentV2 RPC method\nwith asset specific parameters. It allows RPC users to send asset keysend\npayments (direct payments) or payments to an invoice with a specified asset\namount.", "operationId": "TaprootAssetChannels_SendPayment", "responses": { "200": { @@ -1539,11 +1539,16 @@ "asset_id": { "type": "string", "format": "byte", - "description": "The asset ID that will be used to resolve the invoice's satoshi amount." + "description": "The asset ID that will be used to resolve the invoice's satoshi amount.\nMutually exclusive to group_key." }, "pay_req_string": { "type": "string", "description": "The normal LN invoice that whose amount will be mapped to units of the\nasset ID." + }, + "group_key": { + "type": "string", + "format": "byte", + "description": "The group key that will be used to resolve the invoice's satoshi amount.\nMutually exclusive to asset_id." } } }, @@ -1565,7 +1570,7 @@ }, "genesis_info": { "$ref": "#/definitions/taprpcGenesisInfo", - "description": "Genesis information for the asset ID which includes the meta hash, and\nasset ID." + "description": "Genesis information for the asset ID which includes the meta hash, and\nasset ID. This is only set if the payment request was decoded with an\nasset ID and not with a group key (since a group can contain assets from\ndifferent minting events or genesis infos)." }, "pay_req": { "$ref": "#/definitions/lnrpcPayReq", diff --git a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go index 1da8f92e8..cffa39ab8 100644 --- a/taprpc/tapchannelrpc/tapchannel_grpc.pb.go +++ b/taprpc/tapchannelrpc/tapchannel_grpc.pb.go @@ -18,6 +18,7 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TaprootAssetChannelsClient interface { + // litcli: `ln fundchannel` // FundChannel initiates the channel funding negotiation with a peer for the // creation of a channel that contains a specified amount of a given asset. FundChannel(ctx context.Context, in *FundChannelRequest, opts ...grpc.CallOption) (*FundChannelResponse, error) @@ -27,18 +28,21 @@ type TaprootAssetChannelsClient interface { // does not perform any checks on the data provided, other than pure format // validation. EncodeCustomRecords(ctx context.Context, in *EncodeCustomRecordsRequest, opts ...grpc.CallOption) (*EncodeCustomRecordsResponse, error) + // litcli: `ln sendpayment` // SendPayment is a wrapper around lnd's routerrpc.SendPaymentV2 RPC method // with asset specific parameters. It allows RPC users to send asset keysend // payments (direct payments) or payments to an invoice with a specified asset // amount. SendPayment(ctx context.Context, in *SendPaymentRequest, opts ...grpc.CallOption) (TaprootAssetChannels_SendPaymentClient, error) + // litcli: `ln addinvoice` // 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. AddInvoice(ctx context.Context, in *AddInvoiceRequest, opts ...grpc.CallOption) (*AddInvoiceResponse, error) + // litcli: `ln decodeassetinvoice` // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an - // asset ID and returns the invoice amount expressed in asset units along side - // the normal information. + // asset ID or group key and returns the invoice amount expressed in asset + // units along side the normal information. DecodeAssetPayReq(ctx context.Context, in *AssetPayReq, opts ...grpc.CallOption) (*AssetPayReqResponse, error) } @@ -122,6 +126,7 @@ func (c *taprootAssetChannelsClient) DecodeAssetPayReq(ctx context.Context, in * // All implementations must embed UnimplementedTaprootAssetChannelsServer // for forward compatibility type TaprootAssetChannelsServer interface { + // litcli: `ln fundchannel` // FundChannel initiates the channel funding negotiation with a peer for the // creation of a channel that contains a specified amount of a given asset. FundChannel(context.Context, *FundChannelRequest) (*FundChannelResponse, error) @@ -131,18 +136,21 @@ type TaprootAssetChannelsServer interface { // does not perform any checks on the data provided, other than pure format // validation. EncodeCustomRecords(context.Context, *EncodeCustomRecordsRequest) (*EncodeCustomRecordsResponse, error) + // litcli: `ln sendpayment` // SendPayment is a wrapper around lnd's routerrpc.SendPaymentV2 RPC method // with asset specific parameters. It allows RPC users to send asset keysend // payments (direct payments) or payments to an invoice with a specified asset // amount. SendPayment(*SendPaymentRequest, TaprootAssetChannels_SendPaymentServer) error + // litcli: `ln addinvoice` // 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. AddInvoice(context.Context, *AddInvoiceRequest) (*AddInvoiceResponse, error) + // litcli: `ln decodeassetinvoice` // DecodeAssetPayReq is similar to lnd's lnrpc.DecodePayReq, but it accepts an - // asset ID and returns the invoice amount expressed in asset units along side - // the normal information. + // asset ID or group key and returns the invoice amount expressed in asset + // units along side the normal information. DecodeAssetPayReq(context.Context, *AssetPayReq) (*AssetPayReqResponse, error) mustEmbedUnimplementedTaprootAssetChannelsServer() } diff --git a/taprpc/taprootassets.proto b/taprpc/taprootassets.proto index fb86cd661..979718433 100644 --- a/taprpc/taprootassets.proto +++ b/taprpc/taprootassets.proto @@ -149,7 +149,7 @@ service TaprootAssets { involved and the recipient is aware of the transfer through an out-of-band protocol but the daemon hasn't been informed about the completion of the transfer. For this to work, the proof must already be in the recipient's - local universe (e.g. through the use of the universerpc.ImportProof RPC or + local universe (e.g. through the use of the universerpc.InsertProof RPC or the universe proof courier and universe sync mechanisms) and this call simply instructs the daemon to detect the transfer as an asset it owns. */ diff --git a/taprpc/taprootassets.swagger.json b/taprpc/taprootassets.swagger.json index 835ab6ea6..59acbe663 100644 --- a/taprpc/taprootassets.swagger.json +++ b/taprpc/taprootassets.swagger.json @@ -591,7 +591,7 @@ }, "/v1/taproot-assets/assets/transfers/register": { "post": { - "summary": "RegisterTransfer informs the daemon about a new inbound transfer that has\nhappened. This is used for interactive transfers where no TAP address is\ninvolved and the recipient is aware of the transfer through an out-of-band\nprotocol but the daemon hasn't been informed about the completion of the\ntransfer. For this to work, the proof must already be in the recipient's\nlocal universe (e.g. through the use of the universerpc.ImportProof RPC or\nthe universe proof courier and universe sync mechanisms) and this call\nsimply instructs the daemon to detect the transfer as an asset it owns.", + "summary": "RegisterTransfer informs the daemon about a new inbound transfer that has\nhappened. This is used for interactive transfers where no TAP address is\ninvolved and the recipient is aware of the transfer through an out-of-band\nprotocol but the daemon hasn't been informed about the completion of the\ntransfer. For this to work, the proof must already be in the recipient's\nlocal universe (e.g. through the use of the universerpc.InsertProof RPC or\nthe universe proof courier and universe sync mechanisms) and this call\nsimply instructs the daemon to detect the transfer as an asset it owns.", "operationId": "TaprootAssets_RegisterTransfer", "responses": { "200": { diff --git a/taprpc/taprootassets_grpc.pb.go b/taprpc/taprootassets_grpc.pb.go index 3b8b49ba1..e796ff597 100644 --- a/taprpc/taprootassets_grpc.pb.go +++ b/taprpc/taprootassets_grpc.pb.go @@ -114,7 +114,7 @@ type TaprootAssetsClient interface { // involved and the recipient is aware of the transfer through an out-of-band // protocol but the daemon hasn't been informed about the completion of the // transfer. For this to work, the proof must already be in the recipient's - // local universe (e.g. through the use of the universerpc.ImportProof RPC or + // local universe (e.g. through the use of the universerpc.InsertProof RPC or // the universe proof courier and universe sync mechanisms) and this call // simply instructs the daemon to detect the transfer as an asset it owns. RegisterTransfer(ctx context.Context, in *RegisterTransferRequest, opts ...grpc.CallOption) (*RegisterTransferResponse, error) @@ -481,7 +481,7 @@ type TaprootAssetsServer interface { // involved and the recipient is aware of the transfer through an out-of-band // protocol but the daemon hasn't been informed about the completion of the // transfer. For this to work, the proof must already be in the recipient's - // local universe (e.g. through the use of the universerpc.ImportProof RPC or + // local universe (e.g. through the use of the universerpc.InsertProof RPC or // the universe proof courier and universe sync mechanisms) and this call // simply instructs the daemon to detect the transfer as an asset it owns. RegisterTransfer(context.Context, *RegisterTransferRequest) (*RegisterTransferResponse, error) diff --git a/universe/auto_syncer.go b/universe/auto_syncer.go index 074fa6a4f..670769dfb 100644 --- a/universe/auto_syncer.go +++ b/universe/auto_syncer.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" @@ -805,14 +806,27 @@ func (f *FederationEnvoy) tryFetchServers() ([]ServerAddr, error) { } // SyncAssetInfo queries the universes in our federation for genesis and asset -// group information about the given asset ID. +// group information about the given asset. func (f *FederationEnvoy) SyncAssetInfo(ctx context.Context, - assetID *asset.ID) error { + specifier asset.Specifier) error { - if assetID == nil { - return fmt.Errorf("no asset ID provided") + uniID := Identifier{ + ProofType: ProofTypeIssuance, } + // One of asset ID or group key must be set, but not both. + if specifier.HasId() == specifier.HasGroupPubKey() { + return fmt.Errorf("must set either asset ID or group key for " + + "asset sync") + } + + specifier.WhenId(func(id asset.ID) { + uniID.AssetID = id + }) + specifier.WhenGroupPubKey(func(groupKey btcec.PublicKey) { + uniID.GroupKey = &groupKey + }) + // Fetch the set of universe servers in our federation. fedServers, err := f.tryFetchServers() if err != nil { @@ -820,16 +834,14 @@ func (f *FederationEnvoy) SyncAssetInfo(ctx context.Context, } assetConfig := FedUniSyncConfig{ - UniverseID: Identifier{ - AssetID: *assetID, - ProofType: ProofTypeIssuance, - }, + UniverseID: uniID, AllowSyncInsert: true, AllowSyncExport: false, } fullConfig := SyncConfigs{ UniSyncConfigs: []*FedUniSyncConfig{&assetConfig}, } + // We'll sync with Universe servers in parallel and collect the diffs // from any successful syncs. There can only be one diff per server, as // we're only syncing one universe root. @@ -846,8 +858,8 @@ func (f *FederationEnvoy) SyncAssetInfo(ctx context.Context, // Sync failures are expected from Universe servers that do not // have a relevant universe root. if err != nil { - log.Warnf("Asset lookup failed: asset_id=%v, "+ - "remote_server=%v: %v", assetID.String(), + log.Warnf("Asset lookup failed: id=%v, "+ + "remote_server=%v: %v", uniID.String(), addr.HostStr(), err) // We don't want to abort syncing here, as this might @@ -863,8 +875,8 @@ func (f *FederationEnvoy) SyncAssetInfo(ctx context.Context, if len(syncDiff) != 1 { log.Warnf("Unexpected number of sync diffs "+ "when looking up asset: num_diffs=%d, "+ - "asset_id=%v, remote_server=%v", - len(syncDiff), assetID.String(), + "id=%v, remote_server=%v", + len(syncDiff), uniID.String(), addr.HostStr()) // We don't want to abort syncing here, as this @@ -891,12 +903,11 @@ func (f *FederationEnvoy) SyncAssetInfo(ctx context.Context, syncDiffs := fn.Collect(returnedSyncDiffs) log.Infof("Synced new Universe leaves for asset %v, diff_size=%v", - assetID.String(), len(syncDiffs)) + uniID.String(), len(syncDiffs)) - // TODO(jhb): Log successful syncs? if len(syncDiffs) == 0 { return fmt.Errorf("asset lookup failed for asset: %v", - assetID.String()) + uniID.String()) } return nil