Skip to content

feat(plugins): solana-inbox — Solana as first-class inbound channel - #140

Draft
Zartaj0 wants to merge 5 commits into
zeroclaw-labs:mainfrom
Zartaj0:feat/solana-inbox
Draft

feat(plugins): solana-inbox — Solana as first-class inbound channel#140
Zartaj0 wants to merge 5 commits into
zeroclaw-labs:mainfrom
Zartaj0:feat/solana-inbox

Conversation

@Zartaj0

@Zartaj0 Zartaj0 commented Jul 25, 2026

Copy link
Copy Markdown

feat(plugins): solana-inbox — Solana as a first-class inbound channel

What this is

solana-inbox — a wasm32-wasip2 channel 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 standard InboundMessage — 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-plugin component. 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-plugin had zero Solana submissions across all 104 open PRs. The search terms as a channel, agent-to-agent, on-chain event, chain notification, signature subscribe, logsubscribe, account subscribe, as-inbound all 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 an InboundMessage in 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

  • 44 host tests total on cargo test (no wasm toolchain required):
    • 27 concrete unit / integration tests over the pure core, including the direct-wallet-to-wallet, swap-style, and multi-source aggregation SPL sender-attribution cases
    • 7 property-based tests over generated inputs (proptest)
    • 6 real-mainnet-fixture tests over four verbatim getTransaction responses captured 2026-07-25, including versioned txs with address lookup tables and durable-nonce advances
    • 3 unit tests inline in the pure core
    • 1 integration test in the standalone solana-inbox-core crate
  • Formal proofs in proofs/ — Kani harnesses ready to run (cargo kani --harness proof_amount_no_panic, cargo kani --harness proof_pubkey_shape). See PROOFS.md for the full invariant catalog with attack scenarios each rules out.
  • Clippy clean on host and wasm32-wasip2 with -D warnings.
  • cargo build --locked --target wasm32-wasip2 --release produces solana_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.toml uses 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 / bad max_sigs_per_poll / implausible pubkey. A "rpc_urll" typo aborts channel activation rather than silently degrading to a default endpoint. Property P-3 in PROOFS.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_CHECK only. 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-client are not used. Every RPC response is decoded with serde_json; every string operation is hand-rolled. This matters because the standard Solana stack does not build cleanly for wasm32-wasip2 inside a WIT component. The working set here is waki + serde + serde_json and nothing heavier.

SPL sender attribution is intentionally lossy. For SPL token credits, infer_spl_sender walks 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 from preTokenBalances resolve to "unknown" rather than falsely attributing the transfer to the fee-payer. That "unknown" is the honest answer: meta encodes 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 (in crates/) 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 the cupel-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")] in src/lib.rs so the pure core does not carry wit-bindgen or waki as compile-time deps. Same layout as plugins/redact-text and plugins/telegram.

Checklist against the brief

  • Layout matches plugins/redact-text conventions
  • crate-type = ["cdylib", "rlib"], standalone [workspace]
  • All logic in a pure Rust module (src/core.rs); wasm-only deps behind cfg(target_family = "wasm")
  • cargo fmt --check clean
  • cargo test --locked — 44 pass, 0 fail (across plugin and standalone core crate)
  • cargo clippy --all-targets -- -D warnings clean on host
  • cargo clippy --target wasm32-wasip2 --release -- -D warnings clean on wasm
  • cargo build --locked --target wasm32-wasip2 --release clean
  • manifest.toml declares only the permissions actually used (http_client, config_read)
  • configure fails closed on unknown / malformed keys
  • README covers architecture, config table, threat model, worked example, wasm-target notes
  • PROOFS.md documents 7 invariants with the mechanism verifying each
  • Live-devnet transcript + demo video — pending before ready-for-review
  • MIT / Apache-2.0 dual-licensed
  • Not a wrapper around an existing MCP server — real WIT component
  • Not a raw-key custody plugin
  • Not a trading bot
  • Real captured mainnet-beta fixtures in tests/fixtures/, dated

Happy 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

Zartaj0 and others added 5 commits July 25, 2026 17:41
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant