Skip to content

Commit 568e39f

Browse files
committed
Add svm transactions command with signature extraction and block time formatting
1 parent 691a8b0 commit 568e39f

3 files changed

Lines changed: 274 additions & 3 deletions

File tree

cmd/sim/svm/svm.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ func requireSimClient(cmd *cobra.Command) (SimClient, error) {
4242
func NewSvmCmd() *cobra.Command {
4343
cmd := &cobra.Command{
4444
Use: "svm",
45-
Short: "Query SVM chain data (balances)",
46-
Long: "Access real-time SVM blockchain data including token balances\n" +
47-
"for Solana and Eclipse chains.",
45+
Short: "Query SVM chain data (balances, transactions)",
46+
Long: "Access real-time SVM blockchain data including token balances and\n" +
47+
"transaction history for Solana and Eclipse chains.",
4848
}
4949

5050
cmd.AddCommand(NewBalancesCmd())
51+
cmd.AddCommand(NewTransactionsCmd())
5152

5253
return cmd
5354
}

cmd/sim/svm/transactions.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package svm
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
"time"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/duneanalytics/cli/output"
12+
)
13+
14+
// NewTransactionsCmd returns the `sim svm transactions` command.
15+
func NewTransactionsCmd() *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "transactions <address>",
18+
Short: "Get SVM transactions for a wallet address",
19+
Long: "Return transactions for the given SVM wallet address.\n" +
20+
"Raw transaction data is available in JSON output.\n\n" +
21+
"Examples:\n" +
22+
" dune sim svm transactions 86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\n" +
23+
" dune sim svm transactions 86xCnPeV... --limit 20\n" +
24+
" dune sim svm transactions 86xCnPeV... -o json",
25+
Args: cobra.ExactArgs(1),
26+
RunE: runTransactions,
27+
}
28+
29+
cmd.Flags().Int("limit", 0, "Max results (1-1000, default 100)")
30+
cmd.Flags().String("offset", "", "Pagination cursor from previous response")
31+
output.AddFormatFlag(cmd, "text")
32+
33+
return cmd
34+
}
35+
36+
// --- Response types ---
37+
38+
type svmTransactionsResponse struct {
39+
NextOffset string `json:"next_offset,omitempty"`
40+
Transactions []svmTransaction `json:"transactions"`
41+
}
42+
43+
type svmTransaction struct {
44+
Address string `json:"address"`
45+
BlockSlot json.Number `json:"block_slot"`
46+
BlockTime json.Number `json:"block_time"`
47+
Chain string `json:"chain"`
48+
RawTransaction json.RawMessage `json:"raw_transaction,omitempty"`
49+
}
50+
51+
func runTransactions(cmd *cobra.Command, args []string) error {
52+
client, err := requireSimClient(cmd)
53+
if err != nil {
54+
return err
55+
}
56+
57+
address := args[0]
58+
params := url.Values{}
59+
60+
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
61+
params.Set("limit", fmt.Sprintf("%d", v))
62+
}
63+
if v, _ := cmd.Flags().GetString("offset"); v != "" {
64+
params.Set("offset", v)
65+
}
66+
67+
data, err := client.Get(cmd.Context(), "/beta/svm/transactions/"+address, params)
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 svmTransactionsResponse
79+
if err := json.Unmarshal(data, &resp); err != nil {
80+
return fmt.Errorf("parsing response: %w", err)
81+
}
82+
83+
if len(resp.Transactions) == 0 {
84+
fmt.Fprintln(w, "No transactions found.")
85+
return nil
86+
}
87+
88+
columns := []string{"CHAIN", "BLOCK_SLOT", "BLOCK_TIME", "TX_SIGNATURE"}
89+
rows := make([][]string, len(resp.Transactions))
90+
for i, tx := range resp.Transactions {
91+
rows[i] = []string{
92+
tx.Chain,
93+
tx.BlockSlot.String(),
94+
formatBlockTime(tx.BlockTime),
95+
extractSignature(tx.RawTransaction),
96+
}
97+
}
98+
output.PrintTable(w, columns, rows)
99+
100+
if resp.NextOffset != "" {
101+
fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
102+
}
103+
return nil
104+
}
105+
}
106+
107+
// formatBlockTime converts a block_time (microseconds since epoch) to a
108+
// human-readable UTC timestamp.
109+
func formatBlockTime(bt json.Number) string {
110+
us, err := bt.Int64()
111+
if err != nil {
112+
return bt.String()
113+
}
114+
// block_time is in microseconds.
115+
t := time.Unix(0, us*int64(time.Microsecond))
116+
return t.UTC().Format("2006-01-02 15:04:05")
117+
}
118+
119+
// extractSignature pulls the first transaction signature from raw_transaction.
120+
// Returns the signature string or an empty string if unavailable.
121+
func extractSignature(raw json.RawMessage) string {
122+
if len(raw) == 0 {
123+
return ""
124+
}
125+
126+
var rt struct {
127+
Transaction struct {
128+
Signatures []string `json:"signatures"`
129+
} `json:"transaction"`
130+
}
131+
if err := json.Unmarshal(raw, &rt); err != nil {
132+
return ""
133+
}
134+
if len(rt.Transaction.Signatures) > 0 {
135+
return rt.Transaction.Signatures[0]
136+
}
137+
return ""
138+
}

cmd/sim/svm/transactions_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package svm_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestSvmTransactions_Text(t *testing.T) {
13+
key := simAPIKey(t)
14+
15+
root := newSimTestRoot()
16+
var buf bytes.Buffer
17+
root.SetOut(&buf)
18+
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5"})
19+
20+
require.NoError(t, root.Execute())
21+
22+
out := buf.String()
23+
assert.Contains(t, out, "CHAIN")
24+
assert.Contains(t, out, "BLOCK_SLOT")
25+
assert.Contains(t, out, "BLOCK_TIME")
26+
assert.Contains(t, out, "TX_SIGNATURE")
27+
}
28+
29+
func TestSvmTransactions_JSON(t *testing.T) {
30+
key := simAPIKey(t)
31+
32+
root := newSimTestRoot()
33+
var buf bytes.Buffer
34+
root.SetOut(&buf)
35+
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5", "-o", "json"})
36+
37+
require.NoError(t, root.Execute())
38+
39+
var resp map[string]interface{}
40+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
41+
assert.Contains(t, resp, "transactions")
42+
43+
txns, ok := resp["transactions"].([]interface{})
44+
require.True(t, ok)
45+
if len(txns) > 0 {
46+
tx, ok := txns[0].(map[string]interface{})
47+
require.True(t, ok)
48+
assert.Contains(t, tx, "address")
49+
assert.Contains(t, tx, "block_slot")
50+
assert.Contains(t, tx, "block_time")
51+
assert.Contains(t, tx, "chain")
52+
assert.Contains(t, tx, "raw_transaction")
53+
}
54+
}
55+
56+
func TestSvmTransactions_Limit(t *testing.T) {
57+
key := simAPIKey(t)
58+
59+
root := newSimTestRoot()
60+
var buf bytes.Buffer
61+
root.SetOut(&buf)
62+
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "3", "-o", "json"})
63+
64+
require.NoError(t, root.Execute())
65+
66+
var resp map[string]interface{}
67+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
68+
69+
txns, ok := resp["transactions"].([]interface{})
70+
require.True(t, ok)
71+
assert.LessOrEqual(t, len(txns), 3)
72+
}
73+
74+
func TestSvmTransactions_Pagination(t *testing.T) {
75+
key := simAPIKey(t)
76+
77+
root := newSimTestRoot()
78+
var buf bytes.Buffer
79+
root.SetOut(&buf)
80+
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "-o", "json"})
81+
82+
require.NoError(t, root.Execute())
83+
84+
var resp map[string]interface{}
85+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
86+
assert.Contains(t, resp, "transactions")
87+
88+
// If next_offset is present, fetch page 2.
89+
if offset, ok := resp["next_offset"].(string); ok && offset != "" {
90+
root2 := newSimTestRoot()
91+
var buf2 bytes.Buffer
92+
root2.SetOut(&buf2)
93+
root2.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "--offset", offset, "-o", "json"})
94+
95+
require.NoError(t, root2.Execute())
96+
97+
var resp2 map[string]interface{}
98+
require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
99+
assert.Contains(t, resp2, "transactions")
100+
}
101+
}
102+
103+
func TestSvmTransactions_RawTransactionInJSON(t *testing.T) {
104+
key := simAPIKey(t)
105+
106+
root := newSimTestRoot()
107+
var buf bytes.Buffer
108+
root.SetOut(&buf)
109+
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "1", "-o", "json"})
110+
111+
require.NoError(t, root.Execute())
112+
113+
var resp map[string]interface{}
114+
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
115+
116+
txns, ok := resp["transactions"].([]interface{})
117+
require.True(t, ok)
118+
if len(txns) > 0 {
119+
tx, ok := txns[0].(map[string]interface{})
120+
require.True(t, ok)
121+
122+
// raw_transaction should be a nested object with transaction data.
123+
rawTx, ok := tx["raw_transaction"].(map[string]interface{})
124+
if ok {
125+
// Should contain transaction with signatures.
126+
txData, ok := rawTx["transaction"].(map[string]interface{})
127+
if ok {
128+
assert.Contains(t, txData, "signatures")
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)