Skip to content

Add Solana plugin suite: payments, on-chain intelligence & a shared wasm core (Tracks A, D, E)#116

Open
oblivisheee wants to merge 16 commits into
zeroclaw-labs:mainfrom
oblivisheee:solana-plugins
Open

Add Solana plugin suite: payments, on-chain intelligence & a shared wasm core (Tracks A, D, E)#116
oblivisheee wants to merge 16 commits into
zeroclaw-labs:mainfrom
oblivisheee:solana-plugins

Conversation

@oblivisheee

Copy link
Copy Markdown

Solana plugin suite for ZeroClaw — payments, on-chain intelligence, and a shared wasm core

Six wasm32-wasip2 tool-plugins implementing the wit/v0 tool-plugin world, plus the shared Solana substrate they build on. Custody stays at T0/T1 throughout — no plugin holds a key or signs anything.

Tracks: A (payments), D (on-chain intelligence, safety & naming), E (shared core).

Plugin Tier What it does
plugins/solana-pay-request T1, zero secrets "charge 25 USDC" → Solana Pay URL / QR. Config-pinned recipient, token allowlist, per-request cap — enforced in-plugin, below the LLM.
plugins/payment-watch T0 Watch the receiving wallet for a recent incoming payment (SOL/SPL), optionally amount-matched, and confirm settlement. Closes the loop on solana-pay-request.
plugins/spl-transfer-build T1 Unsigned versioned transfer tx (base64): TransferChecked, ATA creation, memo, durable-nonce mode, mint allowlist + caps in-plugin. Emits a read-only Explorer review link + QR.
plugins/token-risk-check T0 RED/AMBER/GREEN mint report: mint/freeze authorities, Token-2022 extensions (permanent delegate, transfer hooks/fees), holder concentration.
plugins/sns-resolve T0 Resolve a .sol domain to its on-chain wallet — derived and verified, not hallucinated.
plugins/solana-account T0 Read an address's SOL balance, SPL holdings, account type, and recent activity, shaped to ~200 tokens.
crates/solana-core Track E: base58/PDA/ATA, v0 tx construction, JSON-RPC shaping over a mockable transport, durable nonces, Token-2022 TLV parsing, SNS derivation.

Meets the hard requirements

  • Canonical layout (redact-text): standalone crates, crate-type = ["cdylib", "rlib"], a pure core module + a thin #[cfg(target_family = "wasm")] shim over wit/v0.
  • Host tests98 pass with cargo test (RPC mocked, no network, no wasm toolchain), including fail-closed prompt-injection suites in every plugin and a context-budget test.
  • Builds clean: cargo build --target wasm32-wasip2 --release per plugin; clippy- and rustfmt-clean.
  • Structured logging via the log-record import (never stdout).
  • manifest.toml with minimal permissions; a README per plugin (what it does, config keys, custody tier, threat model, worked example, prompt-injection transcript); MIT.

The wasm32-wasip2 story

solana-sdk/solana-client don't compile into a WIT component, so transactions are hand-assembled from bs58 + sha2 + curve25519-dalek (decompression only, for the PDA off-curve check) + hand-rolled compact-u16 / v0-message / instruction encoding. Every encoding is proven against the decomposed solana-pubkey/solana-message/solana-transaction crates as host-only differential-test oracles (v0 key ordering, PDA/ATA derivation, and wire serialization byte-for-byte). Blockhash-expiry on approval-gated payments is solved with durable nonce accounts. HTTP is one blocking waki call behind a trait, so all protocol logic stays host-testable.

The shared core lives in crates/solana-core and is mirrored into each plugin's vendor/zeroclaw-solana-core/ (via tools/sync-core.py; --check guards drift) so every plugin directory builds in isolation the way CI snapshots them.

Happy to restructure however you prefer — crates/ placement, registry.json entries, or splitting into separate PRs. Opening early per the bounty guidance; feedback very welcome. Demo video to follow.

🤖 Generated with Claude Code

