Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ListBurns RPC #1178

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing wrapped error.

}

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
77 changes: 76 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,16 +130,31 @@ 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)

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)

burnRespJSON, err := formatProtoJSON(burnResp)
require.NoError(t.t, err)
t.Logf("Got response from burning %d units: %v", burnAmt, burnRespJSON)
Expand Down Expand Up @@ -266,6 +282,35 @@ func testBurnAssets(t *harnessTest) {
})
require.NoError(t.t, err)

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, "")

burnRespJSON, err = formatProtoJSON(burnResp)
require.NoError(t.t, err)
t.Logf("Got response from burning units from grouped asset: %v",
Expand Down Expand Up @@ -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 Down
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
46 changes: 46 additions & 0 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ 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/tapdb/sqlc"
"github.com/lightninglabs/taproot-assets/tapfreighter"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tappsbt"
Expand Down Expand Up @@ -3312,12 +3314,56 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
}
}

// At this point everything completed correctly, so we log this burn in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, perhaps we should do this further in the pipeline? So in the same db transaction that we insert the transfer.

Otherwise, if we crash here, then the burn is never inserted in the db, and we exit in an inconsistent state.

// our db.
err = r.cfg.AssetStore.InsertBurn(
GeorgeTsagk marked this conversation as resolved.
Show resolved Hide resolved
ctx, parcel.AnchorTxHash, in.Note, assetID[:],
serializedGroupKey, in.AmountToBurn,
)
GeorgeTsagk marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

return &taprpc.BurnAssetResponse{
BurnTransfer: parcel,
BurnProof: burnProof,
}, 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, sqlc.QueryBurnsParams{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the alias tapdb.QueryBurnsFilters here.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re other comment, I think it would be useful to include the full transfer here, also maybe the raw tx itself?

}, nil
}

// marshalRpcBurn creates an instance of *taprpc.AssetBurn from the tapdb model.
func marshalRpcBurn(b *tapdb.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
Loading