Skip to content

Commit ff23307

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 7b613d5 commit ff23307

6 files changed

Lines changed: 395 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
@@ -35,24 +35,56 @@ func NewBalancesCmd() *cobra.Command {
3535
type balancesResponse struct {
3636
WalletAddress string `json:"wallet_address"`
3737
Balances []balanceEntry `json:"balances"`
38+
Errors *balanceErrors `json:"errors,omitempty"`
3839
NextOffset string `json:"next_offset,omitempty"`
3940
Warnings []warningEntry `json:"warnings,omitempty"`
4041
RequestTime string `json:"request_time,omitempty"`
4142
ResponseTime string `json:"response_time,omitempty"`
4243
}
4344

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

5890
type warningEntry struct {
@@ -138,7 +170,8 @@ func runBalancesEndpoint(cmd *cobra.Command, args []string, pathPrefix, pathSuff
138170
return fmt.Errorf("parsing response: %w", err)
139171
}
140172

141-
// Print warnings to stderr.
173+
// Print errors and warnings to stderr.
174+
printBalanceErrors(cmd, resp.Errors)
142175
printWarnings(cmd, resp.Warnings)
143176

144177
columns := []string{"CHAIN", "SYMBOL", "AMOUNT", "PRICE_USD", "VALUE_USD"}
@@ -217,3 +250,24 @@ func formatUSD(v float64) string {
217250
}
218251
return fmt.Sprintf("%.2f", v)
219252
}
253+
254+
// printBalanceErrors writes balance-level errors to stderr.
255+
func printBalanceErrors(cmd *cobra.Command, errs *balanceErrors) {
256+
if errs == nil {
257+
return
258+
}
259+
stderr := cmd.ErrOrStderr()
260+
if errs.ErrorMessage != "" {
261+
fmt.Fprintf(stderr, "Error: %s\n", errs.ErrorMessage)
262+
}
263+
for _, e := range errs.TokenErrors {
264+
fmt.Fprintf(stderr, " chain_id=%d address=%s", e.ChainID, e.Address)
265+
if e.Description != "" {
266+
fmt.Fprintf(stderr, " — %s", e.Description)
267+
}
268+
fmt.Fprintln(stderr)
269+
}
270+
if errs.ErrorMessage != "" || len(errs.TokenErrors) > 0 {
271+
fmt.Fprintln(stderr)
272+
}
273+
}

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/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)