petrovichp and others added 16 commits July 21, 2026 18:58
Read-only mint safety check: authorities, Token-2022 extensions (permanent
delegate, transfer hooks, fees, default-frozen), holder concentration —
shaped to ~200 tokens. RPC error text clamped before it can reach a model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Offline, zero-secret Solana Pay transfer-request builder. Config-pinned
recipient, token allowlist, per-token amount caps. Roaming-terminal mode
(arg-supplied payee) is opt-in, so a forgotten pin fails closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds unsigned versioned transactions (TransferChecked + ATA create + memo),
with mint allowlist, per-token caps, recipient allowlist, durable-nonce mode
(solves approval-queue blockhash expiry), Token-2022 screening, and a
1232-byte packet-size guard. Holds no key; cannot sign or submit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cap + dedupe Token-2022 extension parsing at the source (MAX_EXTENSIONS=32)
so a mint account padded with thousands of duplicate TLVs can no longer
inflate a downstream report or transfer summary — closes the one demonstrated
context-flood vector that contradicted the risk plugin's threat model.

- token.rs: parse_tlv_extensions dedupes by type, caps at 32, reports
  truncation; parse_mint surfaces extensions_truncated; COption tag strictly 0/1
- token-risk-check: flag malformed/oversized mints RED, hard-clamp report to 2KB
- spl-transfer-build: refuse to build against a malformed/over-capped mint
- solana-pay-request: reject zero/non-positive amounts
- New tests: duplicate-extension flood (core + risk), zero-amount refusal;
  62 host tests total. Threat-model READMEs corrected to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iteration 4)

- format_base_units: checked_pow so an untrusted mint's decimals byte (0..=255,
  where 10^decimals overflows u64) can no longer divide-by-zero / overflow-panic
  the release wasm — the T0 tool now inspects even absurd mints without trapping
- token-risk-check: end-to-end hostile-decimals regression test
- solana-pay-request: cap label/message/memo/invoice_id at 128 chars so the URL
  (the one output that scales with caller input) stays bounded
- doc precision: instruction-encoding provenance, adversarial-test naming
- 65 host tests total

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (judge iteration 5)

The prior display-field cap missed that a numerically-valid but 50k-char
amount ("0"×50000+"1") passes every guard and floods the success-path URL
and output to ~100 KB. Bound every caller-supplied string at the source and
add a final ToolResult clamp in every shim as the definitive backstop:

- amount capped at 40 chars (core parse_decimal_amount + pay validate_decimal)
- Pubkey::parse rejects >44-char input before echoing it
- token symbol and memo length-capped in both payment tools
- getTokenLargestAccounts capped at 20 holders
- every plugin shim clamps ToolResult output+error to 4096 chars — no path,
  success or error, can exceed it regardless of future regressions
- new giant-argument flood tests in both payment tools; 67 host tests total
- README size range corrected to ~200–435 KB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ration 6)

Fourth plugin: sns-resolve resolves a .sol domain to its on-chain wallet so an
agent pays 'lucas.sol' from a derived, verified address instead of a
hallucinated one — the safety primitive that makes the payment tools usable
with human names. Track D. Composes: sns-resolve → token-risk-check →
spl-transfer-build.

- crates/solana-core: new sns module (SPL Name Service derivation +
  registry parsing), derivation regression-locked against the real bonfida.sol
  registry key
- sns-resolve: derived-collision guard, malformed-input fail-closed, output
  clamp; 5 host tests, mocked RPC
- Close the two remaining consistency items from iteration 6: the pay core now
  bounds the recipient echo itself (not only the shim), and decode_hash bounds
  the RPC blockhash echo — every error string is short at its own layer
- 77 host tests total; all four plugins build+test+clippy+fmt clean in
  upstream-CI isolation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge iteration 7)

- SNS label cap raised to a 63-char flood guard (SNS imposes no protocol max;
  labels hash to 32 bytes) with an honest 'too long' message; test now asserts
  a 40-char label resolves
- sns-resolve README gains the literal prompt-injection transcript the other
  three carry
- token-risk-check row states the hard 2 KB output cap alongside the ~200-token
  typical case

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eration 8)

- pay README worked example showed a fabricated payment reference; replace with
  the value the code actually produces (FnzC…3my8) and lock it with a host-test
  assertion so the headline demo can never drift again
- sns.rs: reject an all-zero registry owner (closed/malformed record) instead of
  reporting the system-program address as a payable wallet; add a test
- drop remaining stale '32-char label' references now that the bound is 63

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…polish

