Skip to content

feat(apis/vara-eth): 0.5.0-rc.0 — helpers, streams, typed errors - #2483

Open
ukint-vs wants to merge 12 commits into
mainfrom
vs/vara-eth-wallet-phase-0-1
Open

feat(apis/vara-eth): 0.5.0-rc.0 — helpers, streams, typed errors#2483
ukint-vs wants to merge 12 commits into
mainfrom
vs/vara-eth-wallet-phase-0-1

Conversation

@ukint-vs

@ukint-vs ukint-vs commented May 15, 2026

Copy link
Copy Markdown
Member

Bumps @vara-eth/api 0.4.0 → 0.5.0-rc.0. Lands the lib-side primitives a full-featured wallet — dApp or CLI — needs, without breaking the adapter-shaped contract. The lib still doesn't hold keys; consumers pass an ITransactionSigner and pick their rail.

What this gives consumers

Two signer rails, one API

// dApp (browser): MetaMask / WalletConnect / any EIP-1193 wallet
import { walletClientToSigner } from '@vara-eth/api';
const signer = walletClientToSigner(walletClient);

// CLI / agent: local key, V3 keystore handled at the wallet layer (out of this PR)
import { LocalSigner } from '@vara-eth/api';
const signer = new LocalSigner(privateKey, publicClient);

// Same call from here on, either rail:
await api.programs.deploy(wasmBytes, { signer, executableBalance: 10n ** 18n });
await api.programs.sendAndWait(mirror, payload, { signer, via: 'eth' });

The lib never sees a private key on the dApp rail; on the CLI rail the key custody choice lives entirely in the consumer (keystore, mnemonic, Ledger — all out of scope here by design).

Helpers that collapse multi-step ceremonies

  • api.programs.deploy(code, opts) — WVARA permit → requestCodeValidation → bounded CodeGotValidated wait → fresh executable-balance permit (if requested) → createProgram* variant pick → result.
  • api.programs.sendAndWait(mirror, payload, opts) — on-chain Mirror.sendMessage (default via: 'eth') or off-chain injected via injected_sendTransactionAndWatch. Uniform {messageId, reply, txHash, validator?} result.
  • api.fees.estimate(op) — viem-backed gas + WVARA fee preview before submit.
  • api.stream.{programEvents,routerEvents,blocks}(handlers, opts?) — typed discriminated-union event streams over viem's watchContractEvent / watchBlocks.
  • extractSailsIdl{,OrThrow} — pure WASM custom-section parser, tolerates both sails_idl and sails-idl names.

Typed errors at public boundaries

VaraEthError base + named subclasses for everything callers might want to branch on: ViemForkRequiredError, InjectedTxStaleError, PromiseTimeoutError, PromiseSignatureInvalidError, PermitExpiredError, BlobUnderpricedError, CodeValidationTimeoutError, NoSailsIdlError, RpcConnectionError, ChainIdMismatchError. Internal throw sites untouched — wallet-side errors (WalletLockedError, …) stay in the CLI by design.

What changed under the hood

Phase 0 — gates + packaging

  • scripts/poc-wallet.ts runs the full ceremony against ethexe run --dev (yarn poc:ethexe). yarn typecheck:poc runs in CI on every PR so the script can't silently rot when lib types change.
  • assertViemFork() runtime check at RouterClient.requestCodeValidation* entry points only. EIP-7594 blob upload requires @vara-eth/viem@2.48.11; upstream viem trips ViemForkRequiredError with a remediation message. Read-only consumers never hit the check.
  • JS-side signing golden fixture pins preimage byte layout, keccak256 hash, blake2b messageId, and the deterministic ECDSA signature — catches EIP-191 / raw-bytes / recovery-byte drift before it ships.
  • CJS packaging fix. lib/cjs/ now ships {"type":"commonjs"} so Node doesn't read its require()-based files as ESM. Subpath require exports point at real built files. Top-level main + types added.

