From c65dd10fbf501947aa4be5f34d1866ab95238c4d Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:14:10 +0100 Subject: [PATCH] pr feedback --- cmd/sim/client.go | 15 +++- cmd/sim/evm/evm.go | 26 ++++++- cmd/sim/evm/supported_chains.go | 106 +++++++++++++++++++++++++++ cmd/sim/evm/supported_chains_test.go | 70 ++++++++++++++++++ cmd/sim/sim.go | 5 +- 5 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 cmd/sim/evm/supported_chains.go create mode 100644 cmd/sim/evm/supported_chains_test.go diff --git a/cmd/sim/client.go b/cmd/sim/client.go index 540da40..493e202 100644 --- a/cmd/sim/client.go +++ b/cmd/sim/client.go @@ -30,6 +30,17 @@ func NewSimClient(apiKey string) *SimClient { } } +// 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. @@ -46,7 +57,9 @@ func (c *SimClient) Get(ctx context.Context, path string, params url.Values) ([] if err != nil { return nil, fmt.Errorf("creating request: %w", err) } - req.Header.Set("X-Sim-Api-Key", c.apiKey) + if c.apiKey != "" { + req.Header.Set("X-Sim-Api-Key", c.apiKey) + } req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) diff --git a/cmd/sim/evm/evm.go b/cmd/sim/evm/evm.go index 5af6085..e86dbc1 100644 --- a/cmd/sim/evm/evm.go +++ b/cmd/sim/evm/evm.go @@ -1,9 +1,33 @@ 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{ @@ -14,7 +38,7 @@ func NewEvmCmd() *cobra.Command { "and DeFi positions.", } - // Subcommands will be added here as they are implemented. + cmd.AddCommand(NewSupportedChainsCmd()) return cmd } diff --git a/cmd/sim/evm/supported_chains.go b/cmd/sim/evm/supported_chains.go new file mode 100644 index 0000000..84a87f5 --- /dev/null +++ b/cmd/sim/evm/supported_chains.go @@ -0,0 +1,106 @@ +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 and which endpoints\n" + + "(balances, activity, transactions, etc.) are available for each chain.\n\n" + + "This endpoint is public and does not require a Sim API key.\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 new file mode 100644 index 0000000..f629838 --- /dev/null +++ b/cmd/sim/evm/supported_chains_test.go @@ -0,0 +1,70 @@ +package evm_test + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + "github.com/duneanalytics/cli/cmd/sim" + "github.com/duneanalytics/cli/cmd/sim/evm" + "github.com/duneanalytics/cli/cmdutil" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newEvmTestRoot builds a minimal command tree: dune -> evm -> . +// A bare (unauthenticated) SimClient is stored in context so public-endpoint +// commands can use the shared HTTP infrastructure. +func newEvmTestRoot() *cobra.Command { + root := &cobra.Command{Use: "dune"} + root.SetContext(context.Background()) + + evmCmd := evm.NewEvmCmd() + root.AddCommand(evmCmd) + + // Inject a bare client like simPreRun does for skipSimAuth commands. + cmdutil.SetSimClient(root, sim.NewBareSimClient()) + + return root +} + +// supported-chains is a public endpoint — no API key required. + +func TestSupportedChains_Text(t *testing.T) { + root := newEvmTestRoot() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetArgs([]string{"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 := newEvmTestRoot() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetArgs([]string{"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/sim.go b/cmd/sim/sim.go index 1da7cd8..2204730 100644 --- a/cmd/sim/sim.go +++ b/cmd/sim/sim.go @@ -50,8 +50,11 @@ func simPreRun(cmd *cobra.Command, _ []string) error { // correct duration for telemetry. cmdutil.SetStartTime(cmd, time.Now()) - // Allow commands like `sim auth` to skip sim client creation. + // 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 }