Skip to content

Commit 8785003

Browse files
authored
Merge pull request #1691 from lightninglabs/multi-bug-fix
Improve grouped asset UX for onchain and offchain balances
2 parents 053a88a + bee6d8c commit 8785003

File tree

12 files changed

+981
-842
lines changed

12 files changed

+981
-842
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
[API
6767
docs](https://lightning.engineering/api-docs/api/taproot-assets/price-oracle/query-asset-rates/#priceoraclerpcintent)
6868
for more information on the different values and their meaning.
69+
6970
- The `SendPayment`, `AddInvoice` and `DecodeAssetPayReq` RPCs now have a [new
7071
`price_oracle_metadata` field the user can specify to send additional metadata
7172
to a price oracle](https://github.com/lightninglabs/taproot-assets/pull/1677)
@@ -79,6 +80,11 @@
7980
completed sends with efficient database-level
8081
filtering](https://github.com/lightninglabs/taproot-assets/pull/1685).
8182

83+
- A [new field `unconfirmed_transfers` was added to the response of the
84+
`ListBalances` RPC
85+
method](https://github.com/lightninglabs/taproot-assets/pull/1691) to indicate
86+
that unconfirmed asset-related transactions don't count toward the balance.
87+
8288
## tapcli Additions
8389

8490
- [Rename](https://github.com/lightninglabs/taproot-assets/pull/1682) the mint
@@ -93,6 +99,10 @@
9399

94100
## Functional Updates
95101

102+
- The output of `lncli channelbalance` [now also shows the local and remote
103+
balances of asset channels grouped by group key (if grouped assets were used
104+
in a channel)](https://github.com/lightninglabs/taproot-assets/pull/1691).
105+
96106
## RPC Updates
97107

98108
## tapcli Updates

itest/assertions.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,6 +2279,24 @@ func AssertBalances(t *testing.T, client taprpc.TaprootAssetsClient,
22792279
"asset balance, wanted %d, got: %v", balance,
22802280
toJSON(t, assetIDBalances),
22812281
)
2282+
2283+
// If we query for grouped asset balances too, it means we do
2284+
// have at least some assets with a group key. So the output of
2285+
// the ListBalances call should contain at least one asset
2286+
// balance with a group key set.
2287+
if config.groupedAssetBalance > 0 {
2288+
numGrouped := 0
2289+
for _, bal := range assetIDBalances.AssetBalances {
2290+
if len(bal.GroupKey) > 0 {
2291+
numGrouped++
2292+
}
2293+
}
2294+
require.Greater(
2295+
t, numGrouped, 0, "expected at least one "+
2296+
"asset in ListBalances response to "+
2297+
"have a group key set",
2298+
)
2299+
}
22822300
}
22832301

22842302
// Next, we do the same but grouped by group keys (if requested, since
@@ -2305,6 +2323,12 @@ func AssertBalances(t *testing.T, client taprpc.TaprootAssetsClient,
23052323
"grouped balance, wanted %d, got: %v", balance,
23062324
toJSON(t, assetGroupBalances),
23072325
)
2326+
2327+
// Because we query for grouped assets, the group key
2328+
// field should be set for all asset balances.
2329+
for _, bal := range assetGroupBalances.AssetBalances {
2330+
require.Equal(t, config.groupKey, bal.GroupKey)
2331+
}
23082332
}
23092333
}
23102334

rfqmsg/custom_channel_data.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
// JsonAssetBalance is a struct that represents the balance of a single asset ID
1111
// within a channel.
1212
type JsonAssetBalance struct {
13-
AssetID string `json:"asset_id"`
14-
Name string `json:"name"`
13+
AssetID string `json:"asset_id,omitempty"`
14+
Name string `json:"name,omitempty"`
1515
LocalBalance uint64 `json:"local_balance"`
1616
RemoteBalance uint64 `json:"remote_balance"`
1717
}
@@ -80,9 +80,12 @@ func (c *JsonAssetChannel) HasAllAssetIDs(ids fn.Set[asset.ID]) bool {
8080

8181
// JsonAssetChannelBalances is a struct that represents the balance information
8282
// of all assets within open and pending channels.
83+
// nolint:lll
8384
type JsonAssetChannelBalances struct {
84-
OpenChannels map[string]*JsonAssetBalance `json:"open_channels"`
85-
PendingChannels map[string]*JsonAssetBalance `json:"pending_channels"`
85+
OpenChannels map[string]*JsonAssetBalance `json:"open_channels"`
86+
OpenChannelsByGroup map[string]*JsonAssetBalance `json:"open_channels_by_group"`
87+
PendingChannels map[string]*JsonAssetBalance `json:"pending_channels"`
88+
PendingChannelsByGroup map[string]*JsonAssetBalance `json:"pending_channels_by_group"`
8689
}
8790

8891
// JsonCloseOutput is a struct that represents the additional co-op close output

rpcserver.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,12 +1275,12 @@ func (r *rpcServer) listBalancesByAsset(ctx context.Context,
12751275
}
12761276

12771277
resp := &taprpc.ListBalancesResponse{
1278-
AssetBalances: make(map[string]*taprpc.AssetBalance, len(balances)),
1278+
AssetBalances: make(
1279+
map[string]*taprpc.AssetBalance, len(balances),
1280+
),
12791281
}
12801282

12811283
for _, balance := range balances {
1282-
balance := balance
1283-
12841284
assetIDStr := hex.EncodeToString(balance.ID[:])
12851285

12861286
resp.AssetBalances[assetIDStr] = &taprpc.AssetBalance{
@@ -1291,10 +1291,21 @@ func (r *rpcServer) listBalancesByAsset(ctx context.Context,
12911291
MetaHash: balance.MetaHash[:],
12921292
AssetId: balance.ID[:],
12931293
},
1294-
Balance: balance.Balance,
1294+
GroupKey: balance.GroupKey,
1295+
Balance: balance.Balance,
12951296
}
12961297
}
12971298

1299+
// We will also report the number of unconfirmed transfers. This is
1300+
// useful for clients as unconfirmed asset coins are not included in the
1301+
// balance list.
1302+
outboundParcels, err := r.cfg.AssetStore.QueryParcels(ctx, nil, true)
1303+
if err != nil {
1304+
return nil, fmt.Errorf("unable to query for unconfirmed "+
1305+
"outgoing parcels: %w", err)
1306+
}
1307+
resp.UnconfirmedTransfers = uint64(len(outboundParcels))
1308+
12981309
return resp, nil
12991310
}
13001311

@@ -1331,6 +1342,16 @@ func (r *rpcServer) listBalancesByGroupKey(ctx context.Context,
13311342
}
13321343
}
13331344

1345+
// We will also report the number of unconfirmed transfers. This is
1346+
// useful for clients as unconfirmed asset coins are not included in the
1347+
// balance list.
1348+
outboundParcels, err := r.cfg.AssetStore.QueryParcels(ctx, nil, true)
1349+
if err != nil {
1350+
return nil, fmt.Errorf("unable to query for unconfirmed "+
1351+
"outgoing parcels: %w", err)
1352+
}
1353+
resp.UnconfirmedTransfers = uint64(len(outboundParcels))
1354+
13341355
return resp, nil
13351356
}
13361357

tapchannelmsg/custom_channel_data.go

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -200,83 +200,90 @@ func (b *BalanceCustomData) AsJson() ([]byte, error) {
200200
return []byte{}, nil
201201
}
202202

203-
resp := &rfqmsg.JsonAssetChannelBalances{
204-
OpenChannels: make(map[string]*rfqmsg.JsonAssetBalance),
205-
PendingChannels: make(map[string]*rfqmsg.JsonAssetBalance),
206-
}
207-
for _, openChan := range b.OpenChannels {
208-
for _, assetOutput := range openChan.LocalOutputs() {
209-
assetID := assetOutput.AssetID.Val
203+
type groupedBalance = map[string]*rfqmsg.JsonAssetBalance
210204

211-
assetIDStr := hex.EncodeToString(assetID[:])
212-
assetName := assetOutput.Proof.Val.Asset.Tag
205+
// addOutput is a helper function that adds an asset output to the
206+
// targetByID and targetByGroup maps, updating the local or remote
207+
// balance as appropriate.
208+
addOutput := func(out *AssetOutput, local bool, targetByID,
209+
targetByGroup groupedBalance) {
213210

214-
assetBalance, ok := resp.OpenChannels[assetIDStr]
215-
if !ok {
216-
assetBalance = &rfqmsg.JsonAssetBalance{
217-
AssetID: assetIDStr,
218-
Name: assetName,
219-
}
220-
resp.OpenChannels[assetIDStr] = assetBalance
221-
}
211+
assetID := out.AssetID.Val
212+
assetIDStr := hex.EncodeToString(assetID[:])
213+
assetName := out.Proof.Val.Asset.Tag
214+
amount := out.Amount.Val
215+
groupKey := out.Proof.Val.Asset.GroupKey
222216

223-
assetBalance.LocalBalance += assetOutput.Amount.Val
217+
balanceByID, ok := targetByID[assetIDStr]
218+
if !ok {
219+
balanceByID = &rfqmsg.JsonAssetBalance{
220+
AssetID: assetIDStr,
221+
Name: assetName,
222+
}
223+
targetByID[assetIDStr] = balanceByID
224224
}
225225

226-
for _, assetOutput := range openChan.RemoteOutputs() {
227-
assetID := assetOutput.AssetID.Val
226+
if local {
227+
balanceByID.LocalBalance += amount
228+
} else {
229+
balanceByID.RemoteBalance += amount
230+
}
228231

229-
assetIDStr := hex.EncodeToString(assetID[:])
230-
assetName := assetOutput.Proof.Val.Asset.Tag
232+
if groupKey != nil {
233+
groupKeyStr := hex.EncodeToString(
234+
groupKey.GroupPubKey.SerializeCompressed(),
235+
)
231236

232-
assetBalance, ok := resp.OpenChannels[assetIDStr]
237+
balanceByGroupKey, ok := targetByGroup[groupKeyStr]
233238
if !ok {
234-
assetBalance = &rfqmsg.JsonAssetBalance{
235-
AssetID: assetIDStr,
236-
Name: assetName,
237-
}
238-
resp.OpenChannels[assetIDStr] = assetBalance
239+
balanceByGroupKey = &rfqmsg.JsonAssetBalance{}
240+
targetByGroup[groupKeyStr] = balanceByGroupKey
239241
}
240242

241-
assetBalance.RemoteBalance += assetOutput.Amount.Val
243+
if local {
244+
balanceByGroupKey.LocalBalance += amount
245+
} else {
246+
balanceByGroupKey.RemoteBalance += amount
247+
}
242248
}
243249
}
244250

245-
for _, pendingChan := range b.PendingChannels {
246-
for _, assetOutput := range pendingChan.LocalOutputs() {
247-
assetID := assetOutput.AssetID.Val
251+
resp := &rfqmsg.JsonAssetChannelBalances{
252+
OpenChannels: make(groupedBalance),
253+
OpenChannelsByGroup: make(groupedBalance),
254+
PendingChannels: make(groupedBalance),
255+
PendingChannelsByGroup: make(groupedBalance),
256+
}
248257

249-
assetIDStr := hex.EncodeToString(assetID[:])
250-
assetName := assetOutput.Proof.Val.Asset.Tag
258+
for _, openChan := range b.OpenChannels {
259+
for _, assetOutput := range openChan.LocalOutputs() {
260+
addOutput(
261+
assetOutput, true, resp.OpenChannels,
262+
resp.OpenChannelsByGroup,
263+
)
264+
}
251265

252-
assetBalance, ok := resp.PendingChannels[assetIDStr]
253-
if !ok {
254-
assetBalance = &rfqmsg.JsonAssetBalance{
255-
AssetID: assetIDStr,
256-
Name: assetName,
257-
}
258-
resp.PendingChannels[assetIDStr] = assetBalance
259-
}
266+
for _, assetOutput := range openChan.RemoteOutputs() {
267+
addOutput(
268+
assetOutput, false, resp.OpenChannels,
269+
resp.OpenChannelsByGroup,
270+
)
271+
}
272+
}
260273

261-
assetBalance.LocalBalance += assetOutput.Amount.Val
274+
for _, pendingChan := range b.PendingChannels {
275+
for _, assetOutput := range pendingChan.LocalOutputs() {
276+
addOutput(
277+
assetOutput, true, resp.PendingChannels,
278+
resp.PendingChannelsByGroup,
279+
)
262280
}
263281

264282
for _, assetOutput := range pendingChan.RemoteOutputs() {
265-
assetID := assetOutput.AssetID.Val
266-
267-
assetIDStr := hex.EncodeToString(assetID[:])
268-
assetName := assetOutput.Proof.Val.Asset.Tag
269-
270-
assetBalance, ok := resp.PendingChannels[assetIDStr]
271-
if !ok {
272-
assetBalance = &rfqmsg.JsonAssetBalance{
273-
AssetID: assetIDStr,
274-
Name: assetName,
275-
}
276-
resp.PendingChannels[assetIDStr] = assetBalance
277-
}
278-
279-
assetBalance.RemoteBalance += assetOutput.Amount.Val
283+
addOutput(
284+
assetOutput, false, resp.PendingChannels,
285+
resp.PendingChannelsByGroup,
286+
)
280287
}
281288
}
282289

tapchannelmsg/custom_channel_data_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/btcsuite/btcd/btcec/v2"
1010
"github.com/btcsuite/btcd/wire"
11+
"github.com/lightninglabs/taproot-assets/asset"
1112
"github.com/lightninglabs/taproot-assets/fn"
1213
"github.com/lightninglabs/taproot-assets/internal/test"
1314
"github.com/lightninglabs/taproot-assets/rfqmsg"
@@ -137,6 +138,16 @@ func TestReadBalanceCustomData(t *testing.T) {
137138
proof1 := randProof(t)
138139
proof2 := randProof(t)
139140
proof3 := randProof(t)
141+
142+
// We make it so the assets in proof 1 and proof 2 are in the same
143+
// group, so their balance will be combined in the output.
144+
groupPubKey := test.RandPubKey(t)
145+
proof1.Asset.GroupKey = &asset.GroupKey{
146+
GroupPubKey: *groupPubKey,
147+
}
148+
proof2.Asset.GroupKey = proof1.Asset.GroupKey
149+
groupKeyStr := hex.EncodeToString(groupPubKey.SerializeCompressed())
150+
140151
assetID1 := proof1.Asset.ID()
141152
assetID2 := proof2.Asset.ID()
142153
assetID3 := proof3.Asset.ID()
@@ -226,6 +237,20 @@ func TestReadBalanceCustomData(t *testing.T) {
226237
require.Contains(t, formattedJSON.String(), expectedOpen3)
227238
require.Contains(t, formattedJSON.String(), expectedPending1)
228239
require.Contains(t, formattedJSON.String(), expectedPending2)
240+
241+
// The grouped balance shouldn't show the asset ID or name, since that
242+
// would be confusing as it would only use the ID/name of the first
243+
// asset we have in the channels for that group.
244+
expectedOpenGroup := `"` + groupKeyStr + `": {
245+
"local_balance": 3000,
246+
"remote_balance": 2000
247+
}`
248+
expectedPendingGroup := `"` + groupKeyStr + `": {
249+
"local_balance": 0,
250+
"remote_balance": 1000
251+
}`
252+
require.Contains(t, formattedJSON.String(), expectedOpenGroup)
253+
require.Contains(t, formattedJSON.String(), expectedPendingGroup)
229254
}
230255

231256
// TestCloseOutCustomData tests that we can read the custom data from a channel

tapdb/assets_store.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ type AssetBalance struct {
383383
Type asset.Type
384384
GenesisPoint wire.OutPoint
385385
OutputIndex uint32
386+
GroupKey []byte
386387
}
387388

388389
// AssetGroupBalance holds abalance query result for a particular asset group
@@ -1064,6 +1065,7 @@ func (a *AssetStore) QueryBalancesByAsset(ctx context.Context,
10641065
Tag: assetBalance.AssetTag,
10651066
Type: asset.Type(assetBalance.AssetType),
10661067
OutputIndex: uint32(assetBalance.OutputIndex),
1068+
GroupKey: assetBalance.GroupKey,
10671069
}
10681070

10691071
err = readOutPoint(

tapdb/sqlc/assets.sql.go

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)