Jun 02, 9-10 PM (19)
Jun 02, 10-11 PM (33)
Jun 02, 11-12 AM (22)
Jun 03, 12-1 AM (13)
Jun 03, 1-2 AM (31)
Jun 03, 2-3 AM (16)
Jun 03, 3-4 AM (0)
Jun 03, 4-5 AM (7)
Jun 03, 5-6 AM (12)
Jun 03, 6-7 AM (80)
Jun 03, 7-8 AM (16)
Jun 03, 8-9 AM (24)
Jun 03, 9-10 AM (22)
Jun 03, 10-11 AM (39)
Jun 03, 11-12 PM (76)
Jun 03, 12-1 PM (93)
Jun 03, 1-2 PM (28)
Jun 03, 2-3 PM (62)
Jun 03, 3-4 PM (26)
Jun 03, 4-5 PM (24)
Jun 03, 5-6 PM (23)
Jun 03, 6-7 PM (15)
Jun 03, 7-8 PM (17)
Jun 03, 8-9 PM (19)
Jun 03, 9-10 PM (9)
Jun 03, 10-11 PM (31)
Jun 03, 11-12 AM (14)
Jun 04, 12-1 AM (12)
Jun 04, 1-2 AM (4)
Jun 04, 2-3 AM (1)
Jun 04, 3-4 AM (5)
Jun 04, 4-5 AM (1)
Jun 04, 5-6 AM (0)
Jun 04, 6-7 AM (14)
Jun 04, 7-8 AM (10)
Jun 04, 8-9 AM (11)
Jun 04, 9-10 AM (19)
Jun 04, 10-11 AM (11)
Jun 04, 11-12 PM (14)
Jun 04, 12-1 PM (53)
Jun 04, 1-2 PM (39)
Jun 04, 2-3 PM (60)
Jun 04, 3-4 PM (12)
Jun 04, 4-5 PM (4)
Jun 04, 5-6 PM (7)
Jun 04, 6-7 PM (46)
Jun 04, 7-8 PM (27)
Jun 04, 8-9 PM (4)
Jun 04, 9-10 PM (2)
Jun 04, 10-11 PM (24)
Jun 04, 11-12 AM (7)
Jun 05, 12-1 AM (6)
Jun 05, 1-2 AM (8)
Jun 05, 2-3 AM (1)
Jun 05, 3-4 AM (1)
Jun 05, 4-5 AM (1)
Jun 05, 5-6 AM (5)
Jun 05, 6-7 AM (9)
Jun 05, 7-8 AM (12)
Jun 05, 8-9 AM (8)
Jun 05, 9-10 AM (11)
Jun 05, 10-11 AM (12)
Jun 05, 11-12 PM (8)
Jun 05, 12-1 PM (52)
Jun 05, 1-2 PM (61)
Jun 05, 2-3 PM (26)
Jun 05, 3-4 PM (24)
Jun 05, 4-5 PM (17)
Jun 05, 5-6 PM (7)
Jun 05, 6-7 PM (14)
Jun 05, 7-8 PM (12)
Jun 05, 8-9 PM (6)
Jun 05, 9-10 PM (2)
Jun 05, 10-11 PM (20)
Jun 05, 11-12 AM (9)
Jun 06, 12-1 AM (6)
Jun 06, 1-2 AM (0)
Jun 06, 2-3 AM (3)
Jun 06, 3-4 AM (4)
Jun 06, 4-5 AM (0)
Jun 06, 5-6 AM (24)
Jun 06, 6-7 AM (1)
Jun 06, 7-8 AM (2)
Jun 06, 8-9 AM (3)
Jun 06, 9-10 AM (0)
Jun 06, 10-11 AM (3)
Jun 06, 11-12 PM (6)
Jun 06, 12-1 PM (2)
Jun 06, 1-2 PM (2)
Jun 06, 2-3 PM (2)
Jun 06, 3-4 PM (18)
Jun 06, 4-5 PM (1)
Jun 06, 5-6 PM (6)
Jun 06, 6-7 PM (0)
Jun 06, 7-8 PM (6)
Jun 06, 8-9 PM (0)
Jun 06, 9-10 PM (1)
Jun 06, 10-11 PM (27)
Jun 06, 11-12 AM (9)
Jun 07, 12-1 AM (14)
Jun 07, 1-2 AM (2)
Jun 07, 2-3 AM (0)
Jun 07, 3-4 AM (0)
Jun 07, 4-5 AM (1)
Jun 07, 5-6 AM (1)
Jun 07, 6-7 AM (3)
Jun 07, 7-8 AM (0)
Jun 07, 8-9 AM (0)
Jun 07, 9-10 AM (1)
Jun 07, 10-11 AM (2)
Jun 07, 11-12 PM (2)
Jun 07, 12-1 PM (5)
Jun 07, 1-2 PM (35)
Jun 07, 2-3 PM (2)
Jun 07, 3-4 PM (4)
Jun 07, 4-5 PM (2)
Jun 07, 5-6 PM (4)
Jun 07, 6-7 PM (0)
Jun 07, 7-8 PM (0)
Jun 07, 8-9 PM (17)
Jun 07, 9-10 PM (1)
Jun 07, 10-11 PM (21)
Jun 07, 11-12 AM (9)
Jun 08, 12-1 AM (9)
Jun 08, 1-2 AM (5)
Jun 08, 2-3 AM (3)
Jun 08, 3-4 AM (4)
Jun 08, 4-5 AM (2)
Jun 08, 5-6 AM (9)
Jun 08, 6-7 AM (5)
Jun 08, 7-8 AM (25)
Jun 08, 8-9 AM (36)
Jun 08, 9-10 AM (40)
Jun 08, 10-11 AM (24)
Jun 08, 11-12 PM (22)
Jun 08, 12-1 PM (40)
Jun 08, 1-2 PM (48)
Jun 08, 2-3 PM (33)
Jun 08, 3-4 PM (27)
Jun 08, 4-5 PM (12)
Jun 08, 5-6 PM (23)
Jun 08, 6-7 PM (14)
Jun 08, 7-8 PM (3)
Jun 08, 8-9 PM (6)
Jun 08, 9-10 PM (19)
Jun 08, 10-11 PM (29)
Jun 08, 11-12 AM (8)
Jun 09, 12-1 AM (5)
Jun 09, 1-2 AM (3)
Jun 09, 2-3 AM (1)
Jun 09, 3-4 AM (3)
Jun 09, 4-5 AM (26)
Jun 09, 5-6 AM (5)
Jun 09, 6-7 AM (23)
Jun 09, 7-8 AM (50)
Jun 09, 8-9 AM (35)
Jun 09, 9-10 AM (45)
Jun 09, 10-11 AM (51)
Jun 09, 11-12 PM (44)
Jun 09, 12-1 PM (84)
Jun 09, 1-2 PM (65)
Jun 09, 2-3 PM (36)
Jun 09, 3-4 PM (37)
Jun 09, 4-5 PM (15)
Jun 09, 5-6 PM (17)
Jun 09, 6-7 PM (18)
Jun 09, 7-8 PM (16)
Jun 09, 8-9 PM (8)
Jun 09, 9-10 PM (0)
2,763 commits this week Jun 02, 2026 - Jun 09, 2026
feat(leios): implement stake-truncated committee voting and stake quorum (#2509)
* feat(leios): implement stake-truncated committee voting and stake quorum

Implement the CIP-0164 (post cardano-foundation/CIPs#1196) stake-based
voting committee model in a new ledger/leios package:

- Committee selection: order pools by active stake descending (pool key
  hash ascending on ties), select until cumulative coverage reaches the
  CommitteeStakeCoverage (sigma_c) parameter, and assign stable voter_id
  indexes. The committee derives from the same epoch-2 stake snapshot
  cadence as Praos leader election and is memoized in memory.
- Stake quorum: votes must represent at least QuorumStakeThreshold (tau)
  of total active stake, evaluated with exact rational arithmetic. The
  tau < sigma_c invariant is revalidated wherever parameters are read.
- Votes: one uniform vote per endorser block per voter (slot_no, EB
  hash, voter_id, BLS signature). Real BLS12-381 MinSig sign/verify/
  aggregate via gnark-crypto, with lenient verification pending CIP-0164
  key registration: a static config registry (leiosVoterPublicKeys)
  supplies voter public keys, and votes from unknown voters count toward
  observed stake but never toward certificates.
- Certificates: signers bitfield plus one aggregated BLS signature
  (gouroboros LeiosEbCertificate), built only from verified votes once
  verified stake meets tau, announced via a new leios.eb_quorum event.
  Validation is exposed but not yet wired into block validation; the
  Dijkstra CDDL leios_cert slot is still an upstream placeholder.
- VoteManager: TTL-bounded in-memory vote store with per-(slot, voter)
  dedup (first vote wins on equivocation), per-connection serving
  cursors for the exactly-count LeiosVotes protocol semantics, local
  vote emission for block producers with a leiosVoteSigningKeyFile, and
  pruning on chain rollbacks and epoch transitions.

The LeiosVotes/LeiosFetch protocol handlers now delegate to the vote
manager through a LeiosVoteHandler interface, the LeiosVotes client is
started on outbound connections, and storing an endorser block triggers
local vote emission.

Fixes #2424

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Chris Guiney <[email protected]>

* fix(leios): address review-bot findings

- ComputeCommittee now errors when the coverage target is unreachable
  (pool stake sum below sigma_c of total active stake) instead of
  returning a partial committee
- NewVoterRegistry rejects non-28-byte pool key hashes and duplicate
  entries that normalize to the same pool hash
- LoadVoteSigningKeyFile rejects oversized key files instead of
  silently truncating, and reuses keystore.CheckOpenFilePermissions
  (newly exported) so Windows gets the real DACL check instead of a
  no-op; the leios-local permission shims are removed
- VerifyVoteSignature subgroup-checks the public key for callers
  outside the package
- Vote store prunes expired entries before the dedup check so a stale
  vote id cannot block a fresh vote after TTL
- NextVotes persists the per-connection cursor only on successful
  delivery so aborted waits do not skip undelivered votes
- WithLeiosVoterPublicKeys copies the caller's map

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Chris Guiney <[email protected]>

* fix(leios): decouple vote dedup ledger from serving-store eviction

The vote manager deduplicated votes against the size-capped serving
store while tallies lived on their own TTL. A vote re-received after
its serving entry was evicted (reconnecting peers re-serve their full
log) re-counted its stake into a still-live tally and appended a
duplicate voter id to the verified set, after which BuildEbCertificate
failed on the duplicate forever and the endorser block became
permanently un-certifiable. Post-eviction equivocation was likewise
accepted as fresh, and the tally map was unbounded under fabricated
vote floods in lenient mode.

- Add a voteRecord ledger as the authoritative dedup and
  tally-accounting state, decoupled from serving-store eviction.
  Records are pruned only in lockstep with their endorser block's
  tally (TTL), by epoch at transitions, and by slot on rollbacks, so
  re-delivered votes never re-count and first-wins equivocation
  detection stays durable. The ledger is capacity-capped with
  reject-new semantics (evicting a record would reintroduce the
  re-count); locally emitted votes bypass the cap. The cap
  transitively bounds the tally map.
- Window incoming vote slots against the current (or tip) slot before
  any epoch or committee work. EpochForSlot projects future slots
  indefinitely, so fabricated far-past/future votes previously reached
  stake-snapshot database queries and bloated the committee memo. The
  bounds are provisional pending CIP-0164's L_vote. LedgerState
  satisfies the new SlotProvider directly; a nil provider disables the
  check. The local emission path shares the window so the node does
  not sign votes peers would reject.
- Document the proof-of-possession invariant on BLS aggregate
  verification and the voter registry as its trust root: rogue-key
  attacks make summed-pubkey verification forgeable without it.
- Add a vote-record gauge, slot_window/capacity reject reasons, and
  regression tests covering re-count after size eviction and TTL
  expiry, durable equivocation, capacity semantics, the slot window,
  and rollback re-votes. The re-count tests fail against the previous
  dedup logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Chris Guiney <[email protected]>

* fix(leios): exempt verified votes from record admission cap

The vote record ledger's capacity cap applied to every peer vote, so
in partially-registered lenient mode an attacker could fill it with
fabricated unverified votes (committee members without registered keys
need no valid signature) and starve subsequent verified votes -- the
only ones that feed verified stake and certificates -- stalling
certification of new endorser blocks for the duration of a flood.

Verified votes now bypass the cap: each requires a valid BLS signature
from a registered committee key and dedup bounds them to one record
per (slot, registered voter) inside the slot window, so their record
growth is protocol-bounded without the cap. The cap keeps gating
unverified peer votes, whose only effect is observed-stake visibility.
Identified by cubic on the previous commit.

The capacity regression test now fills the ledger with unverified
votes (the only kind still subject to the cap), and a new test pins
the bypass: a verified vote is admitted past a full ledger while
unverified votes remain rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Chris Guiney <[email protected]>

---------

Signed-off-by: Chris Guiney <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
feat(governance): in-app Ekklesia/Hydra budget voting for multisig DRep
Adds an off-chain DRep voting flow for the Intersect Cardano Budget 2026
ballot, which runs on Ekklesia/Hydra (L2) via CIP-8 signed messages rather
than on-chain CIP-1694 actions. Multisig DReps are supported: each signer
co-signs the same vote package and Ekklesia aggregates at threshold.

- API spec reverse-engineered from the live API + frontend bundle (the
  published docs are JS-rendered); captured in src/lib/ekklesia/SPEC.md
- Server proxy (src/pages/api/ekklesia/[...path].ts) — CORS blocks direct
  browser calls; forwards auth + cookies
- Typed client + orchestration (src/lib/ekklesia/{types,client,voteFlow}.ts),
  reusing the signable subsystem for multi-signer coordination
- HydraBudgetVote UI + page + governance-hub entry
- EKKLESIA_API_BASE env var

Note: per-signer CIP-95 DRep-key signing and the multisig session binding
need verification against a live multisig DRep wallet before relying on it
for the June 12 deadline; manual voting via Ekklesia's CardanoSigner is the
fallback.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
diag: instrument ballot vote witness/body-hash divergence (#271)
Add diagnostic-only logging (no behaviour change) to pinpoint the
`InvalidWitnessesUTXOW` rejection seen when submitting legacy-DRep ballot
votes. Conway `voting_procedures` bodies are re-encoded by some wallets,
so a vkey witness ends up signed over a different body hash than the one
reaching the node.

Three [ballot-witness-diag] log points:
- txScriptRecovery: new diagnoseTxWitnesses() verifies every vkey witness
  against the exact body being submitted, logging any stale witness (with
  pubkey/keyhash) right before submitTx. Covers both signing paths.
- txSignUtils.mergeSignerWitnesses: logs wallet body re-canonicalisation,
  distinguishing first-signer (body adopted) from co-signer (swap skipped,
  witness may be stale).
- api/v1/signTransaction: logs when merge/rebuild changes the body hash the
  collected signatures were made over.

Co-authored-by: Claude Opus 4.8 <[email protected]>
chore(deps-dev): bump tailwindcss from 3.4.19 to 4.3.0
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 3.4.19 to 4.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
chore(deps): bump lucide-react from 0.439.0 to 1.17.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.439.0 to 1.17.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
chore(deps): bump uuid from 11.1.1 to 14.0.0
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.1.1 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.1.1...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Preprod (#229)
* Add bot-runnable API tests and integration smoke suite

* feat: create new ci bot testing framework

- Added support for an optional `paymentNativeScript` parameter in the createWallet API, allowing users to specify a custom payment script tree.
- Implemented validation to ensure the script structure adheres to required formats and that key hashes match the provided signers' addresses.
- Updated related tests to cover new functionality and ensure robust handling of various script configurations.
- Enhanced API documentation to reflect the new parameter and its usage.
- Create initial ci bot testing framework files

* chore: update CI configuration and improve wallet transfer handling

- Added `.gitignore` entry for CI local artifacts.
- Changed `init-db.sh` to use `sh` instead of `bash`.
- Updated `docker-compose.ci.yml` to correctly handle status variable in commands.
- Enhanced CI scripts to support multisig wallet ring transfers, including detailed documentation updates.
- Refactored transfer flow to ensure distinct source and destination wallet types for transactions.
- Improved error handling and validation in wallet script resolution logic.

* chore: enhance CI documentation and improve transfer flow validation

- Updated README.md to clarify real transfer construction and validation notes for preprod scenarios.
- Added checks in run-route-chain.ts to ensure preprod context and testnet address validation.
- Enhanced bot authentication logic in botAuth.ts with caching and retry mechanisms.
- Improved transfer flow in transferFlow.ts to validate wallet addresses and ensure sufficient UTxO availability for transactions.
- Refactored transaction building logic to utilize MeshTxBuilder for better handling of multisig inputs.

* fix: improve bot authentication error handling and enhance test setups

- Updated bot authentication logic in botAuth.ts to provide clearer error messages by including the status of the authentication response.
- Enhanced test setups across multiple test files to ensure consistent initialization of session-related properties, including sessionWallets and primaryWallet.
- Refactored mock implementations in various tests for better type safety and clarity.

* chore: update CI workflow and enhance native script handling

- Removed wallet_types input from the CI workflow configuration for simplicity.
- Added conditional execution for the multisig-v1-smoke job based on the repository.
- Updated README.md to clarify the role of native scripts in CI and added details about script payload storage.
- Implemented a new function to fetch native scripts for different wallet types in the manifest, enhancing the route health checks.

* chore: enhance CI workflows and improve transaction handling

- Added additional redaction rules for sensitive information in CI logs, including mnemonics and private keys.
- Updated `create-wallets.ts` to remove transaction ID from created wallets, reflecting changes in transaction handling.
- Modified `run-pending-transactions-smoke.ts` to focus on specific pending scenarios and improve error messaging.
- Refactored context validation to make transaction ID optional, ensuring compatibility with new wallet creation logic.
- Cleaned up scenario manifest by removing obsolete pending transaction checks and clarifying wallet discovery steps.

* feat: expand wallet scopes and enhance CI documentation

- Updated wallet creation logic in `create-wallets.ts` to include additional scopes: `governance:read` and `ballot:write`.
- Enhanced `README.md` to document new wallet authentication and governance features, including detailed descriptions of new routes and scenarios.
- Added new scenarios in `manifest.ts` for bot identity verification and wallet authentication checks, improving overall CI coverage.

* feat: add wallet balance summary to CI reports

- Introduced a new `walletBalanceSummary` object in `ci-route-chain-report.json` to capture total on-chain balances per wallet.
- Updated `runScenarios` to collect wallet balance data and include it in the run report.
- Defined new types for wallet balance entries and summaries in `types.ts` to support the new reporting feature.
- Enhanced documentation in `README.md` to explain the structure and semantics of the wallet balance summary.

* refactor: restructure CI scripts and update documentation

- Removed obsolete scripts: `create-wallets.ts`, `run-route-chain.ts`, `inspect-context.ts`, `run-pending-transactions-smoke.ts`, `sign-transaction-preprod.ts`, and `template-route-step.ts`.
- Introduced new CLI structure under `scripts/ci/cli/` with `bootstrap.ts` and `route-chain.ts` for improved organization.
- Updated `docker-compose.ci.yml` to reflect changes in script execution.
- Revised documentation in `README.md` to align with new script structure and clarify CI stages.
- Adjusted scenario descriptions in `manifest.ts` to reference new script locations.

* refactor: update scenario functions to accept context parameter

- Modified scenario functions in `manifest.ts`, `authPlane.ts`, `datum.ts`, and `governance.ts` to accept `CIBootstrapContext` as a parameter for improved context handling.
- Updated execution logic in scenarios to utilize the provided context, enhancing flexibility and maintainability.
- Adjusted scenario descriptions to reflect changes in wallet type handling and ensure consistency across different scenarios.

* feat: enhance CI wallet management and context handling

- Updated `docker-compose.ci.yml` to include a new step for checking wallet status after bootstrap.
- Expanded `README.md` to clarify the CI process, detailing the new `wallet-status.ts` script and its role in confirming wallet funding.
- Modified `bootstrap.ts` to derive and store signer stake addresses, and updated the context schema to version 3 to accommodate new fields.
- Enhanced `inspect-context.ts` to display signer stake addresses and SDK multisig reward address.
- Adjusted context validation in `context.ts` to ensure compatibility with the new schema and added checks for signer stake addresses.

* feat: add bot stake and DRep certificate transaction endpoints

- Introduced new API endpoints `/api/v1/botStakeCertificate` and `/api/v1/botDRepCertificate` for building stake and DRep transactions, respectively.
- Updated `package.json` to include new unit tests for the added functionality.
- Enhanced `README.md` with detailed usage instructions for the new endpoints.
- Refactored `addTransaction.ts` to streamline transaction creation for multisig wallets.
- Updated `freeUtxos.ts` to improve UTxO resolution logic.
- Modified `stake.tsx` to integrate new staking action configurations.

* chore: update package-lock.json to mark several dependencies as dev dependencies

Added "dev": true to multiple entries in package-lock.json, indicating that these packages are development dependencies. This change helps clarify the purpose of these packages in the project.

* docs: add roadmap and contributing guide

ROADMAP.md seeded from docs/roadmap-v3 and extended with a
Task ownership section splitting all 12 months of work into
per-contributor lists, plus a Month 1 proof-of-completion
table tracking actual status of each M1 task.

CONTRIBUTING.md captures the review process, branch and
commit conventions, PR checklist, and merge rules. Contributions
are accepted against preprod, which graduates to main after
clean runs in the preprod environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat: enhance transaction signing and certificate handling

- Refactor botDRepCertificate to streamline transaction input handling and certificate script application.
- Update botStakeCertificate to utilize a unified spend script for transaction inputs.
- Introduce fresh query parameter in freeUtxos to allow fetching unspent transaction outputs directly from the blockchain.
- Implement stake key witness validation in signTransaction to support transactions with staking certificates.
- Add new flow for signing stake certificates that includes both payment and stake key witnesses.
- Create comprehensive CI scenarios for DRep registration and retirement, as well as stake registration and deregistration.
- Implement stakeAccountInfo API to fetch stake account status and pool information.

* fix: guard against invalid CBOR in addTransaction and transaction card

Reject unparseable txCbor/txJson at the addTransaction API boundary so a
malformed 4-element CBOR can never be persisted, and render a degraded
card with a Reject button when an existing row's txJson cannot be
parsed, so a single bad row no longer crashes the whole Transactions
page and locks up its UTxOs.

Closes #211

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* docs: refresh M1 proof of completion to 2026-04-23

- #223 marked Done — PR #225 merged, issue closed
- #211 moved to In review — PR #227 open with API validation + degraded card
- External PRs row updated with review status (#212 awaiting rebase, #208 superset)
- #213 note clarifies CI smoke is skipping due to missing SMOKE_* secrets

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix: run prisma migrate deploy only at start, not build

Build-phase migrations fail on Railway because postgres.railway.internal
is only reachable at runtime. The prestart hook already handles this.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* ci: trigger smoke tests on Railway deployment success

Adds deployment_status trigger so smoke runs against the freshly deployed
preprod environment. Filters to Railway bot + preprod env, and checks out
the exact deployed SHA.

Note: deployment_status only fires from workflows on the default branch,
so this must also land on main for the trigger to activate.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat: update CI workflow to include additional branches for pull requests and pushes

- Added support for `main`, `preprod`, and `bot-api-test-suite` branches in the pull request and push triggers of the CI workflow.
- Enhanced the workflow configuration to ensure proper execution across multiple environments.

* feat: add pre-hygiene step for DRep certificate management and implement drepInfo API endpoint

* feat: enhance CI scenarios with new wallet lifecycle and negative token checks

* feat: update CI report format to Markdown and add cleanup step for test wallets

* feat: remove bot-api-test-suite branch from PR workflow triggers

* feat: add retry logic for pulling base Docker image and update fetch redirect behavior

* docs: add comment to clarify validation of URL safety in resolveDRepAnchorFromUrl function

* docs: update comment to clarify URL validation process in resolveDRepAnchorFromUrl function

* feat: update DRep certificate handling to compute anchor data hash and refactor anchor URL resolution

* feat: add CI_DREP_ANCHOR_JSON environment variable and update DRep registration to use JSON input

* feat: update api README - DRep registration to require anchorJson in request body and remove server-side URL fetch

* feat: add CI_DREP_ANCHOR_JSON environment variable to workflow

* feat: add proxy management endpoints and update test scripts

- Introduced new API endpoints for managing proxies, including listing confirmed proxies, setting up proxy transactions, finalizing proxy setups, and spending proxies.
- Updated the README documentation to reflect the new proxy bot API functionality.
- Enhanced the test script to include a new test for proxy-related functionality.

* feat: enhance proxy bot functionality and update CI workflows

- Expanded unit test command in package.json to include additional proxy-related tests.
- Increased timeout for the multisig smoke workflow to accommodate longer execution times.
- Added new proxy bot cleanup instructions to the README for better user guidance.
- Implemented new scenarios in CI for proxy lifecycle management, including smoke tests and full lifecycle coverage.
- Introduced retry logic in HTTP requests to improve resilience against transient errors.

* feat: enhance proxy bot lifecycle management and update documentation

- Updated unit test command in package.json to include additional proxy-related tests.
- Added new steps for proxy row recovery and orphan adoption in CI workflows.
- Enhanced README documentation for proxy bot API, clarifying required parameters and transaction flows.
- Improved API descriptions to reflect changes in handling DRep registration and proxy transactions.
- Adjusted test cases to validate new lifecycle steps for proxy management.

* feat: enhance proxy lifecycle management to support hierarchical wallets

- Updated proxy lifecycle scenarios to include support for hierarchical wallets alongside legacy and SDK wallets.
- Modified README documentation to reflect changes in wallet type coverage for proxy full lifecycle scenarios.
- Improved error handling in proxy setup finalization to ensure valid transaction hashes.
- Added unit tests to validate new hierarchical wallet functionality in proxy management.
- Adjusted existing tests to ensure comprehensive coverage of all wallet types in proxy lifecycle processes.

* feat: enhance DRep certificate handling and testing

- Introduced `applyDRepCert` function to streamline DRep certificate operations (register, update, retire) in `buildDRepCertTx.ts`.
- Updated `RegisterDRep`, `Retire`, and `UpdateDRep` components to utilize the new `applyDRepCert` function for better code reuse and clarity.
- Added Jest tests for DRep certificate functionality in `drepCert.test.ts`, covering various scenarios including registration, update, and retirement.
- Created utility functions and fixtures for testing in `cborUtils.ts`, `fixtures.ts`, and `mockProvider.ts`.
- Established a GitHub Actions workflow for running unit tests and uploading coverage reports.
- Set coverage thresholds for critical files to ensure code quality.

* feat: integrate completeTxWithFreshCostModels into proxy transaction handling

- Updated proxy transaction APIs to utilize `completeTxWithFreshCostModels` for transaction completion, enhancing cost model handling.
- Adjusted `getTxBuilder` to accept a flag for using the CSL serializer, improving flexibility in transaction building.
- Enhanced unit tests for proxy cleanup, setup, spend, vote, and DRep certificate APIs to validate the new transaction completion logic.
- Added error handling for PPView hash mismatches during transaction submission, ensuring better feedback on transaction integrity issues.

* feat: add TRPC test command and update unit test workflow

- Added a new test command for TRPC in package.json to facilitate targeted testing of TRPC-related components.
- Updated the unit test workflow to correct the test path pattern for transaction builder tests, ensuring accurate test execution.
- Removed unused cborUtils.ts file to clean up the test directory and improve maintainability.
- Simplified infrastructure tests by removing unnecessary cbor-x decoding tests.

* refactor: update completeTxWithFreshCostModels integration and enhance tests

- Refactored imports to streamline the usage of `completeTxWithFreshCostModels` across the codebase.
- Updated unit tests to reflect changes in cost model handling, including support for raw arrays and ordering of indexed cost model objects.
- Added new test cases to validate the rejection of improperly ordered cost model objects, ensuring robustness in transaction processing.

* feat: enhance proxy transaction handling with blocked UTxO management

- Integrated functions to extract and filter blocked UTxOs from pending transactions, improving the robustness of proxy transaction processing.
- Updated `MeshProxyContract` to utilize blocked UTxO references when selecting UTxOs for spending, ensuring only available UTxOs are used.
- Enhanced `ProxyControl` component to manage blocked UTxOs and ensure proper handling of available UTxOs for transactions.
- Refactored UTxO selection logic to improve clarity and maintainability in the proxy transaction workflow.

* Refactor proxy UTxO selection and transaction building

- Updated `proxySpend.ts` and `proxyVote.ts` to use `selectAuthTokenUtxo` for selecting the auth token UTxO, incorporating blocked UTxO references.
- Introduced new utility functions in `utxoUtils.ts` for selecting UTxOs and checking asset presence.
- Added comprehensive tests for UTxO selection logic in `proxy-utxo-selection.test.ts`.
- Created `txBuilders.ts` to encapsulate transaction building logic for proxy actions, including setup, spending, voting, and DRep certificate management.
- Implemented cross-verification tests in `proxy-cross-verify.test.ts` to ensure consistency between direct and browser-based transaction builders.

* fix(signing): pin Mesh SDK + reject witnesses that don't verify against the tx body

A user submitting a "Ballot Vote" on a 2-of-3 DRep multisig hit
`ConwayUtxowFailure (InvalidWitnessesUTXOW [VKey ce7cbe8f...])`. Decoding the
failing tx with this repo's CSL showed the witness's vkey hashes to a *valid*
script signer, but its Ed25519 signature does not verify against the tx body
hash. The body itself is byte-stable across CSL round-trips, so the wallet
must have signed a different (re-canonicalised) body.

Cause: package.json had `^1.9.0-beta.87` on @meshsdk/core/core-csl/core-cst/
provider/react; the lockfile had drifted to 1.9.0-beta.102. Some patch in
those 15 betas changed how Mesh emits CBOR (likely voting_procedures), so
app-built bodies no longer match what the wallet's encoder produces — the
wallet's signature verifies against its body but not ours.

Fix:
- Pin Mesh deps to exact .87 (.86 for provider) — no more silent patch drift.
- `mergeSignerWitnesses` returns `{ txHex, invalidVkeyPubKeysHex }`,
  verifying each newly-merged vkey's signature against the merged tx body
  hash. Witnesses already on the tx are not re-verified.
- transaction-card.signTx() and useTransaction.newTransaction() abort with a
  destructive toast (and skip submission + persistence) when the wallet
  returns a witness that doesn't verify. Offending pubkey is logged for
  debugging. The wallet user gets an actionable message instead of a chain
  rejection.
- jest.config.mjs: one-line moduleNameMapper for libsodium-wrappers-sumo's
  broken relative `./libsodium-sumo.mjs` import (pre-existing CI gap).
- .gitattributes: mark package-lock.json as linguist-generated so diff
  reviewers can skip the lockfile churn.

Test plan:
- New unit test `mergeSignerWitnesses.test.ts`: 3 cases (happy, mismatched
  body, pre-existing witness preserved) — all pass.
- `npx tsc --noEmit` on touched files: clean.
- Manual: decoded the user-shared failing txCbor; confirmed
  `blake2b-224(vkey) == 'f6ed79ef...'` (valid signer) but
  `pk.verify(body, sig)` is false. The new guard catches exactly this case
  before submit.
- Pre-existing test failures on preprod (proxyCiPreflight, proxyDRepInfo,
  signTransaction mock missing isBotJwt, pendingTransactions, apiSecurity)
  are in files unrelated to this fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(deps): pin libsodium-wrappers-sumo to 0.7.10 to unbreak Node ESM resolution

The 0.7.16 release ships `dist/modules-sumo-esm/libsodium-wrappers.mjs` with
a relative `import "./libsodium-sumo.mjs"` whose target file isn't in the
package — the actual sumo binary lives in the separate `libsodium-sumo`
package and is wired up via package.json `exports`. Node's strict ESM
resolver (used by `tsx` in the CI smoke runner, and by Next.js at build
time) doesn't follow the cross-package indirection and throws
`ERR_MODULE_NOT_FOUND` before any Mesh helper has a chance to run.

Preprod's pre-existing lockfile pinned 0.7.10, which works. Regenerating the
lockfile to land the Mesh pin pulled 0.7.16 (the latest in the ^0.7.5 range
the cardano-sdk transitively allows), breaking both `multisig-v1-smoke` and
the Vercel build. Override to 0.7.10 forces all `@cardano-sdk/crypto`
copies onto the working version.

Verified: `node --input-type=module -e "import('@meshsdk/core')"` now
succeeds (failed before the override). Unit tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix: pin Mesh to preprod's resolved versions + recover wallet body on first signer

CI failure on the previous push exposed two issues with my initial pin to
.87:

1. **Smoke test "libsodium not initialized"**: pinning Mesh to `.87` pulled
   in different `@cardano-sdk/*` transitive versions than preprod's known-
   good lockfile, and the resulting libsodium-wrappers-sumo / libsodium-sumo
   combination fails WASM init under Node 20 in CI. Preprod runs Mesh
   `.102` for `core/core-csl/core-cst`, `.100` for `provider`, `-40` for
   `react`, and its smoke test passes — so pin to those exact versions.

2. **Pin alone doesn't fix the user**: with `.102` we're back on the same
   CBOR-encoding Mesh that broke their wallet's signature in the first
   place. The verify guard added in the previous commit converts that into
   a friendly error, but the user still can't vote.

   Extended `mergeSignerWitnesses`: when the wallet returns a full signed
   Transaction (not just a witness set), and its body bytes differ from
   ours, and we have no pre-existing witnesses to invalidate, *use the
   wallet's body*. The wallet's vkey signature was made over its body, so
   adopting it makes the signature verify and lets the submit succeed.
   This handles the first-signer case — which is the typical case for
   "I clicked Approve & Sign and it failed" — without changing behaviour
   for multi-signer flows where prior witnesses would be invalidated.

Also dropped the libsodium override commit's package.json entry — it's not
needed once we match preprod's exact Mesh versions, and trying to pin
`libsodium-sumo` to 0.7.10 broke Node 22's WASM loader.

Added unit test covering the body-swap recovery path; existing 3 tests
still pass (4 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(deps): regenerate lockfile with npm 10.8.2 to match CI's Node 20 alpine

The previous push's lockfile was generated with my local npm 11.10.1, which
produces a `lockfileVersion: 3` lockfile npm 10 considers inconsistent:
`@simplewebauthn/[email protected]` + `@simplewebauthn/[email protected]` were marked
"Missing from lock file" and `npm ci` refused to proceed in the Dockerfile.ci
build step. (CI runs `node:20-alpine`, which bundles npm 10.8.2 — the same
notice line in the failure log.)

Same fix the repo has applied twice before: regenerate with the matching
npm version. Confirmed:
- lockfile contains 14 @simplewebauthn entries
- Mesh resolved to the pinned versions (.102 / .100 / -40)
- mergeSignerWitnesses tests still 4/4 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(deps): pin @peculiar/webcrypto to 1.5.0 to keep Vercel webpack build green

Vercel's previous attempts failed during webpack compile with:
  Module build failed: UnhandledSchemeError: Reading from "node:crypto" /
  "node:process" is not handled by plugins (Unhandled scheme).
  Import trace: @peculiar/webcrypto → @meshsdk/web3-sdk → @meshsdk/react

`@peculiar/[email protected]` switched its compiled output from `require('crypto')`
to ESM `import "node:crypto"`. webpack 5 (Next 16 `--webpack` mode) doesn't
handle the `node:` scheme without an explicit plugin, and we don't want to
add one — preprod's known-good lockfile resolves to `1.5.0`, which still
uses bare `crypto`. Regenerating the lockfile after pinning Mesh re-resolved
this to `1.7.1` (latest matching `^1.5.0`), reintroducing the issue. Pin to
1.5.0 to match preprod.

Verified: with the override, `node:` imports no longer appear in the webpack
trace; the residual local build failure is missing
`NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD` env (Vercel has it set).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* feat: integrate completeTxWithFreshCostModels into proxy transaction handling

- Updated proxy transaction APIs to utilize `completeTxWithFreshCostModels` for transaction completion, enhancing cost model handling.
- Adjusted `getTxBuilder` to accept a flag for using the CSL serializer, improving flexibility in transaction building.
- Enhanced unit tests for proxy cleanup, setup, spend, vote, and DRep certificate APIs to validate the new transaction completion logic.
- Added error handling for PPView hash mismatches during transaction submission, ensuring better feedback on transaction integrity issues.

* refactor: update completeTxWithFreshCostModels integration and enhance tests

- Refactored imports to streamline the usage of `completeTxWithFreshCostModels` across the codebase.
- Updated unit tests to reflect changes in cost model handling, including support for raw arrays and ordering of indexed cost model objects.
- Added new test cases to validate the rejection of improperly ordered cost model objects, ensuring robustness in transaction processing.

* feat(wallets): add Import Wallet wizard

Adds an Import Wallet entry point alongside New Wallet with a single-page
wizard covering four sources: another multisig instance (deep link or
root URL + signer picker, with CIP-30 nonce-sign against the origin),
Summon (forwards into the existing /wallets/invite flow), manual
native-script CBOR paste, and JSON backup upload. Wires up a paired
Download JSON backup action on the wallet info page so users can
round-trip exports between instances.

Backend: new wallet.importWallet/exportWallet tRPC procedures sharing
the wallet.createWallet write path, plus /api/v1/exportWallet/{getNonce,
redeem,listMine} cross-instance endpoints that reuse the existing CIP-8
checkSignature verification. No Prisma schema change — provenance lives
in the existing rawImportBodies JSON, with a lockedSigners flag the
wallet info Edit Signers gate respects so imported wallets can't
silently diverge from their origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(wallets): drop audit() call from importWallet on preprod

The audit observability module hasn't landed on preprod yet, so the
audit() call I copied from other procedures in main's version of this
file was unresolved at build time. Drop it for now; can be re-added
once main's observability work merges into preprod.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* Merge origin/main into preprod (PR #229)

Conflict resolution
- jest.config.mjs: union both moduleNameMapper entries (libsodium ESM
  redirect from preprod + styleMock from main)
- package.json: keep preprod's expanded bot test scripts and Mesh SDK
  1.9.0-beta.102 pins; honor main's removal of @jinglescode/nostr-chat-
  plugin (no remaining source refs on either branch)
- package-lock.json: regenerated from the merged package.json
- src/__tests__/{apiSecurity,botBallotsUpsert,governanceActiveProposals,
  signTransaction}.test.ts: take preprod; tighter generics and preprod-
  shaped tRPC ctx (sessionWallets/primaryWallet) match the live router
- src/components/pages/wallet/transactions/transaction-card.tsx: take
  preprod's defensive JSON.parse guard (#211 — malformed txJson must not
  crash the Transactions page)

Security fixes flagged by CodeQL on the merge
- src/lib/server/resolveDRepAnchorFromUrl.ts: close the DNS-rebinding
  TOCTOU window. assertUrlSafeForFetch now returns the resolved IP, and
  the fetch goes through an undici Agent with a buildConnector-pinned
  lookup so the actual TCP connect uses the pre-validated IP. Switched
  from global.fetch to undici.request for the same reason; existing
  hostname blocklist, private/loopback IP rejection, redirect=error,
  body size cap and timeout are all preserved.
- src/__tests__/resolveDRepAnchorFromUrl.test.ts: mock undici instead of
  global.fetch to match the new transport.
- scripts/ci/framework/markdown.ts: rewrite escapeCell as a single
  character-class regex (\\ and | escaped in one pass) so there's no
  ordering ambiguity that triggers js/incomplete-sanitization.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix: restore main-only schema/code lost by the auto-merge

git's three-way merge of preprod←main reported a clean merge of
prisma/schema.prisma and src/server/api/trpc.ts but silently dropped
main's additions — both files had only end-of-file additions on main,
which the auto-merge resolved by taking preprod's tail without pulling
in main's new symbols. The Vercel build then failed on three downstream
references.

prisma/schema.prisma
- Add model AuditLog (referenced by src/lib/observability/audit.ts;
  migration 20260510160404_audit_log_and_indexes already in the tree)
- Make User.nostrKey String? (matches migration
  20260510170000_make_user_nostrkey_optional)

src/server/api/trpc.ts
- Re-export TRPCContext and AuthCtx (used by src/server/api/auth.ts
  added in main's audit-log PR)

Drop the Nostr chat system on preprod to match main
- Remove src/components/pages/wallet/chat and src/pages/wallets/[wallet]/chat
- Drop @jinglescode/nostr-chat-plugin imports from _app.tsx + layout.tsx
- Remove the Chat menu entry from the wallet sidebar
- userRouter.createUser: nostrKey becomes optional (matches the now-
  nullable column) and is only written when supplied
- User profile page: scope nostrKey to a non-null local inside the
  existing `user.nostrKey &&` guard so it still renders for legacy
  users without tripping the nullable narrowing

The repo's nostr-chat-plugin dep was already removed in the prior
merge commit; this commit removes the last call sites and brings the
user-row contract in line with the schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* refactor: streamline multisig wallet address handling and improve error messages in DRep components

* feat: Enhance proxy lifecycle and transaction handling

- Updated README.md to reflect new CI stages and detailed descriptions for proxy-related scripts.
- Refactored proxyBot.ts to utilize new UTxO selection utilities for setup and auth token UTxOs.
- Added unit tests for new UTxO selection functions: selectSetupUtxo and accumulateFundingUtxos.
- Improved proxy transaction builders to accumulate funding UTxOs and ensure sufficient lovelace for operations.
- Implemented cleanupProxy method in MeshProxyContract to handle proxy UTxO cleanup and burning of auth tokens.
- Enhanced error handling for insufficient lovelace scenarios in proxy operations.

* Merge main into preprod: Prisma 7 + mesh 2.0 migration + test-infra fixes (#269)

* Merge main into preprod: resolve conflicts + migrate Prisma 7 & mesh 2.0

Snapshot on staging branch for deeper test-infra testing. Captures the full
resolution of PR #229's 16 conflicts plus two breaking dependency upgrades
from main that required migration:

- Prisma 6->7: add prisma.config.ts, drop schema url/directUrl, wire the
  @prisma/adapter-pg driver in db.ts, fix Decimal import.
- mesh react 2.0: useMeshWallet bridge sources a 1.9 IWallet from react 2.0's
  connection so signing/tx code keeps the 1.9 API (avoids the signData arg
  swap). Null-guards added at call sites.

tsc is green (0 errors). Known WIP: jest suite has an ESM/CJS mock-mode
conflict exposed by the upgrades (jest.mock vs unstable_mockModule) - test
infra fix in progress.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* test-infra: complete Prisma 7 migration in CI scripts + fix test loading

- Fix 6 CI-scenario `new PrismaClient()` sites that Prisma 7 rejects (no
  adapter): add scripts/ci/framework/prismaClient.ts factory and route all
  construction through it.
- Add @/env jest stub (src/__tests__/__mocks__/env.cjs) + moduleNameMapper so
  server modules load under jest without the ESM-only @t3-oss validator.
- Fix botAuth jsonwebtoken mock to expose `default` (esModuleInterop default
  import was undefined).
- Keep preprod's plain-jest invocation (revert the --experimental-vm-modules
  change, which broke jest.mock-based tests).

Bot gate now 128/132; full suite 373/421. Remaining failures are
unstable_mockModule (ESM) tests that don't apply under plain jest - mock-style
unification still pending.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* test-infra: split CJS/ESM jest projects so the full suite passes

The suite mixes two mutually-exclusive jest module modes (jest.mock = CJS,
unstable_mockModule/import.meta/ESM-deps = ESM). The mesh 2.0 + Prisma 7
upgrades tripped the ESM-loading paths that preprod's older deps avoided, so
plain jest could no longer run both. Resolved by running two jest invocations
from a shared base (jest.shared.mjs):

- jest.config.mjs (CJS, plain jest): all tests except the 8 ESM files and the
  trpc/* DB integration tests (their own workflow).
- jest.esm.config.mjs (ESM, --experimental-vm-modules): the 8 unstable_mockModule
  / import.meta files.
- scripts/run-tests.mjs runs both and forwards args; test/test:ci/test:bot:*
  go through it. --runInBand makes the gate deterministic; --coverage is
  CJS-only (v8 coverage + native ESM crashes, and the per-file threshold lives
  in the CJS config).
- collectCoverageFrom excludes src/server/** and src/lib/security/** — jest
  force-loads uncovered files for coverage and their import-time side effects
  polluted mocked tests (freeUtxos).
- Fix signTransaction.test @meshsdk/core mock to export resolveStakeKeyHash
  (ESM static linking needs it).

Result: test:ci green (CJS 357 + ESM 48, 0 failures), tsc 0 errors.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(deps): restore nested @simplewebauthn/[email protected] in lock

My npm 11 lock regen deduped @simplewebauthn/browser to 13.3.0, which doesn't
satisfy @auth/core's ^9.0.1 — CI (npm 10, node 20) rejected `npm ci` as out of
sync. Rebuilt the lock with npm 10 (--package-lock-only) to restore the nested
9.0.1 entry under @auth/prisma-adapter. Key versions unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* test(trpc): restore real timers for DB integration tests

The merged setup.ts (from main) freezes the clock (Date.now/new Date) for unit
determinism. tRPC integration tests hit a real Postgres and Prisma 7 sets
createdAt client-side, so a frozen clock gives identical timestamps and breaks
ordering assertions. Run test:trpc via a dedicated config that layers a
real-timers setup over the global one.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(build): transpile @meshsdk/core-csl + whisky-evaluator for WASM

@meshsdk/core-csl 1.9 pulls whisky-evaluator, which ships a .wasm the Node
server runtime can't load when the package is externalized
(ERR_UNKNOWN_FILE_EXTENSION ".wasm" → createWallet 500). Add both to
transpilePackages so Next bundles them and applies its WebAssembly handling.

Candidate fix — needs the v1 smoke / Vercel deploy to confirm the WASM loads at
runtime (the local build still warns about the .wasm modules).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci(smoke): run v1 smoke against a production build, not next dev

The multisig-v1-smoke ran the app via `next dev`, which mis-resolves the
@meshsdk/core-csl / whisky-evaluator WASM path at runtime (createWallet 500).
Build the app in Dockerfile.ci and serve it with `next start` so the smoke
exercises the same production output Vercel deploys (and starts instantly
instead of compiling routes on demand). Copy prisma.config.ts before npm ci so
the postinstall generate sees the Prisma 7 config; set a dummy DATABASE_URL for
build-time module evaluation (compose overrides it at runtime).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci: bump node 20 -> 22 (required by mesh 2.0 + Prisma 7)

mesh 2.0's whisky-evaluator (WASM) and Prisma 7's @prisma/dev/streams-local
require node >=22. On node 20 the production `next build` fails resolving the
whisky-evaluator WASM export (and streams-local warns EBADENGINE). node 22
matches what Vercel and local builds use. Bumps the app Dockerfiles and the
node-20 workflows; the node-18 scheduled jobs are left for a separate pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci(smoke): use node:22-slim (glibc) instead of alpine (musl)

The production `next build` fails under node:22-alpine resolving the
@meshsdk/core-csl / whisky-evaluator WebAssembly export
(`does not provide an export named 'js_evaluate_tx_scripts'`) — a musl/WASM
incompatibility. The same build succeeds on glibc (local macOS + Vercel).
Switch the CI Dockerfiles to node:22-slim (Debian) and apt for the postgres
client.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(build): externalize whisky WASM packages instead of transpiling

webpack can't statically resolve whisky-evaluator's WASM-backed ESM named
export during `next build` on Linux (passes on macOS), so transpiling it broke
the Docker/CI build. Move whisky-evaluator + @sidan-lab/whisky-js-nodejs to
serverExternalPackages so Node `require`s their cjs/ build at runtime (loads the
WASM synchronously) and webpack never analyzes the ESM. Keep @meshsdk/core-csl
transpiled.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(ci): enable --experimental-wasm-modules for the smoke build/runtime

The production build's page-data worker (and `next start`) load @meshsdk/core-csl
as a native ESM external, which `import`s whisky-evaluator's WASM-backed exports.
node's ESM loader needs --experimental-wasm-modules to resolve them, otherwise:
`whisky-evaluator does not provide an export named 'js_evaluate_tx_scripts'`.
Set via NODE_OPTIONS so it covers both `next build` and the runtime `next start`.
Validated: the Dockerfile.ci image now builds end-to-end on linux.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* ci(smoke): split Dockerfile so ci-runner skips next build

The app build (with the WASM-aware production next build) succeeds and the app
starts healthy — but the bootstrap step rebuilds the ci-runner service from the
same Dockerfile, re-running next build (which ci-runner doesn't need; it only
runs tsx scripts) and failing. Split Dockerfile.ci into a base stage (deps +
source) and an app stage (base + build); ci-runner targets base so the
bootstrap runs the real runtime createWallet test against the healthy app.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(ci): set --experimental-wasm-modules on the base stage

The ci-runner runs tsx scripts that import the Mesh SDK (core-csl -> whisky
WASM), so it needs --experimental-wasm-modules too — not just the app build.
Move NODE_OPTIONS to the shared base stage. Validated in a Linux container:
`@meshsdk/core-csl` now imports cleanly (was: "does not provide an export named
'js_evaluate_tx_scripts'").

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(ci): run CI scripts as esbuild bundles via node, not tsx

tsx's esbuild-based ESM loader can't defer whisky-evaluator's .wasm imports to
node's native --experimental-wasm-modules handling, so `tsx bootstrap.ts` fails
to load @meshsdk/core-csl ("does not provide an export named
'js_evaluate_tx_scripts'"). Pre-bundle the CI CLI scripts with esbuild in the
base image (resolving @/ aliases, externalizing node_modules) and run the
bundles with plain `node` — which loads the WASM natively (flag set on the base
stage). Validated locally: all three scripts now load @meshsdk/core-csl and
fail only on missing runtime env/context, past the previous WASM error.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>

* test(freeUtxos): drop virtual:true from module mocks to fix flakiness

The mocks used jest.mock(..., { virtual: true }) on real modules. Virtual
mocks register by literal specifier, so they applied when the handler imported
e.g. @/utils/nativeScriptUtils directly but not reliably when the real
resolveWalletScriptAddress imported it — under suite pollution the real decode
ran instead of the mock, so the "canonical fallback cannot decode" test saw
"unknown error" instead of "invalid canonical cbor". Removing virtual:true makes
jest resolve by file path, applying each mock to every importer. Verified: the
full CJS suite passes across repeated --runInBand runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: André Diamond <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
diag: instrument ballot vote witness/body-hash divergence
Add diagnostic-only logging (no behaviour change) to pinpoint the
`InvalidWitnessesUTXOW` rejection seen when submitting legacy-DRep ballot
votes. Conway `voting_procedures` bodies are re-encoded by some wallets,
so a vkey witness ends up signed over a different body hash than the one
reaching the node.

Three [ballot-witness-diag] log points:
- txScriptRecovery: new diagnoseTxWitnesses() verifies every vkey witness
  against the exact body being submitted, logging any stale witness (with
  pubkey/keyhash) right before submitTx. Covers both signing paths.
- txSignUtils.mergeSignerWitnesses: logs wallet body re-canonicalisation,
  distinguishing first-signer (body adopted) from co-signer (swap skipped,
  witness may be stale).
- api/v1/signTransaction: logs when merge/rebuild changes the body hash the
  collected signatures were made over.

Co-Authored-By: Claude Opus 4.8 <[email protected]>