Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cmd/sim/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
26 changes: 25 additions & 1 deletion cmd/sim/evm/evm.go
Original file line number Diff line number Diff line change
@@ -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{
Expand All @@ -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
}
106 changes: 106 additions & 0 deletions cmd/sim/evm/supported_chains.go
Original file line number Diff line number Diff line change
@@ -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"
}
70 changes: 70 additions & 0 deletions cmd/sim/evm/supported_chains_test.go
Original file line number Diff line number Diff line change
@@ -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 -> <subcommands>.
// 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")
}
5 changes: 4 additions & 1 deletion cmd/sim/sim.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading