Files
indexer/evm/adapter_test.go
T
zeekayandHanzo Dev 7a4566d4b3 chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 15:21:19 -07:00

2678 lines
73 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package evm
import (
"context"
"encoding/json"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/luxfi/indexer/chain"
)
// TestNew tests the adapter constructor
func TestNew(t *testing.T) {
endpoint := "http://localhost:9650/v1/chain/c/rpc"
adapter := New(endpoint)
if adapter == nil {
t.Fatal("New returned nil adapter")
}
if adapter.rpcEndpoint != endpoint {
t.Errorf("rpcEndpoint = %q, want %q", adapter.rpcEndpoint, endpoint)
}
if adapter.httpClient == nil {
t.Error("httpClient should not be nil")
}
if adapter.httpClient.Timeout != 30*time.Second {
t.Errorf("httpClient.Timeout = %v, want 30s", adapter.httpClient.Timeout)
}
}
// TestConstants tests the package constants
func TestConstants(t *testing.T) {
tests := []struct {
name string
got interface{}
want interface{}
}{
{"DefaultPort", DefaultPort, 4000},
{"DefaultDatabase", DefaultDatabase, "explorer_evm"},
{"ChainID", ChainID, 96369},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want)
}
})
}
}
// TestEventSignatures tests the well-known event topic signatures
func TestEventSignatures(t *testing.T) {
// These should match the Keccak256 hashes of the event signatures
tests := []struct {
name string
topic string
}{
{"TopicTransferERC20", TopicTransferERC20},
{"TopicTransferERC721", TopicTransferERC721}, // Same as ERC20
{"TopicTransferSingle", TopicTransferSingle},
{"TopicTransferBatch", TopicTransferBatch},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.topic == "" {
t.Errorf("%s should not be empty", tt.name)
}
if len(tt.topic) != 66 {
t.Errorf("%s length = %d, want 66 (0x + 64 hex chars)", tt.name, len(tt.topic))
}
if tt.topic[:2] != "0x" {
t.Errorf("%s should start with '0x'", tt.name)
}
})
}
// ERC20 and ERC721 should have same topic
if TopicTransferERC20 != TopicTransferERC721 {
t.Error("TopicTransferERC20 should equal TopicTransferERC721")
}
}
// TestAdapterImplementsInterface verifies Adapter implements chain.Adapter
func TestAdapterImplementsInterface(t *testing.T) {
var _ chain.Adapter = (*Adapter)(nil)
}
// TestParseBlock tests parsing EVM block data
func TestParseBlock(t *testing.T) {
adapter := New("http://localhost:9650")
tests := []struct {
name string
input json.RawMessage
wantID string
wantHeight uint64
wantTxCount int
wantErr bool
}{
{
name: "valid block with tx hashes",
input: json.RawMessage(`{
"hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"parentHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"number": "0x64",
"timestamp": "0x61234567",
"transactions": ["0xtx1", "0xtx2", "0xtx3"],
"gasUsed": "0x5208",
"gasLimit": "0x7a1200",
"miner": "0x0000000000000000000000000000000000000000",
"baseFeePerGas": "0x174876e800",
"size": "0x200"
}`),
wantID: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
wantHeight: 100,
wantTxCount: 3,
wantErr: false,
},
{
name: "valid block with tx objects",
input: json.RawMessage(`{
"hash": "0xabcd",
"parentHash": "0xparent",
"number": "0xa",
"timestamp": "0x5f5e100",
"transactions": [{"hash": "0xtx1"}, {"hash": "0xtx2"}],
"gasUsed": "0x0",
"gasLimit": "0x0",
"miner": "0x0",
"baseFeePerGas": "0x0",
"size": "0x0"
}`),
wantID: "0xabcd",
wantHeight: 10,
wantTxCount: 2,
wantErr: false,
},
{
name: "block with no transactions",
input: json.RawMessage(`{
"hash": "0xempty",
"parentHash": "0xparent",
"number": "0x1",
"timestamp": "0x1",
"transactions": [],
"gasUsed": "0x0",
"gasLimit": "0x0"
}`),
wantID: "0xempty",
wantHeight: 1,
wantTxCount: 0,
wantErr: false,
},
{
name: "invalid JSON",
input: json.RawMessage(`{invalid`),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
block, err := adapter.ParseBlock(tt.input)
if tt.wantErr {
if err == nil {
t.Error("expected error but got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if block.ID != tt.wantID {
t.Errorf("ID = %q, want %q", block.ID, tt.wantID)
}
if block.Height != tt.wantHeight {
t.Errorf("Height = %d, want %d", block.Height, tt.wantHeight)
}
if block.TxCount != tt.wantTxCount {
t.Errorf("TxCount = %d, want %d", block.TxCount, tt.wantTxCount)
}
if block.Status != chain.StatusAccepted {
t.Errorf("Status = %q, want %q", block.Status, chain.StatusAccepted)
}
})
}
}
// TestParseBlockMetadata tests that metadata is correctly extracted
func TestParseBlockMetadata(t *testing.T) {
adapter := New("http://localhost:9650")
input := json.RawMessage(`{
"hash": "0xblock",
"parentHash": "0xparent",
"number": "0x100",
"timestamp": "0x60000000",
"transactions": [],
"gasUsed": "0x1000",
"gasLimit": "0x2000",
"miner": "0xminer123",
"baseFeePerGas": "0x3b9aca00",
"size": "0x500"
}`)
block, err := adapter.ParseBlock(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tests := []struct {
key string
want interface{}
}{
{"gasUsed", uint64(0x1000)},
{"gasLimit", uint64(0x2000)},
{"miner", "0xminer123"},
{"baseFee", "0x3b9aca00"},
{"size", uint64(0x500)},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
got := block.Metadata[tt.key]
if got != tt.want {
t.Errorf("Metadata[%q] = %v, want %v", tt.key, got, tt.want)
}
})
}
}
// TestGetRecentBlocks tests fetching recent blocks
func TestGetRecentBlocks(t *testing.T) {
// Create a mock server
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
method := req["method"].(string)
if method == "eth_blockNumber" {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": "0x64", // Block 100
})
return
}
if method == "eth_getBlockByNumber" {
callCount++
params := req["params"].([]interface{})
blockNum := params[0].(string)
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"hash": fmt.Sprintf("0xblock_%s", blockNum),
"parentHash": "0xparent",
"number": blockNum,
"timestamp": "0x60000000",
"transactions": []string{},
"gasUsed": "0x0",
"gasLimit": "0x0",
},
})
}
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
blocks, err := adapter.GetRecentBlocks(ctx, 5)
if err != nil {
t.Fatalf("GetRecentBlocks failed: %v", err)
}
if len(blocks) != 5 {
t.Errorf("got %d blocks, want 5", len(blocks))
}
if callCount != 5 {
t.Errorf("expected 5 block fetch calls, got %d", callCount)
}
}
// TestGetBlockByID tests fetching a block by hash
func TestGetBlockByID(t *testing.T) {
blockHash := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
method := req["method"].(string)
if method != "eth_getBlockByHash" {
t.Errorf("expected method eth_getBlockByHash, got %s", method)
}
params := req["params"].([]interface{})
hash := params[0].(string)
if hash != blockHash {
t.Errorf("expected hash %s, got %s", blockHash, hash)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"hash": blockHash,
"parentHash": "0xparent",
"number": "0x64",
"timestamp": "0x60000000",
"transactions": []string{},
"gasUsed": "0x0",
"gasLimit": "0x0",
},
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
result, err := adapter.GetBlockByID(ctx, blockHash)
if err != nil {
t.Fatalf("GetBlockByID failed: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
}
// TestGetBlockByHeight tests fetching a block by number
func TestGetBlockByHeight(t *testing.T) {
height := uint64(100)
expectedHex := "0x64"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
method := req["method"].(string)
if method != "eth_getBlockByNumber" {
t.Errorf("expected method eth_getBlockByNumber, got %s", method)
}
params := req["params"].([]interface{})
blockNum := params[0].(string)
if blockNum != expectedHex {
t.Errorf("expected block number %s, got %s", expectedHex, blockNum)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"hash": "0xblock100",
"parentHash": "0xparent",
"number": expectedHex,
"timestamp": "0x60000000",
"transactions": []string{},
"gasUsed": "0x0",
"gasLimit": "0x0",
},
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
result, err := adapter.GetBlockByHeight(ctx, height)
if err != nil {
t.Fatalf("GetBlockByHeight failed: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
}
// TestGetTransactionReceipt tests fetching a transaction receipt
func TestGetTransactionReceipt(t *testing.T) {
txHash := "0xtx123"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"transactionHash": txHash,
"blockHash": "0xblock",
"blockNumber": "0x64",
"from": "0xfrom123",
"to": "0xto456",
"gasUsed": "0x5208",
"status": "0x1",
"contractAddress": "",
"transactionIndex": "0x0",
"logs": []map[string]interface{}{
{
"address": "0xtoken",
"topics": []string{TopicTransferERC20, "0xtopic1", "0xtopic2"},
"data": "0xdata",
"logIndex": "0x0",
"removed": false,
},
},
},
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
tx, logs, err := adapter.GetTransactionReceipt(ctx, txHash)
if err != nil {
t.Fatalf("GetTransactionReceipt failed: %v", err)
}
if tx.Hash != txHash {
t.Errorf("Hash = %q, want %q", tx.Hash, txHash)
}
if tx.BlockNumber != 100 {
t.Errorf("BlockNumber = %d, want 100", tx.BlockNumber)
}
if tx.From != "0xfrom123" {
t.Errorf("From = %q, want 0xfrom123", tx.From)
}
if tx.To != "0xto456" {
t.Errorf("To = %q, want 0xto456", tx.To)
}
if tx.Status == nil || *tx.Status != 1 {
t.Errorf("Status = %v, want 1", tx.Status)
}
if tx.GasUsed != 21000 {
t.Errorf("GasUsed = %d, want 21000", tx.GasUsed)
}
if len(logs) != 1 {
t.Fatalf("expected 1 log, got %d", len(logs))
}
if logs[0].Address != "0xtoken" {
t.Errorf("Log address = %q, want 0xtoken", logs[0].Address)
}
}
// TestGetBalance tests fetching an address balance
func TestGetBalance(t *testing.T) {
address := "0xaddress123"
expectedBalance := "0xde0b6b3a7640000" // 1 ETH in wei
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["method"] != "eth_getBalance" {
t.Errorf("expected eth_getBalance, got %s", req["method"])
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedBalance,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
balance, err := adapter.GetBalance(ctx, address)
if err != nil {
t.Fatalf("GetBalance failed: %v", err)
}
expected := new(big.Int)
expected.SetString("1000000000000000000", 10) // 1 ETH
if balance.Cmp(expected) != 0 {
t.Errorf("Balance = %s, want %s", balance.String(), expected.String())
}
}
// TestGetCode tests fetching contract code
func TestGetCode(t *testing.T) {
address := "0xcontract123"
contractCode := "0x608060405234801561001057600080fd5b50"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["method"] != "eth_getCode" {
t.Errorf("expected eth_getCode, got %s", req["method"])
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": contractCode,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
code, err := adapter.GetCode(ctx, address)
if err != nil {
t.Fatalf("GetCode failed: %v", err)
}
if code != contractCode {
t.Errorf("Code = %q, want %q", code, contractCode)
}
}
// TestGetTokenInfo tests fetching ERC20 token metadata
func TestGetTokenInfo(t *testing.T) {
tokenAddress := "0xtoken123"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
params := req["params"].([]interface{})
callParams := params[0].(map[string]interface{})
data := callParams["data"].(string)
var result string
switch data {
case "0x06fdde03": // name()
// "TestToken" encoded
result = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e0000000000000000000000000000000000000000000000"
case "0x95d89b41": // symbol()
// "TST" encoded
result = "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003545354000000000000000000000000000000000000000000000000000000000000"
case "0x313ce567": // decimals()
result = "0x12" // 18
case "0x18160ddd": // totalSupply()
result = "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" // 1e18
default:
result = "0x"
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": result,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
token, err := adapter.GetTokenInfo(ctx, tokenAddress)
if err != nil {
t.Fatalf("GetTokenInfo failed: %v", err)
}
if token.Address != tokenAddress {
t.Errorf("Address = %q, want %q", token.Address, tokenAddress)
}
if token.Name != "TestToken" {
t.Errorf("Name = %q, want TestToken", token.Name)
}
if token.Symbol != "TST" {
t.Errorf("Symbol = %q, want TST", token.Symbol)
}
if token.Decimals != 18 {
t.Errorf("Decimals = %d, want 18", token.Decimals)
}
if token.TokenType != TypeERC20 {
t.Errorf("TokenType = %q, want ERC20", token.TokenType)
}
}
// TestTransactionStruct tests Transaction JSON serialization
func TestTransactionStruct(t *testing.T) {
tx := Transaction{
Hash: "0xhash123",
BlockHash: "0xblock456",
BlockNumber: 100,
From: "0xfrom",
To: "0xto",
Value: "1000000000000000000",
Gas: 21000,
GasPrice: "1000000000",
GasUsed: 21000,
Nonce: 5,
Input: "0x",
TransactionIndex: 0,
Type: 2,
Status: intPtr(1),
ContractAddress: "",
Timestamp: time.Now(),
}
data, err := json.Marshal(tx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded Transaction
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Hash != tx.Hash {
t.Errorf("Hash = %q, want %q", decoded.Hash, tx.Hash)
}
if decoded.BlockNumber != tx.BlockNumber {
t.Errorf("BlockNumber = %d, want %d", decoded.BlockNumber, tx.BlockNumber)
}
if decoded.Value != tx.Value {
t.Errorf("Value = %q, want %q", decoded.Value, tx.Value)
}
}
// TestAddressStruct tests Address JSON serialization
func TestAddressStruct(t *testing.T) {
addr := Address{
Hash: "0xaddress123",
Balance: "1000000000000000000",
TxCount: 10,
IsContract: true,
ContractCode: "0x608060",
ContractCreator: "0xcreator",
ContractTxHash: "0xtx",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
data, err := json.Marshal(addr)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded Address
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Hash != addr.Hash {
t.Errorf("Hash = %q, want %q", decoded.Hash, addr.Hash)
}
if decoded.IsContract != addr.IsContract {
t.Errorf("IsContract = %v, want %v", decoded.IsContract, addr.IsContract)
}
}
// TestTokenTransferStruct tests TokenTransfer JSON serialization
func TestTokenTransferStruct(t *testing.T) {
transfer := TokenTransfer{
ID: "0xtx-0",
TxHash: "0xtx",
LogIndex: 0,
BlockNumber: 100,
TokenAddress: "0xtoken",
TokenType: TypeERC20,
From: "0xfrom",
To: "0xto",
Value: "1000",
TokenID: "",
Timestamp: time.Now(),
}
data, err := json.Marshal(transfer)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded TokenTransfer
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.ID != transfer.ID {
t.Errorf("ID = %q, want %q", decoded.ID, transfer.ID)
}
if decoded.TokenType != transfer.TokenType {
t.Errorf("TokenType = %q, want %q", decoded.TokenType, transfer.TokenType)
}
}
// TestTokenStruct tests Token JSON serialization
func TestTokenStruct(t *testing.T) {
token := Token{
Address: "0xtoken",
Name: "Test Token",
Symbol: "TST",
Decimals: 18,
TotalSupply: "1000000000000000000000000",
TokenType: TypeERC20,
HolderCount: 1000,
TxCount: 5000,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
data, err := json.Marshal(token)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded Token
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Address != token.Address {
t.Errorf("Address = %q, want %q", decoded.Address, token.Address)
}
if decoded.Decimals != token.Decimals {
t.Errorf("Decimals = %d, want %d", decoded.Decimals, token.Decimals)
}
}
// TestLogStruct tests Log JSON serialization
func TestLogStruct(t *testing.T) {
log := Log{
TxHash: "0xtx",
LogIndex: 0,
BlockNumber: 100,
Address: "0xcontract",
Topics: []string{"0xtopic0", "0xtopic1"},
Data: "0xdata",
Removed: false,
}
data, err := json.Marshal(log)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded Log
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.TxHash != log.TxHash {
t.Errorf("TxHash = %q, want %q", decoded.TxHash, log.TxHash)
}
if len(decoded.Topics) != len(log.Topics) {
t.Errorf("Topics length = %d, want %d", len(decoded.Topics), len(log.Topics))
}
}
// TestInternalTransactionStruct tests InternalTransaction JSON serialization
func TestInternalTransactionStruct(t *testing.T) {
itx := InternalTransaction{
ID: "0xtx-0",
TxHash: "0xtx",
BlockNumber: 100,
TraceIndex: 0,
CallType: "call",
From: "0xfrom",
To: "0xto",
Value: "1000",
Gas: 100000,
GasUsed: 50000,
Input: "0x",
Output: "0x",
Error: "",
Timestamp: time.Now(),
}
data, err := json.Marshal(itx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded InternalTransaction
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.ID != itx.ID {
t.Errorf("ID = %q, want %q", decoded.ID, itx.ID)
}
if decoded.CallType != itx.CallType {
t.Errorf("CallType = %q, want %q", decoded.CallType, itx.CallType)
}
}
// TestHexToUint64 tests the hexToUint64 helper
func TestHexToUint64(t *testing.T) {
tests := []struct {
input string
want uint64
}{
{"0x0", 0},
{"0x1", 1},
{"0xa", 10},
{"0x64", 100},
{"0x100", 256},
{"0xffffffff", 4294967295},
{"", 0},
{"0x", 0},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := hexToUint64(tt.input)
if got != tt.want {
t.Errorf("hexToUint64(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
// TestHexToBigInt tests the hexToBigInt helper
func TestHexToBigInt(t *testing.T) {
tests := []struct {
input string
want string
}{
{"0x0", "0"},
{"0x1", "1"},
{"0x64", "100"},
{"0xde0b6b3a7640000", "1000000000000000000"},
{"", "0"},
{"0x", "0"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := hexToBigInt(tt.input)
if got.String() != tt.want {
t.Errorf("hexToBigInt(%q) = %s, want %s", tt.input, got.String(), tt.want)
}
})
}
}
// TestTopicToAddress tests the topicToAddress helper
func TestTopicToAddress(t *testing.T) {
tests := []struct {
input string
want string
}{
{
"0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd",
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
},
{
"0x000000000000000000000000ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD",
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
},
{"0x1234", ""},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := topicToAddress(tt.input)
if got != tt.want {
t.Errorf("topicToAddress(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestAddressToTopic tests the addressToTopic helper
func TestAddressToTopic(t *testing.T) {
tests := []struct {
input string
want string
}{
{
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd",
},
{
"0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD",
"0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd",
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := addressToTopic(tt.input)
if got != tt.want {
t.Errorf("addressToTopic(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestDecodeString tests the ABI string decoding helper
func TestDecodeString(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "TestToken",
input: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e0000000000000000000000000000000000000000000000",
want: "TestToken",
},
{
name: "TST",
input: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003545354000000000000000000000000000000000000000000000000000000000000",
want: "TST",
},
{
name: "empty input",
input: "",
want: "",
},
{
name: "too short",
input: "0x1234",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := decodeString(tt.input)
if got != tt.want {
t.Errorf("decodeString() = %q, want %q", got, tt.want)
}
})
}
}
// TestNetworkError tests error handling for network failures
func TestNetworkError(t *testing.T) {
adapter := New("http://localhost:99999")
ctx := context.Background()
_, err := adapter.GetRecentBlocks(ctx, 5)
if err == nil {
t.Error("expected error for unreachable server")
}
}
// TestContextCancellation tests context cancellation handling
func TestContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
}))
defer server.Close()
adapter := New(server.URL)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := adapter.GetRecentBlocks(ctx, 5)
if err == nil {
t.Error("expected error for cancelled context")
}
}
// TestRPCError tests RPC error response handling
func TestRPCError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"error": map[string]interface{}{
"code": -32000,
"message": "block not found",
},
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
_, err := adapter.GetBlockByID(ctx, "0xnonexistent")
if err == nil {
t.Error("expected error for RPC error response")
}
if err != nil && !contains(err.Error(), "block not found") {
t.Errorf("error should contain 'block not found', got: %v", err)
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// TestParseTokenTransfersERC20 tests parsing ERC20 transfer logs
func TestParseTokenTransfersERC20(t *testing.T) {
log := Log{
TxHash: "0xtx123",
LogIndex: 0,
BlockNumber: 100,
Address: "0xtoken",
Topics: []string{
TopicTransferERC20,
"0x000000000000000000000000from1234567890abcdef1234567890abcdef1234",
"0x000000000000000000000000to1234567890abcdef1234567890abcdefabcd",
},
Data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
Removed: false,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
transfer := transfers[0]
if transfer.TokenType != TypeERC20 {
t.Errorf("TokenType = %q, want ERC20", transfer.TokenType)
}
if transfer.From != "0xfrom1234567890abcdef1234567890abcdef1234" {
t.Errorf("From = %q", transfer.From)
}
if transfer.Value != "1000000000000000000" {
t.Errorf("Value = %q, want 1000000000000000000", transfer.Value)
}
}
// TestParseTokenTransfersERC721 tests parsing ERC721 transfer logs
func TestParseTokenTransfersERC721(t *testing.T) {
log := Log{
TxHash: "0xtx123",
LogIndex: 0,
BlockNumber: 100,
Address: "0xnft",
Topics: []string{
TopicTransferERC721,
"0x000000000000000000000000from1234567890abcdef1234567890abcdef1234",
"0x000000000000000000000000to1234567890abcdef1234567890abcdefabcd",
"0x0000000000000000000000000000000000000000000000000000000000000001",
},
Data: "0x",
Removed: false,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
transfer := transfers[0]
if transfer.TokenType != TypeERC721 {
t.Errorf("TokenType = %q, want ERC721", transfer.TokenType)
}
if transfer.Value != "1" {
t.Errorf("Value = %q, want 1", transfer.Value)
}
if transfer.TokenID != "0x0000000000000000000000000000000000000000000000000000000000000001" {
t.Errorf("TokenID = %q", transfer.TokenID)
}
}
// TestParseTokenTransfersEmpty tests parsing logs with no transfers
func TestParseTokenTransfersEmpty(t *testing.T) {
// Log with no topics
log := Log{
TxHash: "0xtx",
LogIndex: 0,
BlockNumber: 100,
Address: "0xcontract",
Topics: []string{},
Data: "0x",
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 0 {
t.Errorf("expected 0 transfers, got %d", len(transfers))
}
}
// TestParseTokenTransfersUnknownEvent tests parsing unknown event logs
func TestParseTokenTransfersUnknownEvent(t *testing.T) {
log := Log{
TxHash: "0xtx",
LogIndex: 0,
BlockNumber: 100,
Address: "0xcontract",
Topics: []string{"0xunknowneventtopichash0000000000000000000000000000000000000000"},
Data: "0x",
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 0 {
t.Errorf("expected 0 transfers for unknown event, got %d", len(transfers))
}
}
// Benchmarks
func BenchmarkParseBlock(b *testing.B) {
adapter := New("http://localhost:9650")
input := json.RawMessage(`{
"hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"parentHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"number": "0x64",
"timestamp": "0x61234567",
"transactions": ["0xtx1", "0xtx2", "0xtx3", "0xtx4", "0xtx5"],
"gasUsed": "0x5208",
"gasLimit": "0x7a1200",
"miner": "0x0000000000000000000000000000000000000000",
"baseFeePerGas": "0x174876e800",
"size": "0x200"
}`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = adapter.ParseBlock(input)
}
}
func BenchmarkHexToUint64(b *testing.B) {
inputs := []string{"0x0", "0x64", "0xffffffff", "0xde0b6b3a7640000"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, input := range inputs {
hexToUint64(input)
}
}
}
func BenchmarkParseTokenTransfers(b *testing.B) {
log := Log{
TxHash: "0xtx123",
LogIndex: 0,
BlockNumber: 100,
Address: "0xtoken",
Topics: []string{
TopicTransferERC20,
"0x000000000000000000000000from1234567890abcdef1234567890abcdef1234",
"0x000000000000000000000000to1234567890abcdef1234567890abcdefabcd",
},
Data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
Removed: false,
}
timestamp := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
parseTokenTransfers(log, timestamp)
}
}
func BenchmarkDecodeString(b *testing.B) {
input := "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e0000000000000000000000000000000000000000000000"
b.ResetTimer()
for i := 0; i < b.N; i++ {
decodeString(input)
}
}
// ============================================================================
// Phase 1 Tests: Internal Transaction Tracing
// ============================================================================
// TestFlattenCallFrame tests the call tree flattening with trace addresses
func TestFlattenCallFrame(t *testing.T) {
timestamp := time.Now()
txHash := "0xtx123"
blockNumber := uint64(100)
tests := []struct {
name string
frame *CallFrame
wantCount int
wantAddresses [][]int
wantCallTypes []string
}{
{
name: "single call",
frame: &CallFrame{
Type: "CALL",
From: "0xfrom",
To: "0xto",
Value: "0x100",
Gas: "0x5208",
GasUsed: "0x5000",
Input: "0x",
Output: "0x",
},
wantCount: 1,
wantAddresses: [][]int{{}},
wantCallTypes: []string{"call"},
},
{
name: "nested calls",
frame: &CallFrame{
Type: "CALL",
From: "0xa",
To: "0xb",
Value: "0x0",
Gas: "0x10000",
GasUsed: "0x8000",
Calls: []*CallFrame{
{
Type: "CALL",
From: "0xb",
To: "0xc",
Value: "0x0",
Gas: "0x5000",
GasUsed: "0x3000",
},
{
Type: "DELEGATECALL",
From: "0xb",
To: "0xd",
Value: "0x0",
Gas: "0x3000",
GasUsed: "0x2000",
},
},
},
wantCount: 3,
wantAddresses: [][]int{{}, {0}, {1}},
wantCallTypes: []string{"call", "call", "delegatecall"},
},
{
name: "deeply nested calls",
frame: &CallFrame{
Type: "CALL",
From: "0xa",
To: "0xb",
Gas: "0x10000",
GasUsed: "0x8000",
Calls: []*CallFrame{
{
Type: "CALL",
From: "0xb",
To: "0xc",
Gas: "0x5000",
GasUsed: "0x3000",
Calls: []*CallFrame{
{
Type: "STATICCALL",
From: "0xc",
To: "0xd",
Gas: "0x2000",
GasUsed: "0x1000",
},
},
},
},
},
wantCount: 3,
wantAddresses: [][]int{{}, {0}, {0, 0}},
wantCallTypes: []string{"call", "call", "staticcall"},
},
{
name: "create operation",
frame: &CallFrame{
Type: "CREATE",
From: "0xdeployer",
To: "0xnewcontract",
Value: "0x0",
Gas: "0x100000",
GasUsed: "0x50000",
Input: "0x608060405234801561001057600080fd5b50",
Output: "0x608060405234801561001057600080fd5b50",
},
wantCount: 1,
wantAddresses: [][]int{{}},
wantCallTypes: []string{"create"},
},
{
name: "call with error",
frame: &CallFrame{
Type: "CALL",
From: "0xa",
To: "0xb",
Gas: "0x5208",
GasUsed: "0x5208",
Error: "execution reverted",
},
wantCount: 1,
wantAddresses: [][]int{{}},
wantCallTypes: []string{"call"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var traces []InternalTransaction
result := flattenCallFrame(tt.frame, txHash, blockNumber, timestamp, []int{}, &traces, 0)
if len(result) != tt.wantCount {
t.Errorf("flattenCallFrame() returned %d traces, want %d", len(result), tt.wantCount)
return
}
for i, itx := range result {
// Check trace address
if !intSlicesEqual(itx.TraceAddress, tt.wantAddresses[i]) {
t.Errorf("trace[%d].TraceAddress = %v, want %v", i, itx.TraceAddress, tt.wantAddresses[i])
}
// Check call type
if itx.CallType != tt.wantCallTypes[i] {
t.Errorf("trace[%d].CallType = %q, want %q", i, itx.CallType, tt.wantCallTypes[i])
}
// Check transaction hash
if itx.TxHash != txHash {
t.Errorf("trace[%d].TxHash = %q, want %q", i, itx.TxHash, txHash)
}
// Check block number
if itx.BlockNumber != blockNumber {
t.Errorf("trace[%d].BlockNumber = %d, want %d", i, itx.BlockNumber, blockNumber)
}
}
})
}
}
// intSlicesEqual compares two int slices for equality
func intSlicesEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// TestNormalizeCallType tests call type normalization
func TestNormalizeCallType(t *testing.T) {
tests := []struct {
input string
want string
}{
{"CALL", "call"},
{"Call", "call"},
{"call", "call"},
{"DELEGATECALL", "delegatecall"},
{"STATICCALL", "staticcall"},
{"CALLCODE", "callcode"},
{"CREATE", "create"},
{"CREATE2", "create2"},
{"SELFDESTRUCT", "selfdestruct"},
{"SUICIDE", "selfdestruct"},
{"suicide", "selfdestruct"},
{"unknown", "unknown"},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := normalizeCallType(tt.input)
if got != tt.want {
t.Errorf("normalizeCallType(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestNormalizeValue tests value normalization
func TestNormalizeValue(t *testing.T) {
tests := []struct {
input string
want string
}{
{"", "0x0"},
{"0x0", "0x0"},
{"0x100", "0x100"},
{"0xde0b6b3a7640000", "0xde0b6b3a7640000"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := normalizeValue(tt.input)
if got != tt.want {
t.Errorf("normalizeValue(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestIntSliceToPostgresArray tests PostgreSQL array formatting
func TestIntSliceToPostgresArray(t *testing.T) {
tests := []struct {
input []int
want string
}{
{[]int{}, "{}"},
{[]int{0}, "{0}"},
{[]int{0, 1}, "{0,1}"},
{[]int{0, 1, 2}, "{0,1,2}"},
{[]int{1, 2, 3, 4, 5}, "{1,2,3,4,5}"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
got := intSliceToPostgresArray(tt.input)
if got != tt.want {
t.Errorf("intSliceToPostgresArray(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestPostgresArrayToIntSlice tests PostgreSQL array parsing
func TestPostgresArrayToIntSlice(t *testing.T) {
tests := []struct {
input string
want []int
}{
{"{}", []int{}},
{"{0}", []int{0}},
{"{0,1}", []int{0, 1}},
{"{0,1,2}", []int{0, 1, 2}},
{"{1, 2, 3}", []int{1, 2, 3}},
{"", []int{}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := postgresArrayToIntSlice(tt.input)
if !intSlicesEqual(got, tt.want) {
t.Errorf("postgresArrayToIntSlice(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
// TestTraceAddressToString tests trace address string formatting
func TestTraceAddressToString(t *testing.T) {
tests := []struct {
input []int
want string
}{
{[]int{}, "[]"},
{[]int{0}, "[0]"},
{[]int{0, 1}, "[0,1]"},
{[]int{0, 1, 2}, "[0,1,2]"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
got := TraceAddressToString(tt.input)
if got != tt.want {
t.Errorf("TraceAddressToString(%v) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// TestSortInternalTransactionsByTraceAddress tests sorting by trace address
func TestSortInternalTransactionsByTraceAddress(t *testing.T) {
itxs := []InternalTransaction{
{ID: "1", TraceAddress: []int{1}},
{ID: "2", TraceAddress: []int{0, 1}},
{ID: "3", TraceAddress: []int{}},
{ID: "4", TraceAddress: []int{0}},
{ID: "5", TraceAddress: []int{0, 0}},
}
SortInternalTransactionsByTraceAddress(itxs)
expected := []string{"3", "4", "5", "2", "1"}
for i, itx := range itxs {
if itx.ID != expected[i] {
t.Errorf("sorted[%d].ID = %q, want %q", i, itx.ID, expected[i])
}
}
}
// TestInternalTransactionWithTraceAddress tests InternalTransaction struct with TraceAddress
func TestInternalTransactionWithTraceAddress(t *testing.T) {
itx := InternalTransaction{
ID: "0xtx-0",
TxHash: "0xtx",
BlockNumber: 100,
TraceIndex: 0,
TraceAddress: []int{0, 1, 2},
CallType: "call",
From: "0xfrom",
To: "0xto",
Value: "0x100",
Gas: 100000,
GasUsed: 50000,
Input: "0x",
Output: "0x",
Error: "",
CreatedContractAddress: "",
CreatedContractCode: "",
Init: "",
Timestamp: time.Now(),
}
// Test JSON serialization
data, err := json.Marshal(itx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded InternalTransaction
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if !intSlicesEqual(decoded.TraceAddress, itx.TraceAddress) {
t.Errorf("TraceAddress = %v, want %v", decoded.TraceAddress, itx.TraceAddress)
}
if decoded.CallType != itx.CallType {
t.Errorf("CallType = %q, want %q", decoded.CallType, itx.CallType)
}
}
// ============================================================================
// Phase 1 Tests: Token Balance Tracking
// ============================================================================
// TestTokenBalanceStruct tests TokenBalance JSON serialization
func TestTokenBalanceStruct(t *testing.T) {
tb := TokenBalance{
TokenAddress: "0xtoken",
HolderAddress: "0xholder",
Balance: "1000000000000000000",
TokenID: "123",
BlockNumber: 100,
UpdatedAt: time.Now(),
}
data, err := json.Marshal(tb)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded TokenBalance
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.TokenAddress != tb.TokenAddress {
t.Errorf("TokenAddress = %q, want %q", decoded.TokenAddress, tb.TokenAddress)
}
if decoded.HolderAddress != tb.HolderAddress {
t.Errorf("HolderAddress = %q, want %q", decoded.HolderAddress, tb.HolderAddress)
}
if decoded.Balance != tb.Balance {
t.Errorf("Balance = %q, want %q", decoded.Balance, tb.Balance)
}
if decoded.TokenID != tb.TokenID {
t.Errorf("TokenID = %q, want %q", decoded.TokenID, tb.TokenID)
}
}
// TestGetTokenBalance tests fetching ERC20 token balance via RPC
func TestGetTokenBalance(t *testing.T) {
tokenAddress := "0xtoken123"
holderAddress := "0xholder456"
expectedBalance := "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" // 1e18
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["method"] != "eth_call" {
t.Errorf("expected eth_call, got %s", req["method"])
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedBalance,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
balance, err := adapter.GetTokenBalance(ctx, tokenAddress, holderAddress, 0)
if err != nil {
t.Fatalf("GetTokenBalance failed: %v", err)
}
expected := new(big.Int)
expected.SetString("1000000000000000000", 10)
if balance.Cmp(expected) != 0 {
t.Errorf("Balance = %s, want %s", balance.String(), expected.String())
}
}
// TestGetERC721Owner tests fetching ERC721 owner
func TestGetERC721Owner(t *testing.T) {
tokenAddress := "0xnft123"
tokenID := big.NewInt(42)
expectedOwner := "0x000000000000000000000000abcdef1234567890abcdef1234567890abcdef12"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedOwner,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
owner, err := adapter.GetERC721Owner(ctx, tokenAddress, tokenID)
if err != nil {
t.Fatalf("GetERC721Owner failed: %v", err)
}
if owner != "0xabcdef1234567890abcdef1234567890abcdef12" {
t.Errorf("Owner = %q, want 0xabcdef1234567890abcdef1234567890abcdef12", owner)
}
}
// TestGetERC1155Balance tests fetching ERC1155 balance
func TestGetERC1155Balance(t *testing.T) {
tokenAddress := "0xerc1155"
holderAddress := "0xholder"
tokenID := big.NewInt(1)
expectedBalance := "0x0000000000000000000000000000000000000000000000000000000000000064" // 100
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedBalance,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
balance, err := adapter.GetERC1155Balance(ctx, tokenAddress, holderAddress, tokenID)
if err != nil {
t.Fatalf("GetERC1155Balance failed: %v", err)
}
if balance.Cmp(big.NewInt(100)) != 0 {
t.Errorf("Balance = %s, want 100", balance.String())
}
}
// ============================================================================
// Phase 1 Tests: Address Coin Balance Tracking
// ============================================================================
// TestAddressCoinBalanceStruct tests AddressCoinBalance JSON serialization
func TestAddressCoinBalanceStruct(t *testing.T) {
acb := AddressCoinBalance{
AddressHash: "0xaddress",
BlockNumber: 100,
Value: "1000000000000000000",
ValueFetchedAt: time.Now(),
}
data, err := json.Marshal(acb)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded AddressCoinBalance
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.AddressHash != acb.AddressHash {
t.Errorf("AddressHash = %q, want %q", decoded.AddressHash, acb.AddressHash)
}
if decoded.BlockNumber != acb.BlockNumber {
t.Errorf("BlockNumber = %d, want %d", decoded.BlockNumber, acb.BlockNumber)
}
if decoded.Value != acb.Value {
t.Errorf("Value = %q, want %q", decoded.Value, acb.Value)
}
}
// TestGetBalanceAtBlock tests fetching balance at specific block
func TestGetBalanceAtBlock(t *testing.T) {
address := "0xaddress123"
blockNumber := uint64(100)
expectedBalance := "0xde0b6b3a7640000" // 1 LUX in wei
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["method"] != "eth_getBalance" {
t.Errorf("expected eth_getBalance, got %s", req["method"])
}
params := req["params"].([]interface{})
blockTag := params[1].(string)
if blockTag != "0x64" {
t.Errorf("expected block tag 0x64, got %s", blockTag)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedBalance,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
balance, err := adapter.GetBalanceAtBlock(ctx, address, blockNumber)
if err != nil {
t.Fatalf("GetBalanceAtBlock failed: %v", err)
}
expected := new(big.Int)
expected.SetString("1000000000000000000", 10)
if balance.Cmp(expected) != 0 {
t.Errorf("Balance = %s, want %s", balance.String(), expected.String())
}
}
// TestGetBalanceAtBlockLatest tests fetching balance at latest block
func TestGetBalanceAtBlockLatest(t *testing.T) {
address := "0xaddress123"
expectedBalance := "0xde0b6b3a7640000"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
params := req["params"].([]interface{})
blockTag := params[1].(string)
if blockTag != "latest" {
t.Errorf("expected block tag 'latest', got %s", blockTag)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": expectedBalance,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
// Block number 0 should use "latest"
balance, err := adapter.GetBalanceAtBlock(ctx, address, 0)
if err != nil {
t.Fatalf("GetBalanceAtBlock failed: %v", err)
}
if balance == nil {
t.Error("Balance should not be nil")
}
}
// ============================================================================
// Phase 1 Tests: Tracer Options
// ============================================================================
// TestAdapterOptions tests adapter configuration options
func TestAdapterOptions(t *testing.T) {
endpoint := "http://localhost:9650"
// Test WithTracerType
adapter := New(endpoint, WithTracerType(TracerParity))
if adapter.tracerType != TracerParity {
t.Errorf("tracerType = %v, want TracerParity", adapter.tracerType)
}
// Test WithTraceTimeout
adapter = New(endpoint, WithTraceTimeout("60s"))
if adapter.traceTimeout != "60s" {
t.Errorf("traceTimeout = %q, want 60s", adapter.traceTimeout)
}
// Test multiple options
adapter = New(endpoint,
WithTracerType(TracerJS),
WithTraceTimeout("30s"),
)
if adapter.tracerType != TracerJS {
t.Errorf("tracerType = %v, want TracerJS", adapter.tracerType)
}
if adapter.traceTimeout != "30s" {
t.Errorf("traceTimeout = %q, want 30s", adapter.traceTimeout)
}
}
// TestTracerTypeConstants tests tracer type constants
func TestTracerTypeConstants(t *testing.T) {
tests := []struct {
tracer TracerType
want string
}{
{TracerCallTracer, "callTracer"},
{TracerJS, "js"},
{TracerParity, "parity"},
}
for _, tt := range tests {
t.Run(string(tt.tracer), func(t *testing.T) {
if string(tt.tracer) != tt.want {
t.Errorf("TracerType = %q, want %q", tt.tracer, tt.want)
}
})
}
}
// ============================================================================
// Phase 1 Tests: TraceTransaction RPC Mocking
// ============================================================================
// TestTraceTransactionCallTracer tests debug_traceTransaction with callTracer
func TestTraceTransactionCallTracer(t *testing.T) {
txHash := "0xtx123"
blockNumber := uint64(100)
timestamp := time.Now()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["method"] != "debug_traceTransaction" {
t.Errorf("expected debug_traceTransaction, got %s", req["method"])
}
// Return a call tree with nested calls
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"type": "CALL",
"from": "0xa",
"to": "0xb",
"value": "0x100",
"gas": "0x10000",
"gasUsed": "0x8000",
"input": "0x",
"output": "0x",
"calls": []map[string]interface{}{
{
"type": "CALL",
"from": "0xb",
"to": "0xc",
"value": "0x0",
"gas": "0x5000",
"gasUsed": "0x3000",
"input": "0x",
"output": "0x",
},
},
},
})
}))
defer server.Close()
adapter := New(server.URL, WithTracerType(TracerCallTracer))
ctx := context.Background()
traces, err := adapter.TraceTransaction(ctx, txHash, blockNumber, timestamp)
if err != nil {
t.Fatalf("TraceTransaction failed: %v", err)
}
if len(traces) != 2 {
t.Errorf("expected 2 traces, got %d", len(traces))
return
}
// Check root call
if traces[0].CallType != "call" {
t.Errorf("traces[0].CallType = %q, want call", traces[0].CallType)
}
if !intSlicesEqual(traces[0].TraceAddress, []int{}) {
t.Errorf("traces[0].TraceAddress = %v, want []", traces[0].TraceAddress)
}
// Check nested call
if traces[1].CallType != "call" {
t.Errorf("traces[1].CallType = %q, want call", traces[1].CallType)
}
if !intSlicesEqual(traces[1].TraceAddress, []int{0}) {
t.Errorf("traces[1].TraceAddress = %v, want [0]", traces[1].TraceAddress)
}
}
// TestTraceTransactionCreate tests tracing contract creation
func TestTraceTransactionCreate(t *testing.T) {
txHash := "0xtxcreate"
blockNumber := uint64(100)
timestamp := time.Now()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"type": "CREATE",
"from": "0xdeployer",
"to": "0xnewcontract",
"value": "0x0",
"gas": "0x100000",
"gasUsed": "0x50000",
"input": "0x608060405234801561001057600080fd5b50",
"output": "0x608060",
},
})
}))
defer server.Close()
adapter := New(server.URL, WithTracerType(TracerCallTracer))
ctx := context.Background()
traces, err := adapter.TraceTransaction(ctx, txHash, blockNumber, timestamp)
if err != nil {
t.Fatalf("TraceTransaction failed: %v", err)
}
if len(traces) != 1 {
t.Fatalf("expected 1 trace, got %d", len(traces))
}
trace := traces[0]
if trace.CallType != "create" {
t.Errorf("CallType = %q, want create", trace.CallType)
}
if trace.CreatedContractAddress != "0xnewcontract" {
t.Errorf("CreatedContractAddress = %q, want 0xnewcontract", trace.CreatedContractAddress)
}
if trace.Init != "0x608060405234801561001057600080fd5b50" {
t.Errorf("Init = %q", trace.Init)
}
}
// TestTraceTransactionFailedCreate verifies that a failed CREATE does not set
// CreatedContractAddress or CreatedContractCode (upstream fix: ap-fix-internal-transaction-error-field).
func TestTraceTransactionFailedCreate(t *testing.T) {
txHash := "0xtxfailedcreate"
blockNumber := uint64(200)
timestamp := time.Now()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"result": map[string]interface{}{
"type": "CREATE",
"from": "0xdeployer",
"to": "0xnewcontract",
"value": "0x0",
"gas": "0x100000",
"gasUsed": "0x100000",
"input": "0x608060405234801561001057600080fd5b50",
"output": "0x",
"error": "execution reverted",
},
})
}))
defer server.Close()
adapter := New(server.URL, WithTracerType(TracerCallTracer))
ctx := context.Background()
traces, err := adapter.TraceTransaction(ctx, txHash, blockNumber, timestamp)
if err != nil {
t.Fatalf("TraceTransaction failed: %v", err)
}
if len(traces) != 1 {
t.Fatalf("expected 1 trace, got %d", len(traces))
}
trace := traces[0]
if trace.CallType != "create" {
t.Errorf("CallType = %q, want create", trace.CallType)
}
if trace.Error != "execution reverted" {
t.Errorf("Error = %q, want 'execution reverted'", trace.Error)
}
if trace.CreatedContractAddress != "" {
t.Errorf("CreatedContractAddress = %q, want empty (failed create)", trace.CreatedContractAddress)
}
if trace.CreatedContractCode != "" {
t.Errorf("CreatedContractCode = %q, want empty (failed create)", trace.CreatedContractCode)
}
if trace.Init != "0x608060405234801561001057600080fd5b50" {
t.Errorf("Init = %q, want init code preserved even on failure", trace.Init)
}
}
// TestFlattenCallFrameNestedError verifies error propagation: parent succeeds,
// child fails -- child's error is preserved, parent has no error.
func TestFlattenCallFrameNestedError(t *testing.T) {
timestamp := time.Now()
txHash := "0xtxnestederror"
blockNumber := uint64(300)
frame := &CallFrame{
Type: "CALL",
From: "0xa",
To: "0xb",
Gas: "0x10000",
GasUsed: "0x8000",
// Parent succeeds (no error)
Calls: []*CallFrame{
{
Type: "CALL",
From: "0xb",
To: "0xc",
Gas: "0x5000",
GasUsed: "0x5000",
Error: "out of gas", // Child fails
},
},
}
var traces []InternalTransaction
flattenCallFrame(frame, txHash, blockNumber, timestamp, []int{}, &traces, 0)
if len(traces) != 2 {
t.Fatalf("expected 2 traces, got %d", len(traces))
}
// Parent: no error
if traces[0].Error != "" {
t.Errorf("parent Error = %q, want empty", traces[0].Error)
}
// Child: error preserved
if traces[1].Error != "out of gas" {
t.Errorf("child Error = %q, want 'out of gas'", traces[1].Error)
}
}
// ============================================================================
// Phase 1 Tests: ERC1155 TransferSingle Parsing
// ============================================================================
// TestParseTokenTransfersERC1155Single tests parsing ERC1155 TransferSingle logs
func TestParseTokenTransfersERC1155Single(t *testing.T) {
// Use properly formatted 40-char addresses (without 0x prefix)
// Topics are 32 bytes = 64 hex chars: 24 zeros padding + 40 char address
fromAddr := "1234567890abcdef1234567890abcdef12345678"
toAddr := "abcdef1234567890abcdef1234567890abcdef12"
operatorAddr := "9999999999999999999999999999999999999999"
log := Log{
TxHash: "0xtx123",
LogIndex: 0,
BlockNumber: 100,
Address: "0xerc1155token",
Topics: []string{
TopicTransferSingle,
"0x000000000000000000000000" + operatorAddr, // operator (topic[1])
"0x000000000000000000000000" + fromAddr, // from (topic[2])
"0x000000000000000000000000" + toAddr, // to (topic[3])
},
// Data contains tokenId (32 bytes) + value (32 bytes)
Data: "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000064",
Removed: false,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
transfer := transfers[0]
if transfer.TokenType != TypeERC1155 {
t.Errorf("TokenType = %q, want ERC1155", transfer.TokenType)
}
if transfer.TokenAddress != "0xerc1155token" {
t.Errorf("TokenAddress = %q, want 0xerc1155token", transfer.TokenAddress)
}
expectedFrom := "0x" + fromAddr
if transfer.From != expectedFrom {
t.Errorf("From = %q, want %q", transfer.From, expectedFrom)
}
expectedTo := "0x" + toAddr
if transfer.To != expectedTo {
t.Errorf("To = %q, want %q", transfer.To, expectedTo)
}
}
// ============================================================================
// HIGH Finding Tests: TransferBatch, TransferSingle fix, Non-standard ERC721,
// ERC-404, WETH events, decodeBatchData
// ============================================================================
// TestParseTokenTransfersERC1155Batch tests parsing ERC1155 TransferBatch logs
func TestParseTokenTransfersERC1155Batch(t *testing.T) {
fromAddr := "1234567890abcdef1234567890abcdef12345678"
toAddr := "abcdef1234567890abcdef1234567890abcdef12"
operatorAddr := "9999999999999999999999999999999999999999"
// ABI-encode (uint256[], uint256[]) with 3 items each:
// offset_ids = 0x40 (64 bytes), offset_values = 0xc0 (192 bytes)
// ids = [1, 2, 3], values = [10, 20, 30]
data := "0x" +
"0000000000000000000000000000000000000000000000000000000000000040" + // offset to ids array (64)
"00000000000000000000000000000000000000000000000000000000000000c0" + // offset to values array (192)
"0000000000000000000000000000000000000000000000000000000000000003" + // ids.length = 3
"0000000000000000000000000000000000000000000000000000000000000001" + // ids[0] = 1
"0000000000000000000000000000000000000000000000000000000000000002" + // ids[1] = 2
"0000000000000000000000000000000000000000000000000000000000000003" + // ids[2] = 3
"0000000000000000000000000000000000000000000000000000000000000003" + // values.length = 3
"000000000000000000000000000000000000000000000000000000000000000a" + // values[0] = 10
"0000000000000000000000000000000000000000000000000000000000000014" + // values[1] = 20
"000000000000000000000000000000000000000000000000000000000000001e" // values[2] = 30
log := Log{
TxHash: "0xtxbatch",
LogIndex: 5,
BlockNumber: 200,
Address: "0xerc1155contract",
Topics: []string{
TopicTransferBatch,
"0x000000000000000000000000" + operatorAddr,
"0x000000000000000000000000" + fromAddr,
"0x000000000000000000000000" + toAddr,
},
Data: data,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 3 {
t.Fatalf("expected 3 transfers, got %d", len(transfers))
}
wantIDs := []string{hexWord("1"), hexWord("2"), hexWord("3")}
wantValues := []string{"10", "20", "30"}
for i, tr := range transfers {
if tr.TokenType != TypeERC1155 {
t.Errorf("[%d] TokenType = %q, want ERC1155", i, tr.TokenType)
}
if tr.TokenID != wantIDs[i] {
t.Errorf("[%d] TokenID = %q, want %q", i, tr.TokenID, wantIDs[i])
}
if tr.Value != wantValues[i] {
t.Errorf("[%d] Value = %q, want %q", i, tr.Value, wantValues[i])
}
if tr.From != "0x"+fromAddr {
t.Errorf("[%d] From = %q", i, tr.From)
}
if tr.To != "0x"+toAddr {
t.Errorf("[%d] To = %q", i, tr.To)
}
// Each batch item gets a unique ID suffix
wantID := fmt.Sprintf("0xtxbatch-5-%d", i)
if tr.ID != wantID {
t.Errorf("[%d] ID = %q, want %q", i, tr.ID, wantID)
}
}
}
// TestParseTokenTransfersERC1155SingleFixed tests the fixed data length check
func TestParseTokenTransfersERC1155SingleFixed(t *testing.T) {
fromAddr := "1234567890abcdef1234567890abcdef12345678"
toAddr := "abcdef1234567890abcdef1234567890abcdef12"
operatorAddr := "9999999999999999999999999999999999999999"
// data with 0x prefix + 128 hex chars (two uint256 values)
log := Log{
TxHash: "0xtx1155",
LogIndex: 0,
BlockNumber: 100,
Address: "0xerc1155",
Topics: []string{
TopicTransferSingle,
"0x000000000000000000000000" + operatorAddr,
"0x000000000000000000000000" + fromAddr,
"0x000000000000000000000000" + toAddr,
},
Data: "0x00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000064",
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
if transfers[0].TokenID != hexWord("5") {
t.Errorf("TokenID = %q, want %q", transfers[0].TokenID, hexWord("5"))
}
if transfers[0].Value != "100" {
t.Errorf("Value = %q, want 100", transfers[0].Value)
}
// Short data (less than 128 hex chars after stripping 0x) should produce no transfers
shortLog := log
shortLog.Data = "0x0000000000000000000000000000000000000000000000000000000000000005" // only 64 hex chars
transfers = parseTokenTransfers(shortLog, time.Now())
if len(transfers) != 0 {
t.Errorf("expected 0 transfers for short data, got %d", len(transfers))
}
}
// TestParseTokenTransfersNonStandardERC721 tests the fallback for non-standard ERC721
// that emits Transfer with only topic0 and all params in data
func TestParseTokenTransfersNonStandardERC721(t *testing.T) {
// 3 x 32 bytes in data: from, to, tokenId
fromAddr := "0000000000000000000000001111111111111111111111111111111111111111"
toAddr := "0000000000000000000000002222222222222222222222222222222222222222"
tokenID := "0000000000000000000000000000000000000000000000000000000000000007"
log := Log{
TxHash: "0xtx721ns",
LogIndex: 0,
BlockNumber: 300,
Address: "0xnonstd",
Topics: []string{TopicTransferERC20}, // Only topic0
Data: "0x" + fromAddr + toAddr + tokenID,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
tr := transfers[0]
if tr.TokenType != TypeERC721 {
t.Errorf("TokenType = %q, want ERC721", tr.TokenType)
}
if tr.Value != "1" {
t.Errorf("Value = %q, want 1", tr.Value)
}
if tr.TokenID != hexWord("7") {
t.Errorf("TokenID = %q, want %q", tr.TokenID, hexWord("7"))
}
if tr.From != "0x1111111111111111111111111111111111111111" {
t.Errorf("From = %q", tr.From)
}
if tr.To != "0x2222222222222222222222222222222222222222" {
t.Errorf("To = %q", tr.To)
}
}
// TestParseTokenTransfersERC404 tests ERC-404 hybrid token events
func TestParseTokenTransfersERC404(t *testing.T) {
fromTopic := "0x000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
toTopic := "0x000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
amountData := "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" // 1e18
tokenIDData := "0x0000000000000000000000000000000000000000000000000000000000000042" // 66
t.Run("ERC404 ERC20 side", func(t *testing.T) {
log := Log{
TxHash: "0xtx404erc20",
LogIndex: 0,
BlockNumber: 400,
Address: "0xerc404token",
Topics: []string{TopicERC404ERC20Transfer, fromTopic, toTopic},
Data: amountData,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
if transfers[0].TokenType != TypeERC20 {
t.Errorf("TokenType = %q, want ERC20", transfers[0].TokenType)
}
if transfers[0].Value != "1000000000000000000" {
t.Errorf("Value = %q", transfers[0].Value)
}
})
t.Run("ERC404 ERC721 side", func(t *testing.T) {
log := Log{
TxHash: "0xtx404erc721",
LogIndex: 1,
BlockNumber: 400,
Address: "0xerc404token",
Topics: []string{TopicERC404ERC721Transfer, fromTopic, toTopic},
Data: tokenIDData,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
if transfers[0].TokenType != TypeERC721 {
t.Errorf("TokenType = %q, want ERC721", transfers[0].TokenType)
}
if transfers[0].Value != "1" {
t.Errorf("Value = %q, want 1", transfers[0].Value)
}
if transfers[0].TokenID != hexWord("66") {
t.Errorf("TokenID = %q, want %q", transfers[0].TokenID, hexWord("66"))
}
})
}
// TestParseTokenTransfersWETH tests WETH Deposit and Withdrawal events
func TestParseTokenTransfersWETH(t *testing.T) {
dstTopic := "0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccc"
amountData := "0x0000000000000000000000000000000000000000000000001bc16d674ec80000" // 2e18
t.Run("WETH Deposit", func(t *testing.T) {
log := Log{
TxHash: "0xtxdeposit",
LogIndex: 0,
BlockNumber: 500,
Address: "0xweth",
Topics: []string{TopicWETHDeposit, dstTopic},
Data: amountData,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
tr := transfers[0]
if tr.TokenType != TypeERC20 {
t.Errorf("TokenType = %q, want ERC20", tr.TokenType)
}
if tr.From != "0x0000000000000000000000000000000000000000" {
t.Errorf("From = %q, want zero address", tr.From)
}
if tr.To != "0xcccccccccccccccccccccccccccccccccccccccc" {
t.Errorf("To = %q", tr.To)
}
if tr.Value != "2000000000000000000" {
t.Errorf("Value = %q, want 2000000000000000000", tr.Value)
}
})
t.Run("WETH Withdrawal", func(t *testing.T) {
log := Log{
TxHash: "0xtxwithdraw",
LogIndex: 1,
BlockNumber: 500,
Address: "0xweth",
Topics: []string{TopicWETHWithdrawal, dstTopic},
Data: amountData,
}
transfers := parseTokenTransfers(log, time.Now())
if len(transfers) != 1 {
t.Fatalf("expected 1 transfer, got %d", len(transfers))
}
tr := transfers[0]
if tr.From != "0xcccccccccccccccccccccccccccccccccccccccc" {
t.Errorf("From = %q", tr.From)
}
if tr.To != "0x0000000000000000000000000000000000000000" {
t.Errorf("To = %q, want zero address", tr.To)
}
})
}
// TestDecodeBatchData tests the ABI decoder for TransferBatch data
func TestDecodeBatchData(t *testing.T) {
t.Run("valid 2 items", func(t *testing.T) {
data := "0x" +
"0000000000000000000000000000000000000000000000000000000000000040" + // ids offset=64
"00000000000000000000000000000000000000000000000000000000000000a0" + // values offset=160
"0000000000000000000000000000000000000000000000000000000000000002" + // ids.length=2
"000000000000000000000000000000000000000000000000000000000000000a" + // ids[0]=10
"0000000000000000000000000000000000000000000000000000000000000014" + // ids[1]=20
"0000000000000000000000000000000000000000000000000000000000000002" + // values.length=2
"0000000000000000000000000000000000000000000000000000000000000001" + // values[0]=1
"0000000000000000000000000000000000000000000000000000000000000002" // values[1]=2
ids, values := decodeBatchData(data)
if len(ids) != 2 || len(values) != 2 {
t.Fatalf("ids=%d values=%d, want 2 each", len(ids), len(values))
}
if ids[0] != "10" || ids[1] != "20" {
t.Errorf("ids = %v, want [10 20]", ids)
}
if values[0] != "1" || values[1] != "2" {
t.Errorf("values = %v, want [1 2]", values)
}
})
t.Run("too short", func(t *testing.T) {
ids, values := decodeBatchData("0xdeadbeef")
if ids != nil || values != nil {
t.Error("expected nil for short data")
}
})
t.Run("empty arrays", func(t *testing.T) {
data := "0x" +
"0000000000000000000000000000000000000000000000000000000000000040" +
"0000000000000000000000000000000000000000000000000000000000000060" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000"
ids, values := decodeBatchData(data)
if len(ids) != 0 {
t.Errorf("expected 0 ids, got %d", len(ids))
}
if len(values) != 0 {
t.Errorf("expected 0 values, got %d", len(values))
}
})
}
// TestNewEventSignatureConstants tests the new event topic constants
func TestNewEventSignatureConstants(t *testing.T) {
topics := []struct {
name string
topic string
}{
{"TopicERC404ERC20Transfer", TopicERC404ERC20Transfer},
{"TopicERC404ERC721Transfer", TopicERC404ERC721Transfer},
{"TopicWETHDeposit", TopicWETHDeposit},
{"TopicWETHWithdrawal", TopicWETHWithdrawal},
}
for _, tt := range topics {
t.Run(tt.name, func(t *testing.T) {
if len(tt.topic) != 66 {
t.Errorf("%s length = %d, want 66", tt.name, len(tt.topic))
}
if tt.topic[:2] != "0x" {
t.Errorf("%s should start with 0x", tt.name)
}
})
}
}
// ============================================================================
// Phase 1 Benchmarks
// ============================================================================
func BenchmarkFlattenCallFrame(b *testing.B) {
timestamp := time.Now()
txHash := "0xtx123"
blockNumber := uint64(100)
// Create a moderately complex call tree
frame := &CallFrame{
Type: "CALL",
From: "0xa",
To: "0xb",
Gas: "0x100000",
GasUsed: "0x80000",
Calls: []*CallFrame{
{
Type: "CALL",
From: "0xb",
To: "0xc",
Gas: "0x50000",
GasUsed: "0x30000",
Calls: []*CallFrame{
{Type: "STATICCALL", From: "0xc", To: "0xd", Gas: "0x10000", GasUsed: "0x5000"},
{Type: "STATICCALL", From: "0xc", To: "0xe", Gas: "0x10000", GasUsed: "0x5000"},
},
},
{
Type: "DELEGATECALL",
From: "0xb",
To: "0xf",
Gas: "0x30000",
GasUsed: "0x20000",
},
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var traces []InternalTransaction
flattenCallFrame(frame, txHash, blockNumber, timestamp, []int{}, &traces, 0)
}
}
func BenchmarkNormalizeCallType(b *testing.B) {
types := []string{"CALL", "DELEGATECALL", "STATICCALL", "CREATE", "CREATE2", "SELFDESTRUCT"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, t := range types {
normalizeCallType(t)
}
}
}
func BenchmarkIntSliceToPostgresArray(b *testing.B) {
arrays := [][]int{
{},
{0},
{0, 1, 2},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, arr := range arrays {
intSliceToPostgresArray(arr)
}
}
}
func BenchmarkPostgresArrayToIntSlice(b *testing.B) {
arrays := []string{"{}", "{0}", "{0,1,2}", "{0,1,2,3,4,5,6,7,8,9}"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, arr := range arrays {
postgresArrayToIntSlice(arr)
}
}
}
func BenchmarkSortInternalTransactionsByTraceAddress(b *testing.B) {
// Create a shuffled slice of internal transactions
itxs := make([]InternalTransaction, 100)
for i := range itxs {
depth := i % 5
addr := make([]int, depth)
for j := 0; j < depth; j++ {
addr[j] = i % (j + 1)
}
itxs[i] = InternalTransaction{ID: fmt.Sprintf("%d", i), TraceAddress: addr}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Make a copy to sort
cpy := make([]InternalTransaction, len(itxs))
copy(cpy, itxs)
SortInternalTransactionsByTraceAddress(cpy)
}
}
func intPtr(i int) *int { return &i }
// TestNoHashIndex pins the ingest path against a node that serves blocks and
// receipts but cannot resolve a transaction hash.
//
// A node restored from an RLP import is exactly that: the hash-to-block index
// is built while transactions are accepted, not while blocks are replayed, so
// eth_getTransactionByHash and eth_getTransactionReceipt answer null for
// history the node plainly holds. Reading by block number gets it anyway.
func TestNoHashIndex(t *testing.T) {
const (
txHash = "0xaa"
blockHash = "0xbb"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
Method string `json:"method"`
}
json.NewDecoder(r.Body).Decode(&req)
var result interface{}
switch req.Method {
case "eth_getBlockByNumber":
result = map[string]interface{}{
"hash": blockHash, "parentHash": "0xcc", "number": "0x2a",
"timestamp": "0x64", "gasUsed": "0x5208", "gasLimit": "0x1c9c380",
"transactions": []map[string]interface{}{{
"hash": txHash, "blockHash": blockHash, "blockNumber": "0x2a",
"from": "0xF00D", "to": "0xBEEF", "value": "0x1",
"gas": "0x5208", "gasPrice": "0x7", "nonce": "0x3",
"input": "0xabcd", "transactionIndex": "0x0", "type": "0x2",
}},
}
case "eth_getBlockReceipts":
result = []map[string]interface{}{{
"transactionHash": txHash, "blockHash": blockHash,
"blockNumber": "0x2a", "from": "0xF00D", "to": "0xBEEF",
"gasUsed": "0x5208", "status": "0x1", "contractAddress": "",
"transactionIndex": "0x0",
"logs": []map[string]interface{}{{
"address": "0xtoken", "topics": []string{TopicTransferERC20},
"data": "0xdata", "logIndex": "0x0", "removed": false,
}},
}}
default:
// The hash-keyed calls are the ones with no index behind them.
result = nil
}
json.NewEncoder(w).Encode(map[string]interface{}{
"jsonrpc": "2.0", "id": 1, "result": result,
})
}))
defer server.Close()
adapter := New(server.URL)
ctx := context.Background()
block, err := adapter.GetBlockByNumber(ctx, 42)
if err != nil {
t.Fatalf("GetBlockByNumber: %v", err)
}
if len(block.Transactions) != 1 {
t.Fatalf("block carries %d transactions, want 1", len(block.Transactions))
}
tx := block.Transactions[0]
if tx.Hash != txHash {
t.Errorf("Hash = %q, want %q", tx.Hash, txHash)
}
// The submitted fields come from the block, not from a hash lookup.
if tx.From != "0xf00d" || tx.To != "0xbeef" {
t.Errorf("From/To = %q/%q, want 0xf00d/0xbeef", tx.From, tx.To)
}
if tx.Input != "0xabcd" || tx.Value != "0x1" || tx.Nonce != 3 || tx.Gas != 21000 {
t.Errorf("submitted fields lost: input=%q value=%q nonce=%d gas=%d",
tx.Input, tx.Value, tx.Nonce, tx.Gas)
}
receipts, err := adapter.BlockReceipts(ctx, 42)
if err != nil {
t.Fatalf("BlockReceipts: %v", err)
}
if len(receipts) != 1 {
t.Fatalf("got %d receipts, want 1", len(receipts))
}
r := receipts[0]
if r.TxHash != txHash || r.GasUsed != 21000 {
t.Errorf("receipt = %+v", r)
}
if r.Status == nil || *r.Status != 1 {
t.Errorf("Status = %v, want 1", r.Status)
}
if len(r.Logs) != 1 || r.Logs[0].TxHash != txHash {
t.Errorf("logs = %+v", r.Logs)
}
// A hash the node cannot resolve is a miss, not a blank transaction. An
// empty one would be written as a row keyed by the empty hash, and every
// later miss would merge into it.
miss, missLogs, err := adapter.GetTransactionReceipt(ctx, txHash)
if err != nil {
t.Fatalf("GetTransactionReceipt: %v", err)
}
if miss != nil {
t.Errorf("unresolvable hash returned %+v, want nil", miss)
}
if missLogs != nil {
t.Errorf("unresolvable hash returned logs %+v, want nil", missLogs)
}
}