From 276f717a139e6495b7c9531e55359b00f4057696 Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:05:18 +0100 Subject: [PATCH] Get balance --- cmd/sim/evm/balance.go | 87 +++++++++++++++++++++++++++++++++++++ cmd/sim/evm/balance_test.go | 71 ++++++++++++++++++++++++++++++ cmd/sim/evm/evm.go | 1 + 3 files changed, 159 insertions(+) create mode 100644 cmd/sim/evm/balance.go create mode 100644 cmd/sim/evm/balance_test.go diff --git a/cmd/sim/evm/balance.go b/cmd/sim/evm/balance.go new file mode 100644 index 0000000..4609ddc --- /dev/null +++ b/cmd/sim/evm/balance.go @@ -0,0 +1,87 @@ +package evm + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/spf13/cobra" + + "github.com/duneanalytics/cli/output" +) + +// NewBalanceCmd returns the `sim evm balance` command (single token). +func NewBalanceCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "balance ", + Short: "Get balance for a single token", + Long: "Return the balance of a single token for the given wallet address on one chain.\n" + + "Use \"native\" as the --token value to query the native asset (e.g. ETH).\n\n" + + "Examples:\n" + + " dune sim evm balance 0xd8da... --token native --chain-ids 1\n" + + " dune sim evm balance 0xd8da... --token 0xa0b8...eb48 --chain-ids 8453\n" + + " dune sim evm balance 0xd8da... --token native --chain-ids 1 -o json", + Args: cobra.ExactArgs(1), + RunE: runBalance, + } + + cmd.Flags().String("token", "", "Token contract address or \"native\" (required)") + cmd.Flags().String("chain-ids", "", "Chain ID (required)") + _ = cmd.MarkFlagRequired("token") + _ = cmd.MarkFlagRequired("chain-ids") + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runBalance(cmd *cobra.Command, args []string) error { + client, err := requireSimClient(cmd) + if err != nil { + return err + } + + address := args[0] + tokenAddress, _ := cmd.Flags().GetString("token") + + params := url.Values{} + if v, _ := cmd.Flags().GetString("chain-ids"); v != "" { + params.Set("chain_ids", v) + } + + path := fmt.Sprintf("/v1/evm/balances/%s/token/%s", address, tokenAddress) + data, err := client.Get(cmd.Context(), path, params) + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + var raw json.RawMessage = data + return output.PrintJSON(w, raw) + default: + var resp balancesResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("parsing response: %w", err) + } + + if len(resp.Balances) == 0 { + fmt.Fprintln(w, "No balance found.") + return nil + } + + b := resp.Balances[0] + fmt.Fprintf(w, "Chain: %s (ID: %d)\n", b.Chain, b.ChainID) + fmt.Fprintf(w, "Token: %s\n", b.Address) + fmt.Fprintf(w, "Symbol: %s\n", b.Symbol) + if b.Name != "" { + fmt.Fprintf(w, "Name: %s\n", b.Name) + } + fmt.Fprintf(w, "Decimals: %d\n", b.Decimals) + fmt.Fprintf(w, "Amount: %s\n", formatAmount(b.Amount, b.Decimals)) + fmt.Fprintf(w, "Price USD: %s\n", formatUSD(b.PriceUSD)) + fmt.Fprintf(w, "Value USD: %s\n", formatUSD(b.ValueUSD)) + + return nil + } +} diff --git a/cmd/sim/evm/balance_test.go b/cmd/sim/evm/balance_test.go new file mode 100644 index 0000000..1d01025 --- /dev/null +++ b/cmd/sim/evm/balance_test.go @@ -0,0 +1,71 @@ +package evm_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEvmBalance_NativeText(t *testing.T) { + key := simAPIKey(t) + + root := newSimTestRoot() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balance", evmTestAddress, "--token", "native", "--chain-ids", "1"}) + + require.NoError(t, root.Execute()) + + out := buf.String() + assert.Contains(t, out, "Chain:") + assert.Contains(t, out, "Symbol:") + assert.Contains(t, out, "ETH") + assert.Contains(t, out, "Amount:") + assert.Contains(t, out, "Price USD:") + assert.Contains(t, out, "Value USD:") +} + +func TestEvmBalance_NativeJSON(t *testing.T) { + key := simAPIKey(t) + + root := newSimTestRoot() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balance", evmTestAddress, "--token", "native", "--chain-ids", "1", "-o", "json"}) + + require.NoError(t, root.Execute()) + + var resp map[string]interface{} + require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) + assert.Contains(t, resp, "wallet_address") + assert.Contains(t, resp, "balances") + + balances, ok := resp["balances"].([]interface{}) + require.True(t, ok) + require.Len(t, balances, 1) + + first, ok := balances[0].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "native", first["address"]) +} + +func TestEvmBalance_MissingRequiredFlags(t *testing.T) { + key := simAPIKey(t) + + // Missing --token + root := newSimTestRoot() + root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balance", evmTestAddress, "--chain-ids", "1"}) + err := root.Execute() + assert.Error(t, err) + assert.Contains(t, err.Error(), "token") + + // Missing --chain-ids + root2 := newSimTestRoot() + root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balance", evmTestAddress, "--token", "native"}) + err2 := root2.Execute() + assert.Error(t, err2) + assert.Contains(t, err2.Error(), "chain-ids") +} diff --git a/cmd/sim/evm/evm.go b/cmd/sim/evm/evm.go index b7821da..9f6d088 100644 --- a/cmd/sim/evm/evm.go +++ b/cmd/sim/evm/evm.go @@ -40,6 +40,7 @@ func NewEvmCmd() *cobra.Command { cmd.AddCommand(NewSupportedChainsCmd()) cmd.AddCommand(NewBalancesCmd()) + cmd.AddCommand(NewBalanceCmd()) return cmd }