Files
zeekayandHanzo Dev 984494b3a8 Take the sqlite C under the name nothing else claims
The wrapper this reached for registers the driver as "sqlite", and so does
modernc.org/sqlite, which arrives here with hanzoai/replicate. Two packages
under one name is a panic in init, before main runs.

csqlite is the same house C under "sqlite3" — the name every sql.Open here
already names, and one nothing else in this graph takes.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 15:12:22 -07:00

877 lines
26 KiB
Go

// Package testutil provides test data factories for explorer tests.
// Go test data factory for explorer tests.
package testutil
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"math/big"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
// ONE sqlite C library in this binary, under a name nothing else claims.
// csqlite is the house build of the amalgamation and registers "sqlite3",
// the name every sql.Open here already names. Upstream mattn compiles a
// second copy of the same C, so linking both defines every sqlite3_*
// symbol twice; hanzoai/sqlite wraps this same C but registers "sqlite",
// which modernc.org/sqlite also registers — and modernc arrives with
// hanzoai/replicate. Two packages, one name, and database/sql panics in
// init before main runs.
_ "github.com/hanzoai/csqlite"
)
// DB wraps a test SQLite database with the indexer schema.
type DB struct {
*sql.DB
Path string
}
var seq uint64
func nextSeq() uint64 { return atomic.AddUint64(&seq, 1) }
// NewTestDB creates a temporary SQLite database with the indexer schema.
func NewTestDB(t *testing.T) *DB {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "indexer.db")
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_journal_mode=WAL&_synchronous=NORMAL", path))
if err != nil {
t.Fatalf("open test db: %v", err)
}
t.Cleanup(func() { db.Close() })
if err := createSchema(db); err != nil {
t.Fatalf("create schema: %v", err)
}
return &DB{DB: db, Path: path}
}
func createSchema(db *sql.DB) error {
schema := `
CREATE TABLE IF NOT EXISTS blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
number INTEGER NOT NULL,
hash BLOB NOT NULL,
parent_hash BLOB NOT NULL,
nonce BLOB,
miner BLOB NOT NULL,
difficulty TEXT,
total_difficulty TEXT,
size INTEGER NOT NULL DEFAULT 0,
gas_limit INTEGER NOT NULL DEFAULT 0,
gas_used INTEGER NOT NULL DEFAULT 0,
base_fee TEXT,
timestamp INTEGER NOT NULL,
transaction_count INTEGER NOT NULL DEFAULT 0,
ext_data_hash BLOB,
ext_data_gas_used INTEGER,
block_gas_cost TEXT,
extra_data BLOB,
logs_bloom BLOB,
state_root BLOB,
transactions_root BLOB,
receipts_root BLOB,
indexed_at TEXT DEFAULT (datetime('now')),
consensus_state TEXT DEFAULT 'finalized',
is_empty INTEGER DEFAULT 0,
UNIQUE(chain_id, number)
);
CREATE INDEX IF NOT EXISTS idx_blocks_hash ON blocks(hash);
CREATE INDEX IF NOT EXISTS idx_blocks_timestamp ON blocks(timestamp);
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
hash BLOB NOT NULL,
block_number INTEGER NOT NULL,
block_hash BLOB NOT NULL,
transaction_index INTEGER NOT NULL,
from_address_hash BLOB NOT NULL,
to_address_hash BLOB,
value TEXT NOT NULL DEFAULT '0',
gas INTEGER NOT NULL DEFAULT 0,
gas_price TEXT,
gas_used INTEGER,
max_fee_per_gas TEXT,
max_priority_fee_per_gas TEXT,
max_fee_per_blob_gas TEXT,
blob_gas_used INTEGER,
blob_gas_price TEXT,
input BLOB,
nonce INTEGER NOT NULL DEFAULT 0,
type INTEGER NOT NULL DEFAULT 0,
status INTEGER,
error TEXT,
revert_reason TEXT,
created_contract_address_hash BLOB,
created_contract_code_indexed_at TEXT,
cumulative_gas_used INTEGER,
block_timestamp INTEGER NOT NULL,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(chain_id, hash)
);
CREATE INDEX IF NOT EXISTS idx_txs_block ON transactions(chain_id, block_number);
CREATE INDEX IF NOT EXISTS idx_txs_from ON transactions(from_address_hash);
CREATE INDEX IF NOT EXISTS idx_txs_to ON transactions(to_address_hash);
CREATE TABLE IF NOT EXISTS internal_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
transaction_hash BLOB NOT NULL,
block_number INTEGER NOT NULL,
block_hash BLOB NOT NULL,
"index" INTEGER NOT NULL,
trace_address TEXT NOT NULL DEFAULT '{}',
type TEXT NOT NULL,
call_type TEXT,
from_address_hash BLOB NOT NULL,
to_address_hash BLOB,
created_contract_address_hash BLOB,
created_contract_code BLOB,
value TEXT NOT NULL DEFAULT '0',
gas INTEGER,
gas_used INTEGER,
input BLOB,
output BLOB,
init BLOB,
error TEXT,
block_timestamp INTEGER NOT NULL,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_itxs_tx ON internal_transactions(transaction_hash);
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
block_number INTEGER NOT NULL,
block_hash BLOB NOT NULL,
transaction_hash BLOB NOT NULL,
transaction_index INTEGER NOT NULL,
"index" INTEGER NOT NULL,
address_hash BLOB NOT NULL,
data BLOB,
first_topic BLOB,
second_topic BLOB,
third_topic BLOB,
fourth_topic BLOB,
type TEXT,
block_timestamp INTEGER NOT NULL,
inserted_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_logs_tx ON logs(transaction_hash);
CREATE INDEX IF NOT EXISTS idx_logs_addr ON logs(address_hash);
CREATE TABLE IF NOT EXISTS addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
hash BLOB NOT NULL,
fetched_coin_balance TEXT,
fetched_coin_balance_block_number INTEGER,
contract_code BLOB,
transactions_count INTEGER DEFAULT 0,
token_transfers_count INTEGER DEFAULT 0,
gas_used TEXT DEFAULT '0',
nonce INTEGER,
decompiled INTEGER DEFAULT 0,
verified INTEGER DEFAULT 0,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(chain_id, hash)
);
CREATE TABLE IF NOT EXISTS smart_contracts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
address_hash BLOB NOT NULL,
name TEXT NOT NULL,
compiler_version TEXT NOT NULL,
optimization INTEGER DEFAULT 0,
optimization_runs INTEGER,
contract_source_code TEXT,
abi TEXT,
constructor_arguments TEXT,
evm_version TEXT,
file_path TEXT,
external_libraries TEXT DEFAULT '[]',
secondary_sources TEXT DEFAULT '[]',
verified_via TEXT,
partially_verified INTEGER DEFAULT 0,
is_vyper_contract INTEGER DEFAULT 0,
is_changed_bytecode INTEGER DEFAULT 0,
license_type TEXT,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(chain_id, address_hash)
);
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
contract_address_hash BLOB NOT NULL,
name TEXT,
symbol TEXT,
total_supply TEXT,
decimals INTEGER,
type TEXT NOT NULL,
holder_count INTEGER DEFAULT 0,
fiat_value TEXT,
circulating_market_cap TEXT,
total_supply_updated_at_block INTEGER,
cataloged INTEGER DEFAULT 0,
skip_metadata INTEGER DEFAULT 0,
icon_url TEXT,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(chain_id, contract_address_hash)
);
CREATE TABLE IF NOT EXISTS token_transfers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
transaction_hash BLOB NOT NULL,
log_index INTEGER NOT NULL,
block_number INTEGER NOT NULL,
block_hash BLOB NOT NULL,
from_address_hash BLOB NOT NULL,
to_address_hash BLOB NOT NULL,
token_contract_address_hash BLOB NOT NULL,
amount TEXT,
token_id TEXT,
token_ids TEXT,
amounts TEXT,
token_type TEXT,
block_timestamp INTEGER NOT NULL,
inserted_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_tt_tx ON token_transfers(transaction_hash);
CREATE INDEX IF NOT EXISTS idx_tt_token ON token_transfers(token_contract_address_hash);
CREATE TABLE IF NOT EXISTS address_current_token_balances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
address_hash BLOB NOT NULL,
token_contract_address_hash BLOB NOT NULL,
value TEXT,
value_fetched_at TEXT,
block_number INTEGER NOT NULL,
token_id TEXT,
token_type TEXT,
old_value TEXT,
inserted_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS address_coin_balances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id INTEGER NOT NULL,
address_hash BLOB NOT NULL,
block_number INTEGER NOT NULL,
value TEXT,
value_fetched_at TEXT,
delta TEXT,
inserted_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS dex_orders (
id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
type TEXT NOT NULL,
price INTEGER NOT NULL,
quantity INTEGER NOT NULL,
filled_qty INTEGER NOT NULL DEFAULT 0,
time_in_force TEXT,
post_only INTEGER DEFAULT 0,
reduce_only INTEGER DEFAULT 0,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dex_orders_symbol ON dex_orders(symbol);
CREATE INDEX IF NOT EXISTS idx_dex_orders_status ON dex_orders(status);
CREATE TABLE IF NOT EXISTS dex_trades (
id TEXT PRIMARY KEY,
symbol TEXT NOT NULL,
maker_order_id TEXT NOT NULL,
taker_order_id TEXT NOT NULL,
maker TEXT NOT NULL,
taker TEXT NOT NULL,
side TEXT NOT NULL,
price INTEGER NOT NULL,
quantity INTEGER NOT NULL,
volume INTEGER NOT NULL,
fee INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dex_trades_symbol ON dex_trades(symbol);
CREATE INDEX IF NOT EXISTS idx_dex_trades_timestamp ON dex_trades(timestamp);
CREATE TABLE IF NOT EXISTS dex_market_stats (
symbol TEXT PRIMARY KEY,
last_price INTEGER DEFAULT 0,
high_24h INTEGER DEFAULT 0,
low_24h INTEGER DEFAULT 0,
volume_24h INTEGER DEFAULT 0,
trade_count_24h INTEGER DEFAULT 0,
open_interest INTEGER DEFAULT 0,
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS dex_pools (
id TEXT PRIMARY KEY,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
reserve0 TEXT NOT NULL,
reserve1 TEXT NOT NULL,
lp_supply TEXT NOT NULL,
fee INTEGER DEFAULT 30,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS dex_swaps (
id TEXT PRIMARY KEY,
pool_id TEXT NOT NULL,
sender TEXT NOT NULL,
token_in TEXT NOT NULL,
token_out TEXT NOT NULL,
amount_in TEXT NOT NULL,
amount_out TEXT NOT NULL,
fee TEXT NOT NULL,
timestamp TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dex_swaps_pool ON dex_swaps(pool_id);
CREATE INDEX IF NOT EXISTS idx_dex_swaps_timestamp ON dex_swaps(timestamp);
`
_, err := db.Exec(schema)
return err
}
// ---- Random Data Generators ----
func RandomHash() []byte {
b := make([]byte, 32)
rand.Read(b)
return b
}
func RandomAddress() []byte {
b := make([]byte, 20)
rand.Read(b)
return b
}
func RandomHexHash() string {
return "0x" + hex.EncodeToString(RandomHash())
}
func RandomHexAddress() string {
return "0x" + hex.EncodeToString(RandomAddress())
}
// ---- Factory Types ----
// Block mirrors the indexer's block row.
type Block struct {
ChainID int64
Number int64
Hash []byte
ParentHash []byte
Nonce []byte
Miner []byte
Difficulty string
TotalDifficulty string
Size int
GasLimit int64
GasUsed int64
BaseFee string
Timestamp int64
TransactionCount int
ConsensusState string
}
// Transaction mirrors the indexer's transaction row.
type Transaction struct {
ChainID int64
Hash []byte
BlockNumber int64
BlockHash []byte
TransactionIndex int
FromAddress []byte
ToAddress []byte
Value string
Gas int64
GasPrice string
GasUsed int64
MaxFeePerGas string
MaxPriorityFeePerGas string
Input []byte
Nonce int64
Type int
Status *int // nil = pending
Error string
RevertReason string
CreatedContract []byte
CumulativeGasUsed int64
BlockTimestamp int64
}
// Address mirrors the indexer's address row.
type Address struct {
ChainID int64
Hash []byte
FetchedCoinBalance string
FetchedBalanceBlock int64
ContractCode []byte
TransactionsCount int
TokenTransfersCount int
GasUsed string
Nonce int64
Verified bool
}
// Token mirrors the indexer's token row.
type Token struct {
ChainID int64
ContractAddress []byte
Name string
Symbol string
TotalSupply string
Decimals int
Type string
HolderCount int
IconURL string
}
// TokenTransfer mirrors the indexer's token_transfer row.
type TokenTransfer struct {
ChainID int64
TransactionHash []byte
LogIndex int
BlockNumber int64
BlockHash []byte
FromAddress []byte
ToAddress []byte
TokenContractAddress []byte
Amount string
TokenID string
TokenType string
BlockTimestamp int64
}
// Log mirrors the indexer's log row.
type Log struct {
ChainID int64
BlockNumber int64
BlockHash []byte
TransactionHash []byte
TransactionIndex int
Index int
Address []byte
Data []byte
FirstTopic []byte
SecondTopic []byte
ThirdTopic []byte
FourthTopic []byte
BlockTimestamp int64
}
// InternalTransaction mirrors the indexer's internal_transaction row.
type InternalTx struct {
ChainID int64
TransactionHash []byte
BlockNumber int64
BlockHash []byte
Index int
TraceAddress string
Type string
CallType string
FromAddress []byte
ToAddress []byte
Value string
Gas int64
GasUsed int64
Input []byte
Output []byte
Error string
BlockTimestamp int64
}
// SmartContract mirrors the indexer's smart_contract row.
type SmartContract struct {
ChainID int64
AddressHash []byte
Name string
CompilerVersion string
Optimization bool
OptRuns int
SourceCode string
ABI string
EVMVersion string
VerifiedVia string
IsVyper bool
LicenseType string
}
// ---- Default Factory Functions ----
const DefaultChainID int64 = 96369
func DefaultBlock() Block {
n := int64(nextSeq())
ts := time.Now().Unix() - (1000 - n)
return Block{
ChainID: DefaultChainID,
Number: n,
Hash: RandomHash(),
ParentHash: RandomHash(),
Nonce: []byte{0, 0, 0, 0, 0, 0, 0, byte(n)},
Miner: RandomAddress(),
Difficulty: fmt.Sprintf("%d", 100+n),
TotalDifficulty: fmt.Sprintf("%d", 100000+n),
Size: 1024 + int(n*100),
GasLimit: 8000000,
GasUsed: 21000 * n,
BaseFee: "25000000000",
Timestamp: ts,
TransactionCount: 0,
ConsensusState: "finalized",
}
}
func DefaultTransaction(block Block) Transaction {
n := int64(nextSeq())
status := 1
return Transaction{
ChainID: block.ChainID,
Hash: RandomHash(),
BlockNumber: block.Number,
BlockHash: block.Hash,
TransactionIndex: int(n % 100),
FromAddress: RandomAddress(),
ToAddress: RandomAddress(),
Value: new(big.Int).Mul(big.NewInt(n), big.NewInt(1e15)).String(),
Gas: 21000,
GasPrice: "25000000000",
GasUsed: 21000,
Nonce: n,
Type: 0,
Status: &status,
BlockTimestamp: block.Timestamp,
}
}
func DefaultEIP1559Transaction(block Block) Transaction {
tx := DefaultTransaction(block)
tx.Type = 2
tx.MaxFeePerGas = "50000000000"
tx.MaxPriorityFeePerGas = "2000000000"
tx.GasPrice = ""
return tx
}
func DefaultPendingTransaction(block Block) Transaction {
tx := DefaultTransaction(block)
tx.Status = nil
return tx
}
func DefaultFailedTransaction(block Block) Transaction {
tx := DefaultTransaction(block)
status := 0
tx.Status = &status
tx.Error = "execution reverted"
tx.RevertReason = "ERC20: transfer amount exceeds balance"
return tx
}
func DefaultContractCreation(block Block) Transaction {
tx := DefaultTransaction(block)
tx.ToAddress = nil
tx.CreatedContract = RandomAddress()
tx.Input = []byte{0x60, 0x80, 0x60, 0x40} // contract bytecode prefix
return tx
}
func DefaultAddress() Address {
balance := new(big.Int).Mul(big.NewInt(int64(nextSeq())), big.NewInt(1e18))
return Address{
ChainID: DefaultChainID,
Hash: RandomAddress(),
FetchedCoinBalance: balance.String(),
FetchedBalanceBlock: int64(nextSeq()),
TransactionsCount: int(nextSeq() % 100),
TokenTransfersCount: int(nextSeq() % 50),
GasUsed: "0",
Nonce: int64(nextSeq() % 100),
}
}
func DefaultContractAddress() Address {
addr := DefaultAddress()
addr.ContractCode = []byte{0x60, 0x80, 0x60, 0x40, 0x52}
return addr
}
func DefaultToken() Token {
n := nextSeq()
return Token{
ChainID: DefaultChainID,
ContractAddress: RandomAddress(),
Name: fmt.Sprintf("Token %d", n),
Symbol: fmt.Sprintf("TKN%d", n),
TotalSupply: new(big.Int).Mul(big.NewInt(1e6), big.NewInt(1e18)).String(),
Decimals: 18,
Type: "ERC-20",
HolderCount: int(n % 1000),
}
}
func DefaultERC721Token() Token {
t := DefaultToken()
t.Type = "ERC-721"
t.Decimals = 0
t.TotalSupply = fmt.Sprintf("%d", nextSeq()%10000)
return t
}
func DefaultTokenTransfer(tx Transaction, token Token) TokenTransfer {
return TokenTransfer{
ChainID: tx.ChainID,
TransactionHash: tx.Hash,
LogIndex: int(nextSeq() % 100),
BlockNumber: tx.BlockNumber,
BlockHash: tx.BlockHash,
FromAddress: tx.FromAddress,
ToAddress: tx.ToAddress,
TokenContractAddress: token.ContractAddress,
Amount: new(big.Int).Mul(big.NewInt(100), big.NewInt(1e18)).String(),
TokenType: token.Type,
BlockTimestamp: tx.BlockTimestamp,
}
}
func DefaultLog(tx Transaction) Log {
// Transfer(address,address,uint256) topic
topic0, _ := hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
return Log{
ChainID: tx.ChainID,
BlockNumber: tx.BlockNumber,
BlockHash: tx.BlockHash,
TransactionHash: tx.Hash,
TransactionIndex: tx.TransactionIndex,
Index: int(nextSeq() % 100),
Address: RandomAddress(),
Data: RandomHash(),
FirstTopic: topic0,
SecondTopic: padAddress(tx.FromAddress),
ThirdTopic: padAddress(tx.ToAddress),
BlockTimestamp: tx.BlockTimestamp,
}
}
func DefaultInternalTx(tx Transaction) InternalTx {
return InternalTx{
ChainID: tx.ChainID,
TransactionHash: tx.Hash,
BlockNumber: tx.BlockNumber,
BlockHash: tx.BlockHash,
Index: int(nextSeq()),
TraceAddress: "{}",
Type: "call",
CallType: "call",
FromAddress: tx.FromAddress,
ToAddress: RandomAddress(),
Value: "1000000000000000",
Gas: 21000,
GasUsed: 15000,
Input: []byte{},
Output: []byte{},
BlockTimestamp: tx.BlockTimestamp,
}
}
func DefaultSmartContract() SmartContract {
return SmartContract{
ChainID: DefaultChainID,
AddressHash: RandomAddress(),
Name: "SimpleStorage",
CompilerVersion: "v0.8.10+commit.fc410830",
Optimization: false,
OptRuns: 200,
SourceCode: "pragma solidity ^0.8.10;\ncontract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }",
ABI: `[{"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]`,
EVMVersion: "london",
VerifiedVia: "manual",
LicenseType: "MIT",
}
}
// ---- Insert Functions ----
func (db *DB) InsertBlock(t *testing.T, b Block) Block {
t.Helper()
_, err := db.Exec(`INSERT INTO blocks (chain_id, number, hash, parent_hash, nonce, miner, difficulty, total_difficulty, size, gas_limit, gas_used, base_fee, timestamp, transaction_count, consensus_state) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
b.ChainID, b.Number, b.Hash, b.ParentHash, b.Nonce, b.Miner, b.Difficulty, b.TotalDifficulty, b.Size, b.GasLimit, b.GasUsed, b.BaseFee, b.Timestamp, b.TransactionCount, b.ConsensusState)
if err != nil {
t.Fatalf("insert block: %v", err)
}
return b
}
func (db *DB) InsertTransaction(t *testing.T, tx Transaction) Transaction {
t.Helper()
_, err := db.Exec(`INSERT INTO transactions (chain_id, hash, block_number, block_hash, transaction_index, from_address_hash, to_address_hash, value, gas, gas_price, gas_used, max_fee_per_gas, max_priority_fee_per_gas, input, nonce, type, status, error, revert_reason, created_contract_address_hash, cumulative_gas_used, block_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
tx.ChainID, tx.Hash, tx.BlockNumber, tx.BlockHash, tx.TransactionIndex, tx.FromAddress, tx.ToAddress, tx.Value, tx.Gas, tx.GasPrice, tx.GasUsed, tx.MaxFeePerGas, tx.MaxPriorityFeePerGas, tx.Input, tx.Nonce, tx.Type, tx.Status, tx.Error, tx.RevertReason, tx.CreatedContract, tx.CumulativeGasUsed, tx.BlockTimestamp)
if err != nil {
t.Fatalf("insert transaction: %v", err)
}
return tx
}
func (db *DB) InsertAddress(t *testing.T, a Address) Address {
t.Helper()
_, err := db.Exec(`INSERT INTO addresses (chain_id, hash, fetched_coin_balance, fetched_coin_balance_block_number, contract_code, transactions_count, token_transfers_count, gas_used, nonce, verified) VALUES (?,?,?,?,?,?,?,?,?,?)`,
a.ChainID, a.Hash, a.FetchedCoinBalance, a.FetchedBalanceBlock, a.ContractCode, a.TransactionsCount, a.TokenTransfersCount, a.GasUsed, a.Nonce, a.Verified)
if err != nil {
t.Fatalf("insert address: %v", err)
}
return a
}
func (db *DB) InsertToken(t *testing.T, tk Token) Token {
t.Helper()
_, err := db.Exec(`INSERT INTO tokens (chain_id, contract_address_hash, name, symbol, total_supply, decimals, type, holder_count, icon_url) VALUES (?,?,?,?,?,?,?,?,?)`,
tk.ChainID, tk.ContractAddress, tk.Name, tk.Symbol, tk.TotalSupply, tk.Decimals, tk.Type, tk.HolderCount, tk.IconURL)
if err != nil {
t.Fatalf("insert token: %v", err)
}
return tk
}
func (db *DB) InsertTokenTransfer(t *testing.T, tt TokenTransfer) TokenTransfer {
t.Helper()
_, err := db.Exec(`INSERT INTO token_transfers (chain_id, transaction_hash, log_index, block_number, block_hash, from_address_hash, to_address_hash, token_contract_address_hash, amount, token_id, token_type, block_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
tt.ChainID, tt.TransactionHash, tt.LogIndex, tt.BlockNumber, tt.BlockHash, tt.FromAddress, tt.ToAddress, tt.TokenContractAddress, tt.Amount, tt.TokenID, tt.TokenType, tt.BlockTimestamp)
if err != nil {
t.Fatalf("insert token transfer: %v", err)
}
return tt
}
func (db *DB) InsertLog(t *testing.T, l Log) Log {
t.Helper()
_, err := db.Exec(`INSERT INTO logs (chain_id, block_number, block_hash, transaction_hash, transaction_index, "index", address_hash, data, first_topic, second_topic, third_topic, fourth_topic, block_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
l.ChainID, l.BlockNumber, l.BlockHash, l.TransactionHash, l.TransactionIndex, l.Index, l.Address, l.Data, l.FirstTopic, l.SecondTopic, l.ThirdTopic, l.FourthTopic, l.BlockTimestamp)
if err != nil {
t.Fatalf("insert log: %v", err)
}
return l
}
func (db *DB) InsertInternalTx(t *testing.T, itx InternalTx) InternalTx {
t.Helper()
_, err := db.Exec(`INSERT INTO internal_transactions (chain_id, transaction_hash, block_number, block_hash, "index", trace_address, type, call_type, from_address_hash, to_address_hash, value, gas, gas_used, input, output, error, block_timestamp) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
itx.ChainID, itx.TransactionHash, itx.BlockNumber, itx.BlockHash, itx.Index, itx.TraceAddress, itx.Type, itx.CallType, itx.FromAddress, itx.ToAddress, itx.Value, itx.Gas, itx.GasUsed, itx.Input, itx.Output, itx.Error, itx.BlockTimestamp)
if err != nil {
t.Fatalf("insert internal tx: %v", err)
}
return itx
}
// TokenBalance mirrors a row in address_current_token_balances.
type TokenBalance struct {
ChainID int64
AddressHash []byte
TokenContractAddress []byte
Value string
BlockNumber int64
TokenType string
}
func (db *DB) InsertTokenBalance(t *testing.T, tb TokenBalance) TokenBalance {
t.Helper()
_, err := db.Exec(`INSERT INTO address_current_token_balances (chain_id, address_hash, token_contract_address_hash, value, block_number, token_type) VALUES (?,?,?,?,?,?)`,
tb.ChainID, tb.AddressHash, tb.TokenContractAddress, tb.Value, tb.BlockNumber, tb.TokenType)
if err != nil {
t.Fatalf("insert token balance: %v", err)
}
return tb
}
func (db *DB) InsertSmartContract(t *testing.T, sc SmartContract) SmartContract {
t.Helper()
_, err := db.Exec(`INSERT INTO smart_contracts (chain_id, address_hash, name, compiler_version, optimization, optimization_runs, contract_source_code, abi, evm_version, verified_via, is_vyper_contract, license_type) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
sc.ChainID, sc.AddressHash, sc.Name, sc.CompilerVersion, sc.Optimization, sc.OptRuns, sc.SourceCode, sc.ABI, sc.EVMVersion, sc.VerifiedVia, sc.IsVyper, sc.LicenseType)
if err != nil {
t.Fatalf("insert smart contract: %v", err)
}
return sc
}
// ---- Seed Helpers ----
// SeedChainData inserts a realistic set of chain data for testing.
// Returns the blocks, transactions, and addresses created.
func (db *DB) SeedChainData(t *testing.T, numBlocks int) ([]Block, []Transaction, []Address) {
t.Helper()
var blocks []Block
var txs []Transaction
var addrs []Address
for i := 0; i < numBlocks; i++ {
b := DefaultBlock()
b.TransactionCount = 2
b = db.InsertBlock(t, b)
blocks = append(blocks, b)
// 2 transactions per block
for j := 0; j < 2; j++ {
tx := DefaultTransaction(b)
tx.TransactionIndex = j
tx = db.InsertTransaction(t, tx)
txs = append(txs, tx)
// 1 log per transaction
l := DefaultLog(tx)
db.InsertLog(t, l)
}
}
// Create some addresses
for i := 0; i < 5; i++ {
a := DefaultAddress()
a = db.InsertAddress(t, a)
addrs = append(addrs, a)
}
return blocks, txs, addrs
}
func padAddress(addr []byte) []byte {
if len(addr) == 0 {
return nil
}
padded := make([]byte, 32)
copy(padded[12:], addr)
return padded
}
// EnvironmentDBPath returns EXPLORER_TEST_DB if set (for integration with a real indexer DB).
func EnvironmentDBPath() string {
return os.Getenv("EXPLORER_TEST_DB")
}