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
7 changes: 4 additions & 3 deletions cmd/sim/svm/svm.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ func requireSimClient(cmd *cobra.Command) (SimClient, error) {
func NewSvmCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "svm",
Short: "Query SVM chain data (balances)",
Long: "Access real-time SVM blockchain data including token balances\n" +
"for Solana and Eclipse chains.",
Short: "Query SVM chain data (balances, transactions)",
Long: "Access real-time SVM blockchain data including token balances and\n" +
"transaction history for Solana and Eclipse chains.",
}

cmd.AddCommand(NewBalancesCmd())
cmd.AddCommand(NewTransactionsCmd())

return cmd
}
138 changes: 138 additions & 0 deletions cmd/sim/svm/transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package svm

import (
"encoding/json"
"fmt"
"net/url"
"time"

"github.com/spf13/cobra"

"github.com/duneanalytics/cli/output"
)

// NewTransactionsCmd returns the `sim svm transactions` command.
func NewTransactionsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "transactions <address>",
Short: "Get SVM transactions for a wallet address",
Long: "Return transactions for the given SVM wallet address.\n" +
"Raw transaction data is available in JSON output.\n\n" +
"Examples:\n" +
" dune sim svm transactions 86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\n" +
" dune sim svm transactions 86xCnPeV... --limit 20\n" +
" dune sim svm transactions 86xCnPeV... -o json",
Args: cobra.ExactArgs(1),
RunE: runTransactions,
}

cmd.Flags().Int("limit", 0, "Max results (1-1000, default 100)")
cmd.Flags().String("offset", "", "Pagination cursor from previous response")
output.AddFormatFlag(cmd, "text")

return cmd
}

// --- Response types ---

type svmTransactionsResponse struct {
NextOffset string `json:"next_offset,omitempty"`
Transactions []svmTransaction `json:"transactions"`
}

type svmTransaction struct {
Address string `json:"address"`
BlockSlot json.Number `json:"block_slot"`
BlockTime json.Number `json:"block_time"`
Chain string `json:"chain"`
RawTransaction json.RawMessage `json:"raw_transaction,omitempty"`
}

func runTransactions(cmd *cobra.Command, args []string) error {
client, err := requireSimClient(cmd)
if err != nil {
return err
}

address := args[0]
params := url.Values{}

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(), "/beta/svm/transactions/"+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 svmTransactionsResponse
if err := json.Unmarshal(data, &resp); err != nil {
return fmt.Errorf("parsing response: %w", err)
}

if len(resp.Transactions) == 0 {
fmt.Fprintln(w, "No transactions found.")
return nil
}

columns := []string{"CHAIN", "BLOCK_SLOT", "BLOCK_TIME", "TX_SIGNATURE"}
rows := make([][]string, len(resp.Transactions))
for i, tx := range resp.Transactions {
rows[i] = []string{
tx.Chain,
tx.BlockSlot.String(),
formatBlockTime(tx.BlockTime),
extractSignature(tx.RawTransaction),
}
}
output.PrintTable(w, columns, rows)

if resp.NextOffset != "" {
fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset)
}
return nil
}
}

// formatBlockTime converts a block_time (microseconds since epoch) to a
// human-readable UTC timestamp.
func formatBlockTime(bt json.Number) string {
us, err := bt.Int64()
if err != nil {
return bt.String()
}
// block_time is in microseconds.
t := time.Unix(0, us*int64(time.Microsecond))
return t.UTC().Format("2006-01-02 15:04:05")
}

// extractSignature pulls the first transaction signature from raw_transaction.
// Returns the signature string or an empty string if unavailable.
func extractSignature(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}

var rt struct {
Transaction struct {
Signatures []string `json:"signatures"`
} `json:"transaction"`
}
if err := json.Unmarshal(raw, &rt); err != nil {
return ""
}
if len(rt.Transaction.Signatures) > 0 {
return rt.Transaction.Signatures[0]
}
return ""
}
132 changes: 132 additions & 0 deletions cmd/sim/svm/transactions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package svm_test

import (
"bytes"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSvmTransactions_Text(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5"})

require.NoError(t, root.Execute())

out := buf.String()
assert.Contains(t, out, "CHAIN")
assert.Contains(t, out, "BLOCK_SLOT")
assert.Contains(t, out, "BLOCK_TIME")
assert.Contains(t, out, "TX_SIGNATURE")
}

func TestSvmTransactions_JSON(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "5", "-o", "json"})

require.NoError(t, root.Execute())

var resp map[string]interface{}
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
assert.Contains(t, resp, "transactions")

txns, ok := resp["transactions"].([]interface{})
require.True(t, ok)
if len(txns) > 0 {
tx, ok := txns[0].(map[string]interface{})
require.True(t, ok)
assert.Contains(t, tx, "address")
assert.Contains(t, tx, "block_slot")
assert.Contains(t, tx, "block_time")
assert.Contains(t, tx, "chain")
assert.Contains(t, tx, "raw_transaction")
}
}

func TestSvmTransactions_Limit(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "3", "-o", "json"})

require.NoError(t, root.Execute())

var resp map[string]interface{}
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))

txns, ok := resp["transactions"].([]interface{})
require.True(t, ok)
assert.LessOrEqual(t, len(txns), 3)
}

func TestSvmTransactions_Pagination(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "-o", "json"})

require.NoError(t, root.Execute())

var resp map[string]interface{}
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
assert.Contains(t, resp, "transactions")

// If next_offset is present, fetch page 2.
if offset, ok := resp["next_offset"].(string); ok && offset != "" {
root2 := newSimTestRoot()
var buf2 bytes.Buffer
root2.SetOut(&buf2)
root2.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "2", "--offset", offset, "-o", "json"})

require.NoError(t, root2.Execute())

var resp2 map[string]interface{}
require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2))
assert.Contains(t, resp2, "transactions")
}
}

func TestSvmTransactions_RawTransactionInJSON(t *testing.T) {
key := simAPIKey(t)

root := newSimTestRoot()
var buf bytes.Buffer
root.SetOut(&buf)
root.SetArgs([]string{"sim", "--sim-api-key", key, "svm", "transactions", svmTestAddress, "--limit", "1", "-o", "json"})

require.NoError(t, root.Execute())

var resp map[string]interface{}
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))

txns, ok := resp["transactions"].([]interface{})
require.True(t, ok)
if len(txns) > 0 {
tx, ok := txns[0].(map[string]interface{})
require.True(t, ok)

// raw_transaction should be a nested object with transaction data.
rawTx, ok := tx["raw_transaction"].(map[string]interface{})
if ok {
// Should contain transaction with signatures.
txData, ok := rawTx["transaction"].(map[string]interface{})
if ok {
assert.Contains(t, txData, "signatures")
}
}
}
}
Loading