diff --git a/cmd/sim/evm/evm.go b/cmd/sim/evm/evm.go index 9f6d088..2d797ab 100644 --- a/cmd/sim/evm/evm.go +++ b/cmd/sim/evm/evm.go @@ -41,6 +41,7 @@ func NewEvmCmd() *cobra.Command { cmd.AddCommand(NewSupportedChainsCmd()) cmd.AddCommand(NewBalancesCmd()) cmd.AddCommand(NewBalanceCmd()) + cmd.AddCommand(NewStablecoinsCmd()) return cmd } diff --git a/cmd/sim/evm/stablecoins.go b/cmd/sim/evm/stablecoins.go new file mode 100644 index 0000000..309f677 --- /dev/null +++ b/cmd/sim/evm/stablecoins.go @@ -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
", + 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) + 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 + } +} diff --git a/cmd/sim/evm/stablecoins_test.go b/cmd/sim/evm/stablecoins_test.go new file mode 100644 index 0000000..7dd4888 --- /dev/null +++ b/cmd/sim/evm/stablecoins_test.go @@ -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") +}