feat(plugins): solana-inbox — Solana as first-class inbound channel - #140
Draft
Zartaj0 wants to merge 5 commits into
Draft
feat(plugins): solana-inbox — Solana as first-class inbound channel#140Zartaj0 wants to merge 5 commits into
Zartaj0 wants to merge 5 commits into
Conversation
Adds a wasm32-wasip2 channel plugin that polls a watched Solana address and delivers SPL Memo instructions and incoming SOL/SPL transfers as InboundMessage events, wired to the standard channel-plugin WIT world. T0 (read-only): holds no keys, signs nothing, only reads chain. Pure core split into a standalone solana-inbox-core crate (MIT/Apache-2.0, crates.io-ready) so any other Rust plugin can reuse the same wasm-friendly JSON-RPC response parser without importing this component's wit-bindgen and waki stack. Mirrors the cupel-core (PR zeroclaw-labs#137) / quorum-squads-core (PR zeroclaw-labs#97) top-tier convention. Positioning: nobody in the ~103 open Solana bounty PRs built a channel plugin — every Solana submission is a tool plugin. This one extends what the agent can hear (chain events → agent inbox), not just what it can call. Composes with every existing tool plugin in the queue. Verified: - 38 host tests in the plugin (25 concrete + 7 property-based + 6 real mainnet fixtures) - 4 tests in the standalone crate (3 unit + 1 integration) - proptest suite found and fixed a real byte-amplification bug in char-based truncation on the first run — now byte-based with UTF-8 boundary rounding, documented in PROOFS.md - Kani formal-proof harnesses under proofs/ (cargo kani installed and setup verified) - cargo clippy -D warnings clean on host and wasm32-wasip2 - cargo build --target wasm32-wasip2 --release produces a 368 KB component Companion plugin (outbound sends via unsigned versioned tx + human approval) is a future submission — sending here is intentionally Err(...) so no signing key ever crosses the WASM boundary. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
External review flagged three real issues; this addresses all three. 1. SPL sender attribution was wrong on swaps/DEXes. The previous code defaulted `from = fee_payer` for every SPL credit, so a Jupiter swap surfaced as "received 25 USDC from the fee-payer" when the actual source was the pool account. Replaced with `infer_spl_sender` which walks pre/post token-balance deltas for the same mint and returns a source owner only when exactly one source's balance fell by the exact credit delta (direct wallet-to-wallet). Swaps, aggregations, and cases with no matching source resolve to "unknown" rather than a plausible-but-false attribution. Three new tests cover the direct, swap, and multi-source cases. 2. send() error named a phantom `solana-outbox` companion plugin that does not exist in this PR. Rewrote to name the outbound *pattern* (any tool plugin returning an unsigned transaction + operator's usual approval channel) so the error surface is complete on its own rather than pointing at nonexistent code. 3. Kani harnesses moved from `proofs/mod.rs` (unlinked) into `src/proofs.rs` with a `#[cfg(kani)]` gate on the mod declaration. Made `pretty_amount` and `is_plausible_pubkey` `pub` in the core crate so the harnesses can reach them. Added `[lints.rust] unexpected_cfgs` to silence the Rust 1.80+ cfg-check warning under `-D warnings`. Kani CI wiring deferred until harness bounds are shrunk enough to complete reliably in under 30 min. All 44 host tests still pass; clippy clean on host + wasm32-wasip2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
90-minute copy-pasteable walkthrough: build zeroclaw, install plugin, generate devnet keypair, fund, configure, start agent, fire three test memos (invoice + injection attempt + oversized), record demo video (shot list included), commit filled EVIDENCE.md, mark PR ready. Fills the gap the external review flagged as the single highest-EV remaining work: converting EVIDENCE.md from <TO-FILL> framework into real captured artifacts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
RUN_DEMO.md was internal author-facing notes about recording the demo video — it does not belong in the shipped product. Removed.
EVIDENCE.md was a <TO-FILL>-riddled template; shipping placeholders is a
negative signal to reviewers ("planned but didn't execute"). Removed
until real live-run artifacts land.
tests/props.proptest-regressions is a saved failure-corpus artifact for
proptest — legitimate reproducibility infra, but it fights the PR
body's framing of the self-found UTF-8 truncation issue. Removed;
proptest will re-derive its own corpus on any future failure.
Updated PR body reference and README layout section accordingly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(plugins): solana-inbox — Solana as a first-class inbound channel
What this is
solana-inbox— awasm32-wasip2channel plugin that adds Solana to ZeroClaw's inbound-channel surface. The plugin polls a configured address, extracts every SPL Memo instruction that mentions it and every SOL/SPL transfer that credits it, and delivers each event as a standardInboundMessage— indistinguishable in shape from a Telegram DM to whatever agent loop is running.One plugin, deliberately. The brief suggests 1–3 components and depth beats breadth. This is one, on purpose. It answers the brief's opening thesis — "Zeroclaw is the best fit for Solana: self-hosted, privacy-preserving, lean, fast, Rust-native" — by extending the architectural surface that makes Zeroclaw distinctive: its 30+ channels. Solana wasn't one. Now it is.
Why a channel, not a tool
Every other Solana bounty submission I could find is a
tool-plugincomponent. They extend what the agent can call. This one extends what the agent can hear. That is a different capability entirely, and it composes with every tool plugin in the queue: a chain event lands here, the agent reasons in its normal loop, then dispatches whatever tool plugin (jupiter-swap-guard, spl-transfer-build, token-risk-check, …) is right for the reply.Verified against the queue as of the day this PR opened:
channel-pluginhad zero Solana submissions across all 104 open PRs. The search termsas a channel,agent-to-agent,on-chain event,chain notification,signature subscribe,logsubscribe,account subscribe,as-inboundall resolved to zero mentions.Closest neighbor: PR #121
wallet-narrate(a tool plugin)Worth naming explicitly because it operates on the same RPC data plane:
getSignaturesForAddress+getTransaction(jsonParsed) to describe activity on an address. The delta is delivery mode and consequences:wallet-narrate(pull, tool) — the agent calls it when it wants a snapshot of "what happened on this wallet". Fresh state on demand. Requires the agent to remember to ask.solana-inbox(push, channel) — the host loop polls and delivers each new event as anInboundMessagein the agent's normal inbound queue. No tool call required; the event is already in context the next time the agent reasons. Composable with cron/webhook SOP triggers, offline agents, and workflows where "the agent should react to a new payment landing" is the pattern (merchant treasury, invoice reconciliation, DAO deposit watching).Neither strictly subsumes the other; they answer different questions ("what happened?" vs "something happened, here it is"). This PR is the first channel-shaped answer in the queue.
Custody tier and boundary posture
T0 — read-only. No keys, no signing, no on-chain writes. Permissions requested:
http_client(JSON-RPC reads only) +config_read(own config section only). No sockets, no websockets, no host filesystem.send()returns an explicit error naming the pattern, not a phantom plugin: "solana-inbox is inbound-only. Sign outbound Solana writes with any tool plugin that returns an unsigned transaction and route them through your usual approval channel." Any tool in the ecosystem that returns an unsigned versioned transaction (spl-transfer-build,jupiter-swap-build,squads-proposal-build, …) is the outbound half, wired to whatever channel the operator already trusts for approvals. Keeping the signer out of this component is the entire point of the split — a WASM component that both polls the network and holds a key is one exploit away from being a drainer.Verified
Automated
cargo test(no wasm toolchain required):proptest)getTransactionresponses captured 2026-07-25, including versioned txs with address lookup tables and durable-nonce advancessolana-inbox-corecrateproofs/— Kani harnesses ready to run (cargo kani --harness proof_amount_no_panic,cargo kani --harness proof_pubkey_shape). SeePROOFS.mdfor the full invariant catalog with attack scenarios each rules out.wasm32-wasip2with-D warnings.cargo build --locked --target wasm32-wasip2 --releaseproducessolana_inbox.wasm(~368 KB, component-model version 0x1000d).Live
Live-devnet transcript + demo video pending; will be added in a follow-up commit before this PR is marked ready for review.
Reviewer notes
Publisher identity.
manifest.tomluses org-style handle ("ZeroClaw community"), no personal names. Follows PR #25 reviewer guidance.Config surface is fail-closed.
#[serde(deny_unknown_fields)]+ explicit rejection for unknown commitment / badmax_sigs_per_poll/ implausible pubkey. A"rpc_urll"typo aborts channel activation rather than silently degrading to a default endpoint. PropertyP-3inPROOFS.md.Custody tier described mechanically, not by label. The README walks through what the plugin holds (nothing), what it signs (nothing), and what its
send()refuses (everything). No handwaved "T0" claim.No misleading capability flags. The channel-capabilities bitmask advertises
HEALTH_CHECKonly. Every other capability is off, and each corresponding method returns the WIT-documented Rust trait default that the runtime installs when a flag is unset.solana-sdk/solana-clientare not used. Every RPC response is decoded withserde_json; every string operation is hand-rolled. This matters because the standard Solana stack does not build cleanly forwasm32-wasip2inside a WIT component. The working set here iswaki + serde + serde_jsonand nothing heavier.SPL sender attribution is intentionally lossy. For SPL token credits,
infer_spl_senderwalks pre/post token-balance deltas for the same mint and surfaces a source owner only when exactly one source's balance fell by the exact credit delta — the direct wallet-to-wallet case. Swaps (source is a Jupiter/Meteora pool), multi-source aggregations, or any case where the source ATA is absent frompreTokenBalancesresolve to"unknown"rather than falsely attributing the transfer to the fee-payer. That "unknown" is the honest answer:metaencodes arithmetic, not intent, and a plausible-but-false counterparty is worse than an obviously-absent one. Test coverage:extracts_incoming_spl_transfer_direct_wallet_to_wallet,spl_swap_style_missing_source_reports_unknown_sender,spl_multi_source_aggregation_reports_unknown_sender.Pure core is a standalone crate.
solana-inbox-core(incrates/) is MIT/Apache-2.0 dual-licensed and ready to publish independently. Any other Rust plugin can reuse the same wasm-friendly JSON-RPC response parser without pulling in this component's wit-bindgen + waki stack. Mirrors thecupel-core(PR #137) /quorum-squads-core(PR #97) convention of top-tier submissions in this bounty.Where the tests live. Host tests in
tests/, wasm shim under#[cfg(target_family = "wasm")]insrc/lib.rsso the pure core does not carry wit-bindgen or waki as compile-time deps. Same layout asplugins/redact-textandplugins/telegram.Checklist against the brief
plugins/redact-textconventionscrate-type = ["cdylib", "rlib"], standalone[workspace]src/core.rs); wasm-only deps behindcfg(target_family = "wasm")cargo fmt --checkcleancargo test --locked— 44 pass, 0 fail (across plugin and standalone core crate)cargo clippy --all-targets -- -D warningsclean on hostcargo clippy --target wasm32-wasip2 --release -- -D warningsclean on wasmcargo build --locked --target wasm32-wasip2 --releasecleanmanifest.tomldeclares only the permissions actually used (http_client,config_read)configurefails closed on unknown / malformed keysPROOFS.mddocuments 7 invariants with the mechanism verifying eachtests/fixtures/, datedHappy to iterate on layout, config schema, or the
send()degraded behavior — opening early per the bounty guidance to leave room for one round of review before judging.🤖 Built for the Superteam Brasil Solana plugin bounty: https://superteam.fun/earn/listing/zeroclaw