Phase 1 — helpers + typed errors + Sails IDL

See "What this gives consumers" above.

Phase 2 — event streams

Thin discriminated-union wrappers over viem watchers — reconnection delegated to viem.

Review history

Two independent reviews on the first push (d76776ae) — Claude eng-review + /codex review — found 6 disjoint blockers. ~10% agreement rate, because there were almost zero unit tests for the new helpers and each reviewer caught different classes of bug.

3 P1 regressions (broke on first call, found by codex):

  • sendAndWait(via:'eth') was missing tx.send() between sendMessage and setupReplyListener — the listener throws immediately without it.
  • LocalSigner cast publicClient.transport to Transport, but viem treats transport as a factory. Replaced with custom({ request: publicClient.request }).
  • viem-fork.ts did a top-level import { createRequire } from 'node:module', which poisons browser bundles since the module is reachable from the root @vara-eth/api index. Moved the import behind a Node-runtime guard with Function('return typeof require !== "undefined" ? require : null')() for opaque indirection.

3 deploy-ceremony reliability bugs (eng-review):

  • deployProgram had no timeout on waitForCodeGotValidated() — would hang forever if validators stalled. Now bounded by codeValidationTimeoutMs (default 120 s); on expiry throws CodeValidationTimeoutError carrying codeId + txHash so the caller can resume the second half out-of-band.
  • Executable-balance permit was signed in parallel with the validator wait, sharing one absolute deadline with the code-fee permit. If the wait exceeded 5 minutes (default permit window), createProgramWithExecutableBalance would silently revert. Now signed AFTER CodeGotValidated resolves, with a fresh now-based deadline.
  • InjectedTxStaleError was declared but never thrown. Now thrown from InjectedTx.setReferenceBlock(supplied) when the supplied block is outside the 32-block validity window. Transient RPC failures fall through silently; programmer errors (TypeError/RangeError) are re-thrown.

Each bug now has a fence test in test/unit/. CHANGELOG reconciled (removed stale MessageRevertedError mention, added CodeValidationTimeoutError + codeValidationTimeoutMs). Post-review /simplify pass dedup'd the Promise.race-with-timeout pattern into src/util/promise.ts#withTimeout and parallelized the staleness pre-check.

