WIP Solana / SVM support (#1149)#1150
Draft
JakeHartnell wants to merge 19 commits into
Draft
Conversation
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>
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>
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.
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, defaultConfirmed),ChainKeyNamespace::SOLANASolanaChainConfig+AnyChainConfig::SolanaTrigger::SolanaProgramEvent+TriggerData::SolanaProgramEventwith replay-identity tuple(slot, signature, instruction_index, inner_instruction_index, log_index, data)SolanaEventFilter::{Discriminator, LogContains}bs58 = "0.5.1"workspace depSlice 2 — stream + lookup + CLI validator + health probe (
a5d5e97ba..f5cf1541c)triggerWITsolana-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 notsolana-sdkpackages/wavs/src/subsystems/trigger/streams/solana_stream.rs(ChainKey, program_id, discriminator-or-log-substring)getAccountInfolookupgetHealth+getSlotcargo fmt+ all(slice 2)markers retiredSlice 3 — dev loop + worked demo + e2e (
ee38ff213..a03c914b3)examples/contracts/solana/event-emitter/— minimal program that emits a discriminated log lineexamples/components/solana-event-relay/— relaysTriggerData::SolanaProgramEventto an EVM submission targetjust start-solana-validator+just deploy-solana-fixturetargets in the workspacejustfilepackages/layer-tests/src/e2e/solana_trigger.rs— retires the last(slice 3)marker inrunner.rs:612packages/layer-tests/tests/solana_e2e.rs—solana-test-validator+anvil+ fixture program + WAVS + EVM service managerpackages/wavs/tests/trigger_tests.rs— asserts identical(slot, signature, instruction_index, log_index)does not re-fire the operatorV1 acceptance criteria (per design doc)
chain: solanaand aSolanaProgramEventtrigger ✓ (slice 1+2)solana-test-validatoremits event; WAVS observes ✓ (slice 3)TriggerData::SolanaProgramEvent✓ (slice 1+2)justtarget spins up local validator + WAVS + EVM submission target ✓ (slice 3)What's NOT in this PR
contracts/svm-middleware/)SignatureAlgorithm— v2 (operators continue signing with EVM keys)programSubscribe/ state-change triggers — v1.5 if demand existsZero
(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: cleancargo check --workspace --exclude wavs-app: cleancargo test -p wavs-types: 36 passing (19 new from slice 1; lookup + validator additions in slice 2)cargo testandcargo clippy -- -D warningsdeferred to CI — the local sandbox couldn't link the full test binary due to a container memory cap onwavs-enginewith the new Solana deps, and pre-existingcollapsible_matchlints inwavs/src/subsystems/{aggregator/{p2p,queue}.rs, trigger/streams/hypercore_protocol.rs}(confirmed present onbf2cca07f, predate this branch) make-D warningsfail without--no-depsscopingReviewer caveats
just wit-buildon a host withwkgand confirm the diff is a no-op.solana-sdkis intentionally NOT a dep. Using modular sub-crates keeps the dep footprint trimmed for trigger-only.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.solana-test-validatoron 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
cargo test --workspacegreencargo clippy --workspace --all-targets -- -D warningsgreen (or pre-existing lints fixed in a sibling PR first)just wit-buildre-run produces no diffjust start-solana-validator+just deploy-solana-fixturesucceed on a host with the validator installedsolana-test-validatoravailable