Fifth plugin: payment-watch (T0) closes the payments loop — it watches the
operator's receiving wallet for a recent incoming credit (SOL/SPL, optional
amount match), rather than the Solana Pay reference that wallets often drop.
Ships host tests, a README (threat model + prompt-injection transcript), and a
zero-LLM economical shell-cron watcher recipe.

Pre-submission audit fixes:
- solana-pay-request: cap display fields in BYTES not chars (128 emoji = 512 B
  could overflow the shim clamp and truncate the [PHOTO:] QR marker); move
  output composition into a pure-core, host-tested, bounded render_output.
- UX: unify every plugin's chat output (status glyph + one clickable link +
  plain next step); spl-transfer reframes the base64 as a how-to-sign block and
  flags an unpinned recipient; token-risk gains status glyphs + clean
  pluralization + an Explorer line; sns-resolve keeps the full owner address and
  drops the non-payable registry PDA from chat text.
- emit() logs WARN on failure across all five shims.
- sns-resolve: add a non-circular derivation regression test.
- Docs: root README reflects five plugins and 89 tests; all README worked
  examples regenerated from real execute() output; reworded dangling refs.
- Add per-plugin MIT LICENSE files for isolated-snapshot completeness.

89 host tests pass, 5/5 wasm32-wasip2 builds clean, sync-core --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A prompt-injection ("rotate the payout wallet + charge 1000") exposed two
agent-level weaknesses even though funds were never at risk (recipient pinned):

- The model offered to SPLIT an over-cap charge into smaller requests — the cap
  refusal said "exceeds cap per request", which invites splitting. Reworded to a
  firm, final refusal that instructs the model not to split, retry, or work
  around the cap.
- The reply parroted the injected "new payout wallet" as accepted. Root cause:
  the amount cap was checked before the recipient pin, so an over-cap amount
  masked the recipient rejection. The payee is now resolved FIRST, and the pin
  refusal explicitly states a "rotated wallet" claim is not honored.

Verified end-to-end through the live agent: redirect -> "pinned wallet cannot be
changed, refused"; over-cap -> firm refusal, no split offer. Host test updated to
pin the anti-split language. README injection transcript + threat model updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth plugin. Given a base58 address, returns a shaped ~200-token summary — SOL
balance, SPL token holdings (both token programs, largest first, known-mint
symbols), account type (wallet vs program-owned), and recent activity — so the
agent can answer "what's in wallet X?" without dumping raw getAccountInfo /
getTokenAccountsByOwner JSON into its context. T0: read-only, no keys, no state.

- New shared-core helper get_token_accounts_by_owner() (jsonParsed, both SPL
  Token and Token-2022, zero/dust dropped, capped, largest-first; a per-program
  RPC failure is tolerated).
- Pure core + thin wasm shim, crate-type [cdylib, rlib], waki wasm-only.
- Host tests incl. flood/injection: address length-bounded in the core, output
  bounded with a hostile many-token wallet, fabricated-balance prompt ignored.
- README (threat model + prompt-injection transcript), manifest (minimal
  http_client + config_read), MIT LICENSE. Registered in sync-core.py; vendored
  core re-synced across all plugins.
- Root + core READMEs updated: six plugins, 97 host tests.

97 host tests pass, wasm32-wasip2 builds clean, sync-core --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Emit a Solana Explorer transaction-inspector link (and a QR of it) for the
  unsigned transfer, so a human can review/simulate the exact decoded tx before
  signing. Built from the MESSAGE bytes (not the full wire tx, whose leading
  signature-count byte would make the inspector misparse it), base64 +
  percent-encoded (new shared-core helper links::explorer_inspector_url). The
  [PHOTO:...] marker is emitted first and the description forbids markdown-izing
  it, so the model relays it intact and the channel renders the QR.
  HONEST framing: a tx cannot be signed from a QR/link (that needs a hosted
  Solana Pay transaction-request endpoint a stateless T1 component can't host) —
  signing stays in the wallet/Squads/CLI from the base64. README + a host test
  pin the review-link format.
- Add waki connect_timeout(8s) to every HTTP plugin so a dead RPC fails in
  seconds instead of hanging the agent for minutes (waki 0.5 exposes only
  connect_timeout; a connected-but-slow node still needs a faster rpc_url).

98 host tests pass, all wasm builds clean, sync-core --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ncy in submission)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants