Skip to content
Open
Changes from 1 commit
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
31 changes: 28 additions & 3 deletions cmd/zetatool/cli/tss_balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"sort"
"sync"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -72,6 +73,8 @@ const (
FlagMigrationAmounts = "migration-amounts"
// FlagShowNonces is the flag to show pending nonce low and high columns
FlagShowNonces = "show-nonces"
// FlagTSSNumber is the flag to select a specific TSS by position (1 = oldest, N = latest)
FlagTSSNumber = "tss-number"
)

// NewTSSBalancesCMD creates a new command to check TSS address balances across all chains
Expand All @@ -91,13 +94,16 @@ Examples:
zetatool tss-balances 7000
zetatool tss-balances zeta_mainnet
zetatool tss-balances zeta_testnet --config custom_config.json
zetatool tss-balances zeta_testnet --raw-amounts`,
zetatool tss-balances zeta_testnet --raw-amounts
zetatool tss-balances zeta_mainnet --tss-number 1 # oldest TSS
zetatool tss-balances zeta_mainnet --tss-number 3 # 3rd TSS (use total count for latest)`,
Args: cobra.ExactArgs(1),
RunE: getTSSBalances,
}

cmd.Flags().Bool(FlagMigrationAmounts, false, "Show migration amount and raw migration amount columns")
cmd.Flags().Bool(FlagShowNonces, false, "Show pending nonce low and high columns")
cmd.Flags().Int(FlagTSSNumber, 0, "Show only the Nth TSS ordered by finalized height (1 = oldest, N = latest)")

return cmd
}
Expand Down Expand Up @@ -127,6 +133,11 @@ func getTSSBalances(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to read value for flag %s: %w", FlagShowNonces, err)
}

tssNumber, err := cmd.Flags().GetInt(FlagTSSNumber)
if err != nil {
return fmt.Errorf("failed to read value for flag %s: %w", FlagTSSNumber, err)
}

cfg, err := config.GetConfigByNetwork(network, configFile)
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
Expand All @@ -153,11 +164,25 @@ func getTSSBalances(cmd *cobra.Command, args []string) error {
return fmt.Errorf("no TSS entries found")
}

