Skip to content

Release: preprod → main#280

Merged
QSchlegel merged 85 commits into
mainfrom
preprod
Jun 13, 2026
Merged

Release: preprod → main#280
QSchlegel merged 85 commits into
mainfrom
preprod

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

Promotes preprod → main. 84 commits, ~276 files. This is a large release that bundles the in-progress preprod work plus this week's wallet-auth/governance fixes.

Highlights

Wallet auth & signing (mobile)

Governance

Wallets

Platform (carried in preprod)

Notes

  • This is a big surface (~276 files). Recommend reviewing the full commit list (git log main..preprod) and giving the platform-level changes (Prisma 7 / Mesh 2.0 / proxy) a closer look than the auth fixes, which have been validated on preprod.
  • The session's auth/governance fixes have been exercised against the preprod Railway deployment.

🤖 Generated with Claude Code

Andre-Diamond and others added 30 commits March 30, 2026 08:26
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
…ependencies

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.
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) <noreply@anthropic.com>
docs: add roadmap and contributing guide
- 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.
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) <noreply@anthropic.com>
- #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) <noreply@anthropic.com>
docs: refresh M1 proof of completion (2026-04-23)
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) <noreply@anthropic.com>
fix: guard against invalid CBOR in addTransaction and transaction card
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) <noreply@anthropic.com>
…ests 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.
Andre-Diamond and others added 24 commits May 24, 2026 22:57
…e 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.
…eprod

fix(signing): pin Mesh SDK + reject witnesses that don't verify against the tx body
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
merge(preprod): resolve PR #229 conflicts + fix CodeQL findings
- 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.
…ng-alignment

Refactor/ci-and-browser-tx-building-alignment
…ixes (#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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* fix(deps): restore nested @simplewebauthn/browser@9.0.1 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Content was already integrated via #269 (staging/preprod-prisma7-mesh2, which
merged origin/main into preprod and resolved all conflicts). That PR was
squash-merged, so main was not in preprod's ancestry and the preprod->main PR
re-conflicted. This records main as a parent without changing preprod's tree,
making the preprod->main merge clean.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ep (#272)

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 <noreply@anthropic.com>
#273)

Some CIP-30 wallets (notably mobile in-app browsers) return addresses
from getChangeAddress/getUsedAddresses/getRewardAddresses as hex-encoded
CBOR bytes rather than bech32. WalletAuthModal passed that hex into
deserializeAddress, whose bech32 decoder threw a raw 'Invalid checksum'
error and broke wallet authorization. connect-wallet.tsx had the same
bug in its UTXOS payment/stake normalization blocks, silently swallowed
by try/catch, letting hex addresses flow into the user store.

Add normalizeAddressToBech32 (Address.fromBytes().toBech32() from
@meshsdk/core-cst), which derives the network from the address header
byte, and use it at all three sites. Surface a clear error in the auth
modal when the wallet address still cannot be parsed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…igins (#274)

The "Another instance" import flow failed two ways on mobile:

- The exportWallet endpoints used the allowlist-based CORS middleware,
  so any instance not in CORS_ORIGINS got a rejected preflight and the
  browser surfaced an opaque "Load failed". These endpoints are designed
  for cross-instance calls (credentials omitted, CIP-30 signature
  protected), so give them a public Access-Control-Allow-Origin: *
  policy instead.

- instance-tab passed the raw getRewardAddresses()[0] — hex-encoded
  CBOR bytes in mobile in-app browsers — into Mesh signData (throws on
  hex) and into the origin's bech32 signer-list check (never matches).
  Normalize to bech32 client-side, and defensively server-side too for
  older deployed clients.

Also translate opaque cross-origin fetch errors into an actionable
message, and let the Upload JSON tab accept pasted backup JSON since
mobile in-app browsers often can't reach downloaded files.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…sage (#275)

WalletAuthModal swallowed the wallet's actual signData exception and
replaced it with "Failed to sign nonce. Please try again." On the UTXOS
smart wallet, signData fails inside the provider with a specific error
("Etwas ist schiefgelaufen") that we were discarding, leaving no way to
diagnose why authorization fails on mobile while getNonce succeeds
server-side.

Surface the underlying message in both the toast and console.error,
while keeping the friendly cancel/reject handling on top.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…l heuristic (#276)

The cancel/reject heuristic matched any wallet error message containing
"user" (etc.) and rewrote it to "Signing cancelled", which hid the real
UTXOS signData failure even after we started surfacing it. Show the raw
provider message verbatim so the true cause is visible.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… -2) (#277)

WalletAuthModal signed the nonce with react-2.0's useWallet().wallet, a
low-level CIP-30 wallet whose signData(address, payload) argument order
is swapped relative to Mesh 1.9's signData(payload, address). The call
signData(nonce, address) therefore passed the nonce as the *address* and
the address as the *payload*, so VESPR tried to sign with a bogus signing
address and returned CIP-30 InternalError {code: -2}. (It also explains
the wallet dialog showing the address as the "Nachricht".)

Switch to the 1.9 BrowserWallet via useMeshWallet — the same instance and
(payload, address) order every other signing flow in the app uses. The
UTXOS MeshWallet path is also payload-first, so the single
signData(nonce, address) call is now correct for both wallet types.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… ESLint guardrail (#278)

* fix(auth): use Mesh 1.9 wallet so signData args aren't swapped (VESPR -2)

WalletAuthModal signed the nonce with react-2.0's useWallet().wallet, a
low-level CIP-30 wallet whose signData(address, payload) argument order
is swapped relative to Mesh 1.9's signData(payload, address). The call
signData(nonce, address) therefore passed the nonce as the *address* and
the address as the *payload*, so VESPR tried to sign with a bogus signing
address and returned CIP-30 InternalError {code: -2}. (It also explains
the wallet dialog showing the address as the "Nachricht".)

Switch to the 1.9 BrowserWallet via useMeshWallet — the same instance and
(payload, address) order every other signing flow in the app uses. The
UTXOS MeshWallet path is also payload-first, so the single
signData(nonce, address) call is now correct for both wallet types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(wallet): route all wallet ops through the Mesh 1.9 bridge + guardrail

Every signing/wallet call must use useMeshWallet()/useActiveWallet() (the
Mesh 1.9 IWallet), never react-2.0's useWallet().wallet — the latter is a
low-level CIP-30 wallet whose signData(address, payload) / signTx(tx,
partialSign) signatures differ from 1.9, so a wrong-order call compiles
but signs the wrong bytes.

Fixes 3 live latent copies of that bug (same root cause as the WalletAuth
modal fix):
- HydraBudgetVote: signData on the react-2.0 wallet — the likely source of
  the ballot witness/body-hash divergence.
- instance-tab (cross-instance import): would fail with CIP-30 -2 on VESPR.
- api-docs bearer-token generation: same swapped-args failure.

Also migrates the remaining read-only useWallet().wallet sites (address /
network-id lookups, identical across versions) to the bridge so the
boundary is uniform, and adds null guards where the bridge wallet is
transiently null while enabling.

Guardrail: an ESLint no-restricted-syntax rule makes destructuring
`wallet` from useWallet() a build error, so this bug class can't regress.
useWallet() stays allowed for connection state (name/connected/connect/
disconnect). Verified the rule fires on a violation and passes the bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…te (#279)

HydraBudgetVote gated the vote UI on `multisigWallet.getDRepId()` being
truthy, but that is a local key/script derivation that always succeeds —
it is NOT proof of on-chain registration. So a wallet that is a fully
registered, active DRep (shown correctly by the DRep Information card)
was still told to "Register this wallet as a DRep first."

Detect registration the same way the DRep Information card does: look up
the DRep on-chain and check `active === true`. Done via a React Query
hit to Blockfrost `/governance/dreps/{cip105}` (the same endpoint and
getDRepIds helper the global wallet-data loader uses), which also gives a
clean loading state so we no longer flash "register first" before the
status is known. While the wallet or registration lookup is resolving we
show "Checking DRep registration…" instead of the register prompt.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
multisig Ready Ready Preview, Comment Jun 13, 2026 4:57am

Request Review

…eprod tree is authoritative — main has no unique content)
@railway-app railway-app Bot temporarily deployed to victorious-warmth / preprod June 13, 2026 04:54 Inactive
@QSchlegel QSchlegel merged commit 6db3dc0 into main Jun 13, 2026
12 of 14 checks passed
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.

2 participants