Quickstart¶
This page takes you from an empty project to a signed transaction on both chain families. Every snippet is a complete module — copy it, replace the placeholder addresses and keys, and run it.
1. Install¶
Python 3.13 or later is required.
uv add blockchainpype
pip install blockchainpype
2. Configure the blockchains¶
Two registries do the wiring:
BlockchainsInitializerregisters the blockchain class to use per blockchain type (EthereumBlockchainfor EVM,SolanaBlockchainfor Solana) and one configuration per platform in theBlockchainFactory.WalletsInitializerdoes the same for wallet classes (EthereumWallet,SolanaWallet) in theWalletFactory.
The stock configurations live in
blockchainpype.initializer.BlockchainConfigurations and point at public
endpoints. Subclassing it and overriding a single *_configuration
classmethod is the supported way to use your own RPCs: call super() first and
replace only the connectivity section, so platform identity, native asset and
explorer settings keep the library defaults.
"""Register the blockchains and wallets this application talks to."""
from solana.rpc.async_api import AsyncClient
from blockchainpype.evm.blockchain.configuration import (
EthereumBlockchainConfiguration,
EthereumConnectivityConfiguration,
)
from blockchainpype.evm.blockchain.providers import (
LimitedHTTPProvider,
MultipleHTTPProvider,
)
from blockchainpype.factory import BlockchainFactory
from blockchainpype.initializer import (
BlockchainConfigurations,
BlockchainsInitializer,
WalletsInitializer,
)
from blockchainpype.solana.blockchain.configuration import (
SolanaBlockchainConfiguration,
SolanaConnectivityConfiguration,
)
ETHEREUM_RPC_URLS = (
"https://your-primary-node.example/rpc",
"https://your-failover-node.example/rpc",
)
SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"
class MyConfigurations(BlockchainConfigurations):
"""Stock configurations with the RPC connectivity swapped out."""
@classmethod
def ethereum_configuration(cls) -> EthereumBlockchainConfiguration | None:
config = super().ethereum_configuration()
if config is None:
return None
return config.model_copy(
update={
"connectivity": EthereumConnectivityConfiguration(
rpc_provider=MultipleHTTPProvider(
retrieval_providers=[
LimitedHTTPProvider(url, max_request_per_second=5)
for url in ETHEREUM_RPC_URLS
],
# Reads and broadcasts share the pool; pass an explicit
# list to pin sends to a private relay.
execution_providers=None,
)
)
}
)
@classmethod
def solana_configuration(cls) -> SolanaBlockchainConfiguration | None:
config = super().solana_configuration()
if config is None:
return None
return config.model_copy(
update={
"connectivity": SolanaConnectivityConfiguration(
rpc_provider=AsyncClient(SOLANA_RPC_URL),
)
}
)
def configure() -> None:
"""Register everything. Idempotent: known platforms are left alone."""
BlockchainsInitializer.configure(configurations=MyConfigurations)
WalletsInitializer.configure()
if __name__ == "__main__":
configure()
ethereum = BlockchainFactory.get_evm_blockchain_by_identifier("ethereum")
solana = BlockchainFactory.get_solana_blockchain_by_identifier("solana")
print(ethereum.platform.identifier, solana.platform.identifier)
Configuring is offline: it only builds provider objects, so no RPC call is made until you actually read or write something.
The library ships three named configurations out of the box —
ethereum (chain id 1), hardhat (chain id 31337, local testnet) and
solana. Add your own by defining another *_configuration classmethod on
your subclass: every such method is discovered automatically. Also declare the
new name in configuration_blockchain_types() so that, when
BlockchainsInitializer.configure(blockchain_types=[...]) filters by type, the
configuration can be skipped before it is built (and its RPC client
allocated); undeclared configurations are built first and filtered afterwards.
Explorer API key
The stock Ethereum configuration reads ETHERSCAN_API_KEY from the
environment and attaches it to the EtherscanConfiguration. It is optional
— without it, explorer calls (ABI downloads) are anonymous and rate
limited.
3. Build wallets¶
A wallet needs an identifier (platform + address, optionally a name) and — to sign anything — a signer configuration holding the private key. Without a signer the wallet is read-only: balances and transaction tracking still work.
You can either register configurations and let WalletFactory cache one
instance per identifier:
"""Register wallet configurations and build them through the factory."""
from pydantic import SecretStr
from blockchainpype.evm.blockchain.identifier import EthereumAddress
from blockchainpype.evm.wallet.identifier import EthereumWalletIdentifier
from blockchainpype.evm.wallet.signer import EthereumSignerConfiguration
from blockchainpype.evm.wallet.wallet import (
EthereumWallet,
EthereumWalletConfiguration,
)
from blockchainpype.factory import BlockchainFactory, WalletFactory, WalletRegistry
ETHEREUM_ADDRESS = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
ETHEREUM_PRIVATE_KEY = "0x<your-private-key>"
def build_wallet() -> EthereumWallet:
blockchain = BlockchainFactory.get_evm_blockchain_by_identifier("ethereum")
configuration = EthereumWalletConfiguration(
identifier=EthereumWalletIdentifier(
name="main-evm",
platform=blockchain.platform,
address=EthereumAddress.from_string(ETHEREUM_ADDRESS),
),
signer=EthereumSignerConfiguration(
private_key=SecretStr(ETHEREUM_PRIVATE_KEY)
),
)
identifier = configuration.identifier.identifier # "ethereum:main-evm"
if WalletRegistry.get(identifier) is None:
WalletRegistry.register(configuration)
wallet = WalletFactory.create(identifier)
assert isinstance(wallet, EthereumWallet)
return wallet
…or construct the wallet directly, passing the blockchain explicitly (handy in tests and scripts, where nothing has to be globally registered):
from blockchainpype.evm.wallet.wallet import EthereumWallet
def build_wallet_directly(blockchain, configuration) -> EthereumWallet:
return EthereumWallet(configuration=configuration, blockchain=blockchain)
Never commit a private key
Read it from the environment or a git-ignored .env file. SecretStr
keeps it out of logs and repr(), but it cannot protect a key that is in
your repository.
4. EVM: send ETH and transfer an ERC-20¶
wallet.build_transaction() fills in the sender, the chain id and the gas
fields (estimated with the wallet's GasConfiguration and the chain's
GasStrategy). wallet.sign_and_send_transaction() is synchronous: it
signs, records the transaction in the wallet's tracker and schedules the
broadcast as a background task on the running event loop, so it must be called
from inside a coroutine.
"""Send ETH and transfer an ERC-20, then wait for both receipts."""
import asyncio
from datetime import timedelta
from decimal import Decimal
from web3.types import TxParams, Wei
from blockchainpype.evm.blockchain.identifier import EthereumAddress
from blockchainpype.evm.dapp.erc20 import (
ERC20Contract,
ERC20ContractConfiguration,
ERC20Token,
)
from blockchainpype.evm.wallet.wallet import EthereumWallet
RECIPIENT = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
async def send_native(wallet: EthereumWallet) -> None:
blockchain = wallet.blockchain
recipient = EthereumAddress.from_string(RECIPIENT)
# Nonces are allocated locally; sync once before the first send.
await wallet.sync_nonce()
tx_params = await wallet.build_transaction(
tx_data=TxParams(
to=recipient.raw,
value=Wei(blockchain.native_asset.convert_to_raw(Decimal("0.01"))),
)
)
transaction = wallet.sign_and_send_transaction(
client_operation_id="send-eth-1",
tx_data=dict(tx_params),
)
update = await wallet.get_transaction_update(
transaction, timeout=timedelta(minutes=2), raise_timeout=False
)
print(update.new_state, transaction.operator_operation_id)
async def transfer_erc20(wallet: EthereumWallet) -> None:
blockchain = wallet.blockchain
token_address = EthereumAddress.from_string(USDC_ADDRESS)
token = ERC20Token(
platform=blockchain.platform,
identifier=token_address,
contract=ERC20Contract(
ERC20ContractConfiguration(
platform=blockchain.platform,
address=token_address,
)
),
)
# Fetches name / symbol / decimals from the token contract.
await token.initialize_data()
balance = await token.contract.get_balance_of(wallet.address)
print(f"balance: {balance} {token.data.symbol}")
# Amounts are decimal-adjusted; the contract scales them by `decimals`.
transaction = await token.contract.place_transfer(
wallet,
recipient=EthereumAddress.from_string(RECIPIENT),
amount=Decimal("25.5"),
)
update = await wallet.get_transaction_update(
transaction, timeout=timedelta(minutes=2), raise_timeout=False
)
print(update.new_state)
async def main() -> None:
# build_wallet() is the helper from step 3.
wallet = build_wallet()
await send_native(wallet)
await transfer_erc20(wallet)
if __name__ == "__main__":
asyncio.run(main())
place_transfer (and its siblings place_transfer_from and place_approve)
initializes the contract, syncs the nonce when needed, builds the call through
the wallet and returns the tracked transaction — the whole ERC-20 write path in
one call. All read helpers (get_balance_of, get_total_supply,
get_allowance) return decimal-adjusted amounts; their get_raw_*
counterparts return the untouched on-chain integers.
5. Solana: check a balance and transfer an SPL token¶
Solana wallets take a base58 secret key. Transaction building is explicit: you assemble instructions, wrap them in a message with a recent blockhash, and hand the result to the wallet.
"""Read SOL / SPL balances and transfer an SPL token."""
import asyncio
from datetime import timedelta
from decimal import Decimal
from pydantic import SecretStr
from solders.message import Message
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
from blockchainpype.factory import BlockchainFactory
from blockchainpype.solana.asset import SolanaAssetData
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.dapp.token import (
SPLToken,
SPLTokenProgram,
SPLTokenProgramConfiguration,
)
from blockchainpype.solana.wallet.identifier import SolanaWalletIdentifier
from blockchainpype.solana.wallet.signer import SolanaSignerConfiguration
from blockchainpype.solana.wallet.wallet import SolanaWallet, SolanaWalletConfiguration
SOLANA_ADDRESS = "11111111111111111111111111111112"
SOLANA_PRIVATE_KEY = "<your-base58-keypair>"
RECIPIENT = "11111111111111111111111111111112"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
def build_wallet() -> SolanaWallet:
blockchain = BlockchainFactory.get_solana_blockchain_by_identifier("solana")
return SolanaWallet(
configuration=SolanaWalletConfiguration(
identifier=SolanaWalletIdentifier(
name="main-solana",
platform=blockchain.platform,
address=SolanaAddress.from_string(SOLANA_ADDRESS),
),
signer=SolanaSignerConfiguration(
private_key=SecretStr(SOLANA_PRIVATE_KEY)
),
),
blockchain=blockchain,
)
async def show_balances(wallet: SolanaWallet) -> None:
blockchain = wallet.blockchain
sol_balance = await blockchain.fetch_native_asset_balance(wallet.address)
usdc_balance = await blockchain.fetch_spl_token_balance(
wallet.address, SolanaAddress.from_string(USDC_MINT)
)
# A missing associated token account is reported as a zero balance.
print(f"{sol_balance} SOL / {usdc_balance} USDC")
async def send_sol(wallet: SolanaWallet) -> None:
blockchain = wallet.blockchain
recipient = SolanaAddress.from_string(RECIPIENT)
instruction = transfer(
TransferParams(
from_pubkey=wallet.address.raw,
to_pubkey=recipient.raw,
lamports=blockchain.native_asset.convert_to_raw(Decimal("0.1")),
)
)
recent_blockhash = await blockchain.fetch_recent_blockhash()
message = Message.new_with_blockhash(
[instruction], wallet.address.raw, recent_blockhash
)
transaction = wallet.sign_and_send_transaction(
client_operation_id="send-sol-1",
transaction=Transaction.new_unsigned(message),
recent_blockhash=recent_blockhash,
)
update = await wallet.get_transaction_update(
transaction, timeout=timedelta(seconds=60), raise_timeout=False
)
print(update.new_state, transaction.operator_operation_id)
async def transfer_spl(wallet: SolanaWallet) -> None:
blockchain = wallet.blockchain
mint = SolanaAddress.from_string(USDC_MINT)
program = SPLTokenProgram(
SPLTokenProgramConfiguration(platform=blockchain.platform)
)
token = SPLToken(
platform=blockchain.platform,
identifier=mint,
mint=mint,
program=program,
# Omit `data` to fetch the mint's decimals with initialize_data().
data=SolanaAssetData(name="USD Coin", symbol="USDC", decimals=6),
)
# transferChecked between the two owners' associated token accounts.
transaction = await program.place_transfer(
wallet,
token,
destination=SolanaAddress.from_string(RECIPIENT),
amount=Decimal("25.5"),
)
update = await wallet.get_transaction_update(
transaction, timeout=timedelta(seconds=60), raise_timeout=False
)
print(update.new_state)
async def main() -> None:
wallet = build_wallet()
await show_balances(wallet)
await send_sol(wallet)
await transfer_spl(wallet)
if __name__ == "__main__":
asyncio.run(main())
SPLTokenProgram resolves its blockchain through the BlockchainFactory using
the platform in its configuration, so run BlockchainsInitializer.configure()
(step 2) first.
Next steps¶
- EVM guide — provider failover and rate limiting, gas strategies, ABI sources, the full wallet lifecycle.
- Solana guide — versioned transactions, ATAs, Anchor IDLs.
- DApps guide — swap on Uniswap, lend on Aave or Solend, trade on Polymarket.
- Testing guide — how the library is tested, and how to test code that builds on it.