Skip to content

Commit f35de11

Browse files
authored
Merge pull request #1178 from GeorgeTsagk/track-burns
Add `ListBurns` RPC
2 parents a8f399c + e9364e0 commit f35de11

23 files changed

+1471
-141
lines changed

cmd/tapcli/assets.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var assetsCommands = []cli.Command{
2929
listAssetBalancesCommand,
3030
sendAssetsCommand,
3131
burnAssetsCommand,
32+
listBurnsCommand,
3233
listTransfersCommand,
3334
fetchMetaCommand,
3435
},
@@ -52,6 +53,7 @@ var (
5253
assetShowUnconfMintsName = "show_unconfirmed_mints"
5354
assetGroupKeyName = "group_key"
5455
assetGroupAnchorName = "group_anchor"
56+
anchorTxidName = "anchor_txid"
5557
batchKeyName = "batch_key"
5658
groupByGroupName = "by_group"
5759
assetIDName = "asset_id"
@@ -858,6 +860,71 @@ func burnAssets(ctx *cli.Context) error {
858860
return nil
859861
}
860862

863+
var listBurnsCommand = cli.Command{
864+
Name: "listburns",
865+
Usage: "list burnt assets",
866+
Description: `
867+
List assets that have been burned by this daemon. These are assets that
868+
have been destroyed and are no longer spendable.
869+
870+
Some filters may be used to return more specific results.
871+
`,
872+
Flags: []cli.Flag{
873+
cli.StringFlag{
874+
Name: assetIDName,
875+
Usage: "the asset ID of the burnt asset",
876+
},
877+
cli.StringFlag{
878+
Name: assetGroupKeyName,
879+
Usage: "the group key of the burnt asset",
880+
},
881+
cli.StringFlag{
882+
Name: anchorTxidName,
883+
Usage: "the txid of the transaction the burn was " +
884+
"anchored to",
885+
},
886+
},
887+
Action: listBurns,
888+
}
889+
890+
func listBurns(ctx *cli.Context) error {
891+
assetIDHex := ctx.String(assetIDName)
892+
assetIDBytes, err := hex.DecodeString(assetIDHex)
893+
if err != nil {
894+
return fmt.Errorf("invalid asset ID: %w", err)
895+
}
896+
897+
groupKeyHex := ctx.String(assetGroupKeyName)
898+
groupKeyBytes, err := hex.DecodeString(groupKeyHex)
899+
if err != nil {
900+
return fmt.Errorf("invalid group key: %w", err)
901+
}
902+
903+
anchorTxidStr := ctx.String(anchorTxidName)
904+
anchorTxid, err := hex.DecodeString(anchorTxidStr)
905+
if err != nil {
906+
return fmt.Errorf("invalid anchor txid: %w", err)
907+
}
908+
909+
ctxc := getContext()
910+
client, cleanUp := getClient(ctx)
911+
defer cleanUp()
912+
913+
resp, err := client.ListBurns(
914+
ctxc, &taprpc.ListBurnsRequest{
915+
AssetId: assetIDBytes,
916+
TweakedGroupKey: groupKeyBytes,
917+
AnchorTxid: anchorTxid,
918+
},
919+
)
920+
if err != nil {
921+
return fmt.Errorf("could not list burns: %w", err)
922+
}
923+
924+
printRespJSON(resp)
925+
return nil
926+
}
927+
861928
var listTransfersCommand = cli.Command{
862929
Name: "transfers",
863930
ShortName: "t",

itest/burn_test.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package itest
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/hex"
67

@@ -129,12 +130,17 @@ func testBurnAssets(t *harnessTest) {
129130
// Test case 2: We'll now try to burn a small amount of assets, which
130131
// should select the largest output, which is located alone in an anchor
131132
// output.
132-
const burnAmt = 100
133+
const (
134+
burnAmt = 100
135+
burnNote = "blazeit"
136+
)
137+
133138
burnResp, err := t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{
134139
Asset: &taprpc.BurnAssetRequest_AssetId{
135140
AssetId: simpleAssetID[:],
136141
},
137142
AmountToBurn: burnAmt,
143+
Note: burnNote,
138144
ConfirmationText: taprootassets.AssetBurnConfirmationText,
139145
})
140146
require.NoError(t.t, err)
@@ -169,6 +175,16 @@ func testBurnAssets(t *harnessTest) {
169175
t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount-burnAmt,
170176
)
171177

178+
burns, err := t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{})
179+
require.NoError(t.t, err)
180+
181+
require.Len(t.t, burns.Burns, 1)
182+
burn := burns.Burns[0]
183+
require.Equal(t.t, uint64(burnAmt), burn.Amount)
184+
require.Equal(t.t, burnResp.BurnTransfer.AnchorTxHash, burn.AnchorTxid)
185+
require.Equal(t.t, burn.AssetId, simpleAssetID[:])
186+
require.Equal(t.t, burn.Note, burnNote)
187+
172188
// The burned asset should be pruned from the tree when we next spend
173189
// the anchor output it was in (together with the change). So let's test
174190
// that we can successfully spend the change output.
@@ -280,6 +296,35 @@ func testBurnAssets(t *harnessTest) {
280296
t.t, t.tapd, simpleGroupGen.AssetId, simpleGroup.Amount-burnAmt,
281297
)
282298

299+
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{})
300+
require.NoError(t.t, err)
301+
302+
require.Len(t.t, burns.Burns, 4)
303+
var groupBurn *taprpc.AssetBurn
304+
for _, b := range burns.Burns {
305+
if bytes.Equal(b.AssetId, simpleGroupGen.AssetId) {
306+
groupBurn = b
307+
}
308+
}
309+
310+
// Keep track of the txhash of the anchor transaction that completed
311+
// this transfer. This will be used later to query burns with a txhash
312+
// filter.
313+
groupBurnTxHash := burnResp.BurnTransfer.AnchorTxHash
314+
315+
require.Equal(t.t, uint64(burnAmt), groupBurn.Amount)
316+
require.Equal(
317+
t.t, burnResp.BurnTransfer.AnchorTxHash, groupBurn.AnchorTxid,
318+
)
319+
320+
require.Equal(t.t, groupBurn.AssetId, simpleGroupGen.AssetId[:])
321+
require.Equal(
322+
t.t, groupBurn.TweakedGroupKey,
323+
simpleGroup.AssetGroup.TweakedGroupKey,
324+
)
325+
326+
require.Equal(t.t, groupBurn.Note, "")
327+
283328
// Test case 6: Burn the single unit of a grouped collectible. We start
284329
// by making sure we still have the full balance before burning.
285330
AssertBalanceByID(
@@ -305,6 +350,36 @@ func testBurnAssets(t *harnessTest) {
305350
simpleGroupCollectGen.AssetId, []uint64{1}, 6, 7, 1, true,
306351
)
307352
AssertBalanceByID(t.t, t.tapd, simpleGroupCollectGen.AssetId, 0)
353+
354+
// We now perform some queries to test the filters of the ListBurns
355+
// call.
356+
357+
// Fetch the burns related to the simple asset id, which should have a
358+
// total of 2 burns (tc1 & tc4).
359+
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
360+
AssetId: simpleAssetGen.AssetId,
361+
})
362+
require.NoError(t.t, err)
363+
364+
require.Len(t.t, burns.Burns, 2)
365+
366+
// Fetch the burns related to the group key of the grouped asset in tc5.
367+
// There should be 1 burn.
368+
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
369+
TweakedGroupKey: simpleGroup.AssetGroup.TweakedGroupKey,
370+
})
371+
require.NoError(t.t, err)
372+
373+
require.Len(t.t, burns.Burns, 1)
374+
375+
// Fetch the burns associated with the txhash of the burn in tc5. There
376+
// should be 1 burn returned.
377+
burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{
378+
AnchorTxid: groupBurnTxHash,
379+
})
380+
require.NoError(t.t, err)
381+
382+
require.Len(t.t, burns.Burns, 1)
308383
}
309384

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

317392
firstMintReq = issuableAssets[0]
393+
burnNote = "blazeit"
318394
)
319395

320396
// We start off without any asset groups.
@@ -376,6 +452,7 @@ func testBurnGroupedAssets(t *harnessTest) {
376452
AssetId: burnAssetID,
377453
},
378454
AmountToBurn: burnAmt,
455+
Note: burnNote,
379456
ConfirmationText: taprootassets.AssetBurnConfirmationText,
380457
})
381458
require.NoError(t.t, err)
@@ -414,4 +491,16 @@ func testBurnGroupedAssets(t *harnessTest) {
414491
encodedGroupKey = hex.EncodeToString(assetGroupKey)
415492
assetGroup = assetGroups.Groups[encodedGroupKey]
416493
require.Len(t.t, assetGroup.Assets, 2)
494+
495+
burns, err := t.tapd.ListBurns(ctxb, &taprpc.ListBurnsRequest{
496+
TweakedGroupKey: assetGroupKey,
497+
})
498+
require.NoError(t.t, err)
499+
require.Len(t.t, burns.Burns, 1)
500+
501+
burn := burns.Burns[0]
502+
503+
require.Equal(t.t, burnAmt, burn.Amount)
504+
require.Equal(t.t, burnNote, burn.Note)
505+
require.Equal(t.t, assetGroupKey, burn.TweakedGroupKey)
417506
}

perms/perms.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ var (
7676
Entity: "assets",
7777
Action: "write",
7878
}},
79+
"/taprpc.TaprootAssets/ListBurns": {{
80+
Entity: "assets",
81+
Action: "read",
82+
}},
7983
"/taprpc.TaprootAssets/FetchAssetMeta": {{
8084
Entity: "assets",
8185
Action: "read",

rpcserver.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/lightninglabs/taproot-assets/rfqmsg"
3838
"github.com/lightninglabs/taproot-assets/rpcperms"
3939
"github.com/lightninglabs/taproot-assets/tapchannel"
40+
"github.com/lightninglabs/taproot-assets/tapdb"
4041
"github.com/lightninglabs/taproot-assets/tapfreighter"
4142
"github.com/lightninglabs/taproot-assets/tapgarden"
4243
"github.com/lightninglabs/taproot-assets/tappsbt"
@@ -2275,7 +2276,7 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
22752276
}
22762277

22772278
resp, err := r.cfg.ChainPorter.RequestShipment(
2278-
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments),
2279+
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments, ""),
22792280
)
22802281
if err != nil {
22812282
return nil, fmt.Errorf("error requesting delivery: %w", err)
@@ -3290,7 +3291,7 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
32903291
resp, err := r.cfg.ChainPorter.RequestShipment(
32913292
tapfreighter.NewPreSignedParcel(
32923293
[]*tappsbt.VPacket{fundResp.VPacket},
3293-
fundResp.InputCommitments,
3294+
fundResp.InputCommitments, in.Note,
32943295
),
32953296
)
32963297
if err != nil {
@@ -3328,6 +3329,40 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
33283329
}, nil
33293330
}
33303331

3332+
// ListBurns returns a list of burnt assets. Some filters may be defined in the
3333+
// request to return more specific results.
3334+
func (r *rpcServer) ListBurns(ctx context.Context,
3335+
in *taprpc.ListBurnsRequest) (*taprpc.ListBurnsResponse, error) {
3336+
3337+
burns, err := r.cfg.AssetStore.QueryBurns(
3338+
ctx, tapdb.QueryBurnsFilters{
3339+
AssetID: in.AssetId,
3340+
GroupKey: in.TweakedGroupKey,
3341+
AnchorTxid: in.AnchorTxid,
3342+
},
3343+
)
3344+
if err != nil {
3345+
return nil, err
3346+
}
3347+
3348+
rpcBurns := fn.Map(burns, marshalRpcBurn)
3349+
3350+
return &taprpc.ListBurnsResponse{
3351+
Burns: rpcBurns,
3352+
}, nil
3353+
}
3354+
3355+
// marshalRpcBurn creates an instance of *taprpc.AssetBurn from the tapdb model.
3356+
func marshalRpcBurn(b *tapfreighter.AssetBurn) *taprpc.AssetBurn {
3357+
return &taprpc.AssetBurn{
3358+
Note: b.Note,
3359+
AssetId: b.AssetID,
3360+
TweakedGroupKey: b.GroupKey,
3361+
Amount: b.Amount,
3362+
AnchorTxid: b.AnchorTxid[:],
3363+
}
3364+
}
3365+
33313366
// marshalOutboundParcel turns a pending parcel into its RPC counterpart.
33323367
func marshalOutboundParcel(
33333368
parcel *tapfreighter.OutboundParcel) (*taprpc.AssetTransfer,

0 commit comments

Comments
 (0)