Skip to content

Commit e26d221

Browse files
committed
Supported chains
1 parent 184124b commit e26d221

3 files changed

Lines changed: 258 additions & 1 deletion

File tree

cmd/sim/evm/evm.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
11
package evm
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
8+
"github.com/duneanalytics/cli/cmdutil"
49
"github.com/spf13/cobra"
510
)
611

12+
// SimClient is the interface that evm commands use to talk to the Sim API.
13+
// It is satisfied by *sim.SimClient (stored in the command context by
14+
// the sim parent command's PersistentPreRunE).
15+
type SimClient interface {
16+
Get(ctx context.Context, path string, params url.Values) ([]byte, error)
17+
}
18+
19+
// SimClientFromCmd extracts the SimClient from the command context.
20+
func SimClientFromCmd(cmd *cobra.Command) SimClient {
21+
v := cmdutil.SimClientFromCmd(cmd)
22+
if v == nil {
23+
return nil
24+
}
25+
c, ok := v.(SimClient)
26+
if !ok {
27+
return nil
28+
}
29+
return c
30+
}
31+
32+
// requireSimClient extracts the SimClient or returns an error.
33+
func requireSimClient(cmd *cobra.Command) (SimClient, error) {
34+
c := SimClientFromCmd(cmd)
35+
if c == nil {
36+
return nil, fmt.Errorf("sim client not initialized")
37+
}
38+
return c, nil
39+
}
40+
741
// NewEvmCmd returns the `sim evm` parent command.
842
func NewEvmCmd() *cobra.Command {
943
cmd := &cobra.Command{
@@ -14,7 +48,7 @@ func NewEvmCmd() *cobra.Command {
1448
"and DeFi positions.",
1549
}
1650

17-
// Subcommands will be added here as they are implemented.
51+
cmd.AddCommand(NewSupportedChainsCmd())
1852

1953
return cmd
2054
}

cmd/sim/evm/supported_chains.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package evm
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"strings"
11+
"time"
12+
13+
"github.com/spf13/cobra"
14+
15+
"github.com/duneanalytics/cli/output"
16+
)
17+
18+
// NewSupportedChainsCmd returns the `sim evm supported-chains` command.
19+
func NewSupportedChainsCmd() *cobra.Command {
20+
cmd := &cobra.Command{
21+
Use: "supported-chains",
22+
Short: "List supported EVM chains and their endpoint availability",
23+
Long: "Display all EVM chains supported by the Sim API and which endpoints\n" +
24+
"(balances, activity, transactions, etc.) are available for each chain.\n\n" +
25+
"This endpoint is public and does not require a Sim API key.\n\n" +
26+
"Examples:\n" +
27+
" dune sim evm supported-chains\n" +
28+
" dune sim evm supported-chains -o json",
29+
Annotations: map[string]string{"skipSimAuth": "true"},
30+
RunE: runSupportedChains,
31+
}
32+
33+
output.AddFormatFlag(cmd, "text")
34+
35+
return cmd
36+
}
37+
38+
type supportedChainsResponse struct {
39+
Chains []chainEntry `json:"chains"`
40+
}
41+
42+
type chainEntry struct {
43+
Name string `json:"name"`
44+
ChainID json.Number `json:"chain_id"`
45+
Tags []string `json:"tags"`
46+
Balances endpointSupport `json:"balances"`
47+
Activity endpointSupport `json:"activity"`
48+
Transactions endpointSupport `json:"transactions"`
49+
TokenInfo endpointSupport `json:"token_info"`
50+
TokenHolders endpointSupport `json:"token_holders"`
51+
Collectibles endpointSupport `json:"collectibles"`
52+
DefiPositions endpointSupport `json:"defi_positions"`
53+
}
54+
55+
type endpointSupport struct {
56+
Supported bool `json:"supported"`
57+
}
58+
59+
func runSupportedChains(cmd *cobra.Command, _ []string) error {
60+
// This endpoint is public (no auth required). Use the context client
61+
// if available, otherwise create a bare HTTP client.
62+
client := SimClientFromCmd(cmd)
63+
if client == nil {
64+
client = newBareSimClient()
65+
}
66+
67+
data, err := client.Get(cmd.Context(), "/v1/evm/supported-chains", nil)
68+
if err != nil {
69+
return err
70+
}
71+
72+
w := cmd.OutOrStdout()
73+
switch output.FormatFromCmd(cmd) {
74+
case output.FormatJSON:
75+
var raw json.RawMessage = data
76+
return output.PrintJSON(w, raw)
77+
default:
78+
var resp supportedChainsResponse
79+
if err := json.Unmarshal(data, &resp); err != nil {
80+
return fmt.Errorf("parsing response: %w", err)
81+
}
82+
83+
columns := []string{
84+
"NAME", "CHAIN_ID", "TAGS",
85+
"BALANCES", "ACTIVITY", "TXS",
86+
"TOKEN_INFO", "HOLDERS", "COLLECTIBLES", "DEFI",
87+
}
88+
rows := make([][]string, len(resp.Chains))
89+
for i, c := range resp.Chains {
90+
rows[i] = []string{
91+
c.Name,
92+
c.ChainID.String(),
93+
strings.Join(c.Tags, ","),
94+
boolYN(c.Balances.Supported),
95+
boolYN(c.Activity.Supported),
96+
boolYN(c.Transactions.Supported),
97+
boolYN(c.TokenInfo.Supported),
98+
boolYN(c.TokenHolders.Supported),
99+
boolYN(c.Collectibles.Supported),
100+
boolYN(c.DefiPositions.Supported),
101+
}
102+
}
103+
output.PrintTable(w, columns, rows)
104+
return nil
105+
}
106+
}
107+
108+
func boolYN(b bool) string {
109+
if b {
110+
return "Y"
111+
}
112+
return "N"
113+
}
114+
115+
// bareSimClient is a minimal HTTP client for public Sim API endpoints
116+
// that don't require authentication.
117+
type bareSimClient struct {
118+
httpClient *http.Client
119+
}
120+
121+
func newBareSimClient() *bareSimClient {
122+
return &bareSimClient{
123+
httpClient: &http.Client{Timeout: 30 * time.Second},
124+
}
125+
}
126+
127+
func (c *bareSimClient) Get(ctx context.Context, path string, params url.Values) ([]byte, error) {
128+
u, err := url.Parse("https://api.sim.dune.com" + path)
129+
if err != nil {
130+
return nil, fmt.Errorf("invalid URL: %w", err)
131+
}
132+
if params != nil {
133+
u.RawQuery = params.Encode()
134+
}
135+
136+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
137+
if err != nil {
138+
return nil, fmt.Errorf("creating request: %w", err)
139+
}
140+
req.Header.Set("Accept", "application/json")
141+
142+
resp, err := c.httpClient.Do(req)
143+
if err != nil {
144+
return nil, fmt.Errorf("request failed: %w", err)
145+
}
146+
defer resp.Body.Close()
147+
148+
body, err := io.ReadAll(resp.Body)
149+
if err != nil {
150+
return nil, fmt.Errorf("reading response: %w", err)
151+
}
152+
153+
if resp.StatusCode >= 400 {
154+
return nil, fmt.Errorf("Sim API error (HTTP %d)", resp.StatusCode)
155+
}
156+
157+
return body, nil
158+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package evm_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"testing"
8+
9+
"github.com/duneanalytics/cli/cmd/sim/evm"
10+
"github.com/spf13/cobra"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// newEvmTestRoot builds a minimal command tree: dune -> evm -> <subcommands>.
16+
// No sim parent is needed because evm commands extract the client from context
17+
// directly; for public endpoints the command creates its own client.
18+
func newEvmTestRoot() *cobra.Command {
19+
root := &cobra.Command{Use: "dune"}
20+
root.SetContext(context.Background())
21+
22+
evmCmd := evm.NewEvmCmd()
23+
root.AddCommand(evmCmd)
24+
25+
return root
26+
}
27+
28+
// supported-chains is a public endpoint — no API key required.
29+
30+
func TestSupportedChains_Text(t *testing.T) {
31+
root := newEvmTestRoot()
32+
var buf bytes.Buffer
33+
root.SetOut(&buf)
34+
root.SetArgs([]string{"evm", "supported-chains"})
35+
36+
require.NoError(t, root.Execute())
37+
38+
out := buf.String()
39+
assert.Contains(t, out, "NAME")
40+
assert.Contains(t, out, "CHAIN_ID")
41+
assert.Contains(t, out, "BALANCES")
42+
assert.Contains(t, out, "ethereum")
43+
}
44+
45+
func TestSupportedChains_JSON(t *testing.T) {
46+
root := newEvmTestRoot()
47+
var buf bytes.Buffer
48+
root.SetOut(&buf)
49+
root.SetArgs([]string{"evm", "supported-chains", "-o", "json"})
50+
51+
require.NoError(t, root.Execute())
52+
53+
var resp map[string]interface{}
54+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
55+
assert.Contains(t, resp, "chains")
56+
57+
chains, ok := resp["chains"].([]interface{})
58+
require.True(t, ok, "chains should be an array")
59+
require.NotEmpty(t, chains, "should have at least one chain")
60+
61+
first, ok := chains[0].(map[string]interface{})
62+
require.True(t, ok)
63+
assert.Contains(t, first, "name")
64+
assert.Contains(t, first, "chain_id")
65+
}

0 commit comments

Comments
 (0)