Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/sim/evm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewEvmCmd() *cobra.Command {
cmd.AddCommand(NewActivityCmd())
cmd.AddCommand(NewTransactionsCmd())
cmd.AddCommand(NewCollectiblesCmd())
cmd.AddCommand(NewTokenInfoCmd())

return cmd
}
137 changes: 137 additions & 0 deletions cmd/sim/evm/token_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 <address>",
Short: "Get token metadata and pricing",
Long: "Return metadata and pricing for a token contract address (or \"native\" for the\n" +
"chain's native asset) on a specified chain.\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", "", "Chain ID (required)")
cmd.Flags().String("historical-prices", "", "Hour offsets for historical prices (e.g. 720,168,24)")
cmd.Flags().Int("limit", 0, "Max results")
cmd.Flags().String("offset", "", "Pagination cursor from previous response")
_ = 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, err := requireSimClient(cmd)

Check failure on line 59 in cmd/sim/evm/token_info.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient

Check failure on line 59 in cmd/sim/evm/token_info.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient

Check failure on line 59 in cmd/sim/evm/token_info.go

View workflow job for this annotation

GitHub Actions / test

undefined: requireSimClient
if err != nil {
return err
}

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", 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", 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, formatUSD(hp.PriceUSD))
}
}

if resp.NextOffset != "" {
fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
}
return nil
}
}
126 changes: 126 additions & 0 deletions cmd/sim/evm/token_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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")
}
Loading