Skip to content
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
1 change: 1 addition & 0 deletions cmd/sim/evm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func NewEvmCmd() *cobra.Command {
cmd.AddCommand(NewSupportedChainsCmd())
cmd.AddCommand(NewBalancesCmd())
cmd.AddCommand(NewBalanceCmd())
cmd.AddCommand(NewStablecoinsCmd())

return cmd
}
107 changes: 107 additions & 0 deletions cmd/sim/evm/stablecoins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package evm

import (
"encoding/json"
"fmt"
"net/url"

"github.com/spf13/cobra"

"github.com/duneanalytics/cli/output"
)

// NewStablecoinsCmd returns the `sim evm stablecoins` command.
func NewStablecoinsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "stablecoins <address>",
Short: "Get stablecoin balances for a wallet address",
Long: "Return stablecoin balances for the given wallet address across supported\n" +
"EVM chains, including USD valuations.\n\n" +
"Examples:\n" +
" dune sim evm stablecoins 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
" dune sim evm stablecoins 0xd8da... --chain-ids 1,8453\n" +
" dune sim evm stablecoins 0xd8da... -o json",
Args: cobra.ExactArgs(1),
RunE: runStablecoins,
}

cmd.Flags().String("chain-ids", "", "Comma-separated chain IDs or tags (default: all default chains)")
cmd.Flags().String("filters", "", "Token filter: erc20 or native")
cmd.Flags().String("metadata", "", "Extra metadata fields: logo,url,pools")
cmd.Flags().Bool("exclude-spam", false, "Exclude tokens with <100 USD liquidity")
cmd.Flags().String("historical-prices", "", "Hour offsets for historical prices (e.g. 720,168,24)")
cmd.Flags().Int("limit", 0, "Max results (1-1000)")
cmd.Flags().String("offset", "", "Pagination cursor from previous response")
output.AddFormatFlag(cmd, "text")

return cmd
}

func runStablecoins(cmd *cobra.Command, args []string) error {
client, err := requireSimClient(cmd)

Check failure on line 41 in cmd/sim/evm/stablecoins.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient

Check failure on line 41 in cmd/sim/evm/stablecoins.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient

Check failure on line 41 in cmd/sim/evm/stablecoins.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient
if err != nil {
return err
}

address := args[0]
params := url.Values{}

if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
params.Set("chain_ids", v)
}
if v, _ := cmd.Flags().GetString("filters"); v != "" {
params.Set("filters", v)
}
if v, _ := cmd.Flags().GetString("metadata"); v != "" {
params.Set("metadata", v)
}
if v, _ := cmd.Flags().GetBool("exclude-spam"); v {
params.Set("exclude_spam_tokens", "true")
}
if v, _ := cmd.Flags().GetString("historical-prices"); v != "" {
params.Set("historical_prices", v)
}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
params.Set("limit", fmt.Sprintf("%d", v))
}
if v, _ := cmd.Flags().GetString("offset"); v != "" {
params.Set("offset", v)
}

data, err := client.Get(cmd.Context(), "/v1/evm/balances/"+address+"/stablecoins", 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)
}

printWarnings(cmd, resp.Warnings)

columns := []string{"CHAIN", "SYMBOL", "AMOUNT", "PRICE_USD", "VALUE_USD"}
rows := make([][]string, len(resp.Balances))
for i, b := range resp.Balances {
rows[i] = []string{
b.Chain,
b.Symbol,
formatAmount(b.Amount, b.Decimals),
formatUSD(b.PriceUSD),
formatUSD(b.ValueUSD),
}
}
output.PrintTable(w, columns, rows)

if resp.NextOffset != "" {
fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
}
return nil
}
}
Copy link

Choose a reason for hiding this comment

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

Duplicated run logic between stablecoins and balances commands

Low Severity

runStablecoins is a near-verbatim copy of runBalances — the only differences are the API endpoint path and the omission of the asset-class flag. The flag definitions in NewStablecoinsCmd are similarly duplicated. A shared helper accepting the endpoint path (and optionally registering extra flags) would eliminate ~60 lines of duplication and ensure future bug fixes (e.g., to table rendering or param building) are applied consistently.

Fix in Cursor Fix in Web

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree, can we dedupe some of it ?

42 changes: 42 additions & 0 deletions cmd/sim/evm/stablecoins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package evm_test

import (
"bytes"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEvmStablecoins_Text(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "stablecoins", evmTestAddress, "--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, "VALUE_USD")
}

func TestEvmStablecoins_JSON(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "stablecoins", evmTestAddress, "--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")
}
Loading