for i, tss := range tssHistoryRes.TssList {
// Sort TSS list by finalized zeta height ascending (oldest first, so TSS 1 = oldest, TSS N = latest)
tssList := tssHistoryRes.TssList
sort.Slice(tssList, func(i, j int) bool {
return tssList[i].FinalizedZetaHeight < tssList[j].FinalizedZetaHeight
})
Comment on lines +187 to +191

@coderabbitai coderabbitai Bot Mar 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for test cases related to TSS history sorting
rg -n "FinalizedZetaHeight.*0|TssHistory|tssList" --type=go -g '*_test.go'

Repository: zeta-chain/node

Length of output: 7529


🏁 Script executed:

#!/bin/bash
# Find sample.TssList implementation
rg -n "func.*TssList" --type=go -A 10

Repository: zeta-chain/node

Length of output: 1323


🏁 Script executed:

#!/bin/bash
# Check TSS protobuf definition
fd -e proto | xargs rg -l "message TSS" | head -5

Repository: zeta-chain/node

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Check FinalizedZetaHeight in protobuf
rg -n "FinalizedZetaHeight" --type=proto

Repository: zeta-chain/node

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Find the base Tss() sample function
rg -n "func Tss\(\)" --type=go -A 15

Repository: zeta-chain/node

Length of output: 961


🏁 Script executed:

#!/bin/bash
# Read the TSS protobuf definition
cat proto/zetachain/zetacore/observer/tss.proto | head -50

Repository: zeta-chain/node

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Check how FinalizedZetaHeight is used in actual code (not tests)
rg -n "FinalizedZetaHeight" cmd/zetatool/cli/tss_balances.go -B 5 -A 5

Repository: zeta-chain/node

Length of output: 1624


🏁 Script executed:

#!/bin/bash
# Check how TSS entries are created/stored to see if FinalizedZetaHeight can be 0
rg -n "FinalizedZetaHeight.*=" --type=go -B 2 -A 2 | grep -v test | head -50

Repository: zeta-chain/node

Length of output: 3031


🏁 Script executed:

#!/bin/bash
# Look for the server-side TssHistory query implementation mentioned in the review
fd -name "grpc_query_tss.go" | grep -v test

Repository: zeta-chain/node

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Check server-side TSS sorting logic
rg -n "TssHistory|sort.*TSS" x/observer/keeper/grpc_query_tss.go -A 10 -B 2

Repository: zeta-chain/node

Length of output: 815


🏁 Script executed:

#!/bin/bash
# Check how TSS entries are created and stored (non-test code)
rg -n "TSS\{" --type=go -B 3 -A 8 | grep -v test | head -80

Repository: zeta-chain/node

Length of output: 4874


🏁 Script executed:

#!/bin/bash
# Look for where TSS entries are created with potentially 0 FinalizedZetaHeight
rg -n "SetTSS|SetHistoricalTSS|append.*TSS" x/observer/keeper/ --type=go -B 2 -A 5 | head -60

Repository: zeta-chain/node

Length of output: 3523


🏁 Script executed:

#!/bin/bash
# Check if there's logic for creating new/active TSS with unset FinalizedZetaHeight
rg -n "KeyGenZetaHeight|FinalizedZetaHeight" x/observer/keeper/msg_server*.go -B 3 -A 3

Repository: zeta-chain/node

Length of output: 3272


🏁 Script executed:

#!/bin/bash
# Check actual TSS creation flow - when is SetTSSHistory called?
rg -n "SetTSSHistory|AppendTss" x/observer/keeper/ --type=go -B 3 -A 5

Repository: zeta-chain/node

Length of output: 7307


🏁 Script executed:

#!/bin/bash
# Check what ctx.BlockHeight() returns and if it can be 0
rg -n "BlockHeight\(\)" x/observer/keeper/msg_server_vote_tss.go -B 2 -A 2

Repository: zeta-chain/node

Length of output: 661


🏁 Script executed:

#!/bin/bash
# Verify if any TSS history entries are ever created with FinalizedZetaHeight = 0
rg -n "FinalizedZetaHeight.*0|FinalizedZetaHeight\s*:" x/observer/keeper --type=go -B 2 -A 2 | grep -v test | grep -v "!= 0"

Repository: zeta-chain/node

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Check if there's any path where TSS could be stored with FinalizedZetaHeight = 0
rg -n "SetTSSHistory" --type=go -B 5 -A 2 | grep -v test

Repository: zeta-chain/node

Length of output: 3420


🏁 Script executed:

#!/bin/bash
# Verify: does GetAllTSS only return historical TSS or include current active TSS?
rg -n "GetAllTSS|TSSHistoryKey|TSSKey" x/observer/keeper/tss.go -A 3

Repository: zeta-chain/node

Length of output: 2148


🏁 Script executed:

#!/bin/bash
# Check the actual TSS query response to understand what data is returned
cat cmd/zetatool/cli/tss_balances.go | sed -n '160,180p'

Repository: zeta-chain/node

Length of output: 739


🏁 Script executed:

#!/bin/bash
# Confirm the server-side sorting happens before client receives data
cat x/observer/keeper/grpc_query_tss.go | sed -n '32,42p'

Repository: zeta-chain/node

Length of output: 517


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or comments about the intent of TSS numbering
rg -n "TSS.*1.*oldest|oldest.*TSS" cmd/zetatool/cli/ --type=go -B 2 -A 2

Repository: zeta-chain/node

Length of output: 1833


Remove redundant client-side sorting—the server already returns sorted results.

The server-side TssHistory query (x/observer/keeper/grpc_query_tss.go:36–38) sorts TSS entries by FinalizedZetaHeight ascending before returning them. The client-side re-sort at lines 169–171 is unnecessary and violates the DRY principle.

Additionally, FinalizedZetaHeight is always set to ctx.BlockHeight() when a TSS is finalized (x/observer/keeper/msg_server_vote_tss.go:126), which is ≥ 1 for all actual transactions. The scenario of FinalizedZetaHeight == 0 cannot occur in practice, so the original ordering concern is not a functional risk.

Remove the client-side sort entirely to simplify the code and rely on the server's pre-sorted response:

Proposed fix: Remove redundant sort
 	// Sort TSS list by finalized zeta height ascending (oldest first, so TSS 1 = oldest, TSS N = latest)
 	tssList := tssHistoryRes.TssList
-	sort.Slice(tssList, func(i, j int) bool {
-		return tssList[i].FinalizedZetaHeight < tssList[j].FinalizedZetaHeight
-	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Sort TSS list by finalized zeta height ascending (oldest first, so TSS 1 = oldest, TSS N = latest)
tssList := tssHistoryRes.TssList
sort.Slice(tssList, func(i, j int) bool {
return tssList[i].FinalizedZetaHeight < tssList[j].FinalizedZetaHeight
})
// TSS list is already sorted by finalized zeta height ascending (oldest first, so TSS 1 = oldest, TSS N = latest)
tssList := tssHistoryRes.TssList
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/zetatool/cli/tss_balances.go` around lines 167 - 171, The client is
re-sorting tssHistoryRes.TssList by FinalizedZetaHeight even though the server
already returns entries sorted; remove the redundant sort.Slice block (the
anonymous func comparing tssList[i].FinalizedZetaHeight and
tssList[j].FinalizedZetaHeight) so the code simply uses tssHistoryRes.TssList as
returned, relying on the server-side ordering and eliminating dead/duplicative
logic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is fine , there is no harm is sorting again

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@kingpinXD, understood — a defensive re-sort is indeed harmless and adds a degree of resilience against any future change to the server-side ordering. Happy to defer to your judgement here.

(◕ᴥ◕ )


🧠 Learnings used
Learnt from: gartnera
Repo: zeta-chain/node PR: 3228
File: zetaclient/orchestrator/orchestrator.go:388-401
Timestamp: 2024-11-27T22:01:49.732Z
Learning: When reviewing code changes in `zetaclient/orchestrator/orchestrator.go`, avoid suggesting refactoring that is unrelated to the current PR.

Learnt from: gartnera
Repo: zeta-chain/node PR: 3070
File: cmd/zetae2e/init.go:0-0
Timestamp: 2024-10-30T17:56:16.341Z
Learning: In code reviews for Go files like `cmd/zetae2e/init.go` in the ZetaChain project, avoid suggesting unrelated refactoring. Focus comments on changes relevant to the PR objectives.

Learnt from: kingpinXD
Repo: zeta-chain/node PR: 3627
File: e2e/runner/require.go:77-83
Timestamp: 2025-03-04T17:31:11.517Z
Learning: In the ZetaChain node repo, the ballot list returned from the ObserverClient.Ballots query (ballotsRes.Ballots) is already sorted by creation height, with the earliest ballots first.

Learnt from: kingpinXD
Repo: zeta-chain/node PR: 3627
File: e2e/runner/require.go:77-83
Timestamp: 2025-03-04T17:31:11.517Z
Learning: In the ZetaChain node repo, the ballot list returned from the ObserverClient.Ballots query (ballotsRes.Ballots) is already sorted by creation height, with the earliest ballots first.

Learnt from: kingpinXD
Repo: zeta-chain/node PR: 3503
File: e2e/runner/require.go:0-0
Timestamp: 2025-02-07T19:33:35.631Z
Learning: In e2e tests for the zeta-chain/node repository, minimize computation and avoid redundant checks that are already covered by unit tests to ensure faster execution. For example, ballot sorting is verified in unit tests, so e2e tests can safely assume the order.

Learnt from: lumtis
Repo: zeta-chain/node PR: 4199
File: zetaclient/chains/evm/signer/signer_admin.go:25-26
Timestamp: 2025-09-15T13:42:17.594Z
Learning: In PR 4199, the CLI references to CmdMigrateTssFunds in x/crosschain/client/cli/ files are intentionally not updated as they are out of scope for this specific refactor focused on removing ERC20 custody messages.

Learnt from: kingpinXD
Repo: zeta-chain/node PR: 4064
File: cmd/zetae2e/local/solana.go:0-0
Timestamp: 2025-08-06T01:54:04.100Z
Learning: The `CheckSolanaTSSBalance()` method in e2e/runner has been refactored to not return an error. Instead, it uses internal assertions (require statements) to fail the test immediately if balance checks fail. This is part of a broader refactoring pattern in the E2E runner where balance check methods were changed from error-returning to assertion-based approaches.


// Filter to a specific TSS if --tss-number is set
if tssNumber > 0 {
if tssNumber > len(tssList) {
return fmt.Errorf("TSS number %d out of range, only %d TSS entries available", tssNumber, len(tssList))
}
tssList = []observertypes.TSS{tssList[tssNumber-1]}
}

for i, tss := range tssList {
if i > 0 {
fmt.Println() // Add spacing between TSS entries
}
fmt.Printf("=== TSS %d of %d ===\n", i+1, len(tssHistoryRes.TssList))
fmt.Printf("=== TSS %d of %d ===\n", i+1, len(tssList))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Misleading TSS header when filtering by tss-number

Low Severity

When --tss-number is used, tssList is replaced with a single-element slice, so the header always prints === TSS 1 of 1 === regardless of which TSS was selected. For example, --tss-number 3 on a 5-entry list shows "TSS 1 of 1" instead of "TSS 3 of 5", losing both the requested position and total count context.

Additional Locations (1)

Fix in Cursor Fix in Web

Comment on lines +193 to +205

@coderabbitai coderabbitai Bot Mar 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Display counter loses original TSS count after filtering.

After filtering to a single TSS at line 178, len(tssList) becomes 1. Consequently, line 185 prints "TSS 1 of 1" regardless of which TSS was requested. Users requesting --tss-number 3 would expect to see "TSS 3 of N" to understand the context of their selection.

Proposed fix to preserve original count
 	// Sort TSS list by finalized zeta height ascending (oldest first, so TSS 1 = oldest, TSS N = latest)
 	tssList := tssHistoryRes.TssList
 	sort.Slice(tssList, func(i, j int) bool {
 		return tssList[i].FinalizedZetaHeight < tssList[j].FinalizedZetaHeight
 	})
+	totalTSSCount := len(tssList)

 	// Filter to a specific TSS if --tss-number is set
+	selectedTSSNumber := 0
 	if tssNumber > 0 {
 		if tssNumber > len(tssList) {
 			return fmt.Errorf("TSS number %d out of range, only %d TSS entries available", tssNumber, len(tssList))
 		}
+		selectedTSSNumber = tssNumber
 		tssList = []observertypes.TSS{tssList[tssNumber-1]}
 	}

 	for i, tss := range tssList {
 		if i > 0 {
 			fmt.Println() // Add spacing between TSS entries
 		}
-		fmt.Printf("=== TSS %d of %d ===\n", i+1, len(tssList))
+		displayNum := i + 1
+		if selectedTSSNumber > 0 {
+			displayNum = selectedTSSNumber
+		}
+		fmt.Printf("=== TSS %d of %d ===\n", displayNum, totalTSSCount)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/zetatool/cli/tss_balances.go` around lines 173 - 185, The display counter
should show the original total TSS count even after filtering; before the block
that mutates tssList when tssNumber > 0, capture the original length (e.g.,
originalCount := len(tssList)), then after potentially reducing tssList use that
originalCount in the print statement inside the loop (replace len(tssList) in
the fmt.Printf call with originalCount) so flags like --tss-number 3 will print
"TSS 3 of N" correctly; update references to tssNumber, tssList, and the
fmt.Printf in the loop accordingly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is fine its an internal tool , i would rather have simplicity of code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@kingpinXD, understood — simplicity is a fair trade-off for an internal tool. Happy to leave it as-is.


🧠 Learnings used
Learnt from: kingpinXD
Repo: zeta-chain/node PR: 4064
File: cmd/zetae2e/local/solana.go:0-0
Timestamp: 2025-08-06T01:54:04.100Z
Learning: The `CheckSolanaTSSBalance()` method in e2e/runner has been refactored to not return an error. Instead, it uses internal assertions (require statements) to fail the test immediately if balance checks fail. This is part of a broader refactoring pattern in the E2E runner where balance check methods were changed from error-returning to assertion-based approaches.

Learnt from: lumtis
Repo: zeta-chain/node PR: 4199
File: zetaclient/chains/evm/signer/signer_admin.go:25-26
Timestamp: 2025-09-15T13:42:17.594Z
Learning: In PR 4199, the CLI references to CmdMigrateTssFunds in x/crosschain/client/cli/ files are intentionally not updated as they are out of scope for this specific refactor focused on removing ERC20 custody messages.


if err := printTSSBalances(
ctx,
Expand Down
Loading