Test plan

  • CI green
  • yarn workspace @vara-eth/api typecheck:poc passes
  • yarn workspace @vara-eth/api test:unit50 tests pass (40 pre-existing + 6 fence tests for the blockers + 4 across IDL/local-signer fixtures)
  • yarn workspace @vara-eth/api build — both ESM and CJS outputs produced
  • CommonJS smoke: `node -e "const a = require('@vara-eth/api'); const v = require('viem'); new a.LocalSigner('0x' + 'ab'.repeat(32), v.createPublicClient({transport: v.http('http://x')}));"\` — does not throw
  • Manual: yarn workspace @vara-eth/api poc:ethexe against ethexe run --dev

Notes for reviewers

  • New public surface in 0.5.0-rc.0: LocalSigner, privateKeyToLocalSigner, WalletClientAdapter, walletClientToSigner, ProgramsNamespace, FeesNamespace, StreamNamespace, all typed errors, extractSailsIdl{,OrThrow}, watchProgramEvents/watchRouterEvents/watchBlocks, buildEventMeta.
  • Validator targeting in sendAndWait defaults to broadcast (zero-address recipient); pool round-robin is opt-in only — slot-based selection is heuristic and worse than broadcast under clock skew / reorgs / validator-set transitions.
  • Out of scope by design: keyring, V3 keystore, mnemonic, HD derivation, Ledger, Sails typed-client codegen, operator/validator mode, cross-chain "deploy to both" workflow. These live in (or arrive with) the wallet CLI consumer, not this lib.

🤖 Generated with Claude Code

ukint-vs and others added 5 commits May 16, 2026 01:47
Land the upstream-library half of the Vara.eth wallet plan:

Phase 0
- scripts/poc-wallet.ts — end-to-end Phase 0 gate script.
  Run with `yarn poc:ethexe --eth ... --ws ... --router ... --key ... --wasm ...`
  against a local `ethexe run --dev` devnet. Performs:
  upload -> create -> send-injected -> wait promise -> print reply.
  Not CI-gated (no Anvil/ethexe in CI); `yarn typecheck:poc` covers
  type-level drift against the lib surface.
- src/util/viem-fork.ts — `assertViemFork()` runtime probe invoked at
  the entry of `RouterClient.requestCodeValidation*` paths (and
  `prepareAndSignRequestCodeValidationPermitData`). Read-only consumers
  never trigger the check. Throws `ViemForkRequiredError` with a clear
  remediation message when the installed viem isn't the
  `@vara-eth/viem` fork.
- test/unit/injected-signing.fixture.test.ts — P0c golden fixture.
  Pins preimage byte layout, keccak256 hash, blake2b messageId, and the
  deterministic ECDSA signature against known inputs (Anvil #0 key,
  zeroed value, fixed reference block + salt). Locks `InjectedTx` byte
  layout against drift from the Rust verifier in
  `ethexe/common/src/injected.rs`.

Phase 1 (upstream helpers, narrow per CEO/eng review)
- src/api/programs/{deploy,send-and-wait,index}.ts — `api.programs.deploy`
  and `api.programs.sendAndWait`. The deploy helper covers WVARA permit
  signing, requestCodeValidation, CodeGotValidated wait, and the right
  createProgram* variant. sendAndWait supports both eth and injected
  rails behind `options.via`, returning a uniform shape with parsed
  `ReplyCode` regardless of path.
- src/api/fees/{estimate,index}.ts — `api.fees.estimate(op)` for
  sendMessage / sendReply / claimValue / uploadCode / createProgram.
- src/errors/vara-eth-error.ts — typed-error taxonomy at PUBLIC API
  boundaries. Wallet-only errors stay in vara-wallet by design.
- src/signer/adapters/local.ts — `LocalSigner` + `privateKeyToLocalSigner`.
  Thin viem WalletClient wrapper, ~50 LOC. Suitable for scripts and
  CLI/agent flows; browser dApps keep using walletClientToSigner.
- src/programs/idl/extract.ts — Sails IDL extractor.
  Pure WASM custom-section parser; accepts both `sails_idl` and
  `sails-idl` naming. Returns null on missing or malformed bytes;
  `extractSailsIdlOrThrow` for strict callers.
- src/eth/ethereumClient.ts — no API change (existing `signer` getter
  is what helpers use to construct ad-hoc MirrorClients).

Bug-fix-in-passing
- Narrow `feeHistory.baseFeePerBlobGas` access in router.contract.ts
  via unknown cast. The @vara-eth/viem fork populates the field but
  upstream viem types don't declare it. Was emitting a TS warning on
  every build.

Tests: 30/30 unit pass.
Build: ESM + CJS clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep after the three review agents:

- **Tautological golden fixture (BUG)**: the P0c test computed EXPECTED_HASH
  from `preimageBytes()` then asserted `bytesToHex(keccak_256(preimageBytes()))
  === EXPECTED_HASH` — a self-consistency check, not a golden fixture. Any
  change to `preimageBytes()` would update both sides simultaneously and the
  test would still pass. Replaced with hardcoded literals for hash / messageId
  / signature / address. This is now an actual cross-impl drift gate.

- **Lying error fallback (BUG)**: `PromiseSignatureInvalidError` accepted
  `'0x0' as Address` when the validator address was unknown — a fake value
  masquerading as a real one. Made `recoveredAddress: Address | undefined`
  and pass through; error message branches on presence.

- **Reuse**: route injected path in `sendAndWaitForReply` through
  `api.createInjectedTransaction` (the single official factory) instead of
  `new InjectedTx(...)`. Pass a `CreateInjectedTransaction` callback into
  `ProgramsNamespace` to avoid a back-ref cycle to `VaraEthApi`.

- **Reuse**: import `ZERO_ADDRESS` from `util/constants.ts` and add
  `ZERO_BYTES32` there too. Drop the duplicates in `fees/estimate.ts`.

- **Dead code**: drop `MessageRevertedError` — declared, exported, never
  thrown anywhere. The pre-existing `decodeContractError` already returns a
  plain `Error` with the decoded revert reason. Re-add when we wire it up.

- **Auto-set error name**: `VaraEthError` base now sets
  `this.name = new.target.name` instead of 10 hand-written `this.name = …`
  overrides in each subclass. Removed all 9 overrides.

- **Typed error codes**: extracted `VaraEthErrorCode` `as const` map +
  derived union type. `VaraEthError.code` is now typed against the union, so
  callers' `switch (err.code)` is compile-time checked.

- **Efficiency**: in `deployProgram`, overlap the executable-balance permit
  signing with the `CodeGotValidated` wait via `Promise.all`. The two are
  independent (permit only needs amount + deadline; wait dominates with
  chain latency), so the signing round-trip happens for free.

- **Efficiency**: in `estimateFee`, parallelize `pc.getBlock` with
  `pc.estimateGas` (state-changing branches) and with the WVARA fee reads
  (uploadCode branch). Saves one RPC round-trip per call.

- **DRY**: `deployProgram` now uses a `signPermit(amount)` local closure
  instead of repeating `wvara.prepareAndSignPermitData(router.address, ...,
  deadline)` at two call sites.

- **Cleanup**: drop the four unrelated re-exports (`Address`, `Hash`, `Hex`,
  `TransactionRequest`) from `signer/adapters/local.ts`. Consumers should
  import these from `viem`.

- **Cleanup**: extract `makeNoEthClientProxy(namespace)` from `api/api.ts`'s
  inline `new Proxy(...)` for `programs`/`fees` stubs.

- **Comments**: drop the `// --- Step N:` narrative comments from
  `deploy.ts`. The function docblock already enumerates the steps.

