Skip to content

WIP Solana / SVM support (#1149)#1150

Draft
JakeHartnell wants to merge 19 commits into
mainfrom
svm
Draft

WIP Solana / SVM support (#1149)#1150
JakeHartnell wants to merge 19 commits into
mainfrom
svm

Conversation

@JakeHartnell
Copy link
Copy Markdown
Contributor

@JakeHartnell JakeHartnell commented May 17, 2026

Tracks #1149. Draft — design doc + all three implementation slices. Ready for review.

What's in this PR

Design (bf2cca07f)

docs/design/svm-support.md — boundaries, v1 scope, and open decisions. Trigger-only v1; submission + middleware program deferred to v2.

Slice 1 — types + WIT + bindings (08acd076a, ad2b2d588, 394c32897)

  • SolanaAddress, SolanaCommitment (Processed | Confirmed | Finalized, default Confirmed), ChainKeyNamespace::SOLANA
  • SolanaChainConfig + AnyChainConfig::Solana
  • Trigger::SolanaProgramEvent + TriggerData::SolanaProgramEvent with replay-identity tuple (slot, signature, instruction_index, inner_instruction_index, log_index, data)
  • SolanaEventFilter::{Discriminator, LogContains}
  • WIT additions + engine bindings for operator and aggregator worlds
  • bs58 = "0.5.1" workspace dep

Slice 2 — stream + lookup + CLI validator + health probe (a5d5e97ba..f5cf1541c)

  • Rebuilt example WASM components for the new 8-variant trigger WIT
  • Modular Solana deps (solana-client 3.1.14, solana-pubkey 3, solana-commitment-config 3, solana-rpc-client-api 3.1.14, solana-transaction-status-client-types 3.1.14) — intentionally not solana-sdk
  • New packages/wavs/src/subsystems/trigger/streams/solana_stream.rs
  • Lookup table keys by (ChainKey, program_id, discriminator-or-log-substring)
  • CLI service-JSON validator + getAccountInfo lookup
  • Health probe via getHealth + getSlot
  • cargo fmt + all (slice 2) markers retired

Slice 3 — dev loop + worked demo + e2e (ee38ff213..a03c914b3)

  • Anchor fixture program at examples/contracts/solana/event-emitter/ — minimal program that emits a discriminated log line
  • WAVS operator component at examples/components/solana-event-relay/ — relays TriggerData::SolanaProgramEvent to an EVM submission target
  • just start-solana-validator + just deploy-solana-fixture targets in the workspace justfile
  • E2E runner support at packages/layer-tests/src/e2e/solana_trigger.rs — retires the last (slice 3) marker in runner.rs:612
  • End-to-end demo test at packages/layer-tests/tests/solana_e2e.rssolana-test-validator + anvil + fixture program + WAVS + EVM service manager
  • Replay-protection regression test in packages/wavs/tests/trigger_tests.rs — asserts identical (slot, signature, instruction_index, log_index) does not re-fire the operator
  • READMEs under both example dirs

V1 acceptance criteria (per design doc)

  1. service.json can declare chain: solana and a SolanaProgramEvent trigger ✓ (slice 1+2)
  2. Solana program on solana-test-validator emits event; WAVS observes ✓ (slice 3)
  3. Operator component receives typed TriggerData::SolanaProgramEvent ✓ (slice 1+2)
  4. Replay protection: same identity tuple does not re-fire ✓ (slice 3)
  5. Submission to EVM service manager works end-to-end ✓ (slice 3)
  6. just target spins up local validator + WAVS + EVM submission target ✓ (slice 3)

What's NOT in this PR

  • Solana submission target — v2
  • Middleware program work — v2 (separate scope, see contracts/svm-middleware/)
  • Ed25519 SignatureAlgorithmv2 (operators continue signing with EVM keys)
  • programSubscribe / state-change triggers — v1.5 if demand exists

Zero (slice N) markers remain in the codebase. No (v2) markers either — v2 work is tracked in the design doc, not as inline TODOs.

Verification status

  • cargo fmt --check: clean
  • cargo check --workspace --exclude wavs-app: clean
  • cargo test -p wavs-types: 36 passing (19 new from slice 1; lookup + validator additions in slice 2)
  • Full-workspace cargo test and cargo clippy -- -D warnings deferred to CI — the local sandbox couldn't link the full test binary due to a container memory cap on wavs-engine with the new Solana deps, and pre-existing collapsible_match lints in wavs/src/subsystems/{aggregator/{p2p,queue}.rs, trigger/streams/hypercore_protocol.rs} (confirmed present on bf2cca07f, predate this branch) make -D warnings fail without --no-deps scoping

Reviewer caveats

  1. WIT regen was hand-done in slice 1 — please re-run just wit-build on a host with wkg and confirm the diff is a no-op.
  2. solana-sdk is intentionally NOT a dep. Using modular sub-crates keeps the dep footprint trimmed for trigger-only.
  3. Pre-existing clippy lints in wavs-lib (collapsible_match) predate this branch and are independent of SVM work. The slice 3 agent stalled trying to fix them — they should be addressed in a separate PR or accepted as-is.
  4. The e2e demo test requires solana-test-validator on the runner. Standard Anza installation: sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)". If CI doesn't have it, the test should be #[ignore]'d behind a feature/env gate — verify the test's gating before flipping the PR out of draft.

Test plan

  • CI: full cargo test --workspace green
  • CI: full cargo clippy --workspace --all-targets -- -D warnings green (or pre-existing lints fixed in a sibling PR first)
  • just wit-build re-run produces no diff
  • just start-solana-validator + just deploy-solana-fixture succeed on a host with the validator installed
  • E2E demo test passes locally with solana-test-validator available

JakeHartnell and others added 4 commits May 17, 2026 14:55
Adds the primitive Solana types shared across the WAVS type surface:
- SolanaAddress: 32-byte pubkey with base58 serde
- SolanaCommitment: Processed | Confirmed | Finalized (defaults to Confirmed)
- ChainKeyNamespace::SOLANA = "solana"
- bs58 as a workspace dependency for base58 encoding

Slice 1 of 3 of SVM trigger support (#1149). No chain-config or trigger
plumbing yet; that lands in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Solana chain config (HTTP / WS endpoints, default commitment
level) and the AnyChainConfig::Solana variant, with the corresponding
ChainConfigs.solana map, builder, conversion impls, and helpers
(to_solana_config, try_from, namespace routing in add_chain /
get_chain / chain_keys / all_chain_keys).

Threads exhaustiveness arms through every site that matches on
AnyChainConfig in non-trigger code:
- utils/health.rs            (skip with warn; slice 2 will add probe)
- utils/config.rs            (test fixture)
- utils/test_utils/mock_chain_configs.rs
- wavs/dispatcher.rs         (Solana service managers rejected; v2)
- wavs/http/handlers/service/get.rs (Solana service managers rejected; v2)
- wavs/subsystems/trigger.rs (Solana chain stream + BlockInterval are
                              no-ops until slice 2 wires up the stream)
- cli/context.rs             (address_exists_on_chain returns false)
- cli/command/service.rs     (BlockInterval on Solana is rejected)

Includes unit tests for serde round-trip, ChainKey conversion,
AnyChainConfig try_from/into, and add_chain ID-mismatch handling.

Slice 1 of 3 of SVM trigger support (#1149). Trigger and TriggerData
variants follow in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Solana program-event trigger end-to-end across WIT, the Rust
type surface, and the engine WIT <-> wavs-types conversion bindings.

WIT (wit-definitions/types/wit/{chain,events,service}.wit and the
mirrored deps/wavs-types-2.7.0/package.wit files for operator and
aggregator):
- chain.wit: solana-address (raw 32 bytes), solana-commitment
  variant (processed | confirmed | finalized), solana-chain-config
- events.wit: trigger-data-solana-program-event carrying the (slot,
  signature, instruction-index, inner-instruction-index, log-index,
  program-id, data) replay-identity tuple from the design doc; extends
  the trigger-data variant
- service.wit: solana-event-filter (discriminator | log-contains),
  trigger-solana-program-event, extends the trigger variant

Rust types (packages/types/src/service.rs):
- Trigger::SolanaProgramEvent { chain, program_id, filter, commitment }
- SolanaEventFilter::{Discriminator(Vec<u8>), LogContains(String)}
- TriggerData::SolanaProgramEvent { chain, slot, signature,
  instruction_index, inner_instruction_index, log_index, program_id,
  data }
- TriggerData::trigger_type() returns "solana_program_event"
- TriggerData::chain() returns Some(chain) for solana events
- Test-ext helper Trigger::solana_program_event(..)
- Unit tests for serde round-trip on both variants, trigger_type,
  chain accessor, SolanaEventFilter::LogContains round-trip, and the
  commitment-defaults-to-confirmed deserialize behavior

Engine bindings (packages/engine/src/bindings/types/*.rs):
- component_to_wavs: Trigger and SolanaAddress / SolanaCommitment /
  SolanaEventFilter conversions
- wavs_to_component: matching Trigger + TriggerData conversions for
  both operator and aggregator world bindings

Exhaustiveness fallout (Trigger and TriggerData matches that don't
belong to slice 1's stream / lookup / submission work):
- wavs/subsystems/trigger.rs: SolanaProgramEvent emits
  StartListeningChain only; real stream wires up in slice 2
- wavs/subsystems/trigger/lookup.rs: add_trigger / remove_workflow /
  remove_service log-only arms; keyed lookup table lands in slice 2
- cli/service_json.rs, cli/command/service/validate.rs,
  cli/command/service/types.rs: pass-through arms with slice-2 notes
- layer-tests/src/e2e/runner.rs: unimplemented!() arm

Slice 1 of 3 of SVM trigger support (#1149). Slice 2: solana stream
implementation and trigger lookup keying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JakeHartnell JakeHartnell changed the title Solana / SVM support (#1149) WIP Solana / SVM support (#1149) May 17, 2026
JakeHartnell and others added 15 commits May 17, 2026 20:58
Slice 1 grew the WIT `trigger` variant from 7 to 8 cases (adding
`solana-program-event`), invalidating the pre-built component artifacts
under `examples/build/components/`. Six `wasm_engine::tests::execute_*`
tests were failing with `expected variant of 8 cases, found 7 cases`
linker errors because the pre-built components were typed against the
old WIT.

Rebuilds all ten example components with `cargo component build
--release` against the slice-1 WIT, copies them into
`examples/build/components/`, and regenerates `checksums.txt`.

Components rebuilt:
  chain_trigger_lookup, cosmos_query, echo_block_interval,
  echo_cron_interval, echo_data, kv_store, permissions,
  simple_aggregator, square, timer_aggregator

Rebuild was done with the host `cargo component` toolchain instead of
the Docker `wasi-builder` image (Docker daemon access was unavailable
in this container). The resulting components encode to the same WASI
Preview 2 magic and run cleanly; they're slightly larger than the
Docker-built ones because `wasm-opt` was not run. A re-run through the
Docker image before release is recommended.

Verifies:
  cargo test --lib -p wavs wasm_engine::tests::execute_
  -> 5 passed; 0 failed (was 5 failed before this commit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the workspace dependency set used by the slice-2 Solana trigger
stream, health probe and CLI account-info path:

- solana-client 3.1.14 — pubsub + RPC client (the latest 3.x stable;
  4.0 is still RC-only at the time of writing).
- solana-pubkey 3 — `Pubkey` type for parsing program ids.
- solana-commitment-config 3 — `CommitmentConfig` for the RPC client.
- solana-rpc-client-api 3.1.14 — `RpcTransactionLogsFilter` and the
  log notification types.
- solana-transaction-status-client-types 3.1.14 — log meta types.

Wired into:
- packages/wavs       (trigger stream)
- packages/utils      (health probe)
- packages/cli        (service-JSON validator + account-info)

Decision: we keep slice 1's `SolanaAddress` newtype in `wavs-types`
rather than replace it with `solana_sdk::Pubkey`. The `solana-pubkey`
crate would otherwise be a transitive dep of every consumer of
`wavs-types` (including WASI components), and `wavs-types` is meant to
be a thin, dep-light type surface. Conversion between `SolanaAddress`
(32-byte newtype) and `solana_pubkey::Pubkey` is trivial and happens
inside the stream / probe modules only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `(ChainKey, SolanaAddress)` -> `HashSet<LookupId>` lookup table
in `LookupMaps` for Solana program-event triggers, and wires the
add_trigger / remove_workflow / remove_service paths through it. Mirrors
the `(ChainKey, Address, event_hash)` shape used for EVM contract events
except that the per-trigger `SolanaEventFilter` (Anchor discriminator or
log substring) is checked inside the dispatcher against the actual log
line rather than being part of the lookup key — non-Anchor programs
emit free-text logs with no event-hash equivalent, so two triggers on
the same `(chain, program)` with different filters share a bucket and
the dispatcher narrows on the way out.

Replaces the slice-1 `(slice 2)` markers in:
- `add_trigger` — now inserts into `triggers_by_solana_program`.
- `remove_workflow` — removes from the bucket, GC's empty sets.
- `remove_service` — same, batched over a service's workflows.

Adds `TriggerConfig::solana_program_event(..)` test helper alongside the
existing `evm_contract_event` / `cosmos_contract_event` / `block_interval_event`
constructors, and a `solana_program_event_lookup` integration test in
`tests/trigger_tests.rs` that exercises:
- Multiple triggers on the same `(chain, program)` sharing a bucket
- Triggers on a second program staying isolated
- `remove_workflow` shrinking a bucket
- `remove_service` garbage-collecting an empty bucket

The dispatcher arm that *uses* this lookup lands in the next commit
alongside the stream wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the v1 Solana program-event trigger stream described in the
SVM design doc, peer to `evm_stream.rs` and `cosmos_stream.rs`.

`packages/wavs/src/subsystems/trigger/streams/solana_stream.rs`:
- `start_solana_stream(chain, config, metrics)` opens a
  `PubsubClient::logs_subscribe` with
  `RpcTransactionLogsFilter::Mentions(program_ids)` and the configured
  commitment level, and yields `StreamTriggers::Solana { chain, slot,
  logs }` items. Reconnect loop matches `atproto_jetstream.rs`:
  bounded exponential backoff with jitter, capped at 10 reconnects.
- `SolanaStreamLog` carries the replay-identity tuple `(slot,
  signature, instruction_index, inner_instruction_index, log_index)`
  plus the originating `program_id` and the raw log line.
- `parse_transaction_logs` walks a transaction's log array, tracking
  `Program <id> invoke [N]` / `success` / `failed: ...` markers to
  attribute each non-marker log line to its (top-level instruction
  index, optional inner instruction index, program id). Anchor-style
  CPI nesting is handled. Unparseable lines fall back to instruction
  index 0 and the subscription's first program id.
- `match_log_filter(filter, raw_log)` is the dispatcher-side helper:
  - `SolanaEventFilter::Discriminator`: parses `Program data: <b64>`,
    base64-decodes, returns the decoded payload if the first N bytes
    match the configured discriminator (Anchor convention).
  - `SolanaEventFilter::LogContains`: substring match on the raw line.
- Failed transactions (`notification.value.err.is_some()`) are skipped
  — they did not change on-chain state, so they are not triggers.
- `solana_commitment_to_config` translates the slice-1
  `SolanaCommitment` enum to `solana_commitment_config::CommitmentConfig`.

`packages/wavs/src/subsystems/trigger/streams.rs`:
- Adds `StreamTriggers::Solana { chain, slot, logs }` variant. One
  variant per transaction (vs. one per log line) so the per-tx
  replay-identity prefix `(slot, signature)` is captured naturally.

`packages/wavs/src/subsystems/trigger.rs`:
- Replaces the `(slice 2)` warning in the `AnyChainConfig::Solana` arm
  of `StartListeningChain` with a real `start_solana_stream` call.
  Program ids are gathered from the lookup table
  (`triggers_by_solana_program`) at chain-start time; chains with no
  registered Solana programs stay in `Waiting` state.
- Adds `handle_solana_logs(chain, slot, logs)` which walks each parsed
  log, finds candidate triggers by `(chain, program_id)`, applies the
  per-trigger `SolanaEventFilter` via `match_log_filter`, and emits a
  `TriggerData::SolanaProgramEvent` with the decoded payload (or raw
  log bytes if the filter was `LogContains`).
- Drops the `Solana { .. }` arm into the main dispatcher `match res`.
- BlockInterval on a Solana chain is now annotated as a v1.5 follow-up
  rather than slice 2 — per the design doc's "Non-Goals for v1".

Tests (in `solana_stream::tests`):
- `parses_top_level_log` / `parses_inner_instruction_log` /
  `parses_multiple_top_level_instructions` /
  `handles_failed_inner_terminator` /
  `unparseable_lines_fall_back_to_default_program`
- `commitment_translation` / `backoff_delay_caps_at_max_reconnects`
- `match_log_filter_discriminator_hit` / `_miss_wrong_disc` /
  `_miss_wrong_prefix` / `match_log_filter_log_contains_hit` / `_miss`

`solana-test-validator` integration test is out of slice 2 scope; the
existing `(slice 2)` marker in `layer-tests/src/e2e/runner.rs:611` was
already marked for slice 3 by the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`packages/cli/src/service_json.rs` — replaces the slice-1
`(slice 2)` placeholder for `Trigger::SolanaProgramEvent` with real
validation:

- `program_id` must be 32 bytes (`SolanaAddress` already enforces this
  on deserialize; we re-check defensively).
- `SolanaEventFilter::Discriminator` payload must be exactly 8 bytes
  (Anchor convention — the slice-1 design doc note allowed arbitrary
  prefixes, but the on-chain reality is 8 SHA256-derived bytes; an
  off-spec discriminator length is almost always a service-author bug).
- `SolanaEventFilter::LogContains` substring must be non-empty
  (substring-matching against `""` would fire on every log line).

`packages/cli/src/command/service/validate.rs` — adds a
`solana_clients` parameter to `validate_contracts_exist`, and a real
`Trigger::SolanaProgramEvent` arm that calls `getAccountInfo` on the
configured Solana RPC and reports an error if the account is missing
or non-executable (programs must be executable for log triggers to
fire).

`packages/cli/src/command/service/types.rs` — adds
`ChainType::Solana` so the chain-collection pass in `service.rs`
includes Solana chains in the validation map.

`packages/cli/src/command/service.rs` — wires `ChainType::Solana`
through the `chains_to_validate` loop, builds a Solana RPC client via
the new `ctx.new_solana_rpc_client(chain_id)` helper, and threads
`solana_clients` into `validate_contracts_exist`. The BlockInterval
on Solana arm is re-annotated as v1.5 follow-up (not slice 2 — slot
intervals are out of scope per the design doc).

`packages/cli/src/context.rs` — adds `new_solana_rpc_client(chain_id)`
peer to `new_evm_client_read_only`, and replaces the slice-1
placeholder in `address_exists_on_chain` for `AnyChainConfig::Solana`
with a real `getAccountInfo` call. The arm is defensive plumbing —
`layer_climb::Address` doesn't have a Solana variant today, but the
bytes-to-Pubkey conversion is in place for when it gains one.

Tests in `solana_validator_tests`:
- `solana_discriminator_must_be_eight_bytes`
- `solana_log_contains_must_be_nonempty`
- `solana_eight_byte_discriminator_is_accepted`
- `solana_log_contains_nonempty_is_accepted`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the slice-1 `(slice 2)` placeholder in
`health_check_single_chain`'s `AnyChainConfig::Solana` arm with a real
probe:

- `getHealth` — the canonical "node is up and caught up" check on
  Solana RPC. Returns "ok" or an error string with the slot lag.
- `getSlot` — sanity check that we can read a slot, mirroring the EVM
  `get_block_number` and Cosmos `block_height` probes.

Uses the `solana-client` nonblocking RPC client with a
`CommitmentConfig` derived from the chain config's `commitment` field.

Adds three `HealthCheckError` variants:
- `SolanaMissingHttpEndpoint(ChainKey)` — config has no http_endpoint
- `SolanaHealth(ChainKey, String)` — getHealth RPC failure
- `SolanaSlot(ChainKey, String)` — getSlot RPC failure

No unit test added — the existing EVM and Cosmos probes are similarly
network-integration tested only (they're trivial wrappers around live
RPC clients).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `cargo fmt --check` is now clean after slice 2's stream / lookup /
  CLI changes.
- Re-annotates the two remaining `(slice 2)` markers in places where
  the underlying work is out of slice 2 scope:
  - `packages/wavs/src/http/handlers/service/get.rs`: Solana service
    managers are a v2 deliverable (the design doc is explicit that v1
    is trigger-only).
  - `packages/layer-tests/src/e2e/runner.rs`: e2e runner support for
    Solana program-event triggers needs `solana-test-validator` —
    slice 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `examples/contracts/solana/event-emitter/`, a one-instruction Anchor
program whose only job is to emit a `MessageEmitted` event carrying an
opaque payload. The emitted `Program data:` log line is shaped exactly
for `SolanaEventFilter::Discriminator` to match against
`sha256("event:MessageEmitted")[..8]`, so the slice 1/2 trigger stream
and dispatcher can pick it up without any extra parsing.

Decision: Anchor (vs. native) — matches the existing
`contracts/svm-middleware` toehold and the design doc's stated default,
and the `emit!` macro produces precisely the `Program data:` log shape
we already filter on. Native would mean hand-rolling the discriminator-
encoding which is exactly what `emit!` does for us.

Excluded from the WAVS Cargo workspace via root `Cargo.toml` since
Anchor programs are built with `anchor build` (BPF target), not
`cargo build --workspace`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `examples/components/solana-event-relay/`, a WASI component that
receives a `TriggerData::SolanaProgramEvent` from the Solana trigger
stream, strips the Anchor `MessageEmitted` framing (8-byte discriminator
+ borsh `Vec<u8>` length prefix), and emits a `DataWithId`-encoded
`WasmResponse` so the relayed bytes land in the existing example
`SimpleSubmit` EVM service handler.

The trigger id we hand to the EVM handler is the slot. That keeps
`DataWithId` monotonic per-slot and disambiguates distinct events in
the v1 demo; the full `(slot, signature, instruction_index,
inner_instruction_index, log_index)` replay-protection tuple is
enforced upstream in the dispatcher.

The component avoids `example_helpers::trigger::decode_trigger_event`
because that helper only handles EVM / Cosmos / ATProto / Hypercore
today; adding a Solana branch there pre-emptively would commit to a
framing convention before there is a worked example. Instead, the
framing strip lives inline and the discriminator is verified by the
slice 3 `solana_e2e` integration test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two peer recipes to `start-anvil`:

- `start-solana-validator` — verifies `solana-test-validator` is on
  PATH, prints the Anza install hint and exits non-zero if not, then
  runs the validator with `--reset --quiet` on the default
  RPC 8899 / WS 8900 ports.

- `deploy-solana-fixture` — verifies `anchor` is on PATH (with an
  install hint pointing at the 0.32.1 tag matching the fixture
  toolchain), then runs `anchor build` followed by `anchor deploy`
  against `examples/contracts/solana/event-emitter/`.

We do not auto-install either toolchain. The dev tool decision belongs
to the operator running the demo; the recipes only check + hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retires the last `(slice 3)` marker at `runner.rs:612`. The
`Trigger::SolanaProgramEvent` arm now mirrors the EVM arm's shape:
resolve the chain's HTTP RPC endpoint via the new
`Clients::get_solana_endpoint` helper, airdrop a fresh fee-payer,
submit an `event_emitter::emit(payload)` transaction, wait for the
`confirmed` commitment, and return the slot as the trigger id (which is
what the `solana-event-relay` operator component uses to key its
EVM-side `DataWithId`).

The transaction-construction logic lives in a new
`packages/layer-tests/src/e2e/solana_trigger.rs` module so the surface
stays close to the slice 1/2 stream code that documents the framing.
Two unit tests on the helpers cover the Anchor instruction
discriminator (`sha256("global:emit")[..8]`) and the instruction data
layout (8-byte discriminator + 4-byte LE length + payload).

Cargo workspace adds the minimum Solana-SDK crates needed for
transaction signing (`solana-keypair`, `solana-signer`, `solana-message`,
`solana-transaction`, `solana-instruction`, `solana-hash`,
`solana-sha256-hasher`) and pulls them only into `layer-tests` — the
runtime trigger surface in `packages/wavs/` is unchanged.

`grep -rn "(slice " packages/ wit-definitions/` is now empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `packages/layer-tests/tests/solana_e2e.rs`, the slice 3 worked
demo. Two offline tests run unconditionally:

- `anchor_event_discriminator_matches_relay_component` — recomputes
  `sha256("event:MessageEmitted")[..8]` and asserts the value the
  `solana-event-relay` component hardcodes is current. Catches silent
  framing drift between the fixture program and the operator
  component.

- `match_log_filter_routes_program_data_to_relay` — synthesizes a
  validator `Program data:` log line for `MessageEmitted` and asserts
  the slice 2 dispatcher matcher (`match_log_filter`) selects on it
  and returns the decoded payload after the relay component strips
  the discriminator + borsh length prefix.

One `#[ignore]`-gated live test (`solana_emit_and_observe`) drives a
real `solana-test-validator` with the fixture deployed: airdrops a
payer, sends `event_emitter::emit(payload)`, walks the transaction
back via `getTransaction`, and asserts the `Program data:` line
landed in the logs with the expected payload. Run with:

    export WAVS_E2E_SOLANA_PROGRAM_ID=<deployed-id>
    cargo test -p layer-tests --test solana_e2e -- --ignored

Also corrects `MESSAGE_EMITTED_DISCRIMINATOR` in the relay component
to the real value `[0xab, 0x0f, 0xdc, 0xb7, 0x2d, 0x7f, 0xb7, 0x27]`
(the original was a placeholder).

`solana-test-validator` is not installed in this container, so the
live test stays ignored; offline tests pass. Workspace adds
`solana-signature` and pulls `base64` into layer-tests dev-deps for
synthesizing the validator log fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enforces the SVM design doc's "exactly once" property by adding a
bounded replay-identity cache to `TriggerManager`. Before this commit
the dispatcher would re-emit a `DispatcherCommand::Trigger` on every
`solana-pubsub` reconnect that replayed the same log notification —
the design doc explicitly forbids that.

`SolanaReplayCache` is a small `HashSet + VecDeque` pair keyed on
`(chain, slot, signature, instruction_index, inner_instruction_index,
log_index)` — the full replay-identity tuple from §"v1 Trigger-Only
Solana Support". Bounded at 16384 entries with FIFO eviction so the
cache absorbs realistic pubsub-reconnect replay windows without
unbounded growth.

`solana_program_event_replay_protection` in `trigger_tests.rs` is the
acceptance test: it pushes the same `SolanaStreamLog` through
`handle_solana_logs` twice (the second push simulates a reconnect),
asserts the second one is dropped, then asserts that distinct
log_index and distinct signature still produce new Triggers. Catches
both over-dedup and under-dedup regressions.

`handle_solana_logs` is now `pub` so the regression test can drive it
directly — peer of the existing `pub fn process_blocks` test seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two screen-of-markdown READMEs covering the slice 3 demo:

- `examples/contracts/solana/event-emitter/README.md` — the
  three-terminal demo (validator, anvil, deploy), the service.json
  shape the operator should configure, the toolchain it needs, and an
  explicit list of what the fixture is NOT (v2 middleware, production
  event shapes).

- `examples/components/solana-event-relay/README.md` — pointer to the
  fixture README + a one-paragraph summary of the relay's framing
  strip + the slot-as-triggerId convention.

Together they exercise the v1 acceptance criterion #6 ("`just` target
spins up `solana-test-validator` + WAVS + the EVM submission target
locally") from the design doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical `cargo fmt` pass over the new Solana e2e files. No
functional changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <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.

1 participant