Solana guide¶
Everything under blockchainpype.solana targets Solana through solana-py
(AsyncClient) and solders.
Blockchain configuration¶
SolanaBlockchainConfiguration mirrors its EVM counterpart:
| Field | Type | Purpose |
|---|---|---|
platform |
BlockchainPlatform |
Identity: identifier, blockchain type, chain_id is None on Solana |
connectivity |
SolanaConnectivityConfiguration |
Holds the solana.rpc.async_api.AsyncClient |
native_asset |
SolanaNativeAssetConfiguration |
Defaults to SOL, 9 decimals |
explorer |
SolscanConfiguration \| None |
Link builder |
The client's own commitment is adopted by the blockchain and used for balance
reads, so configure it on the AsyncClient.
from financepype.platforms.blockchain import BlockchainPlatform
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from blockchainpype.solana.blockchain.blockchain import (
SolanaBlockchain,
SolanaBlockchainType,
)
from blockchainpype.solana.blockchain.configuration import (
SolanaBlockchainConfiguration,
SolanaConnectivityConfiguration,
)
from blockchainpype.solana.explorer.solscan import SolscanConfiguration
configuration = SolanaBlockchainConfiguration(
platform=BlockchainPlatform(
identifier="solana",
type=SolanaBlockchainType,
chain_id=None,
),
connectivity=SolanaConnectivityConfiguration(
rpc_provider=AsyncClient(
"https://api.mainnet-beta.solana.com", commitment=Confirmed
),
),
explorer=SolscanConfiguration(),
)
blockchain = SolanaBlockchain(configuration=configuration)
Identifiers and the native-SOL sentinel¶
SolanaPublicKey (and its SolanaAddress alias) wrap a solders Pubkey;
SolanaTransactionSignature wraps a Signature. Both validate on
construction.
Native SOL has no mint account, so the library identifies it with a
sentinel address, SolanaNullAddress:
from blockchainpype.solana.blockchain.identifier import (
NATIVE_SOL_SENTINEL_ADDRESS,
WRAPPED_SOL_MINT_ADDRESS,
SolanaAddress,
SolanaNullAddress,
)
# Note the trailing "1" vs "2": the sentinel is NOT the wrapped-SOL mint.
assert NATIVE_SOL_SENTINEL_ADDRESS == "So11111111111111111111111111111111111111111"
assert WRAPPED_SOL_MINT_ADDRESS == "So11111111111111111111111111111111111111112"
assert SolanaNullAddress().string == NATIVE_SOL_SENTINEL_ADDRESS
wsol = SolanaAddress.from_string(WRAPPED_SOL_MINT_ADDRESS)
print(wsol.raw)
Wrapped SOL is a real SPL token and must be modelled as an SPLToken on that
mint — never through the sentinel.
Reading the chain¶
import asyncio
from blockchainpype.factory import BlockchainFactory
from blockchainpype.solana.blockchain.identifier import (
SolanaAddress,
SolanaTransactionSignature,
)
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
async def main(signature: str) -> None:
blockchain = BlockchainFactory.get_solana_blockchain_by_identifier("solana")
owner = SolanaAddress.from_string("11111111111111111111111111111112")
slot = await blockchain.fetch_block_number()
block_time = await blockchain.fetch_block_timestamp(slot)
blockhash = await blockchain.fetch_recent_blockhash()
print(slot, block_time, blockhash)
sol = await blockchain.fetch_native_asset_balance(owner) # SOL, not lamports
usdc = await blockchain.fetch_spl_token_balance(
owner, SolanaAddress.from_string(USDC_MINT)
)
print(sol, usdc)
tx_signature = SolanaTransactionSignature.from_string(signature)
status = await blockchain.fetch_transaction_status(tx_signature)
receipt = await blockchain.fetch_transaction_receipt(tx_signature)
print(status, receipt)
asyncio.run(main("<a base58 transaction signature>"))
fetch_block_data passes max_supported_transaction_version=0 by default, so
blocks containing versioned (v0) transactions do not error.
fetch_spl_token_balance derives the owner's associated token account and
scales the amount by the mint's decimals; when the ATA does not exist yet the
RPC error is translated into a zero balance rather than an exception.
Wallets¶
A SolanaWalletConfiguration needs an identifier and, to sign, a
SolanaSignerConfiguration holding the base58 keypair string
(Keypair.from_base58_string).
from pydantic import SecretStr
from blockchainpype.factory import BlockchainFactory
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.wallet.identifier import SolanaWalletIdentifier
from blockchainpype.solana.wallet.signer import SolanaSignerConfiguration
from blockchainpype.solana.wallet.wallet import SolanaWallet, SolanaWalletConfiguration
def build_wallet(address: str, private_key: str | None) -> 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(address),
),
# Without a signer the wallet is read-only.
signer=(
SolanaSignerConfiguration(private_key=SecretStr(private_key))
if private_key
else None
),
),
blockchain=blockchain,
)
Signing: legacy and versioned transactions¶
wallet.sign_transaction(transaction, recent_blockhash, additional_signers=None)
handles both shapes:
- Legacy
Transaction— partially signed in place with the provided blockhash, so a partially signed transaction from a co-signer keeps its existing signatures. VersionedTransaction— rebuilt with the provided blockhash, preserving the compiled instructions, account keys and any address-table lookups, then fully re-signed by the wallet and every additional signer. AMessageV0message keeps itsaddress_table_lookups; a legacy message inside a versioned transaction is rebuilt withMessage.new_with_compiled_instructions.
from solders.hash import Hash
from solders.transaction import Transaction, VersionedTransaction
from blockchainpype.solana.wallet.signer import SolanaSigner
from blockchainpype.solana.wallet.wallet import SolanaWallet
def sign_either(
wallet: SolanaWallet,
transaction: Transaction | VersionedTransaction,
blockhash: Hash,
cosigners: list[SolanaSigner],
) -> Transaction | VersionedTransaction:
return wallet.sign_transaction(
transaction, blockhash, additional_signers=cosigners
)
sign_transaction raises ValueError when the wallet has no signer.
Sending and tracking¶
wallet.sign_and_send_transaction(client_operation_id, transaction, recent_blockhash, ...)
is synchronous: it signs, records the transaction in the tracker and schedules
the broadcast as a background task on the running event loop. Re-calling it
with the client_operation_id of an already-signed transaction is an
idempotent retry.
wallet.get_transaction_update(transaction, timeout, raise_timeout, **kwargs)
polls getSignatureStatuses every 2 s (override with poll_interval) and maps
the result:
| Signature status | Reported state |
|---|---|
err set |
FAILED (with the receipt and other_data["error"]) |
Finalized |
FINALIZED (with the receipt) |
Confirmed |
CONFIRMED (with the receipt) |
| processed / unknown | keeps polling |
On timeout it raises TimeoutError or, with raise_timeout=False, returns an
update carrying the unchanged state.
import asyncio
from datetime import timedelta
from solders.message import Message
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.wallet.wallet import SolanaWallet
async def send_sol(wallet: SolanaWallet, to: str, lamports: int) -> None:
blockchain = wallet.blockchain
instruction = transfer(
TransferParams(
from_pubkey=wallet.address.raw,
to_pubkey=SolanaAddress.from_string(to).raw,
lamports=lamports,
)
)
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="transfer-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)
if __name__ == "__main__":
# Build the wallet as shown above, then:
# asyncio.run(send_sol(wallet, "<recipient>", 100_000_000))
print(asyncio.iscoroutinefunction(send_sol))
SPL tokens¶
Associated token accounts¶
The ATA for a (owner, mint) pair is derived deterministically — no RPC call
is involved — both on the blockchain and on the token program:
from blockchainpype.solana.blockchain.blockchain import SolanaBlockchain
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.dapp.token import (
SPLTokenProgram,
SPLTokenProgramConfiguration,
)
def derive(owner: SolanaAddress, mint: SolanaAddress) -> SolanaAddress:
# Static helper on the blockchain class (canonical SPL Token program).
return SolanaBlockchain.derive_associated_token_account(owner, mint)
def derive_via_program(
platform, owner: SolanaAddress, mint: SolanaAddress
) -> SolanaAddress:
# Program-scoped: uses the program's own address as token_program_id, so a
# configuration pointing at another token program derives its own ATAs.
program = SPLTokenProgram(SPLTokenProgramConfiguration(platform=platform))
return program.get_associated_token_account(owner, mint)
Program interface¶
SPLTokenProgram is a SolanaProgram whose configuration defaults to the
canonical SPL Token program address and to a small in-memory IDL
(SPL_TOKEN_IDL) that merely documents the supported instruction names —
instructions are built with the spl.token helpers shipped with solana-py,
so no IDL-driven encoding is needed.
| Method | What it does |
|---|---|
get_associated_token_account(owner, mint) |
Derives the ATA |
get_token_account_balance(token_account) |
Decimal-adjusted balance of a token account |
get_balance_of(owner, mint) |
Balance through the owner's ATA (the RPC call fails if the ATA does not exist) |
get_token_accounts_by_owner(owner, mint) |
Every token account of an owner for a mint |
build_transfer_instruction(source_owner, destination_owner, mint, raw_amount, decimals) |
A transferChecked instruction between the two owners' ATAs |
place_transfer(wallet, token, destination, amount, client_operation_id=None) |
Builds the transfer, wraps it in a legacy transaction with the wallet as fee payer, then signs, broadcasts and tracks it |
transferChecked is used rather than transfer because it asserts the mint
and decimals on-chain, so a decimals mismatch fails the instruction instead of
moving the wrong amount.
import asyncio
from decimal import Decimal
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.wallet import SolanaWallet
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
async def transfer_usdc(wallet: SolanaWallet, destination: str) -> None:
platform = wallet.blockchain.platform
mint = SolanaAddress.from_string(USDC_MINT)
program = SPLTokenProgram(SPLTokenProgramConfiguration(platform=platform))
token = SPLToken(
platform=platform,
identifier=mint,
mint=mint,
program=program,
data=SolanaAssetData(name="USD Coin", symbol="USDC", decimals=6),
)
transaction = await program.place_transfer(
wallet,
token,
destination=SolanaAddress.from_string(destination),
amount=Decimal("10"),
)
print(transaction.client_operation_id)
assert asyncio.iscoroutinefunction(transfer_usdc)
The SPLToken asset¶
SPLToken is a SolanaAsset carrying the mint and, optionally, the
program. When data is omitted, await token.initialize_data() fetches the
mint's decimals from getTokenSupply (the only field that affects amount
scaling) and sets name/symbol to the mint address string — Solana mints
carry no on-chain name or symbol; that lives in Metaplex metadata, which this
library does not query. initialize_data() raises ValueError when no program
is set and the data is missing.
Programs and IDLs¶
SolanaProgram is built from a SolanaProgramConfiguration (platform,
address, idl_configuration). await program.initialize() loads the IDL;
program.is_initialized guards it and create_instruction raises ValueError
before it.
Two IDL sources ship with the library:
SolanaLocalFileIDL(file_name=..., folder_path=common_idl_path)— reads a JSON object fromcommon/idl(bundled:solend.json,jupiter_dca.json).SolanaDictIDL(idl=...)— an in-memory dictionary.
Two IDL layouts are supported everywhere:
| Layout | instructions shape |
Example |
|---|---|---|
| Real Anchor IDL | a list of instruction definitions | common/idl/jupiter_dca.json |
| Simplified hand-written IDL | a dict keyed by instruction name | common/idl/solend.json |
find_idl_instruction(idl, name) looks a definition up in either shape.
Anchor instructions: discriminators + borsh¶
For a list-form Anchor IDL, create_instruction(name, accounts, args=...)
encodes the data as the 8-byte discriminator followed by the borsh-encoded
arguments declared in the IDL:
- The discriminator is the instruction's explicit
discriminatorfield when present (Anchor ≥ 0.30 emits one), otherwise the first 8 bytes ofsha256("global:<snake_case_name>"). Anchor exposes camelCase names while discriminators derive from the Rust snake_case method name, soto_snake_casebridges the two (openDcaV2→open_dca_v2). - Arguments are encoded little-endian by
encode_borsh_value, which supports the primitivesu8–u128,i8–i128,f32/f64,bool,publicKey/pubkey,string,bytes, and the composites{"option": …},{"vec": …},{"array": [inner, length]}and{"defined": name}(struct kinds and unit-variant enums, resolved against the IDLtypessection). - Unknown argument names, missing non-optional arguments and values that do not
fit their type all raise
ValueError.
from blockchainpype.solana.dapp.program import (
anchor_discriminator,
encode_borsh_value,
to_snake_case,
)
assert to_snake_case("openDcaV2") == "open_dca_v2"
assert len(anchor_discriminator("openDcaV2")) == 8
assert encode_borsh_value(1, "u8") == b"\x01"
assert encode_borsh_value(258, "u16") == b"\x02\x01"
assert encode_borsh_value(None, {"option": "u64"}) == b"\x00"
assert encode_borsh_value("hi", "string") == b"\x02\x00\x00\x00hi"
import asyncio
from solders.instruction import AccountMeta
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.dapp.idl import SolanaLocalFileIDL
from blockchainpype.solana.dapp.program import (
SolanaProgram,
SolanaProgramConfiguration,
)
async def build_anchor_instruction(platform, program_id: str) -> None:
program = SolanaProgram(
SolanaProgramConfiguration(
platform=platform,
address=SolanaAddress.from_string(program_id),
idl_configuration=SolanaLocalFileIDL(file_name="jupiter_dca.json"),
)
)
await program.initialize()
# Every non-optional argument declared by the IDL must be supplied;
# `{"option": ...}` arguments may be omitted (encoded as None).
instruction = program.create_instruction(
"openDcaV2",
accounts=[
AccountMeta(SolanaAddress.from_string(program_id).raw, False, False),
],
args={
"applicationIdx": 0,
"inAmount": 1_000_000,
"inAmountPerCycle": 100_000,
"cycleFrequency": 3600,
"minPrice": None,
"maxPrice": None,
"startAt": None,
},
)
print(instruction.data[:8].hex())
assert asyncio.iscoroutinefunction(build_anchor_instruction)
Simplified IDLs: pre-encoded data¶
Dict-form IDLs carry no argument specs, so they act purely as a name
registry. Instructions with a payload must be given fully pre-encoded data
(including any protocol-specific discriminant); passing args for such an
instruction raises ValueError, as does passing both data and args.
This is exactly how the Solend integration works: Solend is a fork of the SPL token-lending program, not an Anchor program, so every instruction is a single-byte enum discriminant followed by little-endian fields.
from solders.instruction import AccountMeta
from blockchainpype.solana.blockchain.identifier import SolanaAddress
from blockchainpype.solana.dapp.program import SolanaProgram
def build_raw(program: SolanaProgram, account: SolanaAddress) -> None:
# Discriminant 14 = DepositReserveLiquidityAndObligationCollateral,
# followed by the u64 little-endian liquidity amount.
data = bytes([14]) + (1_000_000).to_bytes(8, "little")
instruction = program.create_instruction(
"deposit",
accounts=[AccountMeta(account.raw, False, True)],
data=data,
)
print(instruction.program_id)
Explorer¶
SolscanExplorer is a pure link builder — it performs no network access
(Solscan's HTTP API is paid and intentionally not wrapped).
from blockchainpype.solana.blockchain.identifier import (
SolanaAddress,
SolanaTransactionSignature,
)
from blockchainpype.solana.explorer.solscan import (
SolscanConfiguration,
SolscanExplorer,
)
explorer = SolscanExplorer(SolscanConfiguration())
mint = SolanaAddress.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
print(explorer.get_address_link(mint)) # https://solscan.io/account/<pubkey>
print(explorer.get_token_link(mint)) # https://solscan.io/token/<mint>
print(explorer.get_block_link(123456)) # https://solscan.io/block/123456
def tx_link(signature: SolanaTransactionSignature) -> str:
return explorer.get_transaction_link(signature) # https://solscan.io/tx/<sig>
Point base_url at another deployment (e.g. a devnet view) to change every
generated link. When an explorer is configured on the blockchain, transaction
updates produced by the wallet and by send_transaction carry
explorer_link automatically.
See also¶
- DApps guide — the Solend money-market integration.
- Quickstart — end-to-end setup.