Tests: 30/30 unit pass.
Build: ESM + CJS clean.
Typecheck: poc-wallet.ts clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add typed `api.stream` namespace wrapping viem's watchContractEvent and
watchBlocks. Three methods:

- programEvents(mirror, handlers, opts?) — emits all 14 Mirror events as a
  discriminated union (Message, Reply, StateChanged, ValueClaimed, etc.).
- routerEvents(handlers, opts?) — emits all 13 Router events.
- blocks(handlers, opts?) — emits StreamedBlockHeader on new L1 blocks.

Each event carries EventMeta (blockNumber, blockHash, txHash, txIndex,
logIndex). Re-connection and polling fallback are handled by viem internally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pre-existing packaging defects surfaced when wiring @vara-eth/api into
vara-wallet (CommonJS module mode). All three are necessary for CJS consumers
to require('@vara-eth/api') successfully.

1. Add main + types fields. Node's node10 resolution (the default for
   moduleResolution: "node") doesn't honour the `exports` field; without
   `main`, it falls back to looking for `index.js` at the package root and
   fails.

2. Write a {"type":"commonjs"} package.json into lib/cjs/ during build. The
   root package.json declares "type":"module", so Node treats every .js in
   the package as ESM by default — including the require()-based files in
   lib/cjs/. The nested package.json scopes the CJS subtree back.

3. Repoint subpath exports' `require` field to the lib/cjs/*.js files that
   actually exist. The previous entries pointed at lib/<subpath>/index.cjs
   files that the build doesn't produce.

