Onchain
Pure Elixir Ethereum library. Provides read (eth_call) and write (transaction signing) capabilities using cartouche as the sole Ethereum dependency. No native deps, no Rustler.
Package Family
| Package | Purpose | Deps |
|---|---|---|
| onchain (this) | Core Ethereum primitives, RPC, ABI, signing | cartouche |
| onchain_aave | Aave V3 protocol wrappers | onchain |
| onchain_evm | Rust NIFs: revm simulation, Solidity parsing, codegen | onchain + rustler |
| onchain_js | JS bridge: npm packages on the BEAM via QuickBEAM | onchain + quickbeam |
| onchain_tempo | Tempo chain primitives: 0x76 transactions, TIP-20 encoding | onchain |
Pick what you need — consumers who only need eth_call never compile Rust or Zig.
Installation
def deps do
[
{:onchain, "~> 0.5"},
# Add if you need Aave:
{:onchain_aave, "~> 0.1"},
# Add if you need EVM simulation / Solidity parsing:
{:onchain_evm, "~> 0.1"},
# Add if you need JS bridge (solc-js, Uniswap SDK, etc.):
{:onchain_js, "~> 0.1"},
# Add if you need Tempo chain (0x76 transactions, TIP-20 tokens):
{:onchain_tempo, "~> 0.1"}
]
endRequires an Ethereum JSON-RPC endpoint. Configure via:
# config/config.exs
config :cartouche, :ethereum_node, "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
Or pass the URL per-call to Onchain.RPC functions.
Quick Start
# Read an ERC-20 token balance (USDC on mainnet)
usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
{:ok, balance} = Onchain.ERC20.balance_of(usdc, "0xYourAddress")
# Resolve an ENS name
{:ok, address} = Onchain.ENS.resolve("vitalik.eth")
# Generic contract call (encode -> eth_call -> decode)
{:ok, [name]} = Onchain.Contract.call(usdc, "name()", [], "(string)")
# All functions have bang variants that raise on error
balance = Onchain.ERC20.balance_of!(usdc, "0xYourAddress")Modules
Core
| Module | Purpose |
|---|---|
Onchain.Hex | Hex encoding/decoding (hex<->binary, hex<->integer, 0x prefix) |
Onchain.ABI |
ABI encoding/decoding for contract calls (encode_call/2, decode_response/2, decode_types/2) |
Onchain.Address | Address validation, EIP-55 checksum, normalization |
Onchain.Decimal | Decimal precision helpers (to_decimal, div_pow10, to_basis_points) |
Onchain.RPC |
Ethereum JSON-RPC wrapper (eth_call, eth_getLogs, receipts, nonces, balances, syncing; call/3 for any other method). eth_get_logs/2 accepts atom keys or canonical camelCase string aliases ("fromBlock", "toBlock", "blockHash", "address", "topics"); :block_hash is mutually exclusive with :from_block/:to_block per EIP-1474 |
Onchain.RPC.Helpers | Shared RPC helper functions (hex normalization, block tags, tx hash validation) |
Onchain.Block | Block fetching with parsed fields, timestamp-based binary search |
Onchain.Contract | Generic contract call (encode -> eth_call -> decode in one function) |
Onchain.Multicall | Batch multiple eth_call via Multicall3 |
Onchain.Sleuth | Deploy-as-call: ship creation bytecode in one eth_call, decode returned bytes |
Onchain.Log | Event log parsing against ABI signatures |
Onchain.Signer | Key management and transaction signing |
Onchain.ERC20 | ERC-20 read (balanceOf, allowance, decimals, symbol, totalSupply) and write (transfer, approve) |
Onchain.ERC721 | ERC-721 NFT reads (owner_of, token_uri, balance_of, name, symbol, get_approved, approved_for_all?) |
Onchain.ERC1155 | ERC-1155 multi-token reads (balance_of, balance_of_batch, uri, approved_for_all?) |
Chain Intelligence
| Module | Purpose |
|---|---|
Onchain.Wallet | Classify address (EOA/contract), native ETH balance |
Onchain.Transfer | Parse ERC-20/721/1155 Transfer events into normalized structs |
Onchain.ENS | ENS name resolution (forward, reverse, text records, contenthash) |
Onchain.Subscription | Real-time streaming via eth_subscribe (newHeads, pendingTx, logs) |
Onchain.Subscription.Parser | Pure parsing for eth_subscribe notification payloads (newHeads, pendingTx, logs) |
All public functions have function!/1 bang variants that raise on error instead of returning {:error, reason} tuples.
Real-time Subscriptions
Stream new blocks, pending transactions, and event logs via WebSocket:
# Connect to a WebSocket endpoint
{:ok, sub} = Onchain.Subscription.connect("wss://eth-mainnet.g.alchemy.com/v2/KEY")
# Subscribe to new block headers
{:ok, sub_id} = Onchain.Subscription.subscribe(sub, :new_heads)
# Events arrive as messages to the calling process
receive do
{:subscription, {:new_heads, ^sub_id, head}} ->
IO.inspect(head.number, label: "new block")
end
# Or provide a custom handler
{:ok, sub} = Onchain.Subscription.connect("wss://...",
handler: fn {:new_heads, _id, head} -> Logger.info("Block #{head.number}") end
)
# Subscribe to filtered event logs
{:ok, _} = Onchain.Subscription.subscribe(sub, {:logs, %{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
topics: [Onchain.Transfer.transfer_topics()]
}})
# Clean up
Onchain.Subscription.unsubscribe(sub, sub_id)
Onchain.Subscription.close(sub)
Requires a WebSocket-capable endpoint (wss:// or ws://), separate from the HTTP RPC URL.
Discovery
All modules use descripex for self-describing APIs:
Onchain.describe() # Module overview
Onchain.describe(:hex) # Function list
Onchain.describe(:hex, :decode) # Full function detailsTesting
mix test.json --quiet # Unit tests (no RPC needed)
mix test.json --quiet --include integration # Integration tests (requires RPC)Integration tests require an Ethereum RPC endpoint:
export ETHEREUM_API_URL="https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"WebSocket subscription tests require a WebSocket endpoint:
export ETHEREUM_WS_URL="wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
Sepolia write tests additionally require ETH_SEPOLIA_RPC_URL and SIGNER_PRIVATE_KEY.