Skip to content

Commit

Permalink
Merge pull request lightninglabs#1178 from GeorgeTsagk/track-burns
Browse files Browse the repository at this point in the history
Add `ListBurns` RPC
  • Loading branch information
guggero authored Nov 26, 2024
2 parents a8f399c + e9364e0 commit f35de11
Show file tree
Hide file tree
Showing 23 changed files with 1,471 additions and 141 deletions.
67 changes: 67 additions & 0 deletions cmd/tapcli/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var assetsCommands = []cli.Command{
listAssetBalancesCommand,
sendAssetsCommand,
burnAssetsCommand,
listBurnsCommand,
listTransfersCommand,
fetchMetaCommand,
},
Expand All @@ -52,6 +53,7 @@ var (
assetShowUnconfMintsName = "show_unconfirmed_mints"
assetGroupKeyName = "group_key"
assetGroupAnchorName = "group_anchor"
anchorTxidName = "anchor_txid"
batchKeyName = "batch_key"
groupByGroupName = "by_group"
assetIDName = "asset_id"
Expand Down Expand Up @@ -858,6 +860,71 @@ func burnAssets(ctx *cli.Context) error {
return nil
}

var listBurnsCommand = cli.Command{
Name: "listburns",
Usage: "list burnt assets",
Description: `
List assets that have been burned by this daemon. These are assets that
have been destroyed and are no longer spendable.
Some filters may be used to return more specific results.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: assetIDName,
Usage: "the asset ID of the burnt asset",
},
cli.StringFlag{
Name: assetGroupKeyName,
Usage: "the group key of the burnt asset",
},
cli.StringFlag{
Name: anchorTxidName,
Usage: "the txid of the transaction the burn was " +
"anchored to",
},
},
Action: listBurns,
}

func listBurns(ctx *cli.Context) error {
assetIDHex := ctx.String(assetIDName)
assetIDBytes, err := hex.DecodeString(assetIDHex)
if err != nil {
return fmt.Errorf("invalid asset ID: %w", err)
}

groupKeyHex := ctx.String(assetGroupKeyName)
groupKeyBytes, err := hex.DecodeString(groupKeyHex)
if err != nil {
return fmt.Errorf("invalid group key: %w", err)
}

anchorTxidStr := ctx.String(anchorTxidName)
anchorTxid, err := hex.DecodeString(anchorTxidStr)
if err != nil {
return fmt.Errorf("invalid anchor txid: %w", err)
}

ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()

resp, err := client.ListBurns(
ctxc, &taprpc.ListBurnsRequest{
AssetId: assetIDBytes,
TweakedGroupKey: groupKeyBytes,
AnchorTxid: anchorTxid,
},
)
if err != nil {
return fmt.Errorf("could not list burns: %w", err)
}

printRespJSON(resp)
return nil
}

var listTransfersCommand = cli.Command{
Name: "transfers",
ShortName: "t",
Expand Down
91 changes: 90 additions & 1 deletion itest/burn_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package itest

import (
"bytes"
"context"
"encoding/hex"

Expand Down Expand Up @@ -129,12 +130,17 @@ func testBurnAssets(t *harnessTest) {
// Test case 2: We'll now try to burn a small amount of assets, which
// should select the largest output, which is located alone in an anchor
// output.
const burnAmt = 100
const (
burnAmt = 100
burnNote = "blazeit"
)

burnResp, err := t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: simpleAssetID[:],
},
AmountToBurn: burnAmt,
Note: burnNote,
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.NoError(t.t, err)
Expand Down Expand Up @@ -169,6 +175,16 @@ func testBurnAssets(t *harnessTest) {
t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount-burnAmt,
)

burns, err := t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{})
require.NoError(t.t, err)

require.Len(t.t, burns.Burns, 1)
burn := burns.Burns[0]
require.Equal(t.t, uint64(burnAmt), burn.Amount)
require.Equal(t.t, burnResp.BurnTransfer.AnchorTxHash, burn.AnchorTxid)
require.Equal(t.t, burn.AssetId, simpleAssetID[:])
require.Equal(t.t, burn.Note, burnNote)

// The burned asset should be pruned from the tree when we next spend
// the anchor output it was in (together with the change). So let's test
// that we can successfully spend the change output.
Expand Down Expand Up @@ -280,6 +296,35 @@ func testBurnAssets(t *harnessTest) {
t.t, t.tapd, simpleGroupGen.AssetId, simpleGroup.Amount-burnAmt,
)

burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{})
require.NoError(t.t, err)

require.Len(t.t, burns.Burns, 4)
var groupBurn *taprpc.AssetBurn
for _, b := range burns.Burns {
if bytes.Equal(b.AssetId, simpleGroupGen.AssetId) {
groupBurn = b
}
}

// Keep track of the txhash of the anchor transaction that completed
// this transfer. This will be used later to query burns with a txhash
// filter.
groupBurnTxHash := burnResp.BurnTransfer.AnchorTxHash

require.Equal(t.t, uint64(burnAmt), groupBurn.Amount)
require.Equal(
t.t, burnResp.BurnTransfer.AnchorTxHash, groupBurn.AnchorTxid,
)

require.Equal(t.t, groupBurn.AssetId, simpleGroupGen.AssetId[:])
require.Equal(
t.t, groupBurn.TweakedGroupKey,
simpleGroup.AssetGroup.TweakedGroupKey,
)

require.Equal(t.t, groupBurn.Note, "")

// Test case 6: Burn the single unit of a grouped collectible. We start
// by making sure we still have the full balance before burning.
AssertBalanceByID(
Expand All @@ -305,6 +350,36 @@ func testBurnAssets(t *harnessTest) {
simpleGroupCollectGen.AssetId, []uint64{1}, 6, 7, 1, true,
)
AssertBalanceByID(t.t, t.tapd, simpleGroupCollectGen.AssetId, 0)

// We now perform some queries to test the filters of the ListBurns
// call.

// Fetch the burns related to the simple asset id, which should have a
// total of 2 burns (tc1 & tc4).
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
AssetId: simpleAssetGen.AssetId,
})
require.NoError(t.t, err)

require.Len(t.t, burns.Burns, 2)

// Fetch the burns related to the group key of the grouped asset in tc5.
// There should be 1 burn.
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
TweakedGroupKey: simpleGroup.AssetGroup.TweakedGroupKey,
})
require.NoError(t.t, err)

require.Len(t.t, burns.Burns, 1)

// Fetch the burns associated with the txhash of the burn in tc5. There
// should be 1 burn returned.
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
AnchorTxid: groupBurnTxHash,
})
require.NoError(t.t, err)

require.Len(t.t, burns.Burns, 1)
}

// testBurnGroupedAssets tests that some amount of an asset from an asset group
Expand All @@ -315,6 +390,7 @@ func testBurnGroupedAssets(t *harnessTest) {
miner = t.lndHarness.Miner().Client

firstMintReq = issuableAssets[0]
burnNote = "blazeit"
)

// We start off without any asset groups.
Expand Down Expand Up @@ -376,6 +452,7 @@ func testBurnGroupedAssets(t *harnessTest) {
AssetId: burnAssetID,
},
AmountToBurn: burnAmt,
Note: burnNote,
ConfirmationText: taprootassets.AssetBurnConfirmationText,
})
require.NoError(t.t, err)
Expand Down Expand Up @@ -414,4 +491,16 @@ func testBurnGroupedAssets(t *harnessTest) {
encodedGroupKey = hex.EncodeToString(assetGroupKey)
assetGroup = assetGroups.Groups[encodedGroupKey]
require.Len(t.t, assetGroup.Assets, 2)

burns, err := t.tapd.ListBurns(ctxb, &taprpc.ListBurnsRequest{
TweakedGroupKey: assetGroupKey,
})
require.NoError(t.t, err)
require.Len(t.t, burns.Burns, 1)

burn := burns.Burns[0]

require.Equal(t.t, burnAmt, burn.Amount)
require.Equal(t.t, burnNote, burn.Note)
require.Equal(t.t, assetGroupKey, burn.TweakedGroupKey)
}
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ var (
Entity: "assets",
Action: "write",
}},
"/taprpc.TaprootAssets/ListBurns": {{
Entity: "assets",
Action: "read",
}},
"/taprpc.TaprootAssets/FetchAssetMeta": {{
Entity: "assets",
Action: "read",
Expand Down
39 changes: 37 additions & 2 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightninglabs/taproot-assets/rpcperms"
"github.com/lightninglabs/taproot-assets/tapchannel"
"github.com/lightninglabs/taproot-assets/tapdb"
"github.com/lightninglabs/taproot-assets/tapfreighter"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tappsbt"
Expand Down Expand Up @@ -2275,7 +2276,7 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
}

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments),
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments, ""),
)
if err != nil {
return nil, fmt.Errorf("error requesting delivery: %w", err)
Expand Down Expand Up @@ -3290,7 +3291,7 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(
[]*tappsbt.VPacket{fundResp.VPacket},
fundResp.InputCommitments,
fundResp.InputCommitments, in.Note,
),
)
if err != nil {
Expand Down Expand Up @@ -3328,6 +3329,40 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
}, nil
}

// ListBurns returns a list of burnt assets. Some filters may be defined in the
// request to return more specific results.
func (r *rpcServer) ListBurns(ctx context.Context,
in *taprpc.ListBurnsRequest) (*taprpc.ListBurnsResponse, error) {

burns, err := r.cfg.AssetStore.QueryBurns(
ctx, tapdb.QueryBurnsFilters{
AssetID: in.AssetId,
GroupKey: in.TweakedGroupKey,
AnchorTxid: in.AnchorTxid,
},
)
if err != nil {
return nil, err
}

rpcBurns := fn.Map(burns, marshalRpcBurn)

return &taprpc.ListBurnsResponse{
Burns: rpcBurns,
}, nil
}

// marshalRpcBurn creates an instance of *taprpc.AssetBurn from the tapdb model.
func marshalRpcBurn(b *tapfreighter.AssetBurn) *taprpc.AssetBurn {
return &taprpc.AssetBurn{
Note: b.Note,
AssetId: b.AssetID,
TweakedGroupKey: b.GroupKey,
Amount: b.Amount,
AnchorTxid: b.AnchorTxid[:],
}
}

// marshalOutboundParcel turns a pending parcel into its RPC counterpart.
func marshalOutboundParcel(
parcel *tapfreighter.OutboundParcel) (*taprpc.AssetTransfer,
Expand Down
Loading

0 comments on commit f35de11

Please sign in to comment.