diff --git a/examples/compatibility-harness/.env.example b/examples/compatibility-harness/.env.example new file mode 100644 index 000000000..90862b576 --- /dev/null +++ b/examples/compatibility-harness/.env.example @@ -0,0 +1,122 @@ +# ══════════════════════════════════════════════════════════════════════════════ +# Zama Compatibility Harness — Environment Configuration +# ══════════════════════════════════════════════════════════════════════════════ +# +# Copy this file to .env and fill in the values for your adapter. +# +# Only ONE section is needed depending on the adapter you run: +# • Built-in EOA adapter (default src/adapter/index.ts) → set PRIVATE_KEY +# • Crossmint adapter (SIGNER_MODULE=./examples/crossmint/signer.ts) +# → set CROSSMINT_* vars +# • Turnkey adapter (SIGNER_MODULE=./examples/turnkey/signer.ts) +# → set TURNKEY_* vars +# • Openfort baseline adapter (SIGNER_MODULE=./examples/openfort/signer.ts) +# → set OPENFORT_* vars +# • Custom adapter module → set whatever it needs +# + +# ── Built-in EOA Adapter ────────────────────────────────────────────────────── +# +# Only needed when using the built-in adapter (default test path). +# Not required when using a custom module through SIGNER_MODULE. +# +# The account must hold Sepolia ETH for the Transaction Execution test. +# Faucets: https://sepoliafaucet.com | https://faucet.alchemy.com/faucets/ethereum-sepolia +# +PRIVATE_KEY=0x + +# ── Crossmint MPC Adapter ───────────────────────────────────────────────────── +# +# Only needed when running: +# SIGNER_MODULE=./examples/crossmint/signer.ts npm test +# See examples/crossmint/COMPATIBILITY.md for setup details. +# +# CROSSMINT_API_KEY=your_server_side_api_key +# +# Wallet locator — one of: +# email:alice@example.com:evm-smart-wallet +# userId:abc123:evm-smart-wallet +# phoneNumber:+1234567890:evm-smart-wallet +# CROSSMINT_WALLET_LOCATOR=email:alice@example.com:evm-smart-wallet +# +# Optional: supply the wallet 0x address directly to skip the API lookup at startup. +# If omitted, the address is resolved via GET /wallets/{locator} before tests run. +# CROSSMINT_WALLET_ADDRESS=0x + +# ── Turnkey API Key Adapter ─────────────────────────────────────────────────── +# +# Only needed when running: +# SIGNER_MODULE=./examples/turnkey/signer.ts npm test +# See examples/turnkey/COMPATIBILITY.md for setup details. +# +# Required: +# TURNKEY_ORG_ID=... +# TURNKEY_PRIVATE_KEY_ID=... +# TURNKEY_API_PUBLIC_KEY=... +# TURNKEY_API_PRIVATE_KEY=... +# +# Optional: +# TURNKEY_WALLET_ADDRESS=0x... +# TURNKEY_BASE_URL=https://api.turnkey.com +# TURNKEY_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY + +# ── Openfort EOA Baseline Adapter ───────────────────────────────────────────── +# +# Only needed when running: +# SIGNER_MODULE=./examples/openfort/signer.ts npm test +# See examples/openfort/COMPATIBILITY.md for scope and caveats. +# +# OPENFORT_TEST_PRIVATE_KEY=0x... +# +# Optional: adapter-specific RPC override (defaults to RPC_URL) +# OPENFORT_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY + +# ── Network ─────────────────────────────────────────────────────────────────── +# +# Profile selector: +# - sepolia (default) +# - mainnet (requires explicit RELAYER_URL) +# +# NETWORK_PROFILE=sepolia +# +# Sepolia JSON-RPC endpoint. Defaults to a public endpoint. +# Override with Infura / Alchemy for better reliability. +# +# RPC_URL=https://ethereum-sepolia-rpc.publicnode.com + +# ── Relayer ─────────────────────────────────────────────────────────────────── +# +# Zama relayer base URL. Defaults to the public Sepolia testnet relayer. +# Override for a private or mainnet relayer. +# +# RELAYER_URL=https://relayer.testnet.zama.org/v2 +# +# API key forwarded as x-api-key. Not required for the public Sepolia relayer. +# Required for mainnet or private relayers. +# +# RELAYER_API_KEY= + +# ── Reporting ───────────────────────────────────────────────────────────────── +# +# Optional machine-readable report artifact path. +# Example: +# REPORT_JSON_PATH=./reports/latest.json +# +# REPORT_JSON_PATH= + +# ── Harness Runtime ────────────────────────────────────────────────────────── +# +# Deterministic local mode: +# - disables network/relayer/registry-dependent validations in the test suite +# - marks those checks as UNTESTED with explicit rationale +# +# HARNESS_MOCK_MODE=1 + +# ── Validation Gate Policy (optional) ──────────────────────────────────────── +# +# Use with `npm run validate`: +# - policy file path (JSON), see ./validation-policy.example.json +# - optional env override to accept PARTIAL outcomes as pass +# +# VALIDATION_POLICY_PATH=./validation-policy.example.json +# VALIDATION_ALLOW_PARTIAL=false diff --git a/examples/compatibility-harness/.github/workflows/compatibility-harness-ci.yml b/examples/compatibility-harness/.github/workflows/compatibility-harness-ci.yml new file mode 100644 index 000000000..3dc0dc896 --- /dev/null +++ b/examples/compatibility-harness/.github/workflows/compatibility-harness-ci.yml @@ -0,0 +1,46 @@ +name: Compatibility Harness CI + +on: + push: + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Test (deterministic mock mode) + env: + HARNESS_MOCK_MODE: "1" + run: npm test + + - name: Validate gate (deterministic mock mode) + env: + HARNESS_MOCK_MODE: "1" + run: | + set +e + npm run validate + status=$? + echo "validate exit code: $status" + # In deterministic mock mode, authorization is untested by design. + # Accept PASS(0), PARTIAL(10), INCONCLUSIVE(30) as non-regression outcomes. + if [ "$status" -eq 0 ] || [ "$status" -eq 10 ] || [ "$status" -eq 30 ]; then + exit 0 + fi + exit "$status" diff --git a/examples/compatibility-harness/.github/workflows/compatibility-harness-live.yml b/examples/compatibility-harness/.github/workflows/compatibility-harness-live.yml new file mode 100644 index 000000000..8b11a86e3 --- /dev/null +++ b/examples/compatibility-harness/.github/workflows/compatibility-harness-live.yml @@ -0,0 +1,79 @@ +name: Compatibility Harness Live (Optional) + +on: + workflow_dispatch: + inputs: + signer_module: + description: "Adapter module path (optional)" + required: false + type: string + validation_target: + description: "Validation target" + required: false + default: AUTHORIZATION_AND_WRITE + type: choice + options: + - AUTHORIZATION + - AUTHORIZATION_AND_WRITE + schedule: + - cron: "0 3 * * *" + +jobs: + live-validate: + name: Live Validate (Non-Blocking) + runs-on: ubuntu-latest + timeout-minutes: 30 + continue-on-error: true + env: + PRIVATE_KEY: ${{ secrets.HARNESS_PRIVATE_KEY }} + RPC_URL: ${{ secrets.HARNESS_RPC_URL }} + RELAYER_URL: ${{ secrets.HARNESS_RELAYER_URL }} + RELAYER_API_KEY: ${{ secrets.HARNESS_RELAYER_API_KEY }} + NETWORK_PROFILE: ${{ secrets.HARNESS_NETWORK_PROFILE }} + REPORT_JSON_PATH: reports/live/latest-report.json + VALIDATION_TARGET: ${{ github.event.inputs.validation_target || 'AUTHORIZATION_AND_WRITE' }} + SIGNER_MODULE: ${{ github.event.inputs.signer_module }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Run live validation (capture exit code) + id: live_validate + run: | + mkdir -p reports/live + set +e + npm run validate 2>&1 | tee reports/live/validate.log + status=${PIPESTATUS[0]} + echo "$status" > reports/live/validate.exitcode + echo "exit_code=$status" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Upload live artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: compatibility-harness-live-${{ github.run_id }} + path: reports/live + if-no-files-found: warn + + - name: Publish run summary + if: always() + run: | + echo "## Compatibility Harness Live Run" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- Non-blocking: yes (continue-on-error job)" >> "$GITHUB_STEP_SUMMARY" + echo "- validate exit code: ${{ steps.live_validate.outputs.exit_code }}" >> "$GITHUB_STEP_SUMMARY" + echo "- Artifact: compatibility-harness-live-${{ github.run_id }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/examples/compatibility-harness/.gitignore b/examples/compatibility-harness/.gitignore new file mode 100644 index 000000000..b72e5a4ce --- /dev/null +++ b/examples/compatibility-harness/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +reports/ diff --git a/examples/compatibility-harness/CODEX_TICKETS.md b/examples/compatibility-harness/CODEX_TICKETS.md new file mode 100644 index 000000000..426556acd --- /dev/null +++ b/examples/compatibility-harness/CODEX_TICKETS.md @@ -0,0 +1,562 @@ +# Compatibility Harness — Codex Tickets + +This file is the execution backlog for Codex-style implementation (small, reviewable commits with explicit acceptance checks). + +## Conventions + +- Branch strategy: feature branch, one ticket per commit whenever practical. +- Verification baseline per ticket: + - `npm run typecheck` + - `npm test` +- Keep integrator UX simple: + - clone + - configure `.env` + - choose adapter + - run command (`doctor`, `test`, or `validate`) + +## Completed + +### T6 — Infrastructure Error Taxonomy + +Status: `DONE` + +Objective: + +- Add stable error-code taxonomy for infra/environment failures. +- Propagate error codes into recorded checks and report output. + +Implementation notes: + +- Extend diagnostics classification with `errorCode`. +- Include `errorCode` in report schema and console output. +- Update tests that record `BLOCKED`/`INCONCLUSIVE` diagnostics. + +Acceptance: + +- Report shows `Error code` on relevant checks. +- Unit tests cover code classification branches. + +Commit: + +- `8752f4ef` (`feat(diagnostics): add infrastructure error-code taxonomy`) + +### T7 — Doctor Preflight CLI + +Status: `DONE` + +Objective: + +- Add fast preflight diagnostics command before full harness run. + +Implementation notes: + +- Add `src/cli/doctor.ts`. +- Add `npm run doctor`. +- Document checks and exit codes in README/SUMMARY. + +Acceptance: + +- `npm run doctor` executes and returns: + - `0` (all pass), `2` (blocked), `3` (inconclusive), `99` (unexpected). + +Commit: + +- `640daac6` (`feat(cli): add doctor preflight command and docs`) + +### T8 — Claim-Based Validation Gate CLI + +Status: `DONE` + +Objective: + +- Add CI-friendly command with stable exit codes based on final claim. + +Implementation notes: + +- Add `src/cli/validate-policy.ts` (claim -> gate decision). +- Add `src/cli/validate.ts` (run harness + parse JSON artifact + apply gate policy). +- Add `npm run validate`. +- Add unit tests for policy logic. +- Document `VALIDATION_TARGET` and exit codes. + +Acceptance: + +- `npm run validate` exits with: + - `0` pass + - `10` partial + - `20` incompatible + - `30/31` inconclusive + - `97/98/99` runtime/config classes +- Policy behavior covered by unit tests. + +Commit: + +- `d7a55e91` (`feat(cli): add claim-based validate gate for CI`) + +## Next Priority + +### T12 — Deterministic Offline Harness Mode + +Status: `DONE` + +Objective: + +- Add deterministic local mode to prevent infra noise from polluting compatibility checks. + +Codex spec: + +1. Add runtime flag `HARNESS_MOCK_MODE`. +2. For network/relayer/registry-dependent checks, record `UNTESTED` with explicit rationale when enabled. +3. Document mode in `.env.example`, README, and summary docs. +4. Add unit tests for runtime flag parsing behavior. + +Acceptance: + +- `HARNESS_MOCK_MODE=1 npm test` marks dependent checks as `UNTESTED`. +- Standard mode behavior remains unchanged. +- Typecheck and tests pass. + +Commit: + +- pending (current working tree) + +### T9 — Turnkey Golden Scenario Fixture + +Status: `DONE` + +Objective: + +- Add deterministic Turnkey fixture docs and expected report profile for regression checks. + +Codex spec: + +1. Add `examples/turnkey/COMPATIBILITY.md` section with: + - expected capability profile, + - expected claim range (pass/partial/inconclusive depending env), + - known blockers and error codes. +2. Add script alias for turnkey validation gate: + - `validate:turnkey` +3. Add minimal smoke command section in README. + +Acceptance: + +- Turnkey docs clearly separate adapter incompatibility from infra blockers. +- `npm run validate:turnkey` works with standard env vars. + +Commit: + +- pending (current working tree) + +### T10 — Openfort Adapter Parity + +Status: `DONE` + +Objective: + +- Bring Openfort example up to same quality level as Turnkey/Crossmint examples. + +Codex spec: + +1. Validate `examples/openfort/signer.ts` metadata/capability declaration quality. +2. Add or refresh `examples/openfort/COMPATIBILITY.md`: + - explicit scope (EOA semantics baseline), + - what is not validated (embedded/session UX), + - expected verdict categories. +3. Add script alias: + - `validate:openfort` + +Acceptance: + +- Openfort example can be run with one command from README. +- Documentation is explicit on proof boundaries. + +Commit: + +- pending (current working tree) + +### T13 — Golden Report Regression Fixtures + +Status: `DONE` + +Objective: + +- Freeze representative report artifacts to catch parser/policy regressions. + +Codex spec: + +1. Add parseable golden report fixtures (full compatibility + infra-blocked scenario). +2. Add unit tests validating: + - artifact schema parsing, + - claim-to-gate mapping behavior. +3. Add strict runtime artifact parsing in `validate` CLI. + +Acceptance: + +- Golden fixture tests pass. +- `validate` rejects malformed or schema-incompatible artifacts with explicit errors. + +Commit: + +- pending (current working tree) + +### T11 — Report Consumer Guide + +Status: `DONE` + +Objective: + +- Improve machine-consumer usability of report artifacts. + +Codex spec: + +1. Add `docs/report-consumption.md` with: + - schema fields needed for CI gates, + - claim/evidence interpretation rules, + - backward compatibility guidance for `schemaVersion`. +2. Provide copy-paste examples for: + - auth-only gate, + - strict auth+write gate. + +Acceptance: + +- A partner engineer can build a parser without reading source code. + +Commit: + +- pending (current working tree) + +### T14 — Validate Policy File Support + +Status: `DONE` + +Objective: + +- Make validation gating configurable without code changes. + +Codex spec: + +1. Add `VALIDATION_POLICY_PATH` JSON policy support. +2. Support claim allow-list and partial acceptance controls. +3. Keep env overrides explicit (`VALIDATION_TARGET`, `VALIDATION_ALLOW_PARTIAL`). +4. Add unit tests for config parsing and policy application. + +Acceptance: + +- `validate` can read policy files and enforce expected claim IDs. +- `validate` can optionally accept partial outcomes via policy/env. +- Invalid policies fail with explicit config errors. + +Commit: + +- pending (current working tree) + +### T15 — Adapter Template Bootstrap Command + +Status: `DONE` + +Objective: + +- Reduce setup friction for new integrators creating a custom adapter module. + +Codex spec: + +1. Add `npm run init:adapter` CLI to scaffold a typed adapter template. +2. Support custom output paths. +3. Infer correct relative `Adapter` type import path for nested outputs. +4. Add unit tests for template path resolution/rendering. + +Acceptance: + +- Running `npm run init:adapter` creates a compilable starter adapter file. +- Custom path generation works without manual import fixes. +- Unit tests pass. + +Commit: + +- pending (current working tree) + +### T16 — Optional ERC-1271 Verification Probe + +Status: `DONE` + +Objective: + +- Improve smart-account diagnostics by probing ERC-1271 when declared by adapter metadata. + +Codex spec: + +1. Add optional `ERC-1271 Verification` check in identity/verification stage. +2. Trigger when adapter declares `SMART_ACCOUNT` architecture or `ERC1271` verification model. +3. Record pass/fail/blocked outcomes with infra-aware diagnostics. +4. Keep recoverability failure semantics unchanged for Zama auth claims. + +Acceptance: + +- Harness reports explicit ERC-1271 check outcomes when applicable. +- Smart-account diagnostics improve without overclaiming Zama authorization compatibility. + +Commit: + +- pending (current working tree) + +### T17 — Network Profile Scaffolding + +Status: `DONE` + +Objective: + +- Move from hardcoded Sepolia assumptions to explicit profile-based network configuration. + +Codex spec: + +1. Introduce `NETWORK_PROFILE` (`sepolia`, `mainnet`) with typed parsing/building. +2. Keep Sepolia defaults backward compatible. +3. Require explicit `RELAYER_URL` on mainnet profile. +4. Surface profile/support metadata in diagnostics (`doctor`). +5. Add unit tests for profile/config parsing and validation. + +Acceptance: + +- Default behavior remains unchanged on Sepolia. +- Misconfigured mainnet profile fails fast with actionable error. +- Network profile selection is documented in README and `.env.example`. + +Commit: + +- pending (current working tree) + +### T18 — GitHub Actions Baseline CI + +Status: `DONE` + +Objective: + +- Add a stable CI workflow that validates harness integrity without network flakiness. + +Codex spec: + +1. Add GitHub Actions workflow for install, typecheck, tests, and validate. +2. Run tests in deterministic `HARNESS_MOCK_MODE=1`. +3. Accept deterministic validate outcomes (`0`, `10`, `30`) and fail on hard errors. +4. Document CI behavior in README. + +Acceptance: + +- CI is reproducible without requiring funded keys or live relayer dependency. +- Workflow still fails on incompatibility/config/runtime regressions. + +Commit: + +- pending (current working tree) + +## Definition of Done (Project-Level) + +- Adapter model remains capability-first and conservative. +- Verdicts are scoped and claim-based (no overclaiming). +- Infra/environment blockers are separated from compatibility failures. +- Built-in examples (Crossmint, Turnkey, Openfort) are runnable and documented. +- README remains <10-minute onboarding for new integrators. + +## Harness-Only Strong Validation Plan + +Objective: + +- Reach strong, trustworthy compatibility validation quickly without adding backend/hosted services. + +Execution rules: + +- One ticket = one reviewable commit when practical. +- Keep integrator path simple (`doctor`, `test`, `validate`) and deterministic CI baseline. +- No claim broadening without explicit evidence from checks. + +### Phase A — Conformance Engine Hardening + +### T19 — Canonical Check Registry + +Status: `DONE` + +Codex spec: + +1. Add a single source of truth for checks (`id`, display name, section, dependencies). +2. Require report entries to reference canonical check IDs. +3. Reject unknown check names in report assembly. + +Acceptance: + +- Reporter/build fails fast on unknown check names. +- Unit tests verify registry coverage and dependency ordering. + +### T20 — Negative Test Matrix + +Status: `DONE` + +Codex spec: + +1. Add explicit negative-path tests per core surface: + - EIP-712 signing failure, + - recoverability mismatch/non-recoverable signature, + - authorization rejection, + - write submission failure. +2. Ensure each path maps to expected `status`, `rootCauseCategory`, `errorCode`. + +Acceptance: + +- Each critical surface has at least one deterministic negative test. +- No negative-path claim regression in verdict matrix tests. + +### T21 — Claim/Status Consistency Guard + +Status: `DONE` + +Codex spec: + +1. Add validator that checks claim evidence vs recorded statuses. +2. Fail report export if claim references missing/incompatible checks. + +Acceptance: + +- Invalid claim/evidence combinations are caught in unit tests. +- `validate` refuses inconsistent artifacts. + +### Phase B — Evidence and Diagnostics Strengthening + +### T22 — Structured Evidence Payloads + +Status: `DONE` + +Codex spec: + +1. Add `evidence.details` in claim payload: + - check ID, + - status, + - short reason category. +2. Keep backward compatibility for existing `claim.evidence`. + +Acceptance: + +- JSON artifact includes machine-usable evidence detail records. +- Parser/unit tests cover both required and optional evidence fields. + +### T23 — Recommendation Map v2 + +Status: `DONE` + +Codex spec: + +1. Centralize recommendations by `errorCode`. +2. Attach `nextCommand` hints where possible (`doctor`, `validate`, env fix). + +Acceptance: + +- BLOCKED/INCONCLUSIVE checks emit deterministic recommendation text. +- Recommendation mapping covered by unit tests. + +### T24 — Artifact Compatibility Contract Tests + +Status: `DONE` + +Codex spec: + +1. Add fixture set for: + - current schema, + - previous supported schema (if any), + - malformed variants. +2. Validate parser behavior and error messaging. + +Acceptance: + +- Parser behavior on malformed artifacts is deterministic and explicit. +- Golden fixtures prevent accidental schema drift. + +### Phase C — Integrator Confidence Pack + +### T25 — Example Baseline Lockfiles + +Status: `DONE` + +Codex spec: + +1. Add expected-profile fixtures for Openfort/Turnkey/Crossmint. +2. Add unit checks that claim outputs remain within expected ranges per example profile. + +Acceptance: + +- Example regressions are detected without live credentials. +- Fixture docs explain expected PASS/PARTIAL/INCONCLUSIVE envelopes. + +### T26 — Adapter Quality Gate Command + +Status: `DONE` + +Codex spec: + +1. Add `npm run adapter:check`: + - validates adapter metadata completeness, + - validates declared capabilities shape, + - checks obvious contradictions. +2. Reuse canonical check registry where relevant. + +Acceptance: + +- Mis-specified adapters fail fast before runtime tests. +- Command output is actionable for external integrators. + +### T27 — CI Dual Mode (Deterministic + Optional Live) + +Status: `DONE` + +Codex spec: + +1. Keep deterministic workflow mandatory. +2. Add optional live workflow (manual/nightly) with artifacts upload. +3. Tag live runs as non-blocking by default. + +Acceptance: + +- Deterministic CI remains stable. +- Live runs preserve JSON artifacts for debugging. + +### Phase D — Documentation for External Teams + +### T28 — Verdict Interpretation Playbook + +Status: `DONE` + +Codex spec: + +1. Add guide mapping each final claim to: + - what is proven, + - what is not proven, + - required next action. +2. Include examples for partner conversations. + +Acceptance: + +- External engineer can interpret verdict scope without reading code. + +### T29 — Claim Catalog + +Status: `DONE` + +Codex spec: + +1. Document each claim ID, trigger conditions, and confidence scope. +2. Link claim IDs to validation gate policy examples. + +Acceptance: + +- Claim semantics are stable, explicit, and docs-backed. + +### T30 — Release/Schema Discipline + +Status: `DONE` + +Codex spec: + +1. Define `schemaVersion` bump policy and changelog template. +2. Add release checklist for harness-only quality gate. + +Acceptance: + +- Any schema-affecting change includes explicit versioning rationale. +- Release notes are sufficient for downstream parser maintainers. diff --git a/examples/compatibility-harness/README.md b/examples/compatibility-harness/README.md new file mode 100644 index 000000000..558fab94d --- /dev/null +++ b/examples/compatibility-harness/README.md @@ -0,0 +1,543 @@ +# Zama Compatibility Validation Harness + +Diagnostic harness to evaluate whether a wallet/custody integration is compatible with the Zama SDK (Sepolia by default, mainnet profile available as experimental). + +The harness is adapter-based (not signer-only) and reports: + +- what your integration exposes (capabilities), +- what was actually validated (checks + statuses), +- what claim is safe to make (conservative final verdict). +- where validation was blocked by infrastructure vs adapter/signer behavior. + +## What This Harness Can Prove + +It can validate, depending on adapter support: + +- identity and address resolution, +- EIP-712 signing and `ecrecover` recoverability, +- raw EOA transaction signing + broadcast, +- adapter-routed contract execution, +- contract reads either via adapter or harness RPC fallback (when adapter read is absent), +- Zama authorization flow (`sdk.allow()`), +- a practical Zama write probe (operator approval write + on-chain verification). + +It can also separate incompatibility from infrastructure blockers: + +- adapter/signer defects, +- RPC/network issues, +- relayer issues, +- registry/token discovery issues, +- environment/configuration issues. + +## What It Cannot Prove + +- Full production readiness of your integration. +- Every possible Zama SDK write/read path. +- Full deterministic guarantees for non-Sepolia environments (mainnet profile is currently experimental). + +If a surface is not tested or not supported by the adapter, the report says so explicitly (`UNTESTED` or `UNSUPPORTED`) and the verdict remains partial. + +## Quickstart + +```bash +git clone +cd examples/compatibility-harness +npm install +cp .env.example .env +``` + +### Option A: Built-in EOA adapter + +Set `PRIVATE_KEY` in `.env` (Sepolia-funded account), then run: + +```bash +npm test +``` + +Local deterministic mode (no network/relayer/registry dependency): + +```bash +HARNESS_MOCK_MODE=1 npm test +``` + +In mock mode, network-dependent checks are explicitly marked `UNTESTED`. + +### Option B: Crossmint example adapter + +Set `CROSSMINT_API_KEY` and `CROSSMINT_WALLET_LOCATOR` in `.env`, then run: + +```bash +npm run test:crossmint +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/crossmint/signer.ts npm test +``` + +Optional Crossmint-specific commands: + +```bash +npm run doctor:crossmint +npm run validate:crossmint +``` + +### Option C: Turnkey example adapter + +Set Turnkey credentials in `.env` (`TURNKEY_ORG_ID`, `TURNKEY_PRIVATE_KEY_ID`, `TURNKEY_API_PUBLIC_KEY`, `TURNKEY_API_PRIVATE_KEY`), then run: + +```bash +npm run test:turnkey +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/turnkey/signer.ts npm test +``` + +Optional Turnkey-specific commands: + +```bash +npm run doctor:turnkey +npm run validate:turnkey +``` + +### Option D: Openfort baseline adapter (EOA semantics) + +Set `OPENFORT_TEST_PRIVATE_KEY` in `.env`, then run: + +```bash +npm run test:openfort +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/openfort/signer.ts npm test +``` + +This adapter validates Openfort-compatible EOA signing/execution semantics in CLI. It does not validate embedded browser auth/session UX. + +Optional Openfort-specific commands: + +```bash +npm run doctor:openfort +npm run validate:openfort +``` + +## Preflight Doctor (Optional) + +Run a fast environment preflight before the full suite: + +```bash +npm run doctor +``` + +With a custom adapter: + +```bash +SIGNER_MODULE=./examples/turnkey/signer.ts npm run doctor +``` + +Doctor checks: + +- adapter module loading, +- declared capabilities visibility, +- adapter init and address resolution, +- RPC connectivity + chain match, +- relayer reachability. + +Exit codes: + +- `0`: all checks passed, +- `2`: at least one `BLOCKED` issue (usually env/config), +- `3`: at least one `INCONCLUSIVE` issue (usually infra/network), +- `99`: unexpected harness/runtime error. + +## Adapter Quality Gate (Optional) + +Run a static adapter-spec quality check (no live Zama flow execution): + +```bash +npm run adapter:check +``` + +With example adapters: + +```bash +npm run adapter:check:openfort +npm run adapter:check:turnkey +npm run adapter:check:crossmint +``` + +This command validates: + +- metadata completeness (`name`, architecture, verification model, supported chains), +- declared capability shape, +- declared vs observed capability contradictions, +- canonical check support preview (from declared capabilities). + +Exit codes: + +- `0`: no quality-gate failures, +- `2`: adapter spec quality failures detected, +- `99`: unexpected runtime failure. + +## Validation Gate (CI-Friendly) + +Run the full harness and return a stable exit code based on the final `claim`: + +```bash +npm run validate +``` + +By default, the gate target is `AUTHORIZATION`. You can require strict auth+write validation: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE npm run validate +``` + +You can also provide a JSON policy file: + +```bash +VALIDATION_POLICY_PATH=./validation-policy.example.json npm run validate +``` + +Policy behavior: + +- `VALIDATION_TARGET` overrides `policy.target` when both are set. +- `VALIDATION_ALLOW_PARTIAL=true` can promote `PARTIAL` gate results to exit code `0`. +- `policy.expectedClaims` can restrict accepted claim IDs. + +Validation gate exit codes: + +- `0`: requested compatibility target validated, +- `10`: partially validated (typically auth passed, write not fully validated, strict target only), +- `20`: incompatible, +- `21`: claim rejected by policy `expectedClaims`, +- `30`: inconclusive (blocked/untested authorization claim), +- `31`: unknown claim mapping, +- `97`: invalid gate config or unreadable report artifact, +- `98`: test runner execution failure, +- `99`: unexpected CLI runtime failure. + +`validate` enforces report artifact contract (`kind` + `schemaVersion` + required claim fields) before applying gate policy. + +## GitHub Actions CI + +Workflow file: + +- `.github/workflows/compatibility-harness-ci.yml` +- `.github/workflows/compatibility-harness-live.yml` (optional manual/nightly, non-blocking) + +Pipeline stages: + +1. `npm ci` +2. `npm run typecheck` +3. `HARNESS_MOCK_MODE=1 npm test` +4. `HARNESS_MOCK_MODE=1 npm run validate` + +The validate step accepts exit codes `0`, `10`, and `30` in deterministic mock mode. +This keeps CI stable while still failing on true regressions (`20`, `21`, `31`, `97+`). + +Optional live workflow (`compatibility-harness-live.yml`): + +- triggers on `workflow_dispatch` and nightly schedule, +- runs `npm run validate` with live env/secrets, +- captures `validate` exit code without blocking the pipeline by default, +- uploads `reports/live/*` artifacts (JSON + logs) for debugging and partner traces. + +## Adapter Model (Primary Interface) + +Bootstrap a custom adapter template: + +```bash +npm run init:adapter +``` + +Optional custom output path: + +```bash +npm run init:adapter -- ./examples/my-provider/signer.ts +``` + +Optional template presets: + +```bash +npm run init:adapter -- --template eoa +npm run init:adapter -- --template mpc +npm run init:adapter -- --template api-routed +npm run init:adapter -- --template turnkey +npm run init:adapter -- --template crossmint +npm run init:adapter -- --template openfort +``` + +`turnkey`, `crossmint`, and `openfort` presets are scaffolding shortcuts based on the example adapters in this repository. They are starter files, not production-certified implementations. + +Show help: + +```bash +npm run init:adapter -- --help +``` + +Then point the harness to your file: + +```bash +SIGNER_MODULE=./examples/my-provider/signer.ts npm test +``` + +Provide a module exporting `adapter` (preferred). The harness also accepts legacy `signer` exports for backward compatibility. + +```ts +import type { Adapter } from "./src/adapter/types.js"; + +export const adapter: Adapter = { + metadata: { + name: "My Adapter", + declaredArchitecture: "UNKNOWN", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + }, + capabilities: { + eip712Signing: "SUPPORTED", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + }, + async init() { + // optional async initialization + }, + async getAddress() { + return "0x..."; + }, + async signTypedData(data) { + return "0x..."; + }, + async writeContract(config) { + return "0x..."; // tx hash + }, +}; +``` + +Minimal integration flow for a first pass: + +1. Generate an EOA baseline template: + +```bash +npm run init:adapter -- --template eoa --output ./examples/my-provider/signer.ts +``` + +2. Replace the `throw new Error(...)` placeholders with your SDK/provider calls. +3. Run fast static checks: + +```bash +SIGNER_MODULE=./examples/my-provider/signer.ts npm run adapter:check +``` + +4. Run preflight connectivity checks: + +```bash +SIGNER_MODULE=./examples/my-provider/signer.ts npm run doctor +``` + +5. Run full harness tests: + +```bash +SIGNER_MODULE=./examples/my-provider/signer.ts npm test +``` + +No harness source changes are required. + +## Capability Model + +The harness tracks these capabilities independently from verdicts: + +- `addressResolution` +- `eip712Signing` +- `recoverableEcdsa` +- `rawTransactionSigning` +- `contractExecution` +- `contractReads` +- `transactionReceiptTracking` +- `zamaAuthorizationFlow` +- `zamaWriteFlow` + +Capability state: + +- `SUPPORTED` +- `UNSUPPORTED` +- `UNKNOWN` + +The adapter profile reports both: + +- `declaredCapabilities` (what the adapter says it supports) +- `observedCapabilities` (what the harness observed from behavior/tests) + +When they diverge, the report includes explicit `contradictions`. + +## Status Model + +Every check uses one status: + +- `PASS` +- `FAIL` +- `UNTESTED` +- `UNSUPPORTED` +- `BLOCKED` +- `INCONCLUSIVE` + +This avoids false pass/fail claims when something is unavailable or blocked by infrastructure. + +## Adapter Classification + +Architecture values: + +- `EOA` +- `MPC` +- `SMART_ACCOUNT` +- `API_ROUTED_EXECUTION` +- `UNKNOWN` + +The report combines declared metadata and observed behavior. If evidence is weak or contradictory, classification remains `UNKNOWN`. + +## Test Surface + +Current checks: + +1. Adapter initialization and address resolution +2. EIP-712 signing + recoverability +3. Optional ERC-1271 signature verification (for declared smart-account/ERC-1271 adapters) +4. Raw transaction execution (when supported) +5. Contract read validation (adapter read when available, otherwise harness RPC fallback) +6. Zama authorization flow (`sdk.allow()`) +7. Zama write flow probe (operator approval write + verification) + +## Final Verdict Model + +The harness emits nuanced verdicts, for example: + +- `ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS` +- `ZAMA COMPATIBLE FOR AUTHORIZATION FLOWS — WRITE FLOW NOT TESTED` +- `PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW UNSUPPORTED` +- `PARTIALLY VALIDATED — AUTHORIZATION PASSED, RECOVERABILITY NOT CONFIRMED` +- `INCOMPATIBLE — ZAMA AUTHORIZATION FLOW FAILED` +- `INCONCLUSIVE — AUTHORIZATION FLOW BLOCKED BY ENVIRONMENT OR INFRASTRUCTURE` + +Verdicts are based on what was actually validated, not on assumptions. + +## Report Sections + +The output is split into: + +- Adapter Profile +- Ethereum Compatibility +- Adapter-Routed Execution +- Zama SDK Compatibility +- Infrastructure / Environment +- Final Verdict + +Each failing/blocked/inconclusive check includes cause and recommendation. +The infrastructure section is synthesized from root-cause evidence across checks (RPC, relayer, registry, local env). +For `BLOCKED`/`INCONCLUSIVE`, recommendation text is deterministic by `errorCode` and includes next-command hints (`doctor` / `validate` / `test`). +Checks may include stable `errorCode` values (for CI triage), e.g.: + +- `ENV_MISSING_CONFIG`, `ENV_INVALID_CONFIG`, `ENV_INSUFFICIENT_FUNDS` +- `RPC_CONNECTIVITY`, `RPC_RATE_LIMIT` +- `RELAYER_UNAVAILABLE` +- `REGISTRY_EMPTY`, `REGISTRY_UNAVAILABLE` + +### Optional JSON artifact + +Set `REPORT_JSON_PATH` to export a machine-readable report payload at the end of the run. + +```bash +REPORT_JSON_PATH=./reports/latest.json npm test +``` + +Current schema: + +- `kind`: `zama-compatibility-report` +- `schemaVersion`: `1.3.0` (parser accepts `1.2.0` and `1.3.0`) +- each check record includes a canonical `checkId` (stable machine key) +- claim payload includes: + - `evidence` (stable map) + - optional `evidenceDetails` (structured check records) + - `confidence` (`HIGH` | `MEDIUM` | `LOW`) +- optional `zama` section includes `writeValidationDepth` (`FULL` | `PARTIAL` | `UNTESTED`) +- top-level sections: `adapterProfile`, `checks`, `sections`, `infrastructure`, `claim`, `finalVerdict` + +`schemaVersion` is the compatibility contract for CI/partner tooling. Consumers should validate `schemaVersion` before parsing. + +See also: [`docs/report-consumption.md`](./docs/report-consumption.md) for CI parsing and gating patterns. +For partner-facing interpretation, see [`docs/verdict-playbook.md`](./docs/verdict-playbook.md). +For stable claim semantics, see [`docs/claim-catalog.md`](./docs/claim-catalog.md). +For schema/release contract policy, see [`docs/schema-release-discipline.md`](./docs/schema-release-discipline.md). + +## Environment Variables + +Copy `.env.example` to `.env` and fill only what your adapter needs. + +Common variables: + +- `NETWORK_PROFILE` (optional; `sepolia` default, or `mainnet` with explicit `RELAYER_URL`) +- `PRIVATE_KEY` (required for built-in EOA adapter) +- `CROSSMINT_API_KEY` / `CROSSMINT_WALLET_LOCATOR` (Crossmint example) +- `TURNKEY_ORG_ID` / `TURNKEY_PRIVATE_KEY_ID` / `TURNKEY_API_PUBLIC_KEY` / `TURNKEY_API_PRIVATE_KEY` (Turnkey example) +- `OPENFORT_TEST_PRIVATE_KEY` (Openfort EOA baseline example) +- `RPC_URL` (optional; defaults to public Sepolia RPC) +- `RELAYER_URL` (optional on Sepolia, required on `NETWORK_PROFILE=mainnet`) +- `RELAYER_API_KEY` (optional; needed for private/protected relayers) +- `REPORT_JSON_PATH` (optional; write final report JSON to this file path) +- `HARNESS_MOCK_MODE` (optional; when enabled, marks network-dependent checks as `UNTESTED`) +- `VALIDATION_POLICY_PATH` (optional; JSON policy for `npm run validate`) +- `VALIDATION_ALLOW_PARTIAL` (optional; override policy to accept `PARTIAL` as pass) + +### Network Profiles + +- `NETWORK_PROFILE=sepolia`: + - default profile, + - defaults RPC/relayer to public testnet values, + - support level: `SUPPORTED`. +- `NETWORK_PROFILE=mainnet`: + - requires explicit `RELAYER_URL`, + - support level: `EXPERIMENTAL`, + - intended for controlled partner/mainnet validation environments. + +## Integrator Workflow (Target: < 10 min) + +1. Clone + install +2. Configure `.env` +3. Point `SIGNER_MODULE` to your adapter (or use built-in EOA) +4. Run `npm test` +5. Read report by sections and final verdict + +## Example References + +See: + +- [`examples/crossmint/signer.ts`](./examples/crossmint/signer.ts) +- [`examples/crossmint/COMPATIBILITY.md`](./examples/crossmint/COMPATIBILITY.md) +- [`examples/turnkey/signer.ts`](./examples/turnkey/signer.ts) +- [`examples/turnkey/COMPATIBILITY.md`](./examples/turnkey/COMPATIBILITY.md) +- [`examples/openfort/signer.ts`](./examples/openfort/signer.ts) +- [`examples/openfort/COMPATIBILITY.md`](./examples/openfort/COMPATIBILITY.md) +- baseline lockfiles used by regression tests: [`src/tests/fixtures/example-baselines`](./src/tests/fixtures/example-baselines) + +## Legacy Compatibility + +Legacy modules exporting `signer` are still supported via automatic wrapping, but new integrations should export `adapter` directly. + +## Commands + +```bash +npm run typecheck +npm test +npm run adapter:check +npm run test:crossmint +npm run test:openfort +npm run test:turnkey +npm run validate:crossmint +npm run validate:openfort +npm run validate:turnkey +``` diff --git a/examples/compatibility-harness/SUMMARY.md b/examples/compatibility-harness/SUMMARY.md new file mode 100644 index 000000000..3b90847e7 --- /dev/null +++ b/examples/compatibility-harness/SUMMARY.md @@ -0,0 +1,217 @@ +# Zama Compatibility Validation Harness — Technical Summary + +## Purpose + +This harness evaluates whether an integration system (wallet, MPC, custody API, smart-account stack) is compatible with the Zama SDK, with Sepolia as the default validated profile and a mainnet profile scaffold marked experimental. + +The key objective is trustworthiness: + +- avoid overclaiming compatibility, +- separate product incompatibility from infra failures, +- report exactly what was validated. + +## Core Design + +The internal model is adapter-based: + +- primary export: `adapter` (preferred), +- legacy support: `signer` auto-wrapped for backward compatibility. + +Adapter shape includes: + +- metadata (name, declared architecture, verification model, chain support), +- capability declarations, +- operational primitives (`getAddress`, `signTypedData`, `signTransaction`, `writeContract`, optional reads/receipt tracking), +- optional async initialization. + +## Capability and Status Models + +Capabilities are tracked independently from outcomes: + +- `addressResolution` +- `eip712Signing` +- `recoverableEcdsa` +- `rawTransactionSigning` +- `contractExecution` +- `contractReads` +- `transactionReceiptTracking` +- `zamaAuthorizationFlow` +- `zamaWriteFlow` + +Capability state: + +- `SUPPORTED` +- `UNSUPPORTED` +- `UNKNOWN` + +Profile output separates: + +- `declaredCapabilities` +- `observedCapabilities` +- `contradictions` when declared and observed states diverge. + +Validation status: + +- `PASS` +- `FAIL` +- `UNTESTED` +- `UNSUPPORTED` +- `BLOCKED` +- `INCONCLUSIVE` + +This prevents false binary conclusions. + +## Classification Model + +Architectures: + +- `EOA` +- `MPC` +- `SMART_ACCOUNT` +- `API_ROUTED_EXECUTION` +- `UNKNOWN` + +Verification models: + +- `RECOVERABLE_ECDSA` +- `ERC1271` +- `PROVIDER_MANAGED` +- `UNKNOWN` + +Classification combines declared metadata and observed behavior. Contradictions degrade to `UNKNOWN`. + +## Validation Surface (Current) + +Test order: + +1. Adapter profile (init + address resolution) +2. EIP-712 signing and recoverability +3. Optional ERC-1271 verification for declared smart-account/ERC-1271 integrations +4. Raw EOA transaction execution (if supported) +5. Contract read validation (adapter read when available, otherwise harness RPC fallback) +6. Zama authorization (`sdk.allow()`) +7. Zama write probe (operator approval write + on-chain verification) + +## Reporting Model + +The report is grouped into: + +- Adapter Profile +- Ethereum Compatibility +- Adapter-Routed Execution +- Zama SDK Compatibility +- Infrastructure / Environment +- Final Verdict + +Each failing or blocked item includes: + +- reason, +- root-cause category, +- stable error code, +- recommendation. + +The infrastructure/environment section is synthesized from observed root causes across all checks. + +Root-cause categories: + +- `ADAPTER` +- `SIGNER` +- `RPC` +- `RELAYER` +- `REGISTRY` +- `ENVIRONMENT` +- `HARNESS` + +Error codes (current taxonomy): + +- `ENV_MISSING_CONFIG` +- `ENV_INVALID_CONFIG` +- `ENV_INSUFFICIENT_FUNDS` +- `RPC_CONNECTIVITY` +- `RPC_RATE_LIMIT` +- `RELAYER_UNAVAILABLE` +- `REGISTRY_EMPTY` +- `REGISTRY_UNAVAILABLE` +- `HARNESS_UNKNOWN` + +## Final Verdict Strategy + +Verdicts are conservative and tied to validated Zama surface, for example: + +- `ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS` +- `ZAMA COMPATIBLE FOR AUTHORIZATION FLOWS — WRITE FLOW NOT TESTED` +- `PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW UNSUPPORTED` +- `PARTIALLY VALIDATED — AUTHORIZATION PASSED, RECOVERABILITY NOT CONFIRMED` +- `INCOMPATIBLE — ZAMA AUTHORIZATION FLOW FAILED` +- `INCONCLUSIVE — AUTHORIZATION FLOW BLOCKED BY ENVIRONMENT OR INFRASTRUCTURE` + +No generic “COMPATIBLE” claim is emitted without scope. + +## Infrastructure Handling + +Infrastructure and environment failures are explicitly separated from adapter incompatibility: + +- configuration defects (`.env`, missing keys) -> `BLOCKED / ENVIRONMENT` +- RPC/network issues -> `INCONCLUSIVE / RPC` +- relayer availability/errors -> `INCONCLUSIVE / RELAYER` +- token registry discovery issues -> `BLOCKED / REGISTRY` + +This reduces false negatives for integrators. + +## Integrator Experience + +Current workflow remains lightweight: + +1. clone, +2. set `.env`, +3. (optional) run `npm run doctor` for preflight diagnostics, +4. (optional) run `npm run adapter:check` for static adapter quality checks, +5. provide adapter (or scaffold one via `npm run init:adapter`), +6. run tests, +7. read structured report and scoped verdict. + +For deterministic local runs without RPC/relayer dependencies, set `HARNESS_MOCK_MODE=1`; network-dependent checks are recorded as `UNTESTED` instead of being conflated with compatibility failures. + +For CI and automated go/no-go checks, `npm run validate` executes the suite, reads the JSON artifact claim, and returns policy-oriented exit codes. Gate target is configurable via `VALIDATION_TARGET` (`AUTHORIZATION` or `AUTHORIZATION_AND_WRITE`). + +`validate` also supports JSON policy files (`VALIDATION_POLICY_PATH`) for explicit claim allow-lists and partial-acceptance behavior, while enforcing the report schema contract before decisioning. + +GitHub Actions includes: + +- mandatory deterministic CI (`compatibility-harness-ci.yml`), +- optional live manual/nightly run with artifact upload (`compatibility-harness-live.yml`, non-blocking by default). + +Operational companion docs: + +- `docs/verdict-playbook.md` +- `docs/claim-catalog.md` +- `docs/schema-release-discipline.md` + +Reference examples currently included: + +- Crossmint API-routed adapter +- Turnkey API key adapter +- Openfort EOA baseline adapter (CLI compatibility baseline for EOA semantics) + +## Report Artifact + +An optional machine-readable report can be exported with: + +- `REPORT_JSON_PATH=./reports/latest.json npm test` + +Versioned schema contract: + +- `kind`: `zama-compatibility-report` +- `schemaVersion`: `1.3.0` (parser remains backward-compatible with `1.2.0`) +- payload includes profile, recorded checks, synthesized environment summary, section views, blockers, optional `zama.writeValidationDepth`, claim metadata (`evidence` + optional `evidenceDetails` + `confidence`), verdict, and run id. + +## Scope Limits + +The harness is still a practical diagnostic tool, not a production certification authority. + +Out of scope today: + +- deterministic guarantees for all non-Sepolia environments (mainnet profile is currently marked experimental), +- exhaustive Zama write/read behavior coverage, +- full ERC-1271 validation matrix, +- full certification-level CI policy automation. diff --git a/examples/compatibility-harness/SUMMARY_V2.md b/examples/compatibility-harness/SUMMARY_V2.md new file mode 100644 index 000000000..c748f19e2 --- /dev/null +++ b/examples/compatibility-harness/SUMMARY_V2.md @@ -0,0 +1,229 @@ +# Zama Compatibility Validation Harness — Summary V2 + +## 1) Context: Why This Harness Matters + +Teams integrating Zama SDK need a reliable answer to: + +> Is our signing/execution system actually compatible with Zama flows? + +This is difficult because real integrations are heterogeneous: + +- EOA wallets, +- MPC systems, +- API-routed custody systems, +- smart-account / ERC-1271 models. + +Without a dedicated compatibility harness, teams typically discover issues late, during full integration. This causes: + +- slow diagnosis, +- confusion between infra errors and real incompatibilities, +- weak or overconfident compatibility claims. + +This project solves that by providing a conservative, evidence-driven validation process before full integration work. + +## 2) Problem and Solution in One Page + +### Problem addressed + +- Integrators do not expose the same primitives. +- A pass/fail-only model is misleading. +- Zama compatibility can be partial (for example: authorization works, write path not validated). + +### Solution implemented + +The harness uses an adapter model and evaluates multiple surfaces with explicit statuses: + +- `PASS`, `FAIL`, `UNTESTED`, `UNSUPPORTED`, `BLOCKED`, `INCONCLUSIVE`. + +It separates: + +- compatibility failures (adapter/signer behavior), +- infrastructure blockers (RPC, relayer, registry, environment). + +It outputs a scoped final claim, plus: + +- `Confidence` (`HIGH`/`MEDIUM`/`LOW`), +- `Write Validation Depth` (`FULL`/`PARTIAL`/`UNTESTED`). + +## 3) What “Compatible” Means in This Harness + +The harness intentionally avoids vague “compatible/incompatible” statements. + +Practical compatibility levels: + +1. Authorization-compatible: + - authorization flow validated, + - recoverability semantics acceptable for the tested model, + - write surface may still be partial or untested. +2. Authorization + write compatible: + - authorization validated, + - write flow validated with stronger evidence (`write depth` and overall claim). + +If evidence is incomplete, the harness keeps the verdict partial or inconclusive. + +## 4) Exact Integrator Workflow + +### Step 1: Setup + +```bash +git clone +cd examples/compatibility-harness +npm install +cp .env.example .env +``` + +### Step 2: Choose and generate adapter template + +Template selection: + +- choose `eoa` if your system exposes standard EOA signing/execution. +- choose `mpc` if signatures are ECDSA but raw tx signing is not exposed. +- choose `api-routed` if execution happens through provider APIs. +- choose `generic` if uncertain. + +Generate: + +```bash +npm run init:adapter -- --template --output ./examples/my-provider/signer.ts +``` + +### Step 3: Implement the generated adapter + +Required fields/methods to fill: + +1. `metadata.name` +2. `metadata.declaredArchitecture` +3. `metadata.verificationModel` +4. `capabilities` (honest declaration: `SUPPORTED`/`UNSUPPORTED`/`UNKNOWN`) +5. `getAddress()` +6. `signTypedData()` when supported +7. `writeContract()` when supported + +Minimal adapter shape: + +```ts +export const adapter: Adapter = { + metadata: { ... }, + capabilities: { ... }, + async getAddress() { ... }, + async signTypedData(data) { ... }, + async writeContract(config) { ... }, +}; +``` + +### Step 4: Implement methods by architecture (required vs optional) + +| Architecture | Required methods | Usually optional | +| ---------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| `EOA` | `getAddress`, `signTypedData`, `writeContract` | `signTransaction`, `readContract`, `waitForTransactionReceipt` | +| `MPC` | `getAddress`, `signTypedData`, `writeContract` | `signTransaction` (often unsupported), `readContract`, `waitForTransactionReceipt` | +| `API_ROUTED_EXECUTION` | `getAddress`, `writeContract` (+ `signTypedData` if auth flow supported) | `signTransaction` (often unsupported), `readContract`, `waitForTransactionReceipt` | +| `SMART_ACCOUNT` | `getAddress`, execution methods exposed by your system | `signTransaction` depends on model; `signTypedData` may be provider-managed | + +### Step 5: Fill `.env` + +Minimum checklist: + +- provider credentials required by your adapter, +- `RPC_URL` for your target chain, +- `RELAYER_URL` if required by your network/profile, +- funded account when writes are tested, +- optional `REPORT_JSON_PATH` if you want a saved artifact path. + +If `doctor` reports invalid/missing config, fix `.env` first and rerun. + +### Step 6: Run commands in order + +```bash +SIGNER_MODULE=./examples/my-provider/signer.ts npm run adapter:check +SIGNER_MODULE=./examples/my-provider/signer.ts npm run doctor +SIGNER_MODULE=./examples/my-provider/signer.ts npm test +SIGNER_MODULE=./examples/my-provider/signer.ts npm run validate +``` + +Strict gate for authorization + write: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE SIGNER_MODULE=./examples/my-provider/signer.ts npm run validate +``` + +## 5) How To Read Results (Simple) + +Read in this order: + +1. `Final` +2. `Claim` +3. `Confidence` +4. `Write Validation Depth` +5. per-check failures and root-cause category + +### Compact output example + +```text +Adapter Initialization PASS +Address Resolution PASS +EIP-712 Signing PASS +EIP-712 Recoverability PASS +Raw Transaction Execution UNSUPPORTED +Zama Authorization Flow PASS +Zama Write Flow PASS +Final: ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS +Write Validation Depth: FULL +Confidence: HIGH +Claim: ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE +``` + +Interpretation: strong evidence for auth+write compatibility in the tested environment, with raw tx unsupported by design. + +## 6) Post-Run Decision Tree + +1. If `Final` is authorization/write compatible and confidence is acceptable: + - proceed to integration pilot scope. +2. If `FAIL` occurs on signer/adapter checks: + - fix adapter implementation or provider integration behavior. +3. If `BLOCKED`/`INCONCLUSIVE` due to infra: + - fix environment/RPC/relayer/registry and rerun before concluding. +4. If result is partial (`UNSUPPORTED`/`UNTESTED`): + - keep claim scoped; do not communicate full compatibility. + +## 7) Common Failure Patterns and Next Action + +| Pattern | Typical cause | Action | +| ----------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- | +| `Address Resolution = BLOCKED` | invalid/missing credentials or address config | fix `.env`, rerun `doctor` | +| `EIP-712 Signing = FAIL` | signer implementation not matching expected typed-data flow | fix adapter signing method and payload mapping | +| `Recoverability = FAIL` | non-recoverable or malformed signature for tested model | verify signature format and verification model assumptions | +| `Zama Authorization = INCONCLUSIVE` | relayer/RPC/registry outage | stabilize infra, rerun | +| `Zama Write = PARTIAL/UNTESTED` | write path unsupported or not fully observed | implement/enable write flow or keep scoped claim | +| `validate` exits `30` | inconclusive authorization evidence | treat as blocked validation, not incompatibility | + +## 8) Current Boundaries + +This harness is strong for integration diagnostics, but it is not: + +- a global permanent certification authority, +- a guarantee of all possible SDK/runtime paths, +- a frontend UX/session lifecycle validation tool. + +## 9) What To Share With Zama For Review + +When asking Zama for technical review, share: + +1. final claim (`Claim` + `Final` line), +2. confidence level, +3. write validation depth, +4. list of non-pass checks with root-cause categories, +5. adapter metadata and declared capabilities, +6. command context (network/profile, key env settings, date of run). + +This enables fast, reproducible partner-level discussions. + +## 10) References + +Reference adapter implementations are available in: + +- `examples/crossmint/signer.ts` +- `examples/turnkey/signer.ts` +- `examples/openfort/signer.ts` + +They are examples for guidance, not normative requirements. diff --git a/examples/compatibility-harness/docs/claim-catalog.md b/examples/compatibility-harness/docs/claim-catalog.md new file mode 100644 index 000000000..8f45902b9 --- /dev/null +++ b/examples/compatibility-harness/docs/claim-catalog.md @@ -0,0 +1,74 @@ +# Claim Catalog + +This catalog defines stable claim semantics emitted by the harness (`claim.id`). + +Use this as the contract for: + +- CI policy gating (`npm run validate`, `VALIDATION_POLICY_PATH`), +- partner discussions about what is validated, +- downstream report consumers. + +## Claim IDs + +| Claim ID | Trigger (high level) | Confidence Scope | Gate (`AUTHORIZATION`) | Gate (`AUTHORIZATION_AND_WRITE`) | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------------- | -------------------------------- | +| `ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE` | auth=PASS, recoverability=PASS, write=PASS | Strong harness evidence for auth+write probe scope | PASS | PASS | +| `ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED` | auth=PASS, recoverability=PASS, write=MISSING | Auth-compatible only; write not validated | PASS | PARTIAL | +| `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED` | auth=PASS, recoverability=PASS, write=UNSUPPORTED | Auth-compatible only; adapter write surface missing | PASS | PARTIAL | +| `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED` | auth=PASS, recoverability=PASS, write=UNTESTED | Auth-compatible only; write intentionally skipped | PASS | PARTIAL | +| `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED` | auth=PASS, recoverability=PASS, write=BLOCKED/INCONCLUSIVE | Auth-compatible only; write blocked by infra/env | PASS | PARTIAL | +| `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED` | auth=PASS, recoverability=PASS, write=FAIL | Auth-compatible only; write probe failed | PASS | PARTIAL | +| `PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED` | auth=PASS, recoverability not confirmed (`MISSING/UNTESTED/UNSUPPORTED/BLOCKED/INCONCLUSIVE`) | Inconclusive for auth compatibility confidence | INCONCLUSIVE | INCONCLUSIVE | +| `PARTIAL_AUTHORIZATION_CHECK_MISSING` | auth check missing | Inconclusive baseline | INCONCLUSIVE | INCONCLUSIVE | +| `INCONCLUSIVE_AUTHORIZATION_BLOCKED` | auth=BLOCKED/INCONCLUSIVE | Inconclusive (infra/env blocker) | INCONCLUSIVE | INCONCLUSIVE | +| `INCONCLUSIVE_AUTHORIZATION_UNTESTED` | auth=UNTESTED | Inconclusive (not executed) | INCONCLUSIVE | INCONCLUSIVE | +| `INCOMPATIBLE_AUTHORIZATION_FAILED` | auth=FAIL | Incompatible for authorization surface | FAIL | FAIL | +| `INCOMPATIBLE_AUTHORIZATION_UNSUPPORTED` | auth=UNSUPPORTED | Incompatible for authorization surface | FAIL | FAIL | +| `INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY` | auth=PASS, recoverability=FAIL | Incompatible for required recoverability model | FAIL | FAIL | + +## Trigger Source of Truth + +The canonical trigger logic lives in: + +- `src/verdict/claims.ts` (claim rule definitions), +- `src/verdict/resolve.ts` (claim resolution), +- `src/verdict/consistency.ts` (claim/evidence consistency guard). + +## Policy File Examples + +### Strict full compatibility only + +```json +{ + "target": "AUTHORIZATION_AND_WRITE", + "allowPartial": false, + "expectedClaims": ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"] +} +``` + +### Authorization-compatible envelope (write may be partial) + +```json +{ + "target": "AUTHORIZATION", + "allowPartial": false, + "expectedClaims": [ + "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED" + ] +} +``` + +### Temporary acceptance of partial strict outcomes + +```json +{ + "target": "AUTHORIZATION_AND_WRITE", + "allowPartial": true, + "expectedClaims": [] +} +``` diff --git a/examples/compatibility-harness/docs/report-consumption.md b/examples/compatibility-harness/docs/report-consumption.md new file mode 100644 index 000000000..7d9682c2a --- /dev/null +++ b/examples/compatibility-harness/docs/report-consumption.md @@ -0,0 +1,100 @@ +# Report Consumption Guide + +This guide explains how to consume the compatibility harness JSON artifact in CI and partner tooling. + +## 1) Contract Checks (Mandatory) + +Before reading business fields, verify: + +- `kind === "zama-compatibility-report"` +- `schemaVersion` is one of: `1.2.0`, `1.3.0` + +If either check fails, treat the artifact as incompatible with your parser. + +## 2) Primary Decision Fields + +Use these top-level fields as canonical outputs: + +- `claim.id` +- `claim.verdictLabel` +- `claim.confidence` +- `finalVerdict` +- optional `zama.writeValidationDepth` +- optional `claim.evidenceDetails` (structured per-check evidence records) + +`finalVerdict` is human-facing; `claim.id` is the stable machine-facing key for CI policy. + +## 3) Infrastructure vs Compatibility + +To separate infra blockers from compatibility defects: + +- inspect `checks.recorded[*].status` +- inspect `checks.recorded[*].checkId` (canonical machine key) +- inspect `checks.recorded[*].rootCauseCategory` +- inspect `checks.recorded[*].errorCode` +- inspect `zama.writeValidationDepth` +- inspect `infrastructure.blockers` + +Rule of thumb: + +- `BLOCKED`/`INCONCLUSIVE` with `RPC`/`RELAYER`/`REGISTRY`/`ENVIRONMENT` indicates infra or setup constraints. +- `FAIL` with `SIGNER`/`ADAPTER` is usually a true compatibility issue. + +## 4) CI Patterns + +### Pattern A: Harness-managed gate (recommended) + +Use: + +```bash +npm run validate +``` + +or strict scope: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE npm run validate +``` + +or policy file: + +```bash +VALIDATION_POLICY_PATH=./validation-policy.example.json npm run validate +``` + +### Pattern B: External gate from artifact + +If you need custom pipelines, parse `claim.id` yourself: + +```bash +jq -r '.claim.id' reports/latest.json +``` + +Example strict allow-list: + +```bash +CLAIM="$(jq -r '.claim.id' reports/latest.json)" +case "$CLAIM" in + ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE) exit 0 ;; + PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED|PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED) exit 10 ;; + INCOMPATIBLE_*) exit 20 ;; + INCONCLUSIVE_*|PARTIAL_AUTHORIZATION_CHECK_MISSING) exit 30 ;; + *) exit 31 ;; +esac +``` + +## 5) Backward Compatibility Guidance + +- Treat `schemaVersion` as the compatibility boundary. +- Treat fields introduced in later versions as optional when parsing older artifacts. +- Do not parse undocumented internal fields for gating. +- Prefer `claim.id` + `validate` exit codes over ad hoc string matching on report text. + +## 6) Suggested Artifact Retention + +Store at least: + +- full report JSON, +- CI logs, +- adapter metadata (`adapterProfile`), +- command context (adapter module path, network config, policy file used). diff --git a/examples/compatibility-harness/docs/schema-release-discipline.md b/examples/compatibility-harness/docs/schema-release-discipline.md new file mode 100644 index 000000000..4285495c6 --- /dev/null +++ b/examples/compatibility-harness/docs/schema-release-discipline.md @@ -0,0 +1,86 @@ +# Schema and Release Discipline + +This document defines how to evolve harness artifacts without breaking consumers unexpectedly. + +## 1) `schemaVersion` Bump Policy + +Current artifact version is controlled by `REPORT_SCHEMA_VERSION` in `src/report/schema.ts`. + +Use semantic intent: + +- Patch (`x.y.Z`): + - no artifact shape change, + - wording/doc-only updates, + - parser behavior unchanged for valid artifacts. + +- Minor (`x.Y.z`): + - backward-compatible additive changes, + - new optional fields only, + - existing required fields and semantics preserved. + +- Major (`X.y.z`): + - any breaking contract change, + - required field changes/removals/renames, + - semantic reinterpretation that can break existing consumers. + +## 2) Required Changes for Schema-Affecting PRs + +When artifact schema changes: + +1. Update `REPORT_SCHEMA_VERSION` in `src/report/schema.ts`. +2. Update parser contract checks in `src/report/parse.ts`. +3. Update claim/report consistency guards if needed. +4. Update fixtures: + - current schema fixtures, + - legacy/unsupported schema fixture, + - malformed fixtures. +5. Update docs: + - `README.md`, + - `SUMMARY.md`, + - `docs/report-consumption.md`, + - `docs/claim-catalog.md` (if claim semantics changed). +6. Run verification: + - `npm run typecheck`, + - `npm test`, + - `HARNESS_MOCK_MODE=1 npm run validate`. + +## 3) Harness Release Checklist + +Before tagging/releasing: + +1. Deterministic CI green (`compatibility-harness-ci.yml`). +2. Artifact compatibility tests green (`artifactCompatibility.unit.test.ts`). +3. Claim consistency tests green (`claimConsistency.unit.test.ts`). +4. Example baseline lockfile tests green (`exampleBaselines.unit.test.ts`). +5. Docs updated for any new claims/status semantics. +6. Optional: run live workflow and store uploaded artifacts. + +## 4) Release Note Template + +Use this template in release notes/PR description: + +```md +## Compatibility Harness Release + +### Schema + +- schemaVersion: -> +- Change type: Patch / Minor / Major +- Consumer impact: + +### Claim/Verdict Semantics + +- Changed claim IDs: +- Gate mapping impact: + +### Validation Surface + +- New checks/capabilities: +- Deprecated checks/capabilities: + +### Migration Guidance + +- Required consumer actions: + - + - +``` diff --git a/examples/compatibility-harness/docs/verdict-playbook.md b/examples/compatibility-harness/docs/verdict-playbook.md new file mode 100644 index 000000000..2585787f0 --- /dev/null +++ b/examples/compatibility-harness/docs/verdict-playbook.md @@ -0,0 +1,107 @@ +# Verdict Interpretation Playbook + +This playbook helps engineers and partner-facing teams interpret harness outcomes conservatively. + +Use this sequence: + +1. Read `claim.id` (machine key). +2. Confirm `claim.evidence` / `claim.evidenceDetails`. +3. State only what is proven by the recorded checks. +4. Explicitly call out untested/blocked surfaces and next action. + +## Claim Interpretation Matrix + +### `ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE` + +- Proven: authorization flow, recoverability, and write probe passed. +- Not proven: full production readiness or all SDK paths. +- Next action: proceed to partner pilot integration; keep monitor checks in CI. + +### `ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED` + +- Proven: authorization + recoverability passed. +- Not proven: write-path compatibility. +- Next action: run write validation in an environment where write checks can execute. + +### `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED` + +- Proven: authorization + recoverability passed. +- Not proven: adapter write surface compatibility. +- Next action: validate if write can be routed through another execution path or explicitly scope to auth-only integration. + +### `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED` + +- Proven: authorization + recoverability passed. +- Not proven: write surface (intentionally not tested). +- Next action: rerun without mock/skip constraints. + +### `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED` + +- Proven: authorization + recoverability passed. +- Not proven: write surface because infra/env blocked validation. +- Next action: resolve blocker root cause (`errorCode`) and rerun write validation. + +### `PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED` + +- Proven: authorization + recoverability passed. +- Not proven: write compatibility (write probe failed). +- Next action: debug adapter execution path and rerun. + +### `PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED` + +- Proven: authorization check passed. +- Not proven: EOA-style recoverability requirement. +- Next action: verify recoverability behavior or adjust integration model expectations. + +### `PARTIAL_AUTHORIZATION_CHECK_MISSING` + +- Proven: nothing about authorization compatibility. +- Not proven: authorization baseline itself. +- Next action: ensure authorization check is executed and recorded. + +### `INCONCLUSIVE_AUTHORIZATION_BLOCKED` + +- Proven: no incompatibility conclusion yet. +- Not proven: authorization compatibility (blocked by infra/env). +- Next action: resolve blockers (env, RPC, relayer, registry) and rerun. + +### `INCONCLUSIVE_AUTHORIZATION_UNTESTED` + +- Proven: no incompatibility conclusion yet. +- Not proven: authorization compatibility (check skipped/untested). +- Next action: execute full authorization validation run. + +### `INCOMPATIBLE_AUTHORIZATION_FAILED` + +- Proven: authorization flow is incompatible in current adapter behavior. +- Not proven: compatibility after adapter fixes. +- Next action: fix signing/identity behavior and rerun from `doctor` + `validate`. + +### `INCOMPATIBLE_AUTHORIZATION_UNSUPPORTED` + +- Proven: adapter does not expose required authorization primitive. +- Not proven: compatibility through alternate execution design. +- Next action: add/route EIP-712 authorization support or scope integration away from unsupported surface. + +### `INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY` + +- Proven: recoverability requirement failed. +- Not proven: compatibility as EOA-style authorization signer. +- Next action: either fix recoverability or explicitly adopt a different model and document limitations. + +## Partner Conversation Patterns + +### Pattern A: Full-compatible outcome + +- "We validated authorization and write probes in the harness scope (`ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE`)." +- "This confirms harness-level compatibility, not full production certification." + +### Pattern B: Auth-only compatible outcome + +- "Authorization is validated, but write compatibility is still partial/not recorded." +- "Integration can proceed for auth-scoped flows while we close write-surface validation." + +### Pattern C: Inconclusive outcome + +- "The run is inconclusive due to infrastructure blockers, not confirmed incompatibility." +- "We need a clean rerun after resolving `` before making compatibility claims." diff --git a/examples/compatibility-harness/examples/crossmint/COMPATIBILITY.md b/examples/compatibility-harness/examples/crossmint/COMPATIBILITY.md new file mode 100644 index 000000000..af360219f --- /dev/null +++ b/examples/compatibility-harness/examples/crossmint/COMPATIBILITY.md @@ -0,0 +1,97 @@ +# Crossmint Adapter Compatibility Notes + +This document describes the expected harness behavior for the Crossmint example adapter at [`signer.ts`](./signer.ts). + +## Adapter Profile (Declared) + +- Name: `Crossmint API-Routed Adapter` +- Declared architecture: `API_ROUTED_EXECUTION` +- Verification model: `UNKNOWN` (confirmed by tests if recoverable) +- `eip712Signing`: `SUPPORTED` +- `rawTransactionSigning`: `UNSUPPORTED` +- `contractExecution`: `SUPPORTED` +- `zamaAuthorizationFlow`: `SUPPORTED` +- `zamaWriteFlow`: `SUPPORTED` + +Crossmint is API-routed: no raw tx signing primitive is exposed, but write execution is supported through `/transactions`. + +## Expected Validation Pattern + +Typical run (with valid credentials and healthy infra): + +- Adapter initialization/address resolution: `PASS` +- EIP-712 recoverability: `PASS` +- Raw transaction execution: `UNSUPPORTED` (expected) +- Adapter contract read: `PASS` (validated via harness RPC fallback because `readContract` is not exposed) +- Zama authorization flow (`sdk.allow()`): `PASS` +- Zama write flow probe: `PASS` + +Expected final verdict: + +- `ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS` + +If relayer/RPC/registry fails, statuses can become `BLOCKED` or `INCONCLUSIVE` without implying adapter incompatibility. + +### Baseline Lockfile (Regression Guard) + +See `src/tests/fixtures/example-baselines/crossmint.lock.json`. + +Claim envelope tracked by unit tests: + +- `PASS`: full auth+write compatibility +- `PARTIAL`: scoped write degradations (blocked/failed/not-recorded) +- `INCONCLUSIVE`: infra/env-blocked authorization outcomes + +## API Mapping Used by the Example + +- `adapter.getAddress`: +- `GET /wallets/{locator}` (unless `CROSSMINT_WALLET_ADDRESS` is preconfigured) +- `adapter.signTypedData`: +- `POST /wallets/{locator}/signatures` with `type: evm-typed-data`, then poll operation +- `adapter.writeContract`: +- encode calldata with viem +- `POST /wallets/{locator}/transactions`, then poll operation +- return on-chain transaction hash + +## Setup + +1. Configure `.env`: + +```dotenv +CROSSMINT_API_KEY=your_server_side_api_key +CROSSMINT_WALLET_LOCATOR=email:alice@example.com:evm-smart-wallet +# Optional: +# CROSSMINT_WALLET_ADDRESS=0x... +``` + +2. Run: + +```bash +npm run test:crossmint +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/crossmint/signer.ts npm test +``` + +Optional preflight and CI gate: + +```bash +npm run doctor:crossmint +npm run validate:crossmint +``` + +Strict gate requiring authorization + write compatibility: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE npm run validate:crossmint +``` + +## Interpretation Guidance + +- `UNSUPPORTED` raw transaction checks are normal for Crossmint and do not invalidate Zama compatibility claims when authorization + write probe pass. +- `contractReads` capability remains `UNSUPPORTED` for the adapter itself even when the harness can validate read behavior through its RPC fallback. +- A `PASS` on authorization alone is not enough to claim full write compatibility. +- The harness verdict is scoped: trust the exact phrase emitted in `Final`. diff --git a/examples/compatibility-harness/examples/crossmint/signer.ts b/examples/compatibility-harness/examples/crossmint/signer.ts new file mode 100644 index 000000000..517d9114e --- /dev/null +++ b/examples/compatibility-harness/examples/crossmint/signer.ts @@ -0,0 +1,201 @@ +import { encodeFunctionData, getAddress } from "viem"; +import type { Hex } from "viem"; +import type { Adapter } from "../../src/adapter/types.js"; + +const CROSSMINT_API_KEY = process.env.CROSSMINT_API_KEY ?? ""; +const CROSSMINT_WALLET_LOCATOR = process.env.CROSSMINT_WALLET_LOCATOR ?? ""; +const CROSSMINT_WALLET_ADDRESS = process.env.CROSSMINT_WALLET_ADDRESS ?? ""; +const CROSSMINT_API_BASE = "https://api.crossmint.com/2022-06-09"; +const CROSSMINT_CHAIN = "ethereum-sepolia"; + +if (!CROSSMINT_API_KEY) { + throw new Error("CROSSMINT_API_KEY is not set. Add it to your .env file."); +} + +if (!CROSSMINT_WALLET_LOCATOR) { + throw new Error( + "CROSSMINT_WALLET_LOCATOR is not set. Example: email:alice@example.com:evm-smart-wallet", + ); +} + +const headers = { + "X-API-KEY": CROSSMINT_API_KEY, + "Content-Type": "application/json", +}; + +let resolvedAddress: string | null = CROSSMINT_WALLET_ADDRESS + ? getAddress(CROSSMINT_WALLET_ADDRESS) + : null; +let resolveAddressPromise: Promise | null = null; + +function jsonWithBigInt(value: unknown): string { + return JSON.stringify(value, (_key, candidate) => + typeof candidate === "bigint" ? candidate.toString() : candidate, + ); +} + +async function resolveAddress(): Promise { + const res = await fetch( + `${CROSSMINT_API_BASE}/wallets/${encodeURIComponent(CROSSMINT_WALLET_LOCATOR)}`, + { headers }, + ); + if (!res.ok) { + throw new Error(`Failed to resolve wallet: ${res.status} ${await res.text()}`); + } + const payload = (await res.json()) as { address?: string }; + if (!payload.address) { + throw new Error("Crossmint wallet lookup response did not include an address"); + } + return getAddress(payload.address); +} + +async function ensureAddress(): Promise { + if (resolvedAddress) { + return resolvedAddress; + } + if (!resolveAddressPromise) { + resolveAddressPromise = resolveAddress().then((addr) => { + resolvedAddress = addr; + return addr; + }); + } + return resolveAddressPromise; +} + +async function pollOperation( + operationId: string, + kind: "signatures" | "transactions", +): Promise> { + const url = `${CROSSMINT_API_BASE}/wallets/${encodeURIComponent(CROSSMINT_WALLET_LOCATOR)}/${kind}/${operationId}`; + for (let attempt = 0; attempt < 30; attempt += 1) { + await new Promise((resolve) => setTimeout(resolve, 2_000)); + const res = await fetch(url, { headers }); + if (!res.ok) { + throw new Error(`Crossmint poll failed: ${res.status} ${await res.text()}`); + } + const payload = (await res.json()) as Record; + const status = String(payload.status ?? ""); + if (status === "succeeded") { + return payload; + } + if (status === "failed") { + throw new Error(`Crossmint operation failed: ${JSON.stringify(payload.error ?? payload)}`); + } + } + throw new Error("Crossmint operation timed out after 60 seconds"); +} + +function parseTransactionHash(payload: Record): Hex { + const onChain = payload.onChain as Record | undefined; + const txHash = String(onChain?.txId ?? payload.txId ?? payload.transactionId ?? ""); + if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) { + throw new Error( + `Crossmint did not return a valid transaction hash (got: ${txHash || "empty"})`, + ); + } + return txHash as Hex; +} + +export const adapter: Adapter = { + metadata: { + name: "Crossmint API-Routed Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + notes: ["Crossmint smart wallet routed via /signatures and /transactions APIs."], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "UNSUPPORTED", + transactionReceiptTracking: "UNSUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + await ensureAddress(); + }, + async getAddress() { + return ensureAddress(); + }, + async signTypedData(data) { + await ensureAddress(); + const res = await fetch( + `${CROSSMINT_API_BASE}/wallets/${encodeURIComponent(CROSSMINT_WALLET_LOCATOR)}/signatures`, + { + method: "POST", + headers, + body: jsonWithBigInt({ + type: "evm-typed-data", + params: { + typedData: { + domain: data.domain, + types: data.types, + primaryType: data.primaryType, + message: data.message, + }, + }, + }), + }, + ); + + if (!res.ok) { + throw new Error(`Crossmint signature request failed: ${res.status} ${await res.text()}`); + } + + const payload = (await res.json()) as { id?: string }; + if (!payload.id) { + throw new Error("Crossmint signature response did not include an operation id"); + } + + const result = await pollOperation(payload.id, "signatures"); + const signature = String(result.signature ?? ""); + if (!/^0x[0-9a-fA-F]+$/.test(signature)) { + throw new Error("Crossmint signature operation did not return a hex signature"); + } + return signature; + }, + async writeContract(config) { + await ensureAddress(); + const calldata = encodeFunctionData({ + abi: config.abi, + functionName: config.functionName, + args: config.args ?? [], + }); + + const res = await fetch( + `${CROSSMINT_API_BASE}/wallets/${encodeURIComponent(CROSSMINT_WALLET_LOCATOR)}/transactions`, + { + method: "POST", + headers, + body: jsonWithBigInt({ + params: { + calls: [ + { + to: getAddress(config.address), + data: calldata, + value: config.value ?? 0n, + }, + ], + chain: CROSSMINT_CHAIN, + }, + }), + }, + ); + + if (!res.ok) { + throw new Error(`Crossmint transaction request failed: ${res.status} ${await res.text()}`); + } + + const payload = (await res.json()) as { id?: string }; + if (!payload.id) { + throw new Error("Crossmint transaction response did not include an operation id"); + } + + const result = await pollOperation(payload.id, "transactions"); + return parseTransactionHash(result); + }, +}; diff --git a/examples/compatibility-harness/examples/openfort/COMPATIBILITY.md b/examples/compatibility-harness/examples/openfort/COMPATIBILITY.md new file mode 100644 index 000000000..6d91aa5e0 --- /dev/null +++ b/examples/compatibility-harness/examples/openfort/COMPATIBILITY.md @@ -0,0 +1,103 @@ +# Openfort Adapter Compatibility Notes + +This document describes expected harness behavior for the Openfort example adapter at [`signer.ts`](./signer.ts). + +## Adapter Profile (Declared) + +- Name: `Openfort EOA Baseline Adapter` +- Declared architecture: `EOA` +- Verification model: `RECOVERABLE_ECDSA` +- `eip712Signing`: `SUPPORTED` +- `recoverableEcdsa`: `SUPPORTED` +- `rawTransactionSigning`: `SUPPORTED` +- `contractExecution`: `SUPPORTED` +- `contractReads`: `SUPPORTED` +- `transactionReceiptTracking`: `SUPPORTED` +- `zamaAuthorizationFlow`: `SUPPORTED` +- `zamaWriteFlow`: `SUPPORTED` + +## What This Example Validates + +This adapter is a CLI baseline meant to validate Openfort integrations that run with **EOA semantics** (`wallet address === signing key`). + +It validates: + +- EIP-712 recoverability expectations used by Zama relayer flows +- raw transaction + contract execution compatibility +- Zama authorization and practical write probe in the harness runtime + +## What This Example Does Not Validate + +It does **not** validate Openfort front-end/runtime concerns such as: + +- `@openfort/react` embedded auth flows +- browser connector lifecycle behavior +- interactive wallet reconnect/recovery UX + +Those concerns are covered by the Openfort browser POC in `~/Code/Zama/Tests/OPENFORT/openfort_zama_integration`. + +## Expected Validation Pattern + +Typical run (valid key, funded wallet, healthy infra): + +- Adapter initialization/address resolution: `PASS` +- EIP-712 signing + recoverability: `PASS` +- Raw transaction execution: `PASS` +- Adapter contract read: `PASS` +- Zama authorization flow (`sdk.allow()`): `PASS` +- Zama write flow probe: `PASS` + +Expected final verdict: + +- `ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS` + +### Baseline Lockfile (Regression Guard) + +See `src/tests/fixtures/example-baselines/openfort.lock.json`. + +Claim envelope tracked by unit tests: + +- `PASS`: full auth+write compatibility +- `PARTIAL`: write-surface degradations (blocked/failed/not-recorded) +- `INCONCLUSIVE`: infra/env-blocked authorization outcomes + +## Setup + +1. Configure `.env`: + +```dotenv +OPENFORT_TEST_PRIVATE_KEY=0x... +# Optional override (otherwise RPC_URL is used): +# OPENFORT_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY +``` + +2. Run: + +```bash +npm run test:openfort +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/openfort/signer.ts npm test +``` + +Optional preflight and CI gate: + +```bash +npm run doctor:openfort +npm run validate:openfort +``` + +Strict gate requiring authorization + write compatibility: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE npm run validate:openfort +``` + +Deterministic local mode without network/relayer dependency: + +```bash +HARNESS_MOCK_MODE=1 npm run test:openfort +``` diff --git a/examples/compatibility-harness/examples/openfort/signer.ts b/examples/compatibility-harness/examples/openfort/signer.ts new file mode 100644 index 000000000..a5f49c9fa --- /dev/null +++ b/examples/compatibility-harness/examples/openfort/signer.ts @@ -0,0 +1,97 @@ +import { createWalletClient, getAddress, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { networkConfig } from "../../src/config/network.js"; +import { publicClient } from "../../src/utils/rpc.js"; +import type { Adapter } from "../../src/adapter/types.js"; + +function getOpenfortRpcUrl(): string { + const fromOpenfort = process.env.OPENFORT_RPC_URL?.trim(); + if (fromOpenfort) return fromOpenfort; + return networkConfig.rpcUrl; +} + +function getConfiguredAccount() { + const rawKey = ( + process.env.OPENFORT_TEST_PRIVATE_KEY ?? + process.env.OPENFORT_PRIVATE_KEY ?? + process.env.PRIVATE_KEY ?? + "" + ).trim(); + const isValidKey = /^0x[0-9a-fA-F]{64}$/.test(rawKey); + + if (!rawKey || !isValidKey) { + throw new Error( + rawKey + ? `OPENFORT_TEST_PRIVATE_KEY is invalid (got "${rawKey.slice(0, 6)}…"). Expected a 0x-prefixed 64-character hex string.` + : "OPENFORT_TEST_PRIVATE_KEY is not set. Add it to .env (or set OPENFORT_PRIVATE_KEY / PRIVATE_KEY).", + ); + } + + return privateKeyToAccount(rawKey as `0x${string}`); +} + +function getWalletClient() { + const account = getConfiguredAccount(); + return createWalletClient({ + account, + chain: networkConfig.chain, + transport: http(getOpenfortRpcUrl()), + }); +} + +export const adapter: Adapter = { + metadata: { + name: "Openfort EOA Baseline Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [networkConfig.chainId], + notes: [ + "Baseline CLI adapter for Openfort integrations running in EOA semantics.", + "Validates cryptographic/runtime compatibility only (not Openfort embedded auth/session UX).", + ], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async getAddress() { + return getConfiguredAccount().address; + }, + async signTypedData(data) { + const account = getConfiguredAccount(); + return account.signTypedData({ + domain: data.domain, + types: data.types, + primaryType: data.primaryType, + message: data.message, + }); + }, + async signTransaction(tx) { + const account = getConfiguredAccount(); + return account.signTransaction(tx); + }, + async writeContract(config) { + const client = getWalletClient(); + return client.writeContract({ + address: getAddress(config.address), + abi: config.abi, + functionName: config.functionName, + args: config.args ?? [], + value: config.value, + ...(config.gas !== undefined ? { gas: config.gas } : {}), + } as never); + }, + async readContract(config) { + return publicClient.readContract(config); + }, + waitForTransactionReceipt(hash) { + return publicClient.waitForTransactionReceipt({ hash }); + }, +}; diff --git a/examples/compatibility-harness/examples/turnkey/COMPATIBILITY.md b/examples/compatibility-harness/examples/turnkey/COMPATIBILITY.md new file mode 100644 index 000000000..4ad8caf61 --- /dev/null +++ b/examples/compatibility-harness/examples/turnkey/COMPATIBILITY.md @@ -0,0 +1,103 @@ +# Turnkey Adapter Compatibility Notes + +This document describes expected harness behavior for the Turnkey example adapter at [`signer.ts`](./signer.ts). + +## Adapter Profile (Declared) + +- Name: `Turnkey API Key Adapter` +- Declared architecture: `API_ROUTED_EXECUTION` +- Verification model: `UNKNOWN` (validated by observed recoverability checks) +- `eip712Signing`: `SUPPORTED` +- `rawTransactionSigning`: `UNSUPPORTED` +- `contractExecution`: `SUPPORTED` +- `contractReads`: `SUPPORTED` +- `transactionReceiptTracking`: `SUPPORTED` +- `zamaAuthorizationFlow`: `SUPPORTED` +- `zamaWriteFlow`: `SUPPORTED` + +This adapter is server-side and non-interactive: it uses Turnkey API key authentication with `@turnkey/http` + `@turnkey/viem`. + +## Expected Validation Pattern + +Typical run (valid credentials, funded wallet, healthy RPC/relayer): + +- Adapter initialization/address resolution: `PASS` +- EIP-712 recoverability: `PASS` +- Raw transaction execution: `UNSUPPORTED` (expected in this adapter) +- Adapter contract read: `PASS` +- Zama authorization flow (`sdk.allow()`): `PASS` +- Zama write flow probe: `PASS` + +Expected final verdict: + +- `ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS` + +If relayer/RPC/registry fails, statuses can become `BLOCKED` or `INCONCLUSIVE` without implying Turnkey incompatibility. + +### Baseline Lockfile (Regression Guard) + +See `src/tests/fixtures/example-baselines/turnkey.lock.json`. + +Claim envelope tracked by unit tests: + +- `PASS`: full auth+write compatibility +- `PARTIAL`: scoped write degradations (blocked/failed/not-recorded) +- `INCONCLUSIVE`: infra/env-blocked authorization outcomes + +## Environment Variables + +Required: + +```dotenv +TURNKEY_ORG_ID=... +TURNKEY_PRIVATE_KEY_ID=... +TURNKEY_API_PUBLIC_KEY=... +TURNKEY_API_PRIVATE_KEY=... +``` + +Optional: + +```dotenv +# If omitted, address is resolved from the Turnkey key metadata. +TURNKEY_WALLET_ADDRESS=0x... + +# Defaults: +# TURNKEY_BASE_URL=https://api.turnkey.com +# TURNKEY_RPC_URL= +``` + +The adapter also accepts `VITE_TURNKEY_ORG_ID`, `VITE_TURNKEY_PRIVATE_KEY_ID`, and `VITE_TURNKEY_WALLET_ADDRESS` to ease reuse from the existing Turnkey demo project. + +## Setup + +1. Fill `.env` with Turnkey credentials. +2. Run: + +```bash +npm run test:turnkey +``` + +Equivalent: + +```bash +SIGNER_MODULE=./examples/turnkey/signer.ts npm test +``` + +Optional preflight and CI gate: + +```bash +npm run doctor:turnkey +npm run validate:turnkey +``` + +Strict gate requiring authorization + write compatibility: + +```bash +VALIDATION_TARGET=AUTHORIZATION_AND_WRITE npm run validate:turnkey +``` + +## Interpretation Guidance + +- This example intentionally does not expose `signTransaction`; raw-transaction checks are expected to be `UNSUPPORTED`. +- Zama compatibility claims should follow the exact scoped final verdict from the harness report. +- A failing run caused by invalid API keys, policy restrictions, or RPC outages should be interpreted as environment/infrastructure blockers, not immediate adapter incompatibility. diff --git a/examples/compatibility-harness/examples/turnkey/signer.ts b/examples/compatibility-harness/examples/turnkey/signer.ts new file mode 100644 index 000000000..8dbe4f99d --- /dev/null +++ b/examples/compatibility-harness/examples/turnkey/signer.ts @@ -0,0 +1,235 @@ +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { TurnkeyClient } from "@turnkey/http"; +import { createAccount } from "@turnkey/viem"; +import { + createPublicClient, + createWalletClient, + encodeFunctionData, + getAddress, + http, + type Abi, + type Address, + type Hex, +} from "viem"; +import { sepolia } from "viem/chains"; +import type { Adapter } from "../../src/adapter/types.js"; + +type TurnkeyRuntime = { + address: Address; + publicClient: ReturnType; + walletClient: ReturnType; +}; + +let runtimePromise: Promise | null = null; + +function required(name: string, value: string | undefined): string { + const normalized = (value ?? "").trim(); + if (!normalized) { + throw new Error(`${name} is not set. Add it to your .env file.`); + } + return normalized; +} + +function configuredAddress(): Address | null { + const raw = + process.env.TURNKEY_WALLET_ADDRESS?.trim() || process.env.VITE_TURNKEY_WALLET_ADDRESS?.trim(); + if (!raw) return null; + return getAddress(raw); +} + +function buildDomainType( + domain: Record | undefined, +): Array<{ name: string; type: string }> { + if (!domain) return []; + const fields: Array<{ name: string; type: string }> = []; + if (domain.name !== undefined) fields.push({ name: "name", type: "string" }); + if (domain.version !== undefined) fields.push({ name: "version", type: "string" }); + if (domain.chainId !== undefined) fields.push({ name: "chainId", type: "uint256" }); + if (domain.verifyingContract !== undefined) { + fields.push({ name: "verifyingContract", type: "address" }); + } + if (domain.salt !== undefined) fields.push({ name: "salt", type: "bytes32" }); + return fields; +} + +function normalizeTypedDataTypes( + types: Record, + domain: Record | undefined, +): Record { + if ("EIP712Domain" in types) { + return types; + } + return { + EIP712Domain: buildDomainType(domain), + ...types, + }; +} + +function resolvePrimaryType( + primaryType: string | undefined, + types: Record, +): string { + if (primaryType) return primaryType; + const inferred = Object.keys(types).find((key) => key !== "EIP712Domain"); + if (!inferred) { + throw new Error("Unable to resolve EIP-712 primaryType from typed-data payload"); + } + return inferred; +} + +async function getRuntime(): Promise { + if (!runtimePromise) { + runtimePromise = (async () => { + const organizationId = required( + "TURNKEY_ORG_ID or VITE_TURNKEY_ORG_ID", + process.env.TURNKEY_ORG_ID ?? process.env.VITE_TURNKEY_ORG_ID, + ); + const signWith = required( + "TURNKEY_PRIVATE_KEY_ID or VITE_TURNKEY_PRIVATE_KEY_ID", + process.env.TURNKEY_PRIVATE_KEY_ID ?? process.env.VITE_TURNKEY_PRIVATE_KEY_ID, + ); + const apiPublicKey = required("TURNKEY_API_PUBLIC_KEY", process.env.TURNKEY_API_PUBLIC_KEY); + const apiPrivateKey = required( + "TURNKEY_API_PRIVATE_KEY", + process.env.TURNKEY_API_PRIVATE_KEY, + ); + const baseUrl = (process.env.TURNKEY_BASE_URL ?? "https://api.turnkey.com").trim(); + const rpcUrl = ( + process.env.TURNKEY_RPC_URL ?? + process.env.RPC_URL ?? + "https://ethereum-sepolia-rpc.publicnode.com" + ).trim(); + const envAddress = configuredAddress(); + + const stamper = new ApiKeyStamper({ + apiPublicKey, + apiPrivateKey, + }); + const client = new TurnkeyClient({ baseUrl }, stamper); + const account = await createAccount({ + client, + organizationId, + signWith, + ethereumAddress: envAddress, + }); + const accountAddress = getAddress(account.address); + + if (envAddress && getAddress(envAddress) !== accountAddress) { + throw new Error( + `TURNKEY_WALLET_ADDRESS (${envAddress}) does not match the Turnkey key address (${accountAddress}).`, + ); + } + + const publicClient = createPublicClient({ + chain: sepolia, + transport: http(rpcUrl), + }); + + const walletClient = createWalletClient({ + account, + chain: sepolia, + transport: http(rpcUrl), + }); + + return { + address: accountAddress, + publicClient, + walletClient, + }; + })(); + } + + return runtimePromise; +} + +export const adapter: Adapter = { + metadata: { + name: "Turnkey API Key Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + notes: [ + "Uses @turnkey/http + @turnkey/viem in a non-interactive server-side flow.", + "Targets Ethereum Sepolia for compatibility-harness validation.", + ], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + await getRuntime(); + }, + async getAddress() { + return (await getRuntime()).address; + }, + async signTypedData(data) { + const runtime = await getRuntime(); + const normalizedTypes = normalizeTypedDataTypes( + data.types as Record, + data.domain as Record | undefined, + ); + const resolvedPrimaryType = resolvePrimaryType( + data.primaryType as string | undefined, + normalizedTypes, + ); + return runtime.walletClient.account.signTypedData({ + domain: data.domain, + types: normalizedTypes as Record, + primaryType: resolvedPrimaryType, + message: data.message, + }); + }, + async writeContract(config) { + const runtime = await getRuntime(); + const { publicClient, walletClient, address } = runtime; + + const calldata = encodeFunctionData({ + abi: config.abi as Abi, + functionName: config.functionName, + args: config.args ?? [], + }); + + const [fees, nonce] = await Promise.all([ + publicClient.estimateFeesPerGas(), + publicClient.getTransactionCount({ + address, + blockTag: "pending", + }), + ]); + + const gas = + config.gas ?? + (await publicClient.estimateGas({ + account: address, + to: getAddress(config.address), + data: calldata, + value: config.value, + })); + + return walletClient.sendTransaction({ + to: getAddress(config.address), + data: calldata, + value: config.value, + gas, + nonce, + chainId: sepolia.id, + maxFeePerGas: fees.maxFeePerGas, + maxPriorityFeePerGas: fees.maxPriorityFeePerGas, + chain: sepolia, + }) as Promise; + }, + async readContract(config) { + return (await getRuntime()).publicClient.readContract(config); + }, + async waitForTransactionReceipt(hash) { + return (await getRuntime()).publicClient.waitForTransactionReceipt({ hash }); + }, +}; diff --git a/examples/compatibility-harness/package-lock.json b/examples/compatibility-harness/package-lock.json new file mode 100644 index 000000000..97bfbdd46 --- /dev/null +++ b/examples/compatibility-harness/package-lock.json @@ -0,0 +1,4018 @@ +{ + "name": "zama-compatibility-harness", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zama-compatibility-harness", + "version": "1.0.0", + "dependencies": { + "@turnkey/api-key-stamper": "^0.6.2", + "@turnkey/http": "^3.17.0", + "@turnkey/viem": "^0.14.25", + "@zama-fhe/sdk": "2.3.0-alpha.4", + "dotenv": "^16.4.0", + "viem": "^2.30.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", + "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz", + "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz", + "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz", + "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz", + "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz", + "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz", + "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz", + "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz", + "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz", + "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz", + "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz", + "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz", + "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz", + "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz", + "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz", + "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz", + "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz", + "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz", + "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz", + "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz", + "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz", + "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz", + "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz", + "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz", + "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz", + "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hpke/chacha20poly1305": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", + "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", + "license": "MIT", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/common": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz", + "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", + "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", + "license": "MIT", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/dhkem-x25519": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/dhkem-x25519/-/dhkem-x25519-1.8.0.tgz", + "integrity": "sha512-S1MWWkAfu+TFxySgv5+2P3O4Mx/jk7BsoplzQaA1s3sfUJVJ2UsZsSzSsMc+FXJumLXncoJFlO6mK6mDGspfmA==", + "license": "MIT", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/dhkem-x448": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/dhkem-x448/-/dhkem-x448-1.8.0.tgz", + "integrity": "sha512-mFfnZfgp4OKkUIS/FKikfUgdnDKRy25ytCKBQiV+N+HbYy3I4v4ZCPBQ69QL+TYmKmCZJeUEnYeS5K+OBRP+Eg==", + "license": "MIT", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@openzeppelin/contracts": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", + "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", + "license": "MIT" + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-x509/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/x509": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.3.tgz", + "integrity": "sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.3.13", + "@peculiar/asn1-csr": "^2.3.13", + "@peculiar/asn1-ecc": "^2.3.14", + "@peculiar/asn1-pkcs9": "^2.3.13", + "@peculiar/asn1-rsa": "^2.3.13", + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "pvtsutils": "^1.3.5", + "reflect-metadata": "^0.2.2", + "tslib": "^2.7.0", + "tsyringe": "^4.8.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/api-key-stamper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@turnkey/api-key-stamper/-/api-key-stamper-0.6.5.tgz", + "integrity": "sha512-dSINHLIzYVw+ZNLwjM1wnEYFkYHPjQDSi43opCUhvow/ws6Fsn6gWkYpzbH+6Ej3rvSJTuaTwsld+dsTG/3HnA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@turnkey/crypto": "2.8.14", + "@turnkey/encoding": "0.6.0", + "sha256-uint8array": "^0.10.7" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/api-key-stamper/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/api-key-stamper/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@turnkey/core/-/core-1.14.1.tgz", + "integrity": "sha512-Jaopir7Xo1QjQk9zIR0n8tqzNytHnykNQv7DV+i+NP7cW6nSzsPbyQFn4p23fuunxucSVy100STLEzFHKPIuFA==", + "license": "MIT", + "dependencies": { + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/crypto": "2.8.14", + "@turnkey/encoding": "0.6.0", + "@turnkey/http": "3.18.1", + "@turnkey/sdk-types": "0.14.0", + "@turnkey/webauthn-stamper": "0.6.0", + "@wallet-standard/app": "^1.1.0", + "@wallet-standard/base": "^1.1.0", + "@walletconnect/sign-client": "^2.23.6", + "@walletconnect/types": "^2.23.0", + "cross-fetch": "^3.1.5", + "ethers": "^6.10.0", + "jwt-decode": "4.0.0", + "uuid": "^11.1.0", + "viem": "^2.33.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "^2.2.0", + "@turnkey/react-native-passkey-stamper": "1.2.13", + "react-native-keychain": "^8.1.0 || ^9.2.2 || ^10.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "@turnkey/react-native-passkey-stamper": { + "optional": true + }, + "react-native-keychain": { + "optional": true + } + } + }, + "node_modules/@turnkey/crypto": { + "version": "2.8.14", + "resolved": "https://registry.npmjs.org/@turnkey/crypto/-/crypto-2.8.14.tgz", + "integrity": "sha512-UZU+DEwhSUyyMHC9VZbp5av8NY/IJsfJ+g1E4dndQjMSAJd5NPKGS7sItlBy1ifN6wBb9JP5AcvmcfCdCfj9bw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.0", + "@noble/hashes": "1.8.0", + "@peculiar/x509": "1.12.3", + "@turnkey/encoding": "0.6.0", + "@turnkey/sdk-types": "0.14.0", + "borsh": "2.0.0", + "cbor-js": "0.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/crypto/node_modules/@noble/curves": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/crypto/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/encoding": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@turnkey/encoding/-/encoding-0.6.0.tgz", + "integrity": "sha512-IC8qXvy36+iGAeiaVIuJvB35uU2Ld/RAWI/DRTKS+ttBej0GXhOn48Ouu5mlca4jt8ZEuwXmDVv74A8uBQclsA==", + "license": "Apache-2.0", + "dependencies": { + "bs58": "6.0.0", + "bs58check": "4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/http": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/@turnkey/http/-/http-3.18.1.tgz", + "integrity": "sha512-KXhDAtohx3PPRigdU0qjdgSTuomD0xl3mONly5hn8/jgrp0Gs7BKXv0OGiw/3Sj02IPmlza3SRWBKGtWyGwNmg==", + "license": "Apache-2.0", + "dependencies": { + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/encoding": "0.6.0", + "@turnkey/webauthn-stamper": "0.6.0", + "cross-fetch": "^3.1.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/iframe-stamper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@turnkey/iframe-stamper/-/iframe-stamper-2.11.0.tgz", + "integrity": "sha512-Iyf4W4S4Hx/RVsXl07PkRTh3g5Ca+vpN6SdUbRy8lt9IQZAJel/vxAk9XMyB7utX9TJx+icSjNYHvC0XGqJ47A==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/indexed-db-stamper": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@turnkey/indexed-db-stamper/-/indexed-db-stamper-1.2.6.tgz", + "integrity": "sha512-RSlCpH96e46PYgc029/TK3nE/HOW4QgKo9bXIYjoUR8J7+xkq+0Z0wjHrp27Yn3PbW8743hEppqK9jxsYc8ptA==", + "license": "Apache-2.0", + "dependencies": { + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/encoding": "0.6.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/sdk-browser": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/@turnkey/sdk-browser/-/sdk-browser-5.16.1.tgz", + "integrity": "sha512-AXBUJVW83nZKnSVNUH235dbsu4S+a4F0Swo25Fm/gHWKPVh4nvpT/Pr2cr6PVh3UR15p787AVUkxO9ZgtNclPw==", + "license": "Apache-2.0", + "dependencies": { + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/crypto": "2.8.14", + "@turnkey/encoding": "0.6.0", + "@turnkey/http": "3.18.1", + "@turnkey/iframe-stamper": "2.11.0", + "@turnkey/indexed-db-stamper": "1.2.6", + "@turnkey/sdk-types": "0.14.0", + "@turnkey/wallet-stamper": "1.1.16", + "@turnkey/webauthn-stamper": "0.6.0", + "buffer": "^6.0.3", + "cross-fetch": "^3.1.5", + "hpke-js": "^1.6.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/sdk-server": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@turnkey/sdk-server/-/sdk-server-5.3.0.tgz", + "integrity": "sha512-X+CpGxq+IGOwmNJpb8dScT9R67om9azM+DxQ+2RH7L5G5axSBVlQWsoSKnMSwROrc31+F5e6t4MMd9rUfrjYww==", + "license": "Apache-2.0", + "dependencies": { + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/http": "3.18.1", + "@turnkey/sdk-types": "0.14.0", + "@turnkey/wallet-stamper": "1.1.16", + "buffer": "^6.0.3", + "cross-fetch": "^3.1.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/sdk-types": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@turnkey/sdk-types/-/sdk-types-0.14.0.tgz", + "integrity": "sha512-FLN4p3Jc3rBMvM17tgFrO4VpqkQN0RgWtLknqcqpLuNgTJ7oqgvzm42YIrzvBlW6DPsnFjVZMzc4yCfaRN7Lmg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@turnkey/viem": { + "version": "0.14.28", + "resolved": "https://registry.npmjs.org/@turnkey/viem/-/viem-0.14.28.tgz", + "integrity": "sha512-UVCqe33EjcJ1BioeAgMxSpfZQL0ueCHjX9Ux3V9s6418Fbx1dueLozhkugIKNlNRYJ8SnpN+9BE9EX3aja9m2w==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "1.8.0", + "@openzeppelin/contracts": "^5.0.2", + "@turnkey/api-key-stamper": "0.6.5", + "@turnkey/core": "1.14.1", + "@turnkey/http": "3.18.1", + "@turnkey/sdk-browser": "5.16.1", + "@turnkey/sdk-server": "5.3.0", + "cross-fetch": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "viem": "^1.16.6 || ^2.24.2" + } + }, + "node_modules/@turnkey/viem/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/viem/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@turnkey/viem/node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/@turnkey/wallet-stamper": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@turnkey/wallet-stamper/-/wallet-stamper-1.1.16.tgz", + "integrity": "sha512-IdgoIRQr5RoiHAPnVag2QyaYg6cmmkZQs1rOig57/tQ/fXdPsC1+2660RisIAvv69ZFrArG8Z8wHaGSQMkStlw==", + "license": "Apache-2.0", + "dependencies": { + "@turnkey/crypto": "2.8.14", + "@turnkey/encoding": "0.6.0" + }, + "optionalDependencies": { + "viem": "^2.21.35" + } + }, + "node_modules/@turnkey/webauthn-stamper": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@turnkey/webauthn-stamper/-/webauthn-stamper-0.6.0.tgz", + "integrity": "sha512-jdN17QEnn7RBykEOhtKIialWmDjnDAH8DzbyITwn8jsKcwT1TBNYge89hTUTjbdsDLBAqQw8cHujPdy0RaAqvw==", + "license": "Apache-2.0", + "dependencies": { + "sha256-uint8array": "^0.10.7" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wallet-standard/app": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/app/-/app-1.1.0.tgz", + "integrity": "sha512-3CijvrO9utx598kjr45hTbbeeykQrQfKmSnxeWOgU25TOEpvcipD/bYDQWIqUv1Oc6KK4YStokSMu/FBNecGUQ==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@walletconnect/core": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.9.tgz", + "integrity": "sha512-ws4WG8LeagUo2ERRo02HryXRcpwIRmCQ3pHLW5gWbVReLXXIpgk6ZAfID3fEGHevIwwnHSGww+nNhNpdXyiq0g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.44.0", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.9.tgz", + "integrity": "sha512-Xj+hw4E6mGRyhCdVOT/RMgnG+up/Y3v0ho5PlkVozvXWeVSqHNh9DmjLuU97a7OACoGd/oHBF6g3NVqD7MgCMQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.9", + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/utils": "2.23.9", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/types": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.9.tgz", + "integrity": "sha512-IUl1PpD/Dig8IE2OZ9XtjbPohEyOZJ73xs92EDUzoIyzRtfm36g2D340pY3iu3AAdLv1yFiaZafB8Hf8RFze8A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.9", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.9.tgz", + "integrity": "sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.9", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@zama-fhe/relayer-sdk": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@zama-fhe/relayer-sdk/-/relayer-sdk-0.4.2.tgz", + "integrity": "sha512-3Q0tINKxz6YseaDxOrNP/648j5Wr2C4Utnjx5npZESjLAc20tWgWFE7BOfx0Kjhs8cx/ykU+tMZBq40+DsiUvA==", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "commander": "^14.0.0", + "ethers": "^6.15.0", + "fetch-retry": "^6.0.0", + "keccak": "^3.0.4", + "node-tfhe": "1.4.0-alpha.3", + "node-tkms": "^0.12.0", + "tfhe": "1.4.0-alpha.3", + "tkms": "^0.12.0", + "wasm-feature-detect": "^1.8.0" + }, + "bin": { + "relayer": "bin/relayer.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@zama-fhe/sdk": { + "version": "2.3.0-alpha.4", + "resolved": "https://registry.npmjs.org/@zama-fhe/sdk/-/sdk-2.3.0-alpha.4.tgz", + "integrity": "sha512-cIJLXeiBeU4rITbp26fP7KgmpB0ko9AdWSCqx+a7MeI6aR7T0109vTcYiXcWsPIC195VAX55YqPveSPkTAyUuw==", + "license": "BSD-3-Clause-Clear", + "dependencies": { + "@zama-fhe/relayer-sdk": "~0.4.2", + "viem": "^2.47.6" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5", + "ethers": ">=6", + "viem": ">=2" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "ethers": { + "optional": true + }, + "viem": { + "optional": true + } + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asn1js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", + "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==", + "license": "Apache-2.0" + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha512-7sQ/TvDZPl7csT1Sif9G0+MA0I0JOVah8+wWlJVQdVEgIbCzlN/ab3x+uvMNsc34TUvO6osQTAmB2ls80JX6tw==", + "license": "MIT" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz", + "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.5", + "@esbuild/android-arm": "0.27.5", + "@esbuild/android-arm64": "0.27.5", + "@esbuild/android-x64": "0.27.5", + "@esbuild/darwin-arm64": "0.27.5", + "@esbuild/darwin-x64": "0.27.5", + "@esbuild/freebsd-arm64": "0.27.5", + "@esbuild/freebsd-x64": "0.27.5", + "@esbuild/linux-arm": "0.27.5", + "@esbuild/linux-arm64": "0.27.5", + "@esbuild/linux-ia32": "0.27.5", + "@esbuild/linux-loong64": "0.27.5", + "@esbuild/linux-mips64el": "0.27.5", + "@esbuild/linux-ppc64": "0.27.5", + "@esbuild/linux-riscv64": "0.27.5", + "@esbuild/linux-s390x": "0.27.5", + "@esbuild/linux-x64": "0.27.5", + "@esbuild/netbsd-arm64": "0.27.5", + "@esbuild/netbsd-x64": "0.27.5", + "@esbuild/openbsd-arm64": "0.27.5", + "@esbuild/openbsd-x64": "0.27.5", + "@esbuild/openharmony-arm64": "0.27.5", + "@esbuild/sunos-x64": "0.27.5", + "@esbuild/win32-arm64": "0.27.5", + "@esbuild/win32-ia32": "0.27.5", + "@esbuild/win32-x64": "0.27.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-retry": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", + "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hpke-js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/hpke-js/-/hpke-js-1.8.0.tgz", + "integrity": "sha512-N0PFQlUQsIPS9++nUNn2ZsxTPSv8pONyyrXIGZl0iiherRfS0XW1SvTd+RmepD0TN1S9zzTJkEutMIWWYt0/4w==", + "license": "MIT", + "dependencies": { + "@hpke/chacha20poly1305": "^1.8.0", + "@hpke/common": "^1.10.0", + "@hpke/core": "^1.9.0", + "@hpke/dhkem-x25519": "^1.8.0", + "@hpke/dhkem-x448": "^1.8.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz", + "integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-tfhe": { + "version": "1.4.0-alpha.3", + "resolved": "https://registry.npmjs.org/node-tfhe/-/node-tfhe-1.4.0-alpha.3.tgz", + "integrity": "sha512-oTcWL0OFA6t6BhScmDiGQ3VA8tU8T3EXCzIzpNxQxcuJDgQtiUF5CV6dgJLOrpWck4KCp1Bo/xLhv07uwn3q6Q==", + "license": "BSD-3-Clause-Clear" + }, + "node_modules/node-tkms": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/node-tkms/-/node-tkms-0.12.8.tgz", + "integrity": "sha512-4erFxgbSVm1HCohIN2qijDfQL2GoIGaBve7SDeIKTu2bNBZZdTRKatcW+ExwHZF5MC6CzGDTvJQhEnG9LD7T3w==", + "license": "BSD-3-Clause-Clear" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ox": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.7.tgz", + "integrity": "sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvtsutils/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tfhe": { + "version": "1.4.0-alpha.3", + "resolved": "https://registry.npmjs.org/tfhe/-/tfhe-1.4.0-alpha.3.tgz", + "integrity": "sha512-xdla7hi2WzLFIdAx2/ihRZ/bKlKcgDDabTJGtoqp1E5oqhLM1PzTXsJE0p7tW8+ebrvxiMGfbgMAWnU3f2ZAIQ==", + "license": "BSD-3-Clause-Clear" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tkms": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/tkms/-/tkms-0.12.8.tgz", + "integrity": "sha512-iXS8wxz3jhx3JlKVJiBZUOibGtP69lC2H9I120EfhAI5amc/4xW/HCM7tmMtBJEuqdCoN5ssnHvfGog8gZ6UKg==", + "license": "BSD-3-Clause-Clear" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/viem": { + "version": "2.47.6", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.6.tgz", + "integrity": "sha512-zExmbI99NGvMdYa7fmqSTLgkwh48dmhgEqFrUgkpL4kfG4XkVefZ8dZqIKVUhZo6Uhf0FrrEXOsHm9LUyIvI2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.7", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/examples/compatibility-harness/package.json b/examples/compatibility-harness/package.json new file mode 100644 index 000000000..b3d1933ed --- /dev/null +++ b/examples/compatibility-harness/package.json @@ -0,0 +1,41 @@ +{ + "name": "zama-compatibility-harness", + "version": "1.0.0", + "private": true, + "description": "Validate whether your signing system is compatible with Ethereum EOA, EIP-712, and the Zama SDK.", + "type": "module", + "scripts": { + "test": "vitest run --reporter=verbose", + "test:crossmint": "SIGNER_MODULE=./examples/crossmint/signer.ts vitest run --reporter=verbose", + "test:openfort": "SIGNER_MODULE=./examples/openfort/signer.ts vitest run --reporter=verbose", + "test:turnkey": "SIGNER_MODULE=./examples/turnkey/signer.ts vitest run --reporter=verbose", + "init:adapter": "node --import tsx src/cli/init-adapter.ts", + "doctor": "node --import tsx src/cli/doctor.ts", + "doctor:crossmint": "SIGNER_MODULE=./examples/crossmint/signer.ts node --import tsx src/cli/doctor.ts", + "doctor:openfort": "SIGNER_MODULE=./examples/openfort/signer.ts node --import tsx src/cli/doctor.ts", + "doctor:turnkey": "SIGNER_MODULE=./examples/turnkey/signer.ts node --import tsx src/cli/doctor.ts", + "adapter:check": "node --import tsx src/cli/adapter-check.ts", + "adapter:check:crossmint": "SIGNER_MODULE=./examples/crossmint/signer.ts node --import tsx src/cli/adapter-check.ts", + "adapter:check:openfort": "SIGNER_MODULE=./examples/openfort/signer.ts node --import tsx src/cli/adapter-check.ts", + "adapter:check:turnkey": "SIGNER_MODULE=./examples/turnkey/signer.ts node --import tsx src/cli/adapter-check.ts", + "validate": "node --import tsx src/cli/validate.ts", + "validate:crossmint": "SIGNER_MODULE=./examples/crossmint/signer.ts node --import tsx src/cli/validate.ts", + "validate:openfort": "SIGNER_MODULE=./examples/openfort/signer.ts node --import tsx src/cli/validate.ts", + "validate:turnkey": "SIGNER_MODULE=./examples/turnkey/signer.ts node --import tsx src/cli/validate.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@turnkey/api-key-stamper": "^0.6.2", + "@turnkey/http": "^3.17.0", + "@turnkey/viem": "^0.14.25", + "@zama-fhe/sdk": "2.3.0-alpha.4", + "dotenv": "^16.4.0", + "viem": "^2.30.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0", + "vitest": "^3.0.0" + } +} diff --git a/examples/compatibility-harness/src/adapter/capability-evidence.ts b/examples/compatibility-harness/src/adapter/capability-evidence.ts new file mode 100644 index 000000000..6db0676c5 --- /dev/null +++ b/examples/compatibility-harness/src/adapter/capability-evidence.ts @@ -0,0 +1,43 @@ +import { + ALL_CAPABILITIES, + emptyCapabilities, + type AdapterCapabilities, + type CapabilityName, + type CapabilityState, +} from "./types.js"; + +function selectFinalState(input: { + structural: CapabilityState; + runtime: CapabilityState; +}): CapabilityState { + // Runtime observation has precedence over static method-shape inference. + return input.runtime !== "UNKNOWN" ? input.runtime : input.structural; +} + +export function resolveFinalCapabilities(input: { + structural: AdapterCapabilities; + runtime: AdapterCapabilities; +}): AdapterCapabilities { + const final = emptyCapabilities(); + for (const capability of ALL_CAPABILITIES) { + final[capability] = selectFinalState({ + structural: input.structural[capability], + runtime: input.runtime[capability], + }); + } + return final; +} + +export function mergeCapabilityPatch(input: { + base: AdapterCapabilities; + patch?: Partial; +}): AdapterCapabilities { + if (!input.patch) return input.base; + const merged = { ...input.base }; + for (const capability of ALL_CAPABILITIES) { + const next = input.patch[capability as CapabilityName]; + if (!next) continue; + merged[capability] = next; + } + return merged; +} diff --git a/examples/compatibility-harness/src/adapter/contradictions.ts b/examples/compatibility-harness/src/adapter/contradictions.ts new file mode 100644 index 000000000..aef88d057 --- /dev/null +++ b/examples/compatibility-harness/src/adapter/contradictions.ts @@ -0,0 +1,22 @@ +import { ALL_CAPABILITIES, type AdapterCapabilities } from "./types.js"; + +function titleize(name: string): string { + return name.replace(/([A-Z])/g, " $1").replace(/^./, (v) => v.toUpperCase()); +} + +export function detectCapabilityContradictions( + declared: AdapterCapabilities, + observed: AdapterCapabilities, +): string[] { + const contradictions: string[] = []; + for (const capability of ALL_CAPABILITIES) { + const declaredState = declared[capability]; + const observedState = observed[capability]; + if (declaredState === "UNKNOWN" || observedState === "UNKNOWN") continue; + if (declaredState === observedState) continue; + contradictions.push( + `${titleize(capability)} declared ${declaredState.toLowerCase()} but observed ${observedState.toLowerCase()}.`, + ); + } + return contradictions; +} diff --git a/examples/compatibility-harness/src/adapter/default.ts b/examples/compatibility-harness/src/adapter/default.ts new file mode 100644 index 000000000..8143a4797 --- /dev/null +++ b/examples/compatibility-harness/src/adapter/default.ts @@ -0,0 +1,105 @@ +import { createWalletClient, encodeFunctionData, getAddress, http, type Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { networkConfig } from "../config/network.js"; +import { publicClient } from "../utils/rpc.js"; +import type { Adapter, LegacySigner } from "./types.js"; + +function getConfiguredAccount() { + const rawKey = process.env.PRIVATE_KEY ?? ""; + const isValidKey = /^0x[0-9a-fA-F]{64}$/.test(rawKey); + + if (!rawKey || !isValidKey) { + throw new Error( + rawKey + ? `PRIVATE_KEY is invalid (got "${rawKey.slice(0, 6)}…"). Expected a 0x-prefixed 64-character hex string (32 bytes).` + : "PRIVATE_KEY is not set. Copy .env.example to .env and fill in your private key.", + ); + } + + return privateKeyToAccount(rawKey as `0x${string}`); +} + +function getWalletClient() { + const account = getConfiguredAccount(); + return { + account, + client: createWalletClient({ + account, + chain: networkConfig.chain, + transport: http(networkConfig.rpcUrl), + }), + }; +} + +export const defaultAdapter: Adapter = { + metadata: { + name: "Built-in EOA Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [networkConfig.chainId], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async getAddress() { + return getConfiguredAccount().address; + }, + async signTypedData(data) { + const { account, client } = getWalletClient(); + return client.signTypedData({ + account, + domain: data.domain, + types: data.types, + primaryType: data.primaryType, + message: data.message, + }); + }, + async signTransaction(tx) { + const { account, client } = getWalletClient(); + return client.signTransaction({ account, ...tx }); + }, + async writeContract(config) { + const { account, client } = getWalletClient(); + const request: Record = { + account, + address: getAddress(config.address), + abi: config.abi, + functionName: config.functionName, + args: config.args ?? [], + value: config.value, + }; + if (config.gas !== undefined) { + request.gas = config.gas; + } + return client.writeContract(request as never); + }, + async readContract(config) { + return publicClient.readContract(config); + }, + waitForTransactionReceipt(hash) { + return publicClient.waitForTransactionReceipt({ hash }); + }, +}; + +export const defaultLegacySigner: LegacySigner = { + get address() { + return getConfiguredAccount().address; + }, + signTypedData: (data) => defaultAdapter.signTypedData!(data), + signTransaction: (tx) => defaultAdapter.signTransaction!(tx), + async writeContract(config) { + if (!defaultAdapter.writeContract) { + throw new Error("writeContract is not implemented by the built-in adapter"); + } + const txHash = await defaultAdapter.writeContract(config); + return txHash as Hex; + }, +}; diff --git a/examples/compatibility-harness/src/adapter/index.ts b/examples/compatibility-harness/src/adapter/index.ts new file mode 100644 index 000000000..72eb6b790 --- /dev/null +++ b/examples/compatibility-harness/src/adapter/index.ts @@ -0,0 +1 @@ +export { defaultAdapter as adapter } from "./default.js"; diff --git a/examples/compatibility-harness/src/adapter/load.ts b/examples/compatibility-harness/src/adapter/load.ts new file mode 100644 index 000000000..fd34d6829 --- /dev/null +++ b/examples/compatibility-harness/src/adapter/load.ts @@ -0,0 +1,205 @@ +import { encodeFunctionData, getAddress, parseGwei, type Hex } from "viem"; +import { networkConfig } from "../config/network.js"; +import { publicClient } from "../utils/rpc.js"; +import type { + Adapter, + AdapterArchitecture, + AdapterCapabilities, + AdapterModuleShape, + CapabilityState, + ContractCallConfig, + LegacySigner, + VerificationModel, +} from "./types.js"; +import { emptyCapabilities } from "./types.js"; +import { resolveFinalCapabilities } from "./capability-evidence.js"; + +export interface LoadedAdapter { + adapter: Adapter; + init: () => Promise; + source: "adapter" | "legacy-signer"; + declaredCapabilities: AdapterCapabilities; + observedStructuralCapabilities: AdapterCapabilities; + observedRuntimeCapabilities: AdapterCapabilities; + observedCapabilities: AdapterCapabilities; +} + +function capabilityFromBoolean(value: boolean): CapabilityState { + return value ? "SUPPORTED" : "UNSUPPORTED"; +} + +function normalizeDeclaredCapabilities(adapter: Adapter): AdapterCapabilities { + return { + ...emptyCapabilities(), + ...adapter.capabilities, + }; +} + +function inferObservedCapabilitiesFromAdapter(adapter: Adapter): AdapterCapabilities { + const capabilities = emptyCapabilities(); + capabilities.addressResolution = "SUPPORTED"; + capabilities.eip712Signing = capabilityFromBoolean(typeof adapter.signTypedData === "function"); + capabilities.rawTransactionSigning = capabilityFromBoolean( + typeof adapter.signTransaction === "function", + ); + capabilities.contractExecution = capabilityFromBoolean( + typeof adapter.writeContract === "function", + ); + capabilities.contractReads = capabilityFromBoolean(typeof adapter.readContract === "function"); + capabilities.transactionReceiptTracking = capabilityFromBoolean( + typeof adapter.waitForTransactionReceipt === "function", + ); + capabilities.zamaAuthorizationFlow = + capabilities.eip712Signing === "SUPPORTED" ? "SUPPORTED" : "UNSUPPORTED"; + capabilities.zamaWriteFlow = + capabilities.contractExecution === "SUPPORTED" ? "SUPPORTED" : "UNSUPPORTED"; + return capabilities; +} + +async function sendWriteViaLegacySigner( + signer: LegacySigner, + config: ContractCallConfig, +): Promise { + if (signer.writeContract) { + return signer.writeContract(config) as Promise; + } + if (!signer.signTransaction) { + throw new Error("Legacy signer does not support contract execution"); + } + + const from = getAddress(signer.address) as `0x${string}`; + const calldata = encodeFunctionData({ + abi: config.abi, + functionName: config.functionName, + args: config.args ?? [], + }); + const nonce = await publicClient.getTransactionCount({ address: from }); + const feeData = await publicClient.estimateFeesPerGas(); + const gas = + config.gas ?? + (await publicClient.estimateGas({ + account: from, + to: getAddress(config.address) as `0x${string}`, + data: calldata, + value: config.value ?? 0n, + })); + + const signedTx = await signer.signTransaction({ + to: getAddress(config.address) as `0x${string}`, + value: config.value ?? 0n, + data: calldata, + gas, + maxFeePerGas: feeData.maxFeePerGas ?? parseGwei("20"), + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? parseGwei("1"), + nonce, + chainId: networkConfig.chainId, + type: "eip1559" as const, + }); + + return publicClient.sendRawTransaction({ + serializedTransaction: signedTx as `0x${string}`, + }); +} + +function wrapLegacySigner(signer: LegacySigner): Adapter { + const declaredArchitecture: AdapterArchitecture = + signer.signTransaction && !signer.writeContract ? "EOA" : "UNKNOWN"; + const verificationModel: VerificationModel = signer.signTransaction + ? "RECOVERABLE_ECDSA" + : "UNKNOWN"; + + return { + metadata: { + name: "Legacy Signer Adapter", + declaredArchitecture, + verificationModel, + supportedChainIds: [networkConfig.chainId], + notes: ["Wrapped from a legacy signer export for backwards compatibility."], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: capabilityFromBoolean(typeof signer.signTypedData === "function"), + rawTransactionSigning: capabilityFromBoolean(typeof signer.signTransaction === "function"), + contractExecution: capabilityFromBoolean( + typeof signer.writeContract === "function" || typeof signer.signTransaction === "function", + ), + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: capabilityFromBoolean(typeof signer.signTypedData === "function"), + zamaWriteFlow: capabilityFromBoolean( + typeof signer.writeContract === "function" || typeof signer.signTransaction === "function", + ), + }, + async getAddress() { + return signer.address; + }, + signTypedData: signer.signTypedData, + signTransaction: signer.signTransaction, + async writeContract(config) { + return sendWriteViaLegacySigner(signer, config); + }, + async readContract(config) { + return publicClient.readContract(config); + }, + waitForTransactionReceipt(hash) { + return publicClient.waitForTransactionReceipt({ hash }); + }, + }; +} + +function assertAdapterModule(module: AdapterModuleShape): LoadedAdapter { + if (module.adapter) { + const adapter = module.adapter; + const declaredCapabilities = normalizeDeclaredCapabilities(adapter); + const observedStructuralCapabilities = inferObservedCapabilitiesFromAdapter(adapter); + const observedRuntimeCapabilities = emptyCapabilities(); + const observedCapabilities = resolveFinalCapabilities({ + structural: observedStructuralCapabilities, + runtime: observedRuntimeCapabilities, + }); + adapter.capabilities = observedCapabilities; + return { + adapter, + source: "adapter", + declaredCapabilities, + observedStructuralCapabilities, + observedRuntimeCapabilities, + observedCapabilities, + init: async () => { + if (module.ready) await module.ready; + if (adapter.init) await adapter.init(); + }, + }; + } + + if (module.signer) { + const adapter = wrapLegacySigner(module.signer); + const declaredCapabilities = normalizeDeclaredCapabilities(adapter); + const observedStructuralCapabilities = inferObservedCapabilitiesFromAdapter(adapter); + const observedRuntimeCapabilities = emptyCapabilities(); + const observedCapabilities = resolveFinalCapabilities({ + structural: observedStructuralCapabilities, + runtime: observedRuntimeCapabilities, + }); + adapter.capabilities = observedCapabilities; + return { + adapter, + source: "legacy-signer", + declaredCapabilities, + observedStructuralCapabilities, + observedRuntimeCapabilities, + observedCapabilities, + init: async () => { + if (module.ready) await module.ready; + }, + }; + } + + throw new Error( + "Adapter module must export either `adapter` (preferred) or `signer` (legacy compatibility).", + ); +} + +export function loadAdapterModule(module: AdapterModuleShape): LoadedAdapter { + return assertAdapterModule(module); +} diff --git a/examples/compatibility-harness/src/adapter/profile.ts b/examples/compatibility-harness/src/adapter/profile.ts new file mode 100644 index 000000000..2b62627aa --- /dev/null +++ b/examples/compatibility-harness/src/adapter/profile.ts @@ -0,0 +1,88 @@ +import type { + AdapterArchitecture, + AdapterCapabilities, + CapabilityState, + VerificationModel, +} from "./types.js"; + +function isSupported(value: CapabilityState): boolean { + return value === "SUPPORTED"; +} + +function isUnsupported(value: CapabilityState): boolean { + return value === "UNSUPPORTED"; +} + +function contradictsDeclared( + declared: AdapterArchitecture, + capabilities: AdapterCapabilities, +): boolean { + switch (declared) { + case "EOA": + return isUnsupported(capabilities.recoverableEcdsa); + case "SMART_ACCOUNT": + case "MPC": + case "API_ROUTED_EXECUTION": + case "UNKNOWN": + return false; + } +} + +export function detectArchitecture( + declared: AdapterArchitecture | undefined, + capabilities: AdapterCapabilities, +): AdapterArchitecture { + if (declared && declared !== "UNKNOWN") { + if (contradictsDeclared(declared, capabilities)) { + return "UNKNOWN"; + } + return declared; + } + + if (isSupported(capabilities.recoverableEcdsa)) { + return "EOA"; + } + + if ( + isSupported(capabilities.contractExecution) && + isUnsupported(capabilities.rawTransactionSigning) && + capabilities.recoverableEcdsa !== "SUPPORTED" + ) { + return "API_ROUTED_EXECUTION"; + } + + if ( + isSupported(capabilities.eip712Signing) && + isUnsupported(capabilities.rawTransactionSigning) && + isUnsupported(capabilities.contractExecution) + ) { + return "MPC"; + } + + return "UNKNOWN"; +} + +export function detectVerificationModel( + declared: VerificationModel | undefined, + capabilities: AdapterCapabilities, +): VerificationModel { + if (declared && declared !== "UNKNOWN") { + if (declared === "RECOVERABLE_ECDSA" && capabilities.recoverableEcdsa === "UNSUPPORTED") { + return "UNKNOWN"; + } + return declared; + } + + if (capabilities.recoverableEcdsa === "SUPPORTED") { + return "RECOVERABLE_ECDSA"; + } + + if ( + capabilities.eip712Signing === "SUPPORTED" && + capabilities.recoverableEcdsa === "UNSUPPORTED" + ) { + return "PROVIDER_MANAGED"; + } + + return "UNKNOWN"; +} diff --git a/examples/compatibility-harness/src/adapter/runtime-observation.ts b/examples/compatibility-harness/src/adapter/runtime-observation.ts new file mode 100644 index 000000000..f69be792c --- /dev/null +++ b/examples/compatibility-harness/src/adapter/runtime-observation.ts @@ -0,0 +1,72 @@ +import type { ValidationStatus } from "./types.js"; +import type { AdapterCapabilities } from "./types.js"; +import type { CanonicalCheckId } from "../report/check-registry.js"; + +function patchForOperationalCheckStatus( + input: ValidationStatus, + capability: keyof AdapterCapabilities, +): Partial { + switch (input) { + case "PASS": + return { [capability]: "SUPPORTED" }; + case "FAIL": + // Failure after invocation still proves the capability surface exists. + return { [capability]: "SUPPORTED" }; + case "UNSUPPORTED": + return { [capability]: "UNSUPPORTED" }; + case "UNTESTED": + case "BLOCKED": + case "INCONCLUSIVE": + return {}; + } +} + +export function inferRuntimeCapabilityPatchFromCheck(input: { + checkId: CanonicalCheckId; + status: ValidationStatus; +}): Partial { + const { checkId, status } = input; + switch (checkId) { + case "ADDRESS_RESOLUTION": + return patchForOperationalCheckStatus(status, "addressResolution"); + case "EIP712_SIGNING": + return patchForOperationalCheckStatus(status, "eip712Signing"); + case "EIP712_RECOVERABILITY": + if (status === "PASS") return { recoverableEcdsa: "SUPPORTED" }; + if (status === "FAIL" || status === "UNSUPPORTED") return { recoverableEcdsa: "UNSUPPORTED" }; + return {}; + case "RAW_TRANSACTION_EXECUTION": + return patchForOperationalCheckStatus(status, "rawTransactionSigning"); + case "ADAPTER_CONTRACT_READ": + return patchForOperationalCheckStatus(status, "contractReads"); + case "ZAMA_AUTHORIZATION_FLOW": + return patchForOperationalCheckStatus(status, "zamaAuthorizationFlow"); + case "ZAMA_WRITE_FLOW": + if (status === "PASS") { + return { + contractExecution: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }; + } + if (status === "FAIL") { + return { + contractExecution: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }; + } + if (status === "UNSUPPORTED") { + return { + contractExecution: "UNSUPPORTED", + zamaWriteFlow: "UNSUPPORTED", + }; + } + return {}; + case "ADAPTER_INITIALIZATION": + case "ERC1271_VERIFICATION": + case "ENVIRONMENT_CONFIGURATION": + case "RPC_CONNECTIVITY": + case "RELAYER_REACHABILITY": + case "REGISTRY_TOKEN_DISCOVERY": + return {}; + } +} diff --git a/examples/compatibility-harness/src/adapter/types.ts b/examples/compatibility-harness/src/adapter/types.ts new file mode 100644 index 000000000..4cc01e5ee --- /dev/null +++ b/examples/compatibility-harness/src/adapter/types.ts @@ -0,0 +1,155 @@ +import type { Hex } from "viem"; + +export type AdapterArchitecture = + | "EOA" + | "MPC" + | "SMART_ACCOUNT" + | "API_ROUTED_EXECUTION" + | "UNKNOWN"; + +export type VerificationModel = "RECOVERABLE_ECDSA" | "ERC1271" | "PROVIDER_MANAGED" | "UNKNOWN"; + +export type CapabilityName = + | "addressResolution" + | "eip712Signing" + | "recoverableEcdsa" + | "rawTransactionSigning" + | "contractExecution" + | "contractReads" + | "transactionReceiptTracking" + | "zamaAuthorizationFlow" + | "zamaWriteFlow"; + +export type CapabilityState = "SUPPORTED" | "UNSUPPORTED" | "UNKNOWN"; + +export type ValidationStatus = + | "PASS" + | "FAIL" + | "UNTESTED" + | "UNSUPPORTED" + | "BLOCKED" + | "INCONCLUSIVE"; + +export type RootCauseCategory = + | "ADAPTER" + | "SIGNER" + | "RPC" + | "RELAYER" + | "REGISTRY" + | "ENVIRONMENT" + | "HARNESS"; + +export type DiagnosticCode = + | "ENV_MISSING_CONFIG" + | "ENV_INVALID_CONFIG" + | "ENV_INSUFFICIENT_FUNDS" + | "RPC_CONNECTIVITY" + | "RPC_RATE_LIMIT" + | "RELAYER_UNAVAILABLE" + | "REGISTRY_EMPTY" + | "REGISTRY_UNAVAILABLE" + | "HARNESS_UNKNOWN"; + +export interface AdapterMetadata { + name: string; + declaredArchitecture?: AdapterArchitecture; + verificationModel?: VerificationModel; + supportedChainIds?: number[]; + notes?: string[]; +} + +export interface AdapterCapabilities { + addressResolution: CapabilityState; + eip712Signing: CapabilityState; + recoverableEcdsa: CapabilityState; + rawTransactionSigning: CapabilityState; + contractExecution: CapabilityState; + contractReads: CapabilityState; + transactionReceiptTracking: CapabilityState; + zamaAuthorizationFlow: CapabilityState; + zamaWriteFlow: CapabilityState; +} + +export interface ContractCallConfig { + address: string; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + value?: bigint; + gas?: bigint; +} + +export interface TransactionReceiptLike { + status?: string; +} + +export interface Adapter { + metadata: AdapterMetadata; + capabilities?: Partial; + init?: () => Promise; + getAddress: () => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTypedData?: (data: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTransaction?: (tx: any) => Promise; + writeContract?: (config: ContractCallConfig) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readContract?: (config: any) => Promise; + waitForTransactionReceipt?: (hash: Hex) => Promise; +} + +export interface LegacySigner { + address: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTypedData: (data: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTransaction?: (tx: any) => Promise; + writeContract?: (config: ContractCallConfig) => Promise; +} + +export interface AdapterModuleShape { + adapter?: Adapter; + signer?: LegacySigner; + ready?: Promise; +} + +export interface ObservedAdapterProfile { + name: string; + source: "adapter" | "legacy-signer"; + declaredArchitecture: AdapterArchitecture; + detectedArchitecture: AdapterArchitecture; + verificationModel: VerificationModel; + address: string; + declaredCapabilities: AdapterCapabilities; + observedStructuralCapabilities: AdapterCapabilities; + observedRuntimeCapabilities: AdapterCapabilities; + observedCapabilities: AdapterCapabilities; + contradictions: string[]; + initializationStatus: ValidationStatus; +} + +export const ALL_CAPABILITIES: CapabilityName[] = [ + "addressResolution", + "eip712Signing", + "recoverableEcdsa", + "rawTransactionSigning", + "contractExecution", + "contractReads", + "transactionReceiptTracking", + "zamaAuthorizationFlow", + "zamaWriteFlow", +]; + +export function emptyCapabilities(): AdapterCapabilities { + return { + addressResolution: "UNKNOWN", + eip712Signing: "UNKNOWN", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNKNOWN", + contractExecution: "UNKNOWN", + contractReads: "UNKNOWN", + transactionReceiptTracking: "UNKNOWN", + zamaAuthorizationFlow: "UNKNOWN", + zamaWriteFlow: "UNKNOWN", + }; +} diff --git a/examples/compatibility-harness/src/cli/adapter-check-core.ts b/examples/compatibility-harness/src/cli/adapter-check-core.ts new file mode 100644 index 000000000..db8373e91 --- /dev/null +++ b/examples/compatibility-harness/src/cli/adapter-check-core.ts @@ -0,0 +1,256 @@ +import { detectCapabilityContradictions } from "../adapter/contradictions.js"; +import { + ALL_CAPABILITIES, + type AdapterArchitecture, + type AdapterCapabilities, + type CapabilityState, + type VerificationModel, +} from "../adapter/types.js"; +import { CHECK_REGISTRY, type CanonicalCheckId } from "../report/check-registry.js"; + +export type AdapterCheckSeverity = "PASS" | "WARN" | "FAIL"; + +export interface AdapterQualityInput { + source: "adapter" | "legacy-signer"; + metadata: { + name?: string; + declaredArchitecture?: AdapterArchitecture; + verificationModel?: VerificationModel; + supportedChainIds?: number[]; + }; + declaredCapabilities: AdapterCapabilities; + observedCapabilities: AdapterCapabilities; + chainId: number; +} + +export interface AdapterQualityCheck { + id: string; + severity: AdapterCheckSeverity; + message: string; + recommendation?: string; +} + +export interface CanonicalCheckSupport { + checkId: CanonicalCheckId; + name: string; + state: CapabilityState; +} + +export interface AdapterQualityReport { + checks: AdapterQualityCheck[]; + canonicalSupport: CanonicalCheckSupport[]; +} + +function isCapabilityState(value: string): value is CapabilityState { + return value === "SUPPORTED" || value === "UNSUPPORTED" || value === "UNKNOWN"; +} + +function canonicalStateForCheck( + checkId: CanonicalCheckId, + capabilities: AdapterCapabilities, + verificationModel?: VerificationModel, +): CapabilityState { + switch (checkId) { + case "ADAPTER_INITIALIZATION": + return "SUPPORTED"; + case "ADDRESS_RESOLUTION": + return capabilities.addressResolution; + case "EIP712_SIGNING": + return capabilities.eip712Signing; + case "EIP712_RECOVERABILITY": + return capabilities.recoverableEcdsa; + case "ERC1271_VERIFICATION": + return verificationModel === "ERC1271" ? "SUPPORTED" : "UNKNOWN"; + case "RAW_TRANSACTION_EXECUTION": + return capabilities.rawTransactionSigning; + case "ADAPTER_CONTRACT_READ": + return capabilities.contractReads; + case "ZAMA_AUTHORIZATION_FLOW": + return capabilities.zamaAuthorizationFlow; + case "ZAMA_WRITE_FLOW": + return capabilities.zamaWriteFlow; + case "ENVIRONMENT_CONFIGURATION": + case "RPC_CONNECTIVITY": + case "RELAYER_REACHABILITY": + case "REGISTRY_TOKEN_DISCOVERY": + return "UNKNOWN"; + } +} + +function summarizeMetadata(input: AdapterQualityInput): AdapterQualityCheck[] { + const checks: AdapterQualityCheck[] = []; + const name = (input.metadata.name ?? "").trim(); + if (!name) { + checks.push({ + id: "METADATA_NAME", + severity: "FAIL", + message: "Adapter metadata.name is missing or empty.", + recommendation: "Set metadata.name to a stable adapter identifier.", + }); + } else { + checks.push({ + id: "METADATA_NAME", + severity: "PASS", + message: `metadata.name="${name}"`, + }); + } + + if (!input.metadata.declaredArchitecture) { + checks.push({ + id: "METADATA_ARCHITECTURE", + severity: "WARN", + message: "Adapter metadata.declaredArchitecture is missing.", + recommendation: + "Declare adapter architecture (EOA, MPC, SMART_ACCOUNT, API_ROUTED_EXECUTION, UNKNOWN).", + }); + } else { + checks.push({ + id: "METADATA_ARCHITECTURE", + severity: "PASS", + message: `declaredArchitecture=${input.metadata.declaredArchitecture}`, + }); + } + + if (!input.metadata.verificationModel) { + checks.push({ + id: "METADATA_VERIFICATION_MODEL", + severity: "WARN", + message: "Adapter metadata.verificationModel is missing.", + recommendation: + "Declare verificationModel (RECOVERABLE_ECDSA, ERC1271, PROVIDER_MANAGED, UNKNOWN).", + }); + } else { + checks.push({ + id: "METADATA_VERIFICATION_MODEL", + severity: "PASS", + message: `verificationModel=${input.metadata.verificationModel}`, + }); + } + + const supportedChainIds = input.metadata.supportedChainIds ?? []; + if (supportedChainIds.length === 0) { + checks.push({ + id: "METADATA_CHAIN_IDS", + severity: "WARN", + message: "Adapter metadata.supportedChainIds is missing or empty.", + recommendation: "Declare at least one supported chain id.", + }); + } else if (!supportedChainIds.includes(input.chainId)) { + checks.push({ + id: "METADATA_CHAIN_IDS", + severity: "WARN", + message: `Current chainId=${input.chainId} is not listed in supportedChainIds=[${supportedChainIds.join(", ")}].`, + recommendation: + "If this profile is intentional, switch NETWORK_PROFILE or update supportedChainIds.", + }); + } else { + checks.push({ + id: "METADATA_CHAIN_IDS", + severity: "PASS", + message: `supportedChainIds include current chainId (${input.chainId}).`, + }); + } + + return checks; +} + +function summarizeCapabilityShape(capabilities: AdapterCapabilities): AdapterQualityCheck[] { + const checks: AdapterQualityCheck[] = []; + for (const capability of ALL_CAPABILITIES) { + const state = capabilities[capability]; + if (!isCapabilityState(state)) { + checks.push({ + id: `CAPABILITY_${capability.toUpperCase()}`, + severity: "FAIL", + message: `${capability} has invalid state "${String(state)}".`, + recommendation: 'Use one of: "SUPPORTED", "UNSUPPORTED", "UNKNOWN".', + }); + } + } + if (checks.length === 0) { + checks.push({ + id: "CAPABILITY_SHAPE", + severity: "PASS", + message: "Declared capability states are valid.", + }); + } + return checks; +} + +function summarizeCapabilityConsistency(input: AdapterQualityInput): AdapterQualityCheck[] { + const checks: AdapterQualityCheck[] = []; + const declared = input.declaredCapabilities; + const observed = input.observedCapabilities; + + const contradictions = detectCapabilityContradictions(declared, observed); + if (contradictions.length === 0) { + checks.push({ + id: "CAPABILITY_CONTRADICTIONS", + severity: "PASS", + message: "No declared/observed capability contradictions detected.", + }); + } else { + for (const contradiction of contradictions) { + checks.push({ + id: "CAPABILITY_CONTRADICTIONS", + severity: "FAIL", + message: contradiction, + recommendation: + "Align declared capabilities with exposed adapter methods before running full validation.", + }); + } + } + + if (declared.zamaAuthorizationFlow === "SUPPORTED" && declared.eip712Signing !== "SUPPORTED") { + checks.push({ + id: "CAPABILITY_AUTH_DEPENDENCY", + severity: "FAIL", + message: "zamaAuthorizationFlow=SUPPORTED requires eip712Signing=SUPPORTED.", + recommendation: + "Mark zamaAuthorizationFlow as unsupported, or implement/sign EIP-712 payloads.", + }); + } + + if (declared.zamaWriteFlow === "SUPPORTED" && declared.contractExecution !== "SUPPORTED") { + checks.push({ + id: "CAPABILITY_WRITE_DEPENDENCY", + severity: "FAIL", + message: "zamaWriteFlow=SUPPORTED requires contractExecution=SUPPORTED.", + recommendation: "Mark zamaWriteFlow as unsupported, or implement writeContract routing.", + }); + } + + if (declared.recoverableEcdsa === "SUPPORTED" && declared.eip712Signing !== "SUPPORTED") { + checks.push({ + id: "CAPABILITY_RECOVERABILITY_DEPENDENCY", + severity: "FAIL", + message: "recoverableEcdsa=SUPPORTED requires eip712Signing=SUPPORTED.", + recommendation: "Align recoverableEcdsa declaration with signing capability.", + }); + } + + return checks; +} + +export function evaluateAdapterQuality(input: AdapterQualityInput): AdapterQualityReport { + const checks: AdapterQualityCheck[] = []; + checks.push(...summarizeMetadata(input)); + checks.push(...summarizeCapabilityShape(input.declaredCapabilities)); + checks.push(...summarizeCapabilityConsistency(input)); + + const canonicalSupport = CHECK_REGISTRY.filter((check) => !check.synthetic).map((check) => ({ + checkId: check.id, + name: check.name, + state: canonicalStateForCheck( + check.id, + input.declaredCapabilities, + input.metadata.verificationModel, + ), + })); + + return { checks, canonicalSupport }; +} + +export function adapterQualityExitCode(report: AdapterQualityReport): number { + return report.checks.some((check) => check.severity === "FAIL") ? 2 : 0; +} diff --git a/examples/compatibility-harness/src/cli/adapter-check.ts b/examples/compatibility-harness/src/cli/adapter-check.ts new file mode 100644 index 000000000..d7acb22d6 --- /dev/null +++ b/examples/compatibility-harness/src/cli/adapter-check.ts @@ -0,0 +1,75 @@ +import "dotenv/config"; +import { errorMessage } from "../harness/diagnostics.js"; +import { + adapterQualityExitCode, + evaluateAdapterQuality, + type AdapterQualityReport, +} from "./adapter-check-core.js"; + +function icon(severity: "PASS" | "WARN" | "FAIL"): string { + if (severity === "PASS") return "✓"; + if (severity === "WARN") return "!"; + return "✗"; +} + +function printReport(input: { name: string; source: string; report: AdapterQualityReport }): void { + const passes = input.report.checks.filter((check) => check.severity === "PASS").length; + const warns = input.report.checks.filter((check) => check.severity === "WARN").length; + const fails = input.report.checks.filter((check) => check.severity === "FAIL").length; + + console.log("\nAdapter Quality Report"); + console.log("======================\n"); + console.log(`Adapter: ${input.name}`); + console.log(`Source: ${input.source}\n`); + + for (const check of input.report.checks) { + console.log(`${icon(check.severity)} ${check.severity} ${check.id}`); + console.log(` ${check.message}`); + if (check.recommendation) { + console.log(` Recommendation: ${check.recommendation}`); + } + } + + console.log("\nCanonical Check Support"); + console.log("-----------------------"); + for (const check of input.report.canonicalSupport) { + console.log(`- ${check.checkId}: ${check.state} (${check.name})`); + } + + console.log(""); + console.log(`Summary: ${passes} PASS, ${warns} WARN, ${fails} FAIL`); +} + +async function runAdapterCheck(): Promise { + try { + const [harness, config] = await Promise.all([ + import("../harness/adapter.js"), + import("../config/network.js"), + ]); + const report = evaluateAdapterQuality({ + source: harness.adapterSource, + metadata: harness.adapter.metadata, + declaredCapabilities: harness.adapterDeclaredCapabilities, + observedCapabilities: harness.adapterObservedCapabilities, + chainId: config.networkConfig.chainId, + }); + printReport({ + name: harness.adapter.metadata.name, + source: harness.adapterSource, + report, + }); + return adapterQualityExitCode(report); + } catch (error) { + console.error(`adapter:check failed: ${errorMessage(error)}`); + return 2; + } +} + +runAdapterCheck() + .then((code) => { + process.exitCode = code; + }) + .catch((error) => { + console.error(`adapter:check failed unexpectedly: ${errorMessage(error)}`); + process.exitCode = 99; + }); diff --git a/examples/compatibility-harness/src/cli/doctor.ts b/examples/compatibility-harness/src/cli/doctor.ts new file mode 100644 index 000000000..30d1089f7 --- /dev/null +++ b/examples/compatibility-harness/src/cli/doctor.ts @@ -0,0 +1,129 @@ +import "dotenv/config"; +import { networkConfig } from "../config/network.js"; +import type { DiagnosticCode, RootCauseCategory } from "../adapter/types.js"; +import { + adapter, + getAdapterAddress, + initializeAdapter, + adapterSource, + adapterDeclaredCapabilities, +} from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { publicClient } from "../utils/rpc.js"; + +type DoctorStatus = "PASS" | "BLOCKED" | "INCONCLUSIVE"; + +type DoctorCheck = { + name: string; + status: DoctorStatus; + note: string; + rootCauseCategory?: RootCauseCategory; + errorCode?: DiagnosticCode; +}; + +async function runCheck(name: string, fn: () => Promise): Promise { + try { + const note = await fn(); + return { name, status: "PASS", note }; + } catch (error) { + const message = errorMessage(error); + const diagnostic = classifyInfrastructureIssue(message); + return { + name, + status: diagnostic.status === "BLOCKED" ? "BLOCKED" : "INCONCLUSIVE", + note: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + }; + } +} + +async function runDoctor(): Promise { + const checks: DoctorCheck[] = []; + + checks.push({ + name: "Adapter Module", + status: "PASS", + note: `${adapter.metadata.name} (${adapterSource})`, + }); + checks.push({ + name: "Network Profile", + status: "PASS", + note: `${networkConfig.profile} (${networkConfig.profileLabel}, chainId=${networkConfig.chainId}, support=${networkConfig.zamaSupport})`, + }); + checks.push({ + name: "Declared Capabilities", + status: "PASS", + note: Object.entries(adapterDeclaredCapabilities) + .map(([key, value]) => `${key}=${value}`) + .join(", "), + }); + + checks.push( + await runCheck("Adapter Initialization", async () => { + await initializeAdapter(); + return "Initialization succeeded"; + }), + ); + + checks.push( + await runCheck("Address Resolution", async () => { + const address = await getAdapterAddress(); + return `Resolved ${address}`; + }), + ); + + checks.push( + await runCheck("RPC Connectivity", async () => { + const chainId = await publicClient.getChainId(); + if (chainId !== networkConfig.chainId) { + throw new Error( + `RPC chain mismatch: expected ${networkConfig.chainId}, got ${chainId} (${networkConfig.rpcUrl})`, + ); + } + return `RPC reachable on chain ${chainId}`; + }), + ); + + checks.push( + await runCheck("Relayer Reachability", async () => { + const response = await fetch(networkConfig.relayerUrl, { + method: "GET", + }); + if (!response.ok) { + throw new Error(`Relayer returned HTTP ${response.status} for ${networkConfig.relayerUrl}`); + } + return `Relayer responded with HTTP ${response.status}`; + }), + ); + + const blocked = checks.filter((check) => check.status === "BLOCKED").length; + const inconclusive = checks.filter((check) => check.status === "INCONCLUSIVE").length; + + console.log("\nDoctor Report"); + console.log("=============\n"); + for (const check of checks) { + const icon = check.status === "PASS" ? "✓" : check.status === "BLOCKED" ? "!" : "?"; + console.log(`${icon} ${check.name}: ${check.status}`); + console.log(` ${check.note}`); + if (check.rootCauseCategory) console.log(` rootCause=${check.rootCauseCategory}`); + if (check.errorCode) console.log(` errorCode=${check.errorCode}`); + } + console.log(""); + console.log( + `Summary: ${checks.length - blocked - inconclusive} PASS, ${blocked} BLOCKED, ${inconclusive} INCONCLUSIVE`, + ); + + if (blocked > 0) return 2; + if (inconclusive > 0) return 3; + return 0; +} + +runDoctor() + .then((code) => { + process.exitCode = code; + }) + .catch((error) => { + console.error("Doctor failed unexpectedly:", errorMessage(error)); + process.exitCode = 99; + }); diff --git a/examples/compatibility-harness/src/cli/init-adapter.ts b/examples/compatibility-harness/src/cli/init-adapter.ts new file mode 100644 index 000000000..e4a419f45 --- /dev/null +++ b/examples/compatibility-harness/src/cli/init-adapter.ts @@ -0,0 +1,448 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const DEFAULT_OUTPUT_PATH = "./my-adapter.ts"; +const DEFAULT_TEMPLATE = "generic"; + +export type AdapterTemplateKind = + | "generic" + | "eoa" + | "mpc" + | "api-routed" + | "turnkey" + | "crossmint" + | "openfort"; + +export interface InitAdapterConfig { + outputPath: string; + template: AdapterTemplateKind; + showHelp: boolean; +} + +const TEMPLATE_ALIASES: Record = { + generic: "generic", + eoa: "eoa", + mpc: "mpc", + api: "api-routed", + "api-routed": "api-routed", + turnkey: "turnkey", + crossmint: "crossmint", + openfort: "openfort", +}; + +export function normalizeTemplate(input: string | undefined): AdapterTemplateKind { + const normalized = input?.trim().toLowerCase(); + if (!normalized) return DEFAULT_TEMPLATE; + const resolved = TEMPLATE_ALIASES[normalized]; + if (!resolved) { + const valid = Object.keys(TEMPLATE_ALIASES).sort().join(", "); + throw new Error(`Unsupported template "${input}". Supported templates: ${valid}.`); + } + return resolved; +} + +export function resolveInitAdapterConfig( + argv: string[] = process.argv, + env: NodeJS.ProcessEnv = process.env, +): InitAdapterConfig { + const args = argv.slice(2); + let outputPath: string | undefined; + let templateInput = env.ADAPTER_TEMPLATE?.trim(); + let showHelp = false; + + for (let i = 0; i < args.length; i += 1) { + const current = args[i]?.trim(); + if (!current) continue; + + if (current === "-h" || current === "--help") { + showHelp = true; + continue; + } + + if (current === "-o" || current === "--output") { + outputPath = args[i + 1]?.trim(); + i += 1; + continue; + } + + if (current === "-t" || current === "--template") { + templateInput = args[i + 1]?.trim(); + i += 1; + continue; + } + + if (!current.startsWith("-")) { + outputPath = current; + continue; + } + + throw new Error(`Unsupported option "${current}". Use --help for usage.`); + } + + const envPath = env.ADAPTER_TEMPLATE_PATH?.trim(); + const resolvedOutputPath = outputPath ?? envPath ?? DEFAULT_OUTPUT_PATH; + const template = normalizeTemplate(templateInput); + return { outputPath: resolvedOutputPath, template, showHelp }; +} + +export function resolveOutputPath( + argv: string[] = process.argv, + env: NodeJS.ProcessEnv = process.env, +): string { + return resolveInitAdapterConfig(argv, env).outputPath; +} + +export function importPathForTarget(outputPath: string): string { + const from = dirname(outputPath); + const relativePath = relative(from, "./src/adapter/types.js"); + const normalized = relativePath.startsWith(".") ? relativePath : `./${relativePath}`; + return normalized.replaceAll("\\", "/"); +} + +export function templateFor( + outputPath: string, + template: AdapterTemplateKind = DEFAULT_TEMPLATE, +): string { + const typesImportPath = importPathForTarget(outputPath); + const sharedHeader = `import type { Adapter } from "${typesImportPath}"; + +`; + + if (template === "eoa") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "My EOA Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template eoa"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + // Optional async bootstrap + }, + async getAddress() { + throw new Error("Implement getAddress()"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData()"); + }, + async signTransaction(_tx) { + throw new Error("Implement signTransaction()"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract()"); + }, + // Optional: + // async readContract(config) { ... } + // async waitForTransactionReceipt(hash) { ... } +}; +`; + } + + if (template === "mpc") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "My MPC Adapter", + declaredArchitecture: "MPC", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template mpc"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + // Optional async bootstrap + }, + async getAddress() { + throw new Error("Implement getAddress()"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData()"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() via your provider API"); + }, + // Optional: + // async readContract(config) { ... } + // async waitForTransactionReceipt(hash) { ... } +}; +`; + } + + if (template === "api-routed") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "My API-Routed Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "PROVIDER_MANAGED", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template api-routed"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "UNKNOWN", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "UNKNOWN", + zamaWriteFlow: "UNKNOWN", + }, + async init() { + // Optional async bootstrap + }, + async getAddress() { + throw new Error("Implement getAddress()"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() via your provider API"); + }, + // Optional: + // async signTypedData(data) { ... } + // async readContract(config) { ... } + // async waitForTransactionReceipt(hash) { ... } +}; +`; + } + + if (template === "turnkey") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "Turnkey API Key Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template turnkey"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + // Optional async bootstrap for API client and account resolution. + }, + async getAddress() { + throw new Error("Implement getAddress() from Turnkey account metadata"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData() via @turnkey/viem account"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() via Turnkey wallet client"); + }, + async readContract(_config) { + throw new Error("Implement readContract() via viem public client"); + }, + async waitForTransactionReceipt(_hash) { + throw new Error("Implement waitForTransactionReceipt() via public client"); + }, +}; +`; + } + + if (template === "crossmint") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "Crossmint API-Routed Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template crossmint"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "UNSUPPORTED", + transactionReceiptTracking: "UNSUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + // Optional async bootstrap for API auth and wallet discovery. + }, + async getAddress() { + throw new Error("Implement getAddress() from Crossmint wallet endpoint"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData() through Crossmint signatures API"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() through Crossmint transactions API"); + }, +}; +`; + } + + if (template === "openfort") { + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "Openfort EOA Baseline Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template openfort"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }, + async init() { + // Optional async bootstrap. + }, + async getAddress() { + throw new Error("Implement getAddress() from Openfort-controlled EOA key"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData() with EOA semantics"); + }, + async signTransaction(_tx) { + throw new Error("Implement signTransaction() if raw signing is exposed"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() with your wallet client"); + }, + async readContract(_config) { + throw new Error("Implement readContract() with public client"); + }, + async waitForTransactionReceipt(_hash) { + throw new Error("Implement waitForTransactionReceipt() with public client"); + }, +}; +`; + } + + return `${sharedHeader}export const adapter: Adapter = { + metadata: { + name: "My Adapter", + declaredArchitecture: "UNKNOWN", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + notes: ["Generated via npm run init:adapter -- --template generic"], + }, + capabilities: { + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "UNKNOWN", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "UNKNOWN", + }, + async init() { + // Optional async bootstrap + }, + async getAddress() { + throw new Error("Implement getAddress()"); + }, + async signTypedData(_data) { + throw new Error("Implement signTypedData()"); + }, + async writeContract(_config) { + throw new Error("Implement writeContract() or mark contractExecution unsupported"); + }, + // Optional: + // async signTransaction(tx) { ... } + // async readContract(config) { ... } + // async waitForTransactionReceipt(hash) { ... } +}; +`; +} + +export function usage(): string { + return `Usage: npm run init:adapter -- [output-path] [--template ] [--output ] + +Templates: + generic Conservative default template + eoa EOA-style adapter (raw transaction signing expected) + mpc MPC-style adapter (API-routed execution, no raw signing) + api-routed Provider-managed execution model + turnkey Turnkey API-key execution baseline + crossmint Crossmint API-routed execution baseline + openfort Openfort EOA baseline +`; +} + +function printUsage(): void { + console.log(usage()); +} + +export function runInitAdapter( + argv: string[] = process.argv, + env: NodeJS.ProcessEnv = process.env, +): number { + let config: InitAdapterConfig; + try { + config = resolveInitAdapterConfig(argv, env); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`init:adapter: ${message}`); + console.error("Run with --help for usage."); + return 2; + } + + if (config.showHelp) { + printUsage(); + return 0; + } + + const { outputPath, template } = config; + if (existsSync(outputPath)) { + console.error(`init:adapter: file already exists at ${outputPath}`); + return 2; + } + + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, templateFor(outputPath, template)); + + console.log("Adapter template created."); + console.log(`Template: ${template}`); + console.log(`Path: ${outputPath}`); + console.log(`Run: SIGNER_MODULE=${outputPath} npm test`); + return 0; +} + +const isMain = process.argv[1] === fileURLToPath(import.meta.url); +if (isMain) { + process.exitCode = runInitAdapter(); +} diff --git a/examples/compatibility-harness/src/cli/validate-config.ts b/examples/compatibility-harness/src/cli/validate-config.ts new file mode 100644 index 000000000..22e300c28 --- /dev/null +++ b/examples/compatibility-harness/src/cli/validate-config.ts @@ -0,0 +1,94 @@ +import { existsSync, readFileSync } from "node:fs"; +import { + parseValidationTarget, + type ValidationPolicy, + type ValidationTarget, +} from "./validate-policy.js"; + +type RawValidationPolicyFile = { + target?: unknown; + allowPartial?: unknown; + expectedClaims?: unknown; +}; + +export interface ValidationConfig { + target: ValidationTarget; + policy: ValidationPolicy; + policyPath?: string; +} + +function parseBoolean(name: string, raw: string | undefined): boolean | undefined { + if (raw === undefined) return undefined; + const normalized = raw.trim().toLowerCase(); + if (!normalized) return undefined; + if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") { + return true; + } + if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") { + return false; + } + throw new Error(`Invalid ${name}="${raw}". Expected true/false.`); +} + +function parsePolicyFile(path: string): RawValidationPolicyFile { + if (!existsSync(path)) { + throw new Error(`Validation policy file not found: ${path}`); + } + const raw = readFileSync(path, "utf-8"); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new Error(`Validation policy file is not valid JSON: ${path}`); + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`Validation policy must be a JSON object: ${path}`); + } + return parsed as RawValidationPolicyFile; +} + +function normalizeExpectedClaims(input: unknown): string[] { + if (input === undefined) return []; + if (!Array.isArray(input)) { + throw new Error("Validation policy expectedClaims must be an array of strings."); + } + const claims = input + .map((v) => (typeof v === "string" ? v.trim() : "")) + .filter((v) => v.length > 0); + if (claims.length !== input.length) { + throw new Error("Validation policy expectedClaims must contain only non-empty strings."); + } + return [...new Set(claims)]; +} + +export function resolveValidationConfig(env: NodeJS.ProcessEnv = process.env): ValidationConfig { + const policyPath = (env.VALIDATION_POLICY_PATH ?? "").trim(); + const filePolicy = policyPath ? parsePolicyFile(policyPath) : {}; + + const target = parseValidationTarget( + typeof env.VALIDATION_TARGET === "string" && env.VALIDATION_TARGET.trim().length > 0 + ? env.VALIDATION_TARGET + : typeof filePolicy.target === "string" + ? filePolicy.target + : undefined, + ); + + const allowPartialFromEnv = parseBoolean( + "VALIDATION_ALLOW_PARTIAL", + env.VALIDATION_ALLOW_PARTIAL, + ); + const allowPartial = + allowPartialFromEnv ?? + (typeof filePolicy.allowPartial === "boolean" ? filePolicy.allowPartial : false); + + const expectedClaims = normalizeExpectedClaims(filePolicy.expectedClaims); + + return { + target, + policy: { + allowPartial, + expectedClaims, + }, + ...(policyPath ? { policyPath } : {}), + }; +} diff --git a/examples/compatibility-harness/src/cli/validate-policy.ts b/examples/compatibility-harness/src/cli/validate-policy.ts new file mode 100644 index 000000000..f5c106082 --- /dev/null +++ b/examples/compatibility-harness/src/cli/validate-policy.ts @@ -0,0 +1,144 @@ +export type ValidationTarget = "AUTHORIZATION" | "AUTHORIZATION_AND_WRITE"; + +export type ValidationGateStatus = "PASS" | "PARTIAL" | "FAIL" | "INCONCLUSIVE"; + +export interface ValidationGateDecision { + target: ValidationTarget; + status: ValidationGateStatus; + exitCode: number; + summary: string; +} + +export interface ValidationPolicy { + allowPartial: boolean; + expectedClaims: string[]; +} + +export interface EffectiveValidationGate { + status: ValidationGateStatus | "FAIL"; + exitCode: number; + summary: string; + note?: string; +} + +const INCOMPATIBLE_CLAIMS = new Set([ + "INCOMPATIBLE_AUTHORIZATION_FAILED", + "INCOMPATIBLE_AUTHORIZATION_UNSUPPORTED", + "INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY", +]); + +const INCONCLUSIVE_CLAIMS = new Set([ + "INCONCLUSIVE_AUTHORIZATION_BLOCKED", + "INCONCLUSIVE_AUTHORIZATION_UNTESTED", + "PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED", + "PARTIAL_AUTHORIZATION_CHECK_MISSING", +]); + +const AUTHORIZATION_COMPATIBLE_CLAIMS = new Set([ + "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED", + "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED", +]); + +function normalizeTarget(raw: string | undefined): string { + return (raw ?? "AUTHORIZATION").trim().toUpperCase(); +} + +export function parseValidationTarget(raw: string | undefined): ValidationTarget { + const normalized = normalizeTarget(raw); + if (normalized === "AUTHORIZATION") return "AUTHORIZATION"; + if (normalized === "AUTHORIZATION_AND_WRITE") return "AUTHORIZATION_AND_WRITE"; + throw new Error( + `Invalid VALIDATION_TARGET="${raw}". Expected AUTHORIZATION or AUTHORIZATION_AND_WRITE.`, + ); +} + +export function resolveValidationGate( + claimId: string | undefined, + target: ValidationTarget, +): ValidationGateDecision { + const id = (claimId ?? "").trim(); + + if (INCOMPATIBLE_CLAIMS.has(id)) { + return { + target, + status: "FAIL", + exitCode: 20, + summary: "Authorization compatibility failed.", + }; + } + + if (INCONCLUSIVE_CLAIMS.has(id)) { + return { + target, + status: "INCONCLUSIVE", + exitCode: 30, + summary: "Authorization compatibility is inconclusive.", + }; + } + + if (AUTHORIZATION_COMPATIBLE_CLAIMS.has(id)) { + if (id === "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE") { + return { + target, + status: "PASS", + exitCode: 0, + summary: "Authorization and write compatibility validated.", + }; + } + if (target === "AUTHORIZATION") { + return { + target, + status: "PASS", + exitCode: 0, + summary: "Authorization compatibility validated for requested scope.", + }; + } + return { + target, + status: "PARTIAL", + exitCode: 10, + summary: "Authorization validated, but write compatibility is only partially validated.", + }; + } + + return { + target, + status: "INCONCLUSIVE", + exitCode: 31, + summary: "Unknown claim. Compatibility gate is inconclusive.", + }; +} + +export function applyValidationPolicy( + decision: ValidationGateDecision, + claimId: string, + policy: ValidationPolicy, +): EffectiveValidationGate { + if (policy.expectedClaims.length > 0 && !policy.expectedClaims.includes(claimId)) { + return { + status: "FAIL", + exitCode: 21, + summary: `Claim "${claimId}" is not allowed by policy.`, + note: `Allowed claims: ${policy.expectedClaims.join(", ")}`, + }; + } + + if (decision.status === "PARTIAL" && policy.allowPartial) { + return { + status: "PASS", + exitCode: 0, + summary: "Partial validation accepted by policy.", + note: "allowPartial=true", + }; + } + + return { + status: decision.status, + exitCode: decision.exitCode, + summary: decision.summary, + }; +} diff --git a/examples/compatibility-harness/src/cli/validate.ts b/examples/compatibility-harness/src/cli/validate.ts new file mode 100644 index 000000000..bd3a04fd4 --- /dev/null +++ b/examples/compatibility-harness/src/cli/validate.ts @@ -0,0 +1,128 @@ +import "dotenv/config"; +import { existsSync, readFileSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import type { ReportArtifact } from "../report/schema.js"; +import { parseReportArtifact } from "../report/parse.js"; +import { resolveValidationConfig } from "./validate-config.js"; +import { applyValidationPolicy, resolveValidationGate } from "./validate-policy.js"; + +function resolveReportPath(): { path: string; ephemeral: boolean } { + const configured = (process.env.REPORT_JSON_PATH ?? "").trim(); + if (configured) return { path: configured, ephemeral: false }; + const generated = join(tmpdir(), `zama-harness-report-${Date.now()}-${process.pid}.json`); + return { path: generated, ephemeral: true }; +} + +function runHarness(reportPath: string): number { + const child = spawnSync("npm", ["test"], { + stdio: "inherit", + env: { + ...process.env, + REPORT_JSON_PATH: reportPath, + }, + }); + + if (child.error) { + console.error(`validate: unable to run npm test: ${child.error.message}`); + return 98; + } + if (typeof child.status === "number") { + if (child.status !== 0) { + console.error(`validate: npm test exited with code ${child.status}.`); + } + return child.status; + } + console.error("validate: npm test did not return a numeric exit status."); + return 98; +} + +function readArtifact(path: string): ReportArtifact { + if (!existsSync(path)) { + throw new Error(`Report artifact not found at ${path}.`); + } + const raw = readFileSync(path, "utf-8"); + return parseReportArtifact(raw); +} + +function printValidationSummary(input: { + target: string; + reportPath: string; + claimId: string; + finalVerdict: string; + decision: ReturnType; + effective: ReturnType; + policyPath?: string; +}): void { + console.log("\nValidation Gate"); + console.log("===============\n"); + console.log(`Target: ${input.target}`); + if (input.policyPath) { + console.log(`Policy: ${input.policyPath}`); + } + console.log(`Claim: ${input.claimId}`); + console.log(`Verdict: ${input.finalVerdict}`); + console.log(`Gate Status: ${input.effective.status}`); + console.log(`Summary: ${input.effective.summary}`); + if (input.effective.note) { + console.log(`Policy note: ${input.effective.note}`); + } + if ( + input.decision.status !== input.effective.status || + input.decision.exitCode !== input.effective.exitCode + ) { + console.log(`Base gate: ${input.decision.status} (exit ${input.decision.exitCode})`); + } + console.log(`Exit code: ${input.effective.exitCode}`); + console.log(`Report JSON: ${input.reportPath}\n`); +} + +async function runValidate(): Promise { + let config: ReturnType; + try { + config = resolveValidationConfig(process.env); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`validate: ${message}`); + return 97; + } + + const { path: reportPath, ephemeral } = resolveReportPath(); + const testExit = runHarness(reportPath); + if (testExit !== 0) return testExit; + + try { + const artifact = readArtifact(reportPath); + const decision = resolveValidationGate(artifact.claim.id, config.target); + const effective = applyValidationPolicy(decision, artifact.claim.id, config.policy); + printValidationSummary({ + target: config.target, + reportPath, + claimId: artifact.claim.id, + finalVerdict: artifact.finalVerdict, + decision, + effective, + policyPath: config.policyPath, + }); + return effective.exitCode; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`validate: ${message}`); + return 97; + } finally { + if (ephemeral && existsSync(reportPath)) { + unlinkSync(reportPath); + } + } +} + +runValidate() + .then((code) => { + process.exitCode = code; + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`validate: unexpected failure: ${message}`); + process.exitCode = 99; + }); diff --git a/examples/compatibility-harness/src/config/network.ts b/examples/compatibility-harness/src/config/network.ts new file mode 100644 index 000000000..13bb42206 --- /dev/null +++ b/examples/compatibility-harness/src/config/network.ts @@ -0,0 +1,101 @@ +import { MainnetConfig, SepoliaConfig } from "@zama-fhe/sdk"; +import { mainnet, sepolia, type Chain } from "viem/chains"; + +export type NetworkProfileName = "sepolia" | "mainnet"; +export type ZamaSupportLevel = "SUPPORTED" | "EXPERIMENTAL"; +type SdkPresetConfig = typeof SepoliaConfig | typeof MainnetConfig; + +export interface RuntimeNetworkConfig { + profile: NetworkProfileName; + profileLabel: string; + zamaSupport: ZamaSupportLevel; + chain: Chain; + chainId: number; + rpcUrl: string; + relayerUrl: string; + apiKey: string; + sdkConfig: SdkPresetConfig; +} + +type NetworkPreset = { + profileLabel: string; + chain: Chain; + sdkConfig: SdkPresetConfig; + zamaSupport: ZamaSupportLevel; + defaultRpcUrl: string; + defaultRelayerUrl: string; +}; + +const NETWORK_PRESETS: Record = { + sepolia: { + profileLabel: "Ethereum Sepolia", + chain: sepolia, + sdkConfig: SepoliaConfig, + zamaSupport: "SUPPORTED", + defaultRpcUrl: "https://ethereum-sepolia-rpc.publicnode.com", + defaultRelayerUrl: "https://relayer.testnet.zama.org/v2", + }, + mainnet: { + profileLabel: "Ethereum Mainnet", + chain: mainnet, + sdkConfig: MainnetConfig, + zamaSupport: "EXPERIMENTAL", + defaultRpcUrl: "https://ethereum-rpc.publicnode.com", + defaultRelayerUrl: "", + }, +}; + +function nonEmpty(value: string | undefined): string { + return (value ?? "").trim(); +} + +export function parseNetworkProfile(raw: string | undefined): NetworkProfileName { + const normalized = nonEmpty(raw).toLowerCase(); + if (!normalized) return "sepolia"; + if (normalized === "sepolia") return "sepolia"; + if (normalized === "mainnet") return "mainnet"; + throw new Error(`Invalid NETWORK_PROFILE="${raw}". Expected one of: sepolia, mainnet.`); +} + +export function buildNetworkConfig(env: NodeJS.ProcessEnv = process.env): RuntimeNetworkConfig { + const profile = parseNetworkProfile(env.NETWORK_PROFILE); + const preset = NETWORK_PRESETS[profile]; + + const rpcUrl = nonEmpty(env.RPC_URL) || preset.defaultRpcUrl; + if (!rpcUrl) { + throw new Error(`RPC_URL is required for NETWORK_PROFILE=${profile}.`); + } + + const relayerUrl = nonEmpty(env.RELAYER_URL) || preset.defaultRelayerUrl; + if (!relayerUrl) { + throw new Error( + `RELAYER_URL is required for NETWORK_PROFILE=${profile}.` + + " Provide your relayer endpoint in .env.", + ); + } + + return { + profile, + profileLabel: preset.profileLabel, + zamaSupport: preset.zamaSupport, + chain: preset.chain, + chainId: preset.chain.id, + rpcUrl, + relayerUrl, + apiKey: nonEmpty(env.RELAYER_API_KEY), + sdkConfig: preset.sdkConfig, + }; +} + +/** + * Runtime network configuration. + * + * `profile` — selected logical profile (`sepolia` or `mainnet`) + * `chain` — viem chain object + * `rpcUrl` — JSON-RPC endpoint + * `relayerUrl` — Zama relayer base URL + * `apiKey` — x-api-key forwarded to the relayer + * `sdkConfig` — Zama SDK chain config preset + * `zamaSupport` — harness support level for the selected profile + */ +export const networkConfig = buildNetworkConfig(); diff --git a/examples/compatibility-harness/src/config/runtime.ts b/examples/compatibility-harness/src/config/runtime.ts new file mode 100644 index 000000000..603f65132 --- /dev/null +++ b/examples/compatibility-harness/src/config/runtime.ts @@ -0,0 +1,12 @@ +function normalizeBoolean(value: string | undefined): string { + return (value ?? "").trim().toLowerCase(); +} + +export function isMockModeEnabled(): boolean { + const normalized = normalizeBoolean(process.env.HARNESS_MOCK_MODE); + return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; +} + +export function mockModeNote(): string { + return "HARNESS_MOCK_MODE is enabled. Network, relayer, and registry dependent checks are marked UNTESTED."; +} diff --git a/examples/compatibility-harness/src/harness/adapter.ts b/examples/compatibility-harness/src/harness/adapter.ts new file mode 100644 index 000000000..443ca97ce --- /dev/null +++ b/examples/compatibility-harness/src/harness/adapter.ts @@ -0,0 +1,134 @@ +import { getAddress, type Address, type Hex } from "viem"; +import { + ZamaSDK, + MemoryStorage, + isOperatorContract, + setOperatorContract, + type GenericSigner, +} from "@zama-fhe/sdk"; +import { RelayerNode } from "@zama-fhe/sdk/node"; +import * as adapterModule from "../adapter/index.js"; +import { networkConfig } from "../config/network.js"; +import { publicClient } from "../utils/rpc.js"; +import { loadAdapterModule } from "../adapter/load.js"; + +const loaded = loadAdapterModule(adapterModule); + +export const adapter = loaded.adapter; +export const adapterSource = loaded.source; +export const adapterDeclaredCapabilities = loaded.declaredCapabilities; +export const adapterObservedStructuralCapabilities = loaded.observedStructuralCapabilities; +export const adapterObservedRuntimeCapabilities = loaded.observedRuntimeCapabilities; +export const adapterObservedCapabilities = loaded.observedCapabilities; + +let initPromise: Promise | null = null; + +export async function initializeAdapter(): Promise { + if (!initPromise) { + initPromise = loaded.init(); + } + await initPromise; +} + +export async function getAdapterAddress(): Promise
{ + return getAddress(await adapter.getAddress()) as Address; +} + +export function buildGenericSigner(): GenericSigner { + return { + getChainId: () => publicClient.getChainId(), + getAddress: getAdapterAddress, + async signTypedData(typedData) { + if (!adapter.signTypedData) { + throw new Error("Adapter does not support EIP-712 signing"); + } + const signature = await adapter.signTypedData({ + domain: typedData.domain, + types: typedData.types, + primaryType: typedData.primaryType, + message: typedData.message, + }); + return signature as Hex; + }, + async writeContract(config) { + if (!adapter.writeContract) { + throw new Error("Adapter does not support contract execution"); + } + return adapter.writeContract({ + address: config.address, + abi: config.abi, + functionName: config.functionName, + args: config.args as readonly unknown[] | undefined, + value: config.value, + gas: config.gas, + } as never); + }, + async readContract(config: never) { + if (adapter.readContract) { + return adapter.readContract(config) as never; + } + return publicClient.readContract(config) as never; + }, + async waitForTransactionReceipt(hash) { + if (adapter.waitForTransactionReceipt) { + return adapter.waitForTransactionReceipt(hash) as never; + } + return publicClient.waitForTransactionReceipt({ hash }) as never; + }, + async getBlockTimestamp() { + const block = await publicClient.getBlock(); + return block.timestamp; + }, + } as GenericSigner; +} + +export function buildSdk(): ZamaSDK { + const authConfig = networkConfig.apiKey + ? { __type: "ApiKeyHeader" as const, value: networkConfig.apiKey } + : undefined; + + const relayer = new RelayerNode({ + getChainId: () => publicClient.getChainId(), + transports: { + [networkConfig.chainId]: { + network: networkConfig.rpcUrl, + relayerUrl: networkConfig.relayerUrl, + ...(authConfig ? { auth: authConfig } : {}), + }, + }, + }); + + return new ZamaSDK({ + relayer, + signer: buildGenericSigner(), + storage: new MemoryStorage(), + sessionStorage: new MemoryStorage(), + }); +} + +export async function discoverTokenAddress(sdk: ZamaSDK): Promise
{ + const { items } = await sdk.registry.listPairs({ page: 1, pageSize: 1 }); + if (items.length === 0) { + throw new Error( + `No token pairs found in the registry for ${networkConfig.profileLabel} (chainId=${networkConfig.chainId}).`, + ); + } + return items[0]!.tokenAddress; +} + +export async function verifyZamaOperatorApproval(tokenAddress: Address, operator: Address) { + const holder = await getAdapterAddress(); + return publicClient.readContract( + isOperatorContract(tokenAddress, holder, operator), + ) as Promise; +} + +export async function executeZamaWriteProbe( + tokenAddress: Address, + operator: Address, +): Promise { + if (!adapter.writeContract) { + throw new Error("Adapter does not support contract execution"); + } + return adapter.writeContract(setOperatorContract(tokenAddress, operator)); +} diff --git a/examples/compatibility-harness/src/harness/diagnostics.ts b/examples/compatibility-harness/src/harness/diagnostics.ts new file mode 100644 index 000000000..f0be96618 --- /dev/null +++ b/examples/compatibility-harness/src/harness/diagnostics.ts @@ -0,0 +1,139 @@ +import type { DiagnosticCode, RootCauseCategory, ValidationStatus } from "../adapter/types.js"; + +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function normalize(message: string): string { + return message.toLowerCase(); +} + +function containsAny(message: string, patterns: string[]): boolean { + return patterns.some((pattern) => message.includes(pattern)); +} + +export function isFundingIssue(message: string): boolean { + const lower = normalize(message); + return containsAny(lower, [ + "insufficient funds", + "insufficient balance", + "exceeds balance", + "intrinsic gas too low", + ]); +} + +export function isEnvironmentIssue(message: string): boolean { + const lower = normalize(message); + return ( + containsAny(lower, [ + "private_key", + "not set", + "copy .env.example", + "wallet locator", + "turnkey_", + "crossmint_", + "openfort_", + "relayer_api_key", + "api key", + "invalid private key", + "invalid key", + "invalid signature format", + ]) || /[a-z0-9_]+\s+is not set/.test(lower) + ); +} + +export function isRpcIssue(message: string): boolean { + const lower = normalize(message); + return containsAny(lower, [ + "rpc", + "nonce too low", + "replacement transaction underpriced", + "gas price", + "http request failed", + "network", + "fetch failed", + "socket", + "timeout", + "timed out", + "dns", + "429", + "rate limit", + "econnrefused", + "enotfound", + "gateway timeout", + "service unavailable", + ]); +} + +export function isRelayerIssue(message: string): boolean { + const lower = normalize(message); + return containsAny(lower, [ + "relayer", + "keypair", + "credential", + "kms signer", + "fhevm key", + "authorization header", + ]); +} + +export function isRegistryIssue(message: string): boolean { + const lower = normalize(message); + return containsAny(lower, ["registry", "token pairs", "no token pairs", "pair not found"]); +} + +export function classifyInfrastructureIssue(message: string): { + status: ValidationStatus; + rootCauseCategory: RootCauseCategory; + errorCode: DiagnosticCode; +} { + if (isFundingIssue(message)) { + return { + status: "BLOCKED", + rootCauseCategory: "ENVIRONMENT", + errorCode: "ENV_INSUFFICIENT_FUNDS", + }; + } + if (isEnvironmentIssue(message)) { + const lower = normalize(message); + const invalidPattern = + lower.includes("invalid") || lower.includes("malformed") || lower.includes("mismatch"); + return { + status: "BLOCKED", + rootCauseCategory: "ENVIRONMENT", + errorCode: invalidPattern ? "ENV_INVALID_CONFIG" : "ENV_MISSING_CONFIG", + }; + } + if (isRegistryIssue(message)) { + return { + status: "BLOCKED", + rootCauseCategory: "REGISTRY", + errorCode: normalize(message).includes("no token pairs") + ? "REGISTRY_EMPTY" + : "REGISTRY_UNAVAILABLE", + }; + } + if (isRelayerIssue(message)) { + return { + status: "INCONCLUSIVE", + rootCauseCategory: "RELAYER", + errorCode: "RELAYER_UNAVAILABLE", + }; + } + if (isRpcIssue(message)) { + const lower = normalize(message); + return { + status: "INCONCLUSIVE", + rootCauseCategory: "RPC", + errorCode: + lower.includes("429") || lower.includes("rate limit") + ? "RPC_RATE_LIMIT" + : "RPC_CONNECTIVITY", + }; + } + return { + status: "INCONCLUSIVE", + rootCauseCategory: "HARNESS", + errorCode: "HARNESS_UNKNOWN", + }; +} diff --git a/examples/compatibility-harness/src/harness/negative-paths.ts b/examples/compatibility-harness/src/harness/negative-paths.ts new file mode 100644 index 000000000..3f45ea2ec --- /dev/null +++ b/examples/compatibility-harness/src/harness/negative-paths.ts @@ -0,0 +1,50 @@ +import type { DiagnosticCode, RootCauseCategory, ValidationStatus } from "../adapter/types.js"; +import { classifyInfrastructureIssue } from "./diagnostics.js"; + +export interface NegativePathOutcome { + status: ValidationStatus; + rootCauseCategory: RootCauseCategory; + errorCode?: DiagnosticCode; + infrastructure: boolean; +} + +function classifyAsCompatibilityOrInfrastructure( + message: string, + compatibilityRootCause: Extract, +): NegativePathOutcome { + const diagnostic = classifyInfrastructureIssue(message); + const infrastructure = diagnostic.rootCauseCategory !== "HARNESS"; + if (infrastructure) { + return { + status: diagnostic.status, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + infrastructure: true, + }; + } + return { + status: "FAIL", + rootCauseCategory: compatibilityRootCause, + infrastructure: false, + }; +} + +export function classifyEip712SigningFailure(message: string): NegativePathOutcome { + return classifyAsCompatibilityOrInfrastructure(message, "ADAPTER"); +} + +export function classifyZamaAuthorizationFailure(message: string): NegativePathOutcome { + return classifyAsCompatibilityOrInfrastructure(message, "SIGNER"); +} + +export function classifyZamaWriteSubmissionFailure(message: string): NegativePathOutcome { + return classifyAsCompatibilityOrInfrastructure(message, "ADAPTER"); +} + +export function classifyRecoverabilityFailure(): NegativePathOutcome { + return { + status: "FAIL", + rootCauseCategory: "SIGNER", + infrastructure: false, + }; +} diff --git a/examples/compatibility-harness/src/harness/recommendations.ts b/examples/compatibility-harness/src/harness/recommendations.ts new file mode 100644 index 000000000..0c5b09fff --- /dev/null +++ b/examples/compatibility-harness/src/harness/recommendations.ts @@ -0,0 +1,93 @@ +import type { DiagnosticCode, RootCauseCategory, ValidationStatus } from "../adapter/types.js"; + +interface RecommendationDescriptor { + text: string; + nextCommand?: string; +} + +const ERROR_CODE_RECOMMENDATIONS: Record = { + ENV_MISSING_CONFIG: { + text: "Set the required environment variables and credentials for your adapter.", + nextCommand: "npm run doctor", + }, + ENV_INVALID_CONFIG: { + text: "Fix invalid environment values (keys, addresses, URLs, or IDs).", + nextCommand: "npm run doctor", + }, + ENV_INSUFFICIENT_FUNDS: { + text: "Fund the adapter wallet with enough native token to cover gas.", + nextCommand: "npm run validate", + }, + RPC_CONNECTIVITY: { + text: "Check RPC_URL/network reachability and retry once connectivity is stable.", + nextCommand: "npm run doctor", + }, + RPC_RATE_LIMIT: { + text: "Use a less constrained RPC endpoint or reduce request concurrency.", + nextCommand: "npm run doctor", + }, + RELAYER_UNAVAILABLE: { + text: "Verify relayer URL/API key and relayer service health.", + nextCommand: "npm run doctor", + }, + REGISTRY_EMPTY: { + text: "No compatible token pair was found for the selected network.", + nextCommand: "npm run validate", + }, + REGISTRY_UNAVAILABLE: { + text: "Ensure registry services are reachable on the selected network.", + nextCommand: "npm run validate", + }, + HARNESS_UNKNOWN: { + text: "Inspect the underlying error and harness logs to isolate the failure source.", + nextCommand: "npm test", + }, +}; + +const ROOT_CAUSE_RECOMMENDATIONS: Partial> = { + ENVIRONMENT: { + text: "Fix local configuration and credentials before rerunning compatibility checks.", + nextCommand: "npm run doctor", + }, + RPC: { + text: "Validate RPC endpoint health and network reachability.", + nextCommand: "npm run doctor", + }, + RELAYER: { + text: "Validate relayer endpoint health and authentication settings.", + nextCommand: "npm run doctor", + }, + REGISTRY: { + text: "Verify token registry availability and network selection.", + nextCommand: "npm run validate", + }, + HARNESS: { + text: "Investigate harness/runtime logs to determine the blocking condition.", + nextCommand: "npm test", + }, +}; + +function withNextCommand(descriptor: RecommendationDescriptor): string { + if (!descriptor.nextCommand) return descriptor.text; + return `${descriptor.text} Next: \`${descriptor.nextCommand}\`.`; +} + +function isActionablyBlockedStatus(status: ValidationStatus): boolean { + return status === "BLOCKED" || status === "INCONCLUSIVE"; +} + +export function recommendationForDiagnostic(input: { + status: ValidationStatus; + errorCode?: DiagnosticCode; + rootCauseCategory?: RootCauseCategory; +}): string | undefined { + if (!isActionablyBlockedStatus(input.status)) return undefined; + if (input.errorCode) { + return withNextCommand(ERROR_CODE_RECOMMENDATIONS[input.errorCode]); + } + if (input.rootCauseCategory) { + const fallback = ROOT_CAUSE_RECOMMENDATIONS[input.rootCauseCategory]; + if (fallback) return withNextCommand(fallback); + } + return undefined; +} diff --git a/examples/compatibility-harness/src/report/check-registry.ts b/examples/compatibility-harness/src/report/check-registry.ts new file mode 100644 index 000000000..6d63003d9 --- /dev/null +++ b/examples/compatibility-harness/src/report/check-registry.ts @@ -0,0 +1,157 @@ +import type { ReportSection } from "./schema.js"; + +export type CanonicalCheckId = + | "ADAPTER_INITIALIZATION" + | "ADDRESS_RESOLUTION" + | "EIP712_SIGNING" + | "EIP712_RECOVERABILITY" + | "ERC1271_VERIFICATION" + | "RAW_TRANSACTION_EXECUTION" + | "ADAPTER_CONTRACT_READ" + | "ZAMA_AUTHORIZATION_FLOW" + | "ZAMA_WRITE_FLOW" + | "ENVIRONMENT_CONFIGURATION" + | "RPC_CONNECTIVITY" + | "RELAYER_REACHABILITY" + | "REGISTRY_TOKEN_DISCOVERY"; + +export interface CanonicalCheckDefinition { + id: CanonicalCheckId; + name: string; + section: ReportSection; + dependencies: CanonicalCheckId[]; + synthetic?: boolean; +} + +export const CHECK_REGISTRY: readonly CanonicalCheckDefinition[] = [ + { + id: "ADAPTER_INITIALIZATION", + name: "Adapter Initialization", + section: "adapter", + dependencies: [], + }, + { + id: "ADDRESS_RESOLUTION", + name: "Address Resolution", + section: "adapter", + dependencies: ["ADAPTER_INITIALIZATION"], + }, + { + id: "EIP712_SIGNING", + name: "EIP-712 Signing", + section: "ethereum", + dependencies: ["ADAPTER_INITIALIZATION"], + }, + { + id: "EIP712_RECOVERABILITY", + name: "EIP-712 Recoverability", + section: "ethereum", + dependencies: ["EIP712_SIGNING"], + }, + { + id: "ERC1271_VERIFICATION", + name: "ERC-1271 Verification", + section: "ethereum", + dependencies: ["EIP712_SIGNING"], + }, + { + id: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + dependencies: ["ADAPTER_INITIALIZATION", "ADDRESS_RESOLUTION"], + }, + { + id: "ADAPTER_CONTRACT_READ", + name: "Adapter Contract Read", + section: "execution", + dependencies: ["ADAPTER_INITIALIZATION"], + }, + { + id: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + dependencies: ["ADAPTER_INITIALIZATION", "EIP712_SIGNING"], + }, + { + id: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + dependencies: ["ADAPTER_INITIALIZATION"], + }, + { + id: "ENVIRONMENT_CONFIGURATION", + name: "Environment Configuration", + section: "environment", + dependencies: [], + synthetic: true, + }, + { + id: "RPC_CONNECTIVITY", + name: "RPC Connectivity", + section: "environment", + dependencies: [], + synthetic: true, + }, + { + id: "RELAYER_REACHABILITY", + name: "Relayer Reachability", + section: "environment", + dependencies: [], + synthetic: true, + }, + { + id: "REGISTRY_TOKEN_DISCOVERY", + name: "Registry / Token Discovery", + section: "environment", + dependencies: [], + synthetic: true, + }, +] as const; + +const CHECK_BY_ID = new Map( + CHECK_REGISTRY.map((check) => [check.id, check]), +); + +const CHECK_BY_NAME = new Map( + CHECK_REGISTRY.map((check) => [check.name, check]), +); + +export function getCanonicalCheckById(id: CanonicalCheckId): CanonicalCheckDefinition { + return CHECK_BY_ID.get(id)!; +} + +export function getCanonicalCheckByName(name: string): CanonicalCheckDefinition | undefined { + return CHECK_BY_NAME.get(name); +} + +export function isCanonicalCheckId(value: string): value is CanonicalCheckId { + return CHECK_BY_ID.has(value as CanonicalCheckId); +} + +export function checkOrder(id: CanonicalCheckId): number { + const index = CHECK_REGISTRY.findIndex((check) => check.id === id); + return index === -1 ? Number.MAX_SAFE_INTEGER : index; +} + +export interface CheckLike { + checkId: CanonicalCheckId; + name: string; + section: ReportSection; +} + +export function assertCanonicalCheck(check: CheckLike): void { + const expected = CHECK_BY_ID.get(check.checkId); + if (!expected) { + throw new Error(`Unknown checkId "${check.checkId}" in recorded result.`); + } + if (check.name !== expected.name) { + throw new Error( + `Invalid check name for ${check.checkId}: expected "${expected.name}", got "${check.name}".`, + ); + } + if (check.section !== expected.section) { + throw new Error( + `Invalid check section for ${check.checkId}: expected "${expected.section}", got "${check.section}".`, + ); + } +} diff --git a/examples/compatibility-harness/src/report/global-setup.ts b/examples/compatibility-harness/src/report/global-setup.ts new file mode 100644 index 000000000..60d5ddaa3 --- /dev/null +++ b/examples/compatibility-harness/src/report/global-setup.ts @@ -0,0 +1,16 @@ +/** + * Vitest global setup — runs once per test run in the main process (not in workers). + * - setup() clears stale results and profile from a previous run + * - teardown() prints the final compatibility report after all tests finish + */ +import { clearResults, clearProfile, clearZamaWriteObservation, printReport } from "./reporter.js"; + +export function setup(): void { + clearResults(); + clearProfile(); + clearZamaWriteObservation(); +} + +export function teardown(): void { + printReport(); +} diff --git a/examples/compatibility-harness/src/report/parse.ts b/examples/compatibility-harness/src/report/parse.ts new file mode 100644 index 000000000..8270e88bb --- /dev/null +++ b/examples/compatibility-harness/src/report/parse.ts @@ -0,0 +1,189 @@ +import { + REPORT_KIND, + REPORT_SCHEMA_VERSION, + SUPPORTED_REPORT_SCHEMA_VERSIONS, + type ReportArtifact, + type ReportSection, +} from "./schema.js"; +import { assertCanonicalCheck, isCanonicalCheckId } from "./check-registry.js"; +import { assertClaimConsistency } from "../verdict/consistency.js"; +import type { ValidationStatus } from "../adapter/types.js"; + +const VALID_STATUSES = new Set([ + "PASS", + "FAIL", + "UNTESTED", + "UNSUPPORTED", + "BLOCKED", + "INCONCLUSIVE", +]); +const VALID_CONFIDENCE = new Set(["HIGH", "MEDIUM", "LOW"]); +const VALID_WRITE_DEPTH = new Set(["FULL", "PARTIAL", "UNTESTED"]); + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function assertStringField( + object: Record, + field: string, + context: string, +): string { + const value = object[field]; + if (typeof value !== "string" || value.trim() === "") { + throw new Error(`Invalid ${context}.${field}: expected non-empty string.`); + } + return value; +} + +function assertCheckArray(value: unknown, context: string): void { + if (!Array.isArray(value)) { + throw new Error(`Invalid ${context}: expected array.`); + } + + for (let i = 0; i < value.length; i += 1) { + const check = asRecord(value[i]); + if (!check) { + throw new Error(`Invalid ${context}[${i}]: expected object.`); + } + const checkId = assertStringField(check, "checkId", `${context}[${i}]`); + if (!isCanonicalCheckId(checkId)) { + throw new Error(`Invalid ${context}[${i}].checkId: unknown id "${checkId}".`); + } + const name = assertStringField(check, "name", `${context}[${i}]`); + const section = assertStringField(check, "section", `${context}[${i}]`); + const status = assertStringField(check, "status", `${context}[${i}]`); + if (!VALID_STATUSES.has(status as ValidationStatus)) { + throw new Error(`Invalid ${context}[${i}].status: unsupported status "${status}".`); + } + assertCanonicalCheck({ + checkId, + name, + section: section as ReportSection, + }); + } +} + +export function parseReportArtifact(raw: string): ReportArtifact { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new Error("Report artifact is not valid JSON."); + } + + const object = asRecord(parsed); + if (!object) { + throw new Error("Report artifact must be a JSON object."); + } + + const kind = assertStringField(object, "kind", "report"); + if (kind !== REPORT_KIND) { + throw new Error(`Unexpected report kind "${kind}". Expected "${REPORT_KIND}".`); + } + + const schemaVersion = assertStringField(object, "schemaVersion", "report"); + if ( + !SUPPORTED_REPORT_SCHEMA_VERSIONS.includes( + schemaVersion as (typeof SUPPORTED_REPORT_SCHEMA_VERSIONS)[number], + ) + ) { + const supported = SUPPORTED_REPORT_SCHEMA_VERSIONS.join(", "); + throw new Error( + `Unsupported schemaVersion "${schemaVersion}". Supported versions: ${supported}.`, + ); + } + + assertStringField(object, "generatedAt", "report"); + assertStringField(object, "runId", "report"); + assertStringField(object, "finalVerdict", "report"); + + const claim = asRecord(object.claim); + if (!claim) { + throw new Error("Invalid report.claim: expected object."); + } + assertStringField(claim, "id", "report.claim"); + assertStringField(claim, "verdictLabel", "report.claim"); + + const rationale = claim.rationale; + if ( + !Array.isArray(rationale) || + rationale.length === 0 || + rationale.some((v) => typeof v !== "string") + ) { + throw new Error("Invalid report.claim.rationale: expected non-empty string array."); + } + + const evidence = asRecord(claim.evidence); + if (!evidence) { + throw new Error("Invalid report.claim.evidence: expected object."); + } + if (schemaVersion === REPORT_SCHEMA_VERSION) { + const confidence = assertStringField(claim, "confidence", "report.claim"); + if (!VALID_CONFIDENCE.has(confidence)) { + throw new Error(`Invalid report.claim.confidence: unsupported value "${confidence}".`); + } + } + const evidenceDetails = claim.evidenceDetails; + if (evidenceDetails !== undefined) { + if (!Array.isArray(evidenceDetails)) { + throw new Error("Invalid report.claim.evidenceDetails: expected array when provided."); + } + for (let i = 0; i < evidenceDetails.length; i += 1) { + const detail = asRecord(evidenceDetails[i]); + if (!detail) { + throw new Error(`Invalid report.claim.evidenceDetails[${i}]: expected object.`); + } + assertStringField(detail, "check", `report.claim.evidenceDetails[${i}]`); + assertStringField(detail, "checkId", `report.claim.evidenceDetails[${i}]`); + assertStringField(detail, "status", `report.claim.evidenceDetails[${i}]`); + assertStringField(detail, "reasonCategory", `report.claim.evidenceDetails[${i}]`); + } + } + + const checks = asRecord(object.checks); + if (!checks) { + throw new Error("Invalid report.checks: expected object."); + } + for (const required of ["recorded", "environmentSummary", "all"]) { + assertCheckArray(checks[required], `report.checks.${required}`); + } + + const sections = asRecord(object.sections); + if (!sections) { + throw new Error("Invalid report.sections: expected object."); + } + for (const required of ["adapter", "ethereum", "execution", "zama", "environment"]) { + assertCheckArray(sections[required], `report.sections.${required}`); + } + + const infrastructure = asRecord(object.infrastructure); + if (!infrastructure) { + throw new Error("Invalid report.infrastructure: expected object."); + } + const blockers = infrastructure.blockers; + if (!asRecord(blockers)) { + throw new Error("Invalid report.infrastructure.blockers: expected object."); + } + + if (schemaVersion === REPORT_SCHEMA_VERSION) { + const zama = asRecord(object.zama); + if (!zama) { + throw new Error("Invalid report.zama: expected object for schema 1.3.0."); + } + const writeValidationDepth = assertStringField(zama, "writeValidationDepth", "report.zama"); + if (!VALID_WRITE_DEPTH.has(writeValidationDepth)) { + throw new Error( + `Invalid report.zama.writeValidationDepth: unsupported value "${writeValidationDepth}".`, + ); + } + } + + assertClaimConsistency( + claim as unknown as ReportArtifact["claim"], + checks.all as unknown as ReportArtifact["checks"]["all"], + ); + + return object as unknown as ReportArtifact; +} diff --git a/examples/compatibility-harness/src/report/reporter.ts b/examples/compatibility-harness/src/report/reporter.ts new file mode 100644 index 000000000..6134d23c3 --- /dev/null +++ b/examples/compatibility-harness/src/report/reporter.ts @@ -0,0 +1,631 @@ +import { + appendFileSync, + writeFileSync, + readFileSync, + existsSync, + unlinkSync, + mkdirSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import type { + DiagnosticCode, + ObservedAdapterProfile, + RootCauseCategory, + ValidationStatus, + CapabilityState, + AdapterCapabilities, +} from "../adapter/types.js"; +import { emptyCapabilities } from "../adapter/types.js"; +import { mergeCapabilityPatch, resolveFinalCapabilities } from "../adapter/capability-evidence.js"; +import { inferRuntimeCapabilityPatchFromCheck } from "../adapter/runtime-observation.js"; +import { detectCapabilityContradictions } from "../adapter/contradictions.js"; +import { recommendationForDiagnostic } from "../harness/recommendations.js"; +import { + REPORT_KIND, + REPORT_SCHEMA_VERSION, + type InfraRootCause, + type ReportArtifact, + type ReportSection, +} from "./schema.js"; +import { + assertCanonicalCheck, + checkOrder, + getCanonicalCheckById, + type CanonicalCheckId, +} from "./check-registry.js"; +import { resolveClaimFromResults } from "../verdict/resolve.js"; +import { assertClaimConsistency } from "../verdict/consistency.js"; +import { resolveClaimConfidence } from "../verdict/confidence.js"; + +export type TestStatus = ValidationStatus; +export type TestSection = ReportSection; + +export interface TestResult { + checkId: CanonicalCheckId; + name: string; + section: TestSection; + status: TestStatus; + summary?: string; + reason?: string; + rootCauseCategory?: RootCauseCategory; + errorCode?: DiagnosticCode; + likelyCause?: string; + recommendation?: string; +} + +export type AdapterProfile = ObservedAdapterProfile; +export type WriteValidationDepth = "FULL" | "PARTIAL" | "UNTESTED"; + +interface ZamaWriteObservation { + submissionAttempted: boolean; + submissionSucceeded: boolean; + receiptObserved: boolean; + stateVerified: boolean; +} + +// Temp files shared across all vitest worker processes. +// Cleared by globalSetup at the start of each run. +const RUN_ID = (process.env.ZAMA_HARNESS_RUN_ID ?? "default").replace(/[^a-zA-Z0-9._-]/g, "_"); +const RESULTS_FILE = join(tmpdir(), `zama-harness-results-${RUN_ID}.jsonl`); +const PROFILE_FILE = join(tmpdir(), `zama-harness-profile-${RUN_ID}.json`); +const ZAMA_WRITE_OBSERVATION_FILE = join(tmpdir(), `zama-harness-zama-write-${RUN_ID}.json`); + +const INFRA_ROOT_CAUSES = new Set(["ENVIRONMENT", "RPC", "RELAYER", "REGISTRY"]); + +function isInfraRootCause(category: RootCauseCategory | undefined): category is InfraRootCause { + return category !== undefined && INFRA_ROOT_CAUSES.has(category as InfraRootCause); +} + +// ── Results ────────────────────────────────────────────────────────────────── + +/** Append a test result to the shared results file. */ +export function record(result: TestResult): void { + const recommendation = recommendationForDiagnostic({ + status: result.status, + errorCode: result.errorCode, + rootCauseCategory: result.rootCauseCategory, + }); + const normalized: TestResult = recommendation ? { ...result, recommendation } : result; + assertCanonicalCheck(normalized); + appendFileSync(RESULTS_FILE, `${JSON.stringify(normalized)}\n`); +} + +export function recordWithRuntimeObservation( + result: TestResult, + runtimeOverride: Partial = {}, +): void { + record(result); + const inferred = inferRuntimeCapabilityPatchFromCheck({ + checkId: result.checkId, + status: result.status, + }); + const patch = { + ...inferred, + ...runtimeOverride, + }; + if (Object.keys(patch).length === 0) return; + mergeProfile({ + observedRuntimeCapabilities: patch, + }); +} + +/** Read all recorded results. */ +export function readResults(): TestResult[] { + if (!existsSync(RESULTS_FILE)) return []; + const raw = readFileSync(RESULTS_FILE, "utf-8") + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + return raw.map((line) => { + const parsed = JSON.parse(line) as TestResult; + assertCanonicalCheck(parsed); + return parsed; + }); +} + +/** Delete the results file (called by globalSetup at the start of each run). */ +export function clearResults(): void { + if (existsSync(RESULTS_FILE)) unlinkSync(RESULTS_FILE); +} + +// ── Signer profile ─────────────────────────────────────────────────────────── + +/** Record the signer profile (detected once, at the start of the run). */ +export function recordProfile(profile: AdapterProfile): void { + writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2)); +} + +function profileStructuralCapabilities(profile: AdapterProfile): AdapterCapabilities { + return profile.observedStructuralCapabilities ?? profile.observedCapabilities; +} + +function profileRuntimeCapabilities(profile: AdapterProfile): AdapterCapabilities { + return profile.observedRuntimeCapabilities ?? emptyCapabilities(); +} + +/** Merge a partial profile update into the recorded adapter profile. */ +export function mergeProfile( + patch: Partial< + Omit< + AdapterProfile, + | "declaredCapabilities" + | "observedStructuralCapabilities" + | "observedRuntimeCapabilities" + | "observedCapabilities" + > + > & { + declaredCapabilities?: Partial; + observedStructuralCapabilities?: Partial; + observedRuntimeCapabilities?: Partial; + // Backward-compatible alias for runtime observations. + observedCapabilities?: Partial; + }, +): void { + const existing = readProfile(); + const runtimePatch = { + ...patch.observedRuntimeCapabilities, + ...patch.observedCapabilities, + }; + if (!existing) { + const hasCapabilityPatches = + !!patch.declaredCapabilities || + !!patch.observedStructuralCapabilities || + Object.keys(runtimePatch).length > 0; + const hasRequiredMetadata = + !!patch.name && + !!patch.source && + !!patch.declaredArchitecture && + !!patch.detectedArchitecture; + + if (!hasRequiredMetadata && !hasCapabilityPatches) { + return; + } + + const declaredCapabilities = mergeCapabilityPatch({ + base: emptyCapabilities(), + patch: patch.declaredCapabilities, + }); + const observedStructuralCapabilities = mergeCapabilityPatch({ + base: emptyCapabilities(), + patch: patch.observedStructuralCapabilities, + }); + const observedRuntimeCapabilities = mergeCapabilityPatch({ + base: emptyCapabilities(), + patch: runtimePatch, + }); + const observedCapabilities = resolveFinalCapabilities({ + structural: observedStructuralCapabilities, + runtime: observedRuntimeCapabilities, + }); + + recordProfile({ + name: patch.name ?? "(pending adapter profile)", + source: patch.source ?? "adapter", + declaredArchitecture: patch.declaredArchitecture ?? "UNKNOWN", + detectedArchitecture: patch.detectedArchitecture ?? patch.declaredArchitecture ?? "UNKNOWN", + verificationModel: patch.verificationModel ?? "UNKNOWN", + address: patch.address ?? "(unresolved)", + declaredCapabilities, + observedStructuralCapabilities, + observedRuntimeCapabilities, + observedCapabilities, + contradictions: detectCapabilityContradictions(declaredCapabilities, observedCapabilities), + initializationStatus: patch.initializationStatus ?? "UNTESTED", + }); + return; + } + const declaredCapabilities = mergeCapabilityPatch({ + base: existing.declaredCapabilities, + patch: patch.declaredCapabilities, + }); + const observedStructuralCapabilities = mergeCapabilityPatch({ + base: profileStructuralCapabilities(existing), + patch: patch.observedStructuralCapabilities, + }); + const observedRuntimeCapabilities = mergeCapabilityPatch({ + base: profileRuntimeCapabilities(existing), + patch: runtimePatch, + }); + const observedCapabilities = resolveFinalCapabilities({ + structural: observedStructuralCapabilities, + runtime: observedRuntimeCapabilities, + }); + + writeFileSync( + PROFILE_FILE, + JSON.stringify( + { + ...existing, + ...patch, + declaredCapabilities, + observedStructuralCapabilities, + observedRuntimeCapabilities, + observedCapabilities, + contradictions: detectCapabilityContradictions(declaredCapabilities, observedCapabilities), + }, + null, + 2, + ), + ); +} + +/** Read the recorded signer profile. */ +export function readProfile(): AdapterProfile | null { + if (!existsSync(PROFILE_FILE)) return null; + return JSON.parse(readFileSync(PROFILE_FILE, "utf-8")) as AdapterProfile; +} + +/** Delete the profile file (called by globalSetup at the start of each run). */ +export function clearProfile(): void { + if (existsSync(PROFILE_FILE)) unlinkSync(PROFILE_FILE); +} + +export function recordZamaWriteObservation( + patch: Partial, +): ZamaWriteObservation { + const existing = readZamaWriteObservation() ?? { + submissionAttempted: false, + submissionSucceeded: false, + receiptObserved: false, + stateVerified: false, + }; + const next = { + ...existing, + ...patch, + }; + writeFileSync(ZAMA_WRITE_OBSERVATION_FILE, JSON.stringify(next, null, 2)); + return next; +} + +export function readZamaWriteObservation(): ZamaWriteObservation | null { + if (!existsSync(ZAMA_WRITE_OBSERVATION_FILE)) return null; + return JSON.parse(readFileSync(ZAMA_WRITE_OBSERVATION_FILE, "utf-8")) as ZamaWriteObservation; +} + +export function clearZamaWriteObservation(): void { + if (existsSync(ZAMA_WRITE_OBSERVATION_FILE)) unlinkSync(ZAMA_WRITE_OBSERVATION_FILE); +} + +// ── Report ─────────────────────────────────────────────────────────────────── + +const W = 56; +const FULL = "━".repeat(W); +const SUB = "─".repeat(W); + +function icon(status: TestStatus): string { + switch (status) { + case "PASS": + return "✓"; + case "FAIL": + return "✗"; + case "BLOCKED": + return "!"; + case "INCONCLUSIVE": + return "?"; + case "UNSUPPORTED": + return "–"; + case "UNTESTED": + return "·"; + } +} + +function renderCapability(state: CapabilityState): string { + switch (state) { + case "SUPPORTED": + return "supported"; + case "UNSUPPORTED": + return "unsupported"; + case "UNKNOWN": + return "unknown"; + } +} + +function sectionCounts(results: TestResult[]): Record { + return { + PASS: results.filter((r) => r.status === "PASS").length, + FAIL: results.filter((r) => r.status === "FAIL").length, + UNSUPPORTED: results.filter((r) => r.status === "UNSUPPORTED").length, + UNTESTED: results.filter((r) => r.status === "UNTESTED").length, + BLOCKED: results.filter((r) => r.status === "BLOCKED").length, + INCONCLUSIVE: results.filter((r) => r.status === "INCONCLUSIVE").length, + }; +} + +function testOrder(checkId: CanonicalCheckId): number { + return checkOrder(checkId); +} + +function summarizeBlockers(results: TestResult[]): Partial> { + return results + .filter((r) => (r.status === "BLOCKED" || r.status === "INCONCLUSIVE") && r.rootCauseCategory) + .filter((r): r is TestResult & { rootCauseCategory: InfraRootCause } => { + return isInfraRootCause(r.rootCauseCategory); + }) + .reduce>>((acc, result) => { + const key = result.rootCauseCategory; + acc[key] = (acc[key] ?? 0) + 1; + return acc; + }, {}); +} + +function summarizeEnvironmentSection(results: TestResult[]): TestResult[] { + const grouped = new Map(); + for (const result of results) { + if (result.section === "environment") continue; + if (!isInfraRootCause(result.rootCauseCategory)) continue; + const category = result.rootCauseCategory; + const bucket = grouped.get(category); + if (bucket) { + bucket.push(result); + continue; + } + grouped.set(category, [result]); + } + + const sections: TestResult[] = []; + const categoryCheckId: Record = { + ENVIRONMENT: "ENVIRONMENT_CONFIGURATION", + RPC: "RPC_CONNECTIVITY", + RELAYER: "RELAYER_REACHABILITY", + REGISTRY: "REGISTRY_TOKEN_DISCOVERY", + }; + + for (const [category, impacted] of grouped.entries()) { + const checkId = categoryCheckId[category]; + const check = getCanonicalCheckById(checkId); + const status: TestStatus = impacted.some((r) => r.status === "BLOCKED") + ? "BLOCKED" + : impacted.some((r) => r.status === "INCONCLUSIVE") + ? "INCONCLUSIVE" + : "UNTESTED"; + const checks = impacted.map((r) => r.name).join(", "); + sections.push({ + checkId, + name: check.name, + section: "environment", + status, + summary: `${impacted.length} check(s) impacted by ${category.toLowerCase()}`, + reason: checks, + rootCauseCategory: category, + recommendation: + recommendationForDiagnostic({ + status, + rootCauseCategory: category, + }) ?? "Investigate environment dependencies and retry.", + }); + } + + return sections; +} + +export function deriveWriteValidationDepth(input: { + zamaWriteStatus: TestStatus | null; + observation: ZamaWriteObservation | null; +}): WriteValidationDepth { + const { zamaWriteStatus, observation } = input; + if (observation) { + if (observation.submissionSucceeded && observation.stateVerified) { + return "FULL"; + } + if ( + observation.submissionAttempted || + observation.submissionSucceeded || + observation.receiptObserved + ) { + return "PARTIAL"; + } + } + + if (zamaWriteStatus === "PASS") return "FULL"; + return "UNTESTED"; +} + +function exportJsonReport(payload: { + profile: AdapterProfile | null; + results: TestResult[]; + environmentSummary: TestResult[]; + verdict: ReturnType; + blockers: Partial>; + writeValidationDepth: WriteValidationDepth; +}): void { + const outputPath = (process.env.REPORT_JSON_PATH ?? "").trim(); + if (!outputPath) return; + const allResults = [...payload.results, ...payload.environmentSummary]; + const artifact: ReportArtifact = { + kind: REPORT_KIND, + schemaVersion: REPORT_SCHEMA_VERSION, + generatedAt: new Date().toISOString(), + runId: RUN_ID, + adapterProfile: payload.profile, + checks: { + recorded: payload.results, + environmentSummary: payload.environmentSummary, + all: allResults, + }, + sections: { + adapter: allResults.filter((r) => r.section === "adapter"), + ethereum: allResults.filter((r) => r.section === "ethereum"), + execution: allResults.filter((r) => r.section === "execution"), + zama: allResults.filter((r) => r.section === "zama"), + environment: allResults.filter((r) => r.section === "environment"), + }, + infrastructure: { + blockers: payload.blockers, + }, + zama: { + writeValidationDepth: payload.writeValidationDepth, + }, + claim: payload.verdict, + finalVerdict: payload.verdict.verdictLabel, + }; + try { + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, JSON.stringify(artifact, null, 2)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(` [report] Failed to write REPORT_JSON_PATH (${outputPath}): ${message}`); + } +} + +/** Print the final compatibility report to stdout. */ +export function printReport(): void { + const baseResults = readResults(); + const profile = readProfile(); + if (baseResults.length === 0 && profile === null) return; + + const environmentSummary = summarizeEnvironmentSection(baseResults); + const results = [...baseResults, ...environmentSummary]; + + console.log(`\n${FULL}`); + console.log(" Zama Compatibility Report"); + console.log(`${FULL}\n`); + + // ── Adapter profile ──────────────────────────────────────────────────────── + if (profile) { + const structuralCapabilities = profileStructuralCapabilities(profile); + const runtimeCapabilities = profileRuntimeCapabilities(profile); + const finalCapabilities = profile.observedCapabilities; + const shortAddr = + profile.address.length > 20 + ? `${profile.address.slice(0, 10)}…${profile.address.slice(-8)}` + : profile.address; + console.log(` Adapter ${profile.name}`); + console.log(` Source ${profile.source}`); + console.log(` Address ${shortAddr}`); + console.log(` Declared Type ${profile.declaredArchitecture}`); + console.log(` Detected Type ${profile.detectedArchitecture}`); + console.log(` Verification ${profile.verificationModel}`); + console.log(` Init ${profile.initializationStatus}`); + console.log(` Capability Source declared + structural + runtime + final`); + console.log( + ` EIP-712 declared ${renderCapability(profile.declaredCapabilities.eip712Signing)} / structural ${renderCapability(structuralCapabilities.eip712Signing)} / runtime ${renderCapability(runtimeCapabilities.eip712Signing)} / final ${renderCapability(finalCapabilities.eip712Signing)}`, + ); + console.log( + ` Recoverability declared ${renderCapability(profile.declaredCapabilities.recoverableEcdsa)} / structural ${renderCapability(structuralCapabilities.recoverableEcdsa)} / runtime ${renderCapability(runtimeCapabilities.recoverableEcdsa)} / final ${renderCapability(finalCapabilities.recoverableEcdsa)}`, + ); + console.log( + ` Execution rawTx ${renderCapability(finalCapabilities.rawTransactionSigning)} / write ${renderCapability(finalCapabilities.contractExecution)}`, + ); + console.log( + ` Reads & Receipts reads ${renderCapability(finalCapabilities.contractReads)} / receipts ${renderCapability(finalCapabilities.transactionReceiptTracking)}`, + ); + console.log( + ` Zama Surface auth ${renderCapability(finalCapabilities.zamaAuthorizationFlow)} / write ${renderCapability(finalCapabilities.zamaWriteFlow)}`, + ); + if (profile.contradictions.length > 0) { + console.log(" Contradictions"); + for (const contradiction of profile.contradictions) { + console.log(` - ${contradiction}`); + } + } + console.log(); + } + + // ── Sections ─────────────────────────────────────────────────────────────── + const sections: { title: string; key: TestSection }[] = [ + { title: "Adapter Profile", key: "adapter" }, + { title: "Ethereum Compatibility", key: "ethereum" }, + { title: "Adapter-Routed Execution", key: "execution" }, + { title: "Zama SDK Compatibility", key: "zama" }, + { title: "Infrastructure / Environment", key: "environment" }, + ]; + + for (const { title, key } of sections) { + const sectionResults = results.filter((r) => r.section === key); + if (sectionResults.length === 0) continue; + sectionResults.sort((a, b) => testOrder(a.checkId) - testOrder(b.checkId)); + + const pad = Math.max(0, W - title.length - 5); + console.log(` ── ${title} ${"─".repeat(pad)}`); + + for (const r of sectionResults) { + console.log(` ${icon(r.status)} ${r.name.padEnd(36)} ${r.status}`); + + if (r.status === "FAIL") { + if (r.summary) console.log(` Summary: ${r.summary}`); + if (r.reason) console.log(` Reason: ${r.reason}`); + if (r.rootCauseCategory) console.log(` Root cause: ${r.rootCauseCategory}`); + if (r.errorCode) console.log(` Error code: ${r.errorCode}`); + if (r.likelyCause) console.log(` Likely cause: ${r.likelyCause}`); + if (r.recommendation) console.log(` Recommendation: ${r.recommendation}`); + console.log(); + } else if ( + (r.status === "UNSUPPORTED" || + r.status === "UNTESTED" || + r.status === "BLOCKED" || + r.status === "INCONCLUSIVE") && + r.reason + ) { + if (r.summary) console.log(` Summary: ${r.summary}`); + console.log(` Note: ${r.reason}`); + if (r.rootCauseCategory) console.log(` Root cause: ${r.rootCauseCategory}`); + if (r.errorCode) console.log(` Error code: ${r.errorCode}`); + if (r.recommendation) console.log(` Recommendation: ${r.recommendation}`); + } + } + const counts = sectionCounts(sectionResults); + const parts = [ + counts.PASS > 0 ? `${counts.PASS} PASS` : null, + counts.FAIL > 0 ? `${counts.FAIL} FAIL` : null, + counts.UNSUPPORTED > 0 ? `${counts.UNSUPPORTED} UNSUPPORTED` : null, + counts.UNTESTED > 0 ? `${counts.UNTESTED} UNTESTED` : null, + counts.BLOCKED > 0 ? `${counts.BLOCKED} BLOCKED` : null, + counts.INCONCLUSIVE > 0 ? `${counts.INCONCLUSIVE} INCONCLUSIVE` : null, + ].filter(Boolean); + if (parts.length > 0) { + console.log(` Summary ${parts.join(" · ")}`); + } + console.log(); + } + + // ── Verdict ──────────────────────────────────────────────────────────────── + const verdict = resolveClaimFromResults(results); + assertClaimConsistency(verdict, results); + const blockerCounts = summarizeBlockers(results); + const zamaWriteStatus = + results.find((result) => result.checkId === "ZAMA_WRITE_FLOW")?.status ?? null; + const writeValidationDepth = deriveWriteValidationDepth({ + zamaWriteStatus, + observation: readZamaWriteObservation(), + }); + const blockerCount = Object.values(blockerCounts).reduce((acc, count) => acc + (count ?? 0), 0); + const confidence = resolveClaimConfidence({ + evidence: verdict.evidence, + writeValidationDepth, + blockerCount, + }); + const verdictWithConfidence = { + ...verdict, + confidence, + }; + + console.log(SUB); + console.log(` Final: ${verdictWithConfidence.verdictLabel}`); + console.log(` Write Validation Depth: ${writeValidationDepth}`); + console.log(` Confidence: ${confidence}`); + console.log(` Claim: ${verdictWithConfidence.id}`); + for (const line of verdictWithConfidence.rationale) { + console.log(` Why: ${line}`); + } + if (Object.keys(blockerCounts).length > 0) { + const rendered = Object.entries(blockerCounts) + .map(([k, v]) => `${k}=${v}`) + .join(", "); + console.log(` Blockers: ${rendered}`); + } + const jsonPath = (process.env.REPORT_JSON_PATH ?? "").trim(); + if (jsonPath) { + console.log(` JSON: ${jsonPath}`); + } + console.log(`${FULL}\n`); + + exportJsonReport({ + profile, + results: baseResults, + environmentSummary, + verdict: verdictWithConfidence, + blockers: blockerCounts, + writeValidationDepth, + }); +} diff --git a/examples/compatibility-harness/src/report/schema.ts b/examples/compatibility-harness/src/report/schema.ts new file mode 100644 index 000000000..0d99e9324 --- /dev/null +++ b/examples/compatibility-harness/src/report/schema.ts @@ -0,0 +1,54 @@ +import type { + DiagnosticCode, + ObservedAdapterProfile, + RootCauseCategory, + ValidationStatus, +} from "../adapter/types.js"; +import type { ClaimResolution } from "../verdict/types.js"; +import type { CanonicalCheckId } from "./check-registry.js"; + +export const REPORT_KIND = "zama-compatibility-report" as const; +export const REPORT_SCHEMA_VERSION = "1.3.0" as const; +export const SUPPORTED_REPORT_SCHEMA_VERSIONS = ["1.2.0", REPORT_SCHEMA_VERSION] as const; + +export type ReportSection = "adapter" | "ethereum" | "execution" | "zama" | "environment"; +export type InfraRootCause = Extract< + RootCauseCategory, + "ENVIRONMENT" | "RPC" | "RELAYER" | "REGISTRY" +>; +export type WriteValidationDepth = "FULL" | "PARTIAL" | "UNTESTED"; + +export interface ReportCheck { + checkId: CanonicalCheckId; + name: string; + section: ReportSection; + status: ValidationStatus; + summary?: string; + reason?: string; + rootCauseCategory?: RootCauseCategory; + errorCode?: DiagnosticCode; + likelyCause?: string; + recommendation?: string; +} + +export interface ReportArtifact { + kind: typeof REPORT_KIND; + schemaVersion: typeof REPORT_SCHEMA_VERSION; + generatedAt: string; + runId: string; + adapterProfile: ObservedAdapterProfile | null; + checks: { + recorded: ReportCheck[]; + environmentSummary: ReportCheck[]; + all: ReportCheck[]; + }; + sections: Record; + infrastructure: { + blockers: Partial>; + }; + zama?: { + writeValidationDepth: WriteValidationDepth; + }; + claim: ClaimResolution; + finalVerdict: string; +} diff --git a/examples/compatibility-harness/src/setup.ts b/examples/compatibility-harness/src/setup.ts new file mode 100644 index 000000000..a68c042c6 --- /dev/null +++ b/examples/compatibility-harness/src/setup.ts @@ -0,0 +1,3 @@ +// Loaded by vitest before each test file — ensures .env is parsed +// before any module that reads process.env at initialisation time. +import "dotenv/config"; diff --git a/examples/compatibility-harness/src/signer/index.ts b/examples/compatibility-harness/src/signer/index.ts new file mode 100644 index 000000000..2e36b2ccb --- /dev/null +++ b/examples/compatibility-harness/src/signer/index.ts @@ -0,0 +1 @@ +export { defaultLegacySigner as signer } from "../adapter/default.js"; diff --git a/examples/compatibility-harness/src/signer/types.ts b/examples/compatibility-harness/src/signer/types.ts new file mode 100644 index 000000000..977540b3d --- /dev/null +++ b/examples/compatibility-harness/src/signer/types.ts @@ -0,0 +1,74 @@ +/** + * Legacy signer interface (backward compatibility). + * + * New integrations should export `adapter` from a module and run with + * SIGNER_MODULE=. The harness still accepts `signer` exports and wraps + * them into an adapter automatically. + * + * This interface remains documented to support existing integrations. + * + * `signTypedData` is REQUIRED. It is the only method the Zama SDK uses for + * credential authorization (sdk.allow). Without it, Zama compatibility cannot + * be established. + * + * For on-chain transaction testing, implement ONE of: + * + * • `signTransaction` — EOA path. Signs a raw EIP-1559 transaction and + * returns the RLP-encoded signed bytes. Use this for EOA wallets (MetaMask, + * Ledger, viem private key, etc.). + * + * • `writeContract` — MPC / smart-account path. Executes a contract call via + * your own API (Crossmint /transactions, Turnkey, etc.) and returns the + * transaction hash directly. Use this when your system does not expose raw + * transaction signing. + * + * If neither is provided, write-surface checks are marked unsupported. + */ +export interface Signer { + /** The Ethereum address controlled by this signer. */ + address: string; + + /** + * Sign EIP-712 typed data and return the hex signature. + * + * REQUIRED — used by the Zama SDK for credential authorization. + * + * @param data - The full EIP-712 payload: { domain, types, primaryType, message } + * @returns Hex-encoded signature (0x-prefixed, 65 bytes) + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTypedData: (data: any) => Promise; + + /** + * Sign a raw EIP-1559 transaction and return the serialized signed hex. + * + * OPTIONAL (EOA path). Implement this for EOA-compatible signers. + * Leave undefined for MPC wallets or smart accounts — implement + * `writeContract` instead. + * + * @param tx - { to, value, data, gas, maxFeePerGas, maxPriorityFeePerGas, nonce, chainId } + * @returns RLP-encoded signed transaction (0x-prefixed) + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signTransaction?: (tx: any) => Promise; + + /** + * Execute a contract write and return the transaction hash. + * + * OPTIONAL (MPC / smart-account path). Implement this if your system + * submits transactions via a higher-level API rather than raw signing. + * Leave undefined for EOA signers — implement `signTransaction` instead. + * + * @param config - { address, abi, functionName, args?, value? } + * @returns Transaction hash (0x-prefixed) + */ + writeContract?: (config: { + address: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abi: readonly any[]; + functionName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: readonly any[]; + value?: bigint; + }) => Promise; +} diff --git a/examples/compatibility-harness/src/tests/adapterExecution.test.ts b/examples/compatibility-harness/src/tests/adapterExecution.test.ts new file mode 100644 index 000000000..fa663f021 --- /dev/null +++ b/examples/compatibility-harness/src/tests/adapterExecution.test.ts @@ -0,0 +1,103 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { isOperatorContract } from "@zama-fhe/sdk"; +import { isMockModeEnabled, mockModeNote } from "../config/runtime.js"; +import { + adapter, + buildGenericSigner, + buildSdk, + discoverTokenAddress, + getAdapterAddress, + initializeAdapter, +} from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { recordWithRuntimeObservation } from "../report/reporter.js"; + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Adapter-Routed Execution Surface", () => { + it("reads a Zama contract via the adapter or harness RPC fallback", async () => { + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "ADAPTER_CONTRACT_READ", + name: "Adapter Contract Read", + section: "execution", + status: diagnostic.status, + summary: "Adapter initialization failed before contract read validation", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Resolve adapter initialization issues first.", + }); + return; + } + + if (isMockModeEnabled()) { + recordWithRuntimeObservation({ + checkId: "ADAPTER_CONTRACT_READ", + name: "Adapter Contract Read", + section: "execution", + status: "UNTESTED", + summary: "Contract read validation skipped in mock mode", + reason: mockModeNote(), + rootCauseCategory: "HARNESS", + recommendation: "Disable HARNESS_MOCK_MODE to validate on-chain read behavior.", + }); + return; + } + + const sdk = buildSdk(); + try { + const tokenAddress = await discoverTokenAddress(sdk); + const holder = await getAdapterAddress(); + const readRequest = isOperatorContract(tokenAddress, holder, holder); + const result = adapter.readContract + ? await adapter.readContract(readRequest) + : await buildGenericSigner().readContract(readRequest as never); + if (typeof result !== "boolean") { + throw new Error("Contract read did not return a boolean value"); + } + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "ADAPTER_CONTRACT_READ", + name: "Adapter Contract Read", + section: "execution", + status: diagnostic.status, + summary: "Contract read validation failed", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Verify RPC access and adapter read routing.", + }); + sdk.terminate(); + return; + } + + sdk.terminate(); + recordWithRuntimeObservation( + { + checkId: "ADAPTER_CONTRACT_READ", + name: "Adapter Contract Read", + section: "execution", + status: "PASS", + summary: adapter.readContract + ? "Adapter read a Zama contract successfully" + : "Adapter read path validated through harness RPC fallback", + }, + { + contractReads: adapter.readContract ? "SUPPORTED" : "UNSUPPORTED", + }, + ); + expect(true).toBe(true); + }); +}); diff --git a/examples/compatibility-harness/src/tests/adapterProfile.test.ts b/examples/compatibility-harness/src/tests/adapterProfile.test.ts new file mode 100644 index 000000000..808daa84b --- /dev/null +++ b/examples/compatibility-harness/src/tests/adapterProfile.test.ts @@ -0,0 +1,142 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { detectArchitecture, detectVerificationModel } from "../adapter/profile.js"; +import { emptyCapabilities } from "../adapter/types.js"; +import { resolveFinalCapabilities } from "../adapter/capability-evidence.js"; +import { + adapter, + adapterDeclaredCapabilities, + adapterObservedStructuralCapabilities, + adapterObservedRuntimeCapabilities, + adapterObservedCapabilities, + adapterSource, + getAdapterAddress, + initializeAdapter, +} from "../harness/adapter.js"; +import { detectCapabilityContradictions } from "../adapter/contradictions.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { + mergeProfile, + readProfile, + recordProfile, + recordWithRuntimeObservation, +} from "../report/reporter.js"; + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Adapter Profile", () => { + it("records adapter metadata, initialization, and address resolution", async () => { + const existingProfile = readProfile(); + const declaredCapabilities = { + ...emptyCapabilities(), + ...adapterDeclaredCapabilities, + ...(existingProfile?.declaredCapabilities ?? {}), + }; + const observedStructuralCapabilities = { + ...emptyCapabilities(), + ...adapterObservedStructuralCapabilities, + ...(existingProfile?.observedStructuralCapabilities ?? {}), + }; + const observedRuntimeCapabilities = { + ...emptyCapabilities(), + ...adapterObservedRuntimeCapabilities, + ...(existingProfile?.observedRuntimeCapabilities ?? {}), + }; + const observedCapabilities = resolveFinalCapabilities({ + structural: { + ...emptyCapabilities(), + ...adapterObservedCapabilities, + ...observedStructuralCapabilities, + }, + runtime: observedRuntimeCapabilities, + }); + const baseProfile = { + name: adapter.metadata.name, + source: adapterSource, + declaredArchitecture: adapter.metadata.declaredArchitecture ?? "UNKNOWN", + detectedArchitecture: detectArchitecture( + adapter.metadata.declaredArchitecture, + observedCapabilities, + ), + verificationModel: detectVerificationModel( + adapter.metadata.verificationModel, + observedCapabilities, + ), + address: "(unresolved)", + declaredCapabilities, + observedStructuralCapabilities, + observedRuntimeCapabilities, + observedCapabilities, + contradictions: detectCapabilityContradictions(declaredCapabilities, observedCapabilities), + initializationStatus: initError + ? classifyInfrastructureIssue(initError).status + : ("PASS" as const), + }; + + recordProfile(baseProfile); + + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "ADAPTER_INITIALIZATION", + name: "Adapter Initialization", + section: "adapter", + status: diagnostic.status, + summary: "Adapter could not initialize", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Fix adapter configuration and retry the harness.", + }); + return; + } + + recordWithRuntimeObservation({ + checkId: "ADAPTER_INITIALIZATION", + name: "Adapter Initialization", + section: "adapter", + status: "PASS", + summary: "Adapter initialized successfully", + }); + + try { + const address = await getAdapterAddress(); + mergeProfile({ + address, + observedRuntimeCapabilities: { + addressResolution: "SUPPORTED", + }, + }); + recordWithRuntimeObservation({ + checkId: "ADDRESS_RESOLUTION", + name: "Address Resolution", + section: "adapter", + status: "PASS", + summary: `Resolved adapter address ${address}`, + }); + expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "ADDRESS_RESOLUTION", + name: "Address Resolution", + section: "adapter", + status: diagnostic.status, + summary: "Adapter address could not be resolved", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Ensure the adapter can resolve its wallet address during initialization.", + }); + return; + } + }); +}); diff --git a/examples/compatibility-harness/src/tests/eip712.test.ts b/examples/compatibility-harness/src/tests/eip712.test.ts new file mode 100644 index 000000000..5bb4fe160 --- /dev/null +++ b/examples/compatibility-harness/src/tests/eip712.test.ts @@ -0,0 +1,304 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { getAddress, hashTypedData, type Hex } from "viem"; +import { detectArchitecture, detectVerificationModel } from "../adapter/profile.js"; +import { emptyCapabilities } from "../adapter/types.js"; +import { networkConfig } from "../config/network.js"; +import { + adapter, + adapterObservedCapabilities, + getAdapterAddress, + initializeAdapter, +} from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { + classifyEip712SigningFailure, + classifyRecoverabilityFailure, +} from "../harness/negative-paths.js"; +import { mergeProfile, recordWithRuntimeObservation } from "../report/reporter.js"; +import { recoverEIP712Signer } from "../utils/crypto.js"; +import { publicClient } from "../utils/rpc.js"; + +const TEST_TYPED_DATA = { + domain: { + name: "Zama Compatibility Harness", + version: "2", + chainId: networkConfig.chainId, + verifyingContract: "0x0000000000000000000000000000000000000001" as const, + }, + types: { + Validation: [ + { name: "purpose", type: "string" }, + { name: "timestamp", type: "uint256" }, + ], + }, + primaryType: "Validation" as const, + message: { + purpose: "identity-and-verification", + timestamp: BigInt(Math.floor(Date.now() / 1000)), + }, +}; + +const ERC1271_ABI = [ + { + name: "isValidSignature", + type: "function", + stateMutability: "view", + inputs: [ + { name: "_hash", type: "bytes32" }, + { name: "_signature", type: "bytes" }, + ], + outputs: [{ name: "magicValue", type: "bytes4" }], + }, +] as const; + +const ERC1271_MAGIC_VALUE = "0x1626ba7e"; + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Identity and Verification", () => { + it("validates EIP-712 signing and recoverability", async () => { + const baseCapabilities = { ...emptyCapabilities(), ...adapterObservedCapabilities }; + + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "EIP712_SIGNING", + name: "EIP-712 Signing", + section: "ethereum", + status: diagnostic.status, + summary: "Adapter initialization failed before signing could be tested", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Resolve adapter initialization errors first.", + }); + return; + } + + if (!adapter.signTypedData) { + recordWithRuntimeObservation( + { + checkId: "EIP712_SIGNING", + name: "EIP-712 Signing", + section: "ethereum", + status: "UNSUPPORTED", + summary: "Adapter does not expose EIP-712 signing", + reason: "signTypedData is not implemented by the adapter", + rootCauseCategory: "ADAPTER", + recommendation: "Implement signTypedData to validate Zama authorization compatibility.", + }, + { + recoverableEcdsa: "UNSUPPORTED", + zamaAuthorizationFlow: "UNSUPPORTED", + }, + ); + return; + } + + let signature: string; + try { + signature = await adapter.signTypedData(TEST_TYPED_DATA); + } catch (err) { + const message = errorMessage(err); + const failure = classifyEip712SigningFailure(message); + recordWithRuntimeObservation({ + checkId: "EIP712_SIGNING", + name: "EIP-712 Signing", + section: "ethereum", + status: failure.status, + summary: failure.infrastructure + ? "EIP-712 signing validation blocked by environment/infrastructure" + : "Adapter failed to produce an EIP-712 signature", + reason: message, + rootCauseCategory: failure.rootCauseCategory, + errorCode: failure.errorCode, + likelyCause: "The adapter rejected the typed-data payload or transformed it incorrectly.", + recommendation: "Ensure the adapter supports standard Ethereum EIP-712 signing.", + }); + if (!failure.infrastructure) { + expect.fail(message); + } + return; + } + + const shouldRunErc1271Check = + adapter.metadata.declaredArchitecture === "SMART_ACCOUNT" || + adapter.metadata.verificationModel === "ERC1271"; + + async function runErc1271Check(adapterAddressHint?: string): Promise { + let signerAddress = adapterAddressHint; + if (!signerAddress) { + try { + signerAddress = await getAdapterAddress(); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "ERC1271_VERIFICATION", + name: "ERC-1271 Verification", + section: "ethereum", + status: diagnostic.status, + summary: "ERC-1271 verification could not resolve adapter address", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: + "Ensure contract-wallet identity can be resolved before ERC-1271 checks.", + }); + return false; + } + } + + try { + const typedDataHash = hashTypedData(TEST_TYPED_DATA); + const result = await publicClient.readContract({ + address: getAddress(signerAddress), + abi: ERC1271_ABI, + functionName: "isValidSignature", + args: [typedDataHash, signature as Hex], + }); + const magicValue = String(result).toLowerCase(); + if (magicValue === ERC1271_MAGIC_VALUE) { + recordWithRuntimeObservation({ + checkId: "ERC1271_VERIFICATION", + name: "ERC-1271 Verification", + section: "ethereum", + status: "PASS", + summary: "Smart-account signature validated through ERC-1271", + }); + return true; + } + recordWithRuntimeObservation({ + checkId: "ERC1271_VERIFICATION", + name: "ERC-1271 Verification", + section: "ethereum", + status: "FAIL", + summary: "Contract did not return ERC-1271 magic value", + reason: `Expected ${ERC1271_MAGIC_VALUE}, got ${magicValue}`, + rootCauseCategory: "SIGNER", + recommendation: "Verify contract-wallet signature validation semantics.", + }); + return false; + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + const isInfra = diagnostic.rootCauseCategory !== "HARNESS"; + recordWithRuntimeObservation({ + checkId: "ERC1271_VERIFICATION", + name: "ERC-1271 Verification", + section: "ethereum", + status: isInfra ? diagnostic.status : "FAIL", + summary: isInfra + ? "ERC-1271 verification was blocked by environment/infrastructure" + : "ERC-1271 verification call failed", + reason: message, + rootCauseCategory: isInfra ? diagnostic.rootCauseCategory : "ADAPTER", + errorCode: isInfra ? diagnostic.errorCode : undefined, + recommendation: isInfra + ? "Check RPC and contract availability for ERC-1271 verification." + : "Verify the adapter address points to an ERC-1271-compatible contract.", + }); + return false; + } + } + + const recovered = await recoverEIP712Signer(TEST_TYPED_DATA, signature); + if (recovered === null) { + const recoverabilityFailure = classifyRecoverabilityFailure(); + const erc1271Pass = shouldRunErc1271Check ? await runErc1271Check() : false; + mergeProfile({ + observedRuntimeCapabilities: { + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNSUPPORTED", + }, + verificationModel: erc1271Pass + ? "ERC1271" + : detectVerificationModel(adapter.metadata.verificationModel, { + ...baseCapabilities, + recoverableEcdsa: "UNSUPPORTED", + }), + detectedArchitecture: detectArchitecture(adapter.metadata.declaredArchitecture, { + ...baseCapabilities, + recoverableEcdsa: "UNSUPPORTED", + }), + }); + recordWithRuntimeObservation({ + checkId: "EIP712_RECOVERABILITY", + name: "EIP-712 Recoverability", + section: "ethereum", + status: recoverabilityFailure.status, + summary: "Signature is not recoverable via ecrecover", + reason: "recoverTypedDataAddress could not recover a matching address", + rootCauseCategory: recoverabilityFailure.rootCauseCategory, + likelyCause: "The verification model is not EOA-style recoverable ECDSA.", + recommendation: + "If this is expected, declare the architecture explicitly and treat Zama authorization as incompatible until proven otherwise.", + }); + expect.fail("Signature was not recoverable"); + return; + } + + const address = await getAdapterAddress(); + if (getAddress(recovered) !== getAddress(address)) { + const recoverabilityFailure = classifyRecoverabilityFailure(); + const erc1271Pass = shouldRunErc1271Check ? await runErc1271Check(address) : false; + const mismatchPatch = { + observedRuntimeCapabilities: { + eip712Signing: "SUPPORTED" as const, + recoverableEcdsa: "UNSUPPORTED" as const, + }, + }; + mergeProfile( + erc1271Pass ? { ...mismatchPatch, verificationModel: "ERC1271" } : mismatchPatch, + ); + recordWithRuntimeObservation({ + checkId: "EIP712_RECOVERABILITY", + name: "EIP-712 Recoverability", + section: "ethereum", + status: recoverabilityFailure.status, + summary: "Recovered address does not match adapter identity", + reason: `Recovered ${recovered}, expected ${address}`, + rootCauseCategory: recoverabilityFailure.rootCauseCategory, + likelyCause: "The adapter is signing with a different key than the resolved address.", + recommendation: + "Verify that address resolution and signing are bound to the same wallet identity.", + }); + expect.fail("Recovered address mismatch"); + return; + } + + const observedRuntimeCapabilities = { + ...baseCapabilities, + eip712Signing: "SUPPORTED" as const, + recoverableEcdsa: "SUPPORTED" as const, + zamaAuthorizationFlow: "SUPPORTED" as const, + }; + mergeProfile({ + observedRuntimeCapabilities, + verificationModel: detectVerificationModel( + adapter.metadata.verificationModel, + observedRuntimeCapabilities, + ), + detectedArchitecture: detectArchitecture( + adapter.metadata.declaredArchitecture, + observedRuntimeCapabilities, + ), + }); + recordWithRuntimeObservation({ + checkId: "EIP712_RECOVERABILITY", + name: "EIP-712 Recoverability", + section: "ethereum", + status: "PASS", + summary: "Signature is recoverable and matches the adapter address", + }); + expect(getAddress(recovered)).toBe(getAddress(address)); + }); +}); diff --git a/examples/compatibility-harness/src/tests/fixtures/example-baselines/crossmint.lock.json b/examples/compatibility-harness/src/tests/fixtures/example-baselines/crossmint.lock.json new file mode 100644 index 000000000..3ffaa32f1 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/example-baselines/crossmint.lock.json @@ -0,0 +1,33 @@ +{ + "id": "crossmint", + "adapterModule": "./examples/crossmint/signer.ts", + "importEnv": { + "CROSSMINT_API_KEY": "baseline-key", + "CROSSMINT_WALLET_LOCATOR": "email:baseline@example.com:evm-smart-wallet" + }, + "expectedProfile": { + "name": "Crossmint API-Routed Adapter", + "declaredArchitecture": "API_ROUTED_EXECUTION", + "verificationModel": "UNKNOWN", + "capabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNSUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "UNSUPPORTED", + "transactionReceiptTracking": "UNSUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + } + }, + "claimEnvelope": { + "PASS": ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + "PARTIAL": [ + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED", + "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED" + ], + "INCONCLUSIVE": ["INCONCLUSIVE_AUTHORIZATION_BLOCKED", "INCONCLUSIVE_AUTHORIZATION_UNTESTED"] + } +} diff --git a/examples/compatibility-harness/src/tests/fixtures/example-baselines/openfort.lock.json b/examples/compatibility-harness/src/tests/fixtures/example-baselines/openfort.lock.json new file mode 100644 index 000000000..299f0404a --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/example-baselines/openfort.lock.json @@ -0,0 +1,30 @@ +{ + "id": "openfort", + "adapterModule": "./examples/openfort/signer.ts", + "importEnv": {}, + "expectedProfile": { + "name": "Openfort EOA Baseline Adapter", + "declaredArchitecture": "EOA", + "verificationModel": "RECOVERABLE_ECDSA", + "capabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + } + }, + "claimEnvelope": { + "PASS": ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + "PARTIAL": [ + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED", + "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED" + ], + "INCONCLUSIVE": ["INCONCLUSIVE_AUTHORIZATION_BLOCKED", "INCONCLUSIVE_AUTHORIZATION_UNTESTED"] + } +} diff --git a/examples/compatibility-harness/src/tests/fixtures/example-baselines/turnkey.lock.json b/examples/compatibility-harness/src/tests/fixtures/example-baselines/turnkey.lock.json new file mode 100644 index 000000000..8b3d9537e --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/example-baselines/turnkey.lock.json @@ -0,0 +1,30 @@ +{ + "id": "turnkey", + "adapterModule": "./examples/turnkey/signer.ts", + "importEnv": {}, + "expectedProfile": { + "name": "Turnkey API Key Adapter", + "declaredArchitecture": "API_ROUTED_EXECUTION", + "verificationModel": "UNKNOWN", + "capabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNSUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + } + }, + "claimEnvelope": { + "PASS": ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + "PARTIAL": [ + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED", + "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED" + ], + "INCONCLUSIVE": ["INCONCLUSIVE_AUTHORIZATION_BLOCKED", "INCONCLUSIVE_AUTHORIZATION_UNTESTED"] + } +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/eoa-full-compatible.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/eoa-full-compatible.report.json new file mode 100644 index 000000000..b05c04bc2 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/eoa-full-compatible.report.json @@ -0,0 +1,148 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.3.0", + "generatedAt": "2026-04-08T00:00:00.000Z", + "runId": "golden-eoa-full-compatible", + "adapterProfile": { + "name": "Golden EOA Adapter", + "source": "adapter", + "declaredArchitecture": "EOA", + "detectedArchitecture": "EOA", + "verificationModel": "RECOVERABLE_ECDSA", + "address": "0x0000000000000000000000000000000000000001", + "declaredCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedStructuralCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedRuntimeCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "contradictions": [], + "initializationStatus": "PASS" + }, + "checks": { + "recorded": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ], + "environmentSummary": [], + "all": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ] + }, + "sections": { + "adapter": [], + "ethereum": [ + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ], + "execution": [], + "zama": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + } + ], + "environment": [] + }, + "infrastructure": { + "blockers": {} + }, + "zama": { + "writeValidationDepth": "FULL" + }, + "claim": { + "id": "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "verdictLabel": "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS", + "confidence": "HIGH", + "rationale": ["Authorization, recoverability, and write-path probe all passed."], + "evidence": { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "PASS" + } + }, + "finalVerdict": "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS" +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.1.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.1.report.json new file mode 100644 index 000000000..7d1f92b16 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.1.report.json @@ -0,0 +1,4 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.1.0" +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.2.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.2.report.json new file mode 100644 index 000000000..de6589867 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/legacy-schema-1.2.report.json @@ -0,0 +1,122 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.2.0", + "generatedAt": "2026-04-08T00:00:00.000Z", + "runId": "golden-eoa-full-compatible", + "adapterProfile": { + "name": "Golden EOA Adapter", + "source": "adapter", + "declaredArchitecture": "EOA", + "detectedArchitecture": "EOA", + "verificationModel": "RECOVERABLE_ECDSA", + "address": "0x0000000000000000000000000000000000000001", + "declaredCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "SUPPORTED", + "rawTransactionSigning": "SUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "contradictions": [], + "initializationStatus": "PASS" + }, + "checks": { + "recorded": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ], + "environmentSummary": [], + "all": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ] + }, + "sections": { + "adapter": [], + "ethereum": [ + { + "checkId": "EIP712_RECOVERABILITY", + "name": "EIP-712 Recoverability", + "section": "ethereum", + "status": "PASS" + } + ], + "execution": [], + "zama": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + }, + { + "checkId": "ZAMA_WRITE_FLOW", + "name": "Zama Write Flow", + "section": "zama", + "status": "PASS" + } + ], + "environment": [] + }, + "infrastructure": { + "blockers": {} + }, + "claim": { + "id": "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "verdictLabel": "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS", + "rationale": ["Authorization, recoverability, and write-path probe all passed."], + "evidence": { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "PASS" + } + }, + "finalVerdict": "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS" +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-claim-requirement-mismatch.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-claim-requirement-mismatch.report.json new file mode 100644 index 000000000..567ee2540 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-claim-requirement-mismatch.report.json @@ -0,0 +1,60 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.2.0", + "generatedAt": "2026-04-08T00:00:00.000Z", + "runId": "malformed-claim-requirement-mismatch", + "adapterProfile": null, + "checks": { + "recorded": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ], + "environmentSummary": [], + "all": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ] + }, + "sections": { + "adapter": [], + "ethereum": [], + "execution": [], + "zama": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ], + "environment": [] + }, + "infrastructure": { + "blockers": {} + }, + "claim": { + "id": "PARTIAL_AUTHORIZATION_CHECK_MISSING", + "verdictLabel": "PARTIALLY VALIDATED — AUTHORIZATION CHECK NOT RECORDED", + "rationale": ["Authorization flow result is missing from the executed checks."], + "evidence": { + "Zama Authorization Flow": "MISSING" + }, + "evidenceDetails": [ + { + "check": "Zama Authorization Flow", + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "status": "MISSING", + "reasonCategory": "MISSING_EVIDENCE" + } + ] + }, + "finalVerdict": "PARTIALLY VALIDATED — AUTHORIZATION CHECK NOT RECORDED" +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-missing-check-id.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-missing-check-id.report.json new file mode 100644 index 000000000..e8509588a --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/malformed-missing-check-id.report.json @@ -0,0 +1,73 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.2.0", + "generatedAt": "2026-04-08T00:00:00.000Z", + "runId": "malformed-missing-check-id", + "adapterProfile": null, + "checks": { + "recorded": [ + { + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ], + "environmentSummary": [], + "all": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ] + }, + "sections": { + "adapter": [], + "ethereum": [], + "execution": [], + "zama": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "PASS" + } + ], + "environment": [] + }, + "infrastructure": { + "blockers": {} + }, + "claim": { + "id": "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED", + "verdictLabel": "ZAMA COMPATIBLE FOR AUTHORIZATION FLOWS — WRITE FLOW NOT TESTED", + "rationale": ["Authorization surface is compatible and no write result was recorded."], + "evidence": { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "MISSING" + }, + "evidenceDetails": [ + { + "check": "Zama Authorization Flow", + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "status": "PASS", + "reasonCategory": "VALIDATED" + }, + { + "check": "EIP-712 Recoverability", + "checkId": "EIP712_RECOVERABILITY", + "status": "PASS", + "reasonCategory": "VALIDATED" + }, + { + "check": "Zama Write Flow", + "checkId": "ZAMA_WRITE_FLOW", + "status": "MISSING", + "reasonCategory": "MISSING_EVIDENCE" + } + ] + }, + "finalVerdict": "ZAMA COMPATIBLE FOR AUTHORIZATION FLOWS — WRITE FLOW NOT TESTED" +} diff --git a/examples/compatibility-harness/src/tests/fixtures/report-artifacts/turnkey-env-blocked.report.json b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/turnkey-env-blocked.report.json new file mode 100644 index 000000000..9b3b6ae08 --- /dev/null +++ b/examples/compatibility-harness/src/tests/fixtures/report-artifacts/turnkey-env-blocked.report.json @@ -0,0 +1,142 @@ +{ + "kind": "zama-compatibility-report", + "schemaVersion": "1.3.0", + "generatedAt": "2026-04-08T00:00:00.000Z", + "runId": "golden-turnkey-env-blocked", + "adapterProfile": { + "name": "Turnkey API Key Adapter", + "source": "adapter", + "declaredArchitecture": "API_ROUTED_EXECUTION", + "detectedArchitecture": "API_ROUTED_EXECUTION", + "verificationModel": "UNKNOWN", + "address": "(unresolved)", + "declaredCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNSUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedStructuralCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNSUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "observedRuntimeCapabilities": { + "addressResolution": "UNKNOWN", + "eip712Signing": "UNKNOWN", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNKNOWN", + "contractExecution": "UNKNOWN", + "contractReads": "UNKNOWN", + "transactionReceiptTracking": "UNKNOWN", + "zamaAuthorizationFlow": "UNKNOWN", + "zamaWriteFlow": "UNKNOWN" + }, + "observedCapabilities": { + "addressResolution": "SUPPORTED", + "eip712Signing": "SUPPORTED", + "recoverableEcdsa": "UNKNOWN", + "rawTransactionSigning": "UNSUPPORTED", + "contractExecution": "SUPPORTED", + "contractReads": "SUPPORTED", + "transactionReceiptTracking": "SUPPORTED", + "zamaAuthorizationFlow": "SUPPORTED", + "zamaWriteFlow": "SUPPORTED" + }, + "contradictions": [], + "initializationStatus": "BLOCKED" + }, + "checks": { + "recorded": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT", + "errorCode": "ENV_MISSING_CONFIG" + } + ], + "environmentSummary": [ + { + "checkId": "ENVIRONMENT_CONFIGURATION", + "name": "Environment Configuration", + "section": "environment", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT" + } + ], + "all": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT", + "errorCode": "ENV_MISSING_CONFIG" + }, + { + "checkId": "ENVIRONMENT_CONFIGURATION", + "name": "Environment Configuration", + "section": "environment", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT" + } + ] + }, + "sections": { + "adapter": [], + "ethereum": [], + "execution": [], + "zama": [ + { + "checkId": "ZAMA_AUTHORIZATION_FLOW", + "name": "Zama Authorization Flow", + "section": "zama", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT", + "errorCode": "ENV_MISSING_CONFIG" + } + ], + "environment": [ + { + "checkId": "ENVIRONMENT_CONFIGURATION", + "name": "Environment Configuration", + "section": "environment", + "status": "BLOCKED", + "rootCauseCategory": "ENVIRONMENT" + } + ] + }, + "infrastructure": { + "blockers": { + "ENVIRONMENT": 1 + } + }, + "zama": { + "writeValidationDepth": "UNTESTED" + }, + "claim": { + "id": "INCONCLUSIVE_AUTHORIZATION_BLOCKED", + "verdictLabel": "INCONCLUSIVE — AUTHORIZATION FLOW BLOCKED BY ENVIRONMENT OR INFRASTRUCTURE", + "confidence": "LOW", + "rationale": [ + "Authorization validation was blocked by environment or infrastructure conditions." + ], + "evidence": { + "Zama Authorization Flow": "BLOCKED" + } + }, + "finalVerdict": "INCONCLUSIVE — AUTHORIZATION FLOW BLOCKED BY ENVIRONMENT OR INFRASTRUCTURE" +} diff --git a/examples/compatibility-harness/src/tests/transaction.test.ts b/examples/compatibility-harness/src/tests/transaction.test.ts new file mode 100644 index 000000000..8209fe118 --- /dev/null +++ b/examples/compatibility-harness/src/tests/transaction.test.ts @@ -0,0 +1,197 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { parseGwei } from "viem"; +import { isMockModeEnabled, mockModeNote } from "../config/runtime.js"; +import { recordWithRuntimeObservation } from "../report/reporter.js"; +import { publicClient } from "../utils/rpc.js"; +import { networkConfig } from "../config/network.js"; +import { adapter, getAdapterAddress, initializeAdapter } from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Ethereum Raw Transaction Flow", () => { + it("signs and broadcasts a raw EIP-1559 transaction when supported", async () => { + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: diagnostic.status, + summary: "Adapter initialization failed before raw transaction validation", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Resolve adapter initialization first.", + }); + return; + } + + if (!adapter.signTransaction) { + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: "UNSUPPORTED", + summary: "Adapter does not expose raw transaction signing", + reason: "signTransaction is not implemented by the adapter", + rootCauseCategory: "ADAPTER", + recommendation: "This is expected for API-routed execution systems.", + }); + return; + } + + if (isMockModeEnabled()) { + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: "UNTESTED", + summary: "Raw transaction execution skipped in mock mode", + reason: mockModeNote(), + rootCauseCategory: "HARNESS", + recommendation: "Disable HARNESS_MOCK_MODE to validate broadcast behavior.", + }); + return; + } + + let address; + try { + address = await getAdapterAddress(); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: diagnostic.status, + summary: "Address resolution blocked raw transaction validation", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: + "Fix adapter identity configuration before validating raw transaction signing.", + }); + return; + } + + let nonce: number; + let gasPrice: { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }; + try { + nonce = await publicClient.getTransactionCount({ address }); + const feeData = await publicClient.estimateFeesPerGas(); + gasPrice = { + maxFeePerGas: feeData.maxFeePerGas ?? parseGwei("20"), + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? parseGwei("1"), + }; + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: diagnostic.status, + summary: "RPC dependencies blocked raw transaction validation", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Check RPC_URL connectivity and retry.", + }); + return; + } + + const tx = { + to: address, + value: 0n, + data: "0x" as const, + gas: 21000n, + maxFeePerGas: gasPrice.maxFeePerGas, + maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, + nonce, + chainId: networkConfig.chainId, + type: "eip1559" as const, + }; + + let signedTx: string; + try { + signedTx = await adapter.signTransaction(tx); + } catch (err) { + const message = errorMessage(err); + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: "FAIL", + summary: "Adapter rejected raw transaction signing", + reason: message, + rootCauseCategory: "ADAPTER", + recommendation: + "Implement signTransaction correctly or rely on contract execution instead.", + }); + expect.fail(message); + return; + } + + try { + const txHash = await publicClient.sendRawTransaction({ + serializedTransaction: signedTx as `0x${string}`, + }); + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + if (receipt.status !== "success") { + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: "FAIL", + summary: "Raw transaction was submitted but did not succeed", + reason: `Transaction reverted (hash: ${txHash})`, + rootCauseCategory: "SIGNER", + recommendation: "Inspect the transaction receipt on Sepolia.", + }); + expect.fail(`Raw transaction reverted: ${txHash}`); + return; + } + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + const isInfra = diagnostic.rootCauseCategory !== "HARNESS"; + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: isInfra ? diagnostic.status : "FAIL", + summary: isInfra + ? "Signed raw transaction broadcast was blocked by infrastructure" + : "Signed raw transaction could not be broadcast successfully", + reason: message, + rootCauseCategory: isInfra ? diagnostic.rootCauseCategory : "SIGNER", + errorCode: isInfra ? diagnostic.errorCode : undefined, + recommendation: isInfra + ? "Fix environment or network prerequisites, then retry." + : "Verify that the signed transaction is a valid EIP-1559 payload.", + }); + if (!isInfra) { + expect.fail(message); + } + return; + } + recordWithRuntimeObservation({ + checkId: "RAW_TRANSACTION_EXECUTION", + name: "Raw Transaction Execution", + section: "ethereum", + status: "PASS", + summary: "Adapter signed and broadcast a raw EIP-1559 transaction successfully", + }); + expect(true).toBe(true); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/adapterCheck.unit.test.ts b/examples/compatibility-harness/src/tests/unit/adapterCheck.unit.test.ts new file mode 100644 index 000000000..b856554a4 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/adapterCheck.unit.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; +import { emptyCapabilities, type AdapterCapabilities } from "../../adapter/types.js"; +import { adapterQualityExitCode, evaluateAdapterQuality } from "../../cli/adapter-check-core.js"; + +function capabilities(patch: Partial = {}): AdapterCapabilities { + return { + ...emptyCapabilities(), + ...patch, + }; +} + +describe("cli.adapter-check-core.evaluateAdapterQuality", () => { + it("passes for a coherent adapter profile", () => { + const report = evaluateAdapterQuality({ + source: "adapter", + metadata: { + name: "Test Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [11155111], + }, + declaredCapabilities: capabilities({ + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }), + observedCapabilities: capabilities({ + addressResolution: "SUPPORTED", + eip712Signing: "SUPPORTED", + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "SUPPORTED", + contractExecution: "SUPPORTED", + contractReads: "SUPPORTED", + transactionReceiptTracking: "SUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + zamaWriteFlow: "SUPPORTED", + }), + chainId: 11155111, + }); + + expect(report.checks.some((check) => check.severity === "FAIL")).toBe(false); + expect(adapterQualityExitCode(report)).toBe(0); + }); + + it("fails when capability dependencies are contradictory", () => { + const report = evaluateAdapterQuality({ + source: "adapter", + metadata: { + name: "Broken Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + }, + declaredCapabilities: capabilities({ + eip712Signing: "UNSUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + }), + observedCapabilities: capabilities({ + eip712Signing: "UNSUPPORTED", + zamaAuthorizationFlow: "SUPPORTED", + }), + chainId: 11155111, + }); + + expect(report.checks.some((check) => check.id === "CAPABILITY_AUTH_DEPENDENCY")).toBe(true); + expect(adapterQualityExitCode(report)).toBe(2); + }); + + it("fails on declared vs observed contradictions", () => { + const report = evaluateAdapterQuality({ + source: "adapter", + metadata: { + name: "Contradictory Adapter", + declaredArchitecture: "EOA", + verificationModel: "RECOVERABLE_ECDSA", + supportedChainIds: [11155111], + }, + declaredCapabilities: capabilities({ + rawTransactionSigning: "SUPPORTED", + }), + observedCapabilities: capabilities({ + rawTransactionSigning: "UNSUPPORTED", + }), + chainId: 11155111, + }); + + expect(report.checks.some((check) => check.id === "CAPABILITY_CONTRADICTIONS")).toBe(true); + expect(adapterQualityExitCode(report)).toBe(2); + }); + + it("maps canonical check support from declared capabilities", () => { + const report = evaluateAdapterQuality({ + source: "adapter", + metadata: { + name: "API Adapter", + declaredArchitecture: "API_ROUTED_EXECUTION", + verificationModel: "UNKNOWN", + supportedChainIds: [11155111], + }, + declaredCapabilities: capabilities({ + rawTransactionSigning: "UNSUPPORTED", + zamaWriteFlow: "SUPPORTED", + contractExecution: "SUPPORTED", + }), + observedCapabilities: capabilities({ + rawTransactionSigning: "UNSUPPORTED", + zamaWriteFlow: "SUPPORTED", + contractExecution: "SUPPORTED", + }), + chainId: 11155111, + }); + + const rawTx = report.canonicalSupport.find( + (check) => check.checkId === "RAW_TRANSACTION_EXECUTION", + ); + const writeFlow = report.canonicalSupport.find((check) => check.checkId === "ZAMA_WRITE_FLOW"); + expect(rawTx?.state).toBe("UNSUPPORTED"); + expect(writeFlow?.state).toBe("SUPPORTED"); + }); + + it("treats missing non-critical metadata as warnings, not failures", () => { + const report = evaluateAdapterQuality({ + source: "adapter", + metadata: { + name: "Minimal Adapter", + }, + declaredCapabilities: capabilities(), + observedCapabilities: capabilities(), + chainId: 11155111, + }); + + const architectureCheck = report.checks.find((check) => check.id === "METADATA_ARCHITECTURE"); + const verificationCheck = report.checks.find( + (check) => check.id === "METADATA_VERIFICATION_MODEL", + ); + const chainCheck = report.checks.find((check) => check.id === "METADATA_CHAIN_IDS"); + + expect(architectureCheck?.severity).toBe("WARN"); + expect(verificationCheck?.severity).toBe("WARN"); + expect(chainCheck?.severity).toBe("WARN"); + expect(report.checks.some((check) => check.severity === "FAIL")).toBe(false); + expect(adapterQualityExitCode(report)).toBe(0); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/artifactCompatibility.unit.test.ts b/examples/compatibility-harness/src/tests/unit/artifactCompatibility.unit.test.ts new file mode 100644 index 000000000..6c2de2b9a --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/artifactCompatibility.unit.test.ts @@ -0,0 +1,61 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parseReportArtifact } from "../../report/parse.js"; + +const THIS_DIR = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(THIS_DIR, "..", "fixtures", "report-artifacts"); + +function readFixture(name: string): string { + return readFileSync(join(FIXTURES_DIR, name), "utf-8"); +} + +describe("report artifact compatibility contract", () => { + it("accepts current schema fixtures", () => { + expect(() => parseReportArtifact(readFixture("eoa-full-compatible.report.json"))).not.toThrow(); + expect(() => parseReportArtifact(readFixture("turnkey-env-blocked.report.json"))).not.toThrow(); + }); + + it("accepts legacy 1.2 fixtures during transition", () => { + expect(() => parseReportArtifact(readFixture("legacy-schema-1.2.report.json"))).not.toThrow(); + }); + + it("rejects legacy schema versions with explicit error", () => { + expect(() => parseReportArtifact(readFixture("legacy-schema-1.1.report.json"))).toThrow( + 'Unsupported schemaVersion "1.1.0". Supported versions: 1.2.0, 1.3.0.', + ); + }); + + it("rejects malformed checks missing canonical checkId", () => { + expect(() => + parseReportArtifact(readFixture("malformed-missing-check-id.report.json")), + ).toThrow("Invalid report.checks.recorded[0].checkId: expected non-empty string."); + }); + + it("rejects claim requirements that contradict observed statuses", () => { + expect(() => + parseReportArtifact(readFixture("malformed-claim-requirement-mismatch.report.json")), + ).toThrow('requirement "Zama Authorization Flow" not satisfied'); + }); + + it("rejects schema 1.3 artifacts missing confidence/write-depth fields", () => { + const parsed = JSON.parse(readFixture("eoa-full-compatible.report.json")) as { + claim: Record; + zama: Record; + }; + delete parsed.claim.confidence; + expect(() => parseReportArtifact(JSON.stringify(parsed))).toThrow( + "Invalid report.claim.confidence: expected non-empty string.", + ); + + const parsedWithoutDepth = JSON.parse(readFixture("eoa-full-compatible.report.json")) as { + claim: Record; + zama: Record; + }; + delete parsedWithoutDepth.zama.writeValidationDepth; + expect(() => parseReportArtifact(JSON.stringify(parsedWithoutDepth))).toThrow( + "Invalid report.zama.writeValidationDepth: expected non-empty string.", + ); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/checkRegistry.unit.test.ts b/examples/compatibility-harness/src/tests/unit/checkRegistry.unit.test.ts new file mode 100644 index 000000000..52be80e14 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/checkRegistry.unit.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + CHECK_REGISTRY, + assertCanonicalCheck, + checkOrder, + getCanonicalCheckById, + getCanonicalCheckByName, + isCanonicalCheckId, +} from "../../report/check-registry.js"; + +describe("report.check-registry", () => { + it("defines unique ids and names", () => { + const ids = CHECK_REGISTRY.map((check) => check.id); + const names = CHECK_REGISTRY.map((check) => check.name); + expect(new Set(ids).size).toBe(ids.length); + expect(new Set(names).size).toBe(names.length); + }); + + it("keeps dependencies resolvable and ordered before dependent checks", () => { + for (const check of CHECK_REGISTRY) { + for (const dependency of check.dependencies) { + const dependencyDef = getCanonicalCheckById(dependency); + expect(dependencyDef).toBeDefined(); + expect(checkOrder(dependency)).toBeLessThan(checkOrder(check.id)); + } + } + }); + + it("supports lookup by id and name", () => { + for (const check of CHECK_REGISTRY) { + expect(isCanonicalCheckId(check.id)).toBe(true); + expect(getCanonicalCheckById(check.id).name).toBe(check.name); + expect(getCanonicalCheckByName(check.name)?.id).toBe(check.id); + } + }); + + it("rejects mismatched check metadata", () => { + expect(() => + assertCanonicalCheck({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Adapter Initialization", + section: "zama", + }), + ).toThrow("Invalid check name for ZAMA_AUTHORIZATION_FLOW"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/claimConsistency.unit.test.ts b/examples/compatibility-harness/src/tests/unit/claimConsistency.unit.test.ts new file mode 100644 index 000000000..8e863dbc5 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/claimConsistency.unit.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { getCanonicalCheckByName } from "../../report/check-registry.js"; +import type { TestResult } from "../../report/reporter.js"; +import { assertClaimConsistency } from "../../verdict/consistency.js"; +import { resolveClaimFromResults } from "../../verdict/resolve.js"; +import type { ClaimResolution } from "../../verdict/types.js"; + +function check(name: string, status: TestResult["status"]): TestResult { + const canonical = getCanonicalCheckByName(name); + if (!canonical) { + throw new Error(`Unknown canonical check "${name}"`); + } + return { + checkId: canonical.id, + name: canonical.name, + section: canonical.section, + status, + }; +} + +describe("verdict.assertClaimConsistency", () => { + it("accepts a claim resolved from matching observed statuses", () => { + const results = [ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "FAIL"), + ]; + const claim = resolveClaimFromResults(results); + expect(() => assertClaimConsistency(claim, results)).not.toThrow(); + }); + + it("rejects evidence that disagrees with observed statuses", () => { + const results = [ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "FAIL"), + ]; + const claim = resolveClaimFromResults(results); + const inconsistent: ClaimResolution = { + ...claim, + evidence: { + ...claim.evidence, + "Zama Write Flow": "PASS", + }, + }; + expect(() => assertClaimConsistency(inconsistent, results)).toThrow( + 'Claim evidence mismatch for "Zama Write Flow"', + ); + }); + + it("rejects claims whose rule requirements are not satisfied", () => { + const results = [ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "FAIL"), + ]; + const inconsistent: ClaimResolution = { + id: "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + verdictLabel: "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS", + rationale: ["manually forced inconsistent claim"], + evidence: { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "PASS", + }, + }; + expect(() => assertClaimConsistency(inconsistent, results)).toThrow( + 'requirement "Zama Write Flow" not satisfied', + ); + }); + + it("rejects inconsistent evidenceDetails payloads when provided", () => { + const results = [ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "FAIL"), + ]; + const claim = resolveClaimFromResults(results); + const inconsistent: ClaimResolution = { + ...claim, + evidenceDetails: (claim.evidenceDetails ?? []).map((detail) => { + if (detail.check !== "Zama Write Flow") return detail; + return { + ...detail, + reasonCategory: "VALIDATED", + }; + }), + }; + expect(() => assertClaimConsistency(inconsistent, results)).toThrow( + 'evidenceDetails.reasonCategory mismatch for "Zama Write Flow"', + ); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/contradictions.unit.test.ts b/examples/compatibility-harness/src/tests/unit/contradictions.unit.test.ts new file mode 100644 index 000000000..58668e842 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/contradictions.unit.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { detectCapabilityContradictions } from "../../adapter/contradictions.js"; +import { emptyCapabilities } from "../../adapter/types.js"; + +describe("adapter.detectCapabilityContradictions", () => { + it("returns no contradiction when declared and observed states match", () => { + const declared = { + ...emptyCapabilities(), + eip712Signing: "SUPPORTED" as const, + rawTransactionSigning: "UNSUPPORTED" as const, + }; + const observed = { + ...declared, + }; + expect(detectCapabilityContradictions(declared, observed)).toEqual([]); + }); + + it("reports contradictions when declared and observed states diverge", () => { + const declared = { + ...emptyCapabilities(), + eip712Signing: "SUPPORTED" as const, + rawTransactionSigning: "UNSUPPORTED" as const, + }; + const observed = { + ...emptyCapabilities(), + eip712Signing: "UNSUPPORTED" as const, + rawTransactionSigning: "SUPPORTED" as const, + }; + const contradictions = detectCapabilityContradictions(declared, observed); + expect(contradictions.length).toBe(2); + expect(contradictions[0]).toContain("Eip712 Signing"); + expect(contradictions[1]).toContain("Raw Transaction Signing"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/diagnostics.unit.test.ts b/examples/compatibility-harness/src/tests/unit/diagnostics.unit.test.ts new file mode 100644 index 000000000..c3fb83358 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/diagnostics.unit.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { classifyInfrastructureIssue } from "../../harness/diagnostics.js"; + +describe("diagnostics.classifyInfrastructureIssue", () => { + it("classifies missing env variables as BLOCKED/ENVIRONMENT", () => { + const diagnostic = classifyInfrastructureIssue( + "TURNKEY_ORG_ID is not set. Add it to your .env file.", + ); + expect(diagnostic).toEqual({ + status: "BLOCKED", + rootCauseCategory: "ENVIRONMENT", + errorCode: "ENV_MISSING_CONFIG", + }); + }); + + it("classifies insufficient funds as BLOCKED/ENVIRONMENT", () => { + const diagnostic = classifyInfrastructureIssue( + "insufficient funds for gas * price + value: address 0xabc...", + ); + expect(diagnostic).toEqual({ + status: "BLOCKED", + rootCauseCategory: "ENVIRONMENT", + errorCode: "ENV_INSUFFICIENT_FUNDS", + }); + }); + + it("classifies registry failures as BLOCKED/REGISTRY", () => { + const diagnostic = classifyInfrastructureIssue( + "No token pairs found in the registry on Sepolia", + ); + expect(diagnostic).toEqual({ + status: "BLOCKED", + rootCauseCategory: "REGISTRY", + errorCode: "REGISTRY_EMPTY", + }); + }); + + it("classifies relayer failures as INCONCLUSIVE/RELAYER", () => { + const diagnostic = classifyInfrastructureIssue( + "Relayer unavailable: credential could not be created", + ); + expect(diagnostic).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "RELAYER", + errorCode: "RELAYER_UNAVAILABLE", + }); + }); + + it("classifies rpc/network failures as INCONCLUSIVE/RPC", () => { + const diagnostic = classifyInfrastructureIssue("HTTP request failed. Details: fetch failed"); + expect(diagnostic).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "RPC", + errorCode: "RPC_CONNECTIVITY", + }); + }); + + it("classifies rate limits as INCONCLUSIVE/RPC_RATE_LIMIT", () => { + const diagnostic = classifyInfrastructureIssue("RPC 429 rate limit exceeded"); + expect(diagnostic).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "RPC", + errorCode: "RPC_RATE_LIMIT", + }); + }); + + it("falls back to INCONCLUSIVE/HARNESS when no infra signal is found", () => { + const diagnostic = classifyInfrastructureIssue( + "Unexpected error while building local test fixture", + ); + expect(diagnostic).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "HARNESS", + errorCode: "HARNESS_UNKNOWN", + }); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/exampleBaselines.unit.test.ts b/examples/compatibility-harness/src/tests/unit/exampleBaselines.unit.test.ts new file mode 100644 index 000000000..ff30ed3f1 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/exampleBaselines.unit.test.ts @@ -0,0 +1,107 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { describe, expect, it } from "vitest"; +import type { AdapterCapabilities } from "../../adapter/types.js"; +import { resolveValidationGate } from "../../cli/validate-policy.js"; + +type ExampleBaseline = { + id: string; + adapterModule: string; + importEnv: Record; + expectedProfile: { + name: string; + declaredArchitecture: string; + verificationModel: string; + capabilities: AdapterCapabilities; + }; + claimEnvelope: { + PASS: string[]; + PARTIAL: string[]; + INCONCLUSIVE: string[]; + }; +}; + +const THIS_DIR = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(THIS_DIR, "..", "fixtures", "example-baselines"); + +function readBaselines(): ExampleBaseline[] { + return readdirSync(FIXTURES_DIR) + .filter((name) => name.endsWith(".lock.json")) + .map((name) => { + const raw = readFileSync(join(FIXTURES_DIR, name), "utf-8"); + return JSON.parse(raw) as ExampleBaseline; + }); +} + +async function importAdapterWithEnv( + modulePath: string, + envPatch: Record, +): Promise<{ + metadata: { + name: string; + declaredArchitecture?: string; + verificationModel?: string; + }; + capabilities?: Partial; +}> { + const previous = new Map(); + for (const [key, value] of Object.entries(envPatch)) { + previous.set(key, process.env[key]); + process.env[key] = value; + } + try { + const moduleUrl = pathToFileURL(resolve(process.cwd(), modulePath)).href; + const loaded = (await import(moduleUrl)) as { + adapter: { + metadata: { + name: string; + declaredArchitecture?: string; + verificationModel?: string; + }; + capabilities?: Partial; + }; + }; + return loaded.adapter; + } finally { + for (const [key, value] of previous.entries()) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } +} + +describe("example baseline lockfiles", () => { + const baselines = readBaselines(); + + it("keep claim envelopes aligned with gate status categories", () => { + for (const baseline of baselines) { + for (const claimId of baseline.claimEnvelope.PASS) { + expect(resolveValidationGate(claimId, "AUTHORIZATION_AND_WRITE").status).toBe("PASS"); + } + for (const claimId of baseline.claimEnvelope.PARTIAL) { + expect(resolveValidationGate(claimId, "AUTHORIZATION_AND_WRITE").status).toBe("PARTIAL"); + } + for (const claimId of baseline.claimEnvelope.INCONCLUSIVE) { + expect(resolveValidationGate(claimId, "AUTHORIZATION_AND_WRITE").status).toBe( + "INCONCLUSIVE", + ); + } + } + }); + + it("keep example adapter metadata and declared capabilities locked", async () => { + for (const baseline of baselines) { + const adapter = await importAdapterWithEnv(baseline.adapterModule, baseline.importEnv); + expect(adapter.metadata.name).toBe(baseline.expectedProfile.name); + expect(adapter.metadata.declaredArchitecture).toBe( + baseline.expectedProfile.declaredArchitecture, + ); + expect(adapter.metadata.verificationModel).toBe(baseline.expectedProfile.verificationModel); + expect(adapter.capabilities).toEqual(baseline.expectedProfile.capabilities); + } + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/goldenReports.unit.test.ts b/examples/compatibility-harness/src/tests/unit/goldenReports.unit.test.ts new file mode 100644 index 000000000..5b97ac391 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/goldenReports.unit.test.ts @@ -0,0 +1,95 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parseReportArtifact } from "../../report/parse.js"; +import { resolveValidationGate } from "../../cli/validate-policy.js"; + +const THIS_DIR = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(THIS_DIR, "..", "fixtures", "report-artifacts"); + +function readFixture(name: string): string { + return readFileSync(join(FIXTURES_DIR, name), "utf-8"); +} + +describe("golden report fixtures", () => { + it("keeps full-compatibility fixture parseable and gate-pass", () => { + const report = parseReportArtifact(readFixture("eoa-full-compatible.report.json")); + expect(report.claim.id).toBe("ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"); + expect(report.claim.confidence).toBe("HIGH"); + expect(report.zama?.writeValidationDepth).toBe("FULL"); + expect(resolveValidationGate(report.claim.id, "AUTHORIZATION")).toMatchObject({ + status: "PASS", + exitCode: 0, + }); + expect(resolveValidationGate(report.claim.id, "AUTHORIZATION_AND_WRITE")).toMatchObject({ + status: "PASS", + exitCode: 0, + }); + }); + + it("keeps blocked fixture parseable and gate-inconclusive", () => { + const report = parseReportArtifact(readFixture("turnkey-env-blocked.report.json")); + expect(report.claim.id).toBe("INCONCLUSIVE_AUTHORIZATION_BLOCKED"); + expect(report.claim.confidence).toBe("LOW"); + expect(report.zama?.writeValidationDepth).toBe("UNTESTED"); + expect(resolveValidationGate(report.claim.id, "AUTHORIZATION")).toMatchObject({ + status: "INCONCLUSIVE", + exitCode: 30, + }); + }); + + it("rejects malformed artifacts", () => { + expect(() => parseReportArtifact("{}")).toThrow( + "Invalid report.kind: expected non-empty string.", + ); + }); + + it("accepts optional structured evidenceDetails", () => { + const parsed = JSON.parse(readFixture("eoa-full-compatible.report.json")) as { + claim: { evidenceDetails?: unknown[] }; + }; + parsed.claim.evidenceDetails = [ + { + check: "Zama Authorization Flow", + checkId: "ZAMA_AUTHORIZATION_FLOW", + status: "PASS", + reasonCategory: "VALIDATED", + }, + { + check: "EIP-712 Recoverability", + checkId: "EIP712_RECOVERABILITY", + status: "PASS", + reasonCategory: "VALIDATED", + }, + { + check: "Zama Write Flow", + checkId: "ZAMA_WRITE_FLOW", + status: "PASS", + reasonCategory: "VALIDATED", + }, + ]; + const report = parseReportArtifact(JSON.stringify(parsed)); + expect(report.claim.evidenceDetails?.length).toBe(3); + }); + + it("rejects malformed optional evidenceDetails payloads", () => { + const parsed = JSON.parse(readFixture("eoa-full-compatible.report.json")) as { + claim: { evidenceDetails?: unknown }; + }; + parsed.claim.evidenceDetails = "bad-shape"; + expect(() => parseReportArtifact(JSON.stringify(parsed))).toThrow( + "Invalid report.claim.evidenceDetails: expected array when provided.", + ); + }); + + it("rejects artifacts with inconsistent claim evidence", () => { + const parsed = JSON.parse(readFixture("eoa-full-compatible.report.json")) as { + claim: { evidence: Record }; + }; + parsed.claim.evidence["Zama Write Flow"] = "FAIL"; + expect(() => parseReportArtifact(JSON.stringify(parsed))).toThrow( + 'Claim evidence mismatch for "Zama Write Flow"', + ); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/initAdapter.unit.test.ts b/examples/compatibility-harness/src/tests/unit/initAdapter.unit.test.ts new file mode 100644 index 000000000..d2f1f7621 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/initAdapter.unit.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from "vitest"; +import { + importPathForTarget, + normalizeTemplate, + resolveInitAdapterConfig, + resolveOutputPath, + templateFor, +} from "../../cli/init-adapter.js"; + +describe("cli.init-adapter.resolveOutputPath", () => { + it("prefers argv path", () => { + expect(resolveOutputPath(["node", "script", "./custom.ts"], {})).toBe("./custom.ts"); + }); + + it("falls back to env path", () => { + expect(resolveOutputPath(["node", "script"], { ADAPTER_TEMPLATE_PATH: "./from-env.ts" })).toBe( + "./from-env.ts", + ); + }); + + it("falls back to default", () => { + expect(resolveOutputPath(["node", "script"], {})).toBe("./my-adapter.ts"); + }); +}); + +describe("cli.init-adapter.resolveInitAdapterConfig", () => { + it("parses output and template flags", () => { + expect( + resolveInitAdapterConfig(["node", "script", "--template", "mpc", "--output", "./out.ts"], {}), + ).toEqual({ + outputPath: "./out.ts", + template: "mpc", + showHelp: false, + }); + }); + + it("supports positional output with template alias", () => { + expect(resolveInitAdapterConfig(["node", "script", "./x.ts", "-t", "api"], {})).toEqual({ + outputPath: "./x.ts", + template: "api-routed", + showHelp: false, + }); + }); + + it("enables help mode", () => { + expect(resolveInitAdapterConfig(["node", "script", "--help"], {})).toEqual({ + outputPath: "./my-adapter.ts", + template: "generic", + showHelp: true, + }); + }); + + it("throws on unsupported options", () => { + expect(() => resolveInitAdapterConfig(["node", "script", "--invalid"], {})).toThrow( + 'Unsupported option "--invalid". Use --help for usage.', + ); + }); +}); + +describe("cli.init-adapter.normalizeTemplate", () => { + it("supports known template variants", () => { + expect(normalizeTemplate(undefined)).toBe("generic"); + expect(normalizeTemplate("EOA")).toBe("eoa"); + expect(normalizeTemplate("api")).toBe("api-routed"); + expect(normalizeTemplate("api-routed")).toBe("api-routed"); + expect(normalizeTemplate("turnkey")).toBe("turnkey"); + expect(normalizeTemplate("crossmint")).toBe("crossmint"); + expect(normalizeTemplate("openfort")).toBe("openfort"); + }); + + it("throws on unsupported template values", () => { + expect(() => normalizeTemplate("foobar")).toThrow('Unsupported template "foobar".'); + }); +}); + +describe("cli.init-adapter.importPathForTarget", () => { + it("resolves import path from root target", () => { + expect(importPathForTarget("./my-adapter.ts")).toBe("./src/adapter/types.js"); + }); + + it("resolves import path from nested target", () => { + expect(importPathForTarget("./examples/foo/my-adapter.ts")).toBe("../../src/adapter/types.js"); + }); +}); + +describe("cli.init-adapter.templateFor", () => { + it("includes adapter export and inferred import path", () => { + const template = templateFor("./examples/foo/my-adapter.ts"); + expect(template).toContain('import type { Adapter } from "../../src/adapter/types.js";'); + expect(template).toContain("export const adapter: Adapter = {"); + expect(template).toContain("Implement getAddress()"); + }); + + it("renders EOA template with raw transaction signing", () => { + const template = templateFor("./my-adapter.ts", "eoa"); + expect(template).toContain('declaredArchitecture: "EOA"'); + expect(template).toContain('rawTransactionSigning: "SUPPORTED"'); + expect(template).toContain("Implement signTransaction()"); + }); + + it("renders MPC template with raw transaction signing unsupported", () => { + const template = templateFor("./my-adapter.ts", "mpc"); + expect(template).toContain('declaredArchitecture: "MPC"'); + expect(template).toContain('rawTransactionSigning: "UNSUPPORTED"'); + expect(template).toContain("Implement writeContract() via your provider API"); + }); + + it("renders API-routed template with provider-managed verification", () => { + const template = templateFor("./my-adapter.ts", "api-routed"); + expect(template).toContain('declaredArchitecture: "API_ROUTED_EXECUTION"'); + expect(template).toContain('verificationModel: "PROVIDER_MANAGED"'); + expect(template).toContain('eip712Signing: "UNKNOWN"'); + }); + + it("renders Turnkey template with API-routed capabilities", () => { + const template = templateFor("./my-adapter.ts", "turnkey"); + expect(template).toContain('name: "Turnkey API Key Adapter"'); + expect(template).toContain('declaredArchitecture: "API_ROUTED_EXECUTION"'); + expect(template).toContain('rawTransactionSigning: "UNSUPPORTED"'); + expect(template).toContain("Implement signTypedData() via @turnkey/viem account"); + }); + + it("renders Crossmint template with api-routed write flow", () => { + const template = templateFor("./my-adapter.ts", "crossmint"); + expect(template).toContain('name: "Crossmint API-Routed Adapter"'); + expect(template).toContain('contractReads: "UNSUPPORTED"'); + expect(template).toContain("Crossmint transactions API"); + }); + + it("renders Openfort template with EOA recoverability model", () => { + const template = templateFor("./my-adapter.ts", "openfort"); + expect(template).toContain('name: "Openfort EOA Baseline Adapter"'); + expect(template).toContain('declaredArchitecture: "EOA"'); + expect(template).toContain('verificationModel: "RECOVERABLE_ECDSA"'); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/negativeMatrix.unit.test.ts b/examples/compatibility-harness/src/tests/unit/negativeMatrix.unit.test.ts new file mode 100644 index 000000000..c09170763 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/negativeMatrix.unit.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + classifyEip712SigningFailure, + classifyRecoverabilityFailure, + classifyZamaAuthorizationFailure, + classifyZamaWriteSubmissionFailure, +} from "../../harness/negative-paths.js"; + +describe("harness.negative-paths", () => { + it("maps EIP-712 signing implementation failures to FAIL/ADAPTER", () => { + expect(classifyEip712SigningFailure("adapter rejected typed-data payload")).toEqual({ + status: "FAIL", + rootCauseCategory: "ADAPTER", + infrastructure: false, + }); + }); + + it("maps EIP-712 signing environment failures to BLOCKED/ENVIRONMENT", () => { + expect( + classifyEip712SigningFailure( + 'PRIVATE_KEY is invalid (got "abc"). Expected a 0x-prefixed 64-character hex string.', + ), + ).toEqual({ + status: "BLOCKED", + rootCauseCategory: "ENVIRONMENT", + errorCode: "ENV_INVALID_CONFIG", + infrastructure: true, + }); + }); + + it("maps recoverability failures to FAIL/SIGNER", () => { + expect(classifyRecoverabilityFailure()).toEqual({ + status: "FAIL", + rootCauseCategory: "SIGNER", + infrastructure: false, + }); + }); + + it("maps authorization rejections to FAIL/SIGNER", () => { + expect(classifyZamaAuthorizationFailure("authorization signature rejected")).toEqual({ + status: "FAIL", + rootCauseCategory: "SIGNER", + infrastructure: false, + }); + }); + + it("maps authorization infra blockers to INCONCLUSIVE/RELAYER", () => { + expect( + classifyZamaAuthorizationFailure("Relayer unavailable: credential could not be created"), + ).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "RELAYER", + errorCode: "RELAYER_UNAVAILABLE", + infrastructure: true, + }); + }); + + it("maps write submission implementation failures to FAIL/ADAPTER", () => { + expect(classifyZamaWriteSubmissionFailure("writeContract returned malformed payload")).toEqual({ + status: "FAIL", + rootCauseCategory: "ADAPTER", + infrastructure: false, + }); + }); + + it("maps write submission RPC blockers to INCONCLUSIVE/RPC", () => { + expect( + classifyZamaWriteSubmissionFailure("HTTP request failed. Details: fetch failed"), + ).toEqual({ + status: "INCONCLUSIVE", + rootCauseCategory: "RPC", + errorCode: "RPC_CONNECTIVITY", + infrastructure: true, + }); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/networkConfig.unit.test.ts b/examples/compatibility-harness/src/tests/unit/networkConfig.unit.test.ts new file mode 100644 index 000000000..ec0041967 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/networkConfig.unit.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { buildNetworkConfig, parseNetworkProfile } from "../../config/network.js"; + +describe("config.network.parseNetworkProfile", () => { + it("defaults to sepolia", () => { + expect(parseNetworkProfile(undefined)).toBe("sepolia"); + expect(parseNetworkProfile("")).toBe("sepolia"); + }); + + it("accepts known profiles", () => { + expect(parseNetworkProfile("sepolia")).toBe("sepolia"); + expect(parseNetworkProfile("mainnet")).toBe("mainnet"); + }); + + it("rejects unsupported profiles", () => { + expect(() => parseNetworkProfile("hoodi")).toThrow( + 'Invalid NETWORK_PROFILE="hoodi". Expected one of: sepolia, mainnet.', + ); + }); +}); + +describe("config.network.buildNetworkConfig", () => { + it("builds sepolia defaults", () => { + const config = buildNetworkConfig({}); + expect(config.profile).toBe("sepolia"); + expect(config.chainId).toBe(11155111); + expect(config.relayerUrl).toBe("https://relayer.testnet.zama.org/v2"); + expect(config.zamaSupport).toBe("SUPPORTED"); + }); + + it("requires relayer URL on mainnet profile", () => { + expect(() => buildNetworkConfig({ NETWORK_PROFILE: "mainnet" })).toThrow( + "RELAYER_URL is required for NETWORK_PROFILE=mainnet. Provide your relayer endpoint in .env.", + ); + }); + + it("builds mainnet profile when explicit relayer is provided", () => { + const config = buildNetworkConfig({ + NETWORK_PROFILE: "mainnet", + RELAYER_URL: "https://example-relayer.mainnet/v2", + }); + expect(config.profile).toBe("mainnet"); + expect(config.chainId).toBe(1); + expect(config.relayerUrl).toBe("https://example-relayer.mainnet/v2"); + expect(config.zamaSupport).toBe("EXPERIMENTAL"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/profile.unit.test.ts b/examples/compatibility-harness/src/tests/unit/profile.unit.test.ts new file mode 100644 index 000000000..13d5ae807 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/profile.unit.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { detectArchitecture, detectVerificationModel } from "../../adapter/profile.js"; +import { emptyCapabilities, type AdapterCapabilities } from "../../adapter/types.js"; + +function withCapabilities( + patch: Partial, + base: AdapterCapabilities = emptyCapabilities(), +): AdapterCapabilities { + return { + ...base, + ...patch, + }; +} + +describe("adapter.profile.detectArchitecture", () => { + it("keeps declared EOA when recoverability is not disproven", () => { + const detected = detectArchitecture( + "EOA", + withCapabilities({ + recoverableEcdsa: "UNKNOWN", + rawTransactionSigning: "UNSUPPORTED", + }), + ); + expect(detected).toBe("EOA"); + }); + + it("degrades declared EOA to UNKNOWN when capabilities contradict it", () => { + const detected = detectArchitecture( + "EOA", + withCapabilities({ + recoverableEcdsa: "UNSUPPORTED", + rawTransactionSigning: "SUPPORTED", + }), + ); + expect(detected).toBe("UNKNOWN"); + }); + + it("infers EOA from recoverable signatures even without raw transaction support", () => { + const detected = detectArchitecture( + undefined, + withCapabilities({ + recoverableEcdsa: "SUPPORTED", + rawTransactionSigning: "UNSUPPORTED", + }), + ); + expect(detected).toBe("EOA"); + }); + + it("infers API_ROUTED_EXECUTION from execution support without raw signing", () => { + const detected = detectArchitecture( + undefined, + withCapabilities({ + contractExecution: "SUPPORTED", + rawTransactionSigning: "UNSUPPORTED", + recoverableEcdsa: "UNSUPPORTED", + }), + ); + expect(detected).toBe("API_ROUTED_EXECUTION"); + }); +}); + +describe("adapter.profile.detectVerificationModel", () => { + it("degrades contradictory declared recoverable verification to UNKNOWN", () => { + const detected = detectVerificationModel( + "RECOVERABLE_ECDSA", + withCapabilities({ + recoverableEcdsa: "UNSUPPORTED", + }), + ); + expect(detected).toBe("UNKNOWN"); + }); + + it("infers PROVIDER_MANAGED when typed-data signing works but is not recoverable", () => { + const detected = detectVerificationModel( + undefined, + withCapabilities({ + eip712Signing: "SUPPORTED", + recoverableEcdsa: "UNSUPPORTED", + }), + ); + expect(detected).toBe("PROVIDER_MANAGED"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/recommendations.unit.test.ts b/examples/compatibility-harness/src/tests/unit/recommendations.unit.test.ts new file mode 100644 index 000000000..5c29848c8 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/recommendations.unit.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { recommendationForDiagnostic } from "../../harness/recommendations.js"; + +describe("harness.recommendations.recommendationForDiagnostic", () => { + it("returns deterministic recommendation text for known error codes", () => { + expect( + recommendationForDiagnostic({ + status: "BLOCKED", + errorCode: "ENV_MISSING_CONFIG", + rootCauseCategory: "ENVIRONMENT", + }), + ).toBe( + "Set the required environment variables and credentials for your adapter. Next: `npm run doctor`.", + ); + expect( + recommendationForDiagnostic({ + status: "INCONCLUSIVE", + errorCode: "RPC_CONNECTIVITY", + rootCauseCategory: "RPC", + }), + ).toBe( + "Check RPC_URL/network reachability and retry once connectivity is stable. Next: `npm run doctor`.", + ); + }); + + it("falls back to root-cause recommendations when errorCode is absent", () => { + expect( + recommendationForDiagnostic({ + status: "INCONCLUSIVE", + rootCauseCategory: "RELAYER", + }), + ).toBe("Validate relayer endpoint health and authentication settings. Next: `npm run doctor`."); + }); + + it("returns undefined for non-blocking statuses", () => { + expect( + recommendationForDiagnostic({ + status: "FAIL", + errorCode: "RPC_CONNECTIVITY", + rootCauseCategory: "RPC", + }), + ).toBeUndefined(); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/runtime.unit.test.ts b/examples/compatibility-harness/src/tests/unit/runtime.unit.test.ts new file mode 100644 index 000000000..fb9777256 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/runtime.unit.test.ts @@ -0,0 +1,41 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { isMockModeEnabled, mockModeNote } from "../../config/runtime.js"; + +const ORIGINAL_MOCK_MODE = process.env.HARNESS_MOCK_MODE; + +afterEach(() => { + if (ORIGINAL_MOCK_MODE === undefined) { + delete process.env.HARNESS_MOCK_MODE; + return; + } + process.env.HARNESS_MOCK_MODE = ORIGINAL_MOCK_MODE; +}); + +describe("config.runtime.isMockModeEnabled", () => { + it("is disabled by default", () => { + delete process.env.HARNESS_MOCK_MODE; + expect(isMockModeEnabled()).toBe(false); + }); + + it("accepts true-like values", () => { + process.env.HARNESS_MOCK_MODE = "true"; + expect(isMockModeEnabled()).toBe(true); + process.env.HARNESS_MOCK_MODE = "1"; + expect(isMockModeEnabled()).toBe(true); + process.env.HARNESS_MOCK_MODE = "yes"; + expect(isMockModeEnabled()).toBe(true); + process.env.HARNESS_MOCK_MODE = "on"; + expect(isMockModeEnabled()).toBe(true); + }); + + it("rejects other values", () => { + process.env.HARNESS_MOCK_MODE = "false"; + expect(isMockModeEnabled()).toBe(false); + process.env.HARNESS_MOCK_MODE = "0"; + expect(isMockModeEnabled()).toBe(false); + }); + + it("returns a stable explanatory note", () => { + expect(mockModeNote()).toContain("HARNESS_MOCK_MODE"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/runtimeObservation.unit.test.ts b/examples/compatibility-harness/src/tests/unit/runtimeObservation.unit.test.ts new file mode 100644 index 000000000..d439dfdd7 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/runtimeObservation.unit.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { inferRuntimeCapabilityPatchFromCheck } from "../../adapter/runtime-observation.js"; + +describe("adapter.runtime-observation", () => { + it("marks EIP-712 signing as SUPPORTED on PASS", () => { + expect( + inferRuntimeCapabilityPatchFromCheck({ + checkId: "EIP712_SIGNING", + status: "PASS", + }), + ).toEqual({ eip712Signing: "SUPPORTED" }); + }); + + it("marks recoverability as UNSUPPORTED on recoverability FAIL", () => { + expect( + inferRuntimeCapabilityPatchFromCheck({ + checkId: "EIP712_RECOVERABILITY", + status: "FAIL", + }), + ).toEqual({ recoverableEcdsa: "UNSUPPORTED" }); + }); + + it("marks raw transaction capability as SUPPORTED when signing invocation fails", () => { + expect( + inferRuntimeCapabilityPatchFromCheck({ + checkId: "RAW_TRANSACTION_EXECUTION", + status: "FAIL", + }), + ).toEqual({ rawTransactionSigning: "SUPPORTED" }); + }); + + it("marks Zama write surface as UNSUPPORTED when check is UNSUPPORTED", () => { + expect( + inferRuntimeCapabilityPatchFromCheck({ + checkId: "ZAMA_WRITE_FLOW", + status: "UNSUPPORTED", + }), + ).toEqual({ + contractExecution: "UNSUPPORTED", + zamaWriteFlow: "UNSUPPORTED", + }); + }); + + it("returns empty patch for infra summary checks", () => { + expect( + inferRuntimeCapabilityPatchFromCheck({ + checkId: "RPC_CONNECTIVITY", + status: "INCONCLUSIVE", + }), + ).toEqual({}); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/validateConfig.unit.test.ts b/examples/compatibility-harness/src/tests/unit/validateConfig.unit.test.ts new file mode 100644 index 000000000..08614a040 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/validateConfig.unit.test.ts @@ -0,0 +1,84 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { describe, expect, it } from "vitest"; +import { resolveValidationConfig } from "../../cli/validate-config.js"; + +function makePolicyFile(content: unknown): { dir: string; path: string } { + const dir = mkdtempSync(join(tmpdir(), "zama-validate-policy-")); + const path = join(dir, "policy.json"); + writeFileSync(path, JSON.stringify(content, null, 2)); + return { dir, path }; +} + +describe("cli.validate-config.resolveValidationConfig", () => { + it("uses defaults without policy file", () => { + const config = resolveValidationConfig({}); + expect(config).toEqual({ + target: "AUTHORIZATION", + policy: { + allowPartial: false, + expectedClaims: [], + }, + }); + }); + + it("loads target and claim constraints from policy file", () => { + const { dir, path } = makePolicyFile({ + target: "AUTHORIZATION_AND_WRITE", + expectedClaims: ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + }); + try { + const config = resolveValidationConfig({ + VALIDATION_POLICY_PATH: path, + }); + expect(config).toEqual({ + target: "AUTHORIZATION_AND_WRITE", + policy: { + allowPartial: false, + expectedClaims: ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + }, + policyPath: path, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("allows env to override policy target and allowPartial", () => { + const { dir, path } = makePolicyFile({ + target: "AUTHORIZATION_AND_WRITE", + allowPartial: false, + }); + try { + const config = resolveValidationConfig({ + VALIDATION_POLICY_PATH: path, + VALIDATION_TARGET: "AUTHORIZATION", + VALIDATION_ALLOW_PARTIAL: "true", + }); + expect(config).toEqual({ + target: "AUTHORIZATION", + policy: { + allowPartial: true, + expectedClaims: [], + }, + policyPath: path, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("throws when policy expectedClaims is malformed", () => { + const { dir, path } = makePolicyFile({ + expectedClaims: ["OK", 1], + }); + try { + expect(() => resolveValidationConfig({ VALIDATION_POLICY_PATH: path })).toThrow( + "Validation policy expectedClaims must contain only non-empty strings.", + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/validatePolicy.unit.test.ts b/examples/compatibility-harness/src/tests/unit/validatePolicy.unit.test.ts new file mode 100644 index 000000000..a6ed0c08a --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/validatePolicy.unit.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "vitest"; +import { + applyValidationPolicy, + parseValidationTarget, + resolveValidationGate, +} from "../../cli/validate-policy.js"; + +describe("cli.validate-policy.parseValidationTarget", () => { + it("defaults to AUTHORIZATION", () => { + expect(parseValidationTarget(undefined)).toBe("AUTHORIZATION"); + }); + + it("parses AUTHORIZATION_AND_WRITE", () => { + expect(parseValidationTarget("authorization_and_write")).toBe("AUTHORIZATION_AND_WRITE"); + }); + + it("throws on invalid target", () => { + expect(() => parseValidationTarget("all")).toThrow( + 'Invalid VALIDATION_TARGET="all". Expected AUTHORIZATION or AUTHORIZATION_AND_WRITE.', + ); + }); +}); + +describe("cli.validate-policy.resolveValidationGate", () => { + it("returns PASS for full compatibility on strict target", () => { + expect( + resolveValidationGate("ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", "AUTHORIZATION_AND_WRITE"), + ).toEqual({ + target: "AUTHORIZATION_AND_WRITE", + status: "PASS", + exitCode: 0, + summary: "Authorization and write compatibility validated.", + }); + }); + + it("returns PASS for auth-compatible partial claims on auth-only target", () => { + expect( + resolveValidationGate("PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", "AUTHORIZATION"), + ).toEqual({ + target: "AUTHORIZATION", + status: "PASS", + exitCode: 0, + summary: "Authorization compatibility validated for requested scope.", + }); + }); + + it("returns PARTIAL on strict target when write is not fully validated", () => { + expect( + resolveValidationGate( + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + "AUTHORIZATION_AND_WRITE", + ), + ).toEqual({ + target: "AUTHORIZATION_AND_WRITE", + status: "PARTIAL", + exitCode: 10, + summary: "Authorization validated, but write compatibility is only partially validated.", + }); + }); + + it("returns FAIL for incompatible claims", () => { + expect(resolveValidationGate("INCOMPATIBLE_AUTHORIZATION_FAILED", "AUTHORIZATION")).toEqual({ + target: "AUTHORIZATION", + status: "FAIL", + exitCode: 20, + summary: "Authorization compatibility failed.", + }); + }); + + it("returns INCONCLUSIVE for blocked claims", () => { + expect(resolveValidationGate("INCONCLUSIVE_AUTHORIZATION_BLOCKED", "AUTHORIZATION")).toEqual({ + target: "AUTHORIZATION", + status: "INCONCLUSIVE", + exitCode: 30, + summary: "Authorization compatibility is inconclusive.", + }); + }); + + it("treats recoverability-unconfirmed claims as INCONCLUSIVE", () => { + expect( + resolveValidationGate("PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED", "AUTHORIZATION"), + ).toEqual({ + target: "AUTHORIZATION", + status: "INCONCLUSIVE", + exitCode: 30, + summary: "Authorization compatibility is inconclusive.", + }); + }); + + it("returns unknown-claim INCONCLUSIVE fallback", () => { + expect(resolveValidationGate("SOMETHING_NEW", "AUTHORIZATION")).toEqual({ + target: "AUTHORIZATION", + status: "INCONCLUSIVE", + exitCode: 31, + summary: "Unknown claim. Compatibility gate is inconclusive.", + }); + }); +}); + +describe("cli.validate-policy.applyValidationPolicy", () => { + it("keeps base decision when no extra constraints are configured", () => { + const base = resolveValidationGate( + "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "AUTHORIZATION_AND_WRITE", + ); + expect( + applyValidationPolicy(base, "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", { + allowPartial: false, + expectedClaims: [], + }), + ).toEqual({ + status: "PASS", + exitCode: 0, + summary: "Authorization and write compatibility validated.", + }); + }); + + it("fails when claim is not allowed by expectedClaims policy", () => { + const base = resolveValidationGate("INCONCLUSIVE_AUTHORIZATION_BLOCKED", "AUTHORIZATION"); + expect( + applyValidationPolicy(base, "INCONCLUSIVE_AUTHORIZATION_BLOCKED", { + allowPartial: false, + expectedClaims: ["ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"], + }), + ).toEqual({ + status: "FAIL", + exitCode: 21, + summary: 'Claim "INCONCLUSIVE_AUTHORIZATION_BLOCKED" is not allowed by policy.', + note: "Allowed claims: ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + }); + }); + + it("promotes partial to pass when allowPartial is enabled", () => { + const base = resolveValidationGate( + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + "AUTHORIZATION_AND_WRITE", + ); + expect( + applyValidationPolicy(base, "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", { + allowPartial: true, + expectedClaims: [], + }), + ).toEqual({ + status: "PASS", + exitCode: 0, + summary: "Partial validation accepted by policy.", + note: "allowPartial=true", + }); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/verdict.unit.test.ts b/examples/compatibility-harness/src/tests/unit/verdict.unit.test.ts new file mode 100644 index 000000000..cf246ed2d --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/verdict.unit.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import type { TestResult } from "../../report/reporter.js"; +import { getCanonicalCheckByName } from "../../report/check-registry.js"; +import { resolveClaimFromResults } from "../../verdict/resolve.js"; + +function check(name: string, status: TestResult["status"]): TestResult { + const canonical = getCanonicalCheckByName(name); + if (!canonical) { + throw new Error(`Unknown canonical check "${name}"`); + } + return { checkId: canonical.id, name: canonical.name, section: canonical.section, status }; +} + +describe("verdict.resolveClaimFromResults", () => { + it("returns incompatible when authorization fails", () => { + const claim = resolveClaimFromResults([check("Zama Authorization Flow", "FAIL")]); + expect(claim.id).toBe("INCOMPATIBLE_AUTHORIZATION_FAILED"); + }); + + it("returns inconclusive when authorization is blocked", () => { + const claim = resolveClaimFromResults([check("Zama Authorization Flow", "BLOCKED")]); + expect(claim.id).toBe("INCONCLUSIVE_AUTHORIZATION_BLOCKED"); + }); + + it("returns partial when authorization check is missing", () => { + const claim = resolveClaimFromResults([check("Zama Write Flow", "PASS")]); + expect(claim.id).toBe("PARTIAL_AUTHORIZATION_CHECK_MISSING"); + }); + + it("returns incompatible when recoverability fails after auth pass", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "FAIL"), + ]); + expect(claim.id).toBe("INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY"); + }); + + it("requires recoverability pass before full compatibility claims", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "UNTESTED"), + check("Zama Write Flow", "PASS"), + ]); + expect(claim.id).toBe("PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED"); + }); + + it("returns full compatibility when auth + recoverability + write pass", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "PASS"), + ]); + expect(claim.id).toBe("ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE"); + expect(claim.evidenceDetails).toEqual([ + { + check: "Zama Authorization Flow", + checkId: "ZAMA_AUTHORIZATION_FLOW", + status: "PASS", + reasonCategory: "VALIDATED", + }, + { + check: "EIP-712 Recoverability", + checkId: "EIP712_RECOVERABILITY", + status: "PASS", + reasonCategory: "VALIDATED", + }, + { + check: "Zama Write Flow", + checkId: "ZAMA_WRITE_FLOW", + status: "PASS", + reasonCategory: "VALIDATED", + }, + ]); + }); + + it("returns partial write blocked when write is inconclusive", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "INCONCLUSIVE"), + ]); + expect(claim.id).toBe("PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED"); + }); + + it("returns partial write failed when write submission fails", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + check("Zama Write Flow", "FAIL"), + ]); + expect(claim.id).toBe("PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED"); + }); + + it("returns scoped authorization-compatible claim when write is missing", () => { + const claim = resolveClaimFromResults([ + check("Zama Authorization Flow", "PASS"), + check("EIP-712 Recoverability", "PASS"), + ]); + expect(claim.id).toBe("ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/verdictConfidence.unit.test.ts b/examples/compatibility-harness/src/tests/unit/verdictConfidence.unit.test.ts new file mode 100644 index 000000000..41ccf17dc --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/verdictConfidence.unit.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { resolveClaimConfidence } from "../../verdict/confidence.js"; + +describe("verdict.resolveClaimConfidence", () => { + it("returns HIGH for fully validated auth+recoverability+write without blockers", () => { + expect( + resolveClaimConfidence({ + evidence: { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "PASS", + }, + writeValidationDepth: "FULL", + blockerCount: 0, + }), + ).toBe("HIGH"); + }); + + it("returns MEDIUM for auth-compatible claims when write is not full", () => { + expect( + resolveClaimConfidence({ + evidence: { + "Zama Authorization Flow": "PASS", + "EIP-712 Recoverability": "PASS", + "Zama Write Flow": "BLOCKED", + }, + writeValidationDepth: "PARTIAL", + blockerCount: 1, + }), + ).toBe("MEDIUM"); + }); + + it("returns HIGH for clear incompatibility failures", () => { + expect( + resolveClaimConfidence({ + evidence: { + "Zama Authorization Flow": "FAIL", + }, + writeValidationDepth: "UNTESTED", + blockerCount: 0, + }), + ).toBe("HIGH"); + }); + + it("returns LOW when authorization evidence is inconclusive", () => { + expect( + resolveClaimConfidence({ + evidence: { + "Zama Authorization Flow": "INCONCLUSIVE", + }, + writeValidationDepth: "UNTESTED", + blockerCount: 2, + }), + ).toBe("LOW"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/unit/writeValidationDepth.unit.test.ts b/examples/compatibility-harness/src/tests/unit/writeValidationDepth.unit.test.ts new file mode 100644 index 000000000..136a967b9 --- /dev/null +++ b/examples/compatibility-harness/src/tests/unit/writeValidationDepth.unit.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { deriveWriteValidationDepth } from "../../report/reporter.js"; + +describe("report.deriveWriteValidationDepth", () => { + it("returns FULL when submission and state verification both succeeded", () => { + expect( + deriveWriteValidationDepth({ + zamaWriteStatus: "PASS", + observation: { + submissionAttempted: true, + submissionSucceeded: true, + receiptObserved: true, + stateVerified: true, + }, + }), + ).toBe("FULL"); + }); + + it("returns PARTIAL when submission was attempted but not fully verified", () => { + expect( + deriveWriteValidationDepth({ + zamaWriteStatus: "INCONCLUSIVE", + observation: { + submissionAttempted: true, + submissionSucceeded: false, + receiptObserved: false, + stateVerified: false, + }, + }), + ).toBe("PARTIAL"); + }); + + it("returns UNTESTED when write flow did not execute", () => { + expect( + deriveWriteValidationDepth({ + zamaWriteStatus: "UNTESTED", + observation: null, + }), + ).toBe("UNTESTED"); + }); +}); diff --git a/examples/compatibility-harness/src/tests/zamaFlow.test.ts b/examples/compatibility-harness/src/tests/zamaFlow.test.ts new file mode 100644 index 000000000..fefd54cbd --- /dev/null +++ b/examples/compatibility-harness/src/tests/zamaFlow.test.ts @@ -0,0 +1,125 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { isMockModeEnabled, mockModeNote } from "../config/runtime.js"; +import { recordWithRuntimeObservation } from "../report/reporter.js"; +import { buildSdk, discoverTokenAddress, initializeAdapter } from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { classifyZamaAuthorizationFailure } from "../harness/negative-paths.js"; +import { adapter } from "../harness/adapter.js"; + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Zama Authorization Flow", () => { + it("validates sdk.allow() when EIP-712 signing is supported", async () => { + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: diagnostic.status, + summary: "Adapter initialization failed before Zama authorization validation", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Resolve adapter initialization issues first.", + }); + return; + } + + if (!adapter.signTypedData) { + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: "UNSUPPORTED", + summary: "Authorization flow cannot be validated without typed-data signing", + reason: "signTypedData is not implemented by the adapter", + rootCauseCategory: "ADAPTER", + recommendation: "Implement signTypedData to validate Zama authorization compatibility.", + }); + return; + } + + if (isMockModeEnabled()) { + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: "UNTESTED", + summary: "Zama authorization validation skipped in mock mode", + reason: mockModeNote(), + rootCauseCategory: "HARNESS", + recommendation: + "Disable HARNESS_MOCK_MODE to validate relayer-backed authorization behavior.", + }); + return; + } + + const sdk = buildSdk(); + let tokenAddress; + try { + tokenAddress = await discoverTokenAddress(sdk); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: diagnostic.status, + summary: "Token discovery blocked authorization validation", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Ensure RPC and registry access are working on Sepolia.", + }); + sdk.terminate(); + return; + } + + try { + await sdk.allow(tokenAddress); + } catch (err) { + const message = errorMessage(err); + const failure = classifyZamaAuthorizationFailure(message); + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: failure.status, + summary: failure.infrastructure + ? "Infrastructure blocked Zama authorization validation" + : "sdk.allow() rejected the adapter signature or identity", + reason: message, + rootCauseCategory: failure.rootCauseCategory, + errorCode: failure.errorCode, + recommendation: failure.infrastructure + ? "Check environment, RPC, relayer, and registry connectivity before retrying." + : "Ensure the adapter produces standard Zama-acceptable EIP-712 signatures.", + }); + sdk.terminate(); + if (!failure.infrastructure) { + expect.fail(message); + } + return; + } + + sdk.terminate(); + recordWithRuntimeObservation({ + checkId: "ZAMA_AUTHORIZATION_FLOW", + name: "Zama Authorization Flow", + section: "zama", + status: "PASS", + summary: "sdk.allow() completed successfully", + }); + expect(true).toBe(true); + }); +}); diff --git a/examples/compatibility-harness/src/tests/zamaWriteFlow.test.ts b/examples/compatibility-harness/src/tests/zamaWriteFlow.test.ts new file mode 100644 index 000000000..dcf40f1a1 --- /dev/null +++ b/examples/compatibility-harness/src/tests/zamaWriteFlow.test.ts @@ -0,0 +1,189 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { getAddress } from "viem"; +import { isMockModeEnabled, mockModeNote } from "../config/runtime.js"; +import { + adapter, + buildSdk, + discoverTokenAddress, + executeZamaWriteProbe, + initializeAdapter, + verifyZamaOperatorApproval, +} from "../harness/adapter.js"; +import { classifyInfrastructureIssue, errorMessage } from "../harness/diagnostics.js"; +import { classifyZamaWriteSubmissionFailure } from "../harness/negative-paths.js"; +import { recordWithRuntimeObservation, recordZamaWriteObservation } from "../report/reporter.js"; + +const TEST_OPERATOR = getAddress("0x000000000000000000000000000000000000dEaD"); + +let initError: string | null = null; + +beforeAll(async () => { + try { + await initializeAdapter(); + } catch (err) { + initError = errorMessage(err); + } +}); + +describe("Zama Write Flow", () => { + it("executes and verifies a Zama operator approval write when supported", async () => { + if (initError) { + const diagnostic = classifyInfrastructureIssue(initError); + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: diagnostic.status, + summary: "Adapter initialization failed before write-flow validation", + reason: initError, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Resolve adapter initialization issues first.", + }); + return; + } + + if (!adapter.writeContract) { + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: "UNSUPPORTED", + summary: "Adapter does not expose contract execution", + reason: "writeContract is not implemented by the adapter", + rootCauseCategory: "ADAPTER", + recommendation: + "Use an adapter that can route contract execution to validate the write surface.", + }); + return; + } + + if (isMockModeEnabled()) { + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: "UNTESTED", + summary: "Zama write-flow validation skipped in mock mode", + reason: mockModeNote(), + rootCauseCategory: "HARNESS", + recommendation: "Disable HARNESS_MOCK_MODE to validate write execution and verification.", + }); + return; + } + + const sdk = buildSdk(); + let tokenAddress; + try { + tokenAddress = await discoverTokenAddress(sdk); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: diagnostic.status, + summary: "Token discovery blocked Zama write validation", + reason: message, + rootCauseCategory: diagnostic.rootCauseCategory, + errorCode: diagnostic.errorCode, + recommendation: "Ensure RPC and registry access are working on Sepolia.", + }); + sdk.terminate(); + return; + } + + let txHash: `0x${string}`; + try { + recordZamaWriteObservation({ + submissionAttempted: true, + }); + txHash = await executeZamaWriteProbe(tokenAddress, TEST_OPERATOR); + recordZamaWriteObservation({ + submissionSucceeded: true, + }); + } catch (err) { + const message = errorMessage(err); + const failure = classifyZamaWriteSubmissionFailure(message); + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: failure.status, + summary: failure.infrastructure + ? "Write-flow validation was blocked by infrastructure" + : "Adapter failed to submit a Zama write transaction", + reason: message, + rootCauseCategory: failure.rootCauseCategory, + errorCode: failure.errorCode, + recommendation: failure.infrastructure + ? "Fix environment, RPC, relayer, or registry prerequisites and retry." + : "Verify adapter contract execution and Zama contract routing.", + }); + sdk.terminate(); + if (!failure.infrastructure) { + expect.fail(message); + } + return; + } + + try { + const receipt = adapter.waitForTransactionReceipt + ? await adapter.waitForTransactionReceipt(txHash) + : null; + if (receipt && receipt.status !== "success") { + throw new Error(`Transaction receipt status was ${String(receipt.status)}`); + } + recordZamaWriteObservation({ + receiptObserved: adapter.waitForTransactionReceipt ? true : false, + }); + const approved = await verifyZamaOperatorApproval(tokenAddress, TEST_OPERATOR); + if (!approved) { + throw new Error("On-chain operator approval was not observed after the write"); + } + recordZamaWriteObservation({ + stateVerified: true, + }); + } catch (err) { + const message = errorMessage(err); + const diagnostic = classifyInfrastructureIssue(message); + const isInfra = diagnostic.rootCauseCategory !== "HARNESS"; + recordWithRuntimeObservation({ + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: isInfra ? diagnostic.status : "FAIL", + summary: isInfra + ? "The write was submitted but infrastructure blocked verification" + : "The write was submitted but resulting Zama state could not be verified", + reason: message, + rootCauseCategory: isInfra ? diagnostic.rootCauseCategory : "SIGNER", + errorCode: isInfra ? diagnostic.errorCode : undefined, + recommendation: isInfra + ? "Check RPC/read dependencies and retry verification." + : "Verify receipt tracking and on-chain state verification.", + }); + sdk.terminate(); + if (!isInfra) { + expect.fail(message); + } + return; + } + + sdk.terminate(); + recordWithRuntimeObservation( + { + checkId: "ZAMA_WRITE_FLOW", + name: "Zama Write Flow", + section: "zama", + status: "PASS", + summary: "A Zama operator approval transaction was executed and verified on-chain", + }, + { + transactionReceiptTracking: adapter.waitForTransactionReceipt ? "SUPPORTED" : "UNKNOWN", + }, + ); + expect(true).toBe(true); + }); +}); diff --git a/examples/compatibility-harness/src/utils/crypto.ts b/examples/compatibility-harness/src/utils/crypto.ts new file mode 100644 index 000000000..4b9219ae8 --- /dev/null +++ b/examples/compatibility-harness/src/utils/crypto.ts @@ -0,0 +1,26 @@ +import { recoverTypedDataAddress } from "viem"; + +/** + * Recover the signer address from an EIP-712 signature. + * + * Returns the recovered address (checksummed), or null if the signature is + * malformed / cannot be recovered (e.g. non-standard MPC or ERC-1271 format). + */ +export async function recoverEIP712Signer( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typedData: { domain: any; types: any; primaryType: string; message: any }, + signature: string, +): Promise { + try { + const recovered = await recoverTypedDataAddress({ + domain: typedData.domain, + types: typedData.types, + primaryType: typedData.primaryType, + message: typedData.message, + signature: signature as `0x${string}`, + }); + return recovered; + } catch { + return null; + } +} diff --git a/examples/compatibility-harness/src/utils/rpc.ts b/examples/compatibility-harness/src/utils/rpc.ts new file mode 100644 index 000000000..cad4b81fb --- /dev/null +++ b/examples/compatibility-harness/src/utils/rpc.ts @@ -0,0 +1,11 @@ +import { createPublicClient, http } from "viem"; +import { networkConfig } from "../config/network.js"; + +/** + * Shared viem public client for read-only RPC calls. + * Used by tests and by the internal GenericSigner adapter in zamaFlow.test.ts. + */ +export const publicClient = createPublicClient({ + chain: networkConfig.chain, + transport: http(networkConfig.rpcUrl), +}); diff --git a/examples/compatibility-harness/src/verdict/claims.ts b/examples/compatibility-harness/src/verdict/claims.ts new file mode 100644 index 000000000..97b57a833 --- /dev/null +++ b/examples/compatibility-harness/src/verdict/claims.ts @@ -0,0 +1,123 @@ +import type { ClaimRule } from "./types.js"; + +export const CLAIM_RULES: ClaimRule[] = [ + { + id: "INCOMPATIBLE_AUTHORIZATION_FAILED", + verdictLabel: "INCOMPATIBLE — ZAMA AUTHORIZATION FLOW FAILED", + requirements: [{ check: "Zama Authorization Flow", oneOf: ["FAIL"] }], + rationale: ["`sdk.allow()` execution failed with a compatibility-level error."], + }, + { + id: "INCOMPATIBLE_AUTHORIZATION_UNSUPPORTED", + verdictLabel: "INCOMPATIBLE — ADAPTER DOES NOT SUPPORT ZAMA AUTHORIZATION", + requirements: [{ check: "Zama Authorization Flow", oneOf: ["UNSUPPORTED"] }], + rationale: ["Adapter cannot perform the authorization primitive required by Zama SDK."], + }, + { + id: "INCONCLUSIVE_AUTHORIZATION_BLOCKED", + verdictLabel: "INCONCLUSIVE — AUTHORIZATION FLOW BLOCKED BY ENVIRONMENT OR INFRASTRUCTURE", + requirements: [{ check: "Zama Authorization Flow", oneOf: ["BLOCKED", "INCONCLUSIVE"] }], + rationale: [ + "Authorization validation was blocked by environment or infrastructure conditions.", + ], + }, + { + id: "INCONCLUSIVE_AUTHORIZATION_UNTESTED", + verdictLabel: "INCONCLUSIVE — AUTHORIZATION FLOW NOT TESTED", + requirements: [{ check: "Zama Authorization Flow", oneOf: ["UNTESTED"] }], + rationale: ["Authorization flow check was explicitly untested in this run."], + }, + { + id: "PARTIAL_AUTHORIZATION_CHECK_MISSING", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION CHECK NOT RECORDED", + requirements: [{ check: "Zama Authorization Flow", oneOf: ["MISSING"] }], + rationale: ["Authorization flow result is missing from the executed checks."], + }, + { + id: "INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY", + verdictLabel: "INCOMPATIBLE — AUTHORIZATION RECOVERABILITY FAILED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["FAIL"] }, + ], + rationale: [ + "Authorization passed but EIP-712 recoverability failed, which invalidates the claim.", + ], + }, + { + id: "PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION PASSED, RECOVERABILITY NOT CONFIRMED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { + check: "EIP-712 Recoverability", + oneOf: ["MISSING", "UNTESTED", "UNSUPPORTED", "BLOCKED", "INCONCLUSIVE"], + }, + ], + rationale: [ + "Authorization passed but signer recoverability was not confirmed with a PASS outcome.", + ], + }, + { + id: "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + verdictLabel: "ZAMA COMPATIBLE FOR AUTHORIZATION AND WRITE FLOWS", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["PASS"] }, + ], + rationale: ["Authorization, recoverability, and write-path probe all passed."], + }, + { + id: "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW UNSUPPORTED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["UNSUPPORTED"] }, + ], + rationale: [ + "Authorization surface is compatible; adapter does not expose write validation surface.", + ], + }, + { + id: "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW UNTESTED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["UNTESTED"] }, + ], + rationale: ["Authorization surface is compatible; write flow was intentionally untested."], + }, + { + id: "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW BLOCKED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["BLOCKED", "INCONCLUSIVE"] }, + ], + rationale: ["Authorization surface is compatible; write validation was blocked by infra/env."], + }, + { + id: "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION COMPATIBLE, WRITE FLOW FAILED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["FAIL"] }, + ], + rationale: ["Authorization surface is compatible but the write-flow probe failed."], + }, + { + id: "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED", + verdictLabel: "ZAMA COMPATIBLE FOR AUTHORIZATION FLOWS — WRITE FLOW NOT TESTED", + requirements: [ + { check: "Zama Authorization Flow", oneOf: ["PASS"] }, + { check: "EIP-712 Recoverability", oneOf: ["PASS"] }, + { check: "Zama Write Flow", oneOf: ["MISSING"] }, + ], + rationale: ["Authorization surface is compatible and no write result was recorded."], + }, +]; diff --git a/examples/compatibility-harness/src/verdict/confidence.ts b/examples/compatibility-harness/src/verdict/confidence.ts new file mode 100644 index 000000000..1d866ee57 --- /dev/null +++ b/examples/compatibility-harness/src/verdict/confidence.ts @@ -0,0 +1,35 @@ +import type { CheckStatusOrMissing, ClaimResolution, VerdictConfidence } from "./types.js"; +import type { WriteValidationDepth } from "../report/schema.js"; + +function status( + evidence: ClaimResolution["evidence"], + key: "Zama Authorization Flow" | "EIP-712 Recoverability", +): CheckStatusOrMissing { + return evidence[key] ?? "MISSING"; +} + +export function resolveClaimConfidence(input: { + evidence: ClaimResolution["evidence"]; + writeValidationDepth: WriteValidationDepth; + blockerCount: number; +}): VerdictConfidence { + const authStatus = status(input.evidence, "Zama Authorization Flow"); + const recoverabilityStatus = status(input.evidence, "EIP-712 Recoverability"); + + if (authStatus === "FAIL" || authStatus === "UNSUPPORTED") { + return "HIGH"; + } + + if (authStatus === "PASS" && recoverabilityStatus === "FAIL") { + return "HIGH"; + } + + if (authStatus === "PASS" && recoverabilityStatus === "PASS") { + if (input.writeValidationDepth === "FULL" && input.blockerCount === 0) { + return "HIGH"; + } + return "MEDIUM"; + } + + return "LOW"; +} diff --git a/examples/compatibility-harness/src/verdict/consistency.ts b/examples/compatibility-harness/src/verdict/consistency.ts new file mode 100644 index 000000000..e31df083a --- /dev/null +++ b/examples/compatibility-harness/src/verdict/consistency.ts @@ -0,0 +1,169 @@ +import type { ValidationStatus } from "../adapter/types.js"; +import type { CanonicalCheckId } from "../report/check-registry.js"; +import { CLAIM_RULES } from "./claims.js"; +import type { + CanonicalCheckName, + CheckStatusOrMissing, + ClaimEvidenceReasonCategory, + ClaimResolution, +} from "./types.js"; + +export interface ClaimCheckStatus { + name: string; + status: ValidationStatus; +} + +const CLAIM_CHECK_NAMES: readonly CanonicalCheckName[] = [ + "Zama Authorization Flow", + "EIP-712 Recoverability", + "Zama Write Flow", +]; + +const CLAIM_CHECK_IDS: Record = { + "Zama Authorization Flow": "ZAMA_AUTHORIZATION_FLOW", + "EIP-712 Recoverability": "EIP712_RECOVERABILITY", + "Zama Write Flow": "ZAMA_WRITE_FLOW", +}; + +function isCanonicalClaimCheckName(value: string): value is CanonicalCheckName { + return CLAIM_CHECK_NAMES.includes(value as CanonicalCheckName); +} + +function isCheckStatusOrMissing(value: string): value is CheckStatusOrMissing { + return ( + value === "PASS" || + value === "FAIL" || + value === "UNTESTED" || + value === "UNSUPPORTED" || + value === "BLOCKED" || + value === "INCONCLUSIVE" || + value === "MISSING" + ); +} + +function checkStatus( + checks: Map, + check: CanonicalCheckName, +): CheckStatusOrMissing { + return checks.get(check) ?? "MISSING"; +} + +export function claimCheckId(name: CanonicalCheckName): CanonicalCheckId { + return CLAIM_CHECK_IDS[name]; +} + +export function evidenceReasonCategory(status: CheckStatusOrMissing): ClaimEvidenceReasonCategory { + switch (status) { + case "PASS": + return "VALIDATED"; + case "FAIL": + return "COMPATIBILITY_FAILURE"; + case "UNSUPPORTED": + return "UNSUPPORTED_SURFACE"; + case "UNTESTED": + return "NOT_EXECUTED"; + case "BLOCKED": + case "INCONCLUSIVE": + return "INFRA_OR_ENV_BLOCKER"; + case "MISSING": + return "MISSING_EVIDENCE"; + } +} + +export function assertClaimConsistency( + claim: ClaimResolution, + observedChecks: ClaimCheckStatus[], +): void { + const rule = CLAIM_RULES.find((candidate) => candidate.id === claim.id); + if (!rule) { + throw new Error(`Unknown claim id "${claim.id}" in report artifact.`); + } + + if (claim.verdictLabel !== rule.verdictLabel) { + throw new Error( + `Claim verdict label mismatch for ${claim.id}: expected "${rule.verdictLabel}", got "${claim.verdictLabel}".`, + ); + } + + const statuses = new Map(); + for (const check of observedChecks) { + if (!isCanonicalClaimCheckName(check.name)) continue; + const existing = statuses.get(check.name); + if (existing && existing !== check.status) { + throw new Error( + `Claim check "${check.name}" has conflicting statuses (${existing}, ${check.status}).`, + ); + } + statuses.set(check.name, check.status); + } + + for (const [key, value] of Object.entries(claim.evidence)) { + if (!isCanonicalClaimCheckName(key)) { + throw new Error(`Unexpected claim evidence key "${key}".`); + } + if (!isCheckStatusOrMissing(String(value))) { + throw new Error( + `Claim evidence for "${key}" has invalid status "${String(value)}" in claim ${claim.id}.`, + ); + } + } + + for (const requirement of rule.requirements) { + const status = checkStatus(statuses, requirement.check); + if (!requirement.oneOf.includes(status)) { + throw new Error( + `Claim ${claim.id} requirement "${requirement.check}" not satisfied by observed status "${status}".`, + ); + } + + const evidence = claim.evidence[requirement.check]; + if (!evidence) { + throw new Error(`Missing claim evidence for "${requirement.check}" in claim ${claim.id}.`); + } + if (evidence !== status) { + throw new Error( + `Claim evidence mismatch for "${requirement.check}": expected "${status}", got "${evidence}".`, + ); + } + } + + if (!claim.evidenceDetails) return; + + const detailsByCheck = new Map(); + for (const detail of claim.evidenceDetails) { + if (!isCanonicalClaimCheckName(detail.check)) { + throw new Error(`Unexpected evidenceDetails.check "${detail.check}" in claim ${claim.id}.`); + } + if (detail.checkId !== claimCheckId(detail.check)) { + throw new Error( + `evidenceDetails.checkId mismatch for "${detail.check}": expected "${claimCheckId(detail.check)}", got "${detail.checkId}".`, + ); + } + const expectedCategory = evidenceReasonCategory(detail.status); + if (detail.reasonCategory !== expectedCategory) { + throw new Error( + `evidenceDetails.reasonCategory mismatch for "${detail.check}": expected "${expectedCategory}", got "${detail.reasonCategory}".`, + ); + } + const evidenceStatus = claim.evidence[detail.check]; + if (evidenceStatus && evidenceStatus !== detail.status) { + throw new Error( + `evidenceDetails.status mismatch for "${detail.check}": evidence has "${evidenceStatus}", details has "${detail.status}".`, + ); + } + if (detailsByCheck.has(detail.check)) { + throw new Error( + `Duplicate evidenceDetails entry for "${detail.check}" in claim ${claim.id}.`, + ); + } + detailsByCheck.set(detail.check, detail); + } + + for (const requirement of rule.requirements) { + if (!detailsByCheck.has(requirement.check)) { + throw new Error( + `Missing evidenceDetails entry for "${requirement.check}" in claim ${claim.id}.`, + ); + } + } +} diff --git a/examples/compatibility-harness/src/verdict/resolve.ts b/examples/compatibility-harness/src/verdict/resolve.ts new file mode 100644 index 000000000..8df6e5abd --- /dev/null +++ b/examples/compatibility-harness/src/verdict/resolve.ts @@ -0,0 +1,88 @@ +import type { ValidationStatus } from "../adapter/types.js"; +import type { TestResult } from "../report/reporter.js"; +import { CLAIM_RULES } from "./claims.js"; +import { claimCheckId, evidenceReasonCategory } from "./consistency.js"; +import type { + CanonicalCheckName, + CheckStatusOrMissing, + ClaimResolution, + ClaimRule, +} from "./types.js"; + +function checkStatus( + checks: Partial>, + check: CanonicalCheckName, +): CheckStatusOrMissing { + return checks[check] ?? "MISSING"; +} + +function buildCanonicalCheckMap( + results: TestResult[], +): Partial> { + const byName = new Map(results.map((result) => [result.name, result])); + return { + "Zama Authorization Flow": byName.get("Zama Authorization Flow")?.status, + "Zama Write Flow": byName.get("Zama Write Flow")?.status, + "EIP-712 Recoverability": byName.get("EIP-712 Recoverability")?.status, + }; +} + +function ruleMatches( + checks: Partial>, + rule: ClaimRule, +): boolean { + return rule.requirements.every((requirement) => { + const status = checkStatus(checks, requirement.check); + return requirement.oneOf.includes(status); + }); +} + +export function resolveClaimFromResults(results: TestResult[]): ClaimResolution { + const canonicalChecks = buildCanonicalCheckMap(results); + for (const rule of CLAIM_RULES) { + if (!ruleMatches(canonicalChecks, rule)) continue; + const evidence = Object.fromEntries( + rule.requirements.map((requirement) => { + return [requirement.check, checkStatus(canonicalChecks, requirement.check)]; + }), + ) as ClaimResolution["evidence"]; + const evidenceDetails = rule.requirements.map((requirement) => { + const status = checkStatus(canonicalChecks, requirement.check); + return { + check: requirement.check, + checkId: claimCheckId(requirement.check), + status, + reasonCategory: evidenceReasonCategory(status), + }; + }); + + return { + id: rule.id, + verdictLabel: rule.verdictLabel, + rationale: rule.rationale, + evidence, + evidenceDetails, + }; + } + + const fallbackStatus = checkStatus(canonicalChecks, "Zama Authorization Flow"); + const fallbackEvidence: ClaimResolution["evidence"] = { + "Zama Authorization Flow": fallbackStatus, + }; + const fallbackEvidenceDetails = [ + { + check: "Zama Authorization Flow" as const, + checkId: claimCheckId("Zama Authorization Flow"), + status: fallbackStatus, + reasonCategory: evidenceReasonCategory(fallbackStatus), + }, + ]; + + return { + id: "PARTIAL_AUTHORIZATION_CHECK_MISSING", + verdictLabel: "PARTIALLY VALIDATED — AUTHORIZATION CHECK NOT RECORDED", + rationale: ["No claim rule matched the observed result set."], + evidence: fallbackEvidence, + evidenceDetails: fallbackEvidenceDetails, + }; +} diff --git a/examples/compatibility-harness/src/verdict/types.ts b/examples/compatibility-harness/src/verdict/types.ts new file mode 100644 index 000000000..30e0b10e3 --- /dev/null +++ b/examples/compatibility-harness/src/verdict/types.ts @@ -0,0 +1,59 @@ +import type { ValidationStatus } from "../adapter/types.js"; +import type { CanonicalCheckId } from "../report/check-registry.js"; + +export type CanonicalCheckName = + | "Zama Authorization Flow" + | "Zama Write Flow" + | "EIP-712 Recoverability"; + +export type CheckStatusOrMissing = ValidationStatus | "MISSING"; +export type ClaimEvidenceReasonCategory = + | "VALIDATED" + | "COMPATIBILITY_FAILURE" + | "UNSUPPORTED_SURFACE" + | "NOT_EXECUTED" + | "INFRA_OR_ENV_BLOCKER" + | "MISSING_EVIDENCE"; + +export interface ClaimEvidenceDetail { + check: CanonicalCheckName; + checkId: CanonicalCheckId; + status: CheckStatusOrMissing; + reasonCategory: ClaimEvidenceReasonCategory; +} + +export type ClaimId = + | "INCOMPATIBLE_AUTHORIZATION_FAILED" + | "INCOMPATIBLE_AUTHORIZATION_UNSUPPORTED" + | "INCONCLUSIVE_AUTHORIZATION_BLOCKED" + | "INCONCLUSIVE_AUTHORIZATION_UNTESTED" + | "PARTIAL_AUTHORIZATION_CHECK_MISSING" + | "INCOMPATIBLE_AUTHORIZATION_RECOVERABILITY" + | "PARTIAL_AUTHORIZATION_RECOVERABILITY_UNCONFIRMED" + | "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE" + | "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED" + | "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED" + | "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_BLOCKED" + | "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_FAILED" + | "ZAMA_AUTHORIZATION_COMPATIBLE_WRITE_NOT_RECORDED"; + +export type VerdictConfidence = "HIGH" | "MEDIUM" | "LOW"; + +export interface ClaimResolution { + id: ClaimId; + verdictLabel: string; + confidence?: VerdictConfidence; + rationale: string[]; + evidence: Partial>; + evidenceDetails?: ClaimEvidenceDetail[]; +} + +export interface ClaimRule { + id: ClaimId; + verdictLabel: string; + requirements: Array<{ + check: CanonicalCheckName; + oneOf: CheckStatusOrMissing[]; + }>; + rationale: string[]; +} diff --git a/examples/compatibility-harness/tsconfig.json b/examples/compatibility-harness/tsconfig.json new file mode 100644 index 000000000..efb243ca5 --- /dev/null +++ b/examples/compatibility-harness/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/compatibility-harness/validation-policy.example.json b/examples/compatibility-harness/validation-policy.example.json new file mode 100644 index 000000000..1b76aadac --- /dev/null +++ b/examples/compatibility-harness/validation-policy.example.json @@ -0,0 +1,9 @@ +{ + "target": "AUTHORIZATION_AND_WRITE", + "allowPartial": false, + "expectedClaims": [ + "ZAMA_AUTHORIZATION_AND_WRITE_COMPATIBLE", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNSUPPORTED", + "PARTIAL_AUTHORIZATION_COMPATIBLE_WRITE_UNTESTED" + ] +} diff --git a/examples/compatibility-harness/vitest.config.ts b/examples/compatibility-harness/vitest.config.ts new file mode 100644 index 000000000..b13d1dac1 --- /dev/null +++ b/examples/compatibility-harness/vitest.config.ts @@ -0,0 +1,89 @@ +import { defineConfig } from "vitest/config"; +import { randomUUID } from "node:crypto"; +import { resolve } from "node:path"; + +// ── Pluggable signer adapter ─────────────────────────────────────────────────── +// +// By default the harness uses src/adapter/index.ts (built-in EOA adapter). +// +// To use a different adapter without modifying any source file, set SIGNER_MODULE +// to the path of a module exporting: +// - `adapter` (preferred), or +// - `signer` (legacy compatibility wrapper), +// and optionally `ready` for async initialization: +// +// SIGNER_MODULE=./examples/crossmint/signer.ts npm test +// +// The alias below intercepts imports of src/adapter/index.{ts,js} and +// src/signer/index.{ts,js} at the Vite/vitest level and redirects them to the +// specified module — no file copy and no harness source edits required. + +const signerModule = process.env.SIGNER_MODULE ? resolve(process.env.SIGNER_MODULE) : null; + +// Use a per-run ID so parallel runs in the same machine do not share report temp files. +if (!process.env.ZAMA_HARNESS_RUN_ID) { + process.env.ZAMA_HARNESS_RUN_ID = `${Date.now()}-${randomUUID()}`; +} + +export default defineConfig({ + resolve: { + alias: signerModule + ? [ + { + // Matches the entire import specifier ending with /adapter/index.ts or /signer/index.ts + // ^.* is required so that id.replace(regex, replacement) substitutes + // the whole string, not just the suffix (which would produce ../abs/path). + find: /^.*[/\\](adapter|signer)[/\\]index\.(ts|js)$/, + replacement: signerModule, + }, + ] + : [], + }, + test: { + globals: true, + environment: "node", + setupFiles: ["./src/setup.ts"], + testTimeout: 60_000, // network calls to relayer + RPC can be slow + hookTimeout: 30_000, + globalSetup: "./src/report/global-setup.ts", + fileParallelism: false, + // Run test files sequentially so results appear in a predictable order. + sequence: { concurrent: false }, + // Include files explicitly to control execution order: + // 1. Unit tests for pure classification/diagnostics logic + // 2. Adapter profile + // 3. Identity / verification + // 4. Raw transaction flow + // 5. Adapter execution + // 6. Zama authorization + // 7. Zama write flow + include: [ + "src/tests/unit/diagnostics.unit.test.ts", + "src/tests/unit/recommendations.unit.test.ts", + "src/tests/unit/networkConfig.unit.test.ts", + "src/tests/unit/runtime.unit.test.ts", + "src/tests/unit/runtimeObservation.unit.test.ts", + "src/tests/unit/writeValidationDepth.unit.test.ts", + "src/tests/unit/contradictions.unit.test.ts", + "src/tests/unit/checkRegistry.unit.test.ts", + "src/tests/unit/negativeMatrix.unit.test.ts", + "src/tests/unit/profile.unit.test.ts", + "src/tests/unit/verdict.unit.test.ts", + "src/tests/unit/verdictConfidence.unit.test.ts", + "src/tests/unit/artifactCompatibility.unit.test.ts", + "src/tests/unit/goldenReports.unit.test.ts", + "src/tests/unit/exampleBaselines.unit.test.ts", + "src/tests/unit/adapterCheck.unit.test.ts", + "src/tests/unit/validatePolicy.unit.test.ts", + "src/tests/unit/validateConfig.unit.test.ts", + "src/tests/unit/initAdapter.unit.test.ts", + "src/tests/unit/claimConsistency.unit.test.ts", + "src/tests/adapterProfile.test.ts", + "src/tests/eip712.test.ts", + "src/tests/transaction.test.ts", + "src/tests/adapterExecution.test.ts", + "src/tests/zamaFlow.test.ts", + "src/tests/zamaWriteFlow.test.ts", + ], + }, +});