Skip to content

Commit f381fe4

Browse files
committed
Add evm transactions command and fix OpenAPI spec gaps across all EVM commands
- Add dune sim evm transactions with --chain-ids, --decode, --limit, --offset flags - Add --decode text-mode stderr hint and E2E tests (text, JSON, decode-text, decode-JSON, pagination) - Fix spec gaps: add missing struct fields across all EVM commands - transactions: block_version, max_fee_per_gas, max_priority_fee_per_gas, decoded, logs, errors - balances/balance/stablecoins: historical_prices, token_metadata, pool, errors + printBalanceErrors - activity: tokenMetadata.standard, functionInfo.inputs - balance: --metadata and --historical-prices CLI flags - Use json.RawMessage for decoded input values to handle non-string ABI types safely
1 parent bd39336 commit f381fe4

7 files changed

Lines changed: 396 additions & 14 deletions

File tree

cmd/sim/evm/activity.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,19 @@ type tokenMetadata struct {
7878
Logo string `json:"logo,omitempty"`
7979
PriceUSD float64 `json:"price_usd"`
8080
PoolSize float64 `json:"pool_size,omitempty"`
81+
Standard string `json:"standard,omitempty"`
8182
}
8283

8384
type functionInfo struct {
84-
Signature string `json:"signature,omitempty"`
85-
Name string `json:"name,omitempty"`
85+
Signature string `json:"signature,omitempty"`
86+
Name string `json:"name,omitempty"`
87+
Inputs []functionInput `json:"inputs,omitempty"`
88+
}
89+
90+
type functionInput struct {
91+
Name string `json:"name,omitempty"`
92+
Type string `json:"type,omitempty"`
93+
Value json.RawMessage `json:"value,omitempty"`
8694
}
8795

8896
type contractMetaObj struct {

cmd/sim/evm/balance.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func NewBalanceCmd() *cobra.Command {
2727

2828
cmd.Flags().String("token", "", "Token contract address or \"native\" (required)")
2929
cmd.Flags().String("chain-ids", "", "Chain ID (required)")
30+
cmd.Flags().String("metadata", "", "Extra metadata fields: logo,url,pools")
31+
cmd.Flags().String("historical-prices", "", "Hour offsets for historical prices (e.g. 720,168,24)")
3032
_ = cmd.MarkFlagRequired("token")
3133
_ = cmd.MarkFlagRequired("chain-ids")
3234
output.AddFormatFlag(cmd, "text")
@@ -47,6 +49,12 @@ func runBalance(cmd *cobra.Command, args []string) error {
4749
if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
4850
params.Set("chain_ids", v)
4951
}
52+
if v, _ := cmd.Flags().GetString("metadata"); v != "" {
53+
params.Set("metadata", v)
54+
}
55+
if v, _ := cmd.Flags().GetString("historical-prices"); v != "" {
56+
params.Set("historical_prices", v)
57+
}
5058

5159
path := fmt.Sprintf("/v1/evm/balances/%s/token/%s", address, tokenAddress)
5260
data, err := client.Get(cmd.Context(), path, params)

cmd/sim/evm/balances.go

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,56 @@ func NewBalancesCmd() *cobra.Command {
4242
type balancesResponse struct {
4343
WalletAddress string `json:"wallet_address"`
4444
Balances []balanceEntry `json:"balances"`
45+
Errors *balanceErrors `json:"errors,omitempty"`
4546
NextOffset string `json:"next_offset,omitempty"`
4647
Warnings []warningEntry `json:"warnings,omitempty"`
4748
RequestTime string `json:"request_time,omitempty"`
4849
ResponseTime string `json:"response_time,omitempty"`
4950
}
5051

52+
type balanceErrors struct {
53+
ErrorMessage string `json:"error_message,omitempty"`
54+
TokenErrors []balanceErrorInfo `json:"token_errors,omitempty"`
55+
}
56+
57+
type balanceErrorInfo struct {
58+
ChainID int64 `json:"chain_id"`
59+
Address string `json:"address"`
60+
Description string `json:"description,omitempty"`
61+
}
62+
5163
type balanceEntry struct {
52-
Chain string `json:"chain"`
53-
ChainID int64 `json:"chain_id"`
54-
Address string `json:"address"`
55-
Amount string `json:"amount"`
56-
Symbol string `json:"symbol"`
57-
Name string `json:"name"`
58-
Decimals int `json:"decimals"`
59-
PriceUSD float64 `json:"price_usd"`
60-
ValueUSD float64 `json:"value_usd"`
61-
PoolSize float64 `json:"pool_size"`
62-
LowLiquidity bool `json:"low_liquidity"`
64+
Chain string `json:"chain"`
65+
ChainID int64 `json:"chain_id"`
66+
Address string `json:"address"`
67+
Amount string `json:"amount"`
68+
Symbol string `json:"symbol"`
69+
Name string `json:"name"`
70+
Decimals int `json:"decimals"`
71+
PriceUSD float64 `json:"price_usd"`
72+
ValueUSD float64 `json:"value_usd"`
73+
PoolSize float64 `json:"pool_size"`
74+
LowLiquidity bool `json:"low_liquidity"`
75+
HistoricalPrices []historicalPrice `json:"historical_prices,omitempty"`
76+
TokenMetadata *balanceTokenMeta `json:"token_metadata,omitempty"`
77+
Pool *poolMetadata `json:"pool,omitempty"`
78+
}
79+
80+
type historicalPrice struct {
81+
OffsetHours int `json:"offset_hours"`
82+
PriceUSD float64 `json:"price_usd"`
83+
}
84+
85+
type balanceTokenMeta struct {
86+
Logo string `json:"logo,omitempty"`
87+
URL string `json:"url,omitempty"`
88+
}
89+
90+
type poolMetadata struct {
91+
PoolType string `json:"pool_type"`
92+
Address string `json:"address"`
93+
Token0 string `json:"token0"`
94+
Token1 string `json:"token1"`
6395
}
6496

6597
type warningEntry struct {
@@ -119,7 +151,8 @@ func runBalances(cmd *cobra.Command, args []string) error {
119151
return fmt.Errorf("parsing response: %w", err)
120152
}
121153

122-
// Print warnings to stderr.
154+
// Print errors and warnings to stderr.
155+
printBalanceErrors(cmd, resp.Errors)
123156
printWarnings(cmd, resp.Warnings)
124157

125158
columns := []string{"CHAIN", "SYMBOL", "AMOUNT", "PRICE_USD", "VALUE_USD"}
@@ -198,3 +231,24 @@ func formatUSD(v float64) string {
198231
}
199232
return fmt.Sprintf("%.2f", v)
200233
}
234+
235+
// printBalanceErrors writes balance-level errors to stderr.
236+
func printBalanceErrors(cmd *cobra.Command, errs *balanceErrors) {
237+
if errs == nil {
238+
return
239+
}
240+
stderr := cmd.ErrOrStderr()
241+
if errs.ErrorMessage != "" {
242+
fmt.Fprintf(stderr, "Error: %s\n", errs.ErrorMessage)
243+
}
244+
for _, e := range errs.TokenErrors {
245+
fmt.Fprintf(stderr, " chain_id=%d address=%s", e.ChainID, e.Address)
246+
if e.Description != "" {
247+
fmt.Fprintf(stderr, " — %s", e.Description)
248+
}
249+
fmt.Fprintln(stderr)
250+
}
251+
if errs.ErrorMessage != "" || len(errs.TokenErrors) > 0 {
252+
fmt.Fprintln(stderr)
253+
}
254+
}

cmd/sim/evm/evm.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func NewEvmCmd() *cobra.Command {
4343
cmd.AddCommand(NewBalanceCmd())
4444
cmd.AddCommand(NewStablecoinsCmd())
4545
cmd.AddCommand(NewActivityCmd())
46+
cmd.AddCommand(NewTransactionsCmd())
4647

4748
return cmd
4849
}

cmd/sim/evm/stablecoins.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func runStablecoins(cmd *cobra.Command, args []string) error {
8484
return fmt.Errorf("parsing response: %w", err)
8585
}
8686

87+
printBalanceErrors(cmd, resp.Errors)
8788
printWarnings(cmd, resp.Warnings)
8889

8990
columns := []string{"CHAIN", "SYMBOL", "AMOUNT", "PRICE_USD", "VALUE_USD"}

cmd/sim/evm/transactions.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package evm
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/duneanalytics/cli/output"
11+
)
12+
13+
// NewTransactionsCmd returns the `sim evm transactions` command.
14+
func NewTransactionsCmd() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "transactions <address>",
17+
Short: "Get EVM transactions for a wallet address",
18+
Long: "Return transaction history for the given wallet address across supported EVM chains.\n" +
19+
"Use --decode with -o json to include decoded function calls and event logs.\n\n" +
20+
"Examples:\n" +
21+
" dune sim evm transactions 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" +
22+
" dune sim evm transactions 0xd8da... --chain-ids 1 --decode -o json\n" +
23+
" dune sim evm transactions 0xd8da... --limit 50 -o json",
24+
Args: cobra.ExactArgs(1),
25+
RunE: runTransactions,
26+
}
27+
28+
cmd.Flags().String("chain-ids", "", "Comma-separated chain IDs or tags (default: all default chains)")
29+
cmd.Flags().Bool("decode", false, "Include decoded transaction data and logs (use with -o json)")
30+
cmd.Flags().Int("limit", 0, "Max results (1-100)")
31+
cmd.Flags().String("offset", "", "Pagination cursor from previous response")
32+
output.AddFormatFlag(cmd, "text")
33+
34+
return cmd
35+
}
36+
37+
type transactionsResponse struct {
38+
WalletAddress string `json:"wallet_address"`
39+
Transactions []transactionTx `json:"transactions"`
40+
Errors *transactionErrors `json:"errors,omitempty"`
41+
NextOffset string `json:"next_offset,omitempty"`
42+
Warnings []warningEntry `json:"warnings,omitempty"`
43+
RequestTime string `json:"request_time,omitempty"`
44+
ResponseTime string `json:"response_time,omitempty"`
45+
}
46+
47+
type transactionErrors struct {
48+
ErrorMessage string `json:"error_message,omitempty"`
49+
TransactionErrors []transactionErrorInfo `json:"transaction_errors,omitempty"`
50+
}
51+
52+
type transactionErrorInfo struct {
53+
ChainID int64 `json:"chain_id"`
54+
Address string `json:"address"`
55+
Description string `json:"description,omitempty"`
56+
}
57+
58+
type transactionTx struct {
59+
Address string `json:"address"`
60+
BlockHash string `json:"block_hash"`
61+
BlockNumber json.Number `json:"block_number"`
62+
BlockTime string `json:"block_time"`
63+
BlockVersion int `json:"block_version,omitempty"`
64+
Chain string `json:"chain"`
65+
From string `json:"from"`
66+
To string `json:"to"`
67+
Data string `json:"data,omitempty"`
68+
GasPrice string `json:"gas_price,omitempty"`
69+
Hash string `json:"hash"`
70+
Index json.Number `json:"index,omitempty"`
71+
MaxFeePerGas string `json:"max_fee_per_gas,omitempty"`
72+
MaxPriorityFeePerGas string `json:"max_priority_fee_per_gas,omitempty"`
73+
Nonce string `json:"nonce,omitempty"`
74+
TransactionType string `json:"transaction_type,omitempty"`
75+
Value string `json:"value"`
76+
Decoded *decodedCall `json:"decoded,omitempty"`
77+
Logs []transactionLog `json:"logs,omitempty"`
78+
}
79+
80+
type decodedCall struct {
81+
Name string `json:"name,omitempty"`
82+
Inputs []decodedInput `json:"inputs,omitempty"`
83+
}
84+
85+
type decodedInput struct {
86+
Name string `json:"name,omitempty"`
87+
Type string `json:"type,omitempty"`
88+
Value json.RawMessage `json:"value,omitempty"`
89+
}
90+
91+
type transactionLog struct {
92+
Address string `json:"address,omitempty"`
93+
Data string `json:"data,omitempty"`
94+
Topics []string `json:"topics,omitempty"`
95+
Decoded *decodedCall `json:"decoded,omitempty"`
96+
}
97+
98+
func runTransactions(cmd *cobra.Command, args []string) error {
99+
client, err := requireSimClient(cmd)
100+
if err != nil {
101+
return err
102+
}
103+
104+
address := args[0]
105+
params := url.Values{}
106+
107+
if v, _ := cmd.Flags().GetString("chain-ids"); v != "" {
108+
params.Set("chain_ids", v)
109+
}
110+
if v, _ := cmd.Flags().GetBool("decode"); v {
111+
params.Set("decode", "true")
112+
}
113+
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
114+
params.Set("limit", fmt.Sprintf("%d", v))
115+
}
116+
if v, _ := cmd.Flags().GetString("offset"); v != "" {
117+
params.Set("offset", v)
118+
}
119+
120+
data, err := client.Get(cmd.Context(), "/v1/evm/transactions/"+address, params)
121+
if err != nil {
122+
return err
123+
}
124+
125+
w := cmd.OutOrStdout()
126+
switch output.FormatFromCmd(cmd) {
127+
case output.FormatJSON:
128+
var raw json.RawMessage = data
129+
return output.PrintJSON(w, raw)
130+
default:
131+
var resp transactionsResponse
132+
if err := json.Unmarshal(data, &resp); err != nil {
133+
return fmt.Errorf("parsing response: %w", err)
134+
}
135+
136+
// Warn if --decode is used in text mode since the table can't show decoded data.
137+
if decode, _ := cmd.Flags().GetBool("decode"); decode {
138+
fmt.Fprintln(cmd.ErrOrStderr(), "Note: --decode data is only visible in JSON output. Use -o json to see decoded fields.")
139+
}
140+
141+
// Print errors to stderr.
142+
printTransactionErrors(cmd, resp.Errors)
143+
144+
// Print warnings to stderr.
145+
printWarnings(cmd, resp.Warnings)
146+
147+
columns := []string{"CHAIN", "HASH", "FROM", "TO", "VALUE", "BLOCK_TIME"}
148+
rows := make([][]string, len(resp.Transactions))
149+
for i, tx := range resp.Transactions {
150+
rows[i] = []string{
151+
tx.Chain,
152+
truncateHash(tx.Hash),
153+
truncateHash(tx.From),
154+
truncateHash(tx.To),
155+
tx.Value,
156+
tx.BlockTime,
157+
}
158+
}
159+
output.PrintTable(w, columns, rows)
160+
161+
if resp.NextOffset != "" {
162+
fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
163+
}
164+
return nil
165+
}
166+
}
167+
168+
// printTransactionErrors writes transaction-level errors to stderr.
169+
func printTransactionErrors(cmd *cobra.Command, errs *transactionErrors) {
170+
if errs == nil {
171+
return
172+
}
173+
stderr := cmd.ErrOrStderr()
174+
if errs.ErrorMessage != "" {
175+
fmt.Fprintf(stderr, "Error: %s\n", errs.ErrorMessage)
176+
}
177+
for _, e := range errs.TransactionErrors {
178+
fmt.Fprintf(stderr, " chain_id=%d address=%s", e.ChainID, e.Address)
179+
if e.Description != "" {
180+
fmt.Fprintf(stderr, " — %s", e.Description)
181+
}
182+
fmt.Fprintln(stderr)
183+
}
184+
if errs.ErrorMessage != "" || len(errs.TransactionErrors) > 0 {
185+
fmt.Fprintln(stderr)
186+
}
187+
}

0 commit comments

Comments
 (0)