diff --git a/authconfig/authconfig.go b/authconfig/authconfig.go
index c8857bd..640f753 100644
--- a/authconfig/authconfig.go
+++ b/authconfig/authconfig.go
@@ -13,7 +13,6 @@ import (
// Config holds the persisted CLI configuration.
type Config struct {
APIKey string `yaml:"api_key"`
- SimAPIKey string `yaml:"sim_api_key,omitempty"`
Telemetry *bool `yaml:"telemetry,omitempty"`
}
diff --git a/cli/root.go b/cli/root.go
index 0a35809..cf8fd90 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -22,7 +22,6 @@ import (
"github.com/duneanalytics/cli/cmd/execution"
"github.com/duneanalytics/cli/cmd/matview"
"github.com/duneanalytics/cli/cmd/query"
- "github.com/duneanalytics/cli/cmd/sim"
"github.com/duneanalytics/cli/cmd/usage"
"github.com/duneanalytics/cli/cmd/visualization"
"github.com/duneanalytics/cli/cmd/whoami"
@@ -45,7 +44,6 @@ var rootCmd = &cobra.Command{
" - Create and manage visualizations (charts, tables, counters) on query results\n" +
" - Create and manage dashboards with visualizations and text widgets\n" +
" - Browse Dune documentation for DuneSQL syntax, API references, and guides\n" +
- " - Query real-time wallet and token data via the Sim API\n" +
" - Monitor credit usage, storage consumption, and billing periods\n\n" +
"Authenticate with an API key via --api-key, the DUNE_API_KEY environment variable,\n" +
"or by running `dune auth`.",
@@ -110,8 +108,7 @@ var rootCmd = &cobra.Command{
commandPath = parts[1]
}
- isSim := strings.HasPrefix(commandPath, "sim")
- tr.Track(commandPath, tracking.StatusSuccess, "", durationMs, isSim)
+ tr.Track(commandPath, tracking.StatusSuccess, "", durationMs)
return nil
},
}
@@ -127,7 +124,6 @@ func init() {
rootCmd.AddCommand(execution.NewExecutionCmd())
rootCmd.AddCommand(usage.NewUsageCmd())
rootCmd.AddCommand(whoami.NewWhoAmICmd())
- rootCmd.AddCommand(sim.NewSimCmd())
rootCmd.AddCommand(visualization.NewVisualizationCmd())
rootCmd.AddCommand(dashboard.NewDashboardCmd())
}
@@ -152,8 +148,7 @@ func Execute(version, commit, date, amplitudeKey string) {
); err != nil {
// Build best-effort command path from os.Args (strip flags).
commandPath := commandPathFromArgs(os.Args)
- isSim := strings.HasPrefix(commandPath, "sim")
- tracker.Track(commandPath, tracking.StatusError, err.Error(), 0, isSim)
+ tracker.Track(commandPath, tracking.StatusError, err.Error(), 0)
// Flush the event before exiting — os.Exit does not run deferred funcs,
// so defer tracker.Shutdown() above would never fire.
tracker.Shutdown()
diff --git a/cmd/sim/auth.go b/cmd/sim/auth.go
deleted file mode 100644
index 767a275..0000000
--- a/cmd/sim/auth.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package sim
-
-import (
- "bufio"
- "fmt"
- "os"
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/authconfig"
-)
-
-// NewAuthCmd returns the `sim auth` command.
-func NewAuthCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "auth",
- Short: "Save your Sim API key to the local configuration file",
- Long: "Persist your Sim API key to ~/.config/dune/config.yaml so subsequent\n" +
- "'dune sim' commands authenticate automatically without requiring\n" +
- "--sim-api-key or the DUNE_SIM_API_KEY environment variable.\n\n" +
- "The key can be provided via:\n" +
- " 1. --api-key flag\n" +
- " 2. DUNE_SIM_API_KEY environment variable\n" +
- " 3. Interactive prompt (if neither of the above is set)\n\n" +
- "The saved key is used as the lowest-priority fallback; --sim-api-key and\n" +
- "DUNE_SIM_API_KEY always take precedence when set.\n\n" +
- "Examples:\n" +
- " dune sim auth\n" +
- " dune sim auth --api-key sim_abc123...",
- Annotations: map[string]string{"skipSimAuth": "true"},
- RunE: runSimAuth,
- }
-
- cmd.Flags().String("api-key", "", "Sim API key to save (prefixed 'sim_'); if omitted, reads from DUNE_SIM_API_KEY or prompts interactively")
-
- return cmd
-}
-
-func runSimAuth(cmd *cobra.Command, _ []string) error {
- key, _ := cmd.Flags().GetString("api-key")
-
- if key == "" {
- key = os.Getenv("DUNE_SIM_API_KEY")
- }
-
- if key == "" {
- fmt.Fprint(cmd.ErrOrStderr(), "Enter your Sim API key: ")
- scanner := bufio.NewScanner(cmd.InOrStdin())
- if scanner.Scan() {
- key = strings.TrimSpace(scanner.Text())
- }
- }
-
- if key == "" {
- return fmt.Errorf("no API key provided")
- }
-
- cfg, err := authconfig.Load()
- if err != nil {
- return fmt.Errorf("loading existing config: %w", err)
- }
- if cfg == nil {
- cfg = &authconfig.Config{}
- }
- cfg.SimAPIKey = key
- if err := authconfig.Save(cfg); err != nil {
- return fmt.Errorf("saving config: %w", err)
- }
-
- p, _ := authconfig.Path()
- fmt.Fprintf(cmd.OutOrStdout(), "Sim API key saved to %s\n", p)
- return nil
-}
diff --git a/cmd/sim/auth_test.go b/cmd/sim/auth_test.go
deleted file mode 100644
index ad950a5..0000000
--- a/cmd/sim/auth_test.go
+++ /dev/null
@@ -1,124 +0,0 @@
-package sim_test
-
-import (
- "bytes"
- "context"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/duneanalytics/cli/authconfig"
- "github.com/duneanalytics/cli/cmd/sim"
- "github.com/spf13/cobra"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
-)
-
-func setupAuthTest(t *testing.T) string {
- t.Helper()
- dir := t.TempDir()
- authconfig.SetDirFunc(func() (string, error) { return dir, nil })
- t.Cleanup(authconfig.ResetDirFunc)
- // Clear env var so it doesn't interfere with tests.
- t.Setenv("DUNE_SIM_API_KEY", "")
- return dir
-}
-
-func newSimAuthRoot() *cobra.Command {
- root := &cobra.Command{Use: "dune"}
- root.SetContext(context.Background())
-
- simCmd := sim.NewSimCmd()
- root.AddCommand(simCmd)
-
- return root
-}
-
-func TestSimAuth_WithFlag(t *testing.T) {
- dir := setupAuthTest(t)
-
- root := newSimAuthRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "auth", "--api-key", "sk_sim_flag_key"})
- require.NoError(t, root.Execute())
-
- data, err := os.ReadFile(filepath.Join(dir, "config.yaml"))
- require.NoError(t, err)
- assert.Contains(t, string(data), "sk_sim_flag_key")
- assert.Contains(t, buf.String(), "Sim API key saved to")
-}
-
-func TestSimAuth_WithEnvVar(t *testing.T) {
- dir := setupAuthTest(t)
-
- t.Setenv("DUNE_SIM_API_KEY", "sk_sim_env_key")
-
- root := newSimAuthRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "auth"})
- require.NoError(t, root.Execute())
-
- data, err := os.ReadFile(filepath.Join(dir, "config.yaml"))
- require.NoError(t, err)
- assert.Contains(t, string(data), "sk_sim_env_key")
-}
-
-func TestSimAuth_WithPrompt(t *testing.T) {
- dir := setupAuthTest(t)
-
- root := newSimAuthRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetIn(strings.NewReader("sk_sim_prompt_key\n"))
- root.SetArgs([]string{"sim", "auth"})
- require.NoError(t, root.Execute())
-
- data, err := os.ReadFile(filepath.Join(dir, "config.yaml"))
- require.NoError(t, err)
- assert.Contains(t, string(data), "sk_sim_prompt_key")
-}
-
-func TestSimAuth_EmptyInput(t *testing.T) {
- setupAuthTest(t)
-
- root := newSimAuthRoot()
- root.SetIn(strings.NewReader("\n"))
- root.SetArgs([]string{"sim", "auth"})
- err := root.Execute()
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "no API key provided")
-}
-
-func TestSimAuth_PreservesExistingConfig(t *testing.T) {
- dir := setupAuthTest(t)
-
- // Pre-populate config with existing fields.
- existing := &authconfig.Config{
- APIKey: "existing_dune_key",
- }
- telemetryTrue := true
- existing.Telemetry = &telemetryTrue
- require.NoError(t, authconfig.Save(existing))
-
- root := newSimAuthRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "auth", "--api-key", "sk_sim_new"})
- require.NoError(t, root.Execute())
-
- // Verify all fields are preserved.
- data, err := os.ReadFile(filepath.Join(dir, "config.yaml"))
- require.NoError(t, err)
-
- var cfg authconfig.Config
- require.NoError(t, yaml.Unmarshal(data, &cfg))
-
- assert.Equal(t, "existing_dune_key", cfg.APIKey, "existing api_key should be preserved")
- assert.Equal(t, "sk_sim_new", cfg.SimAPIKey, "sim_api_key should be set")
- require.NotNil(t, cfg.Telemetry, "telemetry should be preserved")
- assert.True(t, *cfg.Telemetry, "telemetry value should be preserved")
-}
diff --git a/cmd/sim/client.go b/cmd/sim/client.go
deleted file mode 100644
index 493e202..0000000
--- a/cmd/sim/client.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package sim
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "time"
-)
-
-const defaultBaseURL = "https://api.sim.dune.com"
-
-// SimClient is a lightweight HTTP client for the Sim API.
-type SimClient struct {
- baseURL string
- apiKey string
- httpClient *http.Client
-}
-
-// NewSimClient creates a new Sim API client with the given API key.
-func NewSimClient(apiKey string) *SimClient {
- return &SimClient{
- baseURL: defaultBaseURL,
- apiKey: apiKey,
- httpClient: &http.Client{
- Timeout: 30 * time.Second,
- },
- }
-}
-
-// NewBareSimClient creates a Sim API client without authentication.
-// Use this for public endpoints that don't require an API key.
-func NewBareSimClient() *SimClient {
- return &SimClient{
- baseURL: defaultBaseURL,
- httpClient: &http.Client{
- Timeout: 30 * time.Second,
- },
- }
-}
-
-// Get performs a GET request to the Sim API and returns the raw JSON response body.
-// The path should include the leading slash (e.g. "/v1/evm/supported-chains").
-// Query parameters are appended from params.
-func (c *SimClient) Get(ctx context.Context, path string, params url.Values) ([]byte, error) {
- u, err := url.Parse(c.baseURL + path)
- if err != nil {
- return nil, fmt.Errorf("invalid URL: %w", err)
- }
- if params != nil {
- u.RawQuery = params.Encode()
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
- if err != nil {
- return nil, fmt.Errorf("creating request: %w", err)
- }
- if c.apiKey != "" {
- req.Header.Set("X-Sim-Api-Key", c.apiKey)
- }
- req.Header.Set("Accept", "application/json")
-
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("request failed: %w", err)
- }
- defer resp.Body.Close()
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("reading response: %w", err)
- }
-
- if resp.StatusCode >= 400 {
- return nil, httpError(resp.StatusCode, body)
- }
-
- return body, nil
-}
-
-// httpError returns a user-friendly error for HTTP error status codes.
-func httpError(status int, body []byte) error {
- // Try to extract a message from the JSON error response.
- var errResp struct {
- Error string `json:"error"`
- Message string `json:"message"`
- }
- msg := ""
- if json.Unmarshal(body, &errResp) == nil {
- if errResp.Error != "" {
- msg = errResp.Error
- } else if errResp.Message != "" {
- msg = errResp.Message
- }
- }
-
- switch status {
- case http.StatusBadRequest:
- if msg != "" {
- return fmt.Errorf("bad request: %s", msg)
- }
- return fmt.Errorf("bad request")
- case http.StatusUnauthorized:
- return fmt.Errorf("authentication failed: check your Sim API key")
- case http.StatusNotFound:
- if msg != "" {
- return fmt.Errorf("not found: %s", msg)
- }
- return fmt.Errorf("not found")
- case http.StatusTooManyRequests:
- return fmt.Errorf("rate limit exceeded: try again later")
- default:
- if status >= 500 {
- return fmt.Errorf("Sim API server error (HTTP %d): try again later", status)
- }
- if msg != "" {
- return fmt.Errorf("Sim API error (HTTP %d): %s", status, msg)
- }
- return fmt.Errorf("Sim API error (HTTP %d)", status)
- }
-}
diff --git a/cmd/sim/evm/activity.go b/cmd/sim/evm/activity.go
deleted file mode 100644
index 9ca52a0..0000000
--- a/cmd/sim/evm/activity.go
+++ /dev/null
@@ -1,218 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewActivityCmd returns the `sim evm activity` command.
-func NewActivityCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "activity
",
- Short: "Get a decoded activity feed for a wallet address across EVM chains",
- Long: "Return a reverse-chronological feed of human-readable on-chain activity for\n" +
- "the given wallet address. Unlike raw transactions, activity entries are decoded\n" +
- "and classified into semantic types: sends, receives, mints, burns, token swaps,\n" +
- "approvals, and contract calls.\n\n" +
- "Activity types:\n" +
- " - send/receive: native or token transfers to/from the wallet\n" +
- " - mint/burn: token creation or destruction involving the wallet\n" +
- " - swap: DEX token exchanges (includes from/to token details)\n" +
- " - approve: ERC20/ERC721 spending approvals\n" +
- " - call: contract interactions with decoded function name and inputs\n\n" +
- "Asset types: native, erc20, erc721, erc1155.\n\n" +
- "Each activity item includes the transaction context, transfer amounts with\n" +
- "USD values, and token metadata. Swap entries include both sides of the trade.\n" +
- "Call entries include the decoded function name and inputs.\n\n" +
- "By default, returns all activity types across all default chains.\n" +
- "Run 'dune sim evm supported-chains' to see which chains support activity.\n\n" +
- "For raw transaction data (hashes, gas, calldata), use 'dune sim evm transactions'.\n\n" +
- "Examples:\n" +
- " dune sim evm activity 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
- " dune sim evm activity 0xd8da... --activity-type send,receive --chain-ids 1\n" +
- " dune sim evm activity 0xd8da... --asset-type erc20 --limit 50 -o json\n" +
- " dune sim evm activity 0xd8da... --token-address 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
- Args: cobra.ExactArgs(1),
- RunE: runActivity,
- }
-
- cmd.Flags().String("chain-ids", "", "Restrict to specific chains by numeric ID or tag name (comma-separated, e.g. '1,8453' or 'default'); defaults to all chains tagged 'default'")
- cmd.Flags().String("token-address", "", "Filter activities involving specific token contracts (comma-separated ERC20/ERC721/ERC1155 addresses)")
- cmd.Flags().String("activity-type", "", "Filter by activity classification (comma-separated): send, receive, mint, burn, swap, approve, call; defaults to all types")
- cmd.Flags().String("asset-type", "", "Filter by token standard (comma-separated): native, erc20, erc721, erc1155; defaults to all standards")
- cmd.Flags().Int("limit", 0, "Maximum number of activity items to return per page (1-100, default: server-determined)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type activityResponse struct {
- Activity []activityItem `json:"activity"`
- NextOffset string `json:"next_offset,omitempty"`
- Warnings []warningEntry `json:"warnings,omitempty"`
-}
-
-type activityItem struct {
- ChainID int64 `json:"chain_id"`
- BlockNumber int64 `json:"block_number"`
- BlockTime string `json:"block_time"`
- TxHash string `json:"tx_hash"`
- Type string `json:"type"`
- AssetType string `json:"asset_type"`
- TokenAddress string `json:"token_address,omitempty"`
- From string `json:"from,omitempty"`
- To string `json:"to,omitempty"`
- Value string `json:"value,omitempty"`
- ValueUSD float64 `json:"value_usd"`
- ID string `json:"id,omitempty"` // ERC721/ERC1155 token ID
- Spender string `json:"spender,omitempty"`
- TokenMeta *tokenMetadata `json:"token_metadata,omitempty"`
-
- // Swap-specific fields.
- FromTokenAddress string `json:"from_token_address,omitempty"`
- FromTokenValue string `json:"from_token_value,omitempty"`
- FromTokenMetadata *tokenMetadata `json:"from_token_metadata,omitempty"`
- ToTokenAddress string `json:"to_token_address,omitempty"`
- ToTokenValue string `json:"to_token_value,omitempty"`
- ToTokenMetadata *tokenMetadata `json:"to_token_metadata,omitempty"`
-
- // Contract call fields.
- Function *functionInfo `json:"function,omitempty"`
- ContractMetadata *contractMetaObj `json:"contract_metadata,omitempty"`
-}
-
-type tokenMetadata struct {
- Symbol string `json:"symbol"`
- Decimals int `json:"decimals"`
- Name string `json:"name,omitempty"`
- Logo string `json:"logo,omitempty"`
- PriceUSD float64 `json:"price_usd"`
- PoolSize float64 `json:"pool_size,omitempty"`
- Standard string `json:"standard,omitempty"`
-}
-
-type functionInfo struct {
- Signature string `json:"signature,omitempty"`
- Name string `json:"name,omitempty"`
- Inputs []functionInput `json:"inputs,omitempty"`
-}
-
-type functionInput struct {
- Name string `json:"name,omitempty"`
- Type string `json:"type,omitempty"`
- Value json.RawMessage `json:"value,omitempty"`
-}
-
-type contractMetaObj struct {
- Name string `json:"name,omitempty"`
-}
-
-func runActivity(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- address := args[0]
- params := url.Values{}
-
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
- if v, _ := cmd.Flags().GetString("token-address"); v != "" {
- params.Set("token_address", v)
- }
- if v, _ := cmd.Flags().GetString("activity-type"); v != "" {
- params.Set("activity_type", v)
- }
- if v, _ := cmd.Flags().GetString("asset-type"); v != "" {
- params.Set("asset_type", 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/activity/"+address, 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 activityResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- // Print warnings to stderr.
- printWarnings(cmd, resp.Warnings)
-
- columns := []string{"CHAIN_ID", "TYPE", "ASSET_TYPE", "SYMBOL", "VALUE_USD", "TX_HASH", "BLOCK_TIME"}
- rows := make([][]string, len(resp.Activity))
- for i, a := range resp.Activity {
- rows[i] = []string{
- fmt.Sprintf("%d", a.ChainID),
- a.Type,
- a.AssetType,
- activitySymbol(a),
- output.FormatUSD(a.ValueUSD),
- truncateHash(a.TxHash),
- a.BlockTime,
- }
- }
- output.PrintTable(w, columns, rows)
-
- if resp.NextOffset != "" {
- fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
- }
- return nil
- }
-}
-
-// activitySymbol returns the best symbol to display for the activity.
-// For swaps it shows "FROM -> TO", for regular activities it uses token_metadata.
-func activitySymbol(a activityItem) string {
- if a.Type == "swap" {
- from := ""
- to := ""
- if a.FromTokenMetadata != nil {
- from = a.FromTokenMetadata.Symbol
- }
- if a.ToTokenMetadata != nil {
- to = a.ToTokenMetadata.Symbol
- }
- if from != "" || to != "" {
- return from + " -> " + to
- }
- return ""
- }
- if a.TokenMeta != nil {
- return a.TokenMeta.Symbol
- }
- // Native transfers may not have token_metadata.
- if a.AssetType == "native" {
- return "ETH"
- }
- return ""
-}
-
-// truncateHash shortens a hex hash for table display.
-func truncateHash(hash string) string {
- if len(hash) <= 14 {
- return hash
- }
- return hash[:8] + "..." + hash[len(hash)-4:]
-}
diff --git a/cmd/sim/evm/activity_test.go b/cmd/sim/evm/activity_test.go
deleted file mode 100644
index 824b03c..0000000
--- a/cmd/sim/evm/activity_test.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmActivity_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", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN_ID")
- assert.Contains(t, out, "TYPE")
- assert.Contains(t, out, "ASSET_TYPE")
- assert.Contains(t, out, "SYMBOL")
- assert.Contains(t, out, "VALUE_USD")
- assert.Contains(t, out, "TX_HASH")
- assert.Contains(t, out, "BLOCK_TIME")
-}
-
-func TestEvmActivity_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", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "activity")
-}
-
-func TestEvmActivity_ActivityTypeFilter(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--activity-type", "receive", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp struct {
- Activity []struct {
- Type string `json:"type"`
- } `json:"activity"`
- }
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- // All returned activities should be of the filtered type.
- for _, a := range resp.Activity {
- assert.Equal(t, "receive", a.Type)
- }
-}
-
-func TestEvmActivity_AssetTypeFilter(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--asset-type", "erc20", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp struct {
- Activity []struct {
- AssetType string `json:"asset_type"`
- } `json:"activity"`
- }
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- for _, a := range resp.Activity {
- assert.Equal(t, "erc20", a.AssetType)
- }
-}
-
-func TestEvmActivity_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- // Fetch page 1 with a small limit to trigger pagination.
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "activity")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "activity")
- }
-}
diff --git a/cmd/sim/evm/balance.go b/cmd/sim/evm/balance.go
deleted file mode 100644
index 01d36c2..0000000
--- a/cmd/sim/evm/balance.go
+++ /dev/null
@@ -1,102 +0,0 @@
-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 the balance of a single token for a wallet on one chain",
- Long: "Return the balance of a specific token for the given wallet address on a\n" +
- "single EVM chain. This is a targeted lookup that returns exactly one balance\n" +
- "entry, unlike 'dune sim evm balances' which returns all tokens across chains.\n\n" +
- "Both --token and --chain-ids are required. Use the literal string 'native'\n" +
- "as the --token value to query the chain's native asset (e.g. ETH on Ethereum,\n" +
- "MATIC on Polygon), or pass an ERC20 contract address.\n\n" +
- "For multi-token or multi-chain lookups, use 'dune sim evm balances' instead.\n\n" +
- "Examples:\n" +
- " dune sim evm balance 0xd8da... --token native --chain-ids 1\n" +
- " dune sim evm balance 0xd8da... --token 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --chain-ids 8453\n" +
- " dune sim evm balance 0xd8da... --token native --chain-ids 1 --historical-prices 168,24 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runBalance,
- }
-
- cmd.Flags().String("token", "", "Token to query: an ERC20 contract address (0x...) or the literal string 'native' for the chain's native asset (required)")
- cmd.Flags().String("chain-ids", "", "Numeric EVM chain ID to query (required, single value, e.g. '1' for Ethereum, '8453' for Base)")
- cmd.Flags().String("metadata", "", "Request additional metadata fields in the response (comma-separated): 'logo' (token icon URL), 'url' (project website), 'pools' (liquidity pool details)")
- cmd.Flags().String("historical-prices", "", "Include historical USD prices at the specified hour offsets from now (comma-separated, e.g. '720,168,24' for 30d, 7d, 1d ago)")
- _ = cmd.MarkFlagRequired("token")
- _ = cmd.MarkFlagRequired("chain-ids")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-func runBalance(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- address := args[0]
- tokenAddress, _ := cmd.Flags().GetString("token")
-
- params := url.Values{}
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
- if v, _ := cmd.Flags().GetString("metadata"); v != "" {
- params.Set("metadata", v)
- }
- if v, _ := cmd.Flags().GetString("historical-prices"); v != "" {
- params.Set("historical_prices", 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)
- }
-
- printBalanceErrors(cmd, resp.Errors)
-
- 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", output.FormatUSD(b.PriceUSD))
- fmt.Fprintf(w, "Value USD: %s\n", output.FormatUSD(b.ValueUSD))
-
- return nil
- }
-}
diff --git a/cmd/sim/evm/balance_test.go b/cmd/sim/evm/balance_test.go
deleted file mode 100644
index 1d01025..0000000
--- a/cmd/sim/evm/balance_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-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/balances.go b/cmd/sim/evm/balances.go
deleted file mode 100644
index 8857107..0000000
--- a/cmd/sim/evm/balances.go
+++ /dev/null
@@ -1,290 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewBalancesCmd returns the `sim evm balances` command.
-func NewBalancesCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "balances ",
- Short: "Get EVM token balances for a wallet address across multiple chains",
- Long: "Return native and ERC20 token balances for the given wallet address across\n" +
- "supported EVM chains. Each balance entry includes the token identity, raw\n" +
- "balance amount, current USD price, total USD value, and liquidity indicators.\n" +
- "Data comes from Dune's real-time index.\n\n" +
- "By default, queries all chains tagged 'default'. Use --chain-ids to restrict\n" +
- "to specific networks. Run 'dune sim evm supported-chains' to see valid IDs.\n\n" +
- "For a single-token balance lookup, use 'dune sim evm balance' instead.\n" +
- "For stablecoin-only balances, use 'dune sim evm stablecoins'.\n\n" +
- "Results are paginated; use --offset with the next_offset value from a\n" +
- "previous response to retrieve additional pages.\n\n" +
- "Examples:\n" +
- " dune sim evm balances 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
- " dune sim evm balances 0xd8da... --chain-ids 1,8453 --exclude-spam\n" +
- " dune sim evm balances 0xd8da... --filters erc20 --metadata logo,url\n" +
- " dune sim evm balances 0xd8da... --historical-prices 720,168,24 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runBalances,
- }
-
- addBalanceFlags(cmd)
- cmd.Flags().String("asset-class", "", "Filter by asset classification: 'stablecoin' (returns only stablecoins like USDC, USDT, DAI); prefer 'dune sim evm stablecoins' as a shorthand")
-
- return cmd
-}
-
-type balancesResponse struct {
- WalletAddress string `json:"wallet_address"`
- Balances []balanceEntry `json:"balances"`
- Errors *balanceErrors `json:"errors,omitempty"`
- NextOffset string `json:"next_offset,omitempty"`
- Warnings []warningEntry `json:"warnings,omitempty"`
- RequestTime string `json:"request_time,omitempty"`
- ResponseTime string `json:"response_time,omitempty"`
-}
-
-type balanceErrors struct {
- ErrorMessage string `json:"error_message,omitempty"`
- TokenErrors []apiChainError `json:"token_errors,omitempty"`
-}
-
-// apiChainError is a per-chain error returned by several Sim API endpoints
-// (balances, transactions, etc.). It is intentionally shared across commands.
-type apiChainError struct {
- ChainID int64 `json:"chain_id"`
- Address string `json:"address"`
- Description string `json:"description,omitempty"`
-}
-
-type balanceEntry struct {
- Chain string `json:"chain"`
- ChainID int64 `json:"chain_id"`
- Address string `json:"address"`
- Amount string `json:"amount"`
- Symbol string `json:"symbol"`
- Name string `json:"name"`
- Decimals int `json:"decimals"`
- PriceUSD float64 `json:"price_usd"`
- ValueUSD float64 `json:"value_usd"`
- PoolSize float64 `json:"pool_size"`
- LowLiquidity bool `json:"low_liquidity"`
- HistoricalPrices []historicalPrice `json:"historical_prices,omitempty"`
- TokenMetadata *balanceTokenMeta `json:"token_metadata,omitempty"`
- Pool *poolMetadata `json:"pool,omitempty"`
-}
-
-type historicalPrice struct {
- OffsetHours int `json:"offset_hours"`
- PriceUSD float64 `json:"price_usd"`
-}
-
-type balanceTokenMeta struct {
- Logo string `json:"logo,omitempty"`
- URL string `json:"url,omitempty"`
-}
-
-type poolMetadata struct {
- PoolType string `json:"pool_type"`
- Address string `json:"address"`
- Token0 string `json:"token0"`
- Token1 string `json:"token1"`
-}
-
-type warningEntry struct {
- Code string `json:"code"`
- Message string `json:"message"`
- ChainIDs []int64 `json:"chain_ids,omitempty"`
- DocsURL string `json:"docs_url,omitempty"`
-}
-
-func runBalances(cmd *cobra.Command, args []string) error {
- return runBalancesEndpoint(cmd, args, "/v1/evm/balances/", "")
-}
-
-// addBalanceFlags registers the common flags shared by the balances and
-// stablecoins commands.
-func addBalanceFlags(cmd *cobra.Command) {
- cmd.Flags().String("chain-ids", "", "Restrict to specific chains by numeric ID or tag name (comma-separated, e.g. '1,8453' or 'default'); defaults to all chains tagged 'default'. Run 'dune sim evm supported-chains' for valid values")
- cmd.Flags().String("filters", "", "Filter by token standard: 'erc20' (only ERC20 tokens) or 'native' (only native chain assets like ETH)")
- cmd.Flags().String("metadata", "", "Request additional metadata fields in the response (comma-separated): 'logo' (token icon URL), 'url' (project website), 'pools' (liquidity pool details)")
- cmd.Flags().Bool("exclude-spam", false, "Exclude low-liquidity tokens (less than $100 USD pool size) commonly associated with spam airdrops")
- cmd.Flags().Bool("exclude-unpriced", true, "Exclude tokens without a USD price (default: true); pass --exclude-unpriced=false to include them")
- cmd.Flags().String("historical-prices", "", "Include historical USD prices at the specified hour offsets from now (comma-separated, e.g. '720,168,24' for 30d, 7d, 1d ago)")
- cmd.Flags().Int("limit", 0, "Maximum number of balance entries to return per page (1-1000, default: server-determined)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-}
-
-// runBalancesEndpoint is the shared run implementation for the balances and
-// stablecoins commands. The final API path is built as:
-//
-// pathPrefix + address + pathSuffix
-//
-// For example "/v1/evm/balances/" + addr + "" for balances,
-// or "/v1/evm/balances/" + addr + "/stablecoins" for stablecoins.
-func runBalancesEndpoint(cmd *cobra.Command, args []string, pathPrefix, pathSuffix string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- 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)
- }
- // asset-class is only registered on the balances command; silently ignored
- // when the flag is absent.
- if v, _ := cmd.Flags().GetString("asset-class"); v != "" {
- params.Set("asset_class", 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().GetBool("exclude-unpriced"); v {
- params.Set("exclude_unpriced", "true")
- } else {
- params.Set("exclude_unpriced", "false")
- }
- 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(), pathPrefix+address+pathSuffix, 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)
- }
-
- // Print errors and warnings to stderr.
- printBalanceErrors(cmd, resp.Errors)
- 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),
- output.FormatUSD(b.PriceUSD),
- output.FormatUSD(b.ValueUSD),
- }
- }
- output.PrintTable(w, columns, rows)
-
- if resp.NextOffset != "" {
- fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
- }
- return nil
- }
-}
-
-// printWarnings writes API warnings to stderr.
-func printWarnings(cmd *cobra.Command, warnings []warningEntry) {
- if len(warnings) == 0 {
- return
- }
- stderr := cmd.ErrOrStderr()
- for _, w := range warnings {
- fmt.Fprintf(stderr, "Warning: %s\n", w.Message)
- if len(w.ChainIDs) > 0 {
- ids := make([]string, len(w.ChainIDs))
- for i, id := range w.ChainIDs {
- ids[i] = fmt.Sprintf("%d", id)
- }
- fmt.Fprintf(stderr, " Unsupported chain IDs: %s\n", strings.Join(ids, ", "))
- }
- if w.DocsURL != "" {
- fmt.Fprintf(stderr, " See %s\n", w.DocsURL)
- }
- }
- fmt.Fprintln(stderr)
-}
-
-// formatAmount converts a raw token amount string with decimals to a
-// human-readable decimal representation.
-func formatAmount(raw string, decimals int) string {
- if decimals <= 0 || raw == "" || raw == "0" {
- return raw
- }
-
- // Pad with leading zeros if the raw string is shorter than decimals.
- for len(raw) <= decimals {
- raw = "0" + raw
- }
-
- intPart := raw[:len(raw)-decimals]
- fracPart := raw[len(raw)-decimals:]
-
- // Trim trailing zeros from the fractional part, keep up to 6 digits.
- if len(fracPart) > 6 {
- fracPart = fracPart[:6]
- }
- fracPart = strings.TrimRight(fracPart, "0")
-
- if fracPart == "" {
- return intPart
- }
- return intPart + "." + fracPart
-}
-
-// printBalanceErrors writes balance-level errors to stderr.
-func printBalanceErrors(cmd *cobra.Command, errs *balanceErrors) {
- if errs == nil {
- return
- }
- printAPIChainErrors(cmd, errs.ErrorMessage, errs.TokenErrors)
-}
-
-// printAPIChainErrors is a shared helper that writes per-chain API errors to
-// stderr. It is used by both balance and transaction commands to avoid
-// duplicating the same formatting logic.
-func printAPIChainErrors(cmd *cobra.Command, msg string, errs []apiChainError) {
- if msg == "" && len(errs) == 0 {
- return
- }
- stderr := cmd.ErrOrStderr()
- if msg != "" {
- fmt.Fprintf(stderr, "Error: %s\n", msg)
- }
- for _, e := range errs {
- fmt.Fprintf(stderr, " chain_id=%d address=%s", e.ChainID, e.Address)
- if e.Description != "" {
- fmt.Fprintf(stderr, " — %s", e.Description)
- }
- fmt.Fprintln(stderr)
- }
- fmt.Fprintln(stderr)
-}
diff --git a/cmd/sim/evm/balances_test.go b/cmd/sim/evm/balances_test.go
deleted file mode 100644
index 227b74e..0000000
--- a/cmd/sim/evm/balances_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmBalances_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", "balances", evmTestAddress, "--chain-ids", "1", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "SYMBOL")
- assert.Contains(t, out, "AMOUNT")
- assert.Contains(t, out, "PRICE_USD")
- assert.Contains(t, out, "VALUE_USD")
-}
-
-func TestEvmBalances_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", "balances", evmTestAddress, "--chain-ids", "1", "--limit", "5", "-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")
-}
-
-func TestEvmBalances_WithFilters(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balances", evmTestAddress, "--chain-ids", "1", "--filters", "native", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "ETH")
-}
-
-func TestEvmBalances_ExcludeSpam(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balances", evmTestAddress, "--chain-ids", "1", "--exclude-spam", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
-}
-
-func TestEvmBalances_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- // Fetch page 1 with a small limit to trigger pagination.
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balances", evmTestAddress, "--chain-ids", "1", "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "balances")
-
- // If next_offset is present, pagination is working.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- // Fetch page 2 using the offset.
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "balances", evmTestAddress, "--chain-ids", "1", "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "balances")
- }
-}
diff --git a/cmd/sim/evm/collectibles.go b/cmd/sim/evm/collectibles.go
deleted file mode 100644
index 31b52d1..0000000
--- a/cmd/sim/evm/collectibles.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewCollectiblesCmd returns the `sim evm collectibles` command.
-func NewCollectiblesCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "collectibles ",
- Short: "Get NFT collectibles (ERC721/ERC1155) held by a wallet address",
- Long: "Return ERC721 and ERC1155 collectibles (NFTs) held by the given wallet\n" +
- "address across supported EVM chains. Each entry includes the collection and\n" +
- "token identity, image URL, quantity held, acquisition timestamp, and spam\n" +
- "classification when requested. Spam filtering is enabled by default to hide\n" +
- "airdropped junk NFTs.\n\n" +
- "Spam filtering uses a scoring model based on collection traits (holder count,\n" +
- "transfer patterns, metadata quality). Disable with --filter-spam=false to see\n" +
- "all NFTs including suspected spam.\n\n" +
- "By default, queries all chains tagged 'default'. Run 'dune sim evm\n" +
- "supported-chains' to see which chains support collectibles.\n\n" +
- "Examples:\n" +
- " dune sim evm collectibles 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
- " dune sim evm collectibles 0xd8da... --chain-ids 1\n" +
- " dune sim evm collectibles 0xd8da... --filter-spam=false --show-spam-scores -o json\n" +
- " dune sim evm collectibles 0xd8da... --limit 100 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runCollectibles,
- }
-
- cmd.Flags().String("chain-ids", "", "Restrict to specific chains by numeric ID or tag name (comma-separated, e.g. '1,8453' or 'default'); defaults to all chains tagged 'default'")
- cmd.Flags().Bool("filter-spam", true, "Hide collectibles identified as spam by the scoring model (default: true); set --filter-spam=false to include all NFTs")
- cmd.Flags().Bool("show-spam-scores", false, "Include spam classification details in the response: is_spam flag, numeric spam_score, and per-feature explanations with weights")
- cmd.Flags().Int("limit", 0, "Maximum number of collectibles to return per page (1-2500, default: 250)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type collectiblesResponse struct {
- Address string `json:"address"`
- Entries []collectibleEntry `json:"entries"`
- Warnings []warningEntry `json:"warnings,omitempty"`
- NextOffset string `json:"next_offset,omitempty"`
- RequestTime string `json:"request_time,omitempty"`
- ResponseTime string `json:"response_time,omitempty"`
-}
-
-type collectibleEntry struct {
- ContractAddress string `json:"contract_address"`
- TokenStandard string `json:"token_standard"`
- TokenID string `json:"token_id"`
- Chain string `json:"chain"`
- ChainID int64 `json:"chain_id"`
- Name string `json:"name,omitempty"`
- Symbol string `json:"symbol,omitempty"`
- Description string `json:"description,omitempty"`
- ImageURL string `json:"image_url,omitempty"`
- LastSalePrice string `json:"last_sale_price,omitempty"`
- Metadata *collectibleMetadata `json:"metadata,omitempty"`
- Balance string `json:"balance"`
- LastAcquired string `json:"last_acquired"`
- IsSpam bool `json:"is_spam"`
- SpamScore int `json:"spam_score,omitempty"`
- Explanations []spamExplanation `json:"explanations,omitempty"`
-}
-
-type collectibleMetadata struct {
- URI string `json:"uri,omitempty"`
- Attributes []collectibleAttribute `json:"attributes,omitempty"`
-}
-
-type collectibleAttribute struct {
- Key string `json:"key"`
- Value string `json:"value"`
- Format string `json:"format,omitempty"`
-}
-
-type spamExplanation struct {
- Feature string `json:"feature"`
- Value json.RawMessage `json:"value,omitempty"`
- FeatureScore int `json:"feature_score,omitempty"`
- FeatureWeight float64 `json:"feature_weight,omitempty"`
-}
-
-func runCollectibles(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
-
- address := args[0]
- params := url.Values{}
-
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
- // filter_spam defaults to true on the API side, so only send when explicitly false.
- if v, _ := cmd.Flags().GetBool("filter-spam"); !v {
- params.Set("filter_spam", "false")
- }
- if v, _ := cmd.Flags().GetBool("show-spam-scores"); v {
- params.Set("show_spam_scores", "true")
- }
- 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/collectibles/"+address, 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 collectiblesResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- // Print warnings to stderr.
- printWarnings(cmd, resp.Warnings)
-
- showSpam, _ := cmd.Flags().GetBool("show-spam-scores")
-
- columns := []string{"CHAIN", "NAME", "SYMBOL", "TOKEN_ID", "STANDARD", "BALANCE"}
- if showSpam {
- columns = append(columns, "SPAM", "SPAM_SCORE")
- }
- rows := make([][]string, len(resp.Entries))
- for i, e := range resp.Entries {
- row := []string{
- e.Chain,
- e.Name,
- e.Symbol,
- e.TokenID,
- e.TokenStandard,
- e.Balance,
- }
- if showSpam {
- spam := "N"
- if e.IsSpam {
- spam = "Y"
- }
- row = append(row, spam, fmt.Sprintf("%d", e.SpamScore))
- }
- rows[i] = row
- }
- 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/collectibles_test.go b/cmd/sim/evm/collectibles_test.go
deleted file mode 100644
index c4f9f27..0000000
--- a/cmd/sim/evm/collectibles_test.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmCollectibles_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", "collectibles", evmTestAddress, "--chain-ids", "1", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "NAME")
- assert.Contains(t, out, "SYMBOL")
- assert.Contains(t, out, "TOKEN_ID")
- assert.Contains(t, out, "STANDARD")
- assert.Contains(t, out, "BALANCE")
-}
-
-func TestEvmCollectibles_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", "collectibles", evmTestAddress, "--chain-ids", "1", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "entries")
- assert.Contains(t, resp, "address")
-}
-
-func TestEvmCollectibles_FilterSpamDisabled(t *testing.T) {
- key := simAPIKey(t)
-
- // Fetch with spam filtered (default) and without, compare counts.
- rootFiltered := newSimTestRoot()
- var bufFiltered bytes.Buffer
- rootFiltered.SetOut(&bufFiltered)
- rootFiltered.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--limit", "250", "-o", "json"})
- require.NoError(t, rootFiltered.Execute())
-
- var respFiltered map[string]interface{}
- require.NoError(t, json.Unmarshal(bufFiltered.Bytes(), &respFiltered))
- filteredEntries, ok := respFiltered["entries"].([]interface{})
- require.True(t, ok)
-
- rootUnfiltered := newSimTestRoot()
- var bufUnfiltered bytes.Buffer
- rootUnfiltered.SetOut(&bufUnfiltered)
- rootUnfiltered.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--filter-spam=false", "--limit", "250", "-o", "json"})
- require.NoError(t, rootUnfiltered.Execute())
-
- var respUnfiltered map[string]interface{}
- require.NoError(t, json.Unmarshal(bufUnfiltered.Bytes(), &respUnfiltered))
- unfilteredEntries, ok := respUnfiltered["entries"].([]interface{})
- require.True(t, ok)
-
- // With spam filtering disabled we should get at least as many entries.
- assert.GreaterOrEqual(t, len(unfilteredEntries), len(filteredEntries),
- "disabling spam filter should return >= entries than with filter enabled")
-}
-
-func TestEvmCollectibles_SpamScores(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--filter-spam=false", "--show-spam-scores", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "entries")
-
- // When show_spam_scores is enabled, entries should contain spam_score.
- entries, ok := resp["entries"].([]interface{})
- require.True(t, ok)
- if len(entries) > 0 {
- entry, ok := entries[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, entry, "spam_score", "spam_score should be present when --show-spam-scores is set")
- assert.Contains(t, entry, "is_spam")
- }
-}
-
-// TestEvmCollectibles_SpamScoresText verifies that --show-spam-scores
-// adds SPAM and SPAM_SCORE columns in text mode.
-func TestEvmCollectibles_SpamScoresText(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--show-spam-scores", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "SPAM")
- assert.Contains(t, out, "SPAM_SCORE")
-}
-
-func TestEvmCollectibles_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "entries")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "collectibles", evmTestAddress, "--chain-ids", "1", "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "entries")
- }
-}
diff --git a/cmd/sim/evm/defi_positions.go b/cmd/sim/evm/defi_positions.go
deleted file mode 100644
index 6cf9342..0000000
--- a/cmd/sim/evm/defi_positions.go
+++ /dev/null
@@ -1,282 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "net/url"
- "sort"
- "strconv"
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewDefiPositionsCmd returns the `sim evm defi-positions` command.
-func NewDefiPositionsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "defi-positions ",
- Short: "Get DeFi positions (lending, LPs, vaults) for a wallet address",
- Long: "Return DeFi positions for the given wallet address across supported EVM\n" +
- "chains and protocols. Each position includes USD valuation and protocol-\n" +
- "specific metadata. The response also includes aggregation summaries with\n" +
- "total USD value and per-chain breakdowns.\n\n" +
- "Supported position types:\n" +
- " - Erc4626: ERC-4626 vault positions (e.g. yield vaults, staking wrappers)\n" +
- " - Tokenized: lending protocol positions with receipt tokens (e.g. Aave\n" +
- " aTokens, Compound cTokens)\n" +
- " - UniswapV2: AMM liquidity provider positions (Uniswap V2 and forks)\n" +
- " - Nft: Uniswap V3 concentrated liquidity positions (NFT-based)\n" +
- " - NftV4: Uniswap V4 concentrated liquidity positions (NFT-based)\n\n" +
- "Each position includes the chain, USD value, protocol name, and type-\n" +
- "specific details (token identities, pool info, balances). The response\n" +
- "also includes aggregation summaries with total value and per-chain\n" +
- "breakdowns. Use -o json for the full structured response.\n\n" +
- "Run 'dune sim evm supported-chains' to see which chains support defi-positions.\n\n" +
- "Examples:\n" +
- " dune sim evm defi-positions 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
- " dune sim evm defi-positions 0xd8da... --chain-ids 1,8453\n" +
- " dune sim evm defi-positions 0xd8da... -o json",
- Args: cobra.ExactArgs(1),
- RunE: runDefiPositions,
- }
-
- cmd.Flags().String("chain-ids", "", "Restrict to specific chains by numeric ID or tag name (comma-separated, e.g. '1,8453' or 'default'); defaults to all chains tagged 'default'")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-// --- Response types ---
-
-type defiPositionsResponse struct {
- Positions []defiPosition `json:"positions"`
- Aggregations *defiAggregations `json:"aggregations,omitempty"`
- Warnings []warningEntry `json:"warnings,omitempty"`
-}
-
-type defiAggregations struct {
- TotalValueUSD float64 `json:"total_value_usd"`
- TotalByChain map[string]float64 `json:"total_by_chain,omitempty"`
-}
-
-// defiTokenInfo represents a token object returned by the API with address,
-// name, symbol, and optional numeric fields depending on position type.
-type defiTokenInfo struct {
- Address string `json:"address,omitempty"`
- Name string `json:"name,omitempty"`
- Symbol string `json:"symbol,omitempty"`
- Decimals int `json:"decimals,omitempty"`
- Holdings float64 `json:"holdings,omitempty"`
- PriceUSD float64 `json:"price_usd,omitempty"`
-}
-
-// nftTokenDetails holds per-token data inside an NFT concentrated-liquidity position.
-type nftTokenDetails struct {
- PriceUSD float64 `json:"price_usd"`
- Holdings float64 `json:"holdings,omitempty"`
- Rewards float64 `json:"rewards,omitempty"`
-}
-
-// defiPosition matches the polymorphic DefiPosition schema returned by the API.
-// Fields are optional depending on the `type` discriminator.
-type defiPosition struct {
- Type string `json:"type"`
- Chain string `json:"chain,omitempty"`
- ChainID int64 `json:"chain_id"`
- ValueUSD float64 `json:"value_usd"`
- Logo *string `json:"logo,omitempty"`
-
- // Erc4626 / Tokenized fields
- TokenType string `json:"token_type,omitempty"`
- Token *defiTokenInfo `json:"token,omitempty"`
- UnderlyingToken *defiTokenInfo `json:"underlying_token,omitempty"`
- LendingPool string `json:"lending_pool,omitempty"`
-
- // Erc4626 / Tokenized / UniswapV2 fields
- Balance float64 `json:"balance,omitempty"`
- PriceUSD float64 `json:"price_usd,omitempty"`
-
- // UniswapV2 / Nft / NftV4 fields
- Protocol string `json:"protocol,omitempty"`
- Pool string `json:"pool,omitempty"`
- PoolID string `json:"pool_id,omitempty"`
- PoolManager string `json:"pool_manager,omitempty"`
- Salt string `json:"salt,omitempty"`
- Token0 *defiTokenInfo `json:"token0,omitempty"`
- Token1 *defiTokenInfo `json:"token1,omitempty"`
- LPBalance string `json:"lp_balance,omitempty"`
-
- // Nft / NftV4 concentrated liquidity positions
- Positions []nftPositionDetails `json:"positions,omitempty"`
-}
-
-type nftPositionDetails struct {
- TickLower int `json:"tick_lower"`
- TickUpper int `json:"tick_upper"`
- TokenID string `json:"token_id"`
- Token0 *nftTokenDetails `json:"token0,omitempty"`
- Token1 *nftTokenDetails `json:"token1,omitempty"`
-}
-
-func runDefiPositions(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- address := args[0]
- params := url.Values{}
-
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
-
- data, err := client.Get(cmd.Context(), "/v1/evm/defi/positions/"+address, 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 defiPositionsResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- // Print warnings to stderr.
- printWarnings(cmd, resp.Warnings)
-
- if len(resp.Positions) == 0 {
- fmt.Fprintln(w, "No DeFi positions found.")
- return nil
- }
-
- columns := []string{"TYPE", "CHAIN_ID", "PROTOCOL", "USD_VALUE", "DETAILS"}
- rows := make([][]string, len(resp.Positions))
- for i, p := range resp.Positions {
- rows[i] = []string{
- p.Type,
- fmt.Sprintf("%d", p.ChainID),
- p.Protocol,
- output.FormatUSD(p.ValueUSD),
- positionDetails(p),
- }
- }
- output.PrintTable(w, columns, rows)
-
- // Print aggregation summary.
- printAggregations(w, resp.Aggregations)
-
- return nil
- }
-}
-
-// positionDetails returns a human-readable summary for a DeFi position,
-// varying by position type.
-// tokenSymbol safely extracts the symbol from a token info pointer.
-func tokenSymbol(t *defiTokenInfo) string {
- if t == nil {
- return ""
- }
- return t.Symbol
-}
-
-func positionDetails(p defiPosition) string {
- switch p.Type {
- case "Erc4626":
- parts := []string{}
- if sym := tokenSymbol(p.Token); sym != "" {
- parts = append(parts, sym)
- }
- if sym := tokenSymbol(p.UnderlyingToken); sym != "" {
- parts = append(parts, fmt.Sprintf("-> %s", sym))
- }
- if p.Balance != 0 {
- parts = append(parts, fmt.Sprintf("bal=%.6g", p.Balance))
- }
- return strings.Join(parts, " ")
-
- case "Tokenized":
- parts := []string{}
- if p.TokenType != "" {
- parts = append(parts, p.TokenType)
- }
- if sym := tokenSymbol(p.Token); sym != "" {
- parts = append(parts, sym)
- }
- if p.Balance != 0 {
- parts = append(parts, fmt.Sprintf("bal=%.6g", p.Balance))
- }
- return strings.Join(parts, " ")
-
- case "UniswapV2":
- pair := formatPair(tokenSymbol(p.Token0), tokenSymbol(p.Token1))
- if p.Balance != 0 {
- return fmt.Sprintf("%s bal=%.6g", pair, p.Balance)
- }
- return pair
-
- case "Nft", "NftV4":
- pair := formatPair(tokenSymbol(p.Token0), tokenSymbol(p.Token1))
- nPos := len(p.Positions)
- if nPos == 1 {
- return fmt.Sprintf("%s (1 position)", pair)
- }
- if nPos > 1 {
- return fmt.Sprintf("%s (%d positions)", pair, nPos)
- }
- return pair
-
- default:
- return ""
- }
-}
-
-// formatPair returns "SYM0/SYM1" or falls back to individual symbols.
-func formatPair(sym0, sym1 string) string {
- if sym0 != "" && sym1 != "" {
- return sym0 + "/" + sym1
- }
- if sym0 != "" {
- return sym0
- }
- return sym1
-}
-
-// printAggregations prints the aggregation summary after the positions table.
-func printAggregations(w io.Writer, agg *defiAggregations) {
- if agg == nil {
- return
- }
-
- fmt.Fprintf(w, "\nTotal USD Value: %s\n", output.FormatUSD(agg.TotalValueUSD))
-
- if len(agg.TotalByChain) > 0 {
- fmt.Fprintln(w, "Breakdown by chain:")
-
- // Sort chain IDs numerically for natural display order.
- chainIDs := make([]string, 0, len(agg.TotalByChain))
- for k := range agg.TotalByChain {
- chainIDs = append(chainIDs, k)
- }
- sort.Slice(chainIDs, func(i, j int) bool {
- a, errA := strconv.Atoi(chainIDs[i])
- b, errB := strconv.Atoi(chainIDs[j])
- if errA != nil || errB != nil {
- return chainIDs[i] < chainIDs[j] // fallback to lexicographic
- }
- return a < b
- })
-
- for _, cid := range chainIDs {
- fmt.Fprintf(w, " Chain %s: %s\n", cid, output.FormatUSD(agg.TotalByChain[cid]))
- }
- }
-}
diff --git a/cmd/sim/evm/defi_positions_test.go b/cmd/sim/evm/defi_positions_test.go
deleted file mode 100644
index df86b59..0000000
--- a/cmd/sim/evm/defi_positions_test.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmDefiPositions_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", "defi-positions", evmTestAddress})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- // Should contain table headers.
- assert.Contains(t, out, "TYPE")
- assert.Contains(t, out, "CHAIN_ID")
- assert.Contains(t, out, "USD_VALUE")
- assert.Contains(t, out, "DETAILS")
-}
-
-func TestEvmDefiPositions_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", "defi-positions", evmTestAddress, "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "positions")
-
- positions, ok := resp["positions"].([]interface{})
- require.True(t, ok)
- if len(positions) > 0 {
- p, ok := positions[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, p, "type")
- assert.Contains(t, p, "chain_id")
- assert.Contains(t, p, "usd_value")
- }
-}
-
-func TestEvmDefiPositions_WithChainIDs(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "defi-positions", 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, "positions")
-
- // All positions should be on chain 1.
- positions, ok := resp["positions"].([]interface{})
- require.True(t, ok)
- for _, pos := range positions {
- p, ok := pos.(map[string]interface{})
- require.True(t, ok)
- chainID, ok := p["chain_id"].(float64)
- if ok {
- assert.Equal(t, float64(1), chainID)
- }
- }
-}
-
-func TestEvmDefiPositions_Aggregations(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "defi-positions", evmTestAddress, "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- // Check aggregations object.
- agg, ok := resp["aggregations"].(map[string]interface{})
- if ok {
- assert.Contains(t, agg, "total_usd_value")
- }
-}
-
-func TestEvmDefiPositions_TextAggregationSummary(t *testing.T) {
- key := simAPIKey(t)
-
- // First check via JSON whether aggregations are present for this address.
- jsonRoot := newSimTestRoot()
- var jsonBuf bytes.Buffer
- jsonRoot.SetOut(&jsonBuf)
- jsonRoot.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "defi-positions", evmTestAddress, "-o", "json"})
- require.NoError(t, jsonRoot.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(jsonBuf.Bytes(), &resp))
- if _, ok := resp["aggregations"]; !ok {
- t.Skip("API did not return aggregations for this address, skipping text aggregation test")
- }
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "defi-positions", evmTestAddress})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- // When aggregations are present, the summary should appear in text output.
- assert.Contains(t, out, "Total USD Value:")
-}
diff --git a/cmd/sim/evm/defi_positions_unit_test.go b/cmd/sim/evm/defi_positions_unit_test.go
deleted file mode 100644
index 55f3f1d..0000000
--- a/cmd/sim/evm/defi_positions_unit_test.go
+++ /dev/null
@@ -1,256 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// apiResponseJSON is the exact JSON returned by the defi-positions API endpoint,
-// used to verify that our structs can unmarshal every field correctly.
-const apiResponseJSON = `{"positions":[{"type":"Erc4626","chain":"ethereum","chain_id":1,"token":{"address":"0xa3931d71877c0e7a3148cb7eb4463524fec27fbd","name":"Savings USDS","symbol":"sUSDS"},"underlying_token":{"address":"0xdc035d45d973e3ec169d2276ddab16f1e407384f","name":"USDS Stablecoin","symbol":"USDS","decimals":18,"holdings":47.00372505463423},"balance":43.091714709517426,"price_usd":1.0906557333153313,"value_usd":46.99822570632377,"logo":"https://api.sim.dune.com/beta/token/logo/1/0xdc035d45d973e3ec169d2276ddab16f1e407384f"},{"type":"Tokenized","chain":"ethereum","chain_id":1,"token_type":"AtokenV2","token":{"address":"0x030ba81f1c18d280636f32af80b9aad02cf0854e","name":"Aave interest bearing WETH","symbol":"aWETH"},"underlying_token":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","holdings":0.0515820177322061},"lending_pool":"0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9","balance":0.04961716041594229,"price_usd":2257.7211358982086,"value_usd":112.02171177432484,"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"},{"type":"UniswapV2","chain":"ethereum","chain_id":1,"protocol":"ShibaSwapV2","pool":"0x76ec974feaf0293f64cf8643e0f42dea5b71689b","token0":{"address":"0x198065e69a86cb8a9154b333aad8efe7a3c256f8","name":"KOYO","symbol":"KOY","decimals":18,"price_usd":0.00009108374058938265,"holdings":72267.17195098454},"token1":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18,"price_usd":2171.720237,"holdings":0.00297532741832308},"lp_balance":"0x8ac7230489e80000","balance":10.0,"price_usd":1.3043943109184983,"value_usd":13.043943109184983,"logo":"https://api.sim.dune.com/beta/token/logo/1/0x198065e69a86cb8a9154b333aad8efe7a3c256f8"},{"type":"UniswapV2","chain":"ethereum","chain_id":1,"protocol":"UniswapV2","pool":"0x09c29277d081a1b347f41277ff53116a30d4ddff","token0":{"address":"0x4206975c6d7135ad73129476ebe2b06e42f41f50","name":"FWOG","symbol":"FWOG","decimals":18,"price_usd":2.3754275952306002e-11,"holdings":609179876998.8339},"token1":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18,"price_usd":2171.720237,"holdings":0.006683389542586471},"lp_balance":"0xca7455529bd53680000","balance":59754.0,"price_usd":0.00048507345490195365,"value_usd":28.98507922421134,"logo":null},{"type":"Nft","chain":"ethereum","chain_id":1,"protocol":"UniswapV3","pool":"0x7625d7f67e4e44341ddfb1e698801fd5a1574b48","token0":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18},"token1":{"address":"0xd78959df1ff28b45e6b4ea234bdcf9d6609d16e1","name":"Moneda de Caca","symbol":"Mierda","decimals":18},"positions":[{"tick_lower":0,"tick_upper":184200,"token_id":"0xe8ac0","token0":{"price_usd":2171.720237,"holdings":0.0,"rewards":0.000201014351146556},"token1":{"price_usd":0.0,"holdings":100000000.0,"rewards":19678.323702556045}}],"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","value_usd":0.43654693431239977},{"type":"NftV4","chain":"ethereum","chain_id":1,"protocol":"UniswapV4","pool_id":"0x21fb293b9dc53b42fa6e63fa24e1212de76c88eb7a15b94cd220fc66274851bf","pool_manager":"0x000000000004444c5dc75cb358380d2e3de08a90","salt":"0x0000000000000000000000000000000000000000000000000000000000002118","token0":{"address":"0x0000000000000000000000000000000000000000","name":"Ether","symbol":"ETH","decimals":18},"token1":{"address":"0xf9c8631fba291bac14ed549a2dde7c7f2ddff1a8","name":"Mighty Morphin Power Rangers","symbol":"GoGo","decimals":18},"positions":[{"tick_lower":-184220,"tick_upper":207220,"token_id":"0x2118","token0":{"price_usd":2171.720237,"holdings":0.000252141460675072,"rewards":8.852246853764e-6},"token1":{"price_usd":0.0,"holdings":479748570.0393271,"rewards":8399.799985973925}}],"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","value_usd":0.5668053163700324}],"aggregations":{"total_value_usd":6153.428535761592,"total_by_chain":{"1":4206.858757536072,"8453":1946.5539365060883,"42161":0.015841719431772122}}}`
-
-func TestUnmarshal_FullAPIResponse(t *testing.T) {
- var resp defiPositionsResponse
- err := json.Unmarshal([]byte(apiResponseJSON), &resp)
- require.NoError(t, err, "unmarshal must not fail on real API response")
-
- require.Len(t, resp.Positions, 6)
-
- // --- Erc4626 ---
- erc := resp.Positions[0]
- assert.Equal(t, "Erc4626", erc.Type)
- assert.Equal(t, "ethereum", erc.Chain)
- assert.Equal(t, int64(1), erc.ChainID)
- assert.InDelta(t, 46.998, erc.ValueUSD, 0.01)
- assert.InDelta(t, 43.0917, erc.Balance, 0.001)
- assert.InDelta(t, 1.0906, erc.PriceUSD, 0.001)
- require.NotNil(t, erc.Logo)
- assert.Contains(t, *erc.Logo, "api.sim.dune.com")
- // token
- require.NotNil(t, erc.Token)
- assert.Equal(t, "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd", erc.Token.Address)
- assert.Equal(t, "Savings USDS", erc.Token.Name)
- assert.Equal(t, "sUSDS", erc.Token.Symbol)
- // underlying_token
- require.NotNil(t, erc.UnderlyingToken)
- assert.Equal(t, "0xdc035d45d973e3ec169d2276ddab16f1e407384f", erc.UnderlyingToken.Address)
- assert.Equal(t, "USDS Stablecoin", erc.UnderlyingToken.Name)
- assert.Equal(t, "USDS", erc.UnderlyingToken.Symbol)
- assert.Equal(t, 18, erc.UnderlyingToken.Decimals)
- assert.InDelta(t, 47.003, erc.UnderlyingToken.Holdings, 0.001)
-
- // --- Tokenized ---
- tok := resp.Positions[1]
- assert.Equal(t, "Tokenized", tok.Type)
- assert.Equal(t, "AtokenV2", tok.TokenType)
- assert.Equal(t, "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9", tok.LendingPool)
- assert.InDelta(t, 0.04961, tok.Balance, 0.0001)
- assert.InDelta(t, 2257.72, tok.PriceUSD, 0.01)
- assert.InDelta(t, 112.02, tok.ValueUSD, 0.01)
- require.NotNil(t, tok.Token)
- assert.Equal(t, "aWETH", tok.Token.Symbol)
- assert.Equal(t, "Aave interest bearing WETH", tok.Token.Name)
- require.NotNil(t, tok.UnderlyingToken)
- assert.Equal(t, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", tok.UnderlyingToken.Address)
- assert.InDelta(t, 0.05158, tok.UnderlyingToken.Holdings, 0.0001)
-
- // --- UniswapV2 (with logo) ---
- uni := resp.Positions[2]
- assert.Equal(t, "UniswapV2", uni.Type)
- assert.Equal(t, "ShibaSwapV2", uni.Protocol)
- assert.Equal(t, "0x76ec974feaf0293f64cf8643e0f42dea5b71689b", uni.Pool)
- assert.Equal(t, "0x8ac7230489e80000", uni.LPBalance)
- assert.InDelta(t, 10.0, uni.Balance, 0.001)
- require.NotNil(t, uni.Token0)
- assert.Equal(t, "KOY", uni.Token0.Symbol)
- assert.Equal(t, 18, uni.Token0.Decimals)
- assert.InDelta(t, 0.0000911, uni.Token0.PriceUSD, 0.00001)
- assert.InDelta(t, 72267.17, uni.Token0.Holdings, 0.01)
- require.NotNil(t, uni.Token1)
- assert.Equal(t, "WETH", uni.Token1.Symbol)
- assert.InDelta(t, 2171.72, uni.Token1.PriceUSD, 0.01)
-
- // --- UniswapV2 (logo: null) ---
- uniNull := resp.Positions[3]
- assert.Equal(t, "UniswapV2", uniNull.Type)
- assert.Nil(t, uniNull.Logo, "null logo should be nil pointer")
- assert.InDelta(t, 59754.0, uniNull.Balance, 0.1)
-
- // --- Nft (UniswapV3) ---
- nft := resp.Positions[4]
- assert.Equal(t, "Nft", nft.Type)
- assert.Equal(t, "UniswapV3", nft.Protocol)
- assert.Equal(t, "0x7625d7f67e4e44341ddfb1e698801fd5a1574b48", nft.Pool)
- require.NotNil(t, nft.Token0)
- assert.Equal(t, "WETH", nft.Token0.Symbol)
- require.NotNil(t, nft.Token1)
- assert.Equal(t, "Mierda", nft.Token1.Symbol)
- assert.InDelta(t, 0.4365, nft.ValueUSD, 0.001)
- // NFT position details
- require.Len(t, nft.Positions, 1)
- nftPos := nft.Positions[0]
- assert.Equal(t, 0, nftPos.TickLower)
- assert.Equal(t, 184200, nftPos.TickUpper)
- assert.Equal(t, "0xe8ac0", nftPos.TokenID)
- require.NotNil(t, nftPos.Token0)
- assert.InDelta(t, 2171.72, nftPos.Token0.PriceUSD, 0.01)
- assert.InDelta(t, 0.0, nftPos.Token0.Holdings, 0.001)
- assert.InDelta(t, 0.000201, nftPos.Token0.Rewards, 0.00001)
- require.NotNil(t, nftPos.Token1)
- assert.InDelta(t, 0.0, nftPos.Token1.PriceUSD, 0.001)
- assert.InDelta(t, 100000000.0, nftPos.Token1.Holdings, 1.0)
- assert.InDelta(t, 19678.32, nftPos.Token1.Rewards, 0.01)
-
- // --- NftV4 (UniswapV4) ---
- nft4 := resp.Positions[5]
- assert.Equal(t, "NftV4", nft4.Type)
- assert.Equal(t, "UniswapV4", nft4.Protocol)
- assert.Equal(t, "0x21fb293b9dc53b42fa6e63fa24e1212de76c88eb7a15b94cd220fc66274851bf", nft4.PoolID)
- assert.Equal(t, "0x000000000004444c5dc75cb358380d2e3de08a90", nft4.PoolManager)
- assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000002118", nft4.Salt)
- require.NotNil(t, nft4.Token0)
- assert.Equal(t, "ETH", nft4.Token0.Symbol)
- assert.Equal(t, "0x0000000000000000000000000000000000000000", nft4.Token0.Address)
- require.NotNil(t, nft4.Token1)
- assert.Equal(t, "GoGo", nft4.Token1.Symbol)
- assert.InDelta(t, 0.5668, nft4.ValueUSD, 0.001)
- require.Len(t, nft4.Positions, 1)
- nft4Pos := nft4.Positions[0]
- assert.Equal(t, -184220, nft4Pos.TickLower)
- assert.Equal(t, 207220, nft4Pos.TickUpper)
- assert.Equal(t, "0x2118", nft4Pos.TokenID)
- require.NotNil(t, nft4Pos.Token0)
- assert.InDelta(t, 0.000252, nft4Pos.Token0.Holdings, 0.00001)
- assert.InDelta(t, 8.852e-6, nft4Pos.Token0.Rewards, 1e-7)
-
- // --- Aggregations ---
- require.NotNil(t, resp.Aggregations)
- assert.InDelta(t, 6153.43, resp.Aggregations.TotalValueUSD, 0.01)
- assert.Len(t, resp.Aggregations.TotalByChain, 3)
- assert.InDelta(t, 4206.86, resp.Aggregations.TotalByChain["1"], 0.01)
- assert.InDelta(t, 1946.55, resp.Aggregations.TotalByChain["8453"], 0.01)
- assert.InDelta(t, 0.01584, resp.Aggregations.TotalByChain["42161"], 0.0001)
-}
-
-func TestPositionDetails_Erc4626(t *testing.T) {
- p := defiPosition{
- Type: "Erc4626",
- Token: &defiTokenInfo{Symbol: "alUSD"},
- UnderlyingToken: &defiTokenInfo{Symbol: "USDC"},
- Balance: 0.0673736869415349,
- }
- got := positionDetails(p)
- assert.Contains(t, got, "alUSD")
- assert.Contains(t, got, "-> USDC")
- assert.Contains(t, got, "bal=")
-}
-
-func TestPositionDetails_Erc4626_NoBalance(t *testing.T) {
- p := defiPosition{
- Type: "Erc4626",
- Token: &defiTokenInfo{Symbol: "yvDAI"},
- UnderlyingToken: &defiTokenInfo{Symbol: "DAI"},
- }
- assert.Equal(t, "yvDAI -> DAI", positionDetails(p))
-}
-
-func TestPositionDetails_Tokenized(t *testing.T) {
- p := defiPosition{
- Type: "Tokenized",
- TokenType: "AtokenV2",
- Token: &defiTokenInfo{Symbol: "aWETH"},
- Balance: 0.0496171604159423,
- }
- got := positionDetails(p)
- assert.Contains(t, got, "AtokenV2")
- assert.Contains(t, got, "aWETH")
- assert.Contains(t, got, "bal=")
-}
-
-func TestPositionDetails_Tokenized_NoBalance(t *testing.T) {
- p := defiPosition{
- Type: "Tokenized",
- TokenType: "AtokenV2",
- Token: &defiTokenInfo{Symbol: "aWBTC"},
- }
- assert.Equal(t, "AtokenV2 aWBTC", positionDetails(p))
-}
-
-func TestPositionDetails_UniswapV2(t *testing.T) {
- p := defiPosition{
- Type: "UniswapV2",
- Token0: &defiTokenInfo{Symbol: "FWOG"},
- Token1: &defiTokenInfo{Symbol: "WETH"},
- Balance: 59754,
- }
- got := positionDetails(p)
- assert.Contains(t, got, "FWOG/WETH")
- assert.Contains(t, got, "bal=59754")
-}
-
-func TestPositionDetails_UniswapV2_NoBalance(t *testing.T) {
- p := defiPosition{
- Type: "UniswapV2",
- Token0: &defiTokenInfo{Symbol: "USDC"},
- Token1: &defiTokenInfo{Symbol: "WETH"},
- }
- assert.Equal(t, "USDC/WETH", positionDetails(p))
-}
-
-func TestPositionDetails_Nft(t *testing.T) {
- p := defiPosition{
- Type: "Nft",
- Token0: &defiTokenInfo{Symbol: "WETH"},
- Token1: &defiTokenInfo{Symbol: "USDC"},
- Positions: []nftPositionDetails{
- {TickLower: -100, TickUpper: 100, TokenID: "0x1"},
- {TickLower: -200, TickUpper: 200, TokenID: "0x2"},
- {TickLower: -300, TickUpper: 300, TokenID: "0x3"},
- },
- }
- assert.Equal(t, "WETH/USDC (3 positions)", positionDetails(p))
-}
-
-func TestPositionDetails_NftV4(t *testing.T) {
- p := defiPosition{
- Type: "NftV4",
- Token0: &defiTokenInfo{Symbol: "WBTC"},
- Token1: &defiTokenInfo{Symbol: "WETH"},
- Positions: []nftPositionDetails{
- {TickLower: -50, TickUpper: 50, TokenID: "0xabc"},
- },
- }
- assert.Equal(t, "WBTC/WETH (1 position)", positionDetails(p))
-}
-
-func TestPositionDetails_NftNoPositions(t *testing.T) {
- p := defiPosition{
- Type: "Nft",
- Token0: &defiTokenInfo{Symbol: "DAI"},
- Token1: &defiTokenInfo{Symbol: "USDC"},
- }
- assert.Equal(t, "DAI/USDC", positionDetails(p))
-}
-
-func TestPositionDetails_Unknown(t *testing.T) {
- p := defiPosition{Type: "SomeNewType"}
- assert.Equal(t, "", positionDetails(p))
-}
-
-func TestFormatPair(t *testing.T) {
- assert.Equal(t, "WETH/USDC", formatPair("WETH", "USDC"))
- assert.Equal(t, "WETH", formatPair("WETH", ""))
- assert.Equal(t, "USDC", formatPair("", "USDC"))
- assert.Equal(t, "", formatPair("", ""))
-}
-
-func TestTokenSymbol_Nil(t *testing.T) {
- assert.Equal(t, "", tokenSymbol(nil))
-}
-
-func TestTokenSymbol_NonNil(t *testing.T) {
- assert.Equal(t, "ETH", tokenSymbol(&defiTokenInfo{Symbol: "ETH"}))
-}
diff --git a/cmd/sim/evm/evm.go b/cmd/sim/evm/evm.go
deleted file mode 100644
index d64cbda..0000000
--- a/cmd/sim/evm/evm.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package evm
-
-import (
- "context"
- "net/url"
-
- "github.com/duneanalytics/cli/cmdutil"
- "github.com/spf13/cobra"
-)
-
-// SimClient is the interface that evm commands use to talk to the Sim API.
-// It is satisfied by *sim.SimClient (stored in the command context by
-// the sim parent command's PersistentPreRunE).
-type SimClient interface {
- Get(ctx context.Context, path string, params url.Values) ([]byte, error)
-}
-
-// SimClientFromCmd extracts the SimClient from the command context.
-func SimClientFromCmd(cmd *cobra.Command) SimClient {
- v := cmdutil.SimClientFromCmd(cmd)
- if v == nil {
- return nil
- }
- c, ok := v.(SimClient)
- if !ok {
- return nil
- }
- return c
-}
-
-// NewEvmCmd returns the `sim evm` parent command.
-func NewEvmCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "evm",
- Short: "Query EVM chain data (balances, activity, transactions, tokens, NFTs, DeFi)",
- Long: "Access real-time, indexed EVM blockchain data. All commands accept an\n" +
- "Ethereum-style address (0x...) as the primary argument and return data\n" +
- "across multiple EVM chains simultaneously.\n\n" +
- "Available subcommands:\n" +
- " supported-chains - List all supported EVM chains and endpoint availability (public, no auth)\n" +
- " balances - Native + ERC20 token balances with USD valuations\n" +
- " balance - Single-token balance lookup on one chain\n" +
- " stablecoins - Stablecoin-only balances (USDC, USDT, DAI, etc.)\n" +
- " activity - Chronological feed of transfers, swaps, mints, burns, approvals\n" +
- " transactions - Raw transaction history with optional ABI decoding\n" +
- " collectibles - ERC721 and ERC1155 NFT holdings with spam filtering\n" +
- " token-info - Token metadata, pricing, supply, and market cap\n" +
- " token-holders - Top holders of an ERC20 token ranked by balance\n" +
- " defi-positions - DeFi positions across lending, AMM, and vault protocols (beta)\n" +
- " supported-protocols - DeFi protocol families and chains covered by defi-positions\n\n" +
- "Most commands support --chain-ids to restrict results to specific networks.\n" +
- "Run 'dune sim evm supported-chains' to discover valid chain IDs, tags, and\n" +
- "which endpoints are available per chain.\n\n" +
- "All commands except 'supported-chains' require a Sim API key.",
- }
-
- cmd.AddCommand(NewSupportedChainsCmd())
- cmd.AddCommand(NewBalancesCmd())
- cmd.AddCommand(NewBalanceCmd())
- cmd.AddCommand(NewStablecoinsCmd())
- cmd.AddCommand(NewActivityCmd())
- cmd.AddCommand(NewTransactionsCmd())
- cmd.AddCommand(NewCollectiblesCmd())
- cmd.AddCommand(NewTokenInfoCmd())
- cmd.AddCommand(NewTokenHoldersCmd())
- cmd.AddCommand(NewDefiPositionsCmd())
- cmd.AddCommand(NewSupportedProtocolsCmd())
-
- return cmd
-}
diff --git a/cmd/sim/evm/helpers_test.go b/cmd/sim/evm/helpers_test.go
deleted file mode 100644
index 1a5454c..0000000
--- a/cmd/sim/evm/helpers_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package evm_test
-
-import (
- "context"
- "os"
- "testing"
-
- "github.com/duneanalytics/cli/cmd/sim"
- "github.com/duneanalytics/cli/cmd/sim/evm"
- "github.com/spf13/cobra"
-)
-
-// simAPIKey returns the DUNE_SIM_API_KEY env var or skips the test.
-func simAPIKey(t *testing.T) string {
- t.Helper()
- key := os.Getenv("DUNE_SIM_API_KEY")
- if key == "" {
- t.Skip("DUNE_SIM_API_KEY not set, skipping e2e test")
- }
- return key
-}
-
-const evmTestAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
-
-// newEvmTestRoot builds a minimal command tree: dune -> evm -> .
-// No sim parent — used for public endpoints that don't require auth.
-func newEvmTestRoot() *cobra.Command {
- root := &cobra.Command{Use: "dune"}
- root.SetContext(context.Background())
- root.AddCommand(evm.NewEvmCmd())
- return root
-}
-
-// newSimTestRoot builds the full command tree: dune -> sim -> evm -> .
-// Used for authenticated E2E tests. Pass the API key via --sim-api-key in SetArgs.
-func newSimTestRoot() *cobra.Command {
- root := &cobra.Command{Use: "dune"}
- root.SetContext(context.Background())
- root.AddCommand(sim.NewSimCmd())
- return root
-}
diff --git a/cmd/sim/evm/stablecoins.go b/cmd/sim/evm/stablecoins.go
deleted file mode 100644
index 9cdfde7..0000000
--- a/cmd/sim/evm/stablecoins.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package evm
-
-import (
- "github.com/spf13/cobra"
-)
-
-// NewStablecoinsCmd returns the `sim evm stablecoins` command.
-func NewStablecoinsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "stablecoins ",
- Short: "Get stablecoin balances for a wallet address across multiple chains",
- Long: "Return only stablecoin balances (USDC, USDT, DAI, FRAX, etc.) for the given\n" +
- "wallet address across supported EVM chains. This is a convenience shorthand\n" +
- "for 'dune sim evm balances --asset-class stablecoin'.\n\n" +
- "Each balance entry includes the token amount, current USD price, and total\n" +
- "USD value. The same response format and pagination as 'dune sim evm balances'\n" +
- "applies.\n\n" +
- "By default, queries all chains tagged 'default'. Use --chain-ids to restrict\n" +
- "to specific networks. Run 'dune sim evm supported-chains' for valid IDs.\n\n" +
- "For all token balances (not just stablecoins), use 'dune sim evm balances'.\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... --exclude-spam -o json",
- Args: cobra.ExactArgs(1),
- RunE: runStablecoins,
- }
-
- addBalanceFlags(cmd)
-
- return cmd
-}
-
-func runStablecoins(cmd *cobra.Command, args []string) error {
- return runBalancesEndpoint(cmd, args, "/v1/evm/balances/", "/stablecoins")
-}
diff --git a/cmd/sim/evm/stablecoins_test.go b/cmd/sim/evm/stablecoins_test.go
deleted file mode 100644
index 7dd4888..0000000
--- a/cmd/sim/evm/stablecoins_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-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")
-}
diff --git a/cmd/sim/evm/supported_chains.go b/cmd/sim/evm/supported_chains.go
deleted file mode 100644
index 9d6df7e..0000000
--- a/cmd/sim/evm/supported_chains.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewSupportedChainsCmd returns the `sim evm supported-chains` command.
-func NewSupportedChainsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "supported-chains",
- Short: "List supported EVM chains and their endpoint availability",
- Long: "Display all EVM chains supported by the Sim API along with a capability\n" +
- "matrix showing which endpoints are available for each chain. Each chain\n" +
- "entry includes the chain name, numeric ID, tags, and boolean flags\n" +
- "indicating which Sim API endpoints are supported.\n\n" +
- "This is a public endpoint and does not require a Sim API key.\n\n" +
- "Use the chain IDs or tag names from this output as --chain-ids\n" +
- "arguments in other 'dune sim evm' commands.\n\n" +
- "Examples:\n" +
- " dune sim evm supported-chains\n" +
- " dune sim evm supported-chains -o json",
- Annotations: map[string]string{"skipSimAuth": "true"},
- RunE: runSupportedChains,
- }
-
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type supportedChainsResponse struct {
- Chains []chainEntry `json:"chains"`
-}
-
-type chainEntry struct {
- Name string `json:"name"`
- ChainID json.Number `json:"chain_id"`
- Tags []string `json:"tags"`
- Balances endpointSupport `json:"balances"`
- Activity endpointSupport `json:"activity"`
- Transactions endpointSupport `json:"transactions"`
- TokenInfo endpointSupport `json:"token_info"`
- TokenHolders endpointSupport `json:"token_holders"`
- Collectibles endpointSupport `json:"collectibles"`
- DefiPositions endpointSupport `json:"defi_positions"`
-}
-
-type endpointSupport struct {
- Supported bool `json:"supported"`
-}
-
-func runSupportedChains(cmd *cobra.Command, _ []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- data, err := client.Get(cmd.Context(), "/v1/evm/supported-chains", nil)
- 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 supportedChainsResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- columns := []string{
- "NAME", "CHAIN_ID", "TAGS",
- "BALANCES", "ACTIVITY", "TXS",
- "TOKEN_INFO", "HOLDERS", "COLLECTIBLES", "DEFI",
- }
- rows := make([][]string, len(resp.Chains))
- for i, c := range resp.Chains {
- rows[i] = []string{
- c.Name,
- c.ChainID.String(),
- strings.Join(c.Tags, ","),
- boolYN(c.Balances.Supported),
- boolYN(c.Activity.Supported),
- boolYN(c.Transactions.Supported),
- boolYN(c.TokenInfo.Supported),
- boolYN(c.TokenHolders.Supported),
- boolYN(c.Collectibles.Supported),
- boolYN(c.DefiPositions.Supported),
- }
- }
- output.PrintTable(w, columns, rows)
- return nil
- }
-}
-
-func boolYN(b bool) string {
- if b {
- return "Y"
- }
- return "N"
-}
diff --git a/cmd/sim/evm/supported_chains_test.go b/cmd/sim/evm/supported_chains_test.go
deleted file mode 100644
index adaf9ae..0000000
--- a/cmd/sim/evm/supported_chains_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// supported-chains is a public endpoint — no API key required.
-
-func TestSupportedChains_Text(t *testing.T) {
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "evm", "supported-chains"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "NAME")
- assert.Contains(t, out, "CHAIN_ID")
- assert.Contains(t, out, "BALANCES")
- assert.Contains(t, out, "ethereum")
-}
-
-func TestSupportedChains_JSON(t *testing.T) {
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "evm", "supported-chains", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "chains")
-
- chains, ok := resp["chains"].([]interface{})
- require.True(t, ok, "chains should be an array")
- require.NotEmpty(t, chains, "should have at least one chain")
-
- first, ok := chains[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, first, "name")
- assert.Contains(t, first, "chain_id")
-}
diff --git a/cmd/sim/evm/supported_protocols.go b/cmd/sim/evm/supported_protocols.go
deleted file mode 100644
index d372bc5..0000000
--- a/cmd/sim/evm/supported_protocols.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
- "strings"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// Chain-status values returned by the API are lowercase strings.
-const protocolStatusPreview = "preview"
-
-// NewSupportedProtocolsCmd returns the `sim evm supported-protocols` command.
-func NewSupportedProtocolsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "supported-protocols",
- Short: "List DeFi protocol families and chains supported by defi-positions",
- Long: "Display DeFi protocol families covered by the Sim defi-positions\n" +
- "endpoint, the chains each family is available on, and the sub-protocols\n" +
- "(forks) recognized under each family. Each chain entry has a status of\n" +
- "Stable or Preview.\n\n" +
- "Use this to discover which protocols and chains 'dune sim evm defi-positions'\n" +
- "can return data for.\n\n" +
- "Examples:\n" +
- " dune sim evm supported-protocols\n" +
- " dune sim evm supported-protocols --include-preview-chains\n" +
- " dune sim evm supported-protocols --include-preview-protocols\n" +
- " dune sim evm supported-protocols -o json",
- RunE: runSupportedProtocols,
- }
-
- cmd.Flags().Bool("include-preview-chains", false, "Include chains that are marked as preview (not yet publicly available)")
- cmd.Flags().Bool("include-preview-protocols", false, "Include protocols that are marked as preview on the requested chains")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type supportedProtocolsResponse struct {
- ProtocolFamilies []supportedProtocolFamily `json:"protocol_families"`
-}
-
-type supportedProtocolFamily struct {
- Family string `json:"family"`
- Chains []supportedProtocolChain `json:"chains"`
- SubProtocols []string `json:"sub_protocols"`
-}
-
-type supportedProtocolChain struct {
- ChainID json.Number `json:"chain_id"`
- ChainName string `json:"chain_name"`
- Status string `json:"status"`
-}
-
-func runSupportedProtocols(cmd *cobra.Command, _ []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- params := url.Values{}
- if v, _ := cmd.Flags().GetBool("include-preview-chains"); v {
- params.Set("include_preview_chains", "true")
- }
- if v, _ := cmd.Flags().GetBool("include-preview-protocols"); v {
- params.Set("include_preview_protocols", "true")
- }
-
- data, err := client.Get(cmd.Context(), "/v1/evm/defi/supported-protocols", 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 supportedProtocolsResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- columns := []string{"FAMILY", "CHAINS", "SUB_PROTOCOLS"}
- rows := make([][]string, len(resp.ProtocolFamilies))
- for i, f := range resp.ProtocolFamilies {
- rows[i] = []string{
- f.Family,
- formatProtocolChains(f.Chains),
- strings.Join(f.SubProtocols, ","),
- }
- }
- output.PrintTable(w, columns, rows)
- return nil
- }
-}
-
-// formatProtocolChains renders chains as "name(id)" with a "*" suffix for
-// preview-status entries, joined by commas.
-func formatProtocolChains(chains []supportedProtocolChain) string {
- parts := make([]string, len(chains))
- for i, c := range chains {
- entry := fmt.Sprintf("%s(%s)", c.ChainName, c.ChainID.String())
- if strings.EqualFold(c.Status, protocolStatusPreview) {
- entry += "*"
- }
- parts[i] = entry
- }
- return strings.Join(parts, ",")
-}
diff --git a/cmd/sim/evm/supported_protocols_test.go b/cmd/sim/evm/supported_protocols_test.go
deleted file mode 100644
index 56697d5..0000000
--- a/cmd/sim/evm/supported_protocols_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmSupportedProtocols_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", "supported-protocols"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "FAMILY")
- assert.Contains(t, out, "CHAINS")
- assert.Contains(t, out, "SUB_PROTOCOLS")
-}
-
-func TestEvmSupportedProtocols_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", "supported-protocols", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "protocol_families")
-
- families, ok := resp["protocol_families"].([]interface{})
- require.True(t, ok, "protocol_families should be an array")
- require.NotEmpty(t, families, "should have at least one protocol family")
-
- first, ok := families[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, first, "family")
- assert.Contains(t, first, "chains")
- assert.Contains(t, first, "sub_protocols")
-
- chains, ok := first["chains"].([]interface{})
- require.True(t, ok, "chains should be an array")
- if len(chains) > 0 {
- c, ok := chains[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, c, "chain_id")
- assert.Contains(t, c, "chain_name")
- assert.Contains(t, c, "status")
- }
-}
-
-func TestEvmSupportedProtocols_IncludePreviewChainsFlag(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{
- "sim", "--sim-api-key", key, "evm", "supported-protocols",
- "--include-preview-chains", "-o", "json",
- })
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "protocol_families")
-}
diff --git a/cmd/sim/evm/token_holders.go b/cmd/sim/evm/token_holders.go
deleted file mode 100644
index acf221a..0000000
--- a/cmd/sim/evm/token_holders.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
- "strconv"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewTokenHoldersCmd returns the `sim evm token-holders` command.
-func NewTokenHoldersCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "token-holders ",
- Short: "Get the top holders of an ERC20 token ranked by balance",
- Long: "Return a leaderboard of holders for a given ERC20 token contract on a single\n" +
- "EVM chain, ranked by balance in descending order. Useful for analyzing token\n" +
- "distribution, identifying whales, and checking concentration.\n\n" +
- "Both --chain-id (single numeric chain ID) and the token address argument are\n" +
- "required. Unlike other sim evm commands that accept --chain-ids (plural),\n" +
- "this command uses --chain-id (singular) and queries exactly one chain.\n\n" +
- "Each holder entry includes the wallet address, token balance, earliest\n" +
- "acquisition timestamp, and whether they have ever initiated an outgoing\n" +
- "transfer (useful for distinguishing active holders from airdrop recipients).\n\n" +
- "Results are paginated; use --offset with the next_offset value from a\n" +
- "previous response to retrieve additional pages.\n\n" +
- "Run 'dune sim evm supported-chains' to see which chains support token-holders.\n\n" +
- "Examples:\n" +
- " dune sim evm token-holders 0x63706e401c06ac8513145b7687A14804d17f814b --chain-id 8453\n" +
- " dune sim evm token-holders 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --chain-id 1 --limit 50\n" +
- " dune sim evm token-holders 0x63706e... --chain-id 8453 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runTokenHolders,
- }
-
- cmd.Flags().String("chain-id", "", "Numeric EVM chain ID to query (required, single value, e.g. '1' for Ethereum, '8453' for Base); note: singular --chain-id, not --chain-ids")
- cmd.Flags().Int("limit", 0, "Maximum number of holders to return per page (1-500, default: 500)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- _ = cmd.MarkFlagRequired("chain-id")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type tokenHoldersResponse struct {
- TokenAddress string `json:"token_address"`
- ChainID int64 `json:"chain_id"`
- Holders []holder `json:"holders"`
- NextOffset string `json:"next_offset,omitempty"`
-}
-
-type holder struct {
- WalletAddress string `json:"wallet_address"`
- Balance string `json:"balance"`
- FirstAcquired string `json:"first_acquired,omitempty"`
- HasInitiatedTransfer bool `json:"has_initiated_transfer"`
-}
-
-func runTokenHolders(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- tokenAddress := args[0]
- chainID, _ := cmd.Flags().GetString("chain-id")
- // Validate chain_id is a valid integer.
- if _, err := strconv.Atoi(chainID); err != nil {
- return fmt.Errorf("--chain-id must be a numeric value, got %q", chainID)
- }
-
- params := url.Values{}
- 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)
- }
-
- path := fmt.Sprintf("/v1/evm/token-holders/%s/%s", chainID, 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 tokenHoldersResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- if len(resp.Holders) == 0 {
- fmt.Fprintln(w, "No holders found.")
- return nil
- }
-
- columns := []string{"WALLET_ADDRESS", "BALANCE", "FIRST_ACQUIRED", "HAS_TRANSFERRED"}
- rows := make([][]string, len(resp.Holders))
- for i, h := range resp.Holders {
- transferred := "N"
- if h.HasInitiatedTransfer {
- transferred = "Y"
- }
- rows[i] = []string{
- h.WalletAddress,
- h.Balance,
- h.FirstAcquired,
- transferred,
- }
- }
- 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/token_holders_test.go b/cmd/sim/evm/token_holders_test.go
deleted file mode 100644
index d0605f3..0000000
--- a/cmd/sim/evm/token_holders_test.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// Test token: a token on Base with known holders.
-const tokenHoldersChainID = "8453"
-const tokenHoldersAddress = "0x63706e401c06ac8513145b7687A14804d17f814b"
-
-func TestEvmTokenHolders_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", "token-holders", tokenHoldersAddress, "--chain-id", tokenHoldersChainID, "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "WALLET_ADDRESS")
- assert.Contains(t, out, "BALANCE")
- assert.Contains(t, out, "FIRST_ACQUIRED")
- assert.Contains(t, out, "HAS_TRANSFERRED")
-}
-
-func TestEvmTokenHolders_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", "token-holders", tokenHoldersAddress, "--chain-id", tokenHoldersChainID, "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "token_address")
- assert.Contains(t, resp, "chain_id")
- assert.Contains(t, resp, "holders")
-
- holders, ok := resp["holders"].([]interface{})
- require.True(t, ok)
- if len(holders) > 0 {
- h, ok := holders[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, h, "wallet_address")
- assert.Contains(t, h, "balance")
- }
-}
-
-func TestEvmTokenHolders_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-holders", tokenHoldersAddress, "--chain-id", tokenHoldersChainID, "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "holders")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-holders", tokenHoldersAddress, "--chain-id", tokenHoldersChainID, "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "holders")
- }
-}
-
-func TestEvmTokenHolders_InvalidChainID(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-holders", tokenHoldersAddress, "--chain-id", "notanumber"})
-
- err := root.Execute()
- require.Error(t, err)
- assert.Contains(t, err.Error(), "--chain-id must be a numeric value")
-}
-
-func TestEvmTokenHolders_RequiresChainID(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-holders", tokenHoldersAddress})
-
- err := root.Execute()
- require.Error(t, err)
- assert.Contains(t, err.Error(), "chain-id")
-}
diff --git a/cmd/sim/evm/token_info.go b/cmd/sim/evm/token_info.go
deleted file mode 100644
index 77669d9..0000000
--- a/cmd/sim/evm/token_info.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewTokenInfoCmd returns the `sim evm token-info` command.
-func NewTokenInfoCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "token-info ",
- Short: "Get token metadata, pricing, supply, and market cap for a token address",
- Long: "Return metadata and real-time pricing for a token contract address on a\n" +
- "specified EVM chain. Use the literal string 'native' as the address to query\n" +
- "the chain's native asset (e.g. ETH on chain 1, MATIC on chain 137).\n\n" +
- "The --chain-ids flag is required and should specify a single chain ID.\n\n" +
- "Returns the token identity, current USD price (from on-chain DEX pools),\n" +
- "total supply, estimated market cap, and logo. Use --historical-prices to\n" +
- "include past USD prices at specified hour offsets.\n\n" +
- "This command is useful for looking up token details before querying\n" +
- "balances or activity. For wallet-scoped token data, use\n" +
- "'dune sim evm balances' or 'dune sim evm balance'.\n\n" +
- "Examples:\n" +
- " dune sim evm token-info native --chain-ids 1\n" +
- " dune sim evm token-info 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 --chain-ids 8453\n" +
- " dune sim evm token-info native --chain-ids 1 --historical-prices 720,168,24 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runTokenInfo,
- }
-
- cmd.Flags().String("chain-ids", "", "Numeric EVM chain ID to query (required, single value, e.g. '1' for Ethereum, '8453' for Base)")
- cmd.Flags().String("historical-prices", "", "Include historical USD prices at the specified hour offsets from now (comma-separated, e.g. '720,168,24' for 30d, 7d, 1d ago)")
- cmd.Flags().Int("limit", 0, "Maximum number of token entries to return (default: server-determined)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- _ = cmd.MarkFlagRequired("chain-ids")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type tokensResponse struct {
- ContractAddress string `json:"contract_address"`
- Tokens []tokenInfo `json:"tokens"`
- Warnings []warningEntry `json:"warnings,omitempty"`
- NextOffset string `json:"next_offset,omitempty"`
-}
-
-type tokenInfo struct {
- Chain string `json:"chain"`
- ChainID int64 `json:"chain_id"`
- Symbol string `json:"symbol,omitempty"`
- Name string `json:"name,omitempty"`
- Decimals int `json:"decimals,omitempty"`
- PriceUSD float64 `json:"price_usd"`
- HistoricalPrices []historicalPrice `json:"historical_prices,omitempty"`
- TotalSupply string `json:"total_supply,omitempty"`
- MarketCap float64 `json:"market_cap,omitempty"`
- Logo string `json:"logo,omitempty"`
-}
-
-func runTokenInfo(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
-
- address := args[0]
- params := url.Values{}
-
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
- 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/token-info/"+address, 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 tokensResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- // Print warnings to stderr.
- printWarnings(cmd, resp.Warnings)
-
- if len(resp.Tokens) == 0 {
- fmt.Fprintln(w, "No token info found.")
- return nil
- }
-
- // Key-value display for each token entry.
- for i, t := range resp.Tokens {
- if i > 0 {
- fmt.Fprintln(w)
- }
- fmt.Fprintf(w, "Chain: %s (ID: %d)\n", t.Chain, t.ChainID)
- if t.Symbol != "" {
- fmt.Fprintf(w, "Symbol: %s\n", t.Symbol)
- }
- if t.Name != "" {
- fmt.Fprintf(w, "Name: %s\n", t.Name)
- }
- fmt.Fprintf(w, "Decimals: %d\n", t.Decimals)
- fmt.Fprintf(w, "Price USD: %s\n", output.FormatUSD(t.PriceUSD))
- if t.TotalSupply != "" {
- fmt.Fprintf(w, "Total Supply: %s\n", t.TotalSupply)
- }
- if t.MarketCap > 0 {
- fmt.Fprintf(w, "Market Cap: %s\n", output.FormatUSD(t.MarketCap))
- }
- if t.Logo != "" {
- fmt.Fprintf(w, "Logo: %s\n", t.Logo)
- }
- for _, hp := range t.HistoricalPrices {
- fmt.Fprintf(w, "Price %dh ago: %s\n", hp.OffsetHours, output.FormatUSD(hp.PriceUSD))
- }
- }
-
- if resp.NextOffset != "" {
- fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
- }
- return nil
- }
-}
diff --git a/cmd/sim/evm/token_info_test.go b/cmd/sim/evm/token_info_test.go
deleted file mode 100644
index 8537381..0000000
--- a/cmd/sim/evm/token_info_test.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmTokenInfo_Native_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", "token-info", "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, "Price USD:")
-}
-
-func TestEvmTokenInfo_Native_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", "token-info", "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, "contract_address")
- assert.Contains(t, resp, "tokens")
-
- tokens, ok := resp["tokens"].([]interface{})
- require.True(t, ok)
- require.NotEmpty(t, tokens)
-
- token, ok := tokens[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, token, "chain")
- assert.Contains(t, token, "symbol")
- assert.Contains(t, token, "price_usd")
-}
-
-func TestEvmTokenInfo_ERC20(t *testing.T) {
- key := simAPIKey(t)
-
- // USDC on Base
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-info", "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "--chain-ids", "8453", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "tokens")
-
- tokens, ok := resp["tokens"].([]interface{})
- require.True(t, ok)
- if len(tokens) > 0 {
- token, ok := tokens[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, token, "symbol")
- assert.Contains(t, token, "decimals")
- }
-}
-
-func TestEvmTokenInfo_HistoricalPrices(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-info", "native", "--chain-ids", "1", "--historical-prices", "168,24", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- tokens, ok := resp["tokens"].([]interface{})
- require.True(t, ok)
- require.NotEmpty(t, tokens)
-
- token, ok := tokens[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, token, "historical_prices", "historical_prices should be present when --historical-prices is set")
-}
-
-func TestEvmTokenInfo_HistoricalPrices_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", "token-info", "native", "--chain-ids", "1", "--historical-prices", "168"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "Price 168h ago:")
-}
-
-func TestEvmTokenInfo_RequiresChainIds(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "token-info", "native"})
-
- err := root.Execute()
- require.Error(t, err)
- assert.Contains(t, err.Error(), "chain-ids")
-}
diff --git a/cmd/sim/evm/transactions.go b/cmd/sim/evm/transactions.go
deleted file mode 100644
index 1e5e6da..0000000
--- a/cmd/sim/evm/transactions.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package evm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewTransactionsCmd returns the `sim evm transactions` command.
-func NewTransactionsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "transactions ",
- Short: "Get raw EVM transaction history for a wallet address across chains",
- Long: "Return raw transaction history for the given wallet address across supported\n" +
- "EVM chains. Transactions are returned in reverse-chronological order. Each\n" +
- "transaction includes the hash, sender/recipient, value, block context, gas\n" +
- "parameters, and raw calldata.\n\n" +
- "Use --decode with -o json to include ABI-decoded function calls and event\n" +
- "logs. Decoded data adds 'decoded' fields with function name and typed inputs,\n" +
- "plus 'logs' with decoded event emissions. Note: --decode data is only visible\n" +
- "in JSON output mode; the text table cannot display nested decoded structures.\n\n" +
- "For human-readable, classified activity (sends, swaps, approvals, etc.),\n" +
- "use 'dune sim evm activity' instead.\n\n" +
- "By default, queries all chains tagged 'default'. Run 'dune sim evm\n" +
- "supported-chains' to see which chains support transactions.\n\n" +
- "Examples:\n" +
- " dune sim evm transactions 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
- " dune sim evm transactions 0xd8da... --chain-ids 1 --decode -o json\n" +
- " dune sim evm transactions 0xd8da... --limit 50 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runTransactions,
- }
-
- cmd.Flags().String("chain-ids", "", "Restrict to specific chains by numeric ID or tag name (comma-separated, e.g. '1,8453' or 'default'); defaults to all chains tagged 'default'")
- cmd.Flags().Bool("decode", false, "Include ABI-decoded function calls and event logs in the response; only visible in JSON output (-o json), ignored in text table mode")
- cmd.Flags().Int("limit", 0, "Maximum number of transactions to return per page (1-100, default: server-determined)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-type transactionsResponse struct {
- WalletAddress string `json:"wallet_address"`
- Transactions []transactionTx `json:"transactions"`
- Errors *transactionErrors `json:"errors,omitempty"`
- NextOffset string `json:"next_offset,omitempty"`
- Warnings []warningEntry `json:"warnings,omitempty"`
- RequestTime string `json:"request_time,omitempty"`
- ResponseTime string `json:"response_time,omitempty"`
-}
-
-type transactionErrors struct {
- ErrorMessage string `json:"error_message,omitempty"`
- TransactionErrors []apiChainError `json:"transaction_errors,omitempty"`
-}
-
-type transactionTx struct {
- Address string `json:"address"`
- BlockHash string `json:"block_hash"`
- BlockNumber json.Number `json:"block_number"`
- BlockTime string `json:"block_time"`
- BlockVersion int `json:"block_version,omitempty"`
- Chain string `json:"chain"`
- From string `json:"from"`
- To string `json:"to"`
- Data string `json:"data,omitempty"`
- GasPrice string `json:"gas_price,omitempty"`
- Hash string `json:"hash"`
- Index json.Number `json:"index,omitempty"`
- MaxFeePerGas string `json:"max_fee_per_gas,omitempty"`
- MaxPriorityFeePerGas string `json:"max_priority_fee_per_gas,omitempty"`
- Nonce string `json:"nonce,omitempty"`
- TransactionType string `json:"transaction_type,omitempty"`
- Value string `json:"value"`
- Decoded *decodedCall `json:"decoded,omitempty"`
- Logs []transactionLog `json:"logs,omitempty"`
-}
-
-type decodedCall struct {
- Name string `json:"name,omitempty"`
- Inputs []decodedInput `json:"inputs,omitempty"`
-}
-
-type decodedInput struct {
- Name string `json:"name,omitempty"`
- Type string `json:"type,omitempty"`
- Value json.RawMessage `json:"value,omitempty"`
-}
-
-type transactionLog struct {
- Address string `json:"address,omitempty"`
- Data string `json:"data,omitempty"`
- Topics []string `json:"topics,omitempty"`
- Decoded *decodedCall `json:"decoded,omitempty"`
-}
-
-func runTransactions(cmd *cobra.Command, args []string) error {
- client := SimClientFromCmd(cmd)
- if client == nil {
- return fmt.Errorf("sim client not initialized")
- }
-
- address := args[0]
- params := url.Values{}
-
- if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
- params.Set("chain_ids", v)
- }
- if v, _ := cmd.Flags().GetBool("decode"); v {
- params.Set("decode", "true")
- }
- 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/transactions/"+address, 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 transactionsResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- // Warn if --decode is used in text mode since the table can't show decoded data.
- if decode, _ := cmd.Flags().GetBool("decode"); decode {
- fmt.Fprintln(cmd.ErrOrStderr(), "Note: --decode data is only visible in JSON output. Use -o json to see decoded fields.")
- }
-
- // Print errors to stderr.
- printTransactionErrors(cmd, resp.Errors)
-
- // Print warnings to stderr.
- printWarnings(cmd, resp.Warnings)
-
- columns := []string{"CHAIN", "HASH", "FROM", "TO", "VALUE", "BLOCK_TIME"}
- rows := make([][]string, len(resp.Transactions))
- for i, tx := range resp.Transactions {
- rows[i] = []string{
- tx.Chain,
- truncateHash(tx.Hash),
- truncateHash(tx.From),
- truncateHash(tx.To),
- tx.Value,
- tx.BlockTime,
- }
- }
- output.PrintTable(w, columns, rows)
-
- if resp.NextOffset != "" {
- fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
- }
- return nil
- }
-}
-
-// printTransactionErrors writes transaction-level errors to stderr.
-func printTransactionErrors(cmd *cobra.Command, errs *transactionErrors) {
- if errs == nil {
- return
- }
- printAPIChainErrors(cmd, errs.ErrorMessage, errs.TransactionErrors)
-}
diff --git a/cmd/sim/evm/transactions_test.go b/cmd/sim/evm/transactions_test.go
deleted file mode 100644
index b2c205d..0000000
--- a/cmd/sim/evm/transactions_test.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package evm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestEvmTransactions_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", "transactions", evmTestAddress, "--chain-ids", "1", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "HASH")
- assert.Contains(t, out, "FROM")
- assert.Contains(t, out, "TO")
- assert.Contains(t, out, "VALUE")
- assert.Contains(t, out, "BLOCK_TIME")
-}
-
-func TestEvmTransactions_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", "transactions", evmTestAddress, "--chain-ids", "1", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "transactions")
-}
-
-func TestEvmTransactions_DecodeJSON(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "transactions", evmTestAddress, "--chain-ids", "1", "--decode", "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "transactions")
-
- // When decode is enabled, transactions may contain decoded and logs fields.
- // We just verify the response is valid JSON with transactions.
- txs, ok := resp["transactions"].([]interface{})
- require.True(t, ok)
- if len(txs) > 0 {
- tx, ok := txs[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, tx, "hash")
- assert.Contains(t, tx, "chain")
- }
-}
-
-// TestEvmTransactions_DecodeText exercises --decode in text mode (the default).
-// This is the code path that unmarshals decoded inputs into Go structs. If
-// Value were typed as string rather than json.RawMessage, non-string ABI
-// arguments (numbers, booleans, arrays) would cause an unmarshal error here.
-func TestEvmTransactions_DecodeText(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- var errBuf bytes.Buffer
- root.SetOut(&buf)
- root.SetErr(&errBuf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "transactions", evmTestAddress, "--chain-ids", "1", "--decode", "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "HASH")
-
- // Text mode should print the stderr hint about --decode being JSON-only.
- assert.Contains(t, errBuf.String(), "--decode data is only visible in JSON output")
-}
-
-func TestEvmTransactions_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "transactions", evmTestAddress, "--chain-ids", "1", "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "transactions")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "transactions", evmTestAddress, "--chain-ids", "1", "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "transactions")
- }
-}
diff --git a/cmd/sim/sim.go b/cmd/sim/sim.go
deleted file mode 100644
index f7d252d..0000000
--- a/cmd/sim/sim.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package sim
-
-import (
- "fmt"
- "os"
- "strings"
- "time"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/authconfig"
- "github.com/duneanalytics/cli/cmd/sim/evm"
- "github.com/duneanalytics/cli/cmd/sim/svm"
- "github.com/duneanalytics/cli/cmdutil"
-)
-
-var simAPIKeyFlag string
-
-// NewSimCmd returns the `sim` parent command.
-func NewSimCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "sim",
- Short: "Query real-time blockchain data via the Dune Sim API",
- Long: "Access real-time, indexed blockchain data through the Dune Sim API. Unlike\n" +
- "'dune query run' which executes SQL against historical data warehouses, Sim API\n" +
- "endpoints return pre-indexed, low-latency responses for common wallet and token\n" +
- "lookups.\n\n" +
- "Available subcommands:\n" +
- " evm - Query EVM chains: balances, activity, transactions, collectibles,\n" +
- " token-info, token-holders, defi-positions, supported-chains,\n" +
- " supported-protocols\n" +
- " svm - Query SVM chains (Solana, Eclipse): balances, transactions\n" +
- " auth - Save your Sim API key to the local config file\n\n" +
- "Authentication:\n" +
- " Most commands require a Sim API key. The key is resolved in priority order:\n" +
- " 1. --sim-api-key flag\n" +
- " 2. DUNE_SIM_API_KEY environment variable\n" +
- " 3. Saved key in ~/.config/dune/config.yaml (set via 'dune sim auth')\n\n" +
- " Exception: 'dune sim evm supported-chains' is a public endpoint and does\n" +
- " not require authentication.\n\n" +
- "Each API call consumes compute units based on the number of chains queried\n" +
- "and the complexity of the request. Use -o json on any command to get the\n" +
- "full structured response for programmatic consumption.",
- Annotations: map[string]string{"skipAuth": "true"},
- PersistentPreRunE: simPreRun,
- }
-
- cmd.PersistentFlags().StringVar(
- &simAPIKeyFlag, "sim-api-key", "",
- "Sim API key for authentication (overrides DUNE_SIM_API_KEY env var and saved config); keys are prefixed 'sim_'",
- )
-
- cmd.AddCommand(NewAuthCmd())
- cmd.AddCommand(evm.NewEvmCmd())
- cmd.AddCommand(svm.NewSvmCmd())
-
- return cmd
-}
-
-// simPreRun resolves the Sim API key and stores a SimClient in the command context.
-// Commands annotated with "skipSimAuth": "true" bypass this step.
-func simPreRun(cmd *cobra.Command, _ []string) error {
- // The sim command's PersistentPreRunE overrides the root command's hook
- // (cobra does not chain PersistentPreRunE without EnableTraverseRunHooks).
- // Record the start time here so the root's PersistentPostRunE computes a
- // correct duration for telemetry.
- cmdutil.SetStartTime(cmd, time.Now())
-
- // Commands like `sim evm supported-chains` that hit public endpoints
- // don't require an API key. Provide a bare (unauthenticated) client so
- // they can still use the shared HTTP infrastructure and error handling.
- if cmd.Annotations["skipSimAuth"] == "true" {
- cmdutil.SetSimClient(cmd, NewBareSimClient())
- return nil
- }
-
- apiKey := resolveSimAPIKey()
- if apiKey == "" {
- return fmt.Errorf(
- "missing Sim API key: set DUNE_SIM_API_KEY, pass --sim-api-key, or run `dune sim auth`",
- )
- }
-
- client := NewSimClient(apiKey)
- cmdutil.SetSimClient(cmd, client)
-
- return nil
-}
-
-// resolveSimAPIKey resolves the Sim API key from (in priority order):
-// 1. --sim-api-key flag
-// 2. DUNE_SIM_API_KEY environment variable
-// 3. sim_api_key from ~/.config/dune/config.yaml
-func resolveSimAPIKey() string {
- // 1. Flag
- if simAPIKeyFlag != "" {
- return strings.TrimSpace(simAPIKeyFlag)
- }
-
- // 2. Environment variable
- if key := os.Getenv("DUNE_SIM_API_KEY"); key != "" {
- return strings.TrimSpace(key)
- }
-
- // 3. Config file
- cfg, err := authconfig.Load()
- if err != nil || cfg == nil {
- return ""
- }
- return strings.TrimSpace(cfg.SimAPIKey)
-}
-
-// SimClientFromCmd is a convenience helper that extracts and type-asserts the
-// SimClient from the command context.
-func SimClientFromCmd(cmd *cobra.Command) *SimClient {
- v := cmdutil.SimClientFromCmd(cmd)
- if v == nil {
- return nil
- }
- return v.(*SimClient)
-}
diff --git a/cmd/sim/svm/balances.go b/cmd/sim/svm/balances.go
deleted file mode 100644
index eb813a4..0000000
--- a/cmd/sim/svm/balances.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package svm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewBalancesCmd returns the `sim svm balances` command.
-func NewBalancesCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "balances ",
- Short: "Get SPL token balances for an SVM wallet address with USD valuations",
- Long: "Return SPL token balances for the given SVM (Solana Virtual Machine) wallet\n" +
- "address. Each balance entry includes the token identity, balance (both raw\n" +
- "and human-readable), current USD price, total USD value, and liquidity data.\n" +
- "Data comes from Dune's real-time index.\n\n" +
- "Supported chains: Solana, Eclipse (default: Solana only).\n\n" +
- "Note: This endpoint is in beta (served under /beta/svm/balances/*).\n\n" +
- "Results are paginated; use --offset with the next_offset value from a\n" +
- "previous response to retrieve additional pages.\n\n" +
- "Examples:\n" +
- " dune sim svm balances 86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\n" +
- " dune sim svm balances 86xCnPeV... --chains solana,eclipse\n" +
- " dune sim svm balances 86xCnPeV... --limit 50 -o json",
- Args: cobra.ExactArgs(1),
- RunE: runBalances,
- }
-
- cmd.Flags().String("chains", "", "Restrict to specific SVM chains (comma-separated): 'solana', 'eclipse' (default: solana only)")
- cmd.Flags().Int("limit", 0, "Maximum number of balance entries to return per page (1-1000, default: 1000)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-// --- Response types ---
-
-type svmBalancesResponse struct {
- ProcessingTimeMs float64 `json:"processing_time_ms,omitempty"`
- WalletAddress string `json:"wallet_address"`
- NextOffset string `json:"next_offset,omitempty"`
- BalancesCount float64 `json:"balances_count,omitempty"`
- Balances []svmBalanceEntry `json:"balances"`
-}
-
-type svmBalanceEntry struct {
- Chain string `json:"chain"`
- Address string `json:"address"`
- Amount string `json:"amount"`
- Balance string `json:"balance,omitempty"`
- RawBalance string `json:"raw_balance,omitempty"`
- ValueUSD float64 `json:"value_usd,omitempty"`
- ProgramID *string `json:"program_id,omitempty"`
- Decimals float64 `json:"decimals,omitempty"`
- TotalSupply string `json:"total_supply,omitempty"`
- Name string `json:"name,omitempty"`
- Symbol string `json:"symbol,omitempty"`
- URI *string `json:"uri,omitempty"`
- PriceUSD float64 `json:"price_usd,omitempty"`
- LiquidityUSD float64 `json:"liquidity_usd,omitempty"`
- PoolType *string `json:"pool_type,omitempty"`
- PoolAddress *string `json:"pool_address,omitempty"`
- MintAuthority *string `json:"mint_authority,omitempty"`
-}
-
-func runBalances(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("chains"); v != "" {
- params.Set("chains", 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(), "/beta/svm/balances/"+address, 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 svmBalancesResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- if len(resp.Balances) == 0 {
- fmt.Fprintln(w, "No balances found.")
- return nil
- }
-
- columns := []string{"CHAIN", "SYMBOL", "BALANCE", "PRICE_USD", "VALUE_USD"}
- rows := make([][]string, len(resp.Balances))
- for i, b := range resp.Balances {
- bal := b.Balance
- if bal == "" {
- bal = b.Amount
- }
- rows[i] = []string{
- b.Chain,
- b.Symbol,
- bal,
- output.FormatUSD(b.PriceUSD),
- output.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/svm/balances_test.go b/cmd/sim/svm/balances_test.go
deleted file mode 100644
index d9cfbbc..0000000
--- a/cmd/sim/svm/balances_test.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package svm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestSvmBalances_Text(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "SYMBOL")
- assert.Contains(t, out, "BALANCE")
- assert.Contains(t, out, "PRICE_USD")
- assert.Contains(t, out, "VALUE_USD")
-}
-
-func TestSvmBalances_JSON(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress, "-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)
- if len(balances) > 0 {
- b, ok := balances[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, b, "chain")
- assert.Contains(t, b, "address")
- assert.Contains(t, b, "amount")
- }
-}
-
-func TestSvmBalances_WithChains(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress, "--chains", "solana", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "balances")
-
- // All balances should be on solana chain.
- balances, ok := resp["balances"].([]interface{})
- require.True(t, ok)
- for _, bal := range balances {
- b, ok := bal.(map[string]interface{})
- require.True(t, ok)
- assert.Equal(t, "solana", b["chain"])
- }
-}
-
-func TestSvmBalances_Limit(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress, "--limit", "3", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- balances, ok := resp["balances"].([]interface{})
- require.True(t, ok)
- assert.LessOrEqual(t, len(balances), 3)
-}
-
-func TestSvmBalances_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress, "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "balances")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "balances", svmTestAddress, "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "balances")
- }
-}
diff --git a/cmd/sim/svm/helpers_test.go b/cmd/sim/svm/helpers_test.go
deleted file mode 100644
index 376abb7..0000000
--- a/cmd/sim/svm/helpers_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package svm_test
-
-import (
- "context"
- "os"
- "testing"
-
- "github.com/duneanalytics/cli/cmd/sim"
- "github.com/spf13/cobra"
-)
-
-// simAPIKey returns the DUNE_SIM_API_KEY env var or skips the test.
-func simAPIKey(t *testing.T) string {
- t.Helper()
- key := os.Getenv("DUNE_SIM_API_KEY")
- if key == "" {
- t.Skip("DUNE_SIM_API_KEY not set, skipping e2e test")
- }
- return key
-}
-
-const svmTestAddress = "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY"
-
-// newSimTestRoot builds the full command tree: dune -> sim -> svm -> .
-// Used for authenticated E2E tests. Pass the API key via --sim-api-key in SetArgs.
-func newSimTestRoot() *cobra.Command {
- root := &cobra.Command{Use: "dune"}
- root.SetContext(context.Background())
- root.AddCommand(sim.NewSimCmd())
- return root
-}
diff --git a/cmd/sim/svm/svm.go b/cmd/sim/svm/svm.go
deleted file mode 100644
index d81e0da..0000000
--- a/cmd/sim/svm/svm.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package svm
-
-import (
- "context"
- "fmt"
- "net/url"
-
- "github.com/duneanalytics/cli/cmdutil"
- "github.com/spf13/cobra"
-)
-
-// SimClient is the interface that svm commands use to talk to the Sim API.
-// It is satisfied by *sim.SimClient (stored in the command context by
-// the sim parent command's PersistentPreRunE).
-type SimClient interface {
- Get(ctx context.Context, path string, params url.Values) ([]byte, error)
-}
-
-// SimClientFromCmd extracts the SimClient from the command context.
-func SimClientFromCmd(cmd *cobra.Command) SimClient {
- v := cmdutil.SimClientFromCmd(cmd)
- if v == nil {
- return nil
- }
- c, ok := v.(SimClient)
- if !ok {
- return nil
- }
- return c
-}
-
-// requireSimClient extracts the SimClient or returns an error.
-func requireSimClient(cmd *cobra.Command) (SimClient, error) {
- c := SimClientFromCmd(cmd)
- if c == nil {
- return nil, fmt.Errorf("sim client not initialized")
- }
- return c, nil
-}
-
-// NewSvmCmd returns the `sim svm` parent command.
-func NewSvmCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "svm",
- Short: "Query SVM chain data (Solana, Eclipse) for balances and transactions",
- Long: "Access real-time, indexed SVM (Solana Virtual Machine) blockchain data.\n" +
- "Commands accept a Solana-style base58 wallet address as the primary argument.\n\n" +
- "Supported chains: Solana, Eclipse.\n\n" +
- "Available subcommands:\n" +
- " balances - SPL token balances with USD valuations and liquidity data\n" +
- " transactions - Raw transaction history with block slot and signature\n\n" +
- "Note: SVM endpoints are currently in beta (served under /beta/svm/*).\n" +
- "All commands require a Sim API key.",
- }
-
- cmd.AddCommand(NewBalancesCmd())
- cmd.AddCommand(NewTransactionsCmd())
-
- return cmd
-}
diff --git a/cmd/sim/svm/transactions.go b/cmd/sim/svm/transactions.go
deleted file mode 100644
index 45106d8..0000000
--- a/cmd/sim/svm/transactions.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package svm
-
-import (
- "encoding/json"
- "fmt"
- "net/url"
- "time"
-
- "github.com/spf13/cobra"
-
- "github.com/duneanalytics/cli/output"
-)
-
-// NewTransactionsCmd returns the `sim svm transactions` command.
-func NewTransactionsCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "transactions ",
- Short: "Get Solana transaction history for an SVM wallet address",
- Long: "Return transaction history for the given SVM (Solana Virtual Machine) wallet\n" +
- "address. Transactions are returned in reverse-chronological order by block slot.\n" +
- "Each entry includes the chain, block slot, block time, and in JSON output the\n" +
- "full raw Solana transaction with signatures, instructions, and account keys.\n\n" +
- "Note: This endpoint is in beta (served under /beta/svm/transactions/*).\n\n" +
- "Results are paginated; use --offset with the next_offset value from a\n" +
- "previous response to retrieve additional pages.\n\n" +
- "Examples:\n" +
- " dune sim svm transactions 86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\n" +
- " dune sim svm transactions 86xCnPeV... --limit 20\n" +
- " dune sim svm transactions 86xCnPeV... -o json",
- Args: cobra.ExactArgs(1),
- RunE: runTransactions,
- }
-
- cmd.Flags().Int("limit", 0, "Maximum number of transactions to return per page (1-1000, default: 100)")
- cmd.Flags().String("offset", "", "Pagination cursor returned as next_offset in a previous response; use to fetch the next page of results")
- output.AddFormatFlag(cmd, "text")
-
- return cmd
-}
-
-// --- Response types ---
-
-type svmTransactionsResponse struct {
- NextOffset string `json:"next_offset,omitempty"`
- Transactions []svmTransaction `json:"transactions"`
-}
-
-type svmTransaction struct {
- Address string `json:"address"`
- BlockSlot json.Number `json:"block_slot"`
- BlockTime json.Number `json:"block_time"`
- Chain string `json:"chain"`
- RawTransaction json.RawMessage `json:"raw_transaction,omitempty"`
-}
-
-func runTransactions(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().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(), "/beta/svm/transactions/"+address, 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 svmTransactionsResponse
- if err := json.Unmarshal(data, &resp); err != nil {
- return fmt.Errorf("parsing response: %w", err)
- }
-
- if len(resp.Transactions) == 0 {
- fmt.Fprintln(w, "No transactions found.")
- return nil
- }
-
- columns := []string{"CHAIN", "BLOCK_SLOT", "BLOCK_TIME", "TX_SIGNATURE"}
- rows := make([][]string, len(resp.Transactions))
- for i, tx := range resp.Transactions {
- rows[i] = []string{
- tx.Chain,
- tx.BlockSlot.String(),
- formatBlockTime(tx.BlockTime),
- extractSignature(tx.RawTransaction),
- }
- }
- output.PrintTable(w, columns, rows)
-
- if resp.NextOffset != "" {
- fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
- }
- return nil
- }
-}
-
-// formatBlockTime converts a block_time (microseconds since epoch) to a
-// human-readable UTC timestamp.
-func formatBlockTime(bt json.Number) string {
- us, err := bt.Int64()
- if err != nil {
- return bt.String()
- }
- // block_time is in microseconds.
- t := time.Unix(0, us*int64(time.Microsecond))
- return t.UTC().Format("2006-01-02 15:04:05")
-}
-
-// extractSignature pulls the first transaction signature from raw_transaction.
-// Returns the signature string or an empty string if unavailable.
-func extractSignature(raw json.RawMessage) string {
- if len(raw) == 0 {
- return ""
- }
-
- var rt struct {
- Transaction struct {
- Signatures []string `json:"signatures"`
- } `json:"transaction"`
- }
- if err := json.Unmarshal(raw, &rt); err != nil {
- return ""
- }
- if len(rt.Transaction.Signatures) > 0 {
- return rt.Transaction.Signatures[0]
- }
- return ""
-}
diff --git a/cmd/sim/svm/transactions_test.go b/cmd/sim/svm/transactions_test.go
deleted file mode 100644
index 88945da..0000000
--- a/cmd/sim/svm/transactions_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package svm_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestSvmTransactions_Text(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5"})
-
- require.NoError(t, root.Execute())
-
- out := buf.String()
- assert.Contains(t, out, "CHAIN")
- assert.Contains(t, out, "BLOCK_SLOT")
- assert.Contains(t, out, "BLOCK_TIME")
- assert.Contains(t, out, "TX_SIGNATURE")
-}
-
-func TestSvmTransactions_JSON(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "transactions")
-
- txns, ok := resp["transactions"].([]interface{})
- require.True(t, ok)
- if len(txns) > 0 {
- tx, ok := txns[0].(map[string]interface{})
- require.True(t, ok)
- assert.Contains(t, tx, "address")
- assert.Contains(t, tx, "block_slot")
- assert.Contains(t, tx, "block_time")
- assert.Contains(t, tx, "chain")
- assert.Contains(t, tx, "raw_transaction")
- }
-}
-
-func TestSvmTransactions_Limit(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "3", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- txns, ok := resp["transactions"].([]interface{})
- require.True(t, ok)
- assert.LessOrEqual(t, len(txns), 3)
-}
-
-func TestSvmTransactions_Pagination(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
- assert.Contains(t, resp, "transactions")
-
- // If next_offset is present, fetch page 2.
- if offset, ok := resp["next_offset"].(string); ok && offset != "" {
- root2 := newSimTestRoot()
- var buf2 bytes.Buffer
- root2.SetOut(&buf2)
- root2.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "--offset", offset, "-o", "json"})
-
- require.NoError(t, root2.Execute())
-
- var resp2 map[string]interface{}
- require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
- assert.Contains(t, resp2, "transactions")
- }
-}
-
-func TestSvmTransactions_RawTransactionInJSON(t *testing.T) {
- key := simAPIKey(t)
-
- root := newSimTestRoot()
- var buf bytes.Buffer
- root.SetOut(&buf)
- root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "1", "-o", "json"})
-
- require.NoError(t, root.Execute())
-
- var resp map[string]interface{}
- require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
-
- txns, ok := resp["transactions"].([]interface{})
- require.True(t, ok)
- if len(txns) > 0 {
- tx, ok := txns[0].(map[string]interface{})
- require.True(t, ok)
-
- // raw_transaction should be a nested object with transaction data.
- rawTx, ok := tx["raw_transaction"].(map[string]interface{})
- if ok {
- // Should contain transaction with signatures.
- txData, ok := rawTx["transaction"].(map[string]interface{})
- if ok {
- assert.Contains(t, txData, "signatures")
- }
- }
- }
-}
diff --git a/cmdutil/client.go b/cmdutil/client.go
index b611856..ad2adb8 100644
--- a/cmdutil/client.go
+++ b/cmdutil/client.go
@@ -10,7 +10,6 @@ import (
)
type clientKey struct{}
-type simClientKey struct{}
type trackerKey struct{}
type startTimeKey struct{}
@@ -28,22 +27,6 @@ func ClientFromCmd(cmd *cobra.Command) dune.DuneClient {
return cmd.Context().Value(clientKey{}).(dune.DuneClient)
}
-// SetSimClient stores a Sim API client in the command's context.
-// The value is stored as any to avoid a circular import with cmd/sim.
-func SetSimClient(cmd *cobra.Command, client any) {
- ctx := cmd.Context()
- if ctx == nil {
- ctx = context.Background()
- }
- cmd.SetContext(context.WithValue(ctx, simClientKey{}, client))
-}
-
-// SimClientFromCmd extracts the Sim API client stored in the command's context.
-// Callers should type-assert the result to *sim.SimClient.
-func SimClientFromCmd(cmd *cobra.Command) any {
- return cmd.Context().Value(simClientKey{})
-}
-
// SetTracker stores a Tracker in the command's context.
func SetTracker(cmd *cobra.Command, t *tracking.Tracker) {
ctx := cmd.Context()
diff --git a/install.sh b/install.sh
index 8a49dcc..c8bf0a7 100755
--- a/install.sh
+++ b/install.sh
@@ -91,7 +91,6 @@ main() {
echo "" >&2
log "Tip: Run 'npx skills add duneanalytics/skills' to install Dune AI skills."
log "Tip: Run 'dune auth' to authenticate with your Dune account."
- log "Tip: Run 'dune sim auth' to access real-time blockchain data (balances, tokens, NFTs)."
fi
}
@@ -103,7 +102,7 @@ post_install() {
# --- Skill install ---
if has npx; then
log "Dune skills let AI agents (Cursor, Claude Code, etc.) query"
- log "blockchain data and access real-time wallet/token info on your behalf."
+ log "blockchain data on your behalf."
printf " Install Dune skills for your AI coding agent? [Y/n] " >&2
read -r answer < /dev/tty || answer=""
case "$answer" in
@@ -145,32 +144,6 @@ post_install() {
esac
fi
- echo "" >&2
-
- # --- Sim API Authentication ---
- if "$dune_bin" sim evm token-info native --chain-ids 1 > /dev/null 2>&1; then
- log "Already authenticated with Sim API."
- else
- log "Authenticate with the Sim API to access real-time blockchain data"
- log "(wallet balances, token prices, NFTs, DeFi positions, etc.)."
- printf " Authenticate with Sim API now? [Y/n] " >&2
- read -r answer < /dev/tty || answer=""
- case "$answer" in
- [nN]*)
- log "Skipped. You can authenticate later with: dune sim auth"
- ;;
- *)
- echo "" >&2
- log "You'll need a Sim API key. To create one:"
- log " 1. Go to https://sim.dune.com/ and login or create an account"
- log " 2. Click 'Keys'"
- log " 3. Click 'New' to create a new API key"
- echo "" >&2
- "$dune_bin" sim auth < /dev/tty || log "Authentication failed. You can retry with: dune sim auth"
- ;;
- esac
- fi
-
echo "" >&2
log "Dune CLI ${dune_version} installed successfully!"
}
diff --git a/tracking/tracking.go b/tracking/tracking.go
index 1b4ef7d..662371f 100644
--- a/tracking/tracking.go
+++ b/tracking/tracking.go
@@ -76,8 +76,7 @@ func toAmplitudeUserID(customerID string) string {
}
// Track sends a "CLI Command Executed" event to Amplitude.
-// Set isSim to true for commands under `dune sim`.
-func (t *Tracker) Track(commandPath, status, errMsg string, durationMs int64, isSim bool) {
+func (t *Tracker) Track(commandPath, status, errMsg string, durationMs int64) {
if !t.enabled || t.client == nil {
return
}
@@ -95,7 +94,6 @@ func (t *Tracker) Track(commandPath, status, errMsg string, durationMs int64, is
"cli_version": t.version,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
- "is_sim": isSim,
},
})
}
diff --git a/tracking/tracking_test.go b/tracking/tracking_test.go
index 9df02b4..04f276d 100644
--- a/tracking/tracking_test.go
+++ b/tracking/tracking_test.go
@@ -32,8 +32,7 @@ func TestTracker_DisabledNoOp(t *testing.T) {
tr := New(Config{Enabled: false})
assert.False(t, tr.enabled)
// Should not panic.
- tr.Track("test cmd", StatusSuccess, "", 100, false)
- tr.Track("sim evm balances", StatusSuccess, "", 100, true)
+ tr.Track("test cmd", StatusSuccess, "", 100)
tr.Shutdown()
}
@@ -80,31 +79,28 @@ func TestToAmplitudeUserID(t *testing.T) {
func TestTracker_TrackWithoutSetUserID(t *testing.T) {
tr := New(Config{Enabled: true, AmplitudeKey: "test-key"})
// Should not panic — events are sent with "cli" UserID.
- tr.Track("test cmd", StatusSuccess, "", 100, false)
+ tr.Track("test cmd", StatusSuccess, "", 100)
tr.Shutdown()
}
-func TestTrack_IsSim(t *testing.T) {
+func TestTrack_EventProperties(t *testing.T) {
spy := &spyClient{}
tr := newTestTracker(spy)
- tr.Track("query list", StatusSuccess, "", 42, false)
- tr.Track("sim evm balances", StatusSuccess, "", 99, true)
+ tr.Track("query list", StatusSuccess, "boom", 42)
- require.Len(t, spy.events, 2)
+ require.Len(t, spy.events, 1)
- // Non-sim event
props0 := spy.events[0].EventProperties
assert.Equal(t, "CLI Command Executed", spy.events[0].EventType)
assert.Equal(t, "cli", spy.events[0].UserID)
assert.Equal(t, "query list", props0["command_path"])
- assert.Equal(t, false, props0["is_sim"])
-
- // Sim event
- props1 := spy.events[1].EventProperties
- assert.Equal(t, "CLI Command Executed", spy.events[1].EventType)
- assert.Equal(t, "sim evm balances", props1["command_path"])
- assert.Equal(t, true, props1["is_sim"])
+ assert.Equal(t, StatusSuccess, props0["status"])
+ assert.Equal(t, int64(42), props0["duration_ms"])
+ assert.Equal(t, "boom", props0["error_message"])
+ assert.Equal(t, "test", props0["cli_version"])
+ assert.NotEmpty(t, props0["os"])
+ assert.NotEmpty(t, props0["arch"])
}
func TestTrack_SetUserIDReflectedInEvents(t *testing.T) {
@@ -112,7 +108,7 @@ func TestTrack_SetUserIDReflectedInEvents(t *testing.T) {
tr := newTestTracker(spy)
tr.SetUserID("user_42")
- tr.Track("query run", StatusSuccess, "", 10, false)
+ tr.Track("query run", StatusSuccess, "", 10)
require.Len(t, spy.events, 1)
assert.Equal(t, "42", spy.events[0].UserID)