diff --git a/cmd/sim/evm/evm.go b/cmd/sim/evm/evm.go index f66cb26..d64cbda 100644 --- a/cmd/sim/evm/evm.go +++ b/cmd/sim/evm/evm.go @@ -46,7 +46,8 @@ func NewEvmCmd() *cobra.Command { " 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\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" + @@ -63,6 +64,7 @@ func NewEvmCmd() *cobra.Command { cmd.AddCommand(NewTokenInfoCmd()) cmd.AddCommand(NewTokenHoldersCmd()) cmd.AddCommand(NewDefiPositionsCmd()) + cmd.AddCommand(NewSupportedProtocolsCmd()) return cmd } diff --git a/cmd/sim/evm/supported_protocols.go b/cmd/sim/evm/supported_protocols.go new file mode 100644 index 0000000..d372bc5 --- /dev/null +++ b/cmd/sim/evm/supported_protocols.go @@ -0,0 +1,115 @@ +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 new file mode 100644 index 0000000..56697d5 --- /dev/null +++ b/cmd/sim/evm/supported_protocols_test.go @@ -0,0 +1,79 @@ +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/sim.go b/cmd/sim/sim.go index 8d16b92..f7d252d 100644 --- a/cmd/sim/sim.go +++ b/cmd/sim/sim.go @@ -27,7 +27,8 @@ func NewSimCmd() *cobra.Command { "lookups.\n\n" + "Available subcommands:\n" + " evm - Query EVM chains: balances, activity, transactions, collectibles,\n" + - " token-info, token-holders, defi-positions, supported-chains\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" +