The star schema

The star schema for chainstar. Raw blockchain data to clean dimensional tables in your own warehouse, from one command.

Keys are readable strings that start with the chain, for example ethereum:0xdac17f958d2ee523a2206206994597c13d831ec7 or bitcoin:native, so rows from different chains never collide. Addresses and hashes are as each chain writes them: lowercase 0x hex on Ethereum and Base, Bitcoin addresses and txids as the node returns them, base58 public keys and signatures on Solana. Times are UTC.

The shared columns mean the same on every chain: chain; block_number (the Bitcoin block height, the Solana slot); block_timestamp and date_day; transaction_hash (the Bitcoin txid, the Solana transaction's first signature); and the _id keys. A column that does not apply to a chain is NULL there, as noted below.

fct_transfers

One row per value movement:

Column Notes
transfer_id chain:transaction_hash:transfer_position (on EVM chains the same as chain:transaction_hash:log_index)
chain, block_number, block_timestamp, date_day date_day joins to dim_dates
transaction_hash, transaction_index
transfer_position log index (EVM), output index (Bitcoin), instruction position (Solana: 3 is the fourth top-level instruction, 3.1 its second inner instruction)
log_index EVM only
token_id joins to dim_tokens, and on EVM chains to dim_contracts
from_wallet_id, to_wallet_id join to dim_wallets; on EVM the zero address means a mint (sender) or burn (recipient); NULL on Bitcoin where there is no single sender or no address
token_address ERC-20 contract, SPL mint, or native for BTC and SOL
from_address, to_address
input_address_count Bitcoin only: distinct addresses among the inputs
from_token_account, to_token_account Solana SPL only
script_type Bitcoin only: the output script type as the node names it (witness_v0_keyhash, nulldata, ...)
amount_raw the exact value in the asset's smallest unit (satoshis, lamports, token base units), as a whole-number string
amount amount_raw / 10^decimals as a floating-point number (about 15 significant digits); NULL when the token's decimals are unknown

amount_raw is text because ERC-20 values are 256-bit integers and SPL amounts 64-bit unsigned, larger than any numeric type DuckDB and Postgres share. Cast it yourself when you need exact arithmetic, for example amount_raw::numeric in Postgres.

Why Solana transfers come from instructions rather than from token balance changes: balance changes (preTokenBalances / postTokenBalances) net out every movement in a transaction and cannot say who paid whom. Checked against real blocks, the parsed instructions account for every token balance change except wrapped-SOL wrapping and unwrapping (syncNative, closeAccount) and the fee that Token-2022 mints with the transfer-fee extension withhold from the recipient: for those mints amount_raw is the amount sent, and the recipient receives less. Token accounts that are created and closed within one transaction appear in no balance list; their mint and owner are read from the initializeAccount instruction that created them.

fct_fees

One row per fee-paying transaction, with what its payer paid.

Column Notes
fee_id chain:transaction_hash
chain, block_number, block_timestamp, date_day
transaction_hash, transaction_index
payer_wallet_id who paid; joins to dim_wallets
from_address the payer's address
to_address EVM only; NULL for contract creation
transaction_type EVM only: 0 legacy, 1 access list, 2 EIP-1559, 3 blob, 4 set-code, 126 OP-stack deposit (Base)
success always true on Bitcoin
is_vote Solana only
gas_limit, gas_used, effective_gas_price_wei, base_fee_per_gas_wei EVM only
execution_fee_wei, execution_fee_native EVM only, kept from earlier releases; equal to fee_raw and fee_native
fee_raw the fee in the chain's smallest unit (wei, satoshis, lamports), exact
native_symbol, fee_native the fee in ETH, BTC or SOL, as a floating-point number

dim_wallets

Every address that sent or received a transfer, or paid for a transaction.

wallet_id, chain, address, is_zero_address, first_seen_block, first_seen_at, last_seen_block, last_seen_at, transfers_out, transfers_in, transactions_sent

"Wallet" here means any address. Some of them are contracts or programs; this release does not tell them apart. On Bitcoin, transfers_out counts inputs spent from the address and transactions_sent the transactions it helped fund, since an output does not always have a single sender. is_zero_address applies to EVM chains only.

dim_tokens

Every asset moved in the loaded data.

token_id, chain, token_address, token_standard, name, symbol, decimals, metadata_block_number, has_metadata

dim_contracts

Contracts chainstar can identify. In this release that is only the Ethereum and Base token contracts that emitted Transfer events, so it has one row per erc20 row of dim_tokens. Bitcoin has no contracts, and Solana programs are not extracted.

contract_id, chain, contract_address, contract_type (always erc20 for now), first_seen_block, last_seen_block, transfer_events

dim_dates

One row per UTC day from the earliest to the latest loaded block.

date_day, year, quarter, month, day_of_month, iso_day_of_week (1 = Monday), iso_week, is_weekend

Tests

chainstar run finishes with dbt build, which runs these checks and fails the run if any fail: unique and not-null keys on every table, every fact row joining to its date, token and wallet dimensions, and every ERC-20 token joining to dim_contracts. Where a column does not apply to a chain (a Bitcoin output with no single sender, for example), its not-null test skips that chain.