Also re-export the signer subpath from the root so LocalSigner is reachable
via `import { LocalSigner } from '@vara-eth/api'` (matches existing pattern
where contracts/errors/programs are root-exported alongside their subpaths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract buildMeta into a shared buildEventMeta in stream/types.ts. The
  Mirror and Router decoders were carrying byte-identical copies.
- Merge WatchProgramEventsOptions and WatchRouterEventsOptions into a single
  WatchEventsOptions; both interfaces only declared `fromBlock?: bigint`.
- Drop the documented-but-unwired `onReconnect` handler from
  StreamHandlers — viem's watchContractEvent manages its own reconnection
  internally; the field was a leaky abstraction that promised behaviour the
  runtime never delivered.
- Check `eventName` before allocating `meta` so unknown events don't pay the
  cost of building a 5-field object.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the @vara-eth/api library to version 0.5.0-rc.0, introducing several major features including the api.stream namespace for typed event subscriptions, high-level program deployment and message handling via api.programs, and fee estimation through api.fees. It also adds a LocalSigner for private key operations, a Sails IDL extractor for WASM binaries, and a robust typed error taxonomy. Review feedback highlights the need for better resource cleanup in timeout-wrapped promises to prevent leaks and suggests adjustments to the block streaming logic to correctly support pending blocks by allowing nullable block numbers and hashes.

Comment on lines +155 to +167
async function withTimeout<T>(promise: Promise<T>, ms: number, txHash: Hex): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new PromiseTimeoutError(txHash, ms)), ms);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The withTimeout helper uses Promise.race, which does not cancel the underlying promise if a timeout occurs. In sendViaEth and sendViaInjected, the promises returned by waitForReply() and sendAndWaitForPromise() likely involve active event listeners or RPC subscriptions. If a timeout happens, these listeners may remain active in the background, leading to resource leaks over time. Consider implementing a cancellation mechanism using AbortController or ensuring the TxManager provides a way to clean up listeners when the wait is abandoned.

return publicClient.watchBlocks({
blockTag: options.includePending ? 'pending' : 'latest',
onBlock: (block) => {
if (block.number === null || block.hash === null) return; // pending blocks lack canonical IDs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This filtering logic prevents the includePending option from working as intended. Pending blocks typically have a null block number (and sometimes a null hash). By returning early here, the stream effectively suppresses all pending blocks even when the user explicitly requests them. This check should be adjusted to allow null values when options.includePending is enabled.

Suggested change
if (block.number === null || block.hash === null) return; // pending blocks lack canonical IDs
if (!options.includePending && (block.number === null || block.hash === null)) return;

Comment on lines +127 to +128
number: bigint;
hash: Hex;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To correctly support the includePending option in block streaming, the number and hash fields must be nullable. Pending blocks often lack these canonical identifiers until they are included in a block.

Suggested change
number: bigint;
hash: Hex;
number: bigint | null;
hash: Hex | null;

@ukint-vs
ukint-vs marked this pull request as draft May 16, 2026 08:58
Three functional regressions surfaced by `/codex review` (every helper threw
on first call) and three deploy-ceremony reliability bugs from internal eng
review. All six now fenced by unit tests in test/unit/.

Codex P1s (broken on first call):
- 1.1 sendAndWait via=eth now calls tx.send() before tx.setupReplyListener().
  Was throwing "No transaction hash available. Call send() first." every time.
- 1.2 LocalSigner routes through viem's custom({ request: publicClient.request })
  instead of casting the constructed transport. Constructor no longer crashes
  with "parameters.transport is not a function".
- 1.3 viem-fork.ts dropped the top-level `import { createRequire } from
  'node:module'` that broke browser bundles. Probe now gates on
  process.versions.node and accesses require via opaque indirection
  (Function('return require')) so bundlers don't try to resolve node:module.

Deploy-ceremony fixes:
- 2.1 deployProgram now bounds the CodeGotValidated wait with
  codeValidationTimeoutMs (default 120s). On expiry throws
  CodeValidationTimeoutError carrying { codeId, txHash, timeoutMs } so callers
  can resume by feeding codeId into createProgramBuilder out-of-band.
- 2.2 executable-balance permit is signed AFTER CodeGotValidated resolves
  with a fresh now-based deadline. The previous parallel-signing optimization
  shared one 5-min deadline across both permits and caused
  createProgramWithExecutableBalance to revert on long validator waits,
  burning code-validation fees.
- 2.3 InjectedTx.setReferenceBlock(suppliedHash) pre-checks the supplied hash
  against the chain head and throws InjectedTxStaleError if outside the
  32-block validity window. Transient RPC failure falls through to signing.

Tests: 50 passing (40 prior + 10 fence). Each fix has a unit test that fails
before the fix and passes after.

CHANGELOG: reconciled to drop the never-implemented MessageRevertedError and
StreamHandlers.onReconnect entries; added CodeValidationTimeoutError and the
new codeValidationTimeoutMs option.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ukint-vs
ukint-vs marked this pull request as ready for review May 16, 2026 09:12
@ukint-vs

Copy link
Copy Markdown
Member Author

Fix-up pushed — addresses 6 blockers from /codex review + internal eng review

Codex P1s (functional regressions, every helper threw on first call):

  • 1.1 api.programs.sendAndWait({via:'eth'}) now calls tx.send() before setupReplyListener(). The default on-chain path was throwing "No transaction hash available. Call send() first." every time.
  • 1.2 LocalSigner routes through custom({ request: publicClient.request }) instead of casting the already-constructed transport. The constructor no longer throws.
  • 1.3 @vara-eth/api is now importable from browser bundles. Dropped the top-level import { createRequire } from 'node:module' in src/util/viem-fork.ts that crashed esbuild/webpack at bundle resolution. This was a hard blocker for every dApp using MetaMask via the existing walletClientToSigner path.

Deploy-ceremony fixes (Claude eng-review):

  • 2.1 deployProgram now bounds the CodeGotValidated wait with codeValidationTimeoutMs (default 120s). On expiry throws new CodeValidationTimeoutError carrying { codeId, txHash, timeoutMs } so callers can resume out-of-band.
  • 2.2 Executable-balance permit is signed AFTER CodeGotValidated resolves with a fresh now-based deadline. Previously shared the code-fee permit's 5-min deadline → silent revert + burned code-validation fees on long validator waits.
  • 2.3 InjectedTx.setReferenceBlock(suppliedHash) pre-checks against chain head and throws InjectedTxStaleError for blocks outside the 32-block validity window. Transient RPC failure falls through.

Tests: 50 passing (40 prior + 10 fence). Each fix has a unit test that would have caught the bug.

CHANGELOG: reconciled — dropped MessageRevertedError + StreamHandlers.onReconnect mentions (never shipped); added CodeValidationTimeoutError + codeValidationTimeoutMs.

Ready for re-review.

…mmit

Dedup the `Promise.race`-with-timeout pattern duplicated across
`send-and-wait.ts` and `deploy.ts` into a single generic
`util/promise.ts#withTimeout` helper. Callers pass an error factory so the
thrown error keeps call-site-specific context (txHash, codeId).

Parallelize the staleness pre-check in `InjectedTx.setReferenceBlock` —
supplied-block lookup and head-number lookup now race via `Promise.all`,
saving one RTT on the in-window success path. Narrow the surrounding
catch so `TypeError` / `RangeError` aren't swallowed (programmer bugs
shouldn't masquerade as transient RPC failures).

Trim the `CodeValidationTimeoutError` message — the recovery procedure
moves to the JSDoc where it belongs; the message stays scannable.

Fix the misleading `permitDeadline` JSDoc on `DeployProgramOptions`: the
override applies to BOTH permits (not just the code-fee one), each signed
independently, sharing one absolute deadline only safe when the validator
wait is short.

Drop unused `log` parameter from the send-and-wait test mock factory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ukint-vs ukint-vs changed the title feat(apis/vara-eth): wallet-CLI primitives (Phase 0 + 1 + 2) feat(apis/vara-eth): 0.5.0-rc.0 — helpers, streams, typed errors May 16, 2026
ukint-vs and others added 2 commits May 16, 2026 13:55
Cross-verified PR #2483 claims against the actual ethexe sources in the
gear repo. The cryptography and protocol primitives (preimage layout,
keccak/blake2b hashing, EIP-191 path, 32-block validity window, RPC
method names, server timeout, EIP-7594 sidecar use) all matched. Three
user-facing semantics were off; fixing each:

1) `deployProgram` over-permitted by `extraFee`. The direct
   `Router.requestCodeValidation` variant charges only `baseFee`
   (Router.sol:558). `extraFee` is for `requestCodeValidationOnBehalf`
   (Router.sol:644). The helper signed the permit for `baseFee + extraFee`,
   leaking an unused allowance to the router. Now signs for `baseFee`
   only. Same correction in `api.fees.estimate({ type: 'uploadCode' })`
   and in the JSDoc on `RouterClient.requestCodeValidation`.

2) `sendAndWait({ via: 'injected', value: > 0n })` would hit an opaque
   bad-request RPC error from ethexe-rpc relay (relay.rs:47-56), which
   refuses non-zero value on the injected path. Pre-validate client-side
   and throw with a clear message before signing.

3) `SendAndWaitOptions.recipient` documented zero-address as
   "broadcast / any validator picks up." Actual server behavior at
   relay.rs:58-60 + :97-113 is "auto-route to the next-slot producer"
   via `calculate_next_producer`. Fixed the JSDoc; the runtime behavior
   was already correct.

Also clarified `PermitExpiredError` JSDoc: `Router.requestCodeValidation`
wraps `WVARA.permit()` in `try {} catch {}` (Router.sol:559), so an
expired permit doesn't revert at the permit step when the user already
has allowance — `transferFrom` failure is the real signal.

Added fence test for the value-reject path. 51/51 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete refactors from the parallel /simplify review pass over
main..HEAD. The other findings were triaged out: pre-existing code
outside this branch's scope, intentional design choices fenced by
tests, or reviewer misreads of Fix 2.2 (parallelizing the
executable-balance permit with the validator wait would re-introduce
the bug it fixes).

1) deploy.ts: extract `makePermitDeadline()` local so the
   `options.permitDeadline ?? now+5min` expression isn't written
   twice. Names the freshness intent — each permit gets `now` again
   at signing time, never a stale shared deadline. Stale class
   docstring updated.

2) stream/types.ts: extract `decodeEventHeader(log)` shared by
   `decodeProgramLog` and `decodeRouterLog`. Reorders the checks so
   `eventName` short-circuits BEFORE `buildEventMeta` runs — unknown
   logs no longer pay the metadata assembly cost.

3) injected/tx.ts: drop the third RPC `getBlock(headNumber)` in
   `setReferenceBlock`'s stale-path error construction. The error
   message now carries the head BLOCK NUMBER instead of the head
   block HASH — same diagnostic value, one fewer round-trip on the
   error path. `InjectedTxStaleError` constructor signature changed
   accordingly (was `currentBlock: Hex`, now `currentBlockNumber:
   bigint`).

51/51 unit tests pass. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ukint-vs ukint-vs self-assigned this May 16, 2026
@ukint-vs ukint-vs added the C1-feature Feature request label May 16, 2026
ukint-vs and others added 3 commits May 19, 2026 13:33
MirrorClient.sendMessage and sendReply now catch viem's ContractFunctionRevertedError
at simulation time, decode the revert selector against the Mirror + Router ABIs,
and re-throw MessageRevertedError carrying the decoded ErrorName(args) reason
plus a functionName discriminator. Bumps to 0.5.0-rc.1.

Closes follow-up #8 in vara-wallet/docs/vara-eth-followups.md — wallet consumers
can now branch on err.code === 'MESSAGE_REVERTED' instead of regex on .message.

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

C1-feature Feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant