Solana primitives that actually compile to wasm32-wasip2.
solana-sdk and solana-client do not build inside a WebAssembly component
without a fight, and the parts of them a plugin needs are small. This crate is
those parts, written against the wire formats directly:
- Pubkeys and PDAs — base58, on-curve checks,
find_program_address, associated token addresses. - A typed JSON-RPC client over a swappable transport.
wasi:httpin a component, a canned-response mock on the host. - SPL Token and Token-2022 — the 82-byte mint, the 165-byte token account, and the full TLV extension set: permanent delegates, transfer hooks, transfer fees, default account state, pausable, non-transferable, interest-bearing, scaled UI amount, confidential transfers, metadata.
- Unsigned v0 transactions — instruction building, message compilation, short-vec encoding, base64, and a message digest a human can verify.
- Durable nonces — because an approval queue outlives a blockhash.
- Metaplex metadata — enough to answer "can this token still be renamed?".
- Output shaping and hostile-string handling — turning chain data into the two hundred tokens a model needs, and neutralizing the ones an attacker wrote.
No solana-sdk. No bincode. No borsh. No C toolchain. No async runtime.
Six pure-Rust dependencies: serde, serde_json, bs58, sha2,
curve25519-dalek, base64 — plus waki, and only when targeting wasm.
It cannot sign. There is no keypair type, no signer trait, and no path from an instruction to a signature. A plugin built on this crate can hold at most an RPC URL. Signing belongs to a wallet, a human, or a multisig.
It is host-testable. Everything except WakiTransport is pure Rust with no
wasm dependency. Your plugin's cargo test runs on the host against
MockTransport, with no wasm toolchain and no live network — which is exactly
what ZeroClaw's plugin CI requires.
It treats the chain as hostile input. Token names, symbols and metadata URIs
are written by whoever deployed the mint, and they end up in a language model's
context. sanitize exists because a tool that forwards them verbatim is an
injection vector with an RPC bill.
use solana_wasi::prelude::*;
let rpc = RpcClient::new(rpc_url, WakiTransport::new());
let mint = Pubkey::from_base58("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo")?;
let state = MintState::parse(mint, &rpc.require_account(&mint)?)?;
if let Some(delegate) = state.permanent_delegate() {
println!("{} can seize any balance", delegate.abbreviated());
}Building something a human will approve:
let ix = instructions::transfer_checked(
state.program, &source, &mint, &destination, &sender, 25_000_000, 6,
);
let message = Message::compile(&sender, &[ix], blockhash)?;
let digest = message.digest(); // show this to the human
let base64 = UnsignedTransaction::new(message).to_base64()?;Testing it, with no network and no wasm:
let transport = MockTransport::new().on("getAccountInfo", canned_response);
let rpc = RpcClient::new("https://rpc.example", &transport);
assess(&rpc, &mint)?;
assert_eq!(transport.call_count(), 2); // no N+1 RPC regressions94 host tests, and the parts that would be embarrassing to get wrong are checked against the cluster itself rather than only against this crate's own opinion:
- Transaction encoding — a v0 transfer built by this crate, replayed through
simulateTransactionon mainnet-beta, executes witherr: null. - PDA derivation — the ATA and Metaplex metadata addresses this crate derives are the accounts mainnet actually holds.
- Token-2022 parsing — the fixtures are the real USDC and PYUSD mint accounts, byte for byte.
- On-curve checks — cross-checked against an independent ed25519 implementation.
See docs/VERIFICATION.md for the commands, so you can re-run them rather than take this paragraph's word for it.
Written down in docs/VERIFICATION.md, but the short version:
solana-programandsolana-clientare not viable inside a WIT component. The wire formats are small enough to write by hand, and doing so removes the whole problem.- You do not need
borshfor the payments path. SPL Token accounts are C-layout withCOptiondiscriminants; instruction data is a handful of little-endian bytes. Metaplex metadata is borsh-shaped and is parsed by hand in eighty lines. curve25519-dalekbuilds forwasm32-wasip2withdefault-features = false, features = ["alloc"], which is what makesfind_program_address— and therefore every ATA — possible in a component.- The one that costs an hour: do not glob-import a prelude that re-exports a
Result<T>alias inside awit_bindgen::generate!module. A WIT export returnsResult<ToolResult, String>, and the alias shadows it with an error that points at the wrong line.
token-risk-check— T0, who else has power over a token.spl-transfer-build— T1, unsigned transfers for a human or a multisig to approve.
0.1.x. The API will move before 1.0. wit/v0 in ZeroClaw is itself marked
experimental, so pin your version and expect a rebuild.
MIT.