Skip to content

Release gate: v0.40-dev → v0.40 (waits for cross-repo E2E)#347

Open
MuncleUscles wants to merge 44 commits into
v0.40from
v0.40-dev
Open

Release gate: v0.40-dev → v0.40 (waits for cross-repo E2E)#347
MuncleUscles wants to merge 44 commits into
v0.40from
v0.40-dev

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

Standing release-gate PR: v0.40-devv0.40

This is a long-lived gate PR, not a normal feature PR. Do not merge ad-hoc.

Purpose

  • v0.40-dev is the active integration branch for the v0.6 fee work. Individual PRs merge into v0.40-dev and only need to pass normal repo CI — they are not blocked on the full cross-repo release train being green.
  • This PR is the single point where v0.40-dev is promoted to v0.40. It waits for cross-repo E2E (the release train) to go green before it is merged.
  • Other projects in the release train test against v0.40-dev.

How to use

  • Keep this PR open as the running view of "what would ship to v0.40".
  • Merge only when the full release-train E2E is green.

(Mirrors genlayerlabs/genlayer-consensus#1070.)

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbe422b0-1c5f-47f0-bb04-bc374d27caad

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v0.40-dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

MuncleUscles and others added 15 commits June 10, 2026 13:14
…he leader's execution result (#346)

* fix(contracts)!: require consensus acceptance for success, not just leader execution result

- assertSuccessfulExecution now requires status ∈ {ACCEPTED, FINALIZED} via the
  SDK's isSuccessful in addition to FINISHED_WITH_RETURN: txExecutionResult is
  the LEADER's result and exists even on UNDETERMINED — previously an
  undetermined deploy printed 'deployed successfully' with an address that
  would never materialize
- failure messages now name both the consensus outcome and the leader result
  ('decided as UNDETERMINED (no validator majority); leader execution result:
  FINISHED_WITH_RETURN')
- map VoteType 3 (TIMEOUT) / 4 (NONDET_DISAGREE) with real diagnoses instead of
  'did not expose an execution result'
- deploy/write use waitUntil: 'decided' (new SDK API) and report the reached
  consensus status; explicit fee deposits are echoed before sending
- callKey derivation deduplicated: import from genlayer-js (incl. new
  DEPLOY_CALL_KEY for deploy-targeted Mode-2 allocations)

* build: pin genlayer-js to the v2 feature branch until the npm release

npm 1.1.8 lacks the new fee APIs (isSuccessful, DEPLOY_CALL_KEY, waitUntil),
so CI cannot build against it. github:genlayerlabs/genlayer-js#feat/
v06-fee-estimation-rework builds via the SDK's new prepare script (verified:
fresh install produces dist + exports). Re-pin to the npm semver once
genlayer-js v2 ships (release-sequencing item).

* ci: run validate-code on pushes to the v0.40 line

Push trigger was pinned to v0.39 — merges to v0.40-dev/v0.40 got no
post-merge CI.

* ci: make codecov upload non-blocking

The OIDC upload fails on ubuntu/windows independent of this PR (#345 merged
with identical failures); tests/build stay blocking.

* build: re-pin genlayer-js to v2-dev (feature branch merged)
* docs: add branching guide

* ci: harden testnet smoke timeout
* feat: support fee profiles in contract commands

* test: make fee profile deploy test portable
* feat: add staking validators discovery

* feat: epoch-aware validator listing with below-min indicator

* fix(staking): account-less client for read-only staking queries

getReadOnlyStakingClient threw 'Account not found' on fresh installs;
listings and other reads don't need a local account.
* feat: add vesting commands

* feat(vesting): validator subcommands — create/join, deposit, exit, claim, operator-transfer, set-identity, list/status

Drives the CON-607 Vesting.sol validator leg through the SDK's named
vestingValidator* actions; list/status enumerate getValidatorWallets
with per-wallet deposited principal.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Selecting Gemini during `genlayer init` failed with:
  Requested providers '{'geminiai'}' do not match any stored providers.

The selected provider id is forwarded verbatim to
sim_createRandomValidators, but the backend's llm_provider table stores
Gemini as "google". Rename the provider id geminiai -> google so it
matches. Display name ("Gemini") and env var (GEMINI_API_KEY) are
unchanged.

Since "geminiai" never resolved to a valid provider, no working
configuration relied on it.

Fixes #271

Co-authored-by: Edgars Nemše <edgars@genlayerlabs.com>
The sync-docs workflow rsynced the generated category-based
docs/api-references/_meta.json into genlayer-docs and then immediately
overwrote it with a hardcoded heredoc containing the pre-grouping flat
command list (init, up, deploy, ...). Those keys no longer match the
directory layout, so the genlayer-docs sidebar rendered broken entries
on every sync (fixed manually in genlayer-docs#426; this removes the
cause).

Also make the generated root meta complete:
- add "index": "Overview" for the generated index.mdx
- append ungrouped top-level commands (estimate-fees, finalize,
  finalize-batch) so they get explicit nav entries instead of relying
  on Nextra's implicit append

Snapshot under docs/api-references regenerated against current main
(picks up the new estimate-fees command and latest help text).

Co-authored-by: Albert Castellana <albert@genlayer.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Edgars <edgars@entropicsolutions.io>
getSlashingAddress() was removed from the genlayer-js SDK (v0.39+/v2-dev),
causing `genlayer staking validator-history` to crash with
`client.getSlashingAddress is not a function` before any history is fetched.

Resolve the idleness (slashing) contract address dynamically via viem
readContract against consensusMainContract.getIdlenessAddress(), falling
back to the staking contract address if resolution fails so reward events
still display.

Port of #344 (by @ygd58) from the dead v0.39 line to v0.40-dev.
Supersedes #344.

Fixes #341

Co-authored-by: Edgars <edgars@entropicsolutions.io>
)

* feat(network): custom network profiles with deployment-file import

genlayer network add <alias> --base <built-in> [--deployment <json>]
[--rpc <url>] [--consensus-main|--consensus-data|--staking|--fee-manager
|--rounds-storage|--appeals <addr>] [--chain-id <n>] [--deployment-key <path>]

Profiles persist as base + address overrides only; resolveNetwork loads
the base chain fresh from genlayer-js and applies overrides, so ABIs
never go stale. network set/list/info/remove and StakingAction --network
accept custom aliases. The consensus deployments.json shape is parsed by
walking the tree for ContractName->address leaves (ConsensusMain,
ConsensusData, GenStaking/Staking, FeeManager, Rounds/RoundsStorage,
Appeals); flags take precedence over the file. Adds a prepare script so
npm install from a git ref builds dist. Verified: 576 vitest tests, full
manual smoke (add/list/set/info/remove with a deployment file).

* chore(deps): bump genlayer-js to v2-dev tip for vesting actions

The locked v2-dev SHA (28e99fbc) predates the vesting client actions;
vestingValidatorJoin and friends land at 666d1156. Verified live:
vesting validator create succeeds against a #1162-branch consensus
deployment.

* docs(cli): regenerate API references; fix option placeholder regex

The docs generator's option regex only matched <word> placeholders, so
flags with dots or hyphens in the value name (--base <built-in-alias>,
--deployment <path.json>, --deployment-key <dot.path>) were silently
dropped from the options tables. Widen to <[^>]+> and regenerate: adds
the network add/remove pages and the previously undocumented vesting
command section (validator create/deposit/exit/claim, operator-transfer,
set-identity, delegate/undelegate/claim/withdraw/list).

* fix(vesting): resolve validator wallet address in create output

The join receipt does not carry the new wallet address, so the output
printed validatorWallet: undefined. Read getValidatorWallets from the
vesting contract after the join and report the newest entry. Verified
live against a #1162-branch deployment.
* fix: make git install build script self contained

* chore: refresh genlayer-js lockfile

* fix: make keychain dependency optional

* fix: restore git prepare build

* fix: include build dependency for git installs

* chore: keep esbuild as dev dependency
* ci: add clarke cli tarball release

* ci: publish prereleases to npm dist tags
…367)

* feat(staking): browser-wallet signing for validator-join and wizard

Add a dependency-free localhost bridge (node:http) that lets a browser
wallet (MetaMask / any injected window.ethereum) sign-and-broadcast the
validator-join and wizard-identity transactions.

Design: raw-viem bridge, not an SDK account. genlayer-js executeWrite
requires account.signTransaction (sign -> sendRawTransaction), which
MetaMask cannot satisfy (it only does eth_sendTransaction). The CLI
encodes calldata itself, a localhost page eth_sendTransactions it, and
the CLI waits for the receipt via a viem publicClient (reusing the
glHttpConfig id!=0 quirk) and decodes ValidatorJoin for the wallet.

New: src/lib/wallet/{stakingTx,bridgePage,browserBridge}.ts. Flag
--wallet <keystore|browser> on validator-join and wizard, with hard
errors when combined with --password/--account. Only the join + identity
sends route through the bridge; operator keystore generation/export and
config summary are unchanged. Other commands can adopt later via the
getBrowserWalletSession seam.

* feat(wallet): shared browser-wallet plumbing + all staking writes

Generalize the #367 browser-wallet bridge into a reusable signing layer and
adopt it across every staking write command.

- Extract StakingAction.getBrowserWalletSession into a shared
  src/lib/wallet/browserSend.ts (openBrowserWalletSession); add the Lane B
  eip1193Provider shim (eth_sendTransaction -> bridge; eth_chainId/eth_accounts
  local) and setNextLabel. Owns glHttpConfig.
- Grow stakingTx.ts into txBuilders.ts (generic buildTx + encodeExtraCid);
  stakingTx re-exports for back-compat.
- Add shared addWalletModeOption registrar (--wallet <keystore|browser>).
- BaseAction: isBrowserWallet, assertWalletFlags, getBrowserSession/
  closeBrowserSession, and getClient browser mode (Lane B, skips keystore).
- Widen bridge protocol (gas/type pass-through; page drops nonce/chainId).
- Adopt --wallet browser on the 9 remaining staking writes + prime-all;
  refactor validator-join/wizard onto the shared registrar.

Tests: txBuilders, browserSend (incl. eip1193 shim), bridge gas/type
pass-through, per-command keystore-untouched + output parity + flag conflicts.
Docs regenerated.

* feat(vesting): browser-wallet signing for all vesting writes

Adopt --wallet browser (Lane A) across all 12 vesting write commands via
buildTx(VESTING_ABI, vesting, fn, args). Beneficiary resolves to the connected
wallet address; the vesting lookup runs on the account-less read-only client.

BREAKING (deprecated-flag rename): the deprecated --wallet <address> alias on
'vesting validator ...' subcommands is renamed to --validator-wallet <address>
so --wallet is free for the signing-mode flag. The recommended positional
argument form is unaffected. Internally the resolved positional is carried as
'walletAddress' (VestingConfig gains wallet mode + validatorWallet alias).

Tests: vesting action browser-mode (routes through session, keystore client
untouched, output parity) + command parsing incl. the --validator-wallet
rename. Docs regenerated.

* feat(contracts): browser-wallet signing for deploy and write (Lane B)

Add --wallet browser to 'deploy' (incl. deploy-scripts mode) and 'contracts
write'. Sets walletModeOverride so BaseAction.getClient builds the genlayer-js
client with {account: <address>, provider: eip1193 shim}; the SDK's existing
json-rpc-account branch builds the addTransaction tx, deposits fees, waits, and
extracts the GenLayer txId. No genlayer-js changes. closeBrowserSession in
finally; setNextLabel for a friendly wallet prompt.

Tests: createClient wired with {account, provider} and getAccount never called
in browser mode; command parsing. Docs regenerated.

* feat(transactions): browser-wallet signing for appeal and finalize (Lane B)

Add --wallet browser to 'transactions appeal', 'finalize', and 'finalize-batch'.
These already funnel through BaseAction.getClient, so browser mode is the same
Lane B client construction + closeBrowserSession in finally; the SDK's consensus
provider branch does the send. Docs regenerated.

* feat(wallet): session descriptor + shared timing constants

Foundations for the persistent connect-once browser-wallet session:

- sessionConstants.ts: single source of truth for heartbeat/TTL/timeout
  budgets (LONG_POLL, HEARTBEAT_DEAD, TAB_DEAD_GRACE, IDLE_TTL,
  DAEMON_READY, CONNECT/TX timeouts) plus descriptor/log filenames and
  the walletMode/walletSessionTtlMinutes config keys.
- sessionDescriptor.ts: on-disk descriptor (~/.genlayer/wallet-session.json)
  written atomically at 0600; read+schema-validated (bad JSON/version -> null);
  isPidAlive() cheap first-gate (signal 0, EPERM = alive).

Descriptor unit tests: atomic 0600 write, round-trip, garbage/version
rejection, idempotent remove, pid-alive.

* feat(wallet): persistent daemon mode on the browser bridge

Add an opt-in persistent mode so one long-lived bridge can broker txs for
many CLI processes:

- New client routes (token-authed): GET /api/ping, GET /api/state,
  POST /api/enqueue, GET /api/tx?id=, POST /api/shutdown. Page routes
  (/api/connected, /api/result) keep the strict Origin check; client routes
  are Origin-exempt (a Node client sends no Origin; token-in-header forces a
  CORS preflight the server never satisfies).
- Result store so remote HTTP callers can poll a tx to done/sent/rejected/error
  (GC 10min after completion); enqueue returns 409 wallet-not-connected/tab-closed
  for fail-fast.
- Heartbeat: lastPagePollAt stamped on every /api/next; getState() exposes it.
- onConnected / onActivity / onShutdown callbacks for the daemon.
- Shared serializeBridgeTx/parseBridgeTx pair (client and server can't drift);
  serializeTx now reuses it. LONG_POLL_MS sourced from sessionConstants.

Existing bridge tests unchanged; +11 persistent-mode tests (auth, enqueue
round-trip, rejected/error, Origin exemption vs page 403, heartbeat, FIFO,
shutdown, 404 on non-persistent).

* refactor(wallet): split bridge transport; add remote session + client

Separate "how a tx reaches the wallet" from the shared signing lanes so a
command can run over either an in-process bridge or a remote daemon:

- BridgeTransport seam; buildBrowserSession(transport, ...) holds the shared
  preflight + receipt-wait (Lane A), EIP-1193 shim (Lane B), and labels
  verbatim. BrowserSession gains kind/sessionUrl; bridge is now optional
  (present only for local sessions).
- openBrowserWalletSession re-based on a LocalBridgeTransport (existing tests
  pass unchanged). New openRemoteWalletSession wraps a RemoteSessionTransport
  whose close() is a no-op so one failed preflight never kills a shared session.
- WalletSessionClient: fetch-only client for a running daemon (ping/state/
  enqueueTx/waitForTxResult/waitForConnection/shutdown), token on every call,
  fail-fast on stale page heartbeat, 409 -> clear reconnect messages.

sessionClient tests drive a real in-process persistent bridge with a
fetch-simulated page (openUrl mocked); browserSend tests unchanged.

* feat(wallet): session daemon runtime, detached spawn, and resolver

- sessionDaemon.ts: runWalletSessionDaemon owns the bridge + tab + descriptor
  lifecycle. Writes the descriptor only after listen succeeds; rewrites address
  onConnected; bumps lastUsed (throttled) onActivity. Self-terminates on idle
  TTL, tab-dead heartbeat loss, connect timeout, signal, or fatal error, and on
  /api/shutdown -- ALWAYS removing the descriptor first. Deterministic shutdown:
  the bridge's onShutdown fires an ordered teardown (remove descriptor -> close
  bridge -> exit); the keep-alive timer is NOT unref'd, so the daemon only ever
  exits through cleanupAndExit (never by the loop draining after the socket
  closes) -> the "daemon gone => descriptor removed" invariant always holds.
- spawnDaemon.ts: spawnWalletDaemon re-execs the bundled CLI (process.execPath +
  argv[1] + "wallet daemon"), detached + unref, stdio -> 0600 logfile; NO token
  on argv. waitForDaemonReady polls descriptor+pid+/api/ping, surfaces the log
  tail on timeout.
- sessionResolver.ts: resolveBrowserWalletSession -- discover live session (pid +
  ping), stale-cleanup, chain-mismatch hard error, and fallback
  (auto-start-and-persist default | own-bridge | error), degrading auto-start to
  own-bridge if spawn/ready fails.

In-process/mocked tests only (injected spawnFn, mocked openUrl, no real daemon
or browser): daemon descriptor/onConnected/idle/tab-dead/connect-timeout/
singleton/shutdown-cleanup; spawn argv+unref and ready/timeout; resolver
live/stale/mismatch/auto-start-degrade/error matrix.

* feat(wallet): wallet connect/status/disconnect commands (+ hidden daemon)

New `genlayer wallet` command group (registered in src/index.ts):

- connect [--network --rpc]: reuse a matching live session, or spawn the
  detached daemon, print the bridge URL + SSH port-forward hint, and wait for
  the wallet to connect. Different chain -> explicit shutdown + switch.
- status: address/network/chainId/port/URL/age/idle/heartbeat/queue; exit 0
  only when live+connected (scriptable).
- disconnect: /api/shutdown -> wait for exit (SIGTERM fallback) -> remove
  descriptor (idempotent).
- daemon (hidden): detached-process entry point -> runWalletSessionDaemon.

wallet command tests dispatch connect/status/disconnect/daemon and assert
daemon is help-hidden; index test mocks the new initializer.

* feat(wallet): walletMode config default + per-command session adoption

Make browser mode reusable across commands and configurable as the default:

- walletMode config: BaseAction.resolveWalletMode centralises precedence
  (--wallet flag > walletMode config > keystore); invalid flag throws, unknown
  config value warns + keystore. walletOption drops the hardcoded commander
  "keystore" default so an omitted flag is distinguishable from an explicit one
  (and can defer to config). write/deploy/appeal/finalize switch their direct
  `wallet === "browser"` checks to isBrowserWallet(...).
- Adoption: BaseAction.getBrowserSession and StakingAction.getBrowserWalletSession
  now go through resolveBrowserWalletSession (fallback auto-start for writes,
  own-bridge for the wizard). Per-command finally blocks are untouched
  (closeBrowserSession/session.close() are no-ops for remote sessions, so a
  shared daemon survives); validatorJoin and the wizard switch
  session.bridge.close() -> session.close().

walletSession tests cover resolveWalletMode matrix; command/action tests updated
for the no-commander-default behaviour and session.close() (remote sessions
survive finally; wizard/validator-join assert session.close, not bridge.close).

* test(wallet): global open() safety net + regenerated CLI docs

- tests/setup.ts (wired via vitest.config setupFiles): globally mocks the `open`
  package so no automated test can ever launch a real browser or orphan a tab.
  Every bridge/daemon test already injects a mocked openUrl; this is the
  belt-and-suspenders guarantee. system.test.ts keeps its own file-level
  vi.mock("open") which takes precedence there.
- Regenerated api-references: new wallet connect/status/disconnect pages (hidden
  `daemon` excluded) and the updated --wallet help text/default across all write
  commands.

* style(wallet): prettier-format session runtime + tests

* feat(wallet): live wallet session implies browser mode

resolveWalletMode gains a session rung: with no --wallet flag and no
walletMode config, a live wallet session (descriptor present + daemon pid
alive) now resolves to browser mode, so `wallet connect` alone is enough to
route subsequent commands through the bridge. Explicit --wallet keystore or
walletMode=keystore still overrides a live session.

Add hasLiveWalletSession() (sync, never-throws descriptor+pid probe) and make
the test suite hermetic: tests/setup.ts redirects os.homedir() to a throwaway
per-worker temp dir so the descriptor read never sees the developer's real
~/.genlayer/wallet-session.json (which would otherwise flip commands into
browser mode and break otherwise-hermetic tests).

* feat(staking): wizard lists custom networks and honors live wallet session

Step 2 network picker now appends custom networks (from `genlayer network
add`) alongside the built-ins, and the post-selection echo resolves through
both maps so a custom alias no longer crashes on BUILT_IN_NETWORKS lookup.

Step 1 owner detection switches from `options.wallet === "browser"` to
resolveWalletMode(options.wallet) === "browser", so the wizard auto-selects
the browser-wallet owner for --wallet browser, walletMode=browser config, or a
live wallet session — consistent with every other command. The keystore path
and interactive "Connect browser wallet" menu item are unchanged.

* feat(account): account show reflects active network + --network flag

account show printed `network.name`, which for a custom active network is the
inherited base chain name (e.g. "Genlayer Localnet") — a misleading label.
Print the active network alias instead (--network flag > config network key >
localnet) and add a chainId field so a custom network is unambiguous.

Add a --network flag mirroring `account send`, so both the balance query and
the printed alias/chainId reflect the chosen network. New unit test covers a
custom active network (alias + real chainId, not the base name) and the
--network override.

* feat(balances): add read-only balances view (wallet + vesting + committed stake)

* feat(staking): wizard can fund a validator from a vesting contract

Add a funding-source step after network selection: keep 'Your wallet'
as the default (original flow unchanged) or fund the self-stake from a
vesting contract. For the vesting source the wizard resolves the
beneficiary's vesting contract (none -> warn + loop back; one -> use it;
many -> pick), checks the contract's available-to-stake (totalAmount -
totalWithdrawn) against the minimum stake while keeping a wallet gas
sanity warning, and builds+sends vestingValidatorJoin instead of
validatorJoin for both keystore (client.vestingValidatorJoin) and browser
(buildTx + shared session.sendTransaction) owners. Identity setup is
skipped for vesting-backed validators with a pointer to
'genlayer vesting validator set-identity'; the summary reflects the
funding source and notes funds return to the vesting contract on exit.

* fix(vesting): available-to-stake is the contract balance, not derived math

* fix(balances): a live wallet session is the active identity over the keystore default

* fix(network): custom networks display their alias as the name

* fix(wallet): detect a closed tab at session acquire + let connect recover

Browser-mode commands (notably `staking wizard`) only discovered a
closed wallet tab at the final sign step, after every wizard step was
filled in. resolveBrowserWalletSession now checks the page-heartbeat
freshness in its live-session branch and throws the reconnect message
at acquire time, so the wizard fails at the balance-check step instead
of the sign step. lastPagePollAt === 0 (never polled) stays fresh, and
a just-connected page is re-read so it is not misflagged.

`wallet connect` reused a daemon that was pid-alive + pinging +
state.connected but whose tab was dead, printing "Already connected"
and returning — leaving the user stuck. The reuse branch now tears the
stale daemon down (reusing the chain-switch teardown) and starts a
fresh session; the healthy path is unchanged.

TAB_CLOSED_MESSAGE is centralized in sessionConstants and shared by the
client and the resolver.
…on anvil) (#380)

Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.

Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
  StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)

Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.

Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
The `staking validator-deposit` and `staking validator-exit` keystore
paths called viem's `walletClient.writeContract` directly. viem then
negotiates its own fee/tx-type against the GenLayer consensus RPC, which
has no EIP-1559 fee support, so the write fails. Every other staking
command (and both `vesting validator` commands) instead go through the
SDK's staking client, whose `executeWrite` pins `type: "legacy"` and does
manual nonce/gas + sign + sendRawTransaction.

Route both commands through `client.validatorDeposit` /
`client.validatorExit`, which forward to the ValidatorWallet's own
functions (preserving msg.sender == ValidatorWallet on re-entry into
Staking) over the correct legacy-tx path. The genlayer-e2e cli-driver
hard-skips these two with a "SDK bug" note; that was a misdiagnosis — the
SDK actions exist and are correct, the CLI just wasn't using them.

Add keystore-path tests asserting both commands call the SDK client and
never touch getViemClients.
…token, 0700, url scrub, body cap) (#383)

Targeted hardening of the browser-wallet signing bridge per security review; all existing behavior and tests preserved.

- Verify signer: reject a wallet result whose from differs from the connected/expected signer, both in the bridge (handleResult) and in browserSend (assertResultSigner); plumb from through sessionClient.waitForTxResult. On accountsChanged the page re-POSTs /api/connected so the daemon stays coherent, and the misleading CLI-will-verify-the-sender copy now matches the enforced behavior.
- Host-header validation: reject Host != 127.0.0.1:<port>/localhost:<port> with 403 before route dispatch (validated against the port captured at start() so long-polls flushed during teardown still pass).
- Constant-time token compare via length-guarded timingSafeEqual.
- Create ~/.genlayer and keystores dir with mode 0o700 + chmodSync.
- Scrub the hash token from the URL via history.replaceState.
- Cap request bodies at 64KB (413 PayloadTooLargeError).
- Warn against ssh -g / GatewayPorts yes / public-interface binding.

Existing no-CORS, origin-fail-closed, and token-on-every-route checks are unchanged.
Add a fully non-interactive path to `staking wizard` so every prompt can be
supplied by a flag, making it scriptable and e2e-testable.

New flags: --non-interactive/--yes, --funding-source, --vesting-contract,
--operator, --create-operator, --operator-same, --operator-password,
--operator-keystore-out, --amount, and identity flags (--moniker, --logo-uri,
--website, --description, --email, --twitter, --telegram, --github).

When --non-interactive (or --yes) is set the wizard runs end-to-end with zero
prompts, erroring clearly and naming the missing flag when a required choice is
absent. The interactive path is unchanged (additive branches only). The
browser-wallet owner path works non-interactively too.
…ntity) (#385)

Every read command now resolves its default identity through one shared
BaseAction.resolveActiveIdentity() helper, so `wallet connect` alone is
enough to make bare reads follow the connected wallet — matching how signing
already resolves via resolveWalletMode.

Precedence (no keystore ever unlocked): explicit address flag
(--beneficiary/--validator/--delegator) > --account keystore selection >
live browser session > active keystore default > last-resort live session.

Routed onto the helper: balances, vesting list, vesting validator list,
staking validator-info, staking delegation-info, staking validators ("mine"
marking), staking validator-history. Write commands are untouched. account
show stays keystore-scoped (it reports keystore-only fields: name,
locked/unlocked, active).

Also tighten the balances committed-delegation scan: union active +
quarantined + banned validators (de-duped) instead of active-only, so
committed principal — and thus available-to-stake — is not under-counted for
validators that left the active set.
The wizard previously skipped the identity step for vesting-backed
validators, only printing a pointer to `genlayer vesting validator
set-identity`. It now runs the same guided identity prompts (and
--non-interactive flags) on the vesting path and sets identity through
the vesting contract's vestingValidatorSetIdentity — the SDK method the
standalone command uses — for both keystore and browser owners. No
raw-viem bypass. A revert is caught: the wizard warns and still reaches
the summary. --skip-identity remains the opt-out; the wallet path is
unchanged.
…hrough the SDK (#387)

The keystore paths of `staking set-operator`, `staking validator-claim`,
`staking set-identity` and `transactions trace` used raw viem
`walletClient.writeContract` / `client.request`. viem negotiates its own
EIP-1559 fee/tx-type against the GenLayer consensus RPC, which has no
EIP-1559 fee support, so those writes fail. This is the same latently-broken
class already fixed for validator-deposit/exit.

Route each keystore path through the genlayer-js client instead:
- set-operator   -> client.setOperator({validator, operator})
- validator-claim-> client.validatorClaim({validator}) (surfaces claimedAmount)
- set-identity   -> client.setIdentity({validator, moniker, ...}); the SDK now
                    owns extraCid encoding (drop the hand-rolled toHex/TextEncoder)
- trace          -> client.debugTraceTransaction({hash, round}) (typed action,
                    same wire call as gen_dbg_traceTransaction)

setOperator/setIdentity exist on the client at runtime (route through the SDK's
legacy-tx executeWrite) but are missing from the installed genlayer-js
StakingActions .d.ts; a narrow cast bridges that type gap.

Browser-wallet paths are unchanged (the bridge signs, so they were already
correct). With every caller gone, StakingAction.getViemClients is deleted along
with its now-unused viem imports.

Add keystore-path tests asserting each command calls the SDK client, and a
trace test using debugTraceTransaction.
* refactor(wallet): route browser-wallet writes through the SDK; drop txBuilders

Collapse the staking/vesting browser-wallet write lanes onto the same
genlayer-js client calls as the keystore lane, now that the SDK's
executeWrite handles an Address-only (provider) account by routing
eth_sendTransaction through the injected EIP-1193 provider.

Each browser branch builds a provider-bound client
(getBrowserStakingClient / getBrowserVestingClient / the wizard's
getWizardVestingBrowserClient) and calls client.<method>(...) exactly
like the keystore lane, replacing the per-command buildTx(...) +
session.sendTransaction(rawTx) reimplementation. The hand-rolled
calldata layer (txBuilders.ts + the stakingTx.ts shim) is deleted;
validatorJoin's SDK path already decodes the ValidatorJoin event and
returns validatorWallet for both lanes.

Per-tx bridge labels are preserved via session.setNextLabel(label)
before each SDK call (consumed by the EIP-1193 shim for the next
eth_sendTransaction). The SDK now preflight-simulates and throws before
eth_sendTransaction, so the command's failSpinner + session.close()
(no-op for a shared daemon) surfaces reverts cleanly and keeps the
bridge usable; no doubled preflight.

* chore(deps): bump genlayer-js to v2-dev provider-lane build (#200)
The balances command scans the global validator set (active + quarantined +
banned) to compute delegated committed principal per vesting. Those SDK reads
throw on networks with no staking contract ("Staking is not supported on
studio-based networks"), so `genlayer balances` failed outright on studio —
even though wallet balance and vesting totals need no staking data.

Gate the scan on staking availability, mirroring the SDK's own guard (missing
or zero staking address ⇒ unsupported): when absent, use an empty validator set
so delegated principal is 0 (correct — there is no delegation without staking)
and still render wallet + vesting holdings. Self-stake, vested/withdrawable and
available-to-stake all come from vesting/wallet reads and are unaffected.

Fixes genlayer-cli (studio) e2e 090_cli_network_and_balances #2.
…391)

After #389 stopped the staking scan from crashing balances on studio, the
next consensus-dependent read surfaced: the vesting-factory lookup resolves
through ConsensusMain → AddressManager, and on a network without that
infrastructure deployed (studio, or a custom profile on a bare RPC) it decodes
garbage — `genlayer balances` died with 'Position 32 is out of bounds ...
name()' (e2e 090 scenario 2 on the studio stack).

Custom networks inherit the base chain's ConsensusMain address even without a
--consensus-main override, so a studio profile carries a non-null (localnet)
address that simply isn't deployed on the studio RPC — a static check can't
tell them apart. Probe eth_getCode(consensusMain) once up front: if empty, the
consensus infra isn't deployed, so skip the whole consensus-dependent section
(vesting + staking) and render the wallet balance only, with a clear note.
Handles the entire missing-infra class in one capability check rather than one
revert at a time. dev-env (real deployed address) is unaffected.

Verified against a live studio stack: e2e 090 (both scenarios) green.
772/772 unit tests pass.
* fix(account): clear error for password prompts with no TTY

Running e.g. `genlayer account export --password <pw>` without --source-password
in a non-interactive context (piped stdin, CI, automation) died with
`ExitPromptError: User force closed the prompt` — inquirer's message for a
force-closed prompt, which reads exactly like the user hit Ctrl-C when in fact
a required flag is missing.

Guard promptPassword with assertInteractive(): when stdin is not a TTY, throw an
actionable error naming the flag(s) that make the command non-interactive
(--source-password / --password) instead of letting inquirer force-close. export
and import pass contextual hints. confirmPrompt is intentionally left alone
(its callers gate on their own --yes/--overwrite flags).

Adds non-TTY coverage in baseAction.test.ts; existing prompt tests simulate a
TTY (vitest stdin is not one).

* fix(account): catch inquirer force-close instead of guarding on isTTY

The first cut guarded promptPassword with !process.stdin.isTTY, which was too
blunt: it rejected PIPED stdin outright. But piped stdin is exactly how the e2e
harness (and real automation) supply the keystore password — so `genlayer
deploy` and every keystore-unlock path started failing with 'Maximum password
attempts exceeded' in CI (isTTY is false for a pipe, so the guard threw before
inquirer could read the piped password).

Instead, let inquirer run and catch its ExitPromptError ('User force closed the
prompt') — which only fires on an ACTUAL force-close (no TTY AND no piped
input). Rewrite that into the actionable, flag-naming message. Piped stdin now
reaches inquirer normally; the clear error still appears when there is genuinely
no input.

Verified against a keystore: piped source-password exports OK; empty stdin
yields 'No interactive terminal available for a password prompt. Pass
--source-password ...'. 774/774 unit tests.
…392)

The bridge page grabbed the single injected `window.ethereum`, which is
ambiguous when several wallet extensions are installed. Adopt EIP-6963: on load
dispatch `eip6963:requestProvider` and collect each `eip6963:announceProvider`
({info:{name,icon,rdns,uuid}, provider}) into a list; route every provider call
(eth_requestAccounts / eth_chainId / wallet_switch|addEthereumChain /
eth_sendTransaction / accountsChanged) through one selected active provider.

- 1 wallet → auto-select; >=2 → hand-rolled picker (icon+name); 0 announced →
  fall back to window.ethereum (older wallets), else a clear no-wallet message.
- Also subscribe chainChanged (page only heard accountsChanged) to warn on a
  mid-session network switch; ensureChain still corrects it before each tx.
- Transport / session daemon / /api contract untouched.
- Playwright mock now announces via eip6963:announceProvider (keeps
  window.ethereum too) so the Tier-2 lanes discover it.

Verified: Tier-2 browser-signing suite 9 passed / 1 skipped (nightly deploy).
…ng the base explorer (#393)

A custom network profile inherited its base chain's block explorer (e.g. a
pre-clarke profile based on bradbury showed explorer-bradbury.genlayer.com in
`network info`) — misleading, since that explorer never indexed the custom
deployment's transactions.

- Add `--explorer <url>` to `network add` (validated http(s); stored on the
  profile as overrides.explorer).
- applyCustomNetworkProfile no longer inherits the base explorer for custom
  profiles: it sets blockExplorers ONLY from --explorer, otherwise blanks it.
  Built-in networks are unaffected (they don't go through this path).

Also fixes two pre-existing tsc errors in the touched files (a narrowing cast
in parseDeploymentObject; a spy typing in the getNetwork test).

Tests: 778 passed (+3 explorer cases: stores+applies override, no-inherit
default, invalid-URL rejection).
…view (#394)

* feat(staking): warn/block validator self-stake below on-chain minimum

Add a shared self-stake eligibility gate (BaseAction.assertOrWarnSelfStakeMinimum)
used by the four validator write commands (staking validator-join/deposit,
vesting validator-create/deposit). Eligibility counts self-stake only; the
minimum is read from getEpochInfo().validatorMinStakeRaw (never hardcoded) and
mirrors the wizard's epoch-0 carve-out. Below-min blocks by default and is
overridable with --force; epoch-0 and eligible cases only inform.

Add liquid/vesting mixing hard-guards (no override) to the two deposit commands:
staking validator-deposit fails when the wallet is owned by a vesting contract;
vesting validator-deposit fails when the wallet was not created by the vesting
contract (isValidatorWallet). Both would revert on-chain otherwise.

Add informational source notes on join/create, --force flags on the four write
commands, and a best-effort primed-below-minimum note on validator-prime.

Adjust existing action/command tests to mock getEpochInfo/getValidatorInfo for
the new pre-submit checks (min driven from mocked epochInfo, not hardcoded) and
make the test parseStakingAmount mock exact at the integer-GEN boundary.

* test(staking): cover self-stake minimum gate, mixing guards, and clean validator-info view

Add unit tests driving the minimum from mocked epochInfo (never hardcoded):
below-min blocks without --force; --force proceeds with a warning; epoch-0
does not block; deposit counts still-pending self-stake toward the resulting
stake; liquid-into-vesting and vesting-into-liquid mixing hard-blocks (not
overridable); validator-info --json returns the raw object; the clean grouped
view keeps load-bearing values (addresses, GEN amounts, moniker, 'Not banned',
live boolean) as plain substrings and never relabels live as 'active';
display-only below-min warning never blocks; pending-deposit crossing shows the
activation epoch.

* fix(staking): min-stake preflight degrades if the chain can't report the minimum

The self-stake below-minimum check called getEpochInfo() unconditionally in the
validator-join/deposit + vesting create/deposit preflights. On a chain that
can't report epoch/minStake (a minimal or stub staking contract, or a transient
read failure) that read reverts and HARD-FAILS the write — e.g. the Tier-2
browser-signing lanes join against a StakingStub without epoch(), so
validator-join died with 'The contract function "epoch" reverted'.

Wrap the getEpochInfo() read: on failure, skip the (advisory) min check and
proceed, rather than blocking the operation. The mixing guard (owner /
isValidatorWallet) stays a hard, always-enforced check. Also pass --force on the
Tier-2 lanes' deliberately-sub-minimum test joins.

Verified: Tier-2 browser-signing suite 6/6 green; 786 unit tests pass.
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.

3 participants