diff --git a/.gitignore b/.gitignore index 075ffa5..19b1be2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,19 @@ *.local package-lock.json .pnp.* +**/.pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions +# Nested yarn state (e.g. `examples//.yarn/cache`) shouldn't ship. +# Examples install standalone; their cache regenerates from yarn.lock. +**/.yarn/cache +**/.yarn/install-state.gz +**/.yarn/build-state.yml +**/.yarn/unplugged logs log @@ -23,9 +30,42 @@ dist/ gen/ managed/ artifacts/ +# Examples ship their compiled artifacts committed so the deploy walkthrough +# works without the `compact` toolchain installed. Override the global rule +# above only for the example tree. +!examples/**/artifacts/ +!examples/**/artifacts/** +# Module-only artifacts produced by `compact-compiler --hierarchical` for +# vendored library code (FungibleToken, Initializable, Utils, …). These have +# no keys/zkir and aren't deployable on their own — they regenerate on every +# `yarn compile` and otherwise just create diff noise. +examples/**/artifacts/security/ +examples/**/artifacts/token/ +examples/**/artifacts/utils/ midnight-level-db compactc +# Deploy secrets — wallet seeds, signing keys, keystores. Match at any depth +# so nested deploy/ directories (e.g. under examples/) are covered too. +**/deploy/*.seed +**/deploy/*.signingkey +**/deploy/*.keystore.json + +# Deployment records — the JSON the deployer writes after a successful +# deploy. Includes the contract signing key, so treat as a secret. +deployments/ + +# compact-deployer wallet-state cache (per-seed, per-network shielded snapshots). +.states/ +**/.states/ + +# Third-party source pulled in for local experimentation (e.g. the +# midnight-node fork validation under vendor/midnight-node/ — see +# plans/tooling/compact-deploy-rust-fork.md). Never committed. +vendor/ +target/ +.toolkit-cache/ + coverage **/reports diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b637d74 --- /dev/null +++ b/Makefile @@ -0,0 +1,111 @@ +# compact-tools — top-level Makefile. +# +# Single entry point for build / lint / test pipelines and for the +# integration-test docker stack. Workspace tasks delegate to `yarn` +# (which delegates to turbo); docker + compactc orchestration lives +# here because Make's recipes run in /bin/sh and support `trap`, +# which yarn's built-in shell does not. + +INTEGRATION_DIR := tests/integrations +COMPOSE_FILE := $(INTEGRATION_DIR)/local-env.yml +LOGS_DIR := $(INTEGRATION_DIR)/logs +SERVICES := proof-server indexer node + +# One marker file per fixture: Make uses mtime against the .compact +# source to decide whether a re-compile is needed, so `make compile` +# is a no-op when nothing changed (poor man's build cache, free). +COUNTER_OUT := $(INTEGRATION_DIR)/fixtures/artifacts/Counter/contract/index.js +PRIVATE_OUT := $(INTEGRATION_DIR)/fixtures/artifacts/PrivateCounter/contract/index.js + +.PHONY: help \ + build test types lint lint-fix clean \ + env-up env-down env-logs env-status \ + compile test-integration + +help: ## Show this help. + @echo "compact-tools — common targets" + @echo "" + @echo " Workspace tasks (delegate to yarn → turbo)" + @echo " make build Build all workspace packages" + @echo " make test Run unit tests" + @echo " make types Type-check all packages" + @echo " make lint Lint with biome" + @echo " make lint-fix Lint and auto-fix" + @echo " make clean Clean build artifacts" + @echo "" + @echo " Integration-test docker stack" + @echo " make env-up Start local Midnight stack (proof-server + indexer + node)" + @echo " make env-down Stop local stack and remove volumes" + @echo " make env-logs Tail all docker stack logs" + @echo " make env-status Show docker container status" + @echo "" + @echo " Integration-test fixtures + run" + @echo " make compile Compile fixture contracts (idempotent via mtime)" + @echo " make test-integration End-to-end: env-up → compile → vitest → env-down" + +# ── Workspace tasks ──────────────────────────────────────────────────── + +build: + yarn build + +test: + yarn test + +types: + yarn types + +lint: + yarn lint + +lint-fix: + yarn lint:fix + +clean: + yarn clean + +# ── Integration-test docker stack ────────────────────────────────────── + +env-up: env-down + docker compose -f $(COMPOSE_FILE) up -d + @mkdir -p $(LOGS_DIR) + @for svc in $(SERVICES); do \ + docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ + done + @echo "Logs streaming to $(LOGS_DIR)/" + +env-down: + @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true + docker compose -f $(COMPOSE_FILE) down -v + +env-logs: + tail -f $(LOGS_DIR)/*.log + +env-status: + docker compose -f $(COMPOSE_FILE) ps + +# ── Integration-test fixtures ────────────────────────────────────────── +# +# Each fixture has an explicit file dep on its .compact source; Make +# only re-runs `compact compile` when the source is newer than the +# emitted index.js. Idempotent across repeated invocations. + +compile: $(COUNTER_OUT) $(PRIVATE_OUT) + +$(COUNTER_OUT): $(INTEGRATION_DIR)/fixtures/Counter.compact + compact compile $< $(INTEGRATION_DIR)/fixtures/artifacts/Counter + +$(PRIVATE_OUT): $(INTEGRATION_DIR)/fixtures/PrivateCounter.compact + compact compile $< $(INTEGRATION_DIR)/fixtures/artifacts/PrivateCounter + +# ── End-to-end integration test ──────────────────────────────────────── +# +# Runs the whole pipeline in one /bin/sh invocation (note the `\` +# continuations) so the `trap` survives across the chain. Teardown +# fires on success, on any failure, and on Ctrl+C (INT / TERM). + +test-integration: + @trap '$(MAKE) env-down' EXIT INT TERM; \ + rm -rf midnight-level-db && \ + $(MAKE) env-up && \ + $(MAKE) compile && \ + yarn vitest run --config $(INTEGRATION_DIR)/vitest.config.ts diff --git a/compact.toml b/compact.toml new file mode 100644 index 0000000..3ef6ef0 --- /dev/null +++ b/compact.toml @@ -0,0 +1,34 @@ +# Root deployer config — used by `compact-deploy` for real-network deploys. +# Local-stack deploys still go through tests/integrations/compact.toml. + +[profile] +artifacts_dir = "tests/integrations/fixtures/artifacts" +deployments_dir = "deployments/compact" + +# URLs taken verbatim from @midnight-ntwrk/testkit-js PreprodTestEnvironment +# (node_modules/.../testkit-js/dist/index.mjs). Indexer is v4 — the README +# example showed v3, which is stale. +[networks.preprod] +network_id = "preprod" +indexer = "https://indexer.preprod.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preprod.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preprod.midnight.network" +node_ws = "wss://rpc.preprod.midnight.network" +proof_server = "http://127.0.0.1:6300" # reuse the local-stack proof-server from `make env-up` +explorer = "https://preprod.midnightexplorer.com" + +# URLs taken verbatim from @midnight-ntwrk/testkit-js PreviewTestEnvironment. +# Recommended by Midnight team while preprod is blocked on the +# `midnight:event[v9]` DustSpendProcessed deserialization bug. +[networks.preview] +network_id = "preview" +indexer = "https://indexer.preview.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preview.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preview.midnight.network" +node_ws = "wss://rpc.preview.midnight.network" +proof_server = "http://127.0.0.1:6300" # reuse the local-stack proof-server from `make env-up` +explorer = "https://preview.midnightexplorer.com" + +[contracts.Counter] +artifact = "Counter" +signing_key_file = "deploy/Counter.preprod.signingkey" diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..0ad319a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,36 @@ +# compact-tools examples + +Runnable, copy-pasteable starting points for `compact-deployer`. Each example is self-contained: its own `compact.toml`, its own `package.json`, its own compiled artifact, and its own hand-written deploy script using the programmatic deployer API. + +## Available examples + +| Example | What it covers | +|---|---| +| [fungible-token/](./fungible-token/) | Deploys a small ERC20-flavoured contract wrapping OpenZeppelin Compact's `FungibleToken` module. Constructor exercises every common Compact primitive type: strings, `Uint<8/32/64/128>`, `Boolean`, `Bytes<8/32>`. | + +More to come (private state + witnesses, multisig patterns, programmatic API). + +## Conventions + +- Each example builds and runs on Node 24+. +- Compiled artifacts ship committed so you can deploy without installing the `compact` toolchain. +- `deploy/*.signingkey` files are gitignored. Generate per the example README. +- `.states/` (wallet cache) and `deployments/` (deploy records) are gitignored. +- Compact-contracts modules (`FungibleToken`, `Initializable`, `Utils`) are vendored as copies of [openzeppelin/compact-contracts](https://github.com/openzeppelin/compact-contracts) source, not submodules. Refresh by recopying when the library publishes. + +## Setup + +Each example is a yarn workspace member, so a single root-level install wires every binary (`compact-compiler`, `compact-deploy`) into the example. From the repo root: + +```bash +yarn install +yarn build +``` + +After that: + +```bash +cd examples/ +yarn compile # rebuild the artifact if you edit a .compact file +yarn deploy:local # run the example +``` diff --git a/examples/fungible-token/README.md b/examples/fungible-token/README.md new file mode 100644 index 0000000..9a04e0c --- /dev/null +++ b/examples/fungible-token/README.md @@ -0,0 +1,180 @@ +# TokenExample — `compact-deployer` walkthrough with a rich constructor + +Deploys a small ERC20-flavoured contract built on the OpenZeppelin Compact `FungibleToken` module. The example shows two ways to drive the deployer: + +1. **A TS deploy script** that imports `runDeploy()` from `@openzeppelin/compact-deployer` and passes constructor args inline as native JS values. +2. **The `compact-deploy` CLI** binary from `@openzeppelin/compact-cli`, which reads args from a `.args.mjs` module referenced in `compact.toml`. + +Both end up calling the same deployer code; pick whichever fits your workflow. + +The constructor exercises every common Compact primitive type: + +| Constructor arg | Compact type | JS type | +|---|---|---| +| `_name` | `Opaque<"string">` | `string` | +| `_symbol` | `Opaque<"string">` | `string` | +| `_decimals` | `Uint<8>` | `number` | +| `_treasury` | `Bytes<32>` | `Uint8Array(32)` | +| `_maxSupply` | `Uint<128>` | `BigInt` | +| `_feeBps` | `Uint<32>` | `number` | +| `_quorum` | `Uint<64>` | `BigInt` | +| `_isMintable` | `Boolean` | `boolean` | +| `_tag` | `Bytes<8>` | `Uint8Array(8)` | + +## What's in here + +``` +fungible-token/ + contracts/ + TokenExample.compact wrapper with the rich constructor + token/FungibleToken.compact vendored from compact-contracts + security/Initializable.compact vendored from compact-contracts + utils/Utils.compact vendored from compact-contracts + artifacts/TokenExample/ pre-compiled (committed) + compact.toml deployer config (3 networks defined) + deploy/ + deployTokenExample.ts the TS deploy script (path #1) + TokenExample.args.mjs args module read by the CLI (path #2) + TokenExample.signingkey you generate this (gitignored) + deployments/ deployer writes here on success (gitignored) + package.json a workspace member: depends on + @openzeppelin/compact-deployer + + compact-cli via `workspace:^` +``` + +## Prerequisites + +- Node 24+ +- Docker (for the local Midnight stack) +- A one-time root setup: `yarn install && yarn build` from the repo root. This is a yarn workspace, so binaries like `compact-compiler` and `compact-deploy` resolve automatically inside this folder. + +## Run it + +```bash +cd examples/fungible-token + +# 1. Generate a per-contract signing key. +head -c 32 /dev/urandom | xxd -p -c 32 > deploy/TokenExample.signingkey + +# 2. Start the local Midnight stack (from the repo root). +make env-up + +# 3. Pick a path — see below. +``` + +### Path 1 — TS deploy script (args inline) + +```bash +yarn deploy:local # node deploy/deployTokenExample.ts +yarn deploy:preview # …--network preview --sync-timeout 1800 +yarn deploy:preprod # …--network preprod --sync-timeout 7200 +``` + +[`deploy/deployTokenExample.ts`](deploy/deployTokenExample.ts) is the whole script: + +```ts +import { runDeploy } from '@openzeppelin/compact-deployer'; +import { Contract } from '../artifacts/TokenExample/contract/index.js'; + +await runDeploy(Contract)( + 'OpenZeppelin Example Token', // editor: "_name_2: string" + 'OZE', // editor: "_symbol_2: string" + 18n, // editor: "_decimals_2: bigint" + new Uint8Array(32).fill(0xab), + 1_000_000_000_000_000_000_000_000n, + 250n, 7n, true, + new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]), +); +``` + +The curried form names the contract once via the imported `Contract` class — the deployer matches it to `[contracts.TokenExample]` in `compact.toml` by class identity, so no string repetition. Constructor args are typed function parameters: each comma triggers TypeScript signature help showing the next param's name and type. + +To pass extra deploy options (network, dry-run, …), supply them as the second arg: + +```ts +await runDeploy(Contract, { network: 'preview', dryRun: true })( + 'OpenZeppelin Example Token', 'OZE', 18n, /* … */ +); +``` + +`runDeploy()` parses `--network`, `--dry-run`, `--sync-timeout`, `--no-cache`, `--seed-file`, `--proof-server`, `--config`, `--json`, and `-v` / `--verbose` from `process.argv` as defaults. Explicit options on the call win. + +Alternative call shapes: +- `runDeploy({ contract: 'TokenExample', args: [...] })` — options-object form. Use when args come from `compact.toml`, when one `compact.toml` has multiple entries for the same Contract class, or for programmatic flows. +- `runDeploy({ contract: 'TokenExample', args: constructorArgs(Contract, ...) })` — keeps the per-comma editor hints inside an options-object call. +- Named-object args (`args: { _name: '…', … }`) — full autocomplete but requires a hand-written interface; see [logs/feature-compactc-export-constructor-args-interface.md](../../logs/feature-compactc-export-constructor-args-interface.md) for the upstream fix that would eliminate the hand-writing. + +### Path 2 — `compact-deploy` CLI (args in a separate module) + +```bash +yarn cli:local # compact-deploy TokenExample --network local +yarn cli:preview # compact-deploy TokenExample --network preview … +yarn cli:preprod # compact-deploy TokenExample --network preprod … +``` + +`compact.toml` already points at the args module: + +```toml +[contracts.TokenExample] +artifact = "TokenExample" +signing_key_file = "deploy/TokenExample.signingkey" +args = { module = "./deploy/TokenExample.args.mjs", export = "args" } +``` + +[`deploy/TokenExample.args.mjs`](deploy/TokenExample.args.mjs) exports the same JS values as Path 1. The CLI doesn't need a script — `compact-deploy TokenExample --network ` reads everything from `compact.toml`. + +### When to pick which + +| Picking… | When | +|---|---| +| Path 1 (script) | The deploy logic itself is the moving part. Easy to add post-deploy work (seed state, run callTx, write a custom record) in the same file. | +| Path 2 (CLI) | The deploy logic is fixed and only the args vary per network or per build. Lighter footprint — no JS script to maintain. | + +`runDeploy()` actually accepts the same `args` field that you'd put in `compact.toml`, so Path 1 can read from a `.args.mjs` too (drop the `args:` field from the script call and the TOML ref takes over). + +## Type-by-type cheat sheet + +| Compact | JS | +|---|---| +| `Opaque<"string">` | `string` | +| `Uint` (any width) | `bigint` (use the `n` suffix: `18n`, `250n`). The compiler emits every `Uint` as `bigint`. | +| `Boolean` | `boolean` | +| `Bytes` | `new Uint8Array(N)` of length exactly `N` | +| `Vector` | array of length exactly `N` | +| `Maybe` | `{ is_some: true, value: T }` or `{ is_some: false, value: }` | +| `Either` | `{ is_left: true, left: L, right: }` or mirror with `is_left: false` | + +`Bytes` values must be exactly `N` bytes — neither path pads or truncates. + +## Public testnets (preview, preprod) + +```bash +yarn deploy:preview # or yarn cli:preview +yarn deploy:preprod # or yarn cli:preprod +``` + +First sync takes a few minutes on preview and 30–60 minutes on preprod (the deployer caches both shielded + dust state under `.states/` so subsequent runs are near-instant). + +> Preview and preprod are both blocked upstream right now. See the deployer's "Known issues" section in [`packages/deployer/README.md`](../../packages/deployer/README.md). + +## Recompile the contract + +If you edit `contracts/TokenExample.compact` (or any vendored file under `contracts/`): + +```bash +yarn compile +``` + +This runs the workspace's `compact-compiler` (from `@openzeppelin/compact-builder`) over `contracts/` and emits a hierarchical artifact tree under `artifacts/`. Commit the regenerated `artifacts/TokenExample/`. + +## Cleanup + +```bash +make env-down # from the repo root +rm -rf .states deployments deploy/TokenExample.signingkey +``` + +## Where to look next + +- [`packages/deployer/README.md`](../../packages/deployer/README.md) — every CLI flag, keystore format, current known-issues list. +- `contracts/token/FungibleToken.compact` — the full ERC20-ish surface this wrapper delegates to (`transfer`, `_mint`, `allowance`, etc.). Wire more circuits into `TokenExample.compact` to expose them. diff --git a/examples/fungible-token/artifacts/TokenExample/compiler/contract-info.json b/examples/fungible-token/artifacts/TokenExample/compiler/contract-info.json new file mode 100644 index 0000000..ad5170f --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/compiler/contract-info.json @@ -0,0 +1,342 @@ +{ + "compiler-version": "0.31.0", + "language-version": "0.23.0", + "runtime-version": "0.16.0", + "circuits": [ + { + "name": "name", + "pure": false, + "proof": true, + "arguments": [ + ], + "result-type": { + "type-name": "Opaque", + "tsType": "string" + } + }, + { + "name": "symbol", + "pure": false, + "proof": true, + "arguments": [ + ], + "result-type": { + "type-name": "Opaque", + "tsType": "string" + } + }, + { + "name": "decimals", + "pure": false, + "proof": true, + "arguments": [ + ], + "result-type": { + "type-name": "Uint", + "maxval": 255 + } + }, + { + "name": "totalSupply", + "pure": false, + "proof": true, + "arguments": [ + ], + "result-type": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + }, + { + "name": "balanceOf", + "pure": false, + "proof": true, + "arguments": [ + { + "name": "account", + "type": { + "type-name": "Struct", + "name": "Either", + "elements": [ + { + "name": "is_left", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "left", + "type": { + "type-name": "Bytes", + "length": 32 + } + }, + { + "name": "right", + "type": { + "type-name": "Struct", + "name": "ContractAddress", + "elements": [ + { + "name": "bytes", + "type": { + "type-name": "Bytes", + "length": 32 + } + } + ] + } + } + ] + } + } + ], + "result-type": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + } + ], + "witnesses": [ + ], + "contracts": [ + ], + "ledger": [ + { + "name": "_balances", + "index": 0, + "exported": false, + "storage": "Map", + "key": { + "type-name": "Struct", + "name": "Either", + "elements": [ + { + "name": "is_left", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "left", + "type": { + "type-name": "Bytes", + "length": 32 + } + }, + { + "name": "right", + "type": { + "type-name": "Struct", + "name": "ContractAddress", + "elements": [ + { + "name": "bytes", + "type": { + "type-name": "Bytes", + "length": 32 + } + } + ] + } + } + ] + }, + "value": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + }, + { + "name": "_allowances", + "index": 1, + "exported": false, + "storage": "Map", + "key": { + "type-name": "Struct", + "name": "Either", + "elements": [ + { + "name": "is_left", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "left", + "type": { + "type-name": "Bytes", + "length": 32 + } + }, + { + "name": "right", + "type": { + "type-name": "Struct", + "name": "ContractAddress", + "elements": [ + { + "name": "bytes", + "type": { + "type-name": "Bytes", + "length": 32 + } + } + ] + } + } + ] + }, + "value": { + "type-name": "Map", + "key": { + "type-name": "Struct", + "name": "Either", + "elements": [ + { + "name": "is_left", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "left", + "type": { + "type-name": "Bytes", + "length": 32 + } + }, + { + "name": "right", + "type": { + "type-name": "Struct", + "name": "ContractAddress", + "elements": [ + { + "name": "bytes", + "type": { + "type-name": "Bytes", + "length": 32 + } + } + ] + } + } + ] + }, + "value": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + } + }, + { + "name": "_totalSupply", + "index": 2, + "exported": false, + "storage": "Cell", + "type": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + }, + { + "name": "_name", + "index": 3, + "exported": false, + "storage": "Cell", + "type": { + "type-name": "Opaque", + "tsType": "string" + } + }, + { + "name": "_symbol", + "index": 4, + "exported": false, + "storage": "Cell", + "type": { + "type-name": "Opaque", + "tsType": "string" + } + }, + { + "name": "_decimals", + "index": 5, + "exported": false, + "storage": "Cell", + "type": { + "type-name": "Uint", + "maxval": 255 + } + }, + { + "name": "_isInitialized", + "index": 6, + "exported": false, + "storage": "Cell", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "treasury", + "index": 7, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Bytes", + "length": 32 + } + }, + { + "name": "maxSupply", + "index": 8, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Uint", + "maxval": 340282366920938463463374607431768211455 + } + }, + { + "name": "feeBps", + "index": 9, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Uint", + "maxval": 4294967295 + } + }, + { + "name": "quorum", + "index": 10, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Uint", + "maxval": 18446744073709551615 + } + }, + { + "name": "isMintable", + "index": 11, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Boolean" + } + }, + { + "name": "tag", + "index": 12, + "exported": true, + "storage": "Cell", + "type": { + "type-name": "Bytes", + "length": 8 + } + } + ] +} diff --git a/examples/fungible-token/artifacts/TokenExample/contract/index.d.ts b/examples/fungible-token/artifacts/TokenExample/contract/index.d.ts new file mode 100644 index 0000000..905428f --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/contract/index.d.ts @@ -0,0 +1,74 @@ +import type * as __compactRuntime from '@midnight-ntwrk/compact-runtime'; + +export type ContractAddress = { bytes: Uint8Array }; + +export type Either = { is_left: boolean; left: A; right: B }; + +export type Maybe = { is_some: boolean; value: T }; + +export type Witnesses = { +} + +export type ImpureCircuits = { + name(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + symbol(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + decimals(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + totalSupply(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + balanceOf(context: __compactRuntime.CircuitContext, + account_0: Either): __compactRuntime.CircuitResults; +} + +export type ProvableCircuits = { + name(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + symbol(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + decimals(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + totalSupply(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + balanceOf(context: __compactRuntime.CircuitContext, + account_0: Either): __compactRuntime.CircuitResults; +} + +export type PureCircuits = { +} + +export type Circuits = { + name(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + symbol(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + decimals(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + totalSupply(context: __compactRuntime.CircuitContext): __compactRuntime.CircuitResults; + balanceOf(context: __compactRuntime.CircuitContext, + account_0: Either): __compactRuntime.CircuitResults; +} + +export type Ledger = { + readonly treasury: Uint8Array; + readonly maxSupply: bigint; + readonly feeBps: bigint; + readonly quorum: bigint; + readonly isMintable: boolean; + readonly tag: Uint8Array; +} + +export type ContractReferenceLocations = any; + +export declare const contractReferenceLocations : ContractReferenceLocations; + +export declare class Contract = Witnesses> { + witnesses: W; + circuits: Circuits; + impureCircuits: ImpureCircuits; + provableCircuits: ProvableCircuits; + constructor(witnesses: W); + initialState(context: __compactRuntime.ConstructorContext, + _name_2: string, + _symbol_2: string, + _decimals_2: bigint, + _treasury_0: Uint8Array, + _maxSupply_0: bigint, + _feeBps_0: bigint, + _quorum_0: bigint, + _isMintable_0: boolean, + _tag_0: Uint8Array): __compactRuntime.ConstructorResult; +} + +export declare function ledger(state: __compactRuntime.StateValue | __compactRuntime.ChargedState): Ledger; +export declare const pureCircuits: PureCircuits; diff --git a/examples/fungible-token/artifacts/TokenExample/contract/index.js b/examples/fungible-token/artifacts/TokenExample/contract/index.js new file mode 100644 index 0000000..cc7d0d1 --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/contract/index.js @@ -0,0 +1,844 @@ +import * as __compactRuntime from '@midnight-ntwrk/compact-runtime'; +__compactRuntime.checkRuntimeVersion('0.16.0'); + +const _descriptor_0 = new __compactRuntime.CompactTypeUnsignedInteger(340282366920938463463374607431768211455n, 16); + +const _descriptor_1 = __compactRuntime.CompactTypeBoolean; + +const _descriptor_2 = new __compactRuntime.CompactTypeBytes(32); + +class _ContractAddress_0 { + alignment() { + return _descriptor_2.alignment(); + } + fromValue(value_0) { + return { + bytes: _descriptor_2.fromValue(value_0) + } + } + toValue(value_0) { + return _descriptor_2.toValue(value_0.bytes); + } +} + +const _descriptor_3 = new _ContractAddress_0(); + +class _Either_0 { + alignment() { + return _descriptor_1.alignment().concat(_descriptor_2.alignment().concat(_descriptor_3.alignment())); + } + fromValue(value_0) { + return { + is_left: _descriptor_1.fromValue(value_0), + left: _descriptor_2.fromValue(value_0), + right: _descriptor_3.fromValue(value_0) + } + } + toValue(value_0) { + return _descriptor_1.toValue(value_0.is_left).concat(_descriptor_2.toValue(value_0.left).concat(_descriptor_3.toValue(value_0.right))); + } +} + +const _descriptor_4 = new _Either_0(); + +const _descriptor_5 = __compactRuntime.CompactTypeOpaqueString; + +const _descriptor_6 = new __compactRuntime.CompactTypeUnsignedInteger(255n, 1); + +const _descriptor_7 = new __compactRuntime.CompactTypeUnsignedInteger(18446744073709551615n, 8); + +class _Either_1 { + alignment() { + return _descriptor_1.alignment().concat(_descriptor_2.alignment().concat(_descriptor_2.alignment())); + } + fromValue(value_0) { + return { + is_left: _descriptor_1.fromValue(value_0), + left: _descriptor_2.fromValue(value_0), + right: _descriptor_2.fromValue(value_0) + } + } + toValue(value_0) { + return _descriptor_1.toValue(value_0.is_left).concat(_descriptor_2.toValue(value_0.left).concat(_descriptor_2.toValue(value_0.right))); + } +} + +const _descriptor_8 = new _Either_1(); + +const _descriptor_9 = new __compactRuntime.CompactTypeBytes(8); + +const _descriptor_10 = new __compactRuntime.CompactTypeUnsignedInteger(4294967295n, 4); + +export class Contract { + witnesses; + constructor(...args_0) { + if (args_0.length !== 1) { + throw new __compactRuntime.CompactError(`Contract constructor: expected 1 argument, received ${args_0.length}`); + } + const witnesses_0 = args_0[0]; + if (typeof(witnesses_0) !== 'object') { + throw new __compactRuntime.CompactError('first (witnesses) argument to Contract constructor is not an object'); + } + this.witnesses = witnesses_0; + this.circuits = { + name: (...args_1) => { + if (args_1.length !== 1) { + throw new __compactRuntime.CompactError(`name: expected 1 argument (as invoked from Typescript), received ${args_1.length}`); + } + const contextOrig_0 = args_1[0]; + if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) { + __compactRuntime.typeError('name', + 'argument 1 (as invoked from Typescript)', + 'TokenExample.compact line 64 char 1', + 'CircuitContext', + contextOrig_0) + } + const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() }; + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + const result_0 = this._name_1(context, partialProofData); + partialProofData.output = { value: _descriptor_5.toValue(result_0), alignment: _descriptor_5.alignment() }; + return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost }; + }, + symbol: (...args_1) => { + if (args_1.length !== 1) { + throw new __compactRuntime.CompactError(`symbol: expected 1 argument (as invoked from Typescript), received ${args_1.length}`); + } + const contextOrig_0 = args_1[0]; + if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) { + __compactRuntime.typeError('symbol', + 'argument 1 (as invoked from Typescript)', + 'TokenExample.compact line 68 char 1', + 'CircuitContext', + contextOrig_0) + } + const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() }; + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + const result_0 = this._symbol_1(context, partialProofData); + partialProofData.output = { value: _descriptor_5.toValue(result_0), alignment: _descriptor_5.alignment() }; + return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost }; + }, + decimals: (...args_1) => { + if (args_1.length !== 1) { + throw new __compactRuntime.CompactError(`decimals: expected 1 argument (as invoked from Typescript), received ${args_1.length}`); + } + const contextOrig_0 = args_1[0]; + if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) { + __compactRuntime.typeError('decimals', + 'argument 1 (as invoked from Typescript)', + 'TokenExample.compact line 72 char 1', + 'CircuitContext', + contextOrig_0) + } + const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() }; + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + const result_0 = this._decimals_1(context, partialProofData); + partialProofData.output = { value: _descriptor_6.toValue(result_0), alignment: _descriptor_6.alignment() }; + return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost }; + }, + totalSupply: (...args_1) => { + if (args_1.length !== 1) { + throw new __compactRuntime.CompactError(`totalSupply: expected 1 argument (as invoked from Typescript), received ${args_1.length}`); + } + const contextOrig_0 = args_1[0]; + if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) { + __compactRuntime.typeError('totalSupply', + 'argument 1 (as invoked from Typescript)', + 'TokenExample.compact line 76 char 1', + 'CircuitContext', + contextOrig_0) + } + const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() }; + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + const result_0 = this._totalSupply_1(context, partialProofData); + partialProofData.output = { value: _descriptor_0.toValue(result_0), alignment: _descriptor_0.alignment() }; + return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost }; + }, + balanceOf: (...args_1) => { + if (args_1.length !== 2) { + throw new __compactRuntime.CompactError(`balanceOf: expected 2 arguments (as invoked from Typescript), received ${args_1.length}`); + } + const contextOrig_0 = args_1[0]; + const account_0 = args_1[1]; + if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) { + __compactRuntime.typeError('balanceOf', + 'argument 1 (as invoked from Typescript)', + 'TokenExample.compact line 80 char 1', + 'CircuitContext', + contextOrig_0) + } + if (!(typeof(account_0) === 'object' && typeof(account_0.is_left) === 'boolean' && account_0.left.buffer instanceof ArrayBuffer && account_0.left.BYTES_PER_ELEMENT === 1 && account_0.left.length === 32 && typeof(account_0.right) === 'object' && account_0.right.bytes.buffer instanceof ArrayBuffer && account_0.right.bytes.BYTES_PER_ELEMENT === 1 && account_0.right.bytes.length === 32)) { + __compactRuntime.typeError('balanceOf', + 'argument 1 (argument 2 as invoked from Typescript)', + 'TokenExample.compact line 80 char 1', + 'struct Either, right: struct ContractAddress>>', + account_0) + } + const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() }; + const partialProofData = { + input: { + value: _descriptor_4.toValue(account_0), + alignment: _descriptor_4.alignment() + }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + const result_0 = this._balanceOf_1(context, partialProofData, account_0); + partialProofData.output = { value: _descriptor_0.toValue(result_0), alignment: _descriptor_0.alignment() }; + return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost }; + } + }; + this.impureCircuits = { + name: this.circuits.name, + symbol: this.circuits.symbol, + decimals: this.circuits.decimals, + totalSupply: this.circuits.totalSupply, + balanceOf: this.circuits.balanceOf + }; + this.provableCircuits = { + name: this.circuits.name, + symbol: this.circuits.symbol, + decimals: this.circuits.decimals, + totalSupply: this.circuits.totalSupply, + balanceOf: this.circuits.balanceOf + }; + } + initialState(...args_0) { + if (args_0.length !== 10) { + throw new __compactRuntime.CompactError(`Contract state constructor: expected 10 arguments (as invoked from Typescript), received ${args_0.length}`); + } + const constructorContext_0 = args_0[0]; + const _name_2 = args_0[1]; + const _symbol_2 = args_0[2]; + const _decimals_2 = args_0[3]; + const _treasury_0 = args_0[4]; + const _maxSupply_0 = args_0[5]; + const _feeBps_0 = args_0[6]; + const _quorum_0 = args_0[7]; + const _isMintable_0 = args_0[8]; + const _tag_0 = args_0[9]; + if (typeof(constructorContext_0) !== 'object') { + throw new __compactRuntime.CompactError(`Contract state constructor: expected 'constructorContext' in argument 1 (as invoked from Typescript) to be an object`); + } + if (!('initialZswapLocalState' in constructorContext_0)) { + throw new __compactRuntime.CompactError(`Contract state constructor: expected 'initialZswapLocalState' in argument 1 (as invoked from Typescript)`); + } + if (typeof(constructorContext_0.initialZswapLocalState) !== 'object') { + throw new __compactRuntime.CompactError(`Contract state constructor: expected 'initialZswapLocalState' in argument 1 (as invoked from Typescript) to be an object`); + } + if (!(typeof(_decimals_2) === 'bigint' && _decimals_2 >= 0n && _decimals_2 <= 255n)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 3 (argument 4 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Uint<0..256>', + _decimals_2) + } + if (!(_treasury_0.buffer instanceof ArrayBuffer && _treasury_0.BYTES_PER_ELEMENT === 1 && _treasury_0.length === 32)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 4 (argument 5 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Bytes<32>', + _treasury_0) + } + if (!(typeof(_maxSupply_0) === 'bigint' && _maxSupply_0 >= 0n && _maxSupply_0 <= 340282366920938463463374607431768211455n)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 5 (argument 6 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Uint<0..340282366920938463463374607431768211456>', + _maxSupply_0) + } + if (!(typeof(_feeBps_0) === 'bigint' && _feeBps_0 >= 0n && _feeBps_0 <= 4294967295n)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 6 (argument 7 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Uint<0..4294967296>', + _feeBps_0) + } + if (!(typeof(_quorum_0) === 'bigint' && _quorum_0 >= 0n && _quorum_0 <= 18446744073709551615n)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 7 (argument 8 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Uint<0..18446744073709551616>', + _quorum_0) + } + if (!(typeof(_isMintable_0) === 'boolean')) { + __compactRuntime.typeError('Contract state constructor', + 'argument 8 (argument 9 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Boolean', + _isMintable_0) + } + if (!(_tag_0.buffer instanceof ArrayBuffer && _tag_0.BYTES_PER_ELEMENT === 1 && _tag_0.length === 8)) { + __compactRuntime.typeError('Contract state constructor', + 'argument 9 (argument 10 as invoked from Typescript)', + 'TokenExample.compact line 42 char 1', + 'Bytes<8>', + _tag_0) + } + const state_0 = new __compactRuntime.ContractState(); + let stateValue_0 = __compactRuntime.StateValue.newArray(); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + stateValue_0 = stateValue_0.arrayPush(__compactRuntime.StateValue.newNull()); + state_0.data = new __compactRuntime.ChargedState(stateValue_0); + state_0.setOperation('name', new __compactRuntime.ContractOperation()); + state_0.setOperation('symbol', new __compactRuntime.ContractOperation()); + state_0.setOperation('decimals', new __compactRuntime.ContractOperation()); + state_0.setOperation('totalSupply', new __compactRuntime.ContractOperation()); + state_0.setOperation('balanceOf', new __compactRuntime.ContractOperation()); + const context = __compactRuntime.createCircuitContext(__compactRuntime.dummyContractAddress(), constructorContext_0.initialZswapLocalState.coinPublicKey, state_0.data, constructorContext_0.initialPrivateState); + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(0n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newMap( + new __compactRuntime.StateMap() + ).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(1n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newMap( + new __compactRuntime.StateMap() + ).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(2n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_0.toValue(0n), + alignment: _descriptor_0.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(3n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_5.toValue(''), + alignment: _descriptor_5.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(4n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_5.toValue(''), + alignment: _descriptor_5.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(5n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(0n), + alignment: _descriptor_6.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(6n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_1.toValue(false), + alignment: _descriptor_1.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(7n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_2.toValue(new Uint8Array(32)), + alignment: _descriptor_2.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(8n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_0.toValue(0n), + alignment: _descriptor_0.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(9n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_10.toValue(0n), + alignment: _descriptor_10.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(10n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_7.toValue(0n), + alignment: _descriptor_7.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(11n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_1.toValue(false), + alignment: _descriptor_1.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(12n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_9.toValue(new Uint8Array(8)), + alignment: _descriptor_9.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + this._initialize_0(context, + partialProofData, + _name_2, + _symbol_2, + _decimals_2); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(7n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_2.toValue(_treasury_0), + alignment: _descriptor_2.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(8n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_0.toValue(_maxSupply_0), + alignment: _descriptor_0.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(9n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_10.toValue(_feeBps_0), + alignment: _descriptor_10.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(10n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_7.toValue(_quorum_0), + alignment: _descriptor_7.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(11n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_1.toValue(_isMintable_0), + alignment: _descriptor_1.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(12n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_9.toValue(_tag_0), + alignment: _descriptor_9.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + state_0.data = new __compactRuntime.ChargedState(context.currentQueryContext.state.state); + return { + currentContractState: state_0, + currentPrivateState: context.currentPrivateState, + currentZswapLocalState: context.currentZswapLocalState + } + } + _initialize_0(context, partialProofData, name__0, symbol__0, decimals__0) { + this._initialize_1(context, partialProofData); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(3n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_5.toValue(name__0), + alignment: _descriptor_5.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(4n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_5.toValue(symbol__0), + alignment: _descriptor_5.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(5n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(decimals__0), + alignment: _descriptor_6.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + return []; + } + _name_0(context, partialProofData) { + this._assertInitialized_0(context, partialProofData); + return _descriptor_5.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(3n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + _symbol_0(context, partialProofData) { + this._assertInitialized_0(context, partialProofData); + return _descriptor_5.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(4n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + _decimals_0(context, partialProofData) { + this._assertInitialized_0(context, partialProofData); + return _descriptor_6.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(5n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + _totalSupply_0(context, partialProofData) { + this._assertInitialized_0(context, partialProofData); + return _descriptor_0.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(2n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + _balanceOf_0(context, partialProofData, account_0) { + this._assertInitialized_0(context, partialProofData); + const canonAcct_0 = this._canonicalize_0(account_0); + if (!_descriptor_1.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(0n), + alignment: _descriptor_6.alignment() } }] } }, + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_4.toValue(canonAcct_0), + alignment: _descriptor_4.alignment() }).encode() } }, + 'member', + { popeq: { cached: true, + result: undefined } }]).value)) + { + return 0n; + } else { + return _descriptor_0.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(0n), + alignment: _descriptor_6.alignment() } }] } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_4.toValue(canonAcct_0), + alignment: _descriptor_4.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + } + _initialize_1(context, partialProofData) { + this._assertNotInitialized_0(context, partialProofData); + __compactRuntime.queryLedgerState(context, + partialProofData, + [ + { push: { storage: false, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_6.toValue(6n), + alignment: _descriptor_6.alignment() }).encode() } }, + { push: { storage: true, + value: __compactRuntime.StateValue.newCell({ value: _descriptor_1.toValue(true), + alignment: _descriptor_1.alignment() }).encode() } }, + { ins: { cached: false, n: 1 } }]); + return []; + } + _assertInitialized_0(context, partialProofData) { + __compactRuntime.assert(_descriptor_1.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(6n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value), + 'Initializable: contract not initialized'); + return []; + } + _assertNotInitialized_0(context, partialProofData) { + __compactRuntime.assert(!_descriptor_1.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(6n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value), + 'Initializable: contract already initialized'); + return []; + } + _canonicalize_0(value_0) { + if (value_0.is_left) { + return { is_left: true, + left: value_0.left, + right: { bytes: new Uint8Array(32) } }; + } else { + return { is_left: false, left: new Uint8Array(32), right: value_0.right }; + } + } + _name_1(context, partialProofData) { + return this._name_0(context, partialProofData); + } + _symbol_1(context, partialProofData) { + return this._symbol_0(context, partialProofData); + } + _decimals_1(context, partialProofData) { + return this._decimals_0(context, partialProofData); + } + _totalSupply_1(context, partialProofData) { + return this._totalSupply_0(context, partialProofData); + } + _balanceOf_1(context, partialProofData, account_0) { + return this._balanceOf_0(context, partialProofData, account_0); + } +} +export function ledger(stateOrChargedState) { + const state = stateOrChargedState instanceof __compactRuntime.StateValue ? stateOrChargedState : stateOrChargedState.state; + const chargedState = stateOrChargedState instanceof __compactRuntime.StateValue ? new __compactRuntime.ChargedState(stateOrChargedState) : stateOrChargedState; + const context = { + currentQueryContext: new __compactRuntime.QueryContext(chargedState, __compactRuntime.dummyContractAddress()), + costModel: __compactRuntime.CostModel.initialCostModel() + }; + const partialProofData = { + input: { value: [], alignment: [] }, + output: undefined, + publicTranscript: [], + privateTranscriptOutputs: [] + }; + return { + get treasury() { + return _descriptor_2.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(7n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + }, + get maxSupply() { + return _descriptor_0.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(8n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + }, + get feeBps() { + return _descriptor_10.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(9n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + }, + get quorum() { + return _descriptor_7.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(10n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + }, + get isMintable() { + return _descriptor_1.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(11n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + }, + get tag() { + return _descriptor_9.fromValue(__compactRuntime.queryLedgerState(context, + partialProofData, + [ + { dup: { n: 0 } }, + { idx: { cached: false, + pushPath: false, + path: [ + { tag: 'value', + value: { value: _descriptor_6.toValue(12n), + alignment: _descriptor_6.alignment() } }] } }, + { popeq: { cached: false, + result: undefined } }]).value); + } + }; +} +const _emptyContext = { + currentQueryContext: new __compactRuntime.QueryContext(new __compactRuntime.ContractState().data, __compactRuntime.dummyContractAddress()) +}; +const _dummyContract = new Contract({ }); +export const pureCircuits = {}; +export const contractReferenceLocations = + { tag: 'publicLedgerArray', indices: { } }; +//# sourceMappingURL=index.js.map diff --git a/examples/fungible-token/artifacts/TokenExample/contract/index.js.map b/examples/fungible-token/artifacts/TokenExample/contract/index.js.map new file mode 100644 index 0000000..d24b1b7 --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/contract/index.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "file": "index.js", + "sourceRoot": "../../../", + "sources": ["contracts/TokenExample.compact", "contracts/./token/FungibleToken.compact", "contracts/./token/../security/Initializable.compact", "contracts/./token/../utils/Utils.compact"], + "names": [], + "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyCA;;;;;;;;;;MAsBA,AAAA,IAEC;;;;;;;;;;;;;;;;;;;;;;OAAA;MAED,AAAA,MAEC;;;;;;;;;;;;;;;;;;;;;;OAAA;MAED,AAAA,QAEC;;;;;;;;;;;;;;;;;;;;;;OAAA;MAED,AAAA,WAEC;;;;;;;;;;;;;;;;;;;;;;OAAA;MAED,AAAA,SAEC;;;;;cAFwB,SAA2C;;;;;;;;;;;;;;;;;;yCAA3C,SAA2C;;;;;;;sEAA3C,SAA2C;;;OAEnE;;;;;;;;;;;;;;;;GAtBA;EAlBD;;;;;UACE,OAAuB;UACvB,SAAyB;UACzB,WAAkB;UAClB,WAAoB;UACpB,YAAqB;UACrB,SAAiB;UACjB,SAAiB;UACjB,aAAoB;UACpB,MAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICYd;;;;;;;;;;yEAA4E;IAS5E;;;;;;;;;;yEACmF;IAEnF;;;;;;;;;yEAAsC;IAEtC;;;;;;;;;yEAA6C;IAC7C;;;;;;;;;yEAA+C;IAC/C;;;;;;;;;yEAAwC;IClExC;;;;;;;;;yEAAsC;IFsBxC;;;;;;;;;yEAAkC;IAClC;;;;;;;;;yEAAmC;IACnC;;;;;;;;;yEAA+B;IAC/B;;;;;;;;;yEAA+B;IAC/B;;;;;;;;;yEAAkC;IAClC;;;;;;;;;yEAA4B;;;uBAaD,OAAK;uBAAE,SAAO;uBAAE,WAAS;IAClD;;;;;;;2HAAoB,WAAS;;yEAArB;IACR;;;;;;;2HAAqB,YAAU;;yEAAtB;IACT;;;;;;;4HAAkB,SAAO;;yEAAnB;IACN;;;;;;;2HAAkB,SAAO;;yEAAnB;IACN;;;;;;;2HAAsB,aAAW;;yEAAvB;IACV;;;;;;;2HAAe,MAAI;;yEAAhB;;;;;;;GACJ;EC0DC,AAAA,aASC,4BARgB,OAAuB,EACvB,SAAyB,EACzB,WAAkB;;IAGjC;;;;;;;2HAAiB,OAAK;;yEAAjB;IACL;;;;;;;2HAAmB,SAAO;;yEAAnB;IACP;;;;;;;2HAAqB,WAAS;;yEAArB;;GACV;EAaD,AAAA,OAGC;;mCADQ;;;;;;;;;;;wGAAK;GACb;EAaD,AAAA,SAGC;;mCADQ;;;;;;;;;;;wGAAO;GACf;EAaD,AAAA,WAGC;;mCADQ;;;;;;;;;;;wGAAS;GACjB;EAaD,AAAA,cAGC;;mCADQ;;;;;;;;;;;wGAAY;GACpB;EAgBD,AAAA,YASC,4BATwB,SAA2C;;UAE5D,WAAmE,wBAAR,SAAO;iCAEnE;;;;;;;;;;;wJAA0B,WAAS;;;;sGAA1B;;;;qCAIP;;;;;;;;;;;;;;gIAA0B,WAAS;;;0GAA1B;;GACjB;EC9LD,AAAA,aAGC;;IADC;;;;;;;;;yEAAc;;GACf;EAaD,AAAA,oBAEC;oDADQ;;;;;;;;;;;yHAAc;;;GACtB;EAaD,AAAA,uBAEC;qDADS;;;;;;;;;;;0HAAc;;;GACvB;ECgCD,AAAA,eAMC,CALqB,OAAqB;QAElC,OAAK;;qBAC6B,OAAK;;;gEACgB,OAAK;;GACpE;EHjCH,AAAA,OAEC;;GAAA;EAED,AAAA,SAEC;;GAAA;EAED,AAAA,WAEC;;GAAA;EAED,AAAA,cAEC;;GAAA;EAED,AAAA,YAEC,4BAFwB,SAA2C;wDACnC,SAAO;GACvC;;;;;;;;;;;;;;;;IA/CD;qCAAA;;;;;;;;;;;0GAAkC;KAAA;IAClC;qCAAA;;;;;;;;;;;0GAAmC;KAAA;IACnC;sCAAA;;;;;;;;;;;2GAA+B;KAAA;IAC/B;qCAAA;;;;;;;;;;;0GAA+B;KAAA;IAC/B;qCAAA;;;;;;;;;;;0GAAkC;KAAA;IAClC;qCAAA;;;;;;;;;;;0GAA4B;KAAA;;;;;;;;;;" +} diff --git a/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.prover b/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.prover new file mode 100644 index 0000000..a0c6900 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.prover differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.verifier b/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.verifier new file mode 100644 index 0000000..18142a9 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/balanceOf.verifier differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/decimals.prover b/examples/fungible-token/artifacts/TokenExample/keys/decimals.prover new file mode 100644 index 0000000..4e6ef4d Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/decimals.prover differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/decimals.verifier b/examples/fungible-token/artifacts/TokenExample/keys/decimals.verifier new file mode 100644 index 0000000..1ae8659 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/decimals.verifier differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/name.prover b/examples/fungible-token/artifacts/TokenExample/keys/name.prover new file mode 100644 index 0000000..cc72c8a Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/name.prover differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/name.verifier b/examples/fungible-token/artifacts/TokenExample/keys/name.verifier new file mode 100644 index 0000000..5748f2a Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/name.verifier differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/symbol.prover b/examples/fungible-token/artifacts/TokenExample/keys/symbol.prover new file mode 100644 index 0000000..7dee250 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/symbol.prover differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/symbol.verifier b/examples/fungible-token/artifacts/TokenExample/keys/symbol.verifier new file mode 100644 index 0000000..023ff8b Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/symbol.verifier differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.prover b/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.prover new file mode 100644 index 0000000..a86951c Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.prover differ diff --git a/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.verifier b/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.verifier new file mode 100644 index 0000000..7872888 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/keys/totalSupply.verifier differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.bzkir b/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.bzkir new file mode 100644 index 0000000..2f619b6 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.bzkir differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.zkir b/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.zkir new file mode 100644 index 0000000..2e84c63 --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/zkir/balanceOf.zkir @@ -0,0 +1,109 @@ +{ + "version": { "major": 2, "minor": 0 }, + "do_communications_commitment": true, + "num_inputs": 5, + "instructions": [ + { "op": "constrain_to_boolean", "var": 0 }, + { "op": "constrain_bits", "var": 1, "bits": 8 }, + { "op": "constrain_bits", "var": 2, "bits": 248 }, + { "op": "constrain_bits", "var": 3, "bits": 8 }, + { "op": "constrain_bits", "var": 4, "bits": 248 }, + { "op": "load_imm", "imm": "01" }, + { "op": "load_imm", "imm": "30" }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 5, "count": 1 }, + { "op": "load_imm", "imm": "50" }, + { "op": "load_imm", "imm": "06" }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 8 }, + { "op": "pi_skip", "guard": 5, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0C" }, + { "op": "declare_pub_input", "var": 10 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 9 }, + { "op": "pi_skip", "guard": 5, "count": 4 }, + { "op": "assert", "cond": 9 }, + { "op": "load_imm", "imm": "00" }, + { "op": "cond_select", "bit": 0, "a": 1, "b": 11 }, + { "op": "cond_select", "bit": 0, "a": 2, "b": 11 }, + { "op": "cond_select", "bit": 0, "a": 11, "b": 3 }, + { "op": "cond_select", "bit": 0, "a": 11, "b": 4 }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 5, "count": 1 }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 11 }, + { "op": "pi_skip", "guard": 5, "count": 4 }, + { "op": "load_imm", "imm": "10" }, + { "op": "load_imm", "imm": "03" }, + { "op": "load_imm", "imm": "20" }, + { "op": "declare_pub_input", "var": 16 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 17 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 18 }, + { "op": "declare_pub_input", "var": 18 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 12 }, + { "op": "declare_pub_input", "var": 13 }, + { "op": "declare_pub_input", "var": 14 }, + { "op": "declare_pub_input", "var": 15 }, + { "op": "pi_skip", "guard": 5, "count": 11 }, + { "op": "load_imm", "imm": "18" }, + { "op": "declare_pub_input", "var": 19 }, + { "op": "pi_skip", "guard": 5, "count": 1 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0D" }, + { "op": "declare_pub_input", "var": 21 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 20 }, + { "op": "pi_skip", "guard": 5, "count": 4 }, + { "op": "cond_select", "bit": 20, "a": 6, "b": 11 }, + { "op": "declare_pub_input", "var": 22 }, + { "op": "pi_skip", "guard": 20, "count": 1 }, + { "op": "cond_select", "bit": 20, "a": 7, "b": 11 }, + { "op": "declare_pub_input", "var": 23 }, + { "op": "declare_pub_input", "var": 20 }, + { "op": "declare_pub_input", "var": 20 }, + { "op": "cond_select", "bit": 20, "a": 11, "b": 11 }, + { "op": "declare_pub_input", "var": 24 }, + { "op": "pi_skip", "guard": 20, "count": 4 }, + { "op": "cond_select", "bit": 20, "a": 7, "b": 11 }, + { "op": "declare_pub_input", "var": 25 }, + { "op": "cond_select", "bit": 20, "a": 17, "b": 11 }, + { "op": "declare_pub_input", "var": 26 }, + { "op": "declare_pub_input", "var": 20 }, + { "op": "cond_select", "bit": 20, "a": 18, "b": 11 }, + { "op": "declare_pub_input", "var": 27 }, + { "op": "cond_select", "bit": 20, "a": 18, "b": 11 }, + { "op": "declare_pub_input", "var": 28 }, + { "op": "cond_select", "bit": 20, "a": 0, "b": 11 }, + { "op": "declare_pub_input", "var": 29 }, + { "op": "cond_select", "bit": 20, "a": 12, "b": 11 }, + { "op": "declare_pub_input", "var": 30 }, + { "op": "cond_select", "bit": 20, "a": 13, "b": 11 }, + { "op": "declare_pub_input", "var": 31 }, + { "op": "cond_select", "bit": 20, "a": 14, "b": 11 }, + { "op": "declare_pub_input", "var": 32 }, + { "op": "cond_select", "bit": 20, "a": 15, "b": 11 }, + { "op": "declare_pub_input", "var": 33 }, + { "op": "pi_skip", "guard": 20, "count": 10 }, + { "op": "public_input", "guard": 20 }, + { "op": "cond_select", "bit": 20, "a": 10, "b": 11 }, + { "op": "declare_pub_input", "var": 35 }, + { "op": "declare_pub_input", "var": 20 }, + { "op": "cond_select", "bit": 20, "a": 16, "b": 11 }, + { "op": "declare_pub_input", "var": 36 }, + { "op": "cond_select", "bit": 20, "a": 34, "b": 11 }, + { "op": "declare_pub_input", "var": 37 }, + { "op": "pi_skip", "guard": 20, "count": 4 }, + { "op": "cond_select", "bit": 20, "a": 34, "b": 11 }, + { "op": "output", "var": 38 } + ] +} diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/decimals.bzkir b/examples/fungible-token/artifacts/TokenExample/zkir/decimals.bzkir new file mode 100644 index 0000000..fdd5c77 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/zkir/decimals.bzkir differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/decimals.zkir b/examples/fungible-token/artifacts/TokenExample/zkir/decimals.zkir new file mode 100644 index 0000000..4f4381d --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/zkir/decimals.zkir @@ -0,0 +1,41 @@ +{ + "version": { "major": 2, "minor": 0 }, + "do_communications_commitment": true, + "num_inputs": 0, + "instructions": [ + { "op": "load_imm", "imm": "01" }, + { "op": "load_imm", "imm": "30" }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "50" }, + { "op": "load_imm", "imm": "06" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 3 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0C" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 4 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "assert", "cond": 4 }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "05" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "output", "var": 7 } + ] +} diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/name.bzkir b/examples/fungible-token/artifacts/TokenExample/zkir/name.bzkir new file mode 100644 index 0000000..de80e26 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/zkir/name.bzkir differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/name.zkir b/examples/fungible-token/artifacts/TokenExample/zkir/name.zkir new file mode 100644 index 0000000..ce6da60 --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/zkir/name.zkir @@ -0,0 +1,42 @@ +{ + "version": { "major": 2, "minor": 0 }, + "do_communications_commitment": true, + "num_inputs": 0, + "instructions": [ + { "op": "load_imm", "imm": "01" }, + { "op": "load_imm", "imm": "30" }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "50" }, + { "op": "load_imm", "imm": "06" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 3 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0C" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 4 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "assert", "cond": 4 }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "03" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "-01" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 8 }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "output", "var": 7 } + ] +} diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/symbol.bzkir b/examples/fungible-token/artifacts/TokenExample/zkir/symbol.bzkir new file mode 100644 index 0000000..ef85284 Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/zkir/symbol.bzkir differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/symbol.zkir b/examples/fungible-token/artifacts/TokenExample/zkir/symbol.zkir new file mode 100644 index 0000000..a6482e8 --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/zkir/symbol.zkir @@ -0,0 +1,42 @@ +{ + "version": { "major": 2, "minor": 0 }, + "do_communications_commitment": true, + "num_inputs": 0, + "instructions": [ + { "op": "load_imm", "imm": "01" }, + { "op": "load_imm", "imm": "30" }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "50" }, + { "op": "load_imm", "imm": "06" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 3 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0C" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 4 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "assert", "cond": 4 }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "04" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "-01" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 8 }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "output", "var": 7 } + ] +} diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.bzkir b/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.bzkir new file mode 100644 index 0000000..02726de Binary files /dev/null and b/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.bzkir differ diff --git a/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.zkir b/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.zkir new file mode 100644 index 0000000..6479c1d --- /dev/null +++ b/examples/fungible-token/artifacts/TokenExample/zkir/totalSupply.zkir @@ -0,0 +1,42 @@ +{ + "version": { "major": 2, "minor": 0 }, + "do_communications_commitment": true, + "num_inputs": 0, + "instructions": [ + { "op": "load_imm", "imm": "01" }, + { "op": "load_imm", "imm": "30" }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "50" }, + { "op": "load_imm", "imm": "06" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 3 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "0C" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 4 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "assert", "cond": 4 }, + { "op": "declare_pub_input", "var": 1 }, + { "op": "pi_skip", "guard": 0, "count": 1 }, + { "op": "load_imm", "imm": "02" }, + { "op": "declare_pub_input", "var": 2 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 6 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "public_input", "guard": null }, + { "op": "load_imm", "imm": "10" }, + { "op": "declare_pub_input", "var": 5 }, + { "op": "declare_pub_input", "var": 0 }, + { "op": "declare_pub_input", "var": 8 }, + { "op": "declare_pub_input", "var": 7 }, + { "op": "pi_skip", "guard": 0, "count": 4 }, + { "op": "output", "var": 7 } + ] +} diff --git a/examples/fungible-token/compact.toml b/examples/fungible-token/compact.toml new file mode 100644 index 0000000..57bc56f --- /dev/null +++ b/examples/fungible-token/compact.toml @@ -0,0 +1,42 @@ +# compact-deploy config for the TokenExample example. +# All paths resolve against this file's directory. + +[profile] +default_network = "local" +artifacts_dir = "artifacts" +deployments_dir = "deployments" + +[networks.local] +network_id = "undeployed" +indexer = "http://127.0.0.1:8088/api/v4/graphql" +indexer_ws = "ws://127.0.0.1:8088/api/v4/graphql/ws" +node = "http://127.0.0.1:9944" +node_ws = "ws://127.0.0.1:9944" +proof_server = "http://127.0.0.1:6300" +wallet = { source = "local", index = 0 } + +[networks.preview] +network_id = "preview" +indexer = "https://indexer.preview.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preview.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preview.midnight.network" +node_ws = "wss://rpc.preview.midnight.network" +proof_server = "auto" +explorer = "https://preview.midnightexplorer.com" + +[networks.preprod] +network_id = "preprod" +indexer = "https://indexer.preprod.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preprod.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preprod.midnight.network" +node_ws = "wss://rpc.preprod.midnight.network" +proof_server = "auto" +explorer = "https://preprod.midnightexplorer.com" + +[contracts.TokenExample] +artifact = "TokenExample" +signing_key_file = "deploy/TokenExample.signingkey" +# Args come from this module ref. The CLI flow (`yarn cli:*`) reads +# them from here. The programmatic flow (`yarn deploy:*`) overrides +# them inline via the `args:` field on `runDeploy()`. +args = { module = "./deploy/TokenExample.args.mjs", export = "args" } diff --git a/examples/fungible-token/contracts/TokenExample.compact b/examples/fungible-token/contracts/TokenExample.compact new file mode 100644 index 0000000..48961c8 --- /dev/null +++ b/examples/fungible-token/contracts/TokenExample.compact @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +// Educational example for @openzeppelin/compact-deployer. +// +// TokenExample wraps the OpenZeppelin Compact `FungibleToken` module +// and exposes a deliberately rich constructor so the deploy walkthrough +// shows how `compact-deploy` passes every common Compact primitive type +// to a contract: +// +// Opaque<"string"> strings (name, symbol) +// Uint<8> small unsigned ints (decimals) +// Uint<32> medium uints (feeBps) +// Uint<64> medium-large uints (quorum) +// Uint<128> large uints (maxSupply) +// Boolean booleans (isMintable) +// Bytes<32> fixed-size byte arrays (treasury address-like) +// Bytes<8> smaller fixed-size byte arrays (tag) +// +// Extra constructor args beyond what FungibleToken_initialize needs are +// saved into ledger fields so each value is observable on-chain after +// deploy. +// +// NOT for production use — minimal wrapper, no access control, no +// supply cap enforcement. + +pragma language_version >= 0.21.0; + +import CompactStandardLibrary; + +import "./token/FungibleToken" prefix FungibleToken_; + +export { ContractAddress, Either, Maybe }; + +// ── Per-deploy configuration captured from the constructor ── + +export ledger treasury: Bytes<32>; +export ledger maxSupply: Uint<128>; +export ledger feeBps: Uint<32>; +export ledger quorum: Uint<64>; +export ledger isMintable: Boolean; +export ledger tag: Bytes<8>; + +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, + _treasury: Bytes<32>, + _maxSupply: Uint<128>, + _feeBps: Uint<32>, + _quorum: Uint<64>, + _isMintable: Boolean, + _tag: Bytes<8>, +) { + FungibleToken_initialize(_name, _symbol, _decimals); + treasury = disclose(_treasury); + maxSupply = disclose(_maxSupply); + feeBps = disclose(_feeBps); + quorum = disclose(_quorum); + isMintable = disclose(_isMintable); + tag = disclose(_tag); +} + +// ── Pass-throughs to FungibleToken so the deployed contract is usable ── + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} diff --git a/examples/fungible-token/contracts/security/Initializable.compact b/examples/fungible-token/contracts/security/Initializable.compact new file mode 100644 index 0000000..ae1b93c --- /dev/null +++ b/examples/fungible-token/contracts/security/Initializable.compact @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (security/Initializable.compact) + +pragma language_version >= 0.21.0; + +/** + * @module Initializable + * @description Initializable provides a simple mechanism that mimics the functionality of a constructor. + */ +module Initializable { + import CompactStandardLibrary; + + export ledger _isInitialized: Boolean; + + /** + * @description Initializes the state thus ensuring the calling circuit can only be called once. + * + * @circuitInfo k=10, rows=38 + * + * Requirements: + * + * - Contract must not be initialized. + * + * @return {[]} - Empty tuple. + */ + export circuit initialize(): [] { + assertNotInitialized(); + _isInitialized = true; + } + + /** + * @description Asserts that the contract has been initialized, throwing an error if not. + * + * @circuitInfo k=10, rows=31 + * + * Requirements: + * + * - Contract must be initialized. + * + * @return {[]} - Empty tuple. + */ + export circuit assertInitialized(): [] { + assert(_isInitialized, "Initializable: contract not initialized"); + } + + /** + * @description Asserts that the contract has not been initialized, throwing an error if it has. + * + * @circuitInfo k=10, rows=35 + * + * Requirements: + * + * - Contract must not be initialized. + * + * @return {[]} - Empty tuple. + */ + export circuit assertNotInitialized(): [] { + assert(!_isInitialized, "Initializable: contract already initialized"); + } +} diff --git a/examples/fungible-token/contracts/token/FungibleToken.compact b/examples/fungible-token/contracts/token/FungibleToken.compact new file mode 100644 index 0000000..2320833 --- /dev/null +++ b/examples/fungible-token/contracts/token/FungibleToken.compact @@ -0,0 +1,728 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (token/FungibleToken.compact) + +pragma language_version >= 0.21.0; + +/** + * @module FungibleToken + * @description An unshielded FungibleToken library. + * + * @notice One notable difference regarding this implementation and the EIP20 spec + * consists of the token size. Uint<128> is used as the token size because Uint<256> + * cannot be supported. + * This is due to encoding limits on the midnight circuit backend: + * https://github.com/midnightntwrk/compactc/issues/929 + * + * @dev Canonicalization + * All `Either, ContractAddress>` values are canonicalized before use as map keys + * or in ledger writes. Canonicalization zeroes out the inactive branch of the Either, + * ensuring that two values with the same active branch always resolve to the same map key + * regardless of what data the inactive branch carries. Write paths are canonicalized in + * `_update` (for `_balances`) and `_approve` (for `_allowances`). Read paths are + * canonicalized in `balanceOf` and `allowance`. `_spendAllowance` canonicalizes + * independently to ensure consistent lookups before delegating to `_approve`. + * + * @notice At the moment Midnight does not support contract-to-contract communication, but + * there are ongoing efforts to enable this in the future. Thus, the main circuits of this module + * restrict developers from sending tokens to contracts; however, we provide developers + * the ability to experiment with sending tokens to contracts using the `_unsafe` + * transfer methods. Once contract-to-contract communication is available we will follow the + * deprecation plan outlined below: + * + * Initial Minor Version Change: + * + * - Mark _unsafeFN as deprecated and emit a warning if possible. + * - Keep its implementation intact so existing callers continue to work. + * + * Later Major Version Change: + * + * - Drop _unsafeFN and remove `isContract` guard from `FN`. + * - By this point, anyone using _unsafeFN should have migrated to the now C2C-capable `FN`. + * + * Due to the vast incompatibilities with the EIP20 spec, it is our + * opinion that this implementation should not be called ERC20 at this time + * as this would be both very confusing and misleading. This may change as more + * features become available. The list of missing features is as follows: + * + * - Full uint256 support. + * - Events. + * - Contract-to-contract calls. + */ +module FungibleToken { + import CompactStandardLibrary; + import "../security/Initializable" prefix Initializable_; + import "../utils/Utils" prefix Utils_; + + /** + * @description Mapping from account addresses to their token balances. + * @type {Either, ContractAddress>} account - The account address. + * @type {Uint<128>} balance - The balance of the account. + * @type {Map} + * @type {Map, ContractAddress>, Uint<128>>} _balances + */ + export ledger _balances: Map, ContractAddress>, Uint<128>>; + /** + * @description Mapping from owner accounts to spender accounts and their allowances. + * @type {Either, ContractAddress>} account - The owner account address. + * @type {Either, ContractAddress>} spender - The spender account address. + * @type {Uint<128>} allowance - The amount allowed to be spent by the spender. + * @type {Map>} + * @type {Map, ContractAddress>, Map, ContractAddress>, Uint<128>>>} _allowances + */ + export ledger _allowances: Map, ContractAddress>, + Map, ContractAddress>, Uint<128>>>; + + export ledger _totalSupply: Uint<128>; + + export sealed ledger _name: Opaque<"string">; + export sealed ledger _symbol: Opaque<"string">; + export sealed ledger _decimals: Uint<8>; + + + /** + * @witness wit_FungibleTokenSK + * @description Returns the caller's secret key used in deriving the account identifier. + * + * The same key produces the same account identifier across all contracts. Users who + * desire cross-contract unlinkability should use different keys per contract. + * + * @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value. + */ + witness wit_FungibleTokenSK(): Bytes<32>; + + /** + * @description Returns a canonical zero Either value for Bytes<32> and ContractAddress. + * This circuit returns the left variant (Bytes<32>) to avoid misleading contract-to-contract + * error messages. + * + * @return {Either, ContractAddress>} - The zero value. + */ + export pure circuit ZERO(): Either, ContractAddress> { + return Either, ContractAddress> { + is_left: true, left: default>, right: default + }; + } + + /** + * @description Initializes the contract by setting the name, symbol, and decimals. + * @dev This MUST be called in the implementing contract's constructor. Failure to do so + * can lead to an irreparable contract. + * + * @circuitInfo k=10, rows=71 + * + * @param {Opaque<"string">} name_ - The name of the token. + * @param {Opaque<"string">} symbol_ - The symbol of the token. + * @param {Uint<8>} decimals_ - The number of decimals used to get the user representation. + * @return {[]} - Empty tuple. + */ + export circuit initialize( + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8> + ): [] { + Initializable_initialize(); + _name = disclose(name_); + _symbol = disclose(symbol_); + _decimals = disclose(decimals_); + } + + /** + * @description Returns the token name. + * + * @circuitInfo k=6, rows=28 + * + * Requirements: + * + * - Contract is initialized. + * + * @return {Opaque<"string">} - The token name. + */ + export circuit name(): Opaque<"string"> { + Initializable_assertInitialized(); + return _name; + } + + /** + * @description Returns the symbol of the token. + * + * @circuitInfo k=6, rows=28 + * + * Requirements: + * + * - Contract is initialized. + * + * @return {Opaque<"string">} - The token name. + */ + export circuit symbol(): Opaque<"string"> { + Initializable_assertInitialized(); + return _symbol; + } + + /** + * @description Returns the number of decimals used to get its user representation. + * + * @circuitInfo k=6, rows=28 + * + * Requirements: + * + * - Contract is initialized. + * + * @return {Uint<8>} - The account's token balance. + */ + export circuit decimals(): Uint<8> { + Initializable_assertInitialized(); + return _decimals; + } + + /** + * @description Returns the value of tokens in existence. + * + * @circuitInfo k=6, rows=28 + * + * Requirements: + * + * - Contract is initialized. + * + * @return {Uint<128>} - The total supply of tokens. + */ + export circuit totalSupply(): Uint<128> { + Initializable_assertInitialized(); + return _totalSupply; + } + + /** + * @description Returns the value of tokens owned by `account`. + * + * @circuitInfo k=10, rows=673 + * + * @dev Manually checks if `account` is a key in the map and returns 0 if it is not. + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Either, ContractAddress>} account - The account id or contract address to query. + * @return {Uint<128>} - The account's token balance. + */ + export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + Initializable_assertInitialized(); + const canonAcct = Utils_canonicalize, ContractAddress>(account); + + if (!_balances.member(disclose(canonAcct))) { + return 0; + } + + return _balances.lookup(disclose(canonAcct)); + } + + /** + * @description Moves a `value` amount of tokens from the caller's account to `to`. + * + * @circuitInfo k=13, rows=3985 + * + * @notice Transfers to contract addresses are currently disallowed until contract-to-contract + * interactions are supported in Compact. This restriction prevents assets from + * being inadvertently locked in contracts that cannot currently handle token receipt. + * + * Requirements: + * + * - Contract is initialized. + * - `to` is not a ContractAddress. + * - `to` is not the zero address. + * - The caller has a balance of at least `value`. + * + * @param {Either, ContractAddress>} to - The recipient of the transfer, either a user or a contract. + * @param {Uint<128>} value - The amount to transfer. + * @return {Boolean} - As per the IERC20 spec, this MUST return true. + */ + export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + const isContractAddr = !to.is_left; + assert(!isContractAddr, "FungibleToken: unsafe transfer"); + + return _unsafeTransfer(to, value); + } + + /** + * @description Unsafe variant of `transfer` which allows transfers to contract addresses. + * + * @circuitInfo k=13, rows=3982 + * + * @warning Transfers to contract addresses are considered unsafe because contract-to-contract + * calls are not currently supported. Tokens sent to a contract address may become irretrievable. + * Once contract-to-contract calls are supported, this circuit may be deprecated. + * + * Requirements: + * + * - Contract is initialized. + * - `to` is not the zero address. + * - The caller has a balance of at least `value`. + * + * @param {Either, ContractAddress>} to - The recipient of the transfer, either a user or a contract. + * @param {Uint<128>} value - The amount to transfer. + * @return {Boolean} - As per the IERC20 spec, this MUST return true. + */ + export circuit _unsafeTransfer( + to: Either, ContractAddress>, + value: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + const owner = left, ContractAddress>(_computeAccountId()); + _unsafeUncheckedTransfer(owner, to, value); + return true; + } + + /** + * @description Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` + * through `transferFrom`. This value changes when `approve` or `transferFrom` are called. + * + * @circuitInfo k=11, rows=1346 + * + * @dev Manually checks if `owner` and `spender` are keys in the map and returns 0 if they are not. + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Either, ContractAddress>} owner - The account id or contract address of approver. + * @param {Either, ContractAddress>} spender - The account id or contract address of spender. + * @return {Uint<128>} - The `spender`'s allowance over `owner`'s tokens. + */ + export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> + ): Uint<128> { + Initializable_assertInitialized(); + const canonOwner = Utils_canonicalize, ContractAddress>(owner); + const canonSpender = Utils_canonicalize, ContractAddress>(spender); + + if (!_allowances.member(disclose(canonOwner)) || !_allowances.lookup(canonOwner).member(disclose(canonSpender))) { + return 0; + } + + return _allowances.lookup(canonOwner).lookup(disclose(canonSpender)); + } + + /** + * @description Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. + * + * @circuitInfo k=13, rows=3072 + * + * Requirements: + * + * - Contract is initialized. + * - `spender` is not the zero address. + * + * @param {Either, ContractAddress>} spender - The account id or ContractAddress that may spend on behalf of the caller. + * @param {Uint<128>} value - The amount of tokens the `spender` may spend. + * @return {Boolean} - Returns a boolean value indicating whether the operation succeeded. + */ + export circuit approve(spender: Either, ContractAddress>, + value: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + + const owner = left, ContractAddress>(_computeAccountId()); + _approve(owner, spender, value); + return true; + } + + /** + * @description Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. + * `value` is the deducted from the caller's allowance. + * + * @circuitInfo k=13, rows=4960 + * + * @notice Transfers to contract addresses are currently disallowed until contract-to-contract + * interactions are supported in Compact. This restriction prevents assets from + * being inadvertently locked in contracts that cannot currently handle token receipt. + * + * Requirements: + * + * - Contract is initialized. + * - `fromAddress` is not the zero address. + * - `fromAddress` must have a balance of at least `value`. + * - `to` is not the zero address. + * - `to` is not a ContractAddress. + * - The caller has an allowance of `fromAddress`'s tokens of at least `value`. + * + * @param {Either, ContractAddress>} fromAddress - The current owner of the tokens for the transfer, either a user or a contract. + * @param {Either, ContractAddress>} to - The recipient of the transfer, either a user or a contract. + * @param {Uint<128>} value - The amount to transfer. + * @return {Boolean} - As per the IERC20 spec, this MUST return true. + */ + export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + const isContractAddr = !to.is_left; + assert(!isContractAddr, "FungibleToken: unsafe transfer"); + return _unsafeTransferFrom(fromAddress, to, value); + } + + /** + * @description Unsafe variant of `transferFrom` which allows transfers to contract addresses. + * + * @circuitInfo k=13, rows=4957 + * + * @warning Transfers to contract addresses are considered unsafe because contract-to-contract + * calls are not currently supported. Tokens sent to a contract address may become irretrievable. + * Once contract-to-contract calls are supported, this circuit may be deprecated. + * + * Requirements: + * + * - Contract is initialized. + * - `fromAddress` is not the zero address. + * - `fromAddress` must have a balance of at least `value`. + * - `to` is not the zero address. + * - The caller has an allowance of `fromAddress`'s tokens of at least `value`. + * + * @param {Either, ContractAddress>} fromAddress - The current owner of the tokens for the transfer, either a user or a contract. + * @param {Either, ContractAddress>} to - The recipient of the transfer, either a user or a contract. + * @param {Uint<128>} value - The amount to transfer. + * @return {Boolean} - As per the IERC20 spec, this MUST return true. + */ + export circuit _unsafeTransferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + + const spender = left, ContractAddress>(_computeAccountId()); + _spendAllowance(fromAddress, spender, value); + _unsafeUncheckedTransfer(fromAddress, to, value); + return true; + } + + /** + * @description Moves a `value` amount of tokens from `fromAddress` to `to`. + * This circuit is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * @circuitInfo k=12, rows=2345 + * + * @notice Transfers to contract addresses are currently disallowed until contract-to-contract + * interactions are supported in Compact. This restriction prevents assets from + * being inadvertently locked in contracts that cannot currently handle token receipt. + * + * Requirements: + * + * - Contract is initialized. + * - `fromAddress` is not be the zero address. + * - `fromAddress` must have at least a balance of `value`. + * - `to` must not be the zero address. + * - `to` must not be a ContractAddress. + * + * @param {Either, ContractAddress>} fromAddress - The owner of the tokens to transfer. + * @param {Either, ContractAddress>} to - The receipient of the transferred tokens. + * @param {Uint<128>} value - The amount of tokens to transfer. + * @return {[]} - Empty tuple. + */ + export circuit _transfer( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + const isContractAddr = !to.is_left; + assert(!isContractAddr, "FungibleToken: unsafe transfer"); + _unsafeUncheckedTransfer(fromAddress, to, value); + } + + /** + * @description Unsafe variant of `transferFrom` which allows transfers to contract addresses. + * + * @circuitInfo k=12, rows=2342 + * + * @warning Transfers to contract addresses are considered unsafe because contract-to-contract + * calls are not currently supported. Tokens sent to a contract address may become irretrievable. + * Once contract-to-contract calls are supported, this circuit may be deprecated. + * + * Requirements: + * + * - Contract is initialized. + * - `fromAddress` is not the zero address. + * - `to` is not the zero address. + * + * @param {Either, ContractAddress>} fromAddress - The owner of the tokens to transfer. + * @param {Either, ContractAddress>} to - The receipient of the transferred tokens. + * @param {Uint<128>} value - The amount of tokens to transfer. + * @return {[]} - Empty tuple. + */ + export circuit _unsafeUncheckedTransfer( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert(!_isTargetZero(fromAddress), "FungibleToken: invalid sender"); + assert(!_isTargetZero(to), "FungibleToken: invalid receiver"); + + _update(fromAddress, to, value); + } + + /** + * @description Transfers a `value` amount of tokens from `fromAddress` to `to`, or alternatively mints (or burns) if `fromAddress` + * (or `to`) is the zero address. + * @dev Checks for a mint overflow in order to output a more readable error message. + * + * @circuitInfo k=11, rows=1305 + * + * Requirements: + * + * - Contract is initialized. + * + * @param {Either, ContractAddress>} fromAddress - The original owner of the tokens moved (which is 0 if tokens are minted). + * @param {Either, ContractAddress>} to - The recipient of the tokens moved (which is 0 if tokens are burned). + * @param {Uint<128>} value - The amount of tokens moved from `fromAddress` to `to`. + * @return {[]} - Empty tuple. + */ + circuit _update(fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + const canonFrom = Utils_canonicalize, ContractAddress>(fromAddress); + const canonTo = Utils_canonicalize, ContractAddress>(to); + + if (_isTargetZero(disclose(canonFrom))) { + // Mint + const MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow"); + + _totalSupply = disclose(_totalSupply + value as Uint<128>); + } else { + const fromBal = balanceOf(canonFrom); + assert(fromBal >= value, "FungibleToken: insufficient balance"); + _balances.insert(disclose(canonFrom), disclose(fromBal - value as Uint<128>)); + } + + if (_isTargetZero(disclose(canonTo))) { + // Burn + _totalSupply = disclose(_totalSupply - value as Uint<128>); + } else { + const toBal = balanceOf(canonTo); + _balances.insert(disclose(canonTo), disclose(toBal + value as Uint<128>)); + } + } + + /** + * @description Creates a `value` amount of tokens and assigns them to `account`, + * by transferring it from the zero address. Relies on the `update` mechanism. + * + * @circuitInfo k=11, rows=1437 + * + * @notice Transfers to contract addresses are currently disallowed until contract-to-contract + * interactions are supported in Compact. This restriction prevents assets from + * being inadvertently locked in contracts that cannot currently handle token receipt. + * + * Requirements: + * + * - Contract is initialized. + * - `account` is not a ContractAddress. + * - `account` is not the zero address. + * + * @param {Either, ContractAddress>} account - The recipient of tokens minted. + * @param {Uint<128>} value - The amount of tokens minted. + * @return {[]} - Empty tuple. + */ + export circuit _mint(account: Either, ContractAddress>, value: Uint<128>): [] { + Initializable_assertInitialized(); + const isContractAddr = !account.is_left; + assert(!isContractAddr, "FungibleToken: unsafe transfer"); + _unsafeMint(account, value); + } + + /** + * @description Unsafe variant of `_mint` which allows transfers to contract addresses. + * + * @circuitInfo k=11, rows=1434 + * + * @warning Transfers to contract addresses are considered unsafe because contract-to-contract + * calls are not currently supported. Tokens sent to a contract address may become irretrievable. + * Once contract-to-contract calls are supported, this circuit may be deprecated. + * + * Requirements: + * + * - Contract is initialized. + * - `account` is not the zero address. + * + * @param {Either, ContractAddress>} account - The recipient of tokens minted. + * @param {Uint<128>} value - The amount of tokens minted. + * @return {[]} - Empty tuple. + */ + export circuit _unsafeMint( + account: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert(!_isTargetZero(account), "FungibleToken: invalid receiver"); + _update(ZERO(), account, value); + } + + /** + * @description Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * @circuitInfo k=11, rows=1377 + * + * Requirements: + * + * - Contract is initialized. + * - `account` is not the zero address. + * - `account` must have at least a balance of `value`. + * + * @param {Either, ContractAddress>} account - The target owner of tokens to burn. + * @param {Uint<128>} value - The amount of tokens to burn. + * @return {[]} - Empty tuple. + */ + export circuit _burn(account: Either, ContractAddress>, value: Uint<128>): [] { + Initializable_assertInitialized(); + assert(!_isTargetZero(account), "FungibleToken: invalid sender"); + _update(account, ZERO(), value); + } + + /** + * @description Sets `value` as the allowance of `spender` over the `owner`'s tokens. + * This circuit is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * @circuitInfo k=11, rows=1406 + * + * Requirements: + * + * - Contract is initialized. + * - `owner` is not the zero address. + * - `spender` is not the zero address. + * + * @param {Either, ContractAddress>} owner - The owner of the tokens. + * @param {Either, ContractAddress>} spender - The spender of the tokens. + * @param {Uint<128>} value - The amount of tokens `spender` may spend on behalf of `owner`. + * @return {[]} - Empty tuple. + */ + export circuit _approve( + owner: Either, ContractAddress>, + spender: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + const canonOwner = Utils_canonicalize, ContractAddress>(owner); + const canonSpender = Utils_canonicalize, ContractAddress>(spender); + + assert(!_isTargetZero(canonOwner), "FungibleToken: invalid owner"); + assert(!_isTargetZero(canonSpender), "FungibleToken: invalid spender"); + + if (!_allowances.member(disclose(canonOwner))) { + // If owner doesn't exist, create and insert a new sub-map directly + _allowances.insert( + disclose(canonOwner), + default, ContractAddress>, Uint<128>>> + ); + } + _allowances.lookup(canonOwner).insert(disclose(canonSpender), disclose(value)); + } + + /** + * @description Updates `owner`'s allowance for `spender` based on spent `value`. + * Does not update the allowance value in case of infinite allowance. + * + * @circuitInfo k=11, rows=1729 + * + * Requirements: + * + * - Contract is initialized. + * - `spender` must have at least an allowance of `value` from `owner`. + * + * @param {Either, ContractAddress>} owner - The owner of the tokens. + * @param {Either, ContractAddress>} spender - The spender of the tokens. + * @param {Uint<128>} value - The amount of token allowance to spend. + * @return {[]} - Empty tuple. + */ + export circuit _spendAllowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress>, + value: Uint<128> + ): [] { + Initializable_assertInitialized(); + const canonOwner = Utils_canonicalize, ContractAddress>(owner); + const canonSpender = Utils_canonicalize, ContractAddress>(spender); + + assert((_allowances.member(disclose(canonOwner)) && + _allowances.lookup(canonOwner).member(disclose(canonSpender))), + "FungibleToken: insufficient allowance" + ); + + const currentAllowance = _allowances.lookup(canonOwner).lookup(disclose(canonSpender)); + const MAX_UINT128 = 340282366920938463463374607431768211455; + if (currentAllowance < MAX_UINT128) { + assert(currentAllowance >= value, "FungibleToken: insufficient allowance"); + _approve(canonOwner, canonSpender, currentAllowance - value as Uint<128>); + } + } + + /** + * @description Computes the caller's account identifier from the `wit_FungibleTokenSK` witness. + * + * ## ID Derivation + * `accountId = persistentHash(secretKey)` + * + * The result is a 32-byte commitment that uniquely identifies the caller. + * + * @returns {Bytes<32>} accountId - The computed account identifier. + */ + circuit _computeAccountId(): Bytes<32> { + return computeAccountId(wit_FungibleTokenSK()); + } + + /** + * @description Computes an account identifier without on-chain state, allowing a user to derive + * their identity commitment before submitting a token operation. + * This is the off-chain counterpart to {_computeAccountId} and produces an identical result + * given the same inputs. + * + * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can + * permanently compromise the security of this system: + * + * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + * application logs, analytics pipelines, or any observable side-channel. + * - **Store offline or in secure enclaves** — hardware security modules (HSMs), + * air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + * or equivalent; weak or predictable keys can be brute-forced to reveal your identity. + * - **Treat key loss as identity loss** — a lost key cannot be recovered. + * - **Avoid calling this circuit in untrusted environments** — executing this in an + * unverified browser extension, compromised runtime, or shared machine may expose + * the key to a malicious observer. + * + * ## ID Derivation + * `accountId = persistentHash(secretKey)` + * + * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value. + * + * @returns {Bytes<32>} accountId - The computed account identifier. + */ + export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return persistentHash>>([secretKey]); + } + + /** + * @description Returns `true` if `target`'s active branch (as indicated by `is_left`) + * holds the zero value. + * + * @param {Either, ContractAddress>} target - The value to check. + * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise. + */ + circuit _isTargetZero(target: Either, ContractAddress>): Boolean { + if (target.is_left) { + return target.left == default>; + } else { + return target.right == default; + } + } +} diff --git a/examples/fungible-token/contracts/utils/Utils.compact b/examples/fungible-token/contracts/utils/Utils.compact new file mode 100644 index 0000000..e23ae33 --- /dev/null +++ b/examples/fungible-token/contracts/utils/Utils.compact @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (utils/Utils.compact) + +pragma language_version >= 0.21.0; + +/** + * @module Utils. + * @description A library for common utilities used in Compact contracts. + */ +module Utils { + import CompactStandardLibrary; + + /** + * @description Returns whether `keyOrAddress` is the zero address. + * + * @notice Midnight's burn address is represented as left(default) + * in Compact, so we've chosen to represent the zero address as this structure as well. + * + * @param {Either} keyOrAddress - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @return {Boolean} - Returns true if `keyOrAddress` is zero. + */ + export pure circuit isKeyOrAddressZero( + keyOrAddress: Either + ): Boolean { + return isContractAddress(keyOrAddress) + ? default == keyOrAddress.right + : default == keyOrAddress.left; + } + + /** + * @description Returns whether `key` is the zero address. + * + * @param {ZswapCoinPublicKey} key - A ZswapCoinPublicKey + * @return {Boolean} - Returns true if `key` is zero. + */ + export pure circuit isKeyZero(key: ZswapCoinPublicKey): Boolean { + const zero = default; + return zero == key; + } + + /** + * @description Returns whether `keyOrAddress` is equal to `other`. Assumes that a ZswapCoinPublicKey + * and a ContractAddress can never be equal + * + * @param {Either} keyOrAddress - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @param {Either} other - The other value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @return {Boolean} - Returns true if `keyOrAddress` is equal to `other`. + */ + export pure circuit isKeyOrAddressEqual( + keyOrAddress: Either, + other: Either + ): Boolean { + if (keyOrAddress.is_left && other.is_left) { + return keyOrAddress.left == other.left; + } else if (!keyOrAddress.is_left && !other.is_left) { + return keyOrAddress.right == other.right; + } else { + return false; + } + } + + /** + * @description Returns whether `keyOrAddress` is a ContractAddress type. + * + * @param {Either} keyOrAddress - The target value to check, either a ZswapCoinPublicKey or a ContractAddress. + * @return {Boolean} - Returns true if `keyOrAddress` is a ContractAddress. + */ + export pure circuit isContractAddress( + keyOrAddress: Either + ): Boolean { + return !keyOrAddress.is_left; + } + + /** + * @description A helper function that returns the empty string: "". + * + * @return {Opaque<"string">} - The empty string: "". + */ + export pure circuit emptyString(): Opaque<"string"> { + return default>; + } + + /** + * @description Zeroes out the unused side of an `Either` value. Prevents crafted + * inputs where both `left` and `right` fields carry data from bypassing checks that + * only inspect the active side. + * + * @param {Either} value - The value to canonicalize. + * @return {Either} - The canonicalized value. + */ + export pure circuit canonicalize( + value: Either + ): Either { + return value.is_left + ? Either{ is_left: true, left: value.left, right: default } + : Either{ is_left: false, left: default, right: value.right }; + } +} diff --git a/examples/fungible-token/deploy/TokenExample.args.mjs b/examples/fungible-token/deploy/TokenExample.args.mjs new file mode 100644 index 0000000..2191ef6 --- /dev/null +++ b/examples/fungible-token/deploy/TokenExample.args.mjs @@ -0,0 +1,24 @@ +// Constructor args for TokenExample, read by `compact-deploy` via the +// `args = { module = "./deploy/TokenExample.args.mjs" }` ref in +// compact.toml. +// +// Order matches the contract's constructor: +// (_name, _symbol, _decimals, _treasury, _maxSupply, +// _feeBps, _quorum, _isMintable, _tag) +// +// All Compact Uint map to JS BigInt regardless of width (the +// compiler-emitted `.d.ts` types them as `bigint`). + +export function args() { + return [ + 'OpenZeppelin Example Token', // _name: string (Opaque<"string">) + 'OZE', // _symbol: string (Opaque<"string">) + 18n, // _decimals: bigint (Uint<8>) + new Uint8Array(32).fill(0xab), // _treasury: Uint8Array(32) (Bytes<32>) + 1_000_000_000_000_000_000_000_000n, // _maxSupply: bigint (Uint<128>) + 250n, // _feeBps: bigint (Uint<32>) + 7n, // _quorum: bigint (Uint<64>) + true, // _isMintable: boolean (Boolean) + new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]), // _tag: Uint8Array(8) (Bytes<8>) + ]; +} diff --git a/examples/fungible-token/deploy/deployTokenExample.ts b/examples/fungible-token/deploy/deployTokenExample.ts new file mode 100644 index 0000000..4bfe9da --- /dev/null +++ b/examples/fungible-token/deploy/deployTokenExample.ts @@ -0,0 +1,20 @@ +// Deploy script for TokenExample. The curried call form names the +// contract once via the imported `Contract` class — the deployer +// matches it to the `[contracts.TokenExample]` entry in compact.toml +// by class identity. Constructor args are typed function parameters, +// so the editor shows each param's name + type as you type each comma. + +import { runDeploy } from '@openzeppelin/compact-deployer'; +import { Contract } from '../artifacts/TokenExample/contract/index.js'; + +await runDeploy(Contract)( + 'OpenZeppelin Example Token', + 'OZE', + 18n, + new Uint8Array(32).fill(0xab), + 1_000_000_000_000_000_000_000_000n, + 250n, + 7n, + true, + new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]), +); diff --git a/examples/fungible-token/package.json b/examples/fungible-token/package.json new file mode 100644 index 0000000..887365d --- /dev/null +++ b/examples/fungible-token/package.json @@ -0,0 +1,23 @@ +{ + "name": "compact-deployer-example-fungible-token", + "version": "0.0.0", + "private": true, + "description": "compact-deploy walkthrough: deploys a TokenExample contract wrapping the OpenZeppelin Compact FungibleToken module, exercising every common Compact constructor argument type.", + "type": "module", + "scripts": { + "compile": "compact-compiler --src contracts --out artifacts --hierarchical", + "deploy:local": "node deploy/deployTokenExample.ts", + "deploy:preview": "node deploy/deployTokenExample.ts --network preview --sync-timeout 1800", + "deploy:preprod": "node deploy/deployTokenExample.ts --network preprod --sync-timeout 7200", + "cli:local": "compact-deploy TokenExample --network local", + "cli:preview": "compact-deploy TokenExample --network preview --sync-timeout 1800", + "cli:preprod": "compact-deploy TokenExample --network preprod --sync-timeout 7200" + }, + "dependencies": { + "@openzeppelin/compact-cli": "workspace:^", + "@openzeppelin/compact-deployer": "workspace:^" + }, + "engines": { + "node": ">=24" + } +} diff --git a/package.json b/package.json index a9bb156..0fc9b9e 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,17 @@ "private": true, "packageManager": "yarn@4.10.3", "workspaces": [ - "packages/*" + "packages/*", + "examples/*" ], "scripts": { "build": "turbo run build --log-prefix=none", "test": "turbo run test --log-prefix=none", + "coverage": "turbo run coverage --log-prefix=none", + "test:integration": "make test-integration", + "compile:fixtures": "make compile", + "env:up": "make env-up", + "env:down": "make env-down", "lint": "biome check .", "lint:fix": "biome check . --write", "lint:ci": "biome ci . --no-errors-on-unmatched", @@ -17,10 +23,20 @@ }, "devDependencies": { "@biomejs/biome": "2.3.8", + "@openzeppelin/compact-deployer": "workspace:^", "@types/node": "24.10.1", + "@vitest/coverage-v8": "4.0.15", + "pino": "^9.7.0", "ts-node": "^10.9.2", "turbo": "^2.6.1", "typescript": "^5.9.3", "vitest": "^4.0.15" + }, + "resolutions": { + "@midnight-ntwrk/ledger-v8": "8.0.3", + "@midnight-ntwrk/midnight-js-protocol": "file:./vendor/midnight-js-protocol-4.1.0.tgz", + "undici": "^6.24.0", + "glob": "^11.0.0", + "uuid": "^13.0.0" } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 11c8d91..ef2adab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,12 +1,13 @@ { "name": "@openzeppelin/compact-cli", - "description": "CLI for compiling and building Compact smart contracts", + "description": "CLI for compiling, building, and deploying Compact smart contracts", "version": "0.0.1", "keywords": [ "compact", "cli", "compiler", "builder", + "deployer", "testing" ], "author": "OpenZeppelin Community ", @@ -14,7 +15,8 @@ "type": "module", "exports": { "./run-builder": "./dist/runBuilder.js", - "./run-compiler": "./dist/runCompiler.js" + "./run-compiler": "./dist/runCompiler.js", + "./run-deploy": "./dist/runDeploy.js" }, "files": [ "dist", @@ -22,27 +24,34 @@ "LICENSE" ], "engines": { - "node": ">=20" + "node": ">=24" }, "bin": { "compact-builder": "dist/runBuilder.js", - "compact-compiler": "dist/runCompiler.js" + "compact-compiler": "dist/runCompiler.js", + "compact-deploy": "dist/runDeploy.js" }, "scripts": { "build": "tsc -p .", "types": "tsc -p tsconfig.json --noEmit", "test": "yarn vitest run", + "coverage": "yarn vitest run --coverage", "clean": "git clean -fXd" }, "devDependencies": { "@tsconfig/node24": "^24.0.3", "@types/node": "24.10.1", + "@types/ws": "^8.5.10", "typescript": "^5.9.3", "vitest": "^4.0.15" }, "dependencies": { "@openzeppelin/compact-builder": "workspace:^", + "@openzeppelin/compact-deployer": "workspace:^", "chalk": "^5.6.2", - "ora": "^9.0.0" + "ora": "^9.0.0", + "pino": "^9.7.0", + "pino-pretty": "^13.0.0", + "ws": "^8.16.0" } } diff --git a/packages/cli/src/logger.ts b/packages/cli/src/logger.ts new file mode 100644 index 0000000..4f6b267 --- /dev/null +++ b/packages/cli/src/logger.ts @@ -0,0 +1,65 @@ +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import pino, { type Logger } from 'pino'; + +/** + * Pino factory for the three CLI modes: `--json` (raw JSON, no transports), + * default (pretty `info+`), `--verbose` (pretty `info+` to stdout AND + * `debug+` mirrored to `.compact/logs/.log` so the transcript survives + * spinner overwrites). + */ +export interface CreateLoggerOptions { + verbose: boolean; + json: boolean; + logDir?: string; +} + +export function createLogger(opts: CreateLoggerOptions): Logger { + if (opts.json) { + return pino({ level: opts.verbose ? 'debug' : 'info' }); + } + + if (opts.verbose) { + const dir = opts.logDir ?? join(process.cwd(), '.compact', 'logs'); + mkdirSync(dir, { recursive: true }); + const file = join( + dir, + `${new Date().toISOString().replace(/[:.]/g, '-')}.log`, + ); + return pino( + { level: 'debug' }, + pino.transport({ + targets: [ + { + target: 'pino/file', + options: { destination: file }, + level: 'debug', + }, + { + target: 'pino-pretty', + options: { + destination: 1, + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + }, + level: 'info', + }, + ], + }), + ); + } + + return pino( + { level: 'info' }, + pino.transport({ + target: 'pino-pretty', + options: { + destination: 1, + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + }, + }), + ); +} diff --git a/packages/cli/src/prompt.ts b/packages/cli/src/prompt.ts new file mode 100644 index 0000000..a1e1f3b --- /dev/null +++ b/packages/cli/src/prompt.ts @@ -0,0 +1,48 @@ +import { stdin, stdout } from 'node:process'; + +/** Prompt for a keystore passphrase with terminal echo suppressed; falls back to plain line-read off a TTY. */ +export async function promptPassphrase(label: string): Promise { + stdout.write(`Passphrase for ${label}: `); + return readMaskedLine(); +} + +function readMaskedLine(): Promise { + return new Promise((resolveFn, rejectFn) => { + let buffer = ''; + const isTTY = stdin.isTTY === true; + + const cleanup = () => { + if (isTTY) stdin.setRawMode(false); + stdin.pause(); + stdin.removeListener('data', onData); + stdout.write('\n'); + }; + + const onData = (chunk: Buffer) => { + const s = chunk.toString('utf8'); + for (const ch of s) { + const code = ch.charCodeAt(0); + if (code === 0x03) { + cleanup(); + rejectFn(new Error('Aborted')); + return; + } + if (code === 0x0d || code === 0x0a) { + cleanup(); + resolveFn(buffer); + return; + } + if (code === 0x7f || code === 0x08) { + buffer = buffer.slice(0, -1); + continue; + } + buffer += ch; + } + }; + + if (isTTY) stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding('utf8'); + stdin.on('data', onData); + }); +} diff --git a/packages/cli/src/runBuilder.ts b/packages/cli/src/runBuilder.ts index 3719973..a609265 100644 --- a/packages/cli/src/runBuilder.ts +++ b/packages/cli/src/runBuilder.ts @@ -4,30 +4,7 @@ import { CompactBuilder } from '@openzeppelin/compact-builder'; import chalk from 'chalk'; import ora from 'ora'; -/** - * Executes the Compact builder CLI. - * Builds projects using the `CompactBuilder` class with provided options, including compilation and additional steps. - * - * Compiler options (forwarded to `compact-compiler`): - * - `--dir ` - Compile specific subdirectory within srcDir - * - `--src ` - Source directory (default: src) - * - `--out ` - Output directory for artifacts (default: artifacts) - * - `--hierarchical` - Preserve source directory structure in BOTH the - * compiler artifacts output AND the builder's - * .compact copy into dist/ (default off: flat in both) - * - `--exclude ` - Skip .compact files matching pattern, in BOTH the - * compiler's file discovery AND the builder's - * .compact copy (repeatable). When unset, the builder - * falls back to ['Mock*', '*.mock.compact']; the - * compiler defaults to no excludes. - * - `+` - Use specific toolchain version - * - * Builder-only options (control dist/ layout): - * - `--clean-dist` - rm -rf dist before building (default off) - * - `--copy ` - copy an extra file into dist/ for distribution (repeatable; e.g. package.json) - * - * See `packages/cli/README.md` for usage examples. - */ +/** `compact-builder` CLI shell. See `packages/cli/README.md` for options. */ async function runBuilder(): Promise { const spinner = ora(chalk.blue('[BUILD] Compact Builder started')).info(); diff --git a/packages/cli/src/runCompiler.ts b/packages/cli/src/runCompiler.ts index 83e4622..6b766dc 100644 --- a/packages/cli/src/runCompiler.ts +++ b/packages/cli/src/runCompiler.ts @@ -8,41 +8,7 @@ import { import chalk from 'chalk'; import ora, { type Ora } from 'ora'; -/** - * Executes the Compact compiler CLI with improved error handling and user feedback. - * - * Error Handling Architecture: - * - * This CLI follows a layered error handling approach: - * - * - Business logic (Compiler.ts) throws structured errors with context. - * - CLI layer (runCompiler.ts) handles all user-facing error presentation. - * - Custom error types (types/errors.ts) provide semantic meaning and context. - * - * Benefits: Better testability, consistent UI, separation of concerns. - * - * Note: This compiler uses fail-fast error handling. - * Compilation stops on the first error encountered. - * This provides immediate feedback but doesn't attempt to compile remaining files after a failure. - * - * @example Individual module compilation - * ```bash - * npx compact-compiler --dir security --skip-zk - * turbo compact:access -- --skip-zk - * turbo compact:security -- --skip-zk --other-flag - * ``` - * - * @example Full compilation with environment variables - * ```bash - * SKIP_ZK=true turbo compact - * turbo compact - * ``` - * - * @example Version specification - * ```bash - * npx compact-compiler --dir security --skip-zk + - * ``` - */ +/** `compact-compiler` CLI shell — fail-fast, maps error types to user-facing messages via {@link handleError}. */ async function runCompiler(): Promise { const spinner = ora(chalk.blue('[COMPILE] Compact compiler started')).info(); @@ -56,21 +22,7 @@ async function runCompiler(): Promise { } } -/** - * Centralized error handling with specific error types and user-friendly messages. - * - * Handles different error types with appropriate user feedback: - * - * - `CompactCliNotFoundError`: Shows installation instructions. - * - `DirectoryNotFoundError`: Shows available directories. - * - `CompilationError`: Shows file-specific error details with context. - * - Environment validation errors: Shows troubleshooting tips. - * - Argument parsing errors: Shows usage help. - * - Generic errors: Shows general troubleshooting guidance. - * - * @param error - The error that occurred during compilation - * @param spinner - Ora spinner instance for consistent UI messaging - */ +/** Dispatch by error name → spinner output + actionable hint. */ function handleError(error: unknown, spinner: Ora): void { // CompactCliNotFoundError if (error instanceof Error && error.name === 'CompactCliNotFoundError') { @@ -154,9 +106,6 @@ function handleError(error: unknown, spinner: Ora): void { console.log(chalk.gray(' • File system permissions are correct')); } -/** - * Shows available directories when `DirectoryNotFoundError` occurs. - */ function showAvailableDirectories(): void { console.log(chalk.yellow('\nAvailable directories:')); console.log( @@ -168,9 +117,6 @@ function showAvailableDirectories(): void { console.log(chalk.yellow(' --dir utils # Compile utility contracts')); } -/** - * Shows usage help with examples for different scenarios. - */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-compiler [options]')); console.log(chalk.yellow('\nOptions:')); diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts new file mode 100644 index 0000000..c907fb4 --- /dev/null +++ b/packages/cli/src/runDeploy.ts @@ -0,0 +1,308 @@ +#!/usr/bin/env node +// biome-ignore-all lint/suspicious/noConsole: CLI writes user-facing diagnostics to stdout/stderr + +/** + * `compact-deploy` CLI shell over {@link Deployer}. The `globalThis.WebSocket` + * shim is required: midnight-js's indexer client uses the browser WebSocket + * interface, which Node only ships natively from v22. + */ +import { DeployError, Deployer } from '@openzeppelin/compact-deployer'; +import chalk from 'chalk'; +import ora from 'ora'; +import { WebSocket } from 'ws'; +import { createLogger } from './logger.ts'; +import { promptPassphrase } from './prompt.ts'; + +(globalThis as { WebSocket?: unknown }).WebSocket = WebSocket; + +interface ParsedArgs { + contract?: string; + network?: string; + configPath?: string; + seedFile?: string; + proofServer?: string; + syncTimeoutSec?: number; + seedCacheFromDust?: string; + seedCacheFromShielded?: string; + noCache: boolean; + dryRun: boolean; + json: boolean; + verbose: boolean; + help: boolean; + version: boolean; + positional: string[]; +} + +function parseArgs(argv: string[]): ParsedArgs { + const out: ParsedArgs = { + noCache: false, + dryRun: false, + json: false, + verbose: false, + help: false, + version: false, + positional: [], + }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] as string; + switch (arg) { + case '-h': + case '--help': + out.help = true; + break; + case '--version': + out.version = true; + break; + case '-v': + case '--verbose': + out.verbose = true; + break; + case '--json': + out.json = true; + break; + case '--dry-run': + out.dryRun = true; + break; + case '--no-cache': + out.noCache = true; + break; + case '--seed-cache-from-dust': + out.seedCacheFromDust = expectValue( + argv, + ++i, + '--seed-cache-from-dust', + ); + break; + case '--seed-cache-from-shielded': + out.seedCacheFromShielded = expectValue( + argv, + ++i, + '--seed-cache-from-shielded', + ); + break; + case '--network': + out.network = expectValue(argv, ++i, '--network'); + break; + case '--config': + out.configPath = expectValue(argv, ++i, '--config'); + break; + case '--seed-file': + out.seedFile = expectValue(argv, ++i, '--seed-file'); + break; + case '--proof-server': + out.proofServer = expectValue(argv, ++i, '--proof-server'); + break; + case '--sync-timeout': { + const raw = expectValue(argv, ++i, '--sync-timeout'); + const seconds = Number.parseInt(raw, 10); + if (!Number.isFinite(seconds) || seconds <= 0) { + throw new Error( + `--sync-timeout requires a positive integer (seconds); got "${raw}"`, + ); + } + out.syncTimeoutSec = seconds; + break; + } + default: + if (arg.startsWith('--')) throw new Error(`Unknown flag: ${arg}`); + out.positional.push(arg); + } + } + out.contract = out.positional[0]; + return out; +} + +function expectValue(argv: string[], i: number, flag: string): string { + const v = argv[i]; + if (v === undefined || v.startsWith('-')) { + throw new Error(`${flag} requires a value`); + } + return v; +} + +async function main(): Promise { + let args: ParsedArgs; + try { + args = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error(chalk.red(`[DEPLOY] ${(e as Error).message}`)); + showUsage(); + process.exit(2); + return; + } + + if (args.help) { + showUsage(); + return; + } + if (args.version) { + console.log(packageVersion()); + return; + } + + if (!args.contract) { + console.error( + chalk.red('[DEPLOY] Missing required positional argument.'), + ); + showUsage(); + process.exit(2); + return; + } + + const logger = createLogger({ verbose: args.verbose, json: args.json }); + // Spinner narrates two phases: prepare() (proof-server start, wallet + // build, sync to tip — can take minutes on first preprod/preview run) + // then deploy() / dryRun() (proof generation + tx submit). Text is + // updated between phases so the spinner matches the actual stage. + const verbActive = args.dryRun ? 'Dry-running' : 'Deploying'; + const spinner = args.json + ? undefined + : ora( + chalk.blue( + `[DEPLOY] Preparing wallet for ${args.contract} (sync may take minutes)…`, + ), + ).start(); + + try { + await using deployer = await Deployer.prepare({ + contract: args.contract, + network: args.network, + configPath: args.configPath, + seedFile: args.seedFile, + proofServer: args.proofServer, + syncTimeoutMs: + args.syncTimeoutSec !== undefined + ? args.syncTimeoutSec * 1000 + : undefined, + skipWalletCache: args.noCache, + seedCacheDust: args.seedCacheFromDust, + seedCacheShielded: args.seedCacheFromShielded, + logger, + promptPassphrase: async (path) => { + if (spinner) spinner.stop(); + const pp = await promptPassphrase(path); + if (spinner) spinner.start(); + return pp; + }, + }); + // Wallet is ready, providers are up — now we're actually deploying. + if (spinner) { + spinner.text = chalk.blue( + `[DEPLOY] ${verbActive} ${args.contract} (proof gen + submit)…`, + ); + } + const result = args.dryRun + ? await deployer.dryRun() + : await deployer.deploy(); + + if (args.json) { + process.stdout.write(`${JSON.stringify(result)}\n`); + return; + } + if (result.dryRun) { + spinner?.succeed( + chalk.green( + `[DEPLOY] Dry-run for ${result.contractName} on ${result.network} OK`, + ), + ); + return; + } + spinner?.succeed( + chalk.green( + `[DEPLOY] ${result.contractName} deployed on ${result.network}: ${result.address}`, + ), + ); + console.log(chalk.gray(` txId: ${result.txId}`)); + console.log(chalk.gray(` txHash: ${result.txHash}`)); + console.log(chalk.gray(` blockHeight: ${result.blockHeight}`)); + console.log(chalk.gray(` saved to: ${result.deploymentsFile}`)); + if (result.explorerUrl) { + console.log(chalk.gray(` explorer: ${result.explorerUrl}`)); + } + } catch (e) { + const code = e instanceof DeployError ? e.exitCode : 1; + const name = e instanceof Error ? e.name : 'Error'; + const message = e instanceof Error ? e.message : String(e); + if (args.json) { + process.stdout.write( + `${JSON.stringify({ error: name, message, exitCode: code })}\n`, + ); + } else { + spinner?.fail(chalk.red(`[DEPLOY] ${name}: ${message}`)); + if (args.verbose && e instanceof Error && e.stack) { + console.error(chalk.gray(e.stack)); + } + } + process.exit(code); + } +} + +function showUsage(): void { + console.log(chalk.yellow('\nUsage: compact-deploy [options]')); + console.log(chalk.yellow('\nOptions:')); + console.log( + chalk.yellow( + ' --network Target network (or set [profile].default_network)', + ), + ); + console.log( + chalk.yellow( + ' --config Path to compact.toml (default: walk up from CWD)', + ), + ); + console.log( + chalk.yellow( + ' --seed-file Seed override (raw hex or BIP39 mnemonic, one line)', + ), + ); + console.log( + chalk.yellow(' --proof-server Override [networks.X].proof_server'), + ); + console.log( + chalk.yellow( + ' --sync-timeout Max wallet-sync seconds before failing (default 600)', + ), + ); + console.log( + chalk.yellow( + ' --no-cache Ignore the on-disk wallet-state cache; force fresh sync', + ), + ); + console.log( + chalk.yellow( + ' --seed-cache-from-dust Import a pre-warmed dust state file into .states/', + ), + ); + console.log( + chalk.yellow( + ' --seed-cache-from-shielded Import a pre-warmed shielded state file into .states/', + ), + ); + console.log( + chalk.yellow(' --dry-run Load+validate, do NOT submit a tx'), + ); + console.log( + chalk.yellow(' --json Single JSON object on stdout'), + ); + console.log( + chalk.yellow(' -v, --verbose Pino debug logs to .compact/logs/'), + ); + console.log(chalk.yellow(' -h, --help Show this help')); + console.log(chalk.yellow(' --version Print package version')); + console.log(chalk.yellow('\nExamples:')); + console.log(chalk.yellow(' compact-deploy Token --network local')); + console.log( + chalk.yellow( + ' MN_DEPLOYER_SEED=$(cat seed.hex) compact-deploy Vault --network testnet', + ), + ); + console.log( + chalk.yellow(' compact-deploy Token --network preprod --dry-run --json'), + ); +} + +function packageVersion(): string { + return process.env.npm_package_version ?? 'dev'; +} + +main(); diff --git a/packages/cli/test/logger.test.ts b/packages/cli/test/logger.test.ts new file mode 100644 index 0000000..0336845 --- /dev/null +++ b/packages/cli/test/logger.test.ts @@ -0,0 +1,175 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockMkdirSync, mockPino, mockTransport, fakeLogger } = vi.hoisted( + () => { + const fakeLogger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + child: vi.fn(), + }; + return { + mockMkdirSync: vi.fn(), + mockPino: vi.fn(() => fakeLogger), + mockTransport: vi.fn((cfg: unknown) => ({ __transport: cfg })), + fakeLogger, + }; + }, +); + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, mkdirSync: mockMkdirSync }; +}); + +vi.mock('pino', () => { + const pinoFn = (...args: unknown[]) => mockPino(...(args as [])) as unknown; + (pinoFn as unknown as { transport: typeof mockTransport }).transport = + mockTransport; + return { default: pinoFn }; +}); + +import { createLogger } from '../src/logger.ts'; + +describe('createLogger', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('json mode', () => { + it('should return logger at info level when verbose false', () => { + const logger = createLogger({ verbose: false, json: true }); + + expect(mockPino).toHaveBeenCalledTimes(1); + expect(mockPino).toHaveBeenCalledWith({ level: 'info' }); + expect(mockTransport).not.toHaveBeenCalled(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(logger).toBe(fakeLogger); + }); + + it('should return logger at debug level when verbose true', () => { + const logger = createLogger({ verbose: true, json: true }); + + expect(mockPino).toHaveBeenCalledWith({ level: 'debug' }); + expect(mockTransport).not.toHaveBeenCalled(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(logger).toBe(fakeLogger); + }); + }); + + describe('pretty mode (default, non-json)', () => { + it('should configure single pino-pretty transport when not verbose', () => { + const logger = createLogger({ verbose: false, json: false }); + + expect(mockPino).toHaveBeenCalledTimes(1); + const [opts, transport] = mockPino.mock.calls[0] as [ + { level: string }, + unknown, + ]; + expect(opts).toEqual({ level: 'info' }); + expect(transport).toEqual({ + __transport: { + target: 'pino-pretty', + options: { + destination: 1, + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + }, + }, + }); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(logger).toBe(fakeLogger); + }); + }); + + describe('verbose pretty mode', () => { + it('should mkdir the default log dir and configure two transports', () => { + const logger = createLogger({ verbose: true, json: false }); + + expect(mockMkdirSync).toHaveBeenCalledTimes(1); + const [dirArg, mkdirOpts] = mockMkdirSync.mock.calls[0] as [ + string, + { recursive: boolean }, + ]; + expect(dirArg).toContain('.compact'); + expect(dirArg).toContain('logs'); + expect(mkdirOpts).toEqual({ recursive: true }); + + expect(mockPino).toHaveBeenCalledTimes(1); + const [opts, transport] = mockPino.mock.calls[0] as [ + { level: string }, + { __transport: { targets: Array> } }, + ]; + expect(opts).toEqual({ level: 'debug' }); + expect(transport.__transport.targets).toHaveLength(2); + expect(transport.__transport.targets[0]).toMatchObject({ + target: 'pino/file', + level: 'debug', + }); + expect( + (transport.__transport.targets[0] as { options: { destination: string } }) + .options.destination, + ).toMatch(/\.log$/); + expect(transport.__transport.targets[1]).toMatchObject({ + target: 'pino-pretty', + level: 'info', + options: { + destination: 1, + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + }, + }); + expect(logger).toBe(fakeLogger); + }); + + it('should honour a custom logDir override', () => { + createLogger({ verbose: true, json: false, logDir: '/tmp/custom-logs' }); + + expect(mockMkdirSync).toHaveBeenCalledWith('/tmp/custom-logs', { + recursive: true, + }); + const transport = (mockPino.mock.calls[0] as [ + unknown, + { + __transport: { targets: Array<{ options: { destination: string } }> }; + }, + ])[1]; + expect(transport.__transport.targets[0]?.options.destination).toContain( + '/tmp/custom-logs/', + ); + }); + }); + + describe('return value shape', () => { + it('should expose the pino logger contract for every mode combination', () => { + const matrix: Array<{ verbose: boolean; json: boolean }> = [ + { verbose: false, json: false }, + { verbose: true, json: false }, + { verbose: false, json: true }, + { verbose: true, json: true }, + ]; + for (const opts of matrix) { + const logger = createLogger({ + ...opts, + logDir: '/tmp/logger-shape-test', + }); + expect(typeof logger.trace).toBe('function'); + expect(typeof logger.debug).toBe('function'); + expect(typeof logger.info).toBe('function'); + expect(typeof logger.warn).toBe('function'); + expect(typeof logger.error).toBe('function'); + expect(typeof logger.fatal).toBe('function'); + expect(typeof logger.child).toBe('function'); + } + }); + }); +}); diff --git a/packages/cli/test/prompt.test.ts b/packages/cli/test/prompt.test.ts new file mode 100644 index 0000000..9033b45 --- /dev/null +++ b/packages/cli/test/prompt.test.ts @@ -0,0 +1,149 @@ +import { EventEmitter } from 'node:events'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockStdin, mockStdout } = await vi.hoisted(async () => { + const { EventEmitter } = await import('node:events'); + type FakeStdin = InstanceType & { + isTTY: boolean; + setRawMode: ReturnType; + pause: ReturnType; + resume: ReturnType; + setEncoding: ReturnType; + removeListener: (event: string, fn: (...args: unknown[]) => void) => void; + }; + const stdin = new EventEmitter() as FakeStdin; + stdin.isTTY = true; + stdin.setRawMode = vi.fn(); + stdin.pause = vi.fn(); + stdin.resume = vi.fn(); + stdin.setEncoding = vi.fn(); + stdin.removeListener = + stdin.removeListener.bind(stdin) as FakeStdin['removeListener']; + + const stdout = { write: vi.fn() }; + return { mockStdin: stdin, mockStdout: stdout }; +}); + +vi.mock('node:process', () => ({ + stdin: mockStdin, + stdout: mockStdout, +})); + +import { promptPassphrase } from '../src/prompt.ts'; + +function resetStdin(opts: { tty: boolean } = { tty: true }): void { + mockStdin.removeAllListeners(); + (mockStdin as { isTTY: boolean }).isTTY = opts.tty; + (mockStdin.setRawMode as ReturnType).mockClear(); + (mockStdin.pause as ReturnType).mockClear(); + (mockStdin.resume as ReturnType).mockClear(); + (mockStdin.setEncoding as ReturnType).mockClear(); + mockStdout.write.mockClear(); +} + +describe('promptPassphrase', () => { + beforeEach(() => { + resetStdin({ tty: true }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('prompt label and stream setup', () => { + it('should write the label and switch stdin into raw + utf8 mode on a TTY', async () => { + const promise = promptPassphrase('Alice keystore'); + mockStdin.emit('data', Buffer.from('x\n')); + await promise; + + expect(mockStdout.write).toHaveBeenCalledWith( + 'Passphrase for Alice keystore: ', + ); + expect(mockStdin.setRawMode).toHaveBeenCalledWith(true); + expect(mockStdin.resume).toHaveBeenCalled(); + expect(mockStdin.setEncoding).toHaveBeenCalledWith('utf8'); + }); + + it('should NOT call setRawMode when stdin is not a TTY', async () => { + resetStdin({ tty: false }); + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('\n')); + await promise; + + expect(mockStdin.setRawMode).not.toHaveBeenCalled(); + expect(mockStdin.resume).toHaveBeenCalled(); + }); + }); + + describe('successful read paths', () => { + it('should resolve with the typed characters on CR (0x0d)', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('hunter2')); + mockStdin.emit('data', Buffer.from([0x0d])); + const pp = await promise; + + expect(pp).toBe('hunter2'); + expect(mockStdin.setRawMode).toHaveBeenLastCalledWith(false); + expect(mockStdin.pause).toHaveBeenCalled(); + expect(mockStdout.write).toHaveBeenLastCalledWith('\n'); + }); + + it('should resolve on LF (0x0a)', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('p4ss\n')); + const pp = await promise; + expect(pp).toBe('p4ss'); + }); + + it('should return an empty string when user presses Enter immediately', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('\n')); + const pp = await promise; + expect(pp).toBe(''); + }); + + it('should handle DEL (0x7f) as backspace', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('abc')); + mockStdin.emit('data', Buffer.from([0x7f])); + mockStdin.emit('data', Buffer.from('d\n')); + const pp = await promise; + expect(pp).toBe('abd'); + }); + + it('should handle BS (0x08) as backspace', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('xyz')); + mockStdin.emit('data', Buffer.from([0x08, 0x08])); + mockStdin.emit('data', Buffer.from('a\n')); + const pp = await promise; + expect(pp).toBe('xa'); + }); + + it('should drop a trailing backspace that empties the buffer', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from([0x7f])); + mockStdin.emit('data', Buffer.from('q\n')); + const pp = await promise; + expect(pp).toBe('q'); + }); + }); + + describe('abort path', () => { + it('should reject with "Aborted" on Ctrl+C (0x03)', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('partial')); + mockStdin.emit('data', Buffer.from([0x03])); + await expect(promise).rejects.toThrow('Aborted'); + expect(mockStdin.setRawMode).toHaveBeenLastCalledWith(false); + expect(mockStdin.pause).toHaveBeenCalled(); + }); + + it('should ignore characters after Ctrl+C within the same chunk', async () => { + const promise = promptPassphrase('label'); + // 0x03 short-circuits the loop; "abc\n" after it must not resolve. + mockStdin.emit('data', Buffer.from([0x03, 0x61, 0x62, 0x63, 0x0a])); + await expect(promise).rejects.toThrow('Aborted'); + }); + }); +}); diff --git a/packages/cli/test/runBuilder.test.ts b/packages/cli/test/runBuilder.test.ts new file mode 100644 index 0000000..06d47e5 --- /dev/null +++ b/packages/cli/test/runBuilder.test.ts @@ -0,0 +1,116 @@ +import { CompactBuilder } from '@openzeppelin/compact-builder'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Mock the library so we can drive the CLI in isolation. +vi.mock('@openzeppelin/compact-builder', async () => { + const actual = await vi.importActual< + typeof import('@openzeppelin/compact-builder') + >('@openzeppelin/compact-builder'); + return { + ...actual, + CompactBuilder: { + fromArgs: vi.fn(), + }, + }; +}); + +// Mock chalk to a passthrough. +vi.mock('chalk', () => ({ + default: { + blue: (text: string) => text, + red: (text: string, extra?: string) => + extra === undefined ? text : `${text} ${extra}`, + }, +})); + +// Mock ora. +const mockSpinner = { + info: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), +}; +vi.mock('ora', () => ({ + default: vi.fn(() => mockSpinner), +})); + +const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + +describe('runBuilder CLI', () => { + let mockBuild: ReturnType; + let mockFromArgs: ReturnType; + let originalArgv: string[]; + + beforeEach(() => { + originalArgv = [...process.argv]; + + vi.clearAllMocks(); + vi.resetModules(); + + mockBuild = vi.fn(); + mockFromArgs = vi.mocked(CompactBuilder.fromArgs); + mockFromArgs.mockReturnValue({ build: mockBuild } as any); + + mockSpinner.info.mockClear(); + mockSpinner.fail.mockClear(); + mockSpinner.succeed.mockClear(); + mockExit.mockClear(); + }); + + afterEach(() => { + process.argv = originalArgv; + }); + + describe('successful build', () => { + it('should build with no arguments', async () => { + process.argv = ['node', 'runBuilder.js']; + mockBuild.mockResolvedValue(undefined); + + await import('../src/runBuilder.ts'); + + expect(mockSpinner.info).toHaveBeenCalled(); + expect(mockFromArgs).toHaveBeenCalledWith([]); + expect(mockBuild).toHaveBeenCalledTimes(1); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it('should pass argv through to fromArgs', async () => { + const args = ['--watch', '--dir', 'token']; + process.argv = ['node', 'runBuilder.js', ...args]; + mockBuild.mockResolvedValue(undefined); + + await import('../src/runBuilder.ts'); + + expect(mockFromArgs).toHaveBeenCalledWith(args); + expect(mockExit).not.toHaveBeenCalled(); + }); + }); + + describe('error handling', () => { + it('should fail the spinner and exit 1 on build failure', async () => { + const error = new Error('Build broke'); + mockBuild.mockRejectedValue(error); + + await import('../src/runBuilder.ts'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + '[BUILD] Unexpected error: Build broke', + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('should exit 1 on argument parsing failure', async () => { + mockFromArgs.mockImplementation(() => { + throw new Error('bad flag'); + }); + + await import('../src/runBuilder.ts'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + '[BUILD] Unexpected error: bad flag', + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/packages/cli/test/runDeploy.test.ts b/packages/cli/test/runDeploy.test.ts new file mode 100644 index 0000000..ab8161f --- /dev/null +++ b/packages/cli/test/runDeploy.test.ts @@ -0,0 +1,535 @@ +import { + ArtifactNotFoundError, + DeployError, + Deployer, +} from '@openzeppelin/compact-deployer'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// --- Mocks -------------------------------------------------------------- + +vi.mock('@openzeppelin/compact-deployer', async () => { + const actual = await vi.importActual< + typeof import('@openzeppelin/compact-deployer') + >('@openzeppelin/compact-deployer'); + return { + ...actual, + Deployer: { + prepare: vi.fn(), + }, + }; +}); + +vi.mock('chalk', () => ({ + default: { + blue: (text: string) => text, + red: (text: string) => text, + green: (text: string) => text, + gray: (text: string) => text, + yellow: (text: string) => text, + }, +})); + +const mockSpinner = { + start: vi.fn().mockReturnThis(), + stop: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + text: '', +}; +const mockOra = vi.fn(() => mockSpinner); +vi.mock('ora', () => ({ + default: mockOra, +})); + +vi.mock('ws', () => ({ + WebSocket: class FakeWebSocket {}, +})); + +vi.mock('../src/logger.ts', () => ({ + createLogger: vi.fn(() => ({ + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + child: vi.fn(), + })), +})); + +const mockPromptPassphrase = vi.fn(async () => 'secret'); +vi.mock('../src/prompt.ts', () => ({ + promptPassphrase: mockPromptPassphrase, +})); + +// --- Process helpers ---------------------------------------------------- + +const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); +const mockStdoutWrite = vi + .spyOn(process.stdout, 'write') + .mockImplementation(() => true); +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); +const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + +// Fixture builder for the Deployer instance returned by prepare(). +interface FakeDeployerOpts { + result?: Record; + deployError?: unknown; + dryRunResult?: Record; + dryRunError?: unknown; +} +function fakeDeployer(opts: FakeDeployerOpts = {}) { + const deploy = vi.fn(async () => { + if (opts.deployError) throw opts.deployError; + return opts.result ?? defaultResult(); + }); + const dryRun = vi.fn(async () => { + if (opts.dryRunError) throw opts.dryRunError; + return opts.dryRunResult ?? defaultDryRunResult(); + }); + const dispose = vi.fn(async () => {}); + return { + deploy, + dryRun, + [Symbol.asyncDispose]: dispose, + }; +} + +function defaultResult(overrides: Record = {}) { + return { + contractName: 'Token', + network: 'local', + address: '0xabc', + txHash: '0xhash', + txId: 'tx-1', + blockHeight: 42, + deploymentsFile: '/tmp/deployments.json', + dryRun: false, + explorerUrl: '', + ...overrides, + }; +} + +function defaultDryRunResult(overrides: Record = {}) { + return { + contractName: 'Token', + network: 'local', + address: '', + txHash: '', + txId: '', + blockHeight: 0, + deploymentsFile: '', + dryRun: true, + explorerUrl: '', + ...overrides, + }; +} + +// --- parseArgs probe ---------------------------------------------------- +// +// parseArgs is module-private. We exercise it indirectly via main() by +// running with argv variants and asserting on either Deployer.prepare's +// args object (happy path) or on console.error + exit code 2 (parse-error +// path). + +async function runMain(argv: string[]): Promise { + process.argv = ['node', 'runDeploy.js', ...argv]; + vi.resetModules(); + await import('../src/runDeploy.ts'); + // main() is invoked at module top-level but is async. Await a microtask + // tick so its body finishes before assertions. + await new Promise((resolve) => setImmediate(resolve)); +} + +// --- Tests -------------------------------------------------------------- + +describe('runDeploy CLI', () => { + let originalArgv: string[]; + let mockPrepare: ReturnType; + + beforeEach(() => { + originalArgv = [...process.argv]; + vi.clearAllMocks(); + mockPrepare = vi.mocked(Deployer.prepare); + mockSpinner.start.mockClear(); + mockSpinner.stop.mockClear(); + mockSpinner.succeed.mockClear(); + mockSpinner.fail.mockClear(); + mockSpinner.text = ''; + }); + + afterEach(() => { + process.argv = originalArgv; + }); + + // ------------------------------------------------------------------ // + describe('--help / --version short-circuits', () => { + it('should print usage and return on --help', async () => { + await runMain(['--help']); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('Usage: compact-deploy'), + ); + expect(mockExit).not.toHaveBeenCalled(); + expect(mockPrepare).not.toHaveBeenCalled(); + }); + + it('should accept -h shorthand', async () => { + await runMain(['-h']); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('Usage: compact-deploy'), + ); + expect(mockPrepare).not.toHaveBeenCalled(); + }); + + it('should print the package version on --version', async () => { + const prev = process.env.npm_package_version; + process.env.npm_package_version = '9.9.9'; + try { + await runMain(['--version']); + expect(mockConsoleLog).toHaveBeenCalledWith('9.9.9'); + } finally { + if (prev === undefined) delete process.env.npm_package_version; + else process.env.npm_package_version = prev; + } + expect(mockPrepare).not.toHaveBeenCalled(); + }); + + it('should fall back to "dev" when npm_package_version is unset', async () => { + const prev = process.env.npm_package_version; + delete process.env.npm_package_version; + try { + await runMain(['--version']); + expect(mockConsoleLog).toHaveBeenCalledWith('dev'); + } finally { + if (prev !== undefined) process.env.npm_package_version = prev; + } + }); + }); + + // ------------------------------------------------------------------ // + describe('parseArgs (via main)', () => { + beforeEach(() => { + mockPrepare.mockResolvedValue(fakeDeployer()); + }); + + it('should map every flag to the prepare() options', async () => { + await runMain([ + 'Token', + '--network', + 'local', + '--config', + '/c.toml', + '--seed-file', + '/seed.hex', + '--proof-server', + 'http://proof:6300', + '--sync-timeout', + '30', + '--no-cache', + '--seed-cache-from-dust', + '/dust.json', + '--seed-cache-from-shielded', + '/shielded.gz', + ]); + + expect(mockPrepare).toHaveBeenCalledTimes(1); + const opts = mockPrepare.mock.calls[0]?.[0] as Record; + expect(opts.contract).toBe('Token'); + expect(opts.network).toBe('local'); + expect(opts.configPath).toBe('/c.toml'); + expect(opts.seedFile).toBe('/seed.hex'); + expect(opts.proofServer).toBe('http://proof:6300'); + expect(opts.syncTimeoutMs).toBe(30_000); + expect(opts.skipWalletCache).toBe(true); + expect(opts.seedCacheDust).toBe('/dust.json'); + expect(opts.seedCacheShielded).toBe('/shielded.gz'); + }); + + it('should reject --seed-cache-from-dust with no follow-up value', async () => { + await runMain(['Token', '--seed-cache-from-dust']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('--seed-cache-from-dust requires a value'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should leave syncTimeoutMs undefined when --sync-timeout is omitted', async () => { + await runMain(['Token']); + const opts = mockPrepare.mock.calls[0]?.[0] as Record; + expect(opts.syncTimeoutMs).toBeUndefined(); + }); + + it('should reject unknown flags with exit code 2', async () => { + await runMain(['Token', '--bogus']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('Unknown flag: --bogus'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + expect(mockPrepare).not.toHaveBeenCalled(); + }); + + it('should reject --network with no follow-up value', async () => { + await runMain(['Token', '--network']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('--network requires a value'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should reject --network when followed by another flag', async () => { + await runMain(['Token', '--network', '--json']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('--network requires a value'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should reject non-numeric --sync-timeout', async () => { + await runMain(['Token', '--sync-timeout', 'abc']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('--sync-timeout requires a positive integer'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should reject zero/negative --sync-timeout', async () => { + await runMain(['Token', '--sync-timeout', '0']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('--sync-timeout requires a positive integer'), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should accept -v as a shorthand for --verbose', async () => { + await runMain(['Token', '-v']); + expect(mockPrepare).toHaveBeenCalled(); + }); + + it('should accept --dry-run and call deployer.dryRun()', async () => { + const fake = fakeDeployer(); + mockPrepare.mockResolvedValue(fake); + + await runMain(['Token', '--dry-run']); + + expect(fake.dryRun).toHaveBeenCalledTimes(1); + expect(fake.deploy).not.toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------ // + describe('missing contract positional', () => { + it('should exit 2 with a missing-contract message', async () => { + await runMain([]); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('Missing required '), + ); + expect(mockExit).toHaveBeenCalledWith(2); + expect(mockPrepare).not.toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------ // + describe('successful deploy (text output)', () => { + it('should succeed-spin and log the four metadata lines', async () => { + mockPrepare.mockResolvedValue(fakeDeployer()); + await runMain(['Token', '--network', 'local']); + + expect(mockSpinner.succeed).toHaveBeenCalledWith( + expect.stringContaining('Token deployed on local: 0xabc'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('txId:'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('txHash:'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('blockHeight:'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('saved to:'), + ); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it('should also print the explorer line when explorerUrl is set', async () => { + mockPrepare.mockResolvedValue( + fakeDeployer({ + result: defaultResult({ explorerUrl: 'https://explorer/0xabc' }), + }), + ); + await runMain(['Token']); + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('explorer: https://explorer/0xabc'), + ); + }); + }); + + // ------------------------------------------------------------------ // + describe('successful deploy (--json)', () => { + it('should write the result as one JSON line and skip the spinner', async () => { + mockPrepare.mockResolvedValue(fakeDeployer()); + await runMain(['Token', '--json']); + + expect(mockOra).not.toHaveBeenCalled(); + expect(mockStdoutWrite).toHaveBeenCalledWith( + expect.stringMatching(/^\{.*"contractName":"Token".*\}\n$/s), + ); + expect(mockExit).not.toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------ // + describe('dry-run path', () => { + it('should succeed-spin with the dry-run message', async () => { + mockPrepare.mockResolvedValue(fakeDeployer()); + await runMain(['Token', '--dry-run']); + + expect(mockSpinner.succeed).toHaveBeenCalledWith( + expect.stringContaining('Dry-run for Token on local OK'), + ); + // We should NOT see deploy-only metadata lines. + expect(mockConsoleLog).not.toHaveBeenCalledWith( + expect.stringContaining('txId:'), + ); + }); + }); + + // ------------------------------------------------------------------ // + describe('passphrase prompt wiring', () => { + it('should stop the spinner before the prompt and restart it after', async () => { + let captured: + | ((path: string) => Promise) + | undefined; + mockPrepare.mockImplementation(async (opts: any) => { + captured = opts.promptPassphrase; + return fakeDeployer(); + }); + + await runMain(['Token']); + expect(captured).toBeDefined(); + + mockSpinner.stop.mockClear(); + mockSpinner.start.mockClear(); + const pp = await captured?.('/some/path'); + expect(pp).toBe('secret'); + expect(mockSpinner.stop).toHaveBeenCalledTimes(1); + expect(mockSpinner.start).toHaveBeenCalledTimes(1); + // Ordering: stop must come before start. + const stopOrder = (mockSpinner.stop.mock.invocationCallOrder[0] ?? + Number.POSITIVE_INFINITY) as number; + const startOrder = (mockSpinner.start.mock.invocationCallOrder[0] ?? + Number.NEGATIVE_INFINITY) as number; + expect(stopOrder).toBeLessThan(startOrder); + expect(mockPromptPassphrase).toHaveBeenCalledWith('/some/path'); + }); + + it('should NOT touch the spinner when running in --json mode', async () => { + let captured: + | ((path: string) => Promise) + | undefined; + mockPrepare.mockImplementation(async (opts: any) => { + captured = opts.promptPassphrase; + return fakeDeployer(); + }); + + await runMain(['Token', '--json']); + mockSpinner.stop.mockClear(); + mockSpinner.start.mockClear(); + await captured?.('/p'); + expect(mockSpinner.stop).not.toHaveBeenCalled(); + expect(mockSpinner.start).not.toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------ // + describe('error handling', () => { + it('should exit with DeployError.exitCode and log via spinner.fail', async () => { + mockPrepare.mockRejectedValue( + new ArtifactNotFoundError('artifact missing'), + ); + await runMain(['Token']); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining('artifact missing'), + ); + // ArtifactNotFoundError has exitCode 4 (per errors.ts) but we just + // assert the exit happened; we re-check the value once below. + expect(mockExit).toHaveBeenCalledTimes(1); + const callArg = (mockExit.mock.calls[0] as [number])[0]; + expect(typeof callArg).toBe('number'); + }); + + it('should use exitCode 1 for non-DeployError exceptions', async () => { + mockPrepare.mockRejectedValue(new Error('boom')); + await runMain(['Token']); + expect(mockExit).toHaveBeenCalledWith(1); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining('Error: boom'), + ); + }); + + it('should use exitCode 1 for string throws', async () => { + mockPrepare.mockRejectedValue('weird'); + await runMain(['Token']); + expect(mockExit).toHaveBeenCalledWith(1); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining('weird'), + ); + }); + + it('should use the DeployError.exitCode verbatim', async () => { + class CustomError extends DeployError { + constructor() { + super('custom failure', 42); + this.name = 'CustomError'; + } + } + mockPrepare.mockRejectedValue(new CustomError()); + await runMain(['Token']); + expect(mockExit).toHaveBeenCalledWith(42); + }); + + it('should print the stack trace under --verbose', async () => { + const err = new Error('boom'); + err.stack = 'Error: boom\n at fake.ts:1:1'; + mockPrepare.mockRejectedValue(err); + + await runMain(['Token', '--verbose']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('at fake.ts:1:1'), + ); + }); + + it('should NOT print the stack trace without --verbose', async () => { + const err = new Error('boom'); + err.stack = 'Error: boom\n at fake.ts:1:1'; + mockPrepare.mockRejectedValue(err); + + await runMain(['Token']); + const wroteStack = mockConsoleError.mock.calls.some((c) => + String(c[0] ?? '').includes('at fake.ts:1:1'), + ); + expect(wroteStack).toBe(false); + }); + + it('should emit JSON error line in --json mode', async () => { + mockPrepare.mockRejectedValue(new DeployError('json-mode bad', 7)); + await runMain(['Token', '--json']); + + expect(mockStdoutWrite).toHaveBeenCalledWith( + expect.stringMatching( + /^\{"error":"DeployError","message":"json-mode bad","exitCode":7\}\n$/, + ), + ); + expect(mockExit).toHaveBeenCalledWith(7); + // No spinner in json mode. + expect(mockSpinner.fail).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index d57e53a..1d445df 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -6,5 +6,16 @@ export default defineConfig({ environment: 'node', include: ['test/**/*.test.ts'], reporters: 'verbose', + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'json-summary'], + include: ['src/**/*.ts'], + thresholds: { + statements: 95, + branches: 90, + functions: 100, + lines: 95, + }, + }, }, }); diff --git a/packages/deployer/README.md b/packages/deployer/README.md new file mode 100644 index 0000000..21b682e --- /dev/null +++ b/packages/deployer/README.md @@ -0,0 +1,169 @@ +# @openzeppelin/compact-deployer + +```bash +compact-deploy Token --network local +``` + +> **Status: developer-preview, testnet only.** Verified on local devnet + preview. Preprod blocked ([Known issues](#known-issues-may-2026)). Mainnet unsupported: unaudited, no hardware signer, no multisig, no tx retry, no upgrade tooling. See [Roadmap](#roadmap--todo). + +## Quick start + +1. Compile your contract with `compact-compiler` so artifacts land under `src/artifacts//`. +2. Drop a `compact.toml` at your repo root (see [Sample config](#sample-config)). +3. Generate a signing key per contract: `head -c 32 /dev/urandom | xxd -p -c 32 > deploy/Token.signingkey`. +4. Run: + ```bash + compact-deploy Token --network local + ``` + +The deploy result lands in `deployments/compact/.json`. + +## CLI + +``` +compact-deploy + --network required unless [profile].default_network is set + --config default: walk up from CWD for compact.toml + --seed-file seed override (raw hex or BIP39 mnemonic, one line) + --proof-server override [networks.X].proof_server + --sync-timeout max wait for wallet to reach chain tip (default 600) + --no-cache ignore on-disk wallet-state cache; force fresh sync + --seed-cache-from-dust import a pre-warmed dust state file into .states/ + --seed-cache-from-shielded import a pre-warmed shielded state file into .states/ + --dry-run load, validate, build providers, log plan, DO NOT submit + --json single JSON object on stdout (machine-readable) + -v, --verbose pino debug logs to .compact/logs/.log + -h, --help --version +``` + +Exit codes: `0` ok · `2` config error · `3` wallet error · `4` provider unreachable · `5` deploy tx failed · `1` unexpected. + +## Deploying to real networks (preprod, preview, testnet) + +> Preprod is blocked on an upstream wallet-SDK bug. Use `--network preview`. See [Known issues](#known-issues-may-2026). + +- **First sync is slow** (~3 min on preview, 30–60 min on preprod from genesis). Cache makes reruns near-instant. +- **Bump sync timeout**: `--sync-timeout 3600` (default 10 min). +- **Bump Node heap** for long-history chains: `NODE_OPTIONS="--max-old-space-size=8192"`. +- **Seed source**: `--seed-file`, `MN_DEPLOYER_SEED`, or `[wallet].keystore`. The `wallet = { source = "local" }` shorthand is dev-preset only. + +## Wallet cache + +After each successful sync the deployer writes `/.states/--.gz` (one file per shielded / dust sub-wallet). Next run restores from it instead of re-syncing from genesis. + +- Contents: gzipped sub-wallet state (UTXOs, checkpoint). No private keys (re-derived from seed each run). +- Keyed by SHA-256(seed) + network ID, so `local` vs `preprod` keep separate caches. +- Bust it: `--no-cache` (force fresh) or `rm -rf .states/`. Auto-falls-back on corrupt or version-mismatched files. +- Best-effort writes; never block a deploy. Concurrent runs against the same seed race. Don't. +- `.states/` is gitignored. + +### Importing a pre-warmed state file + +If cold sync OOMs on preprod (the known upstream bug) and you already have a `wallet.serializeState()` snapshot from a prior session, drop it in with: + +``` +compact-deploy --network preprod \ + --seed-cache-from-dust /path/to/state.json \ + --seed-cache-from-shielded /path/to/shielded.json # optional +``` + +- Accepts either raw JSON (the direct `serializeState()` output) or its gzipped copy. Gzip is detected by magic bytes. +- The file is renamed to the seed-derived cache name and dropped into `.states/`. +- **The previous cache (if any) is preserved at `.gz.bak`** — never deleted, never overwritten by the import. To roll back from a bad import, `mv .states/.gz.bak .states/.gz`. +- The write itself is atomic: payload lands in `.gz.tmp` first, then is renamed over `.gz`. A mid-write crash can never leave the live cache half-overwritten. +- Restore failure (e.g. schema mismatch) falls through to the normal "fresh sync from genesis" path with a `warn` log — so the deploy still completes if the import doesn't take. +- Ignored under `--no-cache` (with a warning), since load is disabled in that mode. + +## Wallet seed resolution + +Precedence, first non-null wins: + +1. `--seed-file ` +2. `MN_DEPLOYER_SEED` env var (hex or BIP39 mnemonic) +3. `[wallet].keystore` (encrypted JSON, passphrase prompted) +4. `--network local` only: built-in prefunded standalone seed at `[networks.local].wallet.index` (0..3) + +## Sample config + +```toml +[profile] +default_network = "local" +artifacts_dir = "src/artifacts" +deployments_dir = "deployments/compact" + +# ---------- Networks ---------- +[networks.local] +network_id = "undeployed" +indexer = "http://127.0.0.1:8088/api/v3/graphql" +indexer_ws = "ws://127.0.0.1:8088/api/v3/graphql/ws" +node = "http://127.0.0.1:9944" +node_ws = "ws://127.0.0.1:9944" +proof_server = "http://127.0.0.1:6300" +wallet = { source = "local", index = 0 } + +[networks.preview] +network_id = "preview" +indexer = "https://indexer.preview.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preview.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preview.midnight.network" +node_ws = "wss://rpc.preview.midnight.network" +proof_server = "auto" +explorer = "https://preview.midnightexplorer.com" + +[networks.preprod] +network_id = "preprod" +indexer = "https://indexer.preprod.midnight.network/api/v4/graphql" +indexer_ws = "wss://indexer.preprod.midnight.network/api/v4/graphql/ws" +node = "https://rpc.preprod.midnight.network" +node_ws = "wss://rpc.preprod.midnight.network" +proof_server = "auto" +explorer = "https://preprod.midnightexplorer.com" + +# ---------- Wallet (non-local) ---------- +[wallet] +keystore = "./deployer.keystore.json" + +# ---------- Contracts ---------- +[contracts.Token] +artifact = "src/artifacts/Token/Token" +private_state_id = "tokenPrivateState" +init_private_state = { file = "./deploy/Token.private-state.json" } +args = ["MyToken", "MTK", 18] +signing_key_file = "./deploy/Token.signingkey" + +[contracts.Vault] +artifact = "src/artifacts/Vault/Vault" +args = [] +signing_key_file = "./deploy/Vault.signingkey" +``` + +`proof_server`: a URL pins the server; `"auto"` spawns a `testcontainers`-managed proof-server container for the duration of the deploy; omitting it falls back to the env var `PROOF_SERVER_PORT` then to `http://127.0.0.1:6300`. + +## Keystore format + +`compact-deploy` reads/writes a JSON keystore with the Ethereum V3 shape (scrypt + AES-128-CTR) but with `version: "midnight-1"` so other tooling does not silently mis-read it as an Ethereum key. The encrypted secret is a 32-byte Midnight wallet seed (hex). + +## Known issues (May 2026) + +1. **Preview endpoints null-routed.** `rpc.preview.midnight.network` and `indexer.preview.midnight.network` resolve to `0.0.0.0` on the authoritative AWS Route 53 nameservers for `midnight.network` (verified against Google, Cloudflare, and Quad9). Preview was alive on 2026-05-22, broken on 2026-05-24. Blocks every consumer of testkit-js's `PreviewTestEnvironment`. File at [midnightntwrk/servicedesk](https://github.com/midnightntwrk/servicedesk/issues/new?template=bug-report.yml). **Workaround:** none on public testnet. `make env-up` (local standalone) is the only working target until Midnight restores the endpoints. + +2. **Preprod blocked: `Wallet.Sync: Could not deserialize Ledger Event` on `DustSpendProcessed`.** Dust sync aborts mid-stream on a `DustSpendProcessed` event whose `midnight:event[v9]:`-prefixed `raw` bytes fail `effect/Schema` parsing. The thrown `Wallet.Sync` corrupts `DustLocalState`. The next `walletBalance()` call hits `RuntimeError: unreachable` in the ledger WASM. Two independent runs, two different event IDs: 2026-05-22 id **565,975** (confirmed in Midnight dev Discord by `Knife`); 2026-05-24 id **571,224** with `maxId` 676,018. Affected stack: `wallet-sdk-dust-wallet@4.0.0`, `ledger-v8@8.0.3`, `testkit-js@4.1.0`. File at [midnightntwrk/midnight-wallet](https://github.com/midnightntwrk/midnight-wallet/issues/new). Distinct from [#361 `InvalidDustSpendProof`](https://github.com/midnightntwrk/midnight-wallet/issues/361), which is a chain-side tx rejection (this bug is client-side event ingest). **Workaround:** none. Preview is also down (see #1). Local standalone is the only working target today. + +3. **Faucet is manual.** The deployer never hits a faucet. Fund the wallet's `unshielded` address (logged at startup) via the official Midnight faucet site or Discord bot before running. + +4. **Dust fee overhead default breaks faucet wallets.** testkit-js default `additionalFeeOverhead` is `5e20` vs a faucet wallet's `~3e15` dust → `Insufficient Funds: could not balance dust`. Deployer overrides to `5e14` for non-mainnet. Library users constructing their own provider must mirror this. + +5. **Long-history dust sync exhausts default Node heap.** Use `NODE_OPTIONS="--max-old-space-size=16384"` for the first sync. Cache fixes subsequent runs. + +## Programmatic API + +```ts +import { deploy } from "@openzeppelin/compact-deployer"; + +const result = await deploy({ + contract: "Token", + network: "local", + configPath: "./compact.toml", +}); +console.log(result.address); +``` diff --git a/packages/deployer/package.json b/packages/deployer/package.json new file mode 100644 index 0000000..4f88901 --- /dev/null +++ b/packages/deployer/package.json @@ -0,0 +1,69 @@ +{ + "name": "@openzeppelin/compact-deployer", + "description": "Forge-style deployer library for Midnight Compact contracts", + "version": "0.0.1", + "keywords": [ + "compact", + "midnight", + "deploy" + ], + "author": "OpenZeppelin Community ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=24" + }, + "scripts": { + "build": "tsc -p .", + "types": "tsc -p tsconfig.json --noEmit", + "test": "yarn vitest run", + "coverage": "yarn vitest run --coverage", + "clean": "git clean -fXd" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.3", + "@types/node": "24.10.1", + "typescript": "^5.9.3", + "vitest": "^4.0.15" + }, + "dependencies": { + "@midnight-ntwrk/compact-js": "2.5.1", + "@midnight-ntwrk/compact-runtime": "0.16.0", + "@midnight-ntwrk/ledger-v8": "8.0.3", + "@midnight-ntwrk/midnight-js-contracts": "4.1.0", + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.1.0", + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.0", + "@midnight-ntwrk/midnight-js-level-private-state-provider": "4.1.0", + "@midnight-ntwrk/midnight-js-network-id": "4.1.0", + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.1.0", + "@midnight-ntwrk/midnight-js-types": "4.1.0", + "@midnight-ntwrk/midnight-js-utils": "4.1.0", + "@midnight-ntwrk/testkit-js": "4.1.0", + "@midnight-ntwrk/wallet-sdk-address-format": "3.1.1", + "@midnight-ntwrk/wallet-sdk-dust-wallet": "4.0.0", + "@midnight-ntwrk/wallet-sdk-facade": "4.0.0", + "@midnight-ntwrk/wallet-sdk-hd": "3.0.2", + "@midnight-ntwrk/wallet-sdk-shielded": "3.0.0", + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "3.0.0", + "@scure/bip39": "^1.2.1", + "axios": "^1.12.0", + "pino": "^9.7.0", + "rxjs": "^7.8.1", + "smol-toml": "^1.3.4", + "testcontainers": "^10.28.0", + "zod": "^3.23.8" + } +} diff --git a/packages/deployer/src/config/compact-config.test.ts b/packages/deployer/src/config/compact-config.test.ts new file mode 100644 index 0000000..367a887 --- /dev/null +++ b/packages/deployer/src/config/compact-config.test.ts @@ -0,0 +1,122 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { CompactConfig } from './compact-config.ts'; + +const MIN_VALID = ` +[profile] +default_network = "local" + +[networks.local] +network_id = "undeployed" +indexer = "http://127.0.0.1:8088/api/v3/graphql" +indexer_ws = "ws://127.0.0.1:8088/api/v3/graphql/ws" +node = "http://127.0.0.1:9944" +node_ws = "ws://127.0.0.1:9944" +proof_server = "http://127.0.0.1:6300" + +[contracts.Token] +artifact = "src/artifacts/Token/Token" +signing_key_file = "./deploy/Token.signingkey" +`; + +function tmpRepo(toml: string): string { + const dir = mkdtempSync(join(tmpdir(), 'compact-deploy-test-')); + writeFileSync(join(dir, 'compact.toml'), toml); + return dir; +} + +describe('CompactConfig', () => { + it('should parse a minimal valid config', async () => { + const dir = tmpRepo(MIN_VALID); + const config = await CompactConfig.load(undefined, dir); + expect(config.rootDir).toBe(dir); + expect(config.defaultNetwork).toBe('local'); + expect(config.network('local').network_id).toBe('undeployed'); + expect(config.contract('Token').artifact).toBe('src/artifacts/Token/Token'); + }); + + it('should throw with the available set when a lookup misses', async () => { + const dir = tmpRepo(MIN_VALID); + const config = await CompactConfig.load(undefined, dir); + expect(() => config.network('ghost')).toThrow(/Available: local/); + expect(() => config.contract('Vault')).toThrow(/Available: Token/); + }); + + it('should reject a config whose default_network does not exist', async () => { + const dir = tmpRepo(`${MIN_VALID}\n[profile]\ndefault_network = "ghost"\n`); + await expect(CompactConfig.load(undefined, dir)).rejects.toThrow( + ConfigError, + ); + }); + + it('should reject a contract missing signing_key_file', async () => { + const dir = tmpRepo(` +[networks.local] +network_id = "undeployed" +indexer = "http://x" +indexer_ws = "ws://x" +node = "http://x" +node_ws = "ws://x" +proof_server = "http://x" + +[contracts.Token] +artifact = "x" +`); + await expect(CompactConfig.load(undefined, dir)).rejects.toThrow( + ConfigError, + ); + }); + + it('should reject when init_private_state is set but private_state_id is not', async () => { + const dir = tmpRepo(` +[networks.local] +network_id = "undeployed" +indexer = "http://127.0.0.1:8088/api/v3/graphql" +indexer_ws = "ws://127.0.0.1:8088/api/v3/graphql/ws" +node = "http://127.0.0.1:9944" +node_ws = "ws://127.0.0.1:9944" +proof_server = "http://127.0.0.1:6300" + +[contracts.Token] +artifact = "x" +signing_key_file = "x.sk" +init_private_state = { file = "x.json" } +`); + await expect(CompactConfig.load(undefined, dir)).rejects.toThrow( + ConfigError, + ); + }); + + it('should expose hasNetwork / hasContract / listNetworks / listContracts', async () => { + const dir = tmpRepo(`${MIN_VALID} +[contracts.Vault] +artifact = "src/artifacts/Vault/Vault" +signing_key_file = "./deploy/Vault.signingkey" +`); + const config = await CompactConfig.load(undefined, dir); + expect(config.hasNetwork('local')).toBe(true); + expect(config.hasNetwork('ghost')).toBe(false); + expect(config.hasContract('Token')).toBe(true); + expect(config.hasContract('Vault')).toBe(true); + expect(config.hasContract('Ghost')).toBe(false); + expect(config.listNetworks()).toEqual(['local']); + expect(config.listContracts().sort()).toEqual(['Token', 'Vault']); + }); + + it('should throw ConfigError when --config path does not exist', async () => { + const missing = join(tmpdir(), `does-not-exist-${Date.now()}.toml`); + await expect(CompactConfig.load(missing)).rejects.toThrow( + /--config path does not exist/, + ); + }); + + it('should throw ConfigError when no compact.toml exists upward from cwd', async () => { + const dir = mkdtempSync(join(tmpdir(), 'no-compact-toml-')); + await expect(CompactConfig.load(undefined, dir)).rejects.toThrow( + /compact\.toml not found/, + ); + }); +}); diff --git a/packages/deployer/src/config/compact-config.ts b/packages/deployer/src/config/compact-config.ts new file mode 100644 index 0000000..c93fa1c --- /dev/null +++ b/packages/deployer/src/config/compact-config.ts @@ -0,0 +1,143 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { dirname, isAbsolute, resolve } from 'node:path'; +import { parse as parseToml } from 'smol-toml'; +import { ConfigError } from '../errors.ts'; +import { + type CompactConfigData, + type ContractConfig, + configSchema, + type NetworkConfig, + type WalletConfig, +} from './schema.ts'; + +/** + * Parsed + validated `compact.toml` with the resolved project root. + * Single source of truth for the pipeline; `network` / `contract` + * lookups throw {@link ConfigError} with the available set on miss. + */ +export class CompactConfig { + readonly configPath: string; + readonly rootDir: string; + readonly #data: CompactConfigData; + + private constructor(data: CompactConfigData, configPath: string) { + this.#data = data; + this.configPath = configPath; + this.rootDir = dirname(configPath); + } + + /** Walks up from `cwd` Foundry-style when `explicitPath` is omitted. */ + static async load( + explicitPath?: string, + cwd: string = process.cwd(), + ): Promise { + const configPath = explicitPath + ? resolveExplicit(explicitPath, cwd) + : findUpward(cwd); + if (!configPath) { + throw new ConfigError( + `compact.toml not found (searched upward from ${cwd}). Pass --config or create one at the repo root.`, + ); + } + + let raw: string; + try { + raw = await readFile(configPath, 'utf8'); + } catch (e) { + throw new ConfigError( + `Failed to read ${configPath}: ${(e as Error).message}`, + ); + } + + let parsed: unknown; + try { + parsed = parseToml(raw); + } catch (e) { + throw new ConfigError( + `Invalid TOML in ${configPath}: ${(e as Error).message}`, + ); + } + + const result = configSchema.safeParse(parsed); + if (!result.success) { + const issues = result.error.issues + .map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`) + .join('\n'); + throw new ConfigError(`compact.toml validation failed:\n${issues}`); + } + + return new CompactConfig(result.data, configPath); + } + + get defaultNetwork(): string | undefined { + return this.#data.profile.default_network; + } + + get artifactsDir(): string { + return this.#data.profile.artifacts_dir; + } + + get deploymentsDir(): string { + return this.#data.profile.deployments_dir; + } + + get wallet(): WalletConfig | undefined { + return this.#data.wallet; + } + + hasNetwork(name: string): boolean { + return Object.hasOwn(this.#data.networks, name); + } + + hasContract(name: string): boolean { + return Object.hasOwn(this.#data.contracts, name); + } + + listNetworks(): string[] { + return Object.keys(this.#data.networks); + } + + listContracts(): string[] { + return Object.keys(this.#data.contracts); + } + + network(name: string): NetworkConfig { + const n = this.#data.networks[name]; + if (!n) { + throw new ConfigError( + `Network "${name}" not defined. Available: ${this.listNetworks().join(', ')}`, + ); + } + return n; + } + + contract(name: string): ContractConfig { + const c = this.#data.contracts[name]; + if (!c) { + throw new ConfigError( + `Contract "${name}" not defined. Available: ${this.listContracts().join(', ')}`, + ); + } + return c; + } +} + +function resolveExplicit(p: string, cwd: string): string { + const abs = isAbsolute(p) ? p : resolve(cwd, p); + if (!existsSync(abs)) { + throw new ConfigError(`--config path does not exist: ${abs}`); + } + return abs; +} + +function findUpward(start: string): string | undefined { + let dir = resolve(start); + while (true) { + const candidate = resolve(dir, 'compact.toml'); + if (existsSync(candidate)) return candidate; + const parent = dirname(dir); + if (parent === dir) return undefined; + dir = parent; + } +} diff --git a/packages/deployer/src/config/schema.test.ts b/packages/deployer/src/config/schema.test.ts new file mode 100644 index 0000000..7b384ca --- /dev/null +++ b/packages/deployer/src/config/schema.test.ts @@ -0,0 +1,250 @@ +import { describe, expect, it } from 'vitest'; +import { configSchema, isFileRef, isModuleRef } from './schema.ts'; + +const validNetwork = { + network_id: 'testnet', + indexer: 'https://indexer.example/api', + indexer_ws: 'wss://indexer.example/ws', + node: 'https://node.example', + node_ws: 'wss://node.example/ws', +}; + +const validContract = { + artifact: 'src/artifacts/Counter', + signing_key_file: 'keys/counter.signing', +}; + +const baseConfig = { + networks: { testnet: validNetwork }, + contracts: { Counter: validContract }, +}; + +describe('configSchema — profile', () => { + it('should default artifacts_dir and deployments_dir', () => { + const parsed = configSchema.parse(baseConfig); + expect(parsed.profile.artifacts_dir).toBe('src/artifacts'); + expect(parsed.profile.deployments_dir).toBe('deployments/compact'); + }); + + it('should accept an explicit profile block', () => { + const parsed = configSchema.parse({ + ...baseConfig, + profile: { + artifacts_dir: 'out', + deployments_dir: 'deploys', + }, + }); + expect(parsed.profile.artifacts_dir).toBe('out'); + expect(parsed.profile.deployments_dir).toBe('deploys'); + }); +}); + +describe('configSchema — networks', () => { + it('should reject a non-URL indexer', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { testnet: { ...validNetwork, indexer: 'not-a-url' } }, + }), + ).toThrow(); + }); + + it('should accept proof_server = "auto"', () => { + const parsed = configSchema.parse({ + ...baseConfig, + networks: { testnet: { ...validNetwork, proof_server: 'auto' } }, + }); + expect(parsed.networks.testnet.proof_server).toBe('auto'); + }); + + it('should accept proof_server as a URL', () => { + const parsed = configSchema.parse({ + ...baseConfig, + networks: { + testnet: { ...validNetwork, proof_server: 'http://localhost:6300' }, + }, + }); + expect(parsed.networks.testnet.proof_server).toBe('http://localhost:6300'); + }); + + it('should reject proof_server other than URL or "auto"', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { testnet: { ...validNetwork, proof_server: 'manual' } }, + }), + ).toThrow(); + }); + + it('should clamp wallet.index to 0..3 only', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { + testnet: { + ...validNetwork, + wallet: { source: 'local', index: 4 }, + }, + }, + }), + ).toThrow(); + }); + + it('should default wallet.index to 0', () => { + const parsed = configSchema.parse({ + ...baseConfig, + networks: { + testnet: { + ...validNetwork, + wallet: { source: 'local' }, + }, + }, + }); + expect(parsed.networks.testnet.wallet?.index).toBe(0); + }); + +}); + +describe('configSchema — profile.default_network refine', () => { + it('should accept default_network pointing at a defined network', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + profile: { default_network: 'testnet' }, + }), + ).not.toThrow(); + }); + + it('should reject default_network pointing at an undefined network', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + profile: { default_network: 'mainnet' }, + }), + ).toThrow(/default_network.*defined.*networks/); + }); + + it('should allow default_network to be omitted', () => { + const parsed = configSchema.parse(baseConfig); + expect(parsed.profile.default_network).toBeUndefined(); + }); +}); + +describe('configSchema — contract refine (private state pairing)', () => { + it('should accept both private_state_id and init_private_state set together', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { + ...validContract, + private_state_id: 'counter-ps', + init_private_state: { file: 'state.json' }, + }, + }, + }), + ).not.toThrow(); + }); + + it('should accept both omitted', () => { + expect(() => configSchema.parse(baseConfig)).not.toThrow(); + }); + + it('should reject private_state_id without init_private_state', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { ...validContract, private_state_id: 'counter-ps' }, + }, + }), + ).toThrow(/private_state_id and init_private_state must be set together/); + }); + + it('should reject init_private_state without private_state_id', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { + ...validContract, + init_private_state: { file: 'state.json' }, + }, + }, + }), + ).toThrow(/private_state_id and init_private_state must be set together/); + }); +}); + +describe('configSchema — contract args', () => { + it('should accept args as an array', () => { + const parsed = configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { ...validContract, args: [1, 'two', { x: 3 }] }, + }, + }); + expect(parsed.contracts.Counter.args).toEqual([1, 'two', { x: 3 }]); + }); + + it('should accept args as a file ref', () => { + const parsed = configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { ...validContract, args: { file: 'args.json' } }, + }, + }); + expect(parsed.contracts.Counter.args).toEqual({ file: 'args.json' }); + }); + + it('should accept args as a module ref and default export to "default"', () => { + const parsed = configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { ...validContract, args: { module: 'args.ts' } }, + }, + }); + expect(parsed.contracts.Counter.args).toEqual({ + module: 'args.ts', + export: 'default', + }); + }); +}); + +describe('configSchema — required fields', () => { + it('should reject a contract missing signing_key_file', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + contracts: { Counter: { artifact: 'src/artifacts/Counter' } }, + }), + ).toThrow(); + }); + + it('should reject a network missing network_id', () => { + const { network_id: _omit, ...withoutId } = validNetwork; + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { testnet: withoutId }, + }), + ).toThrow(); + }); +}); + +describe('isFileRef / isModuleRef', () => { + it('should distinguish a file ref', () => { + expect(isFileRef({ file: 'x' })).toBe(true); + expect(isFileRef({ module: 'x' })).toBe(false); + expect(isFileRef(undefined)).toBe(false); + expect(isFileRef(null)).toBe(false); + expect(isFileRef('plain string')).toBe(false); + }); + + it('should distinguish a module ref', () => { + expect(isModuleRef({ module: 'x', export: 'default' })).toBe(true); + expect(isModuleRef({ file: 'x' })).toBe(false); + expect(isModuleRef(undefined)).toBe(false); + expect(isModuleRef(null)).toBe(false); + }); +}); diff --git a/packages/deployer/src/config/schema.ts b/packages/deployer/src/config/schema.ts new file mode 100644 index 0000000..ea571b8 --- /dev/null +++ b/packages/deployer/src/config/schema.ts @@ -0,0 +1,105 @@ +/** + * Zod schema for `compact.toml`. Cross-field rules: `profile.default_network` + * must name a defined `[networks.X]`; `private_state_id` and + * `init_private_state` are both-or-neither. + */ + +import { z } from 'zod'; + +const url = z.string().url(); + +const profileSchema = z + .object({ + default_network: z.string().optional(), + artifacts_dir: z.string().default('src/artifacts'), + deployments_dir: z.string().default('deployments/compact'), + }) + .default({}); + +const localWalletSchema = z.object({ + source: z.literal('local'), + index: z.number().int().min(0).max(3).default(0), +}); + +const networkSchema = z.object({ + network_id: z.string().min(1), + indexer: url, + indexer_ws: url, + node: url, + node_ws: url, + proof_server: z.union([url, z.literal('auto')]).optional(), + wallet: localWalletSchema.optional(), + // Optional block-explorer base URL (e.g. `https://preview.midnightexplorer.com`). + // When set, the CLI prints `/contracts/0x
` on a successful + // deploy. Trailing slash is stripped at print time. + explorer: url.optional(), +}); + +const walletObjectSchema = z.object({ + keystore: z.string().optional(), +}); +const walletSchema = walletObjectSchema.optional(); + +const fileRefSchema = z.object({ file: z.string().min(1) }); +const moduleRefSchema = z.object({ + module: z.string().min(1), + export: z.string().default('default'), +}); +const fileOrModuleRefSchema = z.union([fileRefSchema, moduleRefSchema]); + +const argsSchema = z.union([z.array(z.unknown()), fileOrModuleRefSchema]); + +const contractSchema = z + .object({ + artifact: z.string().min(1), + private_state_id: z.string().optional(), + init_private_state: fileOrModuleRefSchema.optional(), + private_state_store_name: z.string().optional(), + args: argsSchema.optional(), + witnesses: fileOrModuleRefSchema.optional(), + signing_key_file: z.string().min(1), + }) + .refine( + (c) => + (c.private_state_id === undefined) === + (c.init_private_state === undefined), + { + message: + 'private_state_id and init_private_state must be set together (or both omitted)', + }, + ); + +export const configSchema = z + .object({ + profile: profileSchema, + networks: z.record(z.string(), networkSchema), + wallet: walletSchema, + contracts: z.record(z.string(), contractSchema), + }) + .refine( + (c) => + c.profile.default_network === undefined || + Object.hasOwn(c.networks, c.profile.default_network), + { + message: + 'profile.default_network must reference a defined [networks.X] block', + path: ['profile', 'default_network'], + }, + ); + +export type CompactConfigData = z.infer; +export type NetworkConfig = z.infer; +export type ContractConfig = z.infer; +export type Profile = z.infer; +export type WalletConfig = z.infer; +export type FileRef = z.infer; +export type ModuleRef = z.infer; +export type FileOrModuleRef = z.infer; + +export function isFileRef(v: unknown): v is FileRef { + return typeof v === 'object' && v !== null && 'file' in v; +} + +export function isModuleRef(v: unknown): v is ModuleRef { + return typeof v === 'object' && v !== null && 'module' in v; +} diff --git a/packages/deployer/src/deployer.test.ts b/packages/deployer/src/deployer.test.ts new file mode 100644 index 0000000..5ac6d08 --- /dev/null +++ b/packages/deployer/src/deployer.test.ts @@ -0,0 +1,765 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; +import type { MidnightWalletProvider } from '@midnight-ntwrk/testkit-js'; +import pino from 'pino'; +import * as Rx from 'rxjs'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from 'vitest'; +import { Deployer } from './deployer.ts'; +import { DeployTxFailedError, UnfundedWalletError } from './errors.ts'; +import { buildProviders } from './providers/build.ts'; +import { WalletHandler } from './wallet/handler.ts'; + +vi.mock('./loaders/artifact.ts', () => ({ + Artifact: { + load: vi.fn(async () => ({ + artifactPath: '/fake/artifact', + zkConfigPath: '/fake/artifact', + compiledContract: { fake: 'compiled' }, + circuitNames: ['increment'], + })), + }, +})); + +vi.mock('./providers/proof-server.ts', () => ({ + ProofServer: { + start: vi.fn(async () => ({ + url: 'http://localhost:6300', + [Symbol.asyncDispose]: async () => { + // no-op for static-URL stub + }, + })), + }, +})); + +vi.mock('./providers/build.ts', () => ({ + buildProviders: vi.fn(() => ({})), +})); + +vi.mock('./wallet/handler.ts', () => ({ + WalletHandler: { build: vi.fn() }, +})); + +vi.mock('@midnight-ntwrk/midnight-js-contracts', () => ({ + deployContract: vi.fn(), +})); + +// Identity-throttle so `syncAndVerifyFunds`'s progress + checkpoint +// subscriptions fire on the single state emission instead of waiting +// 30 s / 5 min in real wall-clock for the trailing tick. +vi.mock('rxjs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + throttleTime: + () => + (src: import('rxjs').Observable): import('rxjs').Observable => + src, + }; +}); + +vi.mock('@midnight-ntwrk/midnight-js-network-id', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + // logWalletAddresses passes whatever this returns to the codec + // mock which ignores the arg. Opaque value is fine. + getNetworkId: vi.fn(() => 0), + }; +}); + +// Stub the bech32 codec triplet so `logWalletAddresses` reaches its +// happy-path info logs instead of catching at the encode call. +vi.mock('@midnight-ntwrk/wallet-sdk-address-format', () => { + const codec = { + encode: vi.fn(() => ({ toString: () => 'addr1stub' })), + }; + return { + ShieldedAddress: { codec }, + UnshieldedAddress: { codec }, + DustAddress: { codec }, + }; +}); + +const silentLogger = pino({ level: 'silent' }); + +interface FakeProvider { + getCoinPublicKey: () => string; + start: Mock; + stop: Mock; + wallet: { + state: () => Rx.Observable; + shielded: { tag: string; state?: Rx.Observable }; + unshielded?: { state: Rx.Observable }; + dust?: { state: Rx.Observable }; + }; +} + +function fakeSubWalletStates() { + const addr = { address: 'addr-bytes' }; + return { + shielded: Rx.of(addr), + unshielded: Rx.of(addr), + dust: Rx.of(addr), + }; +} + +/** + * Emits one already-synced `FacadeState` with a `Proxy` balance map that + * returns `1n` for any token key, so `syncAndVerifyFunds` passes through + * without a real Rx pipeline (we don't mock ledger-v8 in this file). + */ +function fakeProvider(coinKey = '0xCOIN'): FakeProvider { + const anyKeyHasBalance = new Proxy({} as Record, { + get: () => 1n, + }); + const syncedState = { + isSynced: true, + shielded: { + balances: anyKeyHasBalance, + state: { progress: { isStrictlyComplete: () => true } }, + }, + unshielded: { + balances: anyKeyHasBalance, + progress: { isStrictlyComplete: () => true }, + }, + dust: { + state: { progress: { isStrictlyComplete: () => true } }, + balance: () => 1n, + }, + }; + const sub = fakeSubWalletStates(); + return { + getCoinPublicKey: () => coinKey, + start: vi.fn(async () => undefined), + stop: vi.fn(async () => undefined), + wallet: { + state: () => Rx.of(syncedState as unknown), + shielded: { tag: 'shielded', state: sub.shielded }, + unshielded: { state: sub.unshielded }, + dust: { state: sub.dust }, + }, + }; +} + +function asInjected(p: FakeProvider): MidnightWalletProvider { + return p as unknown as MidnightWalletProvider; +} + +interface FakeOwned { + owned: WalletHandler; + provider: FakeProvider; + dispose: Mock; + saveCache: Mock; +} + +function fakeOwnedWallet(coinKey = '0xCOIN'): FakeOwned { + return fakeOwnedFromProvider(fakeProvider(coinKey)); +} + +function fakeOwnedFromProvider(provider: FakeProvider): FakeOwned { + const dispose = vi.fn(async () => { + await provider.stop(); + }); + const saveCache = vi.fn(async () => undefined); + const owned = { + provider, + saveCache, + [Symbol.asyncDispose]: dispose, + } as unknown as WalletHandler; + return { owned, provider, dispose, saveCache }; +} + +/** + * Provider whose `wallet.state()` is fully caller-controlled. Used to drive + * timeout / unfunded / mixed-funds branches of `syncAndVerifyFunds`. + */ +function fakeProviderWithState( + state$: Rx.Observable, + coinKey = '0xCOIN', +): FakeProvider { + const sub = fakeSubWalletStates(); + return { + getCoinPublicKey: () => coinKey, + start: vi.fn(async () => undefined), + stop: vi.fn(async () => undefined), + wallet: { + state: () => state$, + shielded: { tag: 'shielded', state: sub.shielded }, + unshielded: { state: sub.unshielded }, + dust: { state: sub.dust }, + }, + }; +} + +/** Build a single FacadeState with caller-supplied shielded/unshielded balance maps. */ +function syncedState( + shielded: Record, + unshielded: Record, +): unknown { + return { + isSynced: true, + shielded: { + balances: shielded, + state: { progress: { isStrictlyComplete: () => true } }, + }, + unshielded: { + balances: unshielded, + progress: { isStrictlyComplete: () => true }, + }, + dust: { + state: { progress: { isStrictlyComplete: () => true } }, + balance: () => 0n, + }, + }; +} + +type DeployTxResult = Awaited>; +function fakeDeployTxResult(address = '0xCONTRACT'): DeployTxResult { + return { + deployTxData: { + public: { + contractAddress: address, + txHash: '0xHASH', + txId: '0xTX', + blockHeight: 1234, + }, + }, + } as unknown as DeployTxResult; +} + +interface Fixture { + rootDir: string; + configPath: string; + cleanup: () => void; +} + +function writeFixture(opts: { explorer?: string } = {}): Fixture { + const rootDir = mkdtempSync(join(tmpdir(), 'deployer-test-')); + const explorerLine = opts.explorer + ? `explorer = "${opts.explorer}"\n` + : ''; + const toml = ` +[profile] +artifacts_dir = "artifacts" +deployments_dir = "deployments" + +[networks.local] +network_id = "undeployed" +indexer = "http://localhost:8088/api/v1/graphql" +indexer_ws = "ws://localhost:8088/api/v1/graphql/ws" +node = "http://localhost:9944" +node_ws = "ws://localhost:9944" +proof_server = "http://localhost:6300" +wallet = { source = "local", index = 0 } +${explorerLine} +[contracts.Counter] +artifact = "Counter" +signing_key_file = "signing-key.hex" +`; + writeFileSync(join(rootDir, 'compact.toml'), toml); + writeFileSync(join(rootDir, 'signing-key.hex'), `${'aa'.repeat(32)}\n`); + return { + rootDir, + configPath: join(rootDir, 'compact.toml'), + cleanup: () => rmSync(rootDir, { recursive: true, force: true }), + }; +} + +describe('Deployer', () => { + let fx: Fixture; + + beforeEach(() => { + fx = writeFixture(); + // Default owned-build returns a fresh fake; tests that need to + // introspect the built provider override with `mockResolvedValueOnce`. + vi.mocked(WalletHandler.build).mockImplementation( + async () => fakeOwnedWallet().owned, + ); + vi.mocked(deployContract).mockResolvedValue(fakeDeployTxResult()); + }); + + afterEach(() => { + fx.cleanup(); + vi.clearAllMocks(); + }); + + it('should return dryRun:true and not submit a tx on dryRun', async () => { + const injected = fakeProvider('0xINJECTED'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.dryRun(); + + expect(result.dryRun).toBe(true); + expect(result.address).toBe(''); + expect(result.txHash).toBe(''); + expect(result.deploymentsFile).toBe(''); + expect(result.contractName).toBe('Counter'); + expect(result.network).toBe('local'); + expect(result.deployer).toBe('0xINJECTED'); + expect(deployContract).not.toHaveBeenCalled(); + }); + + it('should submit the tx and return the populated success result on deploy', async () => { + const injected = fakeProvider('0xDEPLOYER'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + + expect(deployContract).toHaveBeenCalledTimes(1); + expect(buildProviders).toHaveBeenCalledTimes(1); + expect(result.dryRun).toBe(false); + expect(result.address).toBe('0xCONTRACT'); + expect(result.txHash).toBe('0xHASH'); + expect(result.txId).toBe('0xTX'); + expect(result.blockHeight).toBe(1234); + expect(result.deployer).toBe('0xDEPLOYER'); + expect(result.deploymentsFile).toContain('deployments'); + }); + + it('should adopt an injected walletProvider and not call WalletHandler.build', async () => { + const injected = fakeProvider(); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + expect(d.contractName).toBe('Counter'); + expect(WalletHandler.build).not.toHaveBeenCalled(); + expect(injected.start).not.toHaveBeenCalled(); + }); + + it('should build and start a wallet when none is injected', async () => { + const built = fakeOwnedWallet('0xBUILT'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + }); + expect(d.deployer).toBe('0xBUILT'); + expect(WalletHandler.build).toHaveBeenCalledTimes(1); + // Deployer calls start(false) and then runs its own sync gate + + // saveCache; assert the start arg and that saveCache fired (twice: + // once via the periodic checkpoint tick, once via the post-sync + // final snapshot). + expect(built.provider.start).toHaveBeenCalledWith(false); + expect(built.saveCache.mock.calls.length).toBeGreaterThanOrEqual(1); + }); + + it('should dispose the owned wallet on asyncDispose but not the injected one', async () => { + const built = fakeOwnedWallet('0xOWNED'); + const injected = fakeProvider('0xINJ'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + { + await using owned = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + }); + expect(owned.deployer).toBe('0xOWNED'); + } + { + await using adopted = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + expect(adopted.deployer).toBe('0xINJ'); + } + expect(built.dispose).toHaveBeenCalledTimes(1); + expect(built.provider.stop).toHaveBeenCalledTimes(1); + expect(injected.stop).not.toHaveBeenCalled(); + }); + + it('should wrap midnight-js deploy failures in DeployTxFailedError', async () => { + vi.mocked(deployContract).mockRejectedValueOnce( + new Error('chain rejected'), + ); + const injected = fakeProvider(); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + await expect(d.deploy()).rejects.toBeInstanceOf(DeployTxFailedError); + }); + + describe('syncAndVerifyFunds (owned-wallet branch)', () => { + it('should reject with a timeout error when the wallet never reports isSynced', async () => { + const built = fakeOwnedFromProvider( + fakeProviderWithState(Rx.NEVER, '0xSTUCK'), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 50, + }), + ).rejects.toThrow(/Wallet sync timeout after 50ms/); + }); + + it('should throw UnfundedWalletError when shielded and unshielded balances are both empty', async () => { + const built = fakeOwnedFromProvider( + fakeProviderWithState( + Rx.of(syncedState({}, {})), + '0xEMPTY', + ), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + }), + ).rejects.toBeInstanceOf(UnfundedWalletError); + }); + + it('should NOT throw when only the unshielded side has a positive balance', async () => { + // Use a Proxy so any token key returns the expected balance. The + // ledger token raw key is opaque from this file. + const unshieldedAny = new Proxy({} as Record, { + get: () => 5n, + }); + const built = fakeOwnedFromProvider( + fakeProviderWithState( + Rx.of(syncedState({}, unshieldedAny)), + '0xUNSHIELDED-ONLY', + ), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xUNSHIELDED-ONLY'); + expect(built.saveCache.mock.calls.length).toBeGreaterThanOrEqual(1); + }); + + it('should NOT throw when only the shielded side has a positive balance', async () => { + const shieldedAny = new Proxy({} as Record, { + get: () => 3n, + }); + const built = fakeOwnedFromProvider( + fakeProviderWithState( + Rx.of(syncedState(shieldedAny, {})), + '0xSHIELDED-ONLY', + ), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xSHIELDED-ONLY'); + }); + }); + + describe('explorer URL', () => { + it('should return an empty explorerUrl when no explorer is configured', async () => { + const injected = fakeProvider('0xDEP'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + expect(result.explorerUrl).toBe(''); + }); + + it('should return an empty explorerUrl when the address is empty', async () => { + const customFx = writeFixture({ explorer: 'https://explorer.example' }); + try { + vi.mocked(deployContract).mockResolvedValueOnce({ + deployTxData: { + public: { + contractAddress: '', + txHash: '0xH', + txId: '0xT', + blockHeight: 1, + }, + }, + } as unknown as DeployTxResult); + const injected = fakeProvider('0xDEP'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + expect(result.explorerUrl).toBe(''); + } finally { + customFx.cleanup(); + } + }); + + it('should NOT double-prefix when the address already starts with 0x', async () => { + const customFx = writeFixture({ explorer: 'https://explorer.example' }); + try { + // fakeDeployTxResult default address already includes the 0x prefix. + const injected = fakeProvider('0xDEP'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + expect(result.explorerUrl).toBe( + 'https://explorer.example/contracts/0xCONTRACT', + ); + } finally { + customFx.cleanup(); + } + }); + + it('should add the 0x prefix when the address lacks one', async () => { + const customFx = writeFixture({ explorer: 'https://explorer.example' }); + try { + vi.mocked(deployContract).mockResolvedValueOnce( + fakeDeployTxResult('BARE'), + ); + const injected = fakeProvider('0xDEP'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + expect(result.explorerUrl).toBe( + 'https://explorer.example/contracts/0xBARE', + ); + } finally { + customFx.cleanup(); + } + }); + + it('should strip a trailing slash from the explorer base', async () => { + const customFx = writeFixture({ explorer: 'https://explorer.example/' }); + try { + const injected = fakeProvider('0xDEP'); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + walletProvider: asInjected(injected), + }); + const result = await d.deploy(); + expect(result.explorerUrl).toBe( + 'https://explorer.example/contracts/0xCONTRACT', + ); + } finally { + customFx.cleanup(); + } + }); + }); + + describe('resolveTargets', () => { + it('should throw ConfigError when no --network is passed and no [profile].default_network is set', async () => { + // fixture has no default_network, so omitting `network` triggers the throw + await expect( + Deployer.prepare({ + contract: 'Counter', + configPath: fx.configPath, + logger: silentLogger, + walletProvider: asInjected(fakeProvider()), + }), + ).rejects.toThrow(/No network selected/); + }); + }); + + describe('owned-wallet saveCache failure', () => { + it('should warn-log and continue when the post-sync saveCache throws', async () => { + const provider = fakeProvider('0xWARN'); + const dispose = vi.fn(async () => { + await provider.stop(); + }); + // First call comes from the checkpoint sub (best-effort, never + // awaited by the source) and we let it succeed to avoid leaking + // an unhandled rejection from the `onCheckpoint().finally(...)` + // in the source. The second call is the awaited post-sync save + // whose failure we DO want to assert is warn-logged. + let calls = 0; + const saveCache = vi.fn(async () => { + calls += 1; + if (calls === 1) return; + throw new Error('disk full'); + }); + const owned = { + provider, + saveCache, + [Symbol.asyncDispose]: dispose, + } as unknown as WalletHandler; + vi.mocked(WalletHandler.build).mockResolvedValueOnce(owned); + + const warn = vi.fn(); + const loggerWithWarn = pino({ level: 'silent' }); + // biome-ignore lint/suspicious/noExplicitAny: spy on pino warn + (loggerWithWarn as any).warn = warn; + + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: loggerWithWarn, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xWARN'); + expect(warn).toHaveBeenCalledWith( + expect.objectContaining({ err: 'disk full' }), + expect.stringContaining('Wallet cache save failed'), + ); + }); + }); + + describe('logWalletAddresses', () => { + it('should log the three bech32 addresses on the owned-wallet happy path', async () => { + const built = fakeOwnedWallet('0xADDR'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + + const info = vi.fn(); + const logger = pino({ level: 'silent' }); + // biome-ignore lint/suspicious/noExplicitAny: spy on pino info + (logger as any).info = info; + + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xADDR'); + expect(info).toHaveBeenCalledWith( + 'Wallet addresses (verify these match your seed):', + ); + expect(info).toHaveBeenCalledWith( + expect.stringMatching(/shielded:.*addr1stub/), + ); + }); + }); + + describe('describeProgress branches', () => { + it('should render the progress percentage when highest > 0', async () => { + // Mid-sync state (NOT yet `isSynced`) that drives the progress + // subscription's "else" branch (highest > 0). Then a follow-up + // synced state lets `firstValueFrom(filter(isSynced))` resolve so + // the prepare call terminates. + const midState = { + isSynced: false, + shielded: { + balances: {} as Record, + state: { + progress: { + isStrictlyComplete: () => false, + appliedIndex: 10n, + highestIndex: 100n, + isConnected: true, + }, + }, + }, + unshielded: { + balances: {} as Record, + progress: { + isStrictlyComplete: () => false, + appliedId: 5n, + highestTransactionId: 50n, + isConnected: true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => false, + appliedIndex: 1n, + highestIndex: 10n, + isConnected: true, + }, + }, + balance: () => 0n, + }, + }; + const anyKeyHasBalance = new Proxy({} as Record, { + get: () => 1n, + }); + const syncedState = { + isSynced: true, + shielded: { + balances: anyKeyHasBalance, + state: { progress: { isStrictlyComplete: () => true } }, + }, + unshielded: { + balances: anyKeyHasBalance, + progress: { isStrictlyComplete: () => true }, + }, + dust: { + state: { progress: { isStrictlyComplete: () => true } }, + balance: () => 1n, + }, + }; + const built = fakeOwnedFromProvider( + fakeProviderWithState( + Rx.of(midState as unknown, syncedState as unknown), + '0xPROGRESS', + ), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xPROGRESS'); + }); + }); +}); diff --git a/packages/deployer/src/deployer.ts b/packages/deployer/src/deployer.ts new file mode 100644 index 0000000..59e924c --- /dev/null +++ b/packages/deployer/src/deployer.ts @@ -0,0 +1,692 @@ +import { + shieldedToken, + unshieldedToken, +} from '@midnight-ntwrk/ledger-v8'; +import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; +import { getNetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import type { PrivateStateProvider } from '@midnight-ntwrk/midnight-js-types'; +import { + type EnvironmentConfiguration, + type MidnightWalletProvider, +} from '@midnight-ntwrk/testkit-js'; +import { + DustAddress, + ShieldedAddress, + UnshieldedAddress, +} from '@midnight-ntwrk/wallet-sdk-address-format'; +import type { FacadeState } from '@midnight-ntwrk/wallet-sdk-facade'; +import type { Logger } from 'pino'; +import * as Rx from 'rxjs'; +import { CompactConfig } from './config/compact-config.ts'; +import type { ContractConfig, NetworkConfig } from './config/schema.ts'; +import { type DeploymentRecord, Deployments } from './deployments.ts'; +import { + ConfigError, + DeployTxFailedError, + UnfundedWalletError, +} from './errors.ts'; + +/** + * Default sync ceiling (10 min). Overrides testkit-js's hardcoded 90 s + * `waitForFunds` timeout, which is too short for real networks. + */ +const DEFAULT_SYNC_TIMEOUT_MS = 10 * 60 * 1000; +import { ConstructorArgs } from './loaders/args.ts'; +import { Artifact } from './loaders/artifact.ts'; +import { InitialPrivateState } from './loaders/init-state.ts'; +import { SigningKey } from './loaders/signing-key.ts'; +import { buildProviders } from './providers/build.ts'; +import { applyNetwork } from './providers/network.ts'; +import { ProofServer } from './providers/proof-server.ts'; +import { WalletHandler } from './wallet/handler.ts'; +import { resolveSeed } from './wallet/seeds.ts'; + +/** Inputs to {@link Deployer.prepare}. */ +export interface DeployerOptions { + contract: string; + network?: string; + configPath?: string; + seedFile?: string; + proofServer?: string; + argsOverride?: string; + /** + * Programmatic constructor args. Highest precedence — overrides + * `argsOverride`, the TOML `args` field, and any file/module ref. + * Either a positional array (`[a, b, c]`) or a named object + * (`{ foo: a, bar: b }`); named objects are reordered to match the + * artifact's constructor signature. + */ + args?: readonly unknown[] | Record; + initPrivateStateOverride?: string; + logger: Logger; + promptPassphrase?: (path: string) => Promise; + /** + * Inject a shared wallet so back-to-back deploys reuse one UTXO view. + * When set, prepare skips seed resolution + lifecycle management. + * The caller owns `start()`/`stop()`. Avoids `DustDoubleSpend` from + * indexer lag between rapid deploys. + */ + walletProvider?: MidnightWalletProvider; + /** + * Pass `inMemoryPrivateStateProvider()` in tests; otherwise multiple + * deployers in one process hit fcntl LOCK contention on the default + * LevelDB directory. + */ + privateStateProvider?: PrivateStateProvider; + /** Sync ceiling (ms). Defaults to {@link DEFAULT_SYNC_TIMEOUT_MS}. Ignored when {@link walletProvider} is injected. */ + syncTimeoutMs?: number; + /** Force a fresh sync from genesis. Default `false` (cache reuse saves the 30–60 min first-preprod sync). */ + skipWalletCache?: boolean; + /** + * Import a pre-warmed dust wallet state file into `.states/` before + * the wallet builds. Use this to skip the first-run preprod cold + * sync when you already have a `serializeState()` output from a + * prior session. Argv: `--seed-cache-from-dust`. + */ + seedCacheDust?: string; + /** Like {@link seedCacheDust} but for the shielded sub-wallet. Argv: `--seed-cache-from-shielded`. */ + seedCacheShielded?: string; +} + +/** Result of {@link Deployer.deploy} / {@link Deployer.dryRun}. On-chain fields are empty when `dryRun: true`. */ +export interface DeployResult { + contractName: string; + network: string; + address: string; + txHash: string; + txId: string; + blockHeight: number; + signingKey: string; + deployer: string; + artifact: string; + deploymentsFile: string; + dryRun: boolean; + /** `[networks.X].explorer` + `/contracts/0x
`, or empty when no explorer is configured / in dry-run. */ + explorerUrl: string; +} + +interface PreparedState { + opts: DeployerOptions; + logger: Logger; + config: CompactConfig; + networkName: string; + network: NetworkConfig; + contract: ContractConfig; + signingKey: SigningKey; + artifact: Artifact; + args: ConstructorArgs; + initialPrivateState: InitialPrivateState | undefined; + wallet: MidnightWalletProvider; + deployer: string; + env: EnvironmentConfiguration; + resources: AsyncDisposableStack; +} + +/** + * Stateful handle for one contract's deploy lifecycle. Always acquire + * with `await using`: `[Symbol.asyncDispose]` releases the proof-server + * container (if `"auto"`) and the wallet (if built here, not injected). + */ +export class Deployer implements AsyncDisposable { + /** Contract name as specified in opts. */ + readonly contractName: string; + /** Resolved network name (`opts.network` or `[profile].default_network`). */ + readonly networkName: string; + /** Hex of the deployer's coin public key. */ + readonly deployer: string; + /** Loaded artifact: zk config path + compiled-contract handle. */ + readonly artifact: Artifact; + /** Per-contract signing key loaded from disk. */ + readonly signingKey: SigningKey; + + readonly #state: PreparedState; + + private constructor(state: PreparedState) { + this.#state = state; + this.contractName = state.opts.contract; + this.networkName = state.networkName; + this.deployer = state.deployer; + this.artifact = state.artifact; + this.signingKey = state.signingKey; + } + + /** + * Load config + artifact + signing key, start proof server, build or + * adopt a wallet. Throws typed errors that map to CLI exit codes via + * {@link DeployError.exitCode}. + */ + static async prepare(opts: DeployerOptions): Promise { + const { logger } = opts; + + const config = await CompactConfig.load(opts.configPath); + const { rootDir } = config; + const { networkName, network, contract } = resolveTargets(opts, config); + const signingKey = await SigningKey.load( + rootDir, + contract.signing_key_file, + ); + + const seedResolution = opts.walletProvider + ? undefined + : await resolveSeed({ + config, + networkName, + network, + seedFile: opts.seedFile, + promptPassphrase: opts.promptPassphrase, + }); + if (seedResolution) { + logger.debug(`Resolved deployer seed from: ${seedResolution.origin}`); + } + + // Stack owns every resource acquired below. On any throw before + // the final `stack.move()`, `await using` disposes them in reverse + // order; on success, ownership transfers to the returned Deployer + // and the local `await using` becomes a no-op. + await using stack = new AsyncDisposableStack(); + + const proofServer = await ProofServer.start({ + cliOverride: opts.proofServer, + network, + logger, + }); + stack.use(proofServer); + + const { env } = applyNetwork(network, proofServer.url); + logger.debug( + `Network ID: ${env.networkId}; proof server: ${env.proofServer}`, + ); + + const artifact = await Artifact.load({ + rootDir, + artifactsDir: config.artifactsDir, + artifact: contract.artifact, + contractName: opts.contract, + witnesses: contract.witnesses, + }); + logger.debug( + `Artifact: ${artifact.artifactPath} (${artifact.circuitNames.length} circuits)`, + ); + + let wallet: MidnightWalletProvider; + if (opts.walletProvider) { + wallet = opts.walletProvider; + } else { + if (!seedResolution) { + throw new Error('internal: seedResolution missing for owned wallet'); + } + const owned = await WalletHandler.build(logger, env, seedResolution.seed, { + skipWalletCache: opts.skipWalletCache, + seedCacheDust: opts.seedCacheDust, + seedCacheShielded: opts.seedCacheShielded, + }); + stack.use(owned); + wallet = owned.provider; + // Kick off the wallet's internal indexer subscription without + // blocking on testkit-js's 90 s `waitForFunds` gate (which is too + // short for real networks). Then drive sync ourselves with a + // configurable ceiling and surface a clear `UnfundedWalletError` + // if we reach chain tip and still have no shielded balance. + await wallet.start(false); + // Surface the wallet's derived bech32m addresses right away so + // the user can sanity-check they match the seed they intended + // *before* settling in for a long shielded sync. + await logWalletAddresses(wallet, logger); + await syncAndVerifyFunds({ + wallet, + timeoutMs: opts.syncTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS, + logger, + // Periodic checkpoint: every 5 min during sync, snapshot both + // sub-wallet caches. If the user interrupts a long first-run, + // the next attempt resumes from the most recent checkpoint. + onCheckpoint: () => owned.saveCache(), + }); + // Snapshot the shielded + dust sub-wallets now that sync is + // complete. Best-effort: failures are warn-logged in + // `saveCache`'s caller; never block the deploy on a cache write. + try { + await owned.saveCache(); + } catch (e) { + logger.warn( + { err: (e as Error).message }, + 'Wallet cache save failed; next run will re-sync', + ); + } + } + + const args = await ConstructorArgs.load( + contract, + rootDir, + opts.argsOverride, + opts.args, + artifact.artifactPath, + ); + const initialPrivateState = await InitialPrivateState.load( + contract.init_private_state, + rootDir, + ); + const deployer = wallet.getCoinPublicKey(); + + return new Deployer({ + opts, + logger, + config, + networkName, + network, + contract, + signingKey, + artifact, + args, + initialPrivateState, + wallet, + deployer, + env, + resources: stack.move(), + }); + } + + /** Submit the deploy tx, persist the record under `deployments/.json`, return the result. */ + async deploy(): Promise { + const s = this.#state; + const providers = buildProviders({ + env: s.env, + wallet: s.wallet, + contractName: s.opts.contract, + contract: s.contract, + zkConfigPath: s.artifact.zkConfigPath, + privateStateProvider: s.opts.privateStateProvider, + }); + const txResult = await executeDeploy({ + providers, + contractName: s.opts.contract, + contract: s.contract, + artifact: s.artifact, + signingKey: s.signingKey.hex, + args: s.args.values, + initialPrivateState: s.initialPrivateState?.value, + }); + + const record = toDeploymentRecord({ + deployTxData: txResult.deployTxData, + signingKey: s.signingKey.hex, + deployer: s.deployer, + artifact: s.contract.artifact, + }); + + const deployments = new Deployments({ + rootDir: s.config.rootDir, + deploymentsDir: s.config.deploymentsDir, + network: s.networkName, + }); + const persisted = await deployments.record(s.opts.contract, record); + + return { + contractName: s.opts.contract, + network: s.networkName, + address: record.address, + txHash: record.txHash, + txId: record.txId, + blockHeight: record.blockHeight, + signingKey: record.signingKey, + deployer: record.deployer, + artifact: record.artifact, + deploymentsFile: persisted.head, + dryRun: false, + explorerUrl: buildExplorerUrl(s.network.explorer, record.address), + }; + } + + /** Log a "would deploy" event and return a synthetic result. No tx, no file. */ + async dryRun(): Promise { + const s = this.#state; + s.logger.info( + { + contract: s.opts.contract, + network: s.networkName, + artifact: s.artifact.artifactPath, + argCount: s.args.length, + hasPrivateState: s.initialPrivateState !== undefined, + deployer: s.deployer, + }, + 'dry-run: would deploy', + ); + return { + contractName: s.opts.contract, + network: s.networkName, + address: '', + txHash: '', + txId: '', + blockHeight: 0, + signingKey: s.signingKey.hex, + deployer: s.deployer, + artifact: s.contract.artifact, + deploymentsFile: '', + dryRun: true, + explorerUrl: '', + }; + } + + async [Symbol.asyncDispose](): Promise { + await this.#state.resources.disposeAsync(); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +interface ResolvedTargets { + networkName: string; + network: NetworkConfig; + contract: ContractConfig; +} + +function resolveTargets( + opts: DeployerOptions, + config: CompactConfig, +): ResolvedTargets { + const networkName = opts.network ?? config.defaultNetwork; + if (!networkName) { + throw new ConfigError( + 'No network selected. Pass --network or set [profile].default_network.', + ); + } + return { + networkName, + network: config.network(networkName), + contract: config.contract(opts.contract), + }; +} + +/** + * Print the wallet's three bech32m addresses so the user can verify + * the seed before a long sync. Best-effort: warn-and-continue on + * failure. + */ +async function logWalletAddresses( + wallet: MidnightWalletProvider, + logger: Logger, +): Promise { + try { + const networkId = getNetworkId(); + const [shieldedState, unshieldedState, dustState] = await Promise.all([ + Rx.firstValueFrom(wallet.wallet.shielded.state), + Rx.firstValueFrom(wallet.wallet.unshielded.state), + Rx.firstValueFrom(wallet.wallet.dust.state), + ]); + const shielded = ShieldedAddress.codec + .encode(networkId, shieldedState.address) + .toString(); + const unshielded = UnshieldedAddress.codec + .encode(networkId, unshieldedState.address) + .toString(); + const dust = DustAddress.codec + .encode(networkId, dustState.address) + .toString(); + logger.info(`Wallet addresses (verify these match your seed):`); + logger.info(` shielded: ${shielded}`); + logger.info(` unshielded: ${unshielded}`); + logger.info(` dust: ${dust}`); + } catch (e) { + logger.warn( + { err: (e as Error).message }, + 'Could not derive wallet addresses for display; continuing', + ); + } +} + +/** + * One-liner progress string for "Still syncing". Accepts both progress + * shapes (shielded/dust use `appliedIndex`/`highestIndex`; unshielded + * uses `appliedId`/`highestTransactionId`). + */ +function describeProgress(p: { isStrictlyComplete: () => boolean }): string { + const complete = p.isStrictlyComplete(); + const fields = p as unknown as { + appliedIndex?: bigint; + highestIndex?: bigint; + highestRelevantIndex?: bigint; + appliedId?: bigint; + highestTransactionId?: bigint; + isConnected?: boolean; + }; + const applied = fields.appliedIndex ?? fields.appliedId ?? 0n; + const highest = + fields.highestIndex ?? fields.highestTransactionId ?? 0n; + const connected = fields.isConnected ?? false; + // Once the indexer has told the wallet its max event id, we can + // render a real progress percentage. Until then surface "applied, + // highest unknown" and the subscription's connection state so the + // user can tell "still connecting" from "connected but no events yet" + // from "events flowing". + if (highest === 0n) { + return `applied=${applied} highest=? connected=${connected} complete=${complete}`; + } + const pct = Number((applied * 100n) / highest); + return `${applied}/${highest} (${pct}%) connected=${connected} complete=${complete}`; +} + +/** + * Drive the wallet to chain tip and assert spendable funds. Uses + * `state.isSynced` (strict-complete on all three sub-wallets) as the + * gate. Looser gates regressed on local with `Invalid Transaction + * (custom error 170)`. Throttles progress logs to once per 30 s. + * Throws {@link UnfundedWalletError} on empty wallet. + */ +async function syncAndVerifyFunds(args: { + wallet: MidnightWalletProvider; + timeoutMs: number; + logger: Logger; + /** Periodic checkpoint so a Ctrl+C mid-sync survives. Owned-wallet branch only. */ + onCheckpoint?: () => Promise; +}): Promise { + const { wallet, timeoutMs, logger, onCheckpoint } = args; + logger.info( + `Syncing wallet to chain tip (timeout ${Math.round(timeoutMs / 1000)}s)…`, + ); + const start = Date.now(); + + // Two subscriptions to the same observable: one logs throttled + // progress lines for UX, the other waits for completion. The progress + // tap deliberately runs through `Rx.throttleTime(30_000)` so the + // shielded-sync flood doesn't drown the terminal; the completion gate + // doesn't throttle, so the deploy proceeds the instant sync flips. + const state$ = wallet.wallet.state(); + const progressSub = state$ + .pipe(Rx.throttleTime(30_000, undefined, { leading: false, trailing: true })) + .subscribe((s) => { + const elapsedSec = Math.round((Date.now() - start) / 1000); + const elapsedHms = + elapsedSec < 60 + ? `${elapsedSec}s` + : `${Math.floor(elapsedSec / 60)}m ${elapsedSec % 60}s`; + // Pull running balance projections each tick so the user can + // see funds materialise mid-sync (NIGHT becomes visible the + // moment unshielded completes; dust accumulates as the wallet + // processes events even before its sync is strictly complete). + const shieldedBal = s.shielded.balances[shieldedToken().raw] ?? 0n; + const unshieldedBal = s.unshielded.balances[unshieldedToken().raw] ?? 0n; + const dustBal = s.dust.balance(new Date()); + logger.info( + `Still syncing (${elapsedHms} elapsed). ` + + `shielded ${describeProgress(s.shielded.state.progress)} balance=${shieldedBal}; ` + + `unshielded ${describeProgress(s.unshielded.progress)} balance=${unshieldedBal}; ` + + `dust ${describeProgress(s.dust.state.progress)} balance=${dustBal}`, + ); + }); + + // Periodic checkpoint: snapshot the wallet caches every 5 min so a + // user who Ctrl+C's a long preprod first-run can resume from the + // latest snapshot instead of starting at id=0 again. Best-effort: + // a failed save logs a warning and the sync keeps going. Skipped + // when `onCheckpoint` is not provided (i.e. injected-wallet callers + // where the deployer doesn't own persistence). + let checkpointInFlight = false; + const checkpointSub = onCheckpoint + ? state$ + .pipe( + Rx.throttleTime(5 * 60 * 1000, undefined, { + leading: false, + trailing: true, + }), + ) + .subscribe(() => { + if (checkpointInFlight) return; + checkpointInFlight = true; + onCheckpoint().finally(() => { + checkpointInFlight = false; + }); + }) + : undefined; + + // Per-sub-wallet edge-trigger: the first time each sub-wallet flips + // to `complete=true`, log its current balance immediately. This lets + // a user with NIGHT+dust (the typical preprod-faucet wallet shape) + // see their unshielded balance after ~30 s instead of waiting for + // the full shielded sync (30 – 60 min) to surface anything. + const seenComplete = { shielded: false, unshielded: false, dust: false }; + const balanceSub = state$.subscribe((s) => { + if (!seenComplete.unshielded && s.unshielded.progress.isStrictlyComplete()) { + seenComplete.unshielded = true; + const bal = s.unshielded.balances[unshieldedToken().raw] ?? 0n; + logger.info(`Unshielded sync complete — NIGHT balance: ${bal}`); + } + if (!seenComplete.dust && s.dust.state.progress.isStrictlyComplete()) { + seenComplete.dust = true; + const bal = s.dust.balance(new Date()); + logger.info(`Dust sync complete — dust balance: ${bal}`); + } + if (!seenComplete.shielded && s.shielded.state.progress.isStrictlyComplete()) { + seenComplete.shielded = true; + const bal = s.shielded.balances[shieldedToken().raw] ?? 0n; + logger.info(`Shielded sync complete — shielded balance: ${bal}`); + } + }); + + let synced: FacadeState; + try { + synced = await Rx.firstValueFrom( + state$.pipe( + Rx.filter((s: FacadeState) => s.isSynced), + Rx.timeout({ + each: timeoutMs, + with: () => + Rx.throwError( + () => new Error(`Wallet sync timeout after ${timeoutMs}ms`), + ), + }), + ), + ); + } finally { + progressSub.unsubscribe(); + balanceSub.unsubscribe(); + checkpointSub?.unsubscribe(); + } + + const totalSec = Math.round((Date.now() - start) / 1000); + const totalHms = + totalSec < 60 + ? `${totalSec}s` + : `${Math.floor(totalSec / 60)}m ${totalSec % 60}s`; + logger.info(`Sync complete after ${totalHms}`); + + // Accept funds in either shielded or unshielded. Preprod faucets + // hand out unshielded NIGHT, while a freshly bridged wallet may sit + // entirely in the shielded layer. Both are deployable: dust for + // fees auto-generates from either NIGHT or shielded holdings. + // Mirrors midnight-apps's `waitForUnshieldedFunds` semantics. + const shieldedBal = synced.shielded.balances[shieldedToken().raw]; + const unshieldedBal = synced.unshielded.balances[unshieldedToken().raw]; + const hasShielded = shieldedBal !== undefined && shieldedBal > 0n; + const hasUnshielded = unshieldedBal !== undefined && unshieldedBal > 0n; + if (!hasShielded && !hasUnshielded) { + throw new UnfundedWalletError(wallet.getCoinPublicKey()); + } + logger.info( + `Wallet balance: shielded=${shieldedBal ?? 0n}, unshielded=${unshieldedBal ?? 0n}`, + ); +} + +interface ExecuteDeployArgs { + providers: Parameters[0]; + contractName: string; + contract: ContractConfig; + artifact: Artifact; + signingKey: string; + args: readonly unknown[]; + initialPrivateState: unknown; +} + +/** Submit the deploy tx; wrap failures in {@link DeployTxFailedError}. */ +async function executeDeploy({ + providers, + contractName, + contract, + artifact, + signingKey, + args, + initialPrivateState, +}: ExecuteDeployArgs): Promise>> { + const compiled = artifact.compiledContract as Parameters< + typeof deployContract + >[1]['compiledContract']; + const base = { + compiledContract: compiled, + signingKey, + args, + } as Parameters[1]; + const deployOptions = + contract.private_state_id !== undefined + ? { + ...base, + privateStateId: contract.private_state_id, + initialPrivateState, + } + : base; + + try { + return await deployContract(providers, deployOptions); + } catch (e) { + throw new DeployTxFailedError( + `Deploy of "${contractName}" failed: ${(e as Error).message}`, + { cause: e }, + ); + } +} + +type ContractDeployResult = Awaited>; + +/** Build `/contracts/0x
`, or `''` when no explorer / no address. */ +function buildExplorerUrl( + base: string | undefined, + address: string, +): string { + if (!base || !address) return ''; + const trimmed = base.endsWith('/') ? base.slice(0, -1) : base; + const hex = address.startsWith('0x') ? address : `0x${address}`; + return `${trimmed}/contracts/${hex}`; +} + +function toDeploymentRecord({ + deployTxData, + signingKey, + deployer, + artifact, +}: { + deployTxData: ContractDeployResult['deployTxData']; + signingKey: string; + deployer: string; + artifact: string; +}): DeploymentRecord { + return { + address: deployTxData.public.contractAddress, + txHash: deployTxData.public.txHash, + txId: deployTxData.public.txId, + blockHeight: deployTxData.public.blockHeight, + signingKey, + deployer, + artifact, + timestamp: new Date().toISOString(), + }; +} + diff --git a/packages/deployer/src/deployments.test.ts b/packages/deployer/src/deployments.test.ts new file mode 100644 index 0000000..1ee9dd5 --- /dev/null +++ b/packages/deployer/src/deployments.test.ts @@ -0,0 +1,86 @@ +import { mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { type DeploymentRecord, Deployments } from './deployments.ts'; + +function rec(address: string): DeploymentRecord { + return { + address, + txHash: '0xhash', + txId: '0xtx', + blockHeight: 42, + signingKey: 'aa'.repeat(32), + deployer: '0xdep', + artifact: 'src/artifacts/Token/Token', + timestamp: new Date('2026-05-15T00:00:00Z').toISOString(), + }; +} + +function make(root: string): Deployments { + return new Deployments({ + rootDir: root, + deploymentsDir: 'deployments/compact', + network: 'local', + }); +} + +describe('Deployments', () => { + it('should write a fresh deployments/.json', async () => { + const root = mkdtempSync(join(tmpdir(), 'persist-test-')); + const { head } = await make(root).record('Token', rec('0xaddr1')); + const parsed = JSON.parse(readFileSync(head, 'utf8')); + expect(parsed.Token.address).toBe('0xaddr1'); + }); + + it('should rotate the previous head into history on overwrite', async () => { + const root = mkdtempSync(join(tmpdir(), 'persist-test-')); + const d = make(root); + await d.record('Token', rec('0xfirst')); + const { head, history } = await d.record('Token', rec('0xsecond')); + + const headJson = JSON.parse(readFileSync(head, 'utf8')); + const historyJson = JSON.parse(readFileSync(history, 'utf8')); + + expect(headJson.Token.address).toBe('0xsecond'); + expect(historyJson.Token).toHaveLength(1); + expect(historyJson.Token[0].address).toBe('0xfirst'); + }); + + it('should preserve other contracts when one is updated', async () => { + const root = mkdtempSync(join(tmpdir(), 'persist-test-')); + const d = make(root); + await d.record('Token', rec('0xT1')); + const { head } = await d.record('Vault', rec('0xV1')); + const headJson = JSON.parse(readFileSync(head, 'utf8')); + expect(headJson.Token.address).toBe('0xT1'); + expect(headJson.Vault.address).toBe('0xV1'); + }); + + it('should honour an absolute deploymentsDir and expose paths', async () => { + const absDir = mkdtempSync(join(tmpdir(), 'persist-abs-')); + const d = new Deployments({ + rootDir: '/unused/root', + deploymentsDir: absDir, + network: 'local', + }); + expect(d.paths.head).toBe(join(absDir, 'local.json')); + expect(d.paths.history).toBe(join(absDir, 'local.history.json')); + }); + + it('should let getHead/getHistory/listContracts read what record wrote', async () => { + const root = mkdtempSync(join(tmpdir(), 'persist-test-')); + const d = make(root); + await d.record('Token', rec('0xT1')); + await d.record('Token', rec('0xT2')); + await d.record('Vault', rec('0xV1')); + + expect((await d.getHead('Token'))?.address).toBe('0xT2'); + expect(await d.getHead('Missing')).toBeUndefined(); + expect((await d.getHistory('Token')).map((r) => r.address)).toEqual([ + '0xT1', + ]); + expect(await d.getHistory('Vault')).toEqual([]); + expect(await d.listContracts()).toEqual(['Token', 'Vault']); + }); +}); diff --git a/packages/deployer/src/deployments.ts b/packages/deployer/src/deployments.ts new file mode 100644 index 0000000..701ad26 --- /dev/null +++ b/packages/deployer/src/deployments.ts @@ -0,0 +1,113 @@ +import { existsSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, resolve } from 'node:path'; + +/** + * Two-file per-network deployment ledger: + * `.json` — head map (contract → latest deploy) + * `.history.json` — superseded records (contract → list) + * Each deploy rotates the prior head into history. + */ + +/** A single confirmed deploy. Persisted under the contract name in the head map. */ +export interface DeploymentRecord { + address: string; + txHash: string; + txId: string; + blockHeight: number; + signingKey: string; + deployer: string; + artifact: string; + timestamp: string; +} + +/** Head map: contract name → latest deploy. */ +export type DeploymentsFile = Record; + +/** History map: contract name → past deploys (newest first). */ +export type DeploymentsHistory = Record; + +export interface DeploymentsOptions { + rootDir: string; + deploymentsDir: string; + network: string; +} + +/** + * Per-network deployment ledger. Head file is written last so a crash + * mid-rotate leaves the prior head intact. + */ +export class Deployments { + readonly #headPath: string; + readonly #historyPath: string; + + constructor(opts: DeploymentsOptions) { + const dir = isAbsolute(opts.deploymentsDir) + ? opts.deploymentsDir + : resolve(opts.rootDir, opts.deploymentsDir); + this.#headPath = resolve(dir, `${opts.network}.json`); + this.#historyPath = resolve(dir, `${opts.network}.history.json`); + } + + /** Absolute on-disk paths for the two ledger files. */ + get paths(): { head: string; history: string } { + return { head: this.#headPath, history: this.#historyPath }; + } + + /** Rotate the prior head for `contractName` into history; write `record` as new head. */ + async record( + contractName: string, + record: DeploymentRecord, + ): Promise<{ head: string; history: string }> { + await mkdir(dirname(this.#headPath), { recursive: true }); + + const head = await this.#readHead(); + const previous = head[contractName]; + if (previous) { + const history = await this.#readHistory(); + const bucket = history[contractName] ?? []; + bucket.unshift(previous); + history[contractName] = bucket; + await writeJson(this.#historyPath, history); + } + + head[contractName] = record; + await writeJson(this.#headPath, head); + + return { head: this.#headPath, history: this.#historyPath }; + } + + /** Latest deploy for `contractName`, or `undefined` if none. */ + async getHead(contractName: string): Promise { + return (await this.#readHead())[contractName]; + } + + /** Per-contract history (newest first); empty array if none. */ + async getHistory(contractName: string): Promise { + return (await this.#readHistory())[contractName] ?? []; + } + + /** Names of every contract with a current head record on this network. */ + async listContracts(): Promise { + return Object.keys(await this.#readHead()).sort(); + } + + #readHead(): Promise { + return readJson(this.#headPath, {}); + } + + #readHistory(): Promise { + return readJson(this.#historyPath, {}); + } +} + +async function readJson(path: string, fallback: T): Promise { + if (!existsSync(path)) return fallback; + const raw = await readFile(path, 'utf8'); + if (!raw.trim()) return fallback; + return JSON.parse(raw) as T; +} + +async function writeJson(path: string, value: unknown): Promise { + await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); +} diff --git a/packages/deployer/src/errors.test.ts b/packages/deployer/src/errors.test.ts new file mode 100644 index 0000000..32f4886 --- /dev/null +++ b/packages/deployer/src/errors.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest'; +import { + ArtifactNotFoundError, + ConfigError, + DeployError, + DeployTxFailedError, + IndexerUnreachableError, + ProofServerUnreachableError, + UnfundedWalletError, + WalletError, +} from './errors.ts'; + +describe('DeployError', () => { + it('should default to exit code 1', () => { + const e = new DeployError('boom'); + expect(e.exitCode).toBe(1); + expect(e.name).toBe('DeployError'); + expect(e).toBeInstanceOf(Error); + }); + + it('should accept a custom exit code', () => { + const e = new DeployError('boom', 99); + expect(e.exitCode).toBe(99); + }); + + it('should preserve cause via ErrorOptions', () => { + const cause = new Error('underlying'); + const e = new DeployError('wrapper', 1, { cause }); + expect(e.cause).toBe(cause); + }); +}); + +describe('subclass exit codes', () => { + it('should pin ConfigError to 2', () => { + const e = new ConfigError('bad toml'); + expect(e.exitCode).toBe(2); + expect(e.name).toBe('ConfigError'); + expect(e).toBeInstanceOf(DeployError); + }); + + it('should pin ArtifactNotFoundError to 2', () => { + const e = new ArtifactNotFoundError('/x/y'); + expect(e.exitCode).toBe(2); + expect(e.name).toBe('ArtifactNotFoundError'); + expect(e.message).toContain('/x/y'); + expect(e).toBeInstanceOf(DeployError); + }); + + it('should pin WalletError to 3', () => { + const e = new WalletError('decrypt failed'); + expect(e.exitCode).toBe(3); + expect(e.name).toBe('WalletError'); + }); + + it('should pin UnfundedWalletError to 3 and include the address', () => { + const e = new UnfundedWalletError('mn_addr1...'); + expect(e.exitCode).toBe(3); + expect(e.name).toBe('UnfundedWalletError'); + expect(e.message).toContain('mn_addr1...'); + }); + + it('should pin ProofServerUnreachableError to 4', () => { + const e = new ProofServerUnreachableError('http://ps'); + expect(e.exitCode).toBe(4); + expect(e.name).toBe('ProofServerUnreachableError'); + expect(e.message).toContain('http://ps'); + }); + + it('should pin IndexerUnreachableError to 4', () => { + const e = new IndexerUnreachableError('http://idx'); + expect(e.exitCode).toBe(4); + expect(e.name).toBe('IndexerUnreachableError'); + expect(e.message).toContain('http://idx'); + }); + + it('should pin DeployTxFailedError to 5', () => { + const e = new DeployTxFailedError('rejected'); + expect(e.exitCode).toBe(5); + expect(e.name).toBe('DeployTxFailedError'); + }); +}); + +describe('instanceof chain', () => { + it('should let callers branch on DeployError once for any pipeline failure', () => { + const cases: DeployError[] = [ + new ConfigError('x'), + new WalletError('x'), + new ArtifactNotFoundError('x'), + new ProofServerUnreachableError('x'), + new IndexerUnreachableError('x'), + new UnfundedWalletError('x'), + new DeployTxFailedError('x'), + ]; + for (const c of cases) { + expect(c).toBeInstanceOf(DeployError); + expect(c).toBeInstanceOf(Error); + } + }); +}); diff --git a/packages/deployer/src/errors.ts b/packages/deployer/src/errors.ts new file mode 100644 index 0000000..6f9066f --- /dev/null +++ b/packages/deployer/src/errors.ts @@ -0,0 +1,74 @@ +/** + * Typed errors with stable `exitCode` per failure mode so `bin/compact-deploy` + * (and CI scripts) can branch without parsing messages. + */ + +/** Base deploy-pipeline failure. Default exit code `1`. */ +export class DeployError extends Error { + readonly exitCode: number; + constructor(message: string, exitCode = 1, options?: ErrorOptions) { + super(message, options); + this.name = 'DeployError'; + this.exitCode = exitCode; + } +} + +/** Config / TOML / schema. Exit code `2`. */ +export class ConfigError extends DeployError { + constructor(message: string, options?: ErrorOptions) { + super(message, 2, options); + this.name = 'ConfigError'; + } +} + +/** Seed, keystore, or wallet construction. Exit code `3`. */ +export class WalletError extends DeployError { + constructor(message: string, options?: ErrorOptions) { + super(message, 3, options); + this.name = 'WalletError'; + } +} + +/** Proof server unreachable. Exit code `4`. */ +export class ProofServerUnreachableError extends DeployError { + constructor(url: string, options?: ErrorOptions) { + super(`Proof server unreachable at ${url}`, 4, options); + this.name = 'ProofServerUnreachableError'; + } +} + +/** Indexer GraphQL endpoint unreachable. Exit code `4`. */ +export class IndexerUnreachableError extends DeployError { + constructor(url: string, options?: ErrorOptions) { + super(`Indexer unreachable at ${url}`, 4, options); + this.name = 'IndexerUnreachableError'; + } +} + +/** Deployer wallet has zero balance. Exit code `3`. */ +export class UnfundedWalletError extends DeployError { + constructor(address: string, options?: ErrorOptions) { + super(`Wallet ${address} has zero balance`, 3, options); + this.name = 'UnfundedWalletError'; + } +} + +/** Compiled artifact directory or required subfiles missing. Exit code `2`. */ +export class ArtifactNotFoundError extends DeployError { + constructor(path: string, options?: ErrorOptions) { + super( + `Compiled artifact not found at ${path}. Run \`compact-compiler\` to produce it.`, + 2, + options, + ); + this.name = 'ArtifactNotFoundError'; + } +} + +/** On-chain submission rejected the tx. Exit code `5`. */ +export class DeployTxFailedError extends DeployError { + constructor(message: string, options?: ErrorOptions) { + super(message, 5, options); + this.name = 'DeployTxFailedError'; + } +} diff --git a/packages/deployer/src/index.ts b/packages/deployer/src/index.ts new file mode 100644 index 0000000..c590e34 --- /dev/null +++ b/packages/deployer/src/index.ts @@ -0,0 +1,49 @@ +/** Programmatic API for `@openzeppelin/compact-deployer`; `compact-deploy` is an opinionated shell over this. */ +// biome-ignore-all lint/performance/noBarrelFile: this file is the programmatic API surface for consumers of @openzeppelin/compact-deployer +export { CompactConfig } from './config/compact-config.ts'; +export type { + ContractConfig, + NetworkConfig, + Profile, + WalletConfig, +} from './config/schema.ts'; +export type { DeployerOptions, DeployResult } from './deployer.ts'; +export { Deployer } from './deployer.ts'; +export type { + DeploymentRecord, + DeploymentsFile, + DeploymentsHistory, +} from './deployments.ts'; +export { Deployments } from './deployments.ts'; +export { + ArtifactNotFoundError, + ConfigError, + DeployError, + DeployTxFailedError, + IndexerUnreachableError, + ProofServerUnreachableError, + UnfundedWalletError, + WalletError, +} from './errors.ts'; +export type { ArgsSource } from './loaders/args.ts'; +export { ConstructorArgs } from './loaders/args.ts'; +export type { LoadArtifactOptions } from './loaders/artifact.ts'; +export { Artifact } from './loaders/artifact.ts'; +export { InitialPrivateState } from './loaders/init-state.ts'; +export { SigningKey } from './loaders/signing-key.ts'; +export { ProofServer } from './providers/proof-server.ts'; +export type { + CompactContractClass, + ConstructorArgsOf, + RunDeployOptions, +} from './runDeploy.ts'; +export { constructorArgs, runDeploy } from './runDeploy.ts'; +export { WalletHandler } from './wallet/handler.ts'; +export type { MidnightKeystore } from './wallet/keystore.ts'; +export { Keystore } from './wallet/keystore.ts'; +export type { WalletSeed } from './wallet/seeds.ts'; +export { + classifySeed, + LOCAL_PREFUNDED_SEEDS, + localPrefundedSeed, +} from './wallet/seeds.ts'; diff --git a/packages/deployer/src/loaders/args.test.ts b/packages/deployer/src/loaders/args.test.ts new file mode 100644 index 0000000..5defb72 --- /dev/null +++ b/packages/deployer/src/loaders/args.test.ts @@ -0,0 +1,177 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import type { ContractConfig } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; +import { ConstructorArgs } from './args.ts'; + +function makeFakeArtifact(paramSig: string): string { + const dir = mkdtempSync(join(tmpdir(), 'args-artifact-')); + mkdirSync(join(dir, 'contract')); + writeFileSync( + join(dir, 'contract', 'index.d.ts'), + [ + "import type * as __compactRuntime from '@midnight-ntwrk/compact-runtime';", + 'export declare class Contract {', + ' initialState(context: __compactRuntime.ConstructorContext,', + ` ${paramSig}): __compactRuntime.ConstructorResult;`, + '}', + '', + ].join('\n'), + ); + return dir; +} + +const baseContract = (extra: Partial = {}): ContractConfig => + ({ + artifact: 'x', + signing_key_file: 'x.sk', + ...extra, + }) as ContractConfig; + +describe('ConstructorArgs', () => { + it('should return empty values when args is unset', async () => { + const args = await ConstructorArgs.load(baseContract(), '/tmp'); + expect(args.values).toEqual([]); + expect(args.source).toBe('empty'); + }); + + it('should pass inline arrays through', async () => { + const args = await ConstructorArgs.load( + baseContract({ args: ['MyToken', 'MTK', 18] }), + '/tmp', + ); + expect(args.values).toEqual(['MyToken', 'MTK', 18]); + expect(args.source).toBe('inline'); + }); + + it('should read a JSON file ref and revive bigints', async () => { + const dir = mkdtempSync(join(tmpdir(), 'args-test-')); + writeFileSync(join(dir, 'a.json'), '["x", "100n"]'); + const args = await ConstructorArgs.load( + baseContract({ args: { file: 'a.json' } }), + dir, + ); + expect(args.values).toEqual(['x', 100n]); + expect(args.source).toBe('file'); + }); + + it('should parse a --args override JSON string', async () => { + const args = await ConstructorArgs.load(baseContract(), '/tmp', '[1,2,3]'); + expect(args.values).toEqual([1, 2, 3]); + expect(args.source).toBe('cli'); + }); + + it('should reject a non-array --args override', async () => { + await expect( + ConstructorArgs.load(baseContract(), '/tmp', '{"x":1}'), + ).rejects.toThrow(ConfigError); + }); + + it('should resolve a { module, export } ref to an exported array', async () => { + const dir = mkdtempSync(join(tmpdir(), 'args-test-')); + writeFileSync(join(dir, 'm.mjs'), 'export const values = [1, "two", 3n];'); + const args = await ConstructorArgs.load( + baseContract({ args: { module: 'm.mjs', export: 'values' } }), + dir, + ); + expect(args.values).toEqual([1, 'two', 3n]); + expect(args.source).toBe('module'); + }); + + it('should reject a { module, export } ref whose export is not an array', async () => { + const dir = mkdtempSync(join(tmpdir(), 'args-test-')); + writeFileSync(join(dir, 'm.mjs'), 'export const notArr = { a: 1 };'); + await expect( + ConstructorArgs.load( + baseContract({ args: { module: 'm.mjs', export: 'notArr' } }), + dir, + ), + ).rejects.toThrow(/must be an array/); + }); + + it('should use programmatic apiArgs and win over every other source', async () => { + const dir = mkdtempSync(join(tmpdir(), 'args-test-')); + writeFileSync(join(dir, 'a.json'), '["from-file"]'); + const args = await ConstructorArgs.load( + baseContract({ args: { file: 'a.json' } }), + dir, + '["from-cli"]', + ['from-api', 42n, new Uint8Array([0xAB])], + ); + expect(args.values).toEqual(['from-api', 42n, new Uint8Array([0xAB])]); + expect(args.source).toBe('api'); + }); + + it('should accept an empty apiArgs array', async () => { + const args = await ConstructorArgs.load(baseContract(), '/tmp', undefined, []); + expect(args.values).toEqual([]); + expect(args.source).toBe('api'); + }); + + it('should reject a { file } ref containing malformed JSON', async () => { + const dir = mkdtempSync(join(tmpdir(), 'args-test-')); + writeFileSync(join(dir, 'bad.json'), 'not json'); + await expect( + ConstructorArgs.load( + baseContract({ args: { file: 'bad.json' } }), + dir, + ), + ).rejects.toThrow(/invalid JSON at/); + }); + + it('should reorder a named-object apiArgs to match the artifact constructor', async () => { + const artifactPath = makeFakeArtifact( + '_name_2: string, _decimals_2: bigint, _isMintable_0: boolean', + ); + const args = await ConstructorArgs.load( + baseContract(), + '/tmp', + undefined, + { _isMintable: true, _name: 'OZE', _decimals: 18n }, + artifactPath, + ); + expect(args.values).toEqual(['OZE', 18n, true]); + expect(args.source).toBe('api'); + }); + + it('should reject a named-object apiArgs missing a constructor parameter', async () => { + const artifactPath = makeFakeArtifact( + '_name_2: string, _decimals_2: bigint', + ); + await expect( + ConstructorArgs.load( + baseContract(), + '/tmp', + undefined, + { _name: 'OZE' }, + artifactPath, + ), + ).rejects.toThrow(/missing constructor parameter\(s\): _decimals/); + }); + + it('should reject a named-object apiArgs with unknown keys', async () => { + const artifactPath = makeFakeArtifact('_name_2: string'); + await expect( + ConstructorArgs.load( + baseContract(), + '/tmp', + undefined, + { _name: 'OZE', _bogus: 1 }, + artifactPath, + ), + ).rejects.toThrow(/unknown constructor parameter\(s\): _bogus/); + }); + + it('should reject a named-object apiArgs when artifactPath is missing', async () => { + await expect( + ConstructorArgs.load( + baseContract(), + '/tmp', + undefined, + { foo: 1 }, + ), + ).rejects.toThrow(/named-object args require the artifact path/); + }); +}); diff --git a/packages/deployer/src/loaders/args.ts b/packages/deployer/src/loaders/args.ts new file mode 100644 index 0000000..2276b2b --- /dev/null +++ b/packages/deployer/src/loaders/args.ts @@ -0,0 +1,109 @@ +import { type ContractConfig, isFileRef } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; +import { + loadConstructorParamNames, + reorderNamedArgs, +} from './constructor-meta.ts'; +import { LoaderContext } from './context.ts'; +import { RefResolver } from './ref-resolver.ts'; + +export type ArgsSource = + | 'cli' + | 'inline' + | 'file' + | 'module' + | 'api' + | 'empty'; + +/** Constructor args hydrated from CLI / TOML. `source` records the winning origin. */ +export class ConstructorArgs { + readonly values: readonly unknown[]; + readonly source: ArgsSource; + + private constructor(values: readonly unknown[], source: ArgsSource) { + this.values = values; + this.source = source; + } + + /** + * Precedence: programmatic `DeployerOptions.args` > `--args '[…]'` + * (JSON) > inline TOML array > `args = { file }` (JSON, `"123n"` + * revived as bigint) > `args = { module, export }` (value or + * zero-arg function). Empty result yields `source = 'empty'`. + * + * Programmatic args may be either a positional array or a named + * object. Named objects are reordered to match the artifact's + * constructor by parsing `/contract/index.d.ts` for + * the parameter order; `artifactPath` must be supplied when the + * caller may pass a named-object form. + */ + static async load( + contract: ContractConfig, + rootDir: string, + override?: string, + apiArgs?: readonly unknown[] | Record, + artifactPath?: string, + ): Promise { + if (apiArgs !== undefined) { + if (Array.isArray(apiArgs)) { + return new ConstructorArgs(apiArgs, 'api'); + } + if (artifactPath === undefined) { + throw new ConfigError( + 'named-object args require the artifact path; pass it via Deployer.prepare or use a positional array', + ); + } + const paramNames = loadConstructorParamNames(artifactPath); + const reordered = reorderNamedArgs( + apiArgs as Record, + paramNames, + ); + return new ConstructorArgs(reordered, 'api'); + } + if (override !== undefined) { + return new ConstructorArgs(parseJsonArray(override, '--args'), 'cli'); + } + const raw = contract.args; + if (raw === undefined) return new ConstructorArgs([], 'empty'); + if (Array.isArray(raw)) return new ConstructorArgs(raw, 'inline'); + + const resolver = new RefResolver( + new LoaderContext(rootDir), + 'args', + ); + const values = await resolver.resolve( + raw, + (text, path) => parseJsonArray(text, path), + (value, path, exp) => { + if (!Array.isArray(value)) { + throw new ConfigError( + `args: module ${path} export "${exp}" must be an array`, + ); + } + return value; + }, + ); + return new ConstructorArgs(values, isFileRef(raw) ? 'file' : 'module'); + } + + get length(): number { + return this.values.length; + } +} + +function parseJsonArray(text: string, label: string): unknown[] { + let parsed: unknown; + try { + parsed = JSON.parse(text, (_k, v) => + typeof v === 'string' && /^-?\d+n$/.test(v) ? BigInt(v.slice(0, -1)) : v, + ); + } catch (e) { + throw new ConfigError( + `args: invalid JSON at ${label}: ${(e as Error).message}`, + ); + } + if (!Array.isArray(parsed)) { + throw new ConfigError(`args at ${label} must be a JSON array`); + } + return parsed; +} diff --git a/packages/deployer/src/loaders/artifact.test.ts b/packages/deployer/src/loaders/artifact.test.ts new file mode 100644 index 0000000..a595f3d --- /dev/null +++ b/packages/deployer/src/loaders/artifact.test.ts @@ -0,0 +1,306 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ArtifactNotFoundError, ConfigError } from '../errors.ts'; + +vi.mock('@midnight-ntwrk/compact-js', () => ({ + CompiledContract: { + make: vi.fn((name: string, Ctor: unknown) => ({ name, Ctor })), + withWitnesses: vi.fn((base: unknown, w: unknown) => ({ ...(base as object), w })), + withVacantWitnesses: vi.fn((base: unknown) => ({ ...(base as object), vacant: true })), + withCompiledFileAssets: vi.fn((c: unknown, dir: string) => ({ + ...(c as object), + contractDir: dir, + })), + }, +})); + +const { Artifact } = await import('./artifact.ts'); + +function makeArtifactDir( + root: string, + name: string, + opts: { + contractEntry?: 'cjs' | 'js' | 'top-level-cjs' | 'top-level-js' | 'none'; + keys?: boolean; + zkir?: boolean; + circuits?: string[]; + contractExport?: 'named' | 'default' | 'none'; + } = {}, +): string { + const { + contractEntry = 'cjs', + keys = true, + zkir = true, + circuits = ['inc', 'dec'], + contractExport = 'named', + } = opts; + + const dir = join(root, name); + mkdirSync(dir, { recursive: true }); + + if (contractEntry !== 'none') { + const isTopLevel = contractEntry.startsWith('top-level'); + const ext = contractEntry.endsWith('cjs') ? 'cjs' : 'js'; + const subDir = isTopLevel ? dir : join(dir, 'contract'); + mkdirSync(subDir, { recursive: true }); + + let body = ''; + if (contractExport === 'named') { + body = 'module.exports.Contract = function Counter() {};'; + } else if (contractExport === 'default') { + body = 'module.exports.default = { Contract: function Counter() {} };'; + } else { + body = 'module.exports.somethingElse = 1;'; + } + writeFileSync(join(subDir, `index.${ext}`), body); + } + + if (keys) mkdirSync(join(dir, 'keys'), { recursive: true }); + + if (zkir) { + mkdirSync(join(dir, 'zkir'), { recursive: true }); + for (const c of circuits) { + writeFileSync(join(dir, 'zkir', `${c}.bzkir`), ''); + } + } + + return dir; +} + +describe('Artifact.load — path resolution', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-test-')); + }); + + it('should resolve a relative artifact under rootDir directly', async () => { + makeArtifactDir(root, 'Counter'); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'Counter', + contractName: 'Counter', + }); + expect(art.artifactPath).toBe(join(root, 'Counter')); + }); + + it('should fall back to artifactsDir when the direct path is missing', async () => { + const artifactsRel = 'build/out'; + makeArtifactDir(join(root, artifactsRel), 'Counter'); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: artifactsRel, + artifact: 'Counter', + contractName: 'Counter', + }); + expect(art.artifactPath).toBe(join(root, artifactsRel, 'Counter')); + }); + + it('should treat an absolute artifact path as-is', async () => { + const abs = makeArtifactDir(root, 'AbsCounter'); + const art = await Artifact.load({ + rootDir: '/elsewhere', + artifactsDir: 'unused/', + artifact: abs, + contractName: 'AbsCounter', + }); + expect(art.artifactPath).toBe(abs); + }); +}); + +describe('Artifact.load — error paths', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-err-')); + }); + + it('should throw ArtifactNotFoundError when the directory is missing', async () => { + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'NopeMissing', + contractName: 'NopeMissing', + }), + ).rejects.toThrow(ArtifactNotFoundError); + }); + + it('should throw ArtifactNotFoundError when contract/index entry is missing', async () => { + makeArtifactDir(root, 'NoEntry', { contractEntry: 'none' }); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'NoEntry', + contractName: 'NoEntry', + }), + ).rejects.toThrow(/no contract\/index/); + }); + + it('should throw ArtifactNotFoundError when keys/ is missing', async () => { + makeArtifactDir(root, 'NoKeys', { keys: false }); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'NoKeys', + contractName: 'NoKeys', + }), + ).rejects.toThrow(/missing keys\/ or zkir\//); + }); + + it('should throw ArtifactNotFoundError when zkir/ is missing', async () => { + makeArtifactDir(root, 'NoZkir', { zkir: false }); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'NoZkir', + contractName: 'NoZkir', + }), + ).rejects.toThrow(/missing keys\/ or zkir\//); + }); + + it('should throw ConfigError when index does not export a Contract class', async () => { + makeArtifactDir(root, 'NoExport', { contractExport: 'none' }); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'NoExport', + contractName: 'NoExport', + }), + ).rejects.toThrow(ConfigError); + }); + + it('should throw ConfigError when witnesses ref is a file ref (functions only via module)', async () => { + makeArtifactDir(root, 'WitnessFile'); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'WitnessFile', + contractName: 'WitnessFile', + witnesses: { file: 'w.json' }, + }), + ).rejects.toThrow(/witnesses.*module.*export.*JSON file refs are not supported/); + }); + + it('should throw ConfigError when witnesses module export does not resolve to an object', async () => { + makeArtifactDir(root, 'WitnessNonObject'); + writeFileSync( + join(root, 'w.mjs'), + 'export const witnesses = "not-an-object";', + ); + await expect( + Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'WitnessNonObject', + contractName: 'WitnessNonObject', + witnesses: { module: 'w.mjs', export: 'witnesses' }, + }), + ).rejects.toThrow(/must resolve to an object/); + }); +}); + +describe('Artifact.load — witnesses module ref', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-wit-')); + }); + + it('should accept a { module, export } witnesses ref that resolves to an object', async () => { + makeArtifactDir(root, 'WithWitnesses'); + writeFileSync( + join(root, 'w.mjs'), + 'export const witnesses = { add: () => 1 };', + ); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'src/artifacts', + artifact: 'WithWitnesses', + contractName: 'WithWitnesses', + witnesses: { module: 'w.mjs', export: 'witnesses' }, + }); + expect(art.artifactPath).toBe(join(root, 'WithWitnesses')); + }); +}); + +describe('Artifact.load — entry-file fallbacks', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-entry-')); + }); + + it('should accept contract/index.js when contract/index.cjs is missing', async () => { + makeArtifactDir(root, 'CounterJs', { contractEntry: 'js' }); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'CounterJs', + contractName: 'CounterJs', + }); + expect(art.artifactPath).toBe(join(root, 'CounterJs')); + }); + + it('should fall back to top-level index.cjs when contract/ has no entry', async () => { + makeArtifactDir(root, 'TopLevel', { contractEntry: 'top-level-cjs' }); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'TopLevel', + contractName: 'TopLevel', + }); + expect(art.artifactPath).toBe(join(root, 'TopLevel')); + }); +}); + +describe('Artifact.load — circuit collection', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-circuits-')); + }); + + it('should collect and sort circuit names from .bzkir files', async () => { + makeArtifactDir(root, 'C', { circuits: ['zeta', 'alpha', 'mu'] }); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'C', + contractName: 'C', + }); + expect(art.circuitNames).toEqual(['alpha', 'mu', 'zeta']); + }); + + it('should produce an empty circuit list when zkir/ has no .bzkir files', async () => { + makeArtifactDir(root, 'Empty', { circuits: [] }); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'Empty', + contractName: 'Empty', + }); + expect(art.circuitNames).toEqual([]); + }); +}); + +describe('Artifact.load — default export Contract', () => { + let root: string; + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'artifact-default-')); + }); + + it('should pick Contract from module.default when not on the top namespace', async () => { + makeArtifactDir(root, 'Default', { contractExport: 'default' }); + const art = await Artifact.load({ + rootDir: root, + artifactsDir: 'unused/', + artifact: 'Default', + contractName: 'Default', + }); + expect(art.artifactPath).toBe(join(root, 'Default')); + }); +}); diff --git a/packages/deployer/src/loaders/artifact.ts b/packages/deployer/src/loaders/artifact.ts new file mode 100644 index 0000000..68d471c --- /dev/null +++ b/packages/deployer/src/loaders/artifact.ts @@ -0,0 +1,191 @@ +import { existsSync, readdirSync } from 'node:fs'; +import { isAbsolute, resolve } from 'node:path'; +import { CompiledContract, type Contract } from '@midnight-ntwrk/compact-js'; +import type { Types } from 'effect'; +import { + type FileOrModuleRef, + isFileRef, + isModuleRef, +} from '../config/schema.ts'; +import { ArtifactNotFoundError, ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; + +/** + * A compactc artifact bundle on disk: + * /contract/index.{cjs,js} — Contract class + * /keys/.{prover,verifier} + * /zkir/.bzkir + * Witnesses live outside the bundle, referenced via `[contracts.X].witnesses`. + */ + +type AnyContract = Contract.Any; +type AnyWitnesses = Contract.Witnesses; +type AnyCompiledContract = CompiledContract.CompiledContract< + AnyContract, + unknown, + never +>; + +export interface LoadArtifactOptions { + rootDir: string; + artifactsDir: string; + artifact: string; + contractName: string; + witnesses?: FileOrModuleRef; +} + +export class Artifact { + readonly compiledContract: AnyCompiledContract; + readonly artifactPath: string; + readonly zkConfigPath: string; + readonly circuitNames: readonly string[]; + + private constructor(input: { + compiledContract: AnyCompiledContract; + artifactPath: string; + zkConfigPath: string; + circuitNames: readonly string[]; + }) { + this.compiledContract = input.compiledContract; + this.artifactPath = input.artifactPath; + this.zkConfigPath = input.zkConfigPath; + this.circuitNames = input.circuitNames; + } + + /** Resolve, validate, and import the bundle. Throws {@link ArtifactNotFoundError} on missing dir/entry/keys/zkir. */ + static async load(opts: LoadArtifactOptions): Promise { + const { rootDir, artifactsDir, artifact, contractName, witnesses } = opts; + const ctx = new LoaderContext(rootDir); + const artifactPath = resolveUnderRoot(rootDir, artifact, artifactsDir); + + if (!existsSync(artifactPath)) { + throw new ArtifactNotFoundError(artifactPath); + } + + const contractDir = resolve(artifactPath, 'contract'); + const entry = findEntry(contractDir, artifactPath); + if (!entry) { + throw new ArtifactNotFoundError( + `${artifactPath} (no contract/index.{cjs,js} or index.{cjs,js} found)`, + ); + } + + const keysDir = resolve(artifactPath, 'keys'); + const zkirDir = resolve(artifactPath, 'zkir'); + if (!existsSync(keysDir) || !existsSync(zkirDir)) { + throw new ArtifactNotFoundError( + `${artifactPath} (missing keys/ or zkir/ subdirectory)`, + ); + } + + const circuitNames = collectCircuitNames(zkirDir); + const Ctor = await importContractCtor(ctx, entry); + const witnessImpls = witnesses + ? await importWitnesses(ctx, witnesses) + : undefined; + + const compiledContract = buildCompiledContract({ + contractName, + Ctor, + witnessImpls, + contractDir, + }); + + return new Artifact({ + compiledContract, + artifactPath, + zkConfigPath: artifactPath, + circuitNames, + }); + } +} + +async function importContractCtor( + ctx: LoaderContext, + entry: string, +): Promise> { + const { mod, path } = await ctx.importModule(entry, 'artifact'); + const m = mod as ArtifactModule; + const Ctor = m.Contract ?? m.default?.Contract; + if (!Ctor) { + throw new ConfigError( + `Artifact at ${path} does not export a \`Contract\` class (got keys: ${Object.keys(m).join(', ')})`, + ); + } + return Ctor; +} + +async function importWitnesses( + ctx: LoaderContext, + ref: FileOrModuleRef, +): Promise { + if (isFileRef(ref)) { + throw new ConfigError( + 'witnesses must be a { module, export } reference; JSON file refs are not supported (witnesses are functions)', + ); + } + if (!isModuleRef(ref)) { + throw new ConfigError('witnesses must be { module, export }'); + } + const { mod, path } = await ctx.importModule(ref.module, 'witnesses'); + const exported = mod[ref.export]; + const resolved = + typeof exported === 'function' + ? await (exported as () => unknown)() + : exported; + if (typeof resolved !== 'object' || resolved === null) { + throw new ConfigError( + `witnesses: module ${path} export "${ref.export}" must resolve to an object`, + ); + } + return resolved as AnyWitnesses; +} + +function buildCompiledContract(input: { + contractName: string; + Ctor: Types.Ctor; + witnessImpls: AnyWitnesses | undefined; + contractDir: string; +}): AnyCompiledContract { + const base = CompiledContract.make(input.contractName, input.Ctor); + const withWit = input.witnessImpls + ? CompiledContract.withWitnesses(base, input.witnessImpls) + : CompiledContract.withVacantWitnesses(base); + return CompiledContract.withCompiledFileAssets(withWit, input.contractDir); +} + +interface ArtifactModule { + Contract?: Types.Ctor; + default?: { Contract?: Types.Ctor }; +} + +function resolveUnderRoot( + rootDir: string, + artifact: string, + artifactsDir: string, +): string { + if (isAbsolute(artifact)) return artifact; + const direct = resolve(rootDir, artifact); + if (existsSync(direct)) return direct; + return resolve(rootDir, artifactsDir, artifact); +} + +function findEntry( + contractDir: string, + artifactDir: string, +): string | undefined { + const candidates = [ + resolve(contractDir, 'index.cjs'), + resolve(contractDir, 'index.js'), + resolve(artifactDir, 'index.cjs'), + resolve(artifactDir, 'index.js'), + ]; + return candidates.find(existsSync); +} + +function collectCircuitNames(zkirDir: string): string[] { + return readdirSync(zkirDir) + .filter((f) => f.endsWith('.bzkir')) + .map((f) => f.slice(0, -'.bzkir'.length)) + .sort(); +} diff --git a/packages/deployer/src/loaders/constructor-meta.test.ts b/packages/deployer/src/loaders/constructor-meta.test.ts new file mode 100644 index 0000000..ed312aa --- /dev/null +++ b/packages/deployer/src/loaders/constructor-meta.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { + parseConstructorParamNames, + reorderNamedArgs, +} from './constructor-meta.ts'; + +const dts = (params: string) => + [ + "import type * as __compactRuntime from '@midnight-ntwrk/compact-runtime';", + 'export declare class Contract {', + ' initialState(context: __compactRuntime.ConstructorContext,', + ` ${params}): __compactRuntime.ConstructorResult;`, + '}', + ].join('\n'); + +describe('parseConstructorParamNames', () => { + it('strips the trailing SSA suffix the compiler appends', () => { + expect( + parseConstructorParamNames( + dts('_name_2: string, _symbol_2: string, init_0: boolean'), + ), + ).toEqual(['_name', '_symbol', 'init']); + }); + + it('handles generics with commas inside angle brackets', () => { + expect( + parseConstructorParamNames( + dts('owner_0: Either, isInit_0: boolean'), + ), + ).toEqual(['owner', 'isInit']); + }); + + it('handles Vector / array types', () => { + expect( + parseConstructorParamNames( + dts( + 'salt_0: Uint8Array, commitments_0: Uint8Array[], thresh_0: bigint', + ), + ), + ).toEqual(['salt', 'commitments', 'thresh']); + }); + + it('keeps the SSA suffix when stripping would cause a name collision', () => { + expect( + parseConstructorParamNames( + dts('foo_0: bigint, foo_1: bigint'), + ), + ).toEqual(['foo_0', 'foo_1']); + }); + + it('returns [] for a no-arg constructor', () => { + expect(parseConstructorParamNames(dts(''))).toEqual([]); + }); + + it('returns [] when initialState is not present', () => { + expect(parseConstructorParamNames('// nothing here')).toEqual([]); + }); +}); + +describe('reorderNamedArgs', () => { + it('maps a named record to the positional tuple', () => { + expect( + reorderNamedArgs({ b: 2, a: 1, c: 3 }, ['a', 'b', 'c']), + ).toEqual([1, 2, 3]); + }); + + it('rejects when a required name is missing', () => { + expect(() => reorderNamedArgs({ a: 1 }, ['a', 'b'])).toThrow(ConfigError); + }); + + it('rejects when an extra unknown name is present', () => { + expect(() => reorderNamedArgs({ a: 1, x: 9 }, ['a'])).toThrow( + /unknown constructor parameter\(s\): x/, + ); + }); +}); diff --git a/packages/deployer/src/loaders/constructor-meta.ts b/packages/deployer/src/loaders/constructor-meta.ts new file mode 100644 index 0000000..484fff7 --- /dev/null +++ b/packages/deployer/src/loaders/constructor-meta.ts @@ -0,0 +1,109 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { ArtifactNotFoundError, ConfigError } from '../errors.ts'; + +/** + * Parses an artifact's `contract/index.d.ts` and returns the ordered + * constructor parameter names (with the trailing `_` SSA + * suffix the Compact compiler appends stripped — `_name_2` → `_name`). + * Used to reorder a named-object `args: { ... }` into the positional + * tuple the contract's `initialState` expects. + */ +export function loadConstructorParamNames(artifactPath: string): string[] { + const dtsPath = join(artifactPath, 'contract', 'index.d.ts'); + if (!existsSync(dtsPath)) { + throw new ArtifactNotFoundError( + `${artifactPath} (no contract/index.d.ts — cannot reorder named args)`, + ); + } + const source = readFileSync(dtsPath, 'utf8'); + const names = parseConstructorParamNames(source); + if (names.length === 0) { + throw new ConfigError( + `Contract ${artifactPath} has a no-arg constructor; named args object should be empty`, + ); + } + return names; +} + +/** Reorders a named-object args record into a positional tuple. */ +export function reorderNamedArgs( + named: Record, + paramNames: readonly string[], +): unknown[] { + const missing = paramNames.filter((n) => !(n in named)); + if (missing.length > 0) { + throw new ConfigError( + `args object is missing constructor parameter(s): ${missing.join(', ')}`, + ); + } + const extra = Object.keys(named).filter((k) => !paramNames.includes(k)); + if (extra.length > 0) { + throw new ConfigError( + `args object has unknown constructor parameter(s): ${extra.join(', ')}. Expected: ${paramNames.join(', ')}`, + ); + } + return paramNames.map((n) => named[n]); +} + +/** + * Pulls the constructor parameter names out of a Compact artifact's + * `index.d.ts`. The trailing `_` SSA suffix is stripped; if + * stripping causes a collision in the same constructor, the original + * names are kept. + */ +export function parseConstructorParamNames(dtsSource: string): string[] { + const block = sliceInitialStateParams(dtsSource); + if (block === null) return []; + const params = splitTopLevelParams(block).slice(1); // drop `context: ...` + if (params.length === 0) return []; + const names = params.map(extractParamName); + const stripped = names.map(stripSsaSuffix); + return new Set(stripped).size === stripped.length ? stripped : names; +} + +function sliceInitialStateParams(source: string): string | null { + const head = source.indexOf('initialState('); + if (head === -1) return null; + const open = head + 'initialState('.length; + let depth = 1; + for (let i = open; i < source.length; i++) { + const ch = source[i]; + if (ch === '(') depth++; + else if (ch === ')') { + depth--; + if (depth === 0) return source.slice(open, i); + } + } + return null; +} + +function splitTopLevelParams(block: string): string[] { + const out: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < block.length; i++) { + const ch = block[i]; + if (ch === '<' || ch === '(' || ch === '[' || ch === '{') depth++; + else if (ch === '>' || ch === ')' || ch === ']' || ch === '}') depth--; + else if (ch === ',' && depth === 0) { + out.push(block.slice(start, i).trim()); + start = i + 1; + } + } + const tail = block.slice(start).trim(); + if (tail.length > 0) out.push(tail); + return out; +} + +function extractParamName(raw: string): string { + const colon = raw.indexOf(':'); + if (colon === -1) { + throw new ConfigError(`Cannot parse constructor param: "${raw}"`); + } + return raw.slice(0, colon).trim(); +} + +function stripSsaSuffix(name: string): string { + return name.replace(/_\d+$/, ''); +} diff --git a/packages/deployer/src/loaders/context.test.ts b/packages/deployer/src/loaders/context.test.ts new file mode 100644 index 0000000..e20e075 --- /dev/null +++ b/packages/deployer/src/loaders/context.test.ts @@ -0,0 +1,78 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { isAbsolute, join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; + +describe('LoaderContext.abs', () => { + it('should leave absolute paths unchanged', () => { + const ctx = new LoaderContext('/some/root'); + expect(ctx.abs('/abs/path/file.json')).toBe('/abs/path/file.json'); + }); + + it('should resolve relative paths against rootDir', () => { + const ctx = new LoaderContext('/some/root'); + const resolved = ctx.abs('inner/file.json'); + expect(isAbsolute(resolved)).toBe(true); + expect(resolved).toBe('/some/root/inner/file.json'); + }); +}); + +describe('LoaderContext.readText', () => { + it('should read a file and return text + absolute path', async () => { + const dir = mkdtempSync(join(tmpdir(), 'loader-ctx-')); + writeFileSync(join(dir, 'hello.txt'), 'world'); + const ctx = new LoaderContext(dir); + + const { text, path } = await ctx.readText('hello.txt', 'label'); + expect(text).toBe('world'); + expect(path).toBe(join(dir, 'hello.txt')); + }); + + it('should wrap ENOENT in ConfigError with the label and path', async () => { + const ctx = new LoaderContext('/tmp'); + await expect( + ctx.readText('does-not-exist-zzz.txt', 'my-label'), + ).rejects.toThrow(ConfigError); + await expect( + ctx.readText('does-not-exist-zzz.txt', 'my-label'), + ).rejects.toThrow(/my-label.*failed to read/); + }); +}); + +describe('LoaderContext.importModule', () => { + it('should dynamic-import a module from a relative path', async () => { + const dir = mkdtempSync(join(tmpdir(), 'loader-ctx-imp-')); + writeFileSync( + join(dir, 'sample.mjs'), + 'export const value = 42; export default { value: 7 };', + ); + const ctx = new LoaderContext(dir); + + const { mod, path } = await ctx.importModule('sample.mjs', 'label'); + expect(mod.value).toBe(42); + expect(path).toBe(join(dir, 'sample.mjs')); + }); + + it('should wrap import failures in ConfigError', async () => { + const ctx = new LoaderContext('/tmp'); + await expect( + ctx.importModule('nope-not-there.mjs', 'mods'), + ).rejects.toThrow(ConfigError); + await expect( + ctx.importModule('nope-not-there.mjs', 'mods'), + ).rejects.toThrow(/mods.*failed to import/); + }); + + it('should accept absolute paths unchanged', async () => { + const dir = mkdtempSync(join(tmpdir(), 'loader-ctx-abs-')); + const abs = join(dir, 'abs.mjs'); + writeFileSync(abs, 'export const ok = true;'); + const ctx = new LoaderContext('/unused/root'); + + const { mod, path } = await ctx.importModule(abs, 'l'); + expect(mod.ok).toBe(true); + expect(path).toBe(abs); + }); +}); diff --git a/packages/deployer/src/loaders/context.ts b/packages/deployer/src/loaders/context.ts new file mode 100644 index 0000000..0ceabf4 --- /dev/null +++ b/packages/deployer/src/loaders/context.ts @@ -0,0 +1,50 @@ +import { readFile } from 'node:fs/promises'; +import { isAbsolute, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { ConfigError } from '../errors.ts'; + +/** Per-call I/O bundle for loaders. Centralises the `ConfigError`-wrapping boilerplate. */ +export class LoaderContext { + readonly rootDir: string; + + constructor(rootDir: string) { + this.rootDir = rootDir; + } + + abs(p: string): string { + return isAbsolute(p) ? p : resolve(this.rootDir, p); + } + + async readText( + p: string, + label: string, + ): Promise<{ text: string; path: string }> { + const path = this.abs(p); + try { + const text = await readFile(path, 'utf8'); + return { text, path }; + } catch (e) { + throw new ConfigError( + `${label}: failed to read ${path}: ${(e as Error).message}`, + ); + } + } + + async importModule( + p: string, + label: string, + ): Promise<{ mod: Record; path: string }> { + const path = this.abs(p); + try { + const mod = (await import(pathToFileURL(path).href)) as Record< + string, + unknown + >; + return { mod, path }; + } catch (e) { + throw new ConfigError( + `${label}: failed to import ${path}: ${(e as Error).message}`, + ); + } + } +} diff --git a/packages/deployer/src/loaders/contract-resolve.test.ts b/packages/deployer/src/loaders/contract-resolve.test.ts new file mode 100644 index 0000000..ace8f17 --- /dev/null +++ b/packages/deployer/src/loaders/contract-resolve.test.ts @@ -0,0 +1,177 @@ +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { CompactConfig } from '../config/compact-config.ts'; +import { ConfigError } from '../errors.ts'; +import { resolveContractName } from './contract-resolve.ts'; + +const rootDirs: string[] = []; + +afterEach(() => { + // Module cache holds the imported artifacts. Tests use unique + // tmpdirs so no cleanup is needed. + rootDirs.length = 0; +}); + +function makeProject(entries: Record): string { + const root = mkdtempSync(join(tmpdir(), 'contract-resolve-')); + rootDirs.push(root); + mkdirSync(join(root, 'artifacts')); + const contractsToml = Object.keys(entries) + .map( + (name) => ` +[contracts.${name}] +artifact = "${name}" +signing_key_file = "${name}.sk" +`, + ) + .join('\n'); + writeFileSync( + join(root, 'compact.toml'), + ` +[profile] +default_network = "local" +artifacts_dir = "artifacts" + +[networks.local] +network_id = "local" +indexer = "http://localhost:8088/api/v1/graphql" +indexer_ws = "ws://localhost:8088/api/v1/graphql/ws" +node = "http://localhost:9944" +node_ws = "ws://localhost:9944" +proof_server = "http://localhost:6300" + +${contractsToml} +`, + ); + for (const [name, { Contract }] of Object.entries(entries)) { + const contractDir = join(root, 'artifacts', name, 'contract'); + mkdirSync(contractDir, { recursive: true }); + // The exported class instance is shared via the module cache, so + // the loaded module's `Contract` === the test's reference. + const g = globalThis as unknown as Record; + const key = `__test_contract_${name}_${Date.now()}_${Math.random()}`; + g[key] = Contract; + writeFileSync( + join(contractDir, 'index.js'), + `export const Contract = globalThis['${key}'];\n`, + ); + } + return root; +} + +describe('resolveContractName', () => { + it('returns the entry name whose artifact exports the same Contract class', async () => { + class TokenContract { + initialState() {} + } + class OtherContract { + initialState() {} + } + const root = makeProject({ + TokenExample: { Contract: TokenContract }, + OtherExample: { Contract: OtherContract }, + }); + const config = await CompactConfig.load(join(root, 'compact.toml')); + expect(await resolveContractName(TokenContract, config, root)).toBe( + 'TokenExample', + ); + }); + + it('throws when no entry matches the Contract class', async () => { + class A { + initialState() {} + } + class B { + initialState() {} + } + const root = makeProject({ A: { Contract: A } }); + const config = await CompactConfig.load(join(root, 'compact.toml')); + await expect(resolveContractName(B, config, root)).rejects.toThrow( + /did not match any \[contracts\.X\] entry/, + ); + }); + + it('throws when two entries match the same Contract class (ambiguous)', async () => { + class Shared { + initialState() {} + } + const root = makeProject({ + A: { Contract: Shared }, + B: { Contract: Shared }, + }); + const config = await CompactConfig.load(join(root, 'compact.toml')); + await expect(resolveContractName(Shared, config, root)).rejects.toThrow( + ConfigError, + ); + await expect(resolveContractName(Shared, config, root)).rejects.toThrow( + /Ambiguous Contract/, + ); + }); + + it('lists skipped entries when an artifact dir has no contract module', async () => { + class Target { + initialState() {} + } + const root = makeProject({ A: { Contract: Target } }); + // Inject a second entry whose artifact dir is empty (no + // contract/index.{cjs,js} files). + writeFileSync( + join(root, 'compact.toml'), + readFileSync(join(root, 'compact.toml'), 'utf8') + ` +[contracts.Empty] +artifact = "Empty" +signing_key_file = "Empty.sk" +`, + ); + mkdirSync(join(root, 'artifacts', 'Empty')); + class Other { + initialState() {} + } + const config = await CompactConfig.load(join(root, 'compact.toml')); + await expect(resolveContractName(Other, config, root)).rejects.toThrow( + /Skipped: Empty \(no contract\/index/, + ); + }); + + it('skips entries whose artifact module import throws', async () => { + class Target { + initialState() {} + } + const root = makeProject({ Good: { Contract: Target } }); + writeFileSync( + join(root, 'compact.toml'), + readFileSync(join(root, 'compact.toml'), 'utf8') + ` +[contracts.Broken] +artifact = "Broken" +signing_key_file = "Broken.sk" +`, + ); + mkdirSync(join(root, 'artifacts', 'Broken', 'contract'), { recursive: true }); + writeFileSync( + join(root, 'artifacts', 'Broken', 'contract', 'index.js'), + 'throw new Error("boom on import");\n', + ); + const config = await CompactConfig.load(join(root, 'compact.toml')); + // Target still matches "Good" (good entry has the right Contract); + // the broken entry is just skipped silently in the match path. + expect(await resolveContractName(Target, config, root)).toBe('Good'); + }); + + it('honours an absolute `artifact` path in compact.toml', async () => { + class Target { + initialState() {} + } + const root = makeProject({ Token: { Contract: Target } }); + // Rewrite the [contracts.Token] entry to use an absolute artifact path. + const absPath = join(root, 'artifacts', 'Token'); + const toml = readFileSync(join(root, 'compact.toml'), 'utf8').replace( + 'artifact = "Token"', + `artifact = "${absPath}"`, + ); + writeFileSync(join(root, 'compact.toml'), toml); + const config = await CompactConfig.load(join(root, 'compact.toml')); + expect(await resolveContractName(Target, config, root)).toBe('Token'); + }); +}); diff --git a/packages/deployer/src/loaders/contract-resolve.ts b/packages/deployer/src/loaders/contract-resolve.ts new file mode 100644 index 0000000..091b9f4 --- /dev/null +++ b/packages/deployer/src/loaders/contract-resolve.ts @@ -0,0 +1,92 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { CompactConfig } from '../config/compact-config.ts'; +import { ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; + +/** + * Walks `compact.toml`'s `[contracts.X]` entries and returns the name + * whose compiled `Contract` class is identity-equal to the one + * imported by the caller's deploy script. Used by the curried + * `runDeploy(Contract)(...)` form so the deploy script names the + * contract once. + * + * Throws when: + * - no entry resolves to the same Contract class (the script likely + * imported from a path that isn't referenced by `compact.toml`) + * - two entries match (ambiguous — likely two TOML entries pointing + * at the same artifact directory). + */ +export async function resolveContractName( + Contract: unknown, + config: CompactConfig, + rootDir: string, +): Promise { + const ctx = new LoaderContext(rootDir); + const matches: string[] = []; + const tried: Array<{ name: string; reason: string }> = []; + + for (const name of config.listContracts()) { + const cfg = config.contract(name); + const entry = findContractEntry(rootDir, config.artifactsDir, cfg.artifact); + if (!entry) { + tried.push({ + name, + reason: `no contract/index.{cjs,js} or index.{cjs,js} under ${cfg.artifact}`, + }); + continue; + } + try { + const { mod } = await ctx.importModule(entry, 'artifact'); + const Loaded = + (mod as { Contract?: unknown }).Contract ?? + (mod as { default?: { Contract?: unknown } }).default?.Contract; + if (Loaded === Contract) { + matches.push(name); + } + } catch (e) { + tried.push({ name, reason: (e as Error).message }); + } + } + + if (matches.length === 1) return matches[0] as string; + if (matches.length > 1) { + throw new ConfigError( + `Ambiguous Contract: matches ${matches.length} entries in compact.toml (${matches.join(', ')}). Use the string form: runDeploy({ contract: 'X' }).`, + ); + } + const tail = + tried.length > 0 + ? `\nSkipped: ${tried.map((t) => `${t.name} (${t.reason})`).join('; ')}` + : ''; + throw new ConfigError( + `Contract class did not match any [contracts.X] entry in compact.toml. Make sure the import path resolves to the same artifact directory referenced by the TOML.${tail}`, + ); +} + +function findContractEntry( + rootDir: string, + artifactsDir: string, + artifact: string, +): string | undefined { + const artifactPath = resolveUnderRoot(rootDir, artifact, artifactsDir); + const contractDir = resolve(artifactPath, 'contract'); + const candidates = [ + resolve(contractDir, 'index.cjs'), + resolve(contractDir, 'index.js'), + resolve(artifactPath, 'index.cjs'), + resolve(artifactPath, 'index.js'), + ]; + return candidates.find(existsSync); +} + +function resolveUnderRoot( + rootDir: string, + artifact: string, + artifactsDir: string, +): string { + if (artifact.startsWith('/')) return artifact; + const direct = resolve(rootDir, artifact); + if (existsSync(direct)) return direct; + return resolve(rootDir, artifactsDir, artifact); +} diff --git a/packages/deployer/src/loaders/init-state.test.ts b/packages/deployer/src/loaders/init-state.test.ts new file mode 100644 index 0000000..b8abd6d --- /dev/null +++ b/packages/deployer/src/loaders/init-state.test.ts @@ -0,0 +1,54 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { InitialPrivateState } from './init-state.ts'; + +describe('InitialPrivateState', () => { + it('should return undefined when ref is absent', async () => { + expect(await InitialPrivateState.load(undefined, '/tmp')).toBeUndefined(); + }); + + it('should parse a { file } JSON ref with bigint revival', async () => { + const dir = mkdtempSync(join(tmpdir(), 'initstate-test-')); + writeFileSync(join(dir, 's.json'), '{"counter":"100n","name":"x"}'); + const state = await InitialPrivateState.load({ file: 's.json' }, dir); + expect(state?.value).toEqual({ counter: 100n, name: 'x' }); + }); + + it('should throw ConfigError for missing files', async () => { + await expect( + InitialPrivateState.load({ file: 'does-not-exist.json' }, '/tmp'), + ).rejects.toThrow(ConfigError); + }); + + it('should throw ConfigError for invalid JSON', async () => { + const dir = mkdtempSync(join(tmpdir(), 'initstate-test-')); + writeFileSync(join(dir, 'bad.json'), 'not json'); + await expect( + InitialPrivateState.load({ file: 'bad.json' }, dir), + ).rejects.toThrow(ConfigError); + }); + + it('should resolve a { module, export } ref to its exported value', async () => { + const dir = mkdtempSync(join(tmpdir(), 'initstate-test-')); + writeFileSync( + join(dir, 'm.mjs'), + 'export const state = { counter: 5n, name: "from-mod" };', + ); + const state = await InitialPrivateState.load( + { module: 'm.mjs', export: 'state' }, + dir, + ); + expect(state?.value).toEqual({ counter: 5n, name: 'from-mod' }); + }); + + it('should throw ConfigError when the module export is missing', async () => { + const dir = mkdtempSync(join(tmpdir(), 'initstate-test-')); + writeFileSync(join(dir, 'm.mjs'), 'export const present = 1;'); + await expect( + InitialPrivateState.load({ module: 'm.mjs', export: 'missing' }, dir), + ).rejects.toThrow(/has no export "missing"/); + }); +}); diff --git a/packages/deployer/src/loaders/init-state.ts b/packages/deployer/src/loaders/init-state.ts new file mode 100644 index 0000000..ec6d04f --- /dev/null +++ b/packages/deployer/src/loaders/init-state.ts @@ -0,0 +1,54 @@ +import type { FileOrModuleRef } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; +import { RefResolver } from './ref-resolver.ts'; + +/** Initial private state for the contract constructor. `load` returns `undefined` when omitted in TOML. */ +export class InitialPrivateState { + readonly value: unknown; + + private constructor(value: unknown) { + this.value = value; + } + + /** Source: `{ file }` (JSON with `"123n"` bigint strings) or `{ module, export }` (value or zero-arg function). */ + static async load( + ref: FileOrModuleRef | undefined, + rootDir: string, + ): Promise { + if (!ref) return undefined; + + const resolver = new RefResolver( + new LoaderContext(rootDir), + 'init_private_state', + ); + const value = await resolver.resolve( + ref, + (text, path) => { + try { + return JSON.parse(text, bigintReviver); + } catch (e) { + throw new ConfigError( + `init_private_state: invalid JSON at ${path}: ${(e as Error).message}`, + ); + } + }, + (v, path, exp) => { + if (v === undefined) { + throw new ConfigError( + `init_private_state: module ${path} has no export "${exp}"`, + ); + } + return v; + }, + ); + return new InitialPrivateState(value); + } +} + +function bigintReviver(_key: string, value: unknown): unknown { + if (typeof value === 'string' && /^-?\d+n$/.test(value)) { + return BigInt(value.slice(0, -1)); + } + return value; +} diff --git a/packages/deployer/src/loaders/ref-resolver.test.ts b/packages/deployer/src/loaders/ref-resolver.test.ts new file mode 100644 index 0000000..ae19494 --- /dev/null +++ b/packages/deployer/src/loaders/ref-resolver.test.ts @@ -0,0 +1,107 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; +import { RefResolver } from './ref-resolver.ts'; + +const parseJsonNumber = (text: string): number => Number.parseInt(text, 10); +const expectNumber = (value: unknown): number => { + if (typeof value !== 'number') throw new ConfigError('not a number'); + return value; +}; + +describe('RefResolver.resolve — file branch', () => { + it('should read the file and run parseFile', async () => { + const dir = mkdtempSync(join(tmpdir(), 'refres-file-')); + writeFileSync(join(dir, 'n.txt'), '42'); + const r = new RefResolver(new LoaderContext(dir), 'args'); + + const out = await r.resolve( + { file: 'n.txt' }, + parseJsonNumber, + expectNumber, + ); + expect(out).toBe(42); + }); + + it('should propagate ConfigError from a missing file', async () => { + const r = new RefResolver(new LoaderContext('/tmp'), 'args'); + await expect( + r.resolve( + { file: 'does-not-exist-xx.txt' }, + parseJsonNumber, + expectNumber, + ), + ).rejects.toThrow(ConfigError); + }); +}); + +describe('RefResolver.resolve — module branch', () => { + it('should import a module and pick the named export', async () => { + const dir = mkdtempSync(join(tmpdir(), 'refres-mod-')); + writeFileSync( + join(dir, 'm.mjs'), + 'export const seven = 7; export default 99;', + ); + const r = new RefResolver(new LoaderContext(dir), 'args'); + + const out = await r.resolve( + { module: 'm.mjs', export: 'seven' }, + parseJsonNumber, + expectNumber, + ); + expect(out).toBe(7); + }); + + it('should call a function-shaped export and use its return value', async () => { + const dir = mkdtempSync(join(tmpdir(), 'refres-fn-')); + writeFileSync( + join(dir, 'm.mjs'), + 'export const factory = async () => 123;', + ); + const r = new RefResolver(new LoaderContext(dir), 'args'); + + const out = await r.resolve( + { module: 'm.mjs', export: 'factory' }, + parseJsonNumber, + expectNumber, + ); + expect(out).toBe(123); + }); + + it('should let validateExport throw to reject bad export shapes', async () => { + const dir = mkdtempSync(join(tmpdir(), 'refres-bad-')); + writeFileSync(join(dir, 'm.mjs'), 'export const value = "not a number";'); + const r = new RefResolver(new LoaderContext(dir), 'args'); + + await expect( + r.resolve({ module: 'm.mjs', export: 'value' }, parseJsonNumber, expectNumber), + ).rejects.toThrow(ConfigError); + }); + + it('should propagate ConfigError when the module path is unimportable', async () => { + const r = new RefResolver(new LoaderContext('/tmp'), 'args'); + await expect( + r.resolve( + { module: 'nope-zz.mjs', export: 'default' }, + parseJsonNumber, + expectNumber, + ), + ).rejects.toThrow(ConfigError); + }); +}); + +describe('RefResolver.resolve — invalid ref', () => { + it('should throw a ConfigError carrying the label for unknown ref shapes', async () => { + const r = new RefResolver(new LoaderContext('/tmp'), 'my-label'); + await expect( + r.resolve( + { unknown: 'thing' } as unknown as Parameters[0], + parseJsonNumber, + expectNumber, + ), + ).rejects.toThrow(/my-label/); + }); +}); diff --git a/packages/deployer/src/loaders/ref-resolver.ts b/packages/deployer/src/loaders/ref-resolver.ts new file mode 100644 index 0000000..3964a56 --- /dev/null +++ b/packages/deployer/src/loaders/ref-resolver.ts @@ -0,0 +1,44 @@ +import { + type FileOrModuleRef, + isFileRef, + isModuleRef, +} from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; +import type { LoaderContext } from './context.ts'; + +/** Resolve a `{ file }` / `{ module, export }` ref to a typed value via caller-supplied parse + validate callbacks. */ +export class RefResolver { + readonly #ctx: LoaderContext; + readonly #label: string; + + constructor(ctx: LoaderContext, label: string) { + this.#ctx = ctx; + this.#label = label; + } + + async resolve( + ref: FileOrModuleRef, + parseFile: (text: string, path: string) => T, + validateExport: (value: unknown, path: string, exportName: string) => T, + ): Promise { + if (isFileRef(ref)) { + const { text, path } = await this.#ctx.readText(ref.file, this.#label); + return parseFile(text, path); + } + if (isModuleRef(ref)) { + const { mod, path } = await this.#ctx.importModule( + ref.module, + this.#label, + ); + const exported = mod[ref.export]; + const resolved = + typeof exported === 'function' + ? await (exported as () => unknown)() + : exported; + return validateExport(resolved, path, ref.export); + } + throw new ConfigError( + `${this.#label}: must be { file } or { module, export }`, + ); + } +} diff --git a/packages/deployer/src/loaders/signing-key.test.ts b/packages/deployer/src/loaders/signing-key.test.ts new file mode 100644 index 0000000..c434a39 --- /dev/null +++ b/packages/deployer/src/loaders/signing-key.test.ts @@ -0,0 +1,34 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ConfigError } from '../errors.ts'; +import { SigningKey } from './signing-key.ts'; + +const VALID = 'a'.repeat(64); + +describe('SigningKey', () => { + it('should read and lowercase a 32-byte hex key', async () => { + const dir = mkdtempSync(join(tmpdir(), 'sk-test-')); + writeFileSync(join(dir, 'sk'), `${VALID.toUpperCase()}\n`); + expect((await SigningKey.load(dir, 'sk')).hex).toBe(VALID); + }); + + it('should strip an optional 0x prefix', async () => { + const dir = mkdtempSync(join(tmpdir(), 'sk-test-')); + writeFileSync(join(dir, 'sk'), `0x${VALID}\n`); + expect((await SigningKey.load(dir, 'sk')).hex).toBe(VALID); + }); + + it('should reject a wrong-length key', async () => { + const dir = mkdtempSync(join(tmpdir(), 'sk-test-')); + writeFileSync(join(dir, 'sk'), 'abcd'); + await expect(SigningKey.load(dir, 'sk')).rejects.toThrow(ConfigError); + }); + + it('should reject a missing file', async () => { + await expect(SigningKey.load('/tmp', 'no-such-file')).rejects.toThrow( + ConfigError, + ); + }); +}); diff --git a/packages/deployer/src/loaders/signing-key.ts b/packages/deployer/src/loaders/signing-key.ts new file mode 100644 index 0000000..7a73710 --- /dev/null +++ b/packages/deployer/src/loaders/signing-key.ts @@ -0,0 +1,27 @@ +import { ConfigError } from '../errors.ts'; +import { LoaderContext } from './context.ts'; + +/** + * Maintenance-authority signing key. Canonical form: 64 lowercase hex + * chars, no `0x`. Fuzzy input is rejected so midnight-js can't silently + * auto-sample a key the user then can't recover. + */ +export class SigningKey { + readonly hex: string; + + private constructor(hex: string) { + this.hex = hex; + } + + static async load(rootDir: string, path: string): Promise { + const ctx = new LoaderContext(rootDir); + const { text, path: abs } = await ctx.readText(path, 'signing_key_file'); + const trimmed = text.trim().replace(/^0x/i, ''); + if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) { + throw new ConfigError( + `signing_key_file ${abs}: expected 32 bytes hex-encoded (64 hex chars)`, + ); + } + return new SigningKey(trimmed.toLowerCase()); + } +} diff --git a/packages/deployer/src/providers/build.test.ts b/packages/deployer/src/providers/build.test.ts new file mode 100644 index 0000000..d19ce1e --- /dev/null +++ b/packages/deployer/src/providers/build.test.ts @@ -0,0 +1,239 @@ +import type { + EnvironmentConfiguration, + MidnightWalletProvider, +} from '@midnight-ntwrk/testkit-js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { ContractConfig } from '../config/schema.ts'; + +vi.mock('@midnight-ntwrk/midnight-js-http-client-proof-provider', () => ({ + httpClientProofProvider: vi.fn((url: string) => ({ kind: 'proof', url })), +})); + +vi.mock('@midnight-ntwrk/midnight-js-indexer-public-data-provider', () => ({ + indexerPublicDataProvider: vi.fn((indexer: string, ws: string) => ({ + kind: 'public', + indexer, + ws, + })), +})); + +vi.mock('@midnight-ntwrk/midnight-js-level-private-state-provider', () => ({ + levelPrivateStateProvider: vi.fn( + (opts: { privateStateStoreName: string; accountId: string }) => ({ + kind: 'private', + storeName: opts.privateStateStoreName, + accountId: opts.accountId, + }), + ), +})); + +vi.mock('@midnight-ntwrk/midnight-js-node-zk-config-provider', () => ({ + NodeZkConfigProvider: vi.fn(function NodeZkConfigProvider( + this: { kind: string; path: string }, + path: string, + ) { + this.kind = 'zk'; + this.path = path; + }), +})); + +const { buildProviders } = await import('./build.ts'); +const { httpClientProofProvider } = await import( + '@midnight-ntwrk/midnight-js-http-client-proof-provider' +); +const { indexerPublicDataProvider } = await import( + '@midnight-ntwrk/midnight-js-indexer-public-data-provider' +); +const { levelPrivateStateProvider } = await import( + '@midnight-ntwrk/midnight-js-level-private-state-provider' +); +const { NodeZkConfigProvider } = await import( + '@midnight-ntwrk/midnight-js-node-zk-config-provider' +); + +const env: EnvironmentConfiguration = { + walletNetworkId: 'testnet', + networkId: 'testnet', + indexer: 'https://indexer.example/api', + indexerWS: 'wss://indexer.example/ws', + node: 'https://node.example', + nodeWS: 'wss://node.example/ws', + proofServer: 'http://proof:6300', +} as EnvironmentConfiguration; + +const wallet = { + getEncryptionPublicKey: vi.fn(() => 'enc-pubkey-abc'), + getCoinPublicKey: vi.fn(() => 'coin-pubkey-def'), +} as unknown as MidnightWalletProvider; + +const baseContract: ContractConfig = { + artifact: 'src/artifacts/Counter', + signing_key_file: 'keys/counter.signing', +}; + +describe('buildProviders', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should default the private-state store name to -private-state', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + const opts = vi.mocked(levelPrivateStateProvider).mock.calls[0]?.[0]; + expect(opts?.privateStateStoreName).toBe('Counter-private-state'); + }); + + it('should honor a contract-provided private_state_store_name', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: { ...baseContract, private_state_store_name: 'custom-store' }, + zkConfigPath: '/artifacts/Counter', + }); + + const opts = vi.mocked(levelPrivateStateProvider).mock.calls[0]?.[0]; + expect(opts?.privateStateStoreName).toBe('custom-store'); + }); + + it('should bind the private-state account to the wallet coin pubkey', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + const opts = vi.mocked(levelPrivateStateProvider).mock.calls[0]?.[0]; + expect(opts?.accountId).toBe('coin-pubkey-def'); + }); + + it('should derive the private-state password from the wallet encryption pubkey', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + expect(wallet.getEncryptionPublicKey).toHaveBeenCalledOnce(); + }); + + it('should expose a privateStoragePasswordProvider that returns the derived password', async () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + const opts = vi.mocked(levelPrivateStateProvider).mock.calls[0]?.[0] as { + privateStoragePasswordProvider: () => string | Promise; + }; + const pw = await opts.privateStoragePasswordProvider(); + expect(typeof pw).toBe('string'); + expect(pw.length).toBeGreaterThan(0); + }); + + it('should construct NodeZkConfigProvider with the zkConfigPath', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + expect(NodeZkConfigProvider).toHaveBeenCalledWith('/artifacts/Counter'); + }); + + it('should wire the indexer URLs into the public data provider', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + expect(indexerPublicDataProvider).toHaveBeenCalledWith( + env.indexer, + env.indexerWS, + ); + }); + + it('should wire the proof-server URL into the HTTP proof provider', () => { + buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + const firstArg = vi.mocked(httpClientProofProvider).mock.calls[0]?.[0]; + expect(firstArg).toBe(env.proofServer); + }); + + it('should expose wallet as both walletProvider and midnightProvider', () => { + const providers = buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + expect(providers.walletProvider).toBe(wallet); + expect(providers.midnightProvider).toBe(wallet); + }); + + it('should pass through an injected privateStateProvider and skip the LevelDB construction', () => { + const injected = { + __injected: true, + } as unknown as Parameters[0]['privateStateProvider']; + + const providers = buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + privateStateProvider: injected, + }); + + expect(providers.privateStateProvider).toBe(injected); + expect(levelPrivateStateProvider).not.toHaveBeenCalled(); + expect(wallet.getEncryptionPublicKey).not.toHaveBeenCalled(); + }); + + it('should return all six provider slots', () => { + const providers = buildProviders({ + env, + wallet, + contractName: 'Counter', + contract: baseContract, + zkConfigPath: '/artifacts/Counter', + }); + + expect(Object.keys(providers).sort()).toEqual( + [ + 'privateStateProvider', + 'publicDataProvider', + 'zkConfigProvider', + 'proofProvider', + 'walletProvider', + 'midnightProvider', + ].sort(), + ); + }); +}); diff --git a/packages/deployer/src/providers/build.ts b/packages/deployer/src/providers/build.ts new file mode 100644 index 0000000..551ab7e --- /dev/null +++ b/packages/deployer/src/providers/build.ts @@ -0,0 +1,61 @@ +import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; +import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; +import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; +import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; +import type { + MidnightProviders, + PrivateStateProvider, +} from '@midnight-ntwrk/midnight-js-types'; +import type { + EnvironmentConfiguration, + MidnightWalletProvider, +} from '@midnight-ntwrk/testkit-js'; +import type { ContractConfig } from '../config/schema.ts'; +import { derivePrivateStatePassword } from './private-state-password.ts'; + +export interface BuildProvidersOptions { + env: EnvironmentConfiguration; + wallet: MidnightWalletProvider; + contractName: string; + contract: ContractConfig; + zkConfigPath: string; + /** Inject `inMemoryPrivateStateProvider` in tests to avoid LevelDB file-lock contention. */ + privateStateProvider?: PrivateStateProvider; +} + +export function buildProviders({ + env, + wallet, + contractName, + contract, + zkConfigPath, + privateStateProvider, +}: BuildProvidersOptions): MidnightProviders { + const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath); + + const resolvedPrivateStateProvider: PrivateStateProvider = + privateStateProvider ?? defaultLevelPrivateStateProvider(wallet, contract, contractName); + + return { + privateStateProvider: resolvedPrivateStateProvider, + publicDataProvider: indexerPublicDataProvider(env.indexer, env.indexerWS), + zkConfigProvider, + proofProvider: httpClientProofProvider(env.proofServer, zkConfigProvider), + walletProvider: wallet, + midnightProvider: wallet, + }; +} + +function defaultLevelPrivateStateProvider( + wallet: MidnightWalletProvider, + contract: ContractConfig, + contractName: string, +): PrivateStateProvider { + const password = derivePrivateStatePassword(wallet.getEncryptionPublicKey()); + return levelPrivateStateProvider({ + privateStateStoreName: + contract.private_state_store_name ?? `${contractName}-private-state`, + accountId: wallet.getCoinPublicKey(), + privateStoragePasswordProvider: () => password, + }); +} diff --git a/packages/deployer/src/providers/network.test.ts b/packages/deployer/src/providers/network.test.ts new file mode 100644 index 0000000..721ca77 --- /dev/null +++ b/packages/deployer/src/providers/network.test.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { NetworkConfig } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; +import { applyNetwork } from './network.ts'; + +vi.mock('@midnight-ntwrk/midnight-js-network-id', () => ({ + setNetworkId: vi.fn(), +})); + +const { setNetworkId } = await import('@midnight-ntwrk/midnight-js-network-id'); + +const baseNetwork: NetworkConfig = { + network_id: 'testnet', + indexer: 'https://indexer.example/api', + indexer_ws: 'wss://indexer.example/ws', + node: 'https://node.example', + node_ws: 'wss://node.example/ws', +}; + +describe('applyNetwork', () => { + beforeEach(() => { + vi.mocked(setNetworkId).mockClear(); + }); + + it('should set the network id and assemble the environment for a known id', () => { + const { env } = applyNetwork(baseNetwork, 'http://proof-server:6300'); + + expect(setNetworkId).toHaveBeenCalledWith('testnet'); + expect(env.networkId).toBe('testnet'); + expect(env.indexer).toBe('https://indexer.example/api'); + expect(env.indexerWS).toBe('wss://indexer.example/ws'); + expect(env.node).toBe('https://node.example'); + expect(env.nodeWS).toBe('wss://node.example/ws'); + expect(env.proofServer).toBe('http://proof-server:6300'); + }); + + it.each([ + 'undeployed', + 'devnet', + 'qanet', + 'testnet', + 'preview', + 'preprod', + 'mainnet', + ])('should accept known network id %s', (id) => { + expect(() => + applyNetwork({ ...baseNetwork, network_id: id }, 'http://ps'), + ).not.toThrow(); + expect(setNetworkId).toHaveBeenLastCalledWith(id); + }); + + it('should reject an unknown network id with ConfigError', () => { + expect(() => + applyNetwork( + { ...baseNetwork, network_id: 'bogus-net' }, + 'http://ps', + ), + ).toThrow(ConfigError); + }); + + it('should not call setNetworkId when the id is unknown', () => { + try { + applyNetwork( + { ...baseNetwork, network_id: 'bogus-net' }, + 'http://ps', + ); + } catch { + /* expected */ + } + expect(setNetworkId).not.toHaveBeenCalled(); + }); + + it('should include the allowed-id list in the error message', () => { + expect(() => + applyNetwork( + { ...baseNetwork, network_id: 'bogus' }, + 'http://ps', + ), + ).toThrow(/expected one of:.*testnet/); + }); +}); diff --git a/packages/deployer/src/providers/network.ts b/packages/deployer/src/providers/network.ts new file mode 100644 index 0000000..95c3d8a --- /dev/null +++ b/packages/deployer/src/providers/network.ts @@ -0,0 +1,53 @@ +import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import type { EnvironmentConfiguration } from '@midnight-ntwrk/testkit-js'; +import type { NetworkConfig } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; + +/** + * Set the midnight-js network-id singleton + build an + * `EnvironmentConfiguration`. `KNOWN_NETWORK_IDS` is closed so a typo + * fails fast here instead of as a generic midnight-js error later. + */ + +const KNOWN_NETWORK_IDS: ReadonlySet = new Set([ + 'undeployed', + 'devnet', + 'qanet', + 'testnet', + 'preview', + 'preprod', + 'mainnet', +]); + +export interface ResolvedEnvironment { + env: EnvironmentConfiguration; +} + +export function applyNetwork( + network: NetworkConfig, + proofServerUrl: string, +): ResolvedEnvironment { + if (!KNOWN_NETWORK_IDS.has(network.network_id)) { + throw new ConfigError( + `Unknown network_id "${network.network_id}" (expected one of: ${[...KNOWN_NETWORK_IDS].join(', ')})`, + ); + } + setNetworkId(network.network_id); + + const env: EnvironmentConfiguration = { + walletNetworkId: + network.network_id as EnvironmentConfiguration['walletNetworkId'], + networkId: network.network_id, + indexer: network.indexer, + indexerWS: network.indexer_ws, + node: network.node, + nodeWS: network.node_ws, + proofServer: proofServerUrl, + // testkit-js requires this field even though our deploys never + // hit the faucet themselves. Set to undefined so dependent code + // paths (e.g. wait-for-funds hints) treat it as absent. + faucet: undefined, + }; + + return { env }; +} diff --git a/packages/deployer/src/providers/private-state-password.test.ts b/packages/deployer/src/providers/private-state-password.test.ts new file mode 100644 index 0000000..48abb10 --- /dev/null +++ b/packages/deployer/src/providers/private-state-password.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from 'vitest'; +import { derivePrivateStatePassword } from './private-state-password.ts'; + +describe('derivePrivateStatePassword', () => { + it('should be deterministic for the same input', () => { + const a = derivePrivateStatePassword('abcdef1234567890'); + const b = derivePrivateStatePassword('abcdef1234567890'); + expect(a).toBe(b); + }); + + it('should differ for different inputs', () => { + const a = derivePrivateStatePassword('abcdef1234567890'); + const b = derivePrivateStatePassword('abcdef1234567891'); + expect(a).not.toBe(b); + }); + + it('should not contain 4 identical chars in a row', () => { + for (let i = 0; i < 200; i++) { + const pw = derivePrivateStatePassword(`pubkey-${i}`); + expect(pw).not.toMatch(/(.)\1{3,}/); + } + }); + + it('should produce a password with mixed character classes (uppercase + digit + symbol)', () => { + const pw = derivePrivateStatePassword('any input'); + expect(pw).toMatch(/[A-Z]/); + expect(pw).toMatch(/[0-9]/); + expect(pw).toMatch(/[^A-Za-z0-9]/); + }); + + it('should handle inputs that would have produced naïve-bad passwords', () => { + // A 64-zero hex (the kind of structured pubkey that breaks + // `${encKey}A!`-style derivations) must still produce a valid password. + const pw = derivePrivateStatePassword('0'.repeat(64)); + expect(pw).not.toMatch(/(.)\1{3,}/); + }); + + it('should throw after 1024 rounds when every hash has 4+ identical chars', async () => { + // Force every round to produce a string that hits the `(.)\1{3,}` guard so + // the loop exhausts its retry budget and reaches the explicit throw. + vi.resetModules(); + vi.doMock('node:crypto', async () => { + const actual = await vi.importActual( + 'node:crypto', + ); + return { + ...actual, + createHash: () => ({ + update: () => ({ + digest: () => 'aaaaBBBBccccDDDD', + }), + }), + }; + }); + const { derivePrivateStatePassword: derive } = await import( + './private-state-password.ts' + ); + expect(() => derive('anything')).toThrow(/unable to find a hash/); + vi.doUnmock('node:crypto'); + vi.resetModules(); + }); +}); diff --git a/packages/deployer/src/providers/private-state-password.ts b/packages/deployer/src/providers/private-state-password.ts new file mode 100644 index 0000000..98a890e --- /dev/null +++ b/packages/deployer/src/providers/private-state-password.ts @@ -0,0 +1,27 @@ +import { createHash } from 'node:crypto'; + +/** + * Derive a leveldb-compatible password from the wallet's encryption key. + * level-private-state-provider rejects passwords with 4+ identical chars + * in a row, which structured seeds (TEST_MNEMONIC, `0x…0001`) routinely + * produce. We SHA-256 + base64url + strip + rehash-on-collision until clean, + * then append `A1!` for guaranteed character-class diversity. + */ +export function derivePrivateStatePassword( + encryptionPublicKey: string, +): string { + for (let counter = 0; counter < 1024; counter++) { + const body = createHash('sha256') + .update(`${encryptionPublicKey}:${counter}`) + .digest('base64url') + .replace(/[^A-Za-z0-9]/g, ''); + if (!/(.)\1{3,}/.test(body)) { + return `${body}A1!`; + } + } + // Pathologically improbable. Surface explicitly so the deploy fails loud + // rather than silently retrying forever. + throw new Error( + 'derivePrivateStatePassword: unable to find a hash without 4+ repeated chars after 1024 rounds', + ); +} diff --git a/packages/deployer/src/providers/proof-server.test.ts b/packages/deployer/src/providers/proof-server.test.ts new file mode 100644 index 0000000..8ff3c02 --- /dev/null +++ b/packages/deployer/src/providers/proof-server.test.ts @@ -0,0 +1,193 @@ +import type { Logger } from 'pino'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { NetworkConfig } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; + +vi.mock('@midnight-ntwrk/testkit-js', () => ({ + DynamicProofServerContainer: { + start: vi.fn(async () => ({ + getUrl: () => 'http://dynamic-container:6300', + stop: vi.fn(async () => undefined), + })), + }, + StaticProofServerContainer: vi.fn(function StaticProofServerContainer( + this: { getUrl: () => string; stop: () => Promise }, + port: number, + ) { + this.getUrl = () => `http://127.0.0.1:${port}`; + this.stop = vi.fn(async () => undefined); + }), +})); + +const { + DynamicProofServerContainer, + StaticProofServerContainer, +} = await import('@midnight-ntwrk/testkit-js'); +const { ProofServer } = await import('./proof-server.ts'); + +const makeLogger = (): Logger => { + const noop = vi.fn(); + return { + debug: noop, + info: noop, + warn: noop, + error: noop, + fatal: noop, + trace: noop, + } as unknown as Logger; +}; + +const baseNetwork: NetworkConfig = { + network_id: 'testnet', + indexer: 'https://indexer.example/api', + indexer_ws: 'wss://indexer.example/ws', + node: 'https://node.example', + node_ws: 'wss://node.example/ws', +}; + +describe('ProofServer.start — precedence chain', () => { + const originalPort = process.env.PROOF_SERVER_PORT; + + beforeEach(() => { + vi.mocked(DynamicProofServerContainer.start).mockClear(); + vi.mocked(StaticProofServerContainer).mockClear(); + delete process.env.PROOF_SERVER_PORT; + }); + + afterEach(() => { + if (originalPort === undefined) { + delete process.env.PROOF_SERVER_PORT; + } else { + process.env.PROOF_SERVER_PORT = originalPort; + } + }); + + it('(1) should use cliOverride above everything else', async () => { + process.env.PROOF_SERVER_PORT = '9999'; + const ps = await ProofServer.start({ + cliOverride: 'http://cli:6300', + network: { ...baseNetwork, proof_server: 'http://toml:6300' }, + logger: makeLogger(), + }); + + expect(ps.url).toBe('http://cli:6300'); + expect(DynamicProofServerContainer.start).not.toHaveBeenCalled(); + expect(StaticProofServerContainer).not.toHaveBeenCalled(); + }); + + it('(2) should use the TOML proof_server URL when no CLI override', async () => { + const ps = await ProofServer.start({ + network: { ...baseNetwork, proof_server: 'http://toml:6300' }, + logger: makeLogger(), + }); + expect(ps.url).toBe('http://toml:6300'); + expect(DynamicProofServerContainer.start).not.toHaveBeenCalled(); + }); + + it('(3) should boot a dynamic container when TOML proof_server = "auto"', async () => { + const ps = await ProofServer.start({ + network: { ...baseNetwork, proof_server: 'auto' }, + logger: makeLogger(), + }); + + expect(ps.url).toBe('http://dynamic-container:6300'); + expect(DynamicProofServerContainer.start).toHaveBeenCalledTimes(1); + const callArgs = vi.mocked(DynamicProofServerContainer.start).mock.calls[0]; + expect(callArgs?.[2]).toBe('testnet'); + }); + + it('(4) should use PROOF_SERVER_PORT when no explicit config', async () => { + process.env.PROOF_SERVER_PORT = '7777'; + const ps = await ProofServer.start({ + network: baseNetwork, + logger: makeLogger(), + }); + + expect(ps.url).toBe('http://127.0.0.1:7777'); + expect(StaticProofServerContainer).toHaveBeenCalledWith(7777); + }); + + it('(4) should throw ConfigError for a non-numeric PROOF_SERVER_PORT', async () => { + process.env.PROOF_SERVER_PORT = 'not-a-number'; + await expect( + ProofServer.start({ network: baseNetwork, logger: makeLogger() }), + ).rejects.toThrow(ConfigError); + }); + + it('(5) should fall back to http://127.0.0.1:6300 when nothing is configured', async () => { + const ps = await ProofServer.start({ + network: baseNetwork, + logger: makeLogger(), + }); + + expect(ps.url).toBe('http://127.0.0.1:6300'); + expect(DynamicProofServerContainer.start).not.toHaveBeenCalled(); + expect(StaticProofServerContainer).not.toHaveBeenCalled(); + }); + + it('should prefer cliOverride = "auto" over TOML URL (CLI wins)', async () => { + const ps = await ProofServer.start({ + cliOverride: 'http://cli-static', + network: { ...baseNetwork, proof_server: 'auto' }, + logger: makeLogger(), + }); + + expect(ps.url).toBe('http://cli-static'); + expect(DynamicProofServerContainer.start).not.toHaveBeenCalled(); + }); +}); + +describe('ProofServer — disposal', () => { + it('should be a no-op for static-URL instances', async () => { + const ps = await ProofServer.start({ + network: { ...baseNetwork, proof_server: 'http://static' }, + logger: makeLogger(), + }); + await expect(ps.dispose()).resolves.toBeUndefined(); + }); + + it('should stop the static container for the PROOF_SERVER_PORT path', async () => { + process.env.PROOF_SERVER_PORT = '7777'; + const ps = await ProofServer.start({ + network: baseNetwork, + logger: makeLogger(), + }); + const instance = vi.mocked(StaticProofServerContainer).mock.instances[0]; + await ps.dispose(); + expect(instance?.stop).toHaveBeenCalledOnce(); + }); + + it('should stop the underlying container for the "auto" path', async () => { + const stop = vi.fn(async () => undefined); + vi.mocked(DynamicProofServerContainer.start).mockResolvedValueOnce({ + getUrl: () => 'http://dyn', + stop, + } as never); + + const ps = await ProofServer.start({ + network: { ...baseNetwork, proof_server: 'auto' }, + logger: makeLogger(), + }); + await ps.dispose(); + expect(stop).toHaveBeenCalledOnce(); + }); + + it('Symbol.asyncDispose should swallow teardown errors via the warn log', async () => { + const stop = vi.fn(async () => { + throw new Error('boom'); + }); + vi.mocked(DynamicProofServerContainer.start).mockResolvedValueOnce({ + getUrl: () => 'http://dyn', + stop, + } as never); + + const logger = makeLogger(); + const ps = await ProofServer.start({ + network: { ...baseNetwork, proof_server: 'auto' }, + logger, + }); + + await expect(ps[Symbol.asyncDispose]()).resolves.toBeUndefined(); + expect(logger.warn).toHaveBeenCalled(); + }); +}); diff --git a/packages/deployer/src/providers/proof-server.ts b/packages/deployer/src/providers/proof-server.ts new file mode 100644 index 0000000..8c6a7d0 --- /dev/null +++ b/packages/deployer/src/providers/proof-server.ts @@ -0,0 +1,111 @@ +import { + DynamicProofServerContainer, + StaticProofServerContainer, +} from '@midnight-ntwrk/testkit-js'; +import type { Logger } from 'pino'; +import type { NetworkConfig } from '../config/schema.ts'; +import { ConfigError } from '../errors.ts'; + +export interface ProofServerOptions { + cliOverride?: string; + network: NetworkConfig; + logger: Logger; +} + +/** + * Proof-server handle with a resolved URL + lifecycle. Always acquired via + * {@link ProofServer.start}; {@link dispose} is a no-op for static URLs + * and a container-stop for the `auto` / `PROOF_SERVER_PORT` paths. + */ +export class ProofServer { + /** Resolved URL the proof provider POSTs to. */ + readonly url: string; + readonly #dispose: () => Promise; + readonly #logger: Logger; + + private constructor( + url: string, + dispose: () => Promise, + logger: Logger, + ) { + this.url = url; + this.#dispose = dispose; + this.#logger = logger; + } + + /** + * Resolve URL by precedence: `cliOverride` > TOML `proof_server` URL > + * `proof_server = "auto"` (boots container) > `PROOF_SERVER_PORT` env > + * `http://127.0.0.1:6300`. + */ + static async start(opts: ProofServerOptions): Promise { + const { cliOverride, network, logger } = opts; + const explicit = cliOverride ?? network.proof_server; + + if (explicit && explicit !== 'auto') { + logger.debug(`Using configured proof server: ${explicit}`); + return ProofServer.fromStaticUrl(explicit, logger); + } + + if (explicit === 'auto') { + logger.info('Starting proof-server container (auto)…'); + const container = await DynamicProofServerContainer.start( + logger, + undefined, + network.network_id, + ); + return new ProofServer( + container.getUrl(), + () => container.stop(), + logger, + ); + } + + const port = process.env.PROOF_SERVER_PORT; + if (port !== undefined) { + const parsed = Number.parseInt(port, 10); + if (Number.isNaN(parsed)) { + throw new ConfigError(`Invalid PROOF_SERVER_PORT: ${port}`); + } + logger.debug(`Using PROOF_SERVER_PORT=${parsed}`); + const container = new StaticProofServerContainer(parsed); + return new ProofServer( + container.getUrl(), + () => container.stop(), + logger, + ); + } + + logger.debug( + 'Falling back to default proof server at http://127.0.0.1:6300', + ); + return ProofServer.fromStaticUrl('http://127.0.0.1:6300', logger); + } + + private static fromStaticUrl(url: string, logger: Logger): ProofServer { + return new ProofServer( + url, + async () => { + /* no container to stop */ + }, + logger, + ); + } + + /** Release any underlying container. Idempotent for static-URL instances. */ + async dispose(): Promise { + return this.#dispose(); + } + + /** `await using` hook: swallows teardown errors so they don't mask the deploy's real error. */ + async [Symbol.asyncDispose](): Promise { + try { + await this.#dispose(); + } catch (e) { + this.#logger.warn( + { err: (e as Error).message }, + 'Proof server dispose failed', + ); + } + } +} diff --git a/packages/deployer/src/runDeploy.test.ts b/packages/deployer/src/runDeploy.test.ts new file mode 100644 index 0000000..ef34943 --- /dev/null +++ b/packages/deployer/src/runDeploy.test.ts @@ -0,0 +1,369 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as compactConfigModule from './config/compact-config.ts'; +import * as deployerModule from './deployer.ts'; +import { DeployError } from './errors.ts'; +import * as contractResolveModule from './loaders/contract-resolve.ts'; +import { constructorArgs, runDeploy } from './runDeploy.ts'; + +function fakeDeployResult(overrides: Record = {}) { + return { + contractName: 'X', + network: 'local', + address: '0xaddr', + txHash: '0xtx', + txId: 'tx-id', + blockHeight: 42, + signingKey: '0xsk', + deployer: '0xdep', + artifact: 'X', + deploymentsFile: '/tmp/local.json', + dryRun: false, + explorerUrl: '', + ...overrides, + }; +} + +function fakeDeployer(opts: { dryRun?: () => unknown; deploy?: () => unknown } = {}) { + const deploy = opts.deploy ?? vi.fn(async () => fakeDeployResult()); + const dryRun = + opts.dryRun ?? vi.fn(async () => fakeDeployResult({ dryRun: true })); + return { + deploy, + dryRun, + [Symbol.asyncDispose]: vi.fn(async () => undefined), + }; +} + +let originalArgv: string[]; +let exitSpy: ReturnType; +let writeSpy: ReturnType; + +beforeEach(() => { + originalArgv = process.argv; + exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code ?? 0})`); + }) as never); + writeSpy = vi + .spyOn(process.stdout, 'write') + .mockImplementation(() => true); +}); + +afterEach(() => { + process.argv = originalArgv; + vi.restoreAllMocks(); +}); + +describe('runDeploy', () => { + it('should call Deployer.prepare with merged opts (explicit > argv)', async () => { + process.argv = ['node', 'script.ts', '--network', 'preview', '--dry-run']; + const fakeDep = fakeDeployer(); + const prepare = vi + .spyOn(deployerModule.Deployer, 'prepare') + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + .mockResolvedValue(fakeDep as any); + + await runDeploy({ contract: 'X', network: 'local', args: [1, 2] }); + + const callArgs = prepare.mock.calls[0]?.[0]; + expect(callArgs?.contract).toBe('X'); + expect(callArgs?.network).toBe('local'); // explicit beats argv + expect(callArgs?.args).toEqual([1, 2]); + }); + + it('should pull --network and --dry-run from argv when opts omit them', async () => { + process.argv = ['node', 'script.ts', '--network', 'preview', '--dry-run']; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + + await runDeploy({ contract: 'X' }); + + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + expect((fakeDep.dryRun as any).mock.calls.length).toBe(1); + expect( + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + (deployerModule.Deployer.prepare as any).mock.calls[0][0].network, + ).toBe('preview'); + }); + + it('should convert --sync-timeout seconds to milliseconds', async () => { + process.argv = ['node', 'script.ts', '--sync-timeout', '120']; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + + await runDeploy({ contract: 'X' }); + + expect( + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + (deployerModule.Deployer.prepare as any).mock.calls[0][0].syncTimeoutMs, + ).toBe(120_000); + }); + + it('should reject a non-positive --sync-timeout', async () => { + process.argv = ['node', 'script.ts', '--sync-timeout', 'nope']; + await expect(runDeploy({ contract: 'X' })).rejects.toThrow( + /--sync-timeout requires a positive integer/, + ); + }); + + it('should thread --seed-cache-from-dust and --seed-cache-from-shielded to Deployer.prepare', async () => { + process.argv = [ + 'node', + 'script.ts', + '--seed-cache-from-dust', + '/path/to/dust.json', + '--seed-cache-from-shielded', + '/path/to/shielded.gz', + ]; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + + await runDeploy({ contract: 'X' }); + + const callArgs = + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + (deployerModule.Deployer.prepare as any).mock.calls[0][0]; + expect(callArgs.seedCacheDust).toBe('/path/to/dust.json'); + expect(callArgs.seedCacheShielded).toBe('/path/to/shielded.gz'); + }); + + it('should let explicit seedCacheFromDust opt beat the argv value', async () => { + process.argv = [ + 'node', + 'script.ts', + '--seed-cache-from-dust', + '/argv.json', + ]; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + + await runDeploy({ contract: 'X', seedCacheFromDust: '/explicit.json' }); + + const callArgs = + // biome-ignore lint/suspicious/noExplicitAny: vi mock typing + (deployerModule.Deployer.prepare as any).mock.calls[0][0]; + expect(callArgs.seedCacheDust).toBe('/explicit.json'); + }); + + it('should reject --seed-cache-from-dust with no value', async () => { + process.argv = ['node', 'script.ts', '--seed-cache-from-dust']; + await expect(runDeploy({ contract: 'X' })).rejects.toThrow( + /--seed-cache-from-dust requires a value/, + ); + }); + + it('should emit JSON on stdout in --json mode on success', async () => { + process.argv = ['node', 'script.ts', '--json']; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + + await runDeploy({ contract: 'X' }); + + expect(writeSpy).toHaveBeenCalled(); + const written = writeSpy.mock.calls[0]?.[0] as string; + const parsed = JSON.parse(written); + expect(parsed.contractName).toBe('X'); + expect(parsed.address).toBe('0xaddr'); + }); + + it('should emit JSON error + exit with DeployError.exitCode in --json mode', async () => { + process.argv = ['node', 'script.ts', '--json']; + vi.spyOn(deployerModule.Deployer, 'prepare').mockRejectedValue( + new DeployError('boom', 3), + ); + + await expect(runDeploy({ contract: 'X' })).rejects.toThrow( + 'process.exit(3)', + ); + + const written = writeSpy.mock.calls[0]?.[0] as string; + const parsed = JSON.parse(written); + expect(parsed.error).toBe('DeployError'); + expect(parsed.message).toBe('boom'); + expect(parsed.exitCode).toBe(3); + }); + + it('should exit with code 1 on non-DeployError', async () => { + process.argv = ['node', 'script.ts']; + vi.spyOn(deployerModule.Deployer, 'prepare').mockRejectedValue( + new Error('generic'), + ); + + await expect(runDeploy({ contract: 'X' })).rejects.toThrow( + 'process.exit(1)', + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it('should pass constructorArgs(Contract, ...) tuple through to Deployer.prepare', async () => { + process.argv = ['node', 'script.ts']; + const fakeDep = fakeDeployer(); + const prepare = vi + .spyOn(deployerModule.Deployer, 'prepare') + .mockResolvedValue(fakeDep as never); + + class FakeContract { + initialState(_ctx: never, _a: string, _b: bigint) { + return undefined as never; + } + } + + await runDeploy({ + contract: 'X', + args: constructorArgs(FakeContract as never, 'hello', 42n), + }); + + expect(prepare.mock.calls[0]?.[0]?.args).toEqual(['hello', 42n]); + }); + + it('should forward a named-object args record to Deployer.prepare untouched', async () => { + process.argv = ['node', 'script.ts']; + const fakeDep = fakeDeployer(); + const prepare = vi + .spyOn(deployerModule.Deployer, 'prepare') + .mockResolvedValue(fakeDep as never); + + interface MyArgs { + foo: string; + bar: bigint; + } + + await runDeploy({ + contract: 'X', + args: { foo: 'hello', bar: 42n }, + }); + + // The reorder happens inside ConstructorArgs.load against the + // artifact's index.d.ts; runDeploy just forwards the object as-is. + expect(prepare.mock.calls[0]?.[0]?.args).toEqual({ + foo: 'hello', + bar: 42n, + }); + }); + + it('curried form: should resolve Contract → name and forward args positionally', async () => { + process.argv = ['node', 'script.ts']; + const fakeDep = fakeDeployer(); + const prepare = vi + .spyOn(deployerModule.Deployer, 'prepare') + .mockResolvedValue(fakeDep as never); + vi.spyOn(compactConfigModule.CompactConfig, 'load').mockResolvedValue({ + rootDir: '/tmp', + } as never); + vi.spyOn(contractResolveModule, 'resolveContractName').mockResolvedValue( + 'TokenExample', + ); + + class FakeContract { + initialState(_ctx: never, _a: string, _b: bigint) { + return undefined as never; + } + } + + await runDeploy(FakeContract as never)('hello', 42n); + + expect(prepare.mock.calls[0]?.[0]?.contract).toBe('TokenExample'); + expect(prepare.mock.calls[0]?.[0]?.args).toEqual(['hello', 42n]); + }); + + it('curried form: should merge extra opts (2nd arg) into Deployer.prepare', async () => { + process.argv = ['node', 'script.ts']; + const fakeDep = fakeDeployer(); + const prepare = vi + .spyOn(deployerModule.Deployer, 'prepare') + .mockResolvedValue(fakeDep as never); + vi.spyOn(compactConfigModule.CompactConfig, 'load').mockResolvedValue({ + rootDir: '/tmp', + } as never); + vi.spyOn(contractResolveModule, 'resolveContractName').mockResolvedValue( + 'TokenExample', + ); + + class FakeContract { + initialState(_ctx: never, _a: string) { + return undefined as never; + } + } + + await runDeploy(FakeContract as never, { network: 'preview' })('hello'); + + expect(prepare.mock.calls[0]?.[0]?.network).toBe('preview'); + }); + + it('should print dryRun success line in non-JSON mode', async () => { + process.argv = ['node', 'script.ts', '--dry-run']; + const fakeDep = fakeDeployer(); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + await runDeploy({ + contract: 'X', + logger: logger as never, + }); + + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/^Dry-run for /), + ); + }); + + it('should print the explorer URL line when the result carries one', async () => { + process.argv = ['node', 'script.ts']; + const deploy = vi.fn(async () => + fakeDeployResult({ explorerUrl: 'https://explorer/contracts/0xaddr' }), + ); + const fakeDep = fakeDeployer({ deploy }); + vi.spyOn(deployerModule.Deployer, 'prepare').mockResolvedValue( + fakeDep as never, + ); + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + await runDeploy({ contract: 'X', logger: logger as never }); + + const explorerCall = logger.info.mock.calls.find((c) => + String(c[0]).includes('explorer:'), + ); + expect(explorerCall).toBeDefined(); + }); + + it('should log the stack trace in verbose mode when an Error throws', async () => { + process.argv = ['node', 'script.ts', '--verbose']; + const err = new Error('boom'); + err.stack = 'Error: boom\n at trace'; + vi.spyOn(deployerModule.Deployer, 'prepare').mockRejectedValue(err); + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + await expect( + runDeploy({ contract: 'X', logger: logger as never }), + ).rejects.toThrow('process.exit(1)'); + + expect(logger.debug).toHaveBeenCalledWith('Error: boom\n at trace'); + }); +}); diff --git a/packages/deployer/src/runDeploy.ts b/packages/deployer/src/runDeploy.ts new file mode 100644 index 0000000..e347fd3 --- /dev/null +++ b/packages/deployer/src/runDeploy.ts @@ -0,0 +1,400 @@ +import { pino, type Logger } from 'pino'; +import { CompactConfig } from './config/compact-config.ts'; +import { Deployer, type DeployResult } from './deployer.ts'; +import { DeployError } from './errors.ts'; +import { resolveContractName } from './loaders/contract-resolve.ts'; + +/** + * Shape every Compact-compiled `Contract` class satisfies. The + * compiler emits an `initialState(context, ...args)` method whose tail + * args mirror the constructor signature; {@link ConstructorArgsOf} + * lifts those args out of the artifact's `.d.ts` so deploy scripts + * don't have to redeclare them. + */ +export type CompactContractClass = { + prototype: { initialState(context: never, ...args: never[]): unknown }; +}; + +/** + * Extracts the constructor args tuple from a Compact-compiled + * `Contract` class (strips the leading `context` parameter). The + * result is a labeled tuple — hover on a value shows the param name + * and type. No codegen needed; the artifact's `.d.ts` is the source + * of truth. + * + * @example + * ```ts + * import type { Contract } from '../artifacts/Token/contract/index.js'; + * import { runDeploy, type ConstructorArgsOf } from '@openzeppelin/compact-deployer'; + * + * await runDeploy>({ + * contract: 'Token', + * args: ['OZE', 'OZE', 18n, ... ], + * }); + * ``` + */ +export type ConstructorArgsOf = + C['prototype']['initialState'] extends ( + context: never, + ...args: infer A + ) => unknown + ? A + : never; + +/** + * Typed constructor-args helper. Returns the args tuple unchanged at + * runtime; the work happens in the type system: `Contract` anchors + * the generic so each subsequent arg is checked against the matching + * position in `initialState`, and the editor shows the param name + + * type as you type each comma (TypeScript signature help works on + * function calls but not on tuple literals — that's why this exists + * instead of just `args: [...]`). + * + * @example + * ```ts + * import { runDeploy, constructorArgs } from '@openzeppelin/compact-deployer'; + * import { Contract } from '../artifacts/Token/contract/index.js'; + * + * await runDeploy({ + * contract: 'Token', + * args: constructorArgs(Contract, + * 'OpenZeppelin Token', // _name: string + * 'OZE', // _symbol: string + * 18n, // _decimals: bigint + * ), + * }); + * ``` + */ +export function constructorArgs( + _Contract: C, + ...args: ConstructorArgsOf +): ConstructorArgsOf { + return args; +} + +/** + * Inputs to {@link runDeploy}. Every field has an argv default so a + * hand-written deploy script can be as short as + * `await runDeploy>({ contract: 'X', args: [ … ] });` + * (typed tuple form) or + * `await runDeploy({ contract: 'X', args: { … } });` + * (named-object form — `MyNamedArgs` is hand-written or generated + * separately), or `await runDeploy({ contract: 'X' });` when args + * live in `compact.toml`. + * + * The `Args` generic is either: + * - a `readonly unknown[]` tuple (use `ConstructorArgsOf` + * to get the labeled tuple from the compiled artifact), or + * - a `Record` object describing the constructor's + * named parameters; keys can appear in any order and the deployer + * reorders them via the artifact's `index.d.ts`. + */ +export interface RunDeployOptions< + Args extends readonly unknown[] | Record = + | readonly unknown[] + | Record, +> { + /** Contract name in `compact.toml` (required). */ + contract: string; + /** + * Constructor args, either as a positional tuple or a named object. + * Highest precedence: overrides `compact.toml`'s `args` field. + */ + args?: Args; + /** Path to `compact.toml`. Argv: `--config`. Default: walk up from cwd. */ + configPath?: string; + /** Network name. Argv: `--network`. Default: `[profile].default_network` from `compact.toml`. */ + network?: string; + /** Seed file override. Argv: `--seed-file`. */ + seedFile?: string; + /** Proof-server URL override. Argv: `--proof-server`. */ + proofServer?: string; + /** Wallet-sync timeout in seconds. Argv: `--sync-timeout`. */ + syncTimeoutSec?: number; + /** Skip the on-disk wallet-state cache. Argv: `--no-cache`. */ + skipWalletCache?: boolean; + /** + * Import a pre-warmed dust wallet state file (raw JSON or gzipped) + * into `.states/` before deploy. Use this when first-run preprod + * cold sync OOMs and you already have a `serializeState()` output. + * Argv: `--seed-cache-from-dust`. + */ + seedCacheFromDust?: string; + /** Like {@link seedCacheFromDust} but for the shielded sub-wallet. Argv: `--seed-cache-from-shielded`. */ + seedCacheFromShielded?: string; + /** Dry-run mode (skip on-chain submission). Argv: `--dry-run`. */ + dryRun?: boolean; + /** Emit a machine-readable JSON object on stdout instead of pretty lines. Argv: `--json`. */ + json?: boolean; + /** Pino debug logs to stderr. Argv: `-v` / `--verbose`. */ + verbose?: boolean; + /** Override the auto-built logger (for testing or for embedding in larger apps). */ + logger?: Logger; + /** Keystore passphrase prompt. Only needed when `[wallet].keystore` is set. */ + promptPassphrase?: (path: string) => Promise; +} + +/** + * High-level deploy entrypoint for hand-written deploy scripts. + * + * Two call forms: + * + * 1. **Curried with Contract** — single source of truth for the + * contract identity, and the constructor args are typed function + * parameters (per-comma signature help in the editor): + * ```ts + * await runDeploy(Contract)('OZE', 'OZE', 18n, …); + * ``` + * The deployer matches `Contract` against `[contracts.X]` entries + * in `compact.toml` by class identity to pick the config. Extra + * deploy opts can be passed as a second arg: + * ```ts + * await runDeploy(Contract, { network: 'preview' })('OZE', …); + * ``` + * + * 2. **Options object** (legacy / multi-contract / TOML-driven args): + * ```ts + * await runDeploy({ contract: 'TokenExample', args: [...] }); + * ``` + * + * Parses `--network`, `--dry-run`, `--sync-timeout`, `--no-cache`, + * `--seed-cache-from-dust`, `--seed-cache-from-shielded`, + * `--seed-file`, `--proof-server`, `--config`, `--json`, `-v` / + * `--verbose` from `process.argv` as defaults. Explicit options win + * over argv. Wires a pino logger, runs Deployer.prepare + deploy or + * dryRun, prints the result, and exits with the typed exit code on + * error. + * + * Returns the result on success so callers can chain post-deploy work + * (e.g. seeding state via callTx). On error: logs and calls + * `process.exit` — never returns to the caller. + */ +export function runDeploy( + Contract: C, + opts?: Omit, +): (...args: ConstructorArgsOf) => Promise; +export function runDeploy< + Args extends readonly unknown[] | Record = + | readonly unknown[] + | Record, +>(opts: RunDeployOptions): Promise; +export function runDeploy( + arg: CompactContractClass | RunDeployOptions, + opts2?: Omit, +): + | Promise + | ((...args: unknown[]) => Promise) { + if (isContractClass(arg)) { + const Contract = arg; + return (...args: unknown[]) => + runDeployImpl({ + ...(opts2 ?? {}), + contract: '', + args, + }, Contract); + } + return runDeployImpl(arg); +} + +function isContractClass(x: unknown): x is CompactContractClass { + return ( + typeof x === 'function' && + typeof (x as { prototype?: { initialState?: unknown } }).prototype + ?.initialState === 'function' + ); +} + +async function runDeployImpl( + opts: RunDeployOptions, + Contract?: CompactContractClass, +): Promise { + const argv = parseArgv(process.argv.slice(2)); + const json = opts.json ?? argv.json; + const verbose = opts.verbose ?? argv.verbose; + const dryRun = opts.dryRun ?? argv.dryRun; + const syncTimeoutSec = opts.syncTimeoutSec ?? argv.syncTimeoutSec; + const logger = opts.logger ?? buildLogger({ json, verbose }); + const configPath = opts.configPath ?? argv.configPath; + + try { + // Curried form: resolve the Contract class to the matching + // [contracts.X] entry in compact.toml by identity. + let contractName = opts.contract; + if (Contract) { + const config = await CompactConfig.load(configPath); + contractName = await resolveContractName(Contract, config, config.rootDir); + } + await using deployer = await Deployer.prepare({ + contract: contractName, + network: opts.network ?? argv.network, + configPath, + seedFile: opts.seedFile ?? argv.seedFile, + proofServer: opts.proofServer ?? argv.proofServer, + args: opts.args, + syncTimeoutMs: + syncTimeoutSec !== undefined ? syncTimeoutSec * 1000 : undefined, + skipWalletCache: opts.skipWalletCache ?? argv.noCache, + seedCacheDust: opts.seedCacheFromDust ?? argv.seedCacheFromDust, + seedCacheShielded: + opts.seedCacheFromShielded ?? argv.seedCacheFromShielded, + promptPassphrase: opts.promptPassphrase, + logger, + }); + + const result = dryRun ? await deployer.dryRun() : await deployer.deploy(); + printResult(result, { json, logger }); + return result; + } catch (e) { + handleError(e, { json, verbose, logger }); + throw e; // unreachable — handleError calls process.exit + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +interface ParsedArgv { + network?: string; + configPath?: string; + seedFile?: string; + proofServer?: string; + syncTimeoutSec?: number; + seedCacheFromDust?: string; + seedCacheFromShielded?: string; + noCache: boolean; + dryRun: boolean; + json: boolean; + verbose: boolean; +} + +function parseArgv(argv: string[]): ParsedArgv { + const out: ParsedArgv = { + noCache: false, + dryRun: false, + json: false, + verbose: false, + }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] as string; + switch (arg) { + case '-v': + case '--verbose': + out.verbose = true; + break; + case '--json': + out.json = true; + break; + case '--dry-run': + out.dryRun = true; + break; + case '--no-cache': + out.noCache = true; + break; + case '--seed-cache-from-dust': + out.seedCacheFromDust = expectValue( + argv, + ++i, + '--seed-cache-from-dust', + ); + break; + case '--seed-cache-from-shielded': + out.seedCacheFromShielded = expectValue( + argv, + ++i, + '--seed-cache-from-shielded', + ); + break; + case '--network': + out.network = expectValue(argv, ++i, '--network'); + break; + case '--config': + out.configPath = expectValue(argv, ++i, '--config'); + break; + case '--seed-file': + out.seedFile = expectValue(argv, ++i, '--seed-file'); + break; + case '--proof-server': + out.proofServer = expectValue(argv, ++i, '--proof-server'); + break; + case '--sync-timeout': { + const raw = expectValue(argv, ++i, '--sync-timeout'); + const seconds = Number.parseInt(raw, 10); + if (!Number.isFinite(seconds) || seconds <= 0) { + throw new Error( + `--sync-timeout requires a positive integer (seconds); got "${raw}"`, + ); + } + out.syncTimeoutSec = seconds; + break; + } + default: + // Unknown flags are ignored so the helper coexists with extra + // argv the caller's wrapper script may inject. + break; + } + } + return out; +} + +function expectValue(argv: string[], i: number, flag: string): string { + const v = argv[i]; + if (v === undefined || v.startsWith('-')) { + throw new Error(`${flag} requires a value`); + } + return v; +} + +function buildLogger(opts: { json: boolean; verbose: boolean }): Logger { + // JSON mode keeps stdout machine-parseable, so logger writes to stderr. + // Default mode goes to stdout with the standard pino-pretty fallback. + return pino({ + level: opts.verbose ? 'debug' : 'info', + }); +} + +function printResult( + result: DeployResult, + opts: { json: boolean; logger: Logger }, +): void { + if (opts.json) { + process.stdout.write(`${JSON.stringify(result)}\n`); + return; + } + if (result.dryRun) { + opts.logger.info( + `Dry-run for ${result.contractName} on ${result.network} OK`, + ); + return; + } + opts.logger.info( + `${result.contractName} deployed on ${result.network}: ${result.address}`, + ); + opts.logger.info(` txId: ${result.txId}`); + opts.logger.info(` txHash: ${result.txHash}`); + opts.logger.info(` blockHeight: ${result.blockHeight}`); + opts.logger.info(` saved to: ${result.deploymentsFile}`); + if (result.explorerUrl) { + opts.logger.info(` explorer: ${result.explorerUrl}`); + } +} + +function handleError( + e: unknown, + opts: { json: boolean; verbose: boolean; logger: Logger }, +): never { + const code = e instanceof DeployError ? e.exitCode : 1; + const name = e instanceof Error ? e.name : 'Error'; + const message = e instanceof Error ? e.message : String(e); + if (opts.json) { + process.stdout.write( + `${JSON.stringify({ error: name, message, exitCode: code })}\n`, + ); + } else { + opts.logger.error({ err: message }, `Deploy failed: ${name}`); + if (opts.verbose && e instanceof Error && e.stack) { + opts.logger.debug(e.stack); + } + } + process.exit(code); +} diff --git a/packages/deployer/src/wallet/handler.test.ts b/packages/deployer/src/wallet/handler.test.ts new file mode 100644 index 0000000..25b1840 --- /dev/null +++ b/packages/deployer/src/wallet/handler.test.ts @@ -0,0 +1,529 @@ +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + writeFileSync, +} from 'node:fs'; +import { gzipSync } from 'node:zlib'; +import type { + EnvironmentConfiguration, + MidnightWalletProvider, +} from '@midnight-ntwrk/testkit-js'; +import { + DEFAULT_DUST_OPTIONS, + FluentWalletBuilder, + MidnightWalletProvider as MidnightWalletProviderClass, + WalletFactory, + WalletSaveStateProvider, + WalletSeeds, +} from '@midnight-ntwrk/testkit-js'; +import { createKeystore } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet'; +import type { Logger } from 'pino'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from 'vitest'; +import { WalletHandler } from './handler.ts'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn(() => false), + readFileSync: vi.fn(() => Buffer.alloc(0)), + writeFileSync: vi.fn(), + renameSync: vi.fn(), + copyFileSync: vi.fn(), + mkdirSync: vi.fn(), + }; +}); + +vi.mock('@midnight-ntwrk/testkit-js', () => ({ + DEFAULT_DUST_OPTIONS: { additionalFeeOverhead: 1000n }, + DEFAULT_WALLET_STATE_DIRECTORY: './.states', + FluentWalletBuilder: { forEnvironment: vi.fn() }, + MidnightWalletProvider: { withWallet: vi.fn() }, + WalletFactory: { + createShieldedWallet: vi.fn(() => ({ tag: 'shielded-fresh' })), + createUnshieldedWallet: vi.fn(() => ({ tag: 'unshielded' })), + createDustWallet: vi.fn(() => ({ tag: 'dust' })), + createWalletFacade: vi.fn(async () => ({ tag: 'wallet-facade' })), + restoreShieldedWallet: vi.fn(async () => ({ tag: 'shielded-restored' })), + }, + WalletSaveStateProvider: vi.fn(), + WalletSeeds: { + fromMnemonic: vi.fn(() => ({ + shielded: new Uint8Array(32).fill(0x11), + unshielded: new Uint8Array(32).fill(0x22), + dust: new Uint8Array(32).fill(0x33), + })), + fromMasterSeed: vi.fn(() => ({ + shielded: new Uint8Array(32).fill(0x44), + unshielded: new Uint8Array(32).fill(0x55), + dust: new Uint8Array(32).fill(0x66), + })), + }, +})); + +vi.mock('@midnight-ntwrk/wallet-sdk-unshielded-wallet', () => ({ + createKeystore: vi.fn(() => ({ tag: 'keystore' })), +})); + +vi.mock('@midnight-ntwrk/ledger-v8', () => ({ + ZswapSecretKeys: { fromSeed: vi.fn(() => ({ tag: 'zswap-keys' })) }, + DustSecretKey: { fromSeed: vi.fn(() => ({ tag: 'dust-key' })) }, +})); + +interface FakeProvider { + stop: Mock; + wallet: { shielded: { tag: string } }; +} + +function fakeProvider(opts: { failsOnStop?: boolean } = {}): FakeProvider { + return { + wallet: { shielded: { tag: 'shielded-on-provider' } }, + stop: vi.fn( + opts.failsOnStop + ? async () => { + throw new Error('boom'); + } + : async () => undefined, + ), + }; +} + +interface BuilderChain { + envBuilder: { withDustOptions: Mock; config: unknown }; +} + +function wireTestkitChain(provider: FakeProvider): BuilderChain { + const envBuilder = { + withDustOptions: vi.fn(() => envBuilder), + config: { tag: 'config' }, + }; + vi.mocked(FluentWalletBuilder.forEnvironment).mockReturnValue( + envBuilder as unknown as ReturnType< + typeof FluentWalletBuilder.forEnvironment + >, + ); + vi.mocked(MidnightWalletProviderClass.withWallet).mockResolvedValue( + provider as unknown as MidnightWalletProvider, + ); + return { envBuilder }; +} + +/** Pino-shaped logger whose methods are spies, freshly built per test. */ +function spyLogger(): Logger { + const logger: Record = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + level: 'silent', + }; + logger.child = (): Logger => spyLogger(); + return logger as unknown as Logger; +} + +function fakeEnv( + walletNetworkId: EnvironmentConfiguration['walletNetworkId'] = 'testnet', +): EnvironmentConfiguration { + return { walletNetworkId } as unknown as EnvironmentConfiguration; +} + +describe('WalletHandler', () => { + let logger: Logger; + + beforeEach(() => { + logger = spyLogger(); + vi.mocked(existsSync).mockReturnValue(false); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('seed routing', () => { + it('should route a mnemonic seed through WalletSeeds.fromMnemonic', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { + kind: 'mnemonic', + value: 'abandon abandon abandon', + }); + expect(WalletSeeds.fromMnemonic).toHaveBeenCalledWith( + 'abandon abandon abandon', + ); + expect(WalletSeeds.fromMasterSeed).not.toHaveBeenCalled(); + }); + + it('should route a hex seed through WalletSeeds.fromMasterSeed', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: 'aa'.repeat(32), + }); + expect(WalletSeeds.fromMasterSeed).toHaveBeenCalledWith('aa'.repeat(32)); + expect(WalletSeeds.fromMnemonic).not.toHaveBeenCalled(); + }); + }); + + describe('dust overhead', () => { + it('should override additionalFeeOverhead to a smaller value on non-mainnet networks', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv('preview'), { + kind: 'hex', + value: '00', + }); + // testkit's 5e20 default exceeds a typical preview/preprod wallet's + // dust balance, breaking fee balance. We tune down to 5e14. + expect(WalletFactory.createDustWallet).toHaveBeenCalledWith( + expect.anything(), + expect.any(Uint8Array), + expect.objectContaining({ + additionalFeeOverhead: 500_000_000_000_000n, + }), + ); + }); + + it('should keep the testkit default additionalFeeOverhead on mainnet', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv('mainnet'), { + kind: 'hex', + value: '00', + }); + expect(WalletFactory.createDustWallet).toHaveBeenCalledWith( + expect.anything(), + expect.any(Uint8Array), + expect.objectContaining({ + additionalFeeOverhead: DEFAULT_DUST_OPTIONS.additionalFeeOverhead, + }), + ); + }); + }); + + describe('provider wiring', () => { + it('should expose the wallet built by MidnightWalletProvider.withWallet via .provider', async () => { + const provider = fakeProvider(); + wireTestkitChain(provider); + const handler = await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: '00', + }); + expect(handler.provider).toBe(provider); + }); + + it('should pass the createWalletFacade output to MidnightWalletProvider.withWallet', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { kind: 'hex', value: '00' }); + // The 3rd positional arg to withWallet is the WalletFacade. + const args = vi.mocked(MidnightWalletProviderClass.withWallet).mock + .calls[0]; + expect(args?.[2]).toEqual({ tag: 'wallet-facade' }); + }); + + it('should derive the unshielded keystore from the seed bytes and network id', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv('testnet'), { + kind: 'hex', + value: '00', + }); + expect(createKeystore).toHaveBeenCalledWith( + expect.any(Uint8Array), + 'testnet', + ); + }); + }); + + describe('wallet-state cache', () => { + it('should build the shielded sub-wallet fresh when no cache file exists', async () => { + vi.mocked(existsSync).mockReturnValue(false); + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { kind: 'hex', value: '00' }); + expect(WalletFactory.createShieldedWallet).toHaveBeenCalledTimes(1); + expect(WalletFactory.restoreShieldedWallet).not.toHaveBeenCalled(); + }); + + it('should restore the shielded sub-wallet from cache when the file exists', async () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(WalletSaveStateProvider).mockImplementation( + function (this: object) { + Object.assign(this, { + load: vi.fn(async () => 'serialized-state'), + save: vi.fn(async () => undefined), + }); + } as unknown as new ( + ...args: unknown[] + ) => InstanceType, + ); + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { kind: 'hex', value: '00' }); + expect(WalletFactory.restoreShieldedWallet).toHaveBeenCalledWith( + expect.anything(), + 'serialized-state', + ); + expect(WalletFactory.createShieldedWallet).not.toHaveBeenCalled(); + }); + + it('should skip the cache entirely when skipWalletCache is true', async () => { + vi.mocked(existsSync).mockReturnValue(true); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: '00' }, + { skipWalletCache: true }, + ); + expect(WalletFactory.restoreShieldedWallet).not.toHaveBeenCalled(); + expect(WalletFactory.createShieldedWallet).toHaveBeenCalledTimes(1); + }); + + it('should fall back to a fresh build when restore throws', async () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(WalletSaveStateProvider).mockImplementation( + function (this: object) { + Object.assign(this, { + load: vi.fn(async () => { + throw new Error('corrupt'); + }), + save: vi.fn(async () => undefined), + }); + } as unknown as new ( + ...args: unknown[] + ) => InstanceType, + ); + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv(), { kind: 'hex', value: '00' }); + expect(WalletFactory.createShieldedWallet).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ err: 'corrupt' }), + expect.stringContaining('Wallet cache restore failed'), + ); + }); + + it('should swallow save() failures with a warn log on saveCache()', async () => { + vi.mocked(WalletSaveStateProvider).mockImplementation( + function (this: object) { + Object.assign(this, { + load: vi.fn(), + save: vi.fn(async () => { + throw new Error('disk-full'); + }), + }); + } as unknown as new ( + ...args: unknown[] + ) => InstanceType, + ); + wireTestkitChain(fakeProvider()); + const handler = await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: '00', + }); + await expect(handler.saveCache()).resolves.toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ err: 'disk-full' }), + 'Wallet sub-wallet cache save failed; continuing', + ); + }); + + it('should save the shielded sub-wallet through WalletSaveStateProvider on saveCache()', async () => { + const save = vi.fn(async () => undefined); + vi.mocked(WalletSaveStateProvider).mockImplementation( + function (this: object) { + Object.assign(this, { load: vi.fn(), save }); + } as unknown as new ( + ...args: unknown[] + ) => InstanceType, + ); + const provider = fakeProvider(); + wireTestkitChain(provider); + const handler = await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: '00', + }); + await handler.saveCache(); + expect(save).toHaveBeenCalledWith(provider.wallet.shielded); + }); + }); + + describe('seed cache import', () => { + it('should import a raw-JSON dust source by gzipping into the seed-derived path', async () => { + const raw = Buffer.from('{"state":"raw-json"}', 'utf8'); + vi.mocked(readFileSync).mockReturnValue(raw); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv('preview'), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/path/to/state.json' }, + ); + // Atomic write: payload lands in `.tmp`, then rename to + // ``. Asserts both halves so a future regression that + // skips the rename (or writes directly to target) fails loudly. + expect(writeFileSync).toHaveBeenCalled(); + const [tempPath, payload] = vi.mocked(writeFileSync).mock.calls[0] ?? []; + expect(String(tempPath)).toMatch( + /preview-[0-9a-f]{16}-dust\.gz\.tmp$/, + ); + // Payload was raw → must be gzipped on the way in (magic bytes). + const payloadBuf = payload as Buffer; + expect(payloadBuf[0]).toBe(0x1f); + expect(payloadBuf[1]).toBe(0x8b); + // rename(2) is atomic on POSIX within the same filesystem. + const [renameFrom, renameTo] = + vi.mocked(renameSync).mock.calls[0] ?? []; + expect(String(renameFrom)).toBe(String(tempPath)); + expect(String(renameTo)).toMatch(/preview-[0-9a-f]{16}-dust\.gz$/); + }); + + it('should pass a gzipped dust source through unchanged (no double-gzip)', async () => { + const gzipped = gzipSync(Buffer.from('{"state":"raw-json"}', 'utf8')); + vi.mocked(readFileSync).mockReturnValue(gzipped); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv('preview'), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/path/to/state.gz' }, + ); + const payload = vi.mocked(writeFileSync).mock.calls[0]?.[1]; + expect(payload).toEqual(gzipped); + }); + + it('should ensure the .states/ directory exists before writing', async () => { + vi.mocked(readFileSync).mockReturnValue(Buffer.from('{}', 'utf8')); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/state.json' }, + ); + expect(mkdirSync).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ recursive: true }), + ); + }); + + it('should throw WalletError when the source file is missing', async () => { + vi.mocked(readFileSync).mockImplementationOnce(() => { + throw new Error('ENOENT: no such file'); + }); + wireTestkitChain(fakeProvider()); + await expect( + WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/missing.json' }, + ), + ).rejects.toThrow(/--seed-cache-from-dust:.*missing\.json/); + }); + + it('should back up an existing target cache to .bak before overwriting', async () => { + vi.mocked(readFileSync).mockReturnValue(Buffer.from('{}', 'utf8')); + vi.mocked(existsSync).mockReturnValue(true); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/state.json' }, + ); + // copyFileSync MUST be called with (target, target.bak) so the + // previous cache bytes are preserved forever. If this assertion + // breaks, the safety net we promised the user is gone. + expect(copyFileSync).toHaveBeenCalledTimes(1); + const [src, dest] = vi.mocked(copyFileSync).mock.calls[0] ?? []; + expect(String(src)).toMatch(/-dust\.gz$/); + expect(String(dest)).toMatch(/-dust\.gz\.bak$/); + expect(String(dest)).toBe(`${String(src)}.bak`); + const sawBackupLog = vi + .mocked(logger.info) + .mock.calls.some((c) => + String(c[0]).includes('previous cache backed up to'), + ); + expect(sawBackupLog).toBe(true); + }); + + it('should NOT create a .bak when the target cache does not already exist', async () => { + vi.mocked(readFileSync).mockReturnValue(Buffer.from('{}', 'utf8')); + vi.mocked(existsSync).mockReturnValue(false); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheDust: '/state.json' }, + ); + expect(copyFileSync).not.toHaveBeenCalled(); + }); + + it('should warn and skip the import when --no-cache is also set', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv(), + { kind: 'hex', value: 'aa'.repeat(32) }, + { skipWalletCache: true, seedCacheDust: '/state.json' }, + ); + expect(writeFileSync).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('--seed-cache-from-*'), + ); + }); + + it('should import a shielded source into the matching -shielded.gz path', async () => { + vi.mocked(readFileSync).mockReturnValue(Buffer.from('{}', 'utf8')); + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv('preview'), + { kind: 'hex', value: 'aa'.repeat(32) }, + { seedCacheShielded: '/state.json' }, + ); + // Final atomic rename lands on `-shielded.gz`; the intermediate + // tmp write goes to `-shielded.gz.tmp`. + const renameTo = vi.mocked(renameSync).mock.calls[0]?.[1]; + expect(String(renameTo)).toMatch( + /preview-[0-9a-f]{16}-shielded\.gz$/, + ); + }); + }); + + describe('dispose', () => { + it('should stop the underlying wallet on Symbol.asyncDispose', async () => { + const provider = fakeProvider(); + wireTestkitChain(provider); + const handler = await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: '00', + }); + await handler[Symbol.asyncDispose](); + expect(provider.stop).toHaveBeenCalledTimes(1); + }); + + it('should swallow stop() failures with a warn log on Symbol.asyncDispose', async () => { + const provider = fakeProvider({ failsOnStop: true }); + wireTestkitChain(provider); + const handler = await WalletHandler.build(logger, fakeEnv(), { + kind: 'hex', + value: '00', + }); + await expect(handler[Symbol.asyncDispose]()).resolves.toBeUndefined(); + expect(provider.stop).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ err: 'boom' }), + 'Wallet stop failed', + ); + }); + }); +}); diff --git a/packages/deployer/src/wallet/handler.ts b/packages/deployer/src/wallet/handler.ts new file mode 100644 index 0000000..65ec158 --- /dev/null +++ b/packages/deployer/src/wallet/handler.ts @@ -0,0 +1,464 @@ +import { createHash } from 'node:crypto'; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + writeFileSync, +} from 'node:fs'; +import { resolve } from 'node:path'; +import { gzipSync } from 'node:zlib'; +import { DustSecretKey, ZswapSecretKeys } from '@midnight-ntwrk/ledger-v8'; +import { + DEFAULT_DUST_OPTIONS, + DEFAULT_WALLET_STATE_DIRECTORY, + type DustWalletOptions, + type EnvironmentConfiguration, + FluentWalletBuilder, + MidnightWalletProvider, + WalletFactory, + WalletSaveStateProvider, + WalletSeeds, +} from '@midnight-ntwrk/testkit-js'; +import { + DustWallet, + type DustWalletAPI, +} from '@midnight-ntwrk/wallet-sdk-dust-wallet'; +import type { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade'; +import type { ShieldedWalletAPI } from '@midnight-ntwrk/wallet-sdk-shielded'; +import { + createKeystore, + type UnshieldedKeystore, +} from '@midnight-ntwrk/wallet-sdk-unshielded-wallet'; +import type { Logger } from 'pino'; +import { WalletError } from '../errors.ts'; +import type { WalletSeed } from './seeds.ts'; + +export interface WalletHandlerBuildOptions { + /** Force a fresh sync from genesis (skip the on-disk cache). Default `false`. */ + skipWalletCache?: boolean; + /** + * Import a pre-warmed dust wallet state file into `.states/` before + * the restore path runs. Accepts raw JSON (output of + * `DustWallet.serializeState()`) or a gzipped copy; gzip is detected + * by magic bytes. Overwrites any existing cache for the seed. + * Ignored under {@link skipWalletCache}. + */ + seedCacheDust?: string; + /** Like {@link seedCacheDust} but for the shielded sub-wallet. */ + seedCacheShielded?: string; +} + +/** + * Owned wallet handle: a built `MidnightWalletProvider` plus its + * shielded + dust on-disk caches. Acquire via {@link build} and an + * `AsyncDisposableStack.use()`; call {@link saveCache} after sync. + */ +export class WalletHandler implements AsyncDisposable { + /** The underlying testkit-js wallet provider. */ + readonly provider: MidnightWalletProvider; + /** The unshielded keystore the wallet was built with. */ + readonly unshieldedKeystore: UnshieldedKeystore; + readonly #logger: Logger; + readonly #shieldedCacheFilePath: string; + readonly #dustCacheFilePath: string; + + private constructor( + provider: MidnightWalletProvider, + keystore: UnshieldedKeystore, + logger: Logger, + shieldedCacheFilePath: string, + dustCacheFilePath: string, + ) { + this.provider = provider; + this.unshieldedKeystore = keystore; + this.#logger = logger; + this.#shieldedCacheFilePath = shieldedCacheFilePath; + this.#dustCacheFilePath = dustCacheFilePath; + } + + /** + * Build a `MidnightWalletProvider` with three fixes over the bare + * testkit-js builder: + * 1. Tunes `additionalFeeOverhead` for non-mainnet wallet sizes. + * 2. Routes mnemonic vs hex seed through the right derivation path + * (they derive *different* wallets from the same input). + * 3. Restores the shielded + dust sub-wallets from on-disk cache + * when present (saves the 30–60 min first-preprod sync). + * Caller drives `provider.start()`; call {@link saveCache} post-sync. + */ + static async build( + logger: Logger, + env: EnvironmentConfiguration, + seed: WalletSeed, + opts: WalletHandlerBuildOptions = {}, + ): Promise { + const dustOptions: DustWalletOptions = { + ...DEFAULT_DUST_OPTIONS, + // testkit's 5e20 default exceeds a typical preview/preprod + // wallet's ~3e15 dust, breaking fee balance. 5e14 keeps margin + // without exceeding the balance on non-mainnet networks. + additionalFeeOverhead: + env.walletNetworkId === 'mainnet' + ? DEFAULT_DUST_OPTIONS.additionalFeeOverhead + : 500_000_000_000_000n, + }; + + const walletSeeds: WalletSeeds = + seed.kind === 'mnemonic' + ? WalletSeeds.fromMnemonic(seed.value) + : WalletSeeds.fromMasterSeed(seed.value); + + // testkit-js doesn't export `mapEnvironmentToConfiguration` and + // the `config` field isn't on the .d.ts. Cast through unknown. + const builderForConfig = FluentWalletBuilder.forEnvironment(env); + const config = (builderForConfig as unknown as { config: ConfigShape }) + .config; + + const unshieldedKeystore: UnshieldedKeystore = createKeystore( + walletSeeds.unshielded, + env.walletNetworkId as Parameters[1], + ); + + const shieldedCacheFilePath = computeCacheFilePath( + env, + walletSeeds.shielded, + 'shielded', + ); + const dustCacheFilePath = computeCacheFilePath( + env, + walletSeeds.dust, + 'dust', + ); + + // Pre-warmed cache import: place the user-supplied state file at + // the seed-derived `.states/` path so the existing restore path + // picks it up. Mutual exclusion with `--no-cache` is a warn, not a + // hard error — keeps the flag combinations cheap. + if (opts.skipWalletCache === true) { + if (opts.seedCacheShielded || opts.seedCacheDust) { + logger.warn( + '--seed-cache-from-* is ignored under --no-cache (cache load is disabled)', + ); + } + } else { + if (opts.seedCacheShielded) { + importSeedCache( + logger, + opts.seedCacheShielded, + shieldedCacheFilePath, + 'shielded', + ); + } + if (opts.seedCacheDust) { + importSeedCache( + logger, + opts.seedCacheDust, + dustCacheFilePath, + 'dust', + ); + } + } + + const shieldedWallet = await loadOrCreateShieldedWallet({ + logger, + config, + seed: walletSeeds.shielded, + cacheFilePath: shieldedCacheFilePath, + skipCache: opts.skipWalletCache === true, + }); + + const unshieldedWallet = WalletFactory.createUnshieldedWallet( + config as Parameters[0], + unshieldedKeystore as Parameters< + typeof WalletFactory.createUnshieldedWallet + >[1], + ); + + const dustWallet = await loadOrCreateDustWallet({ + logger, + config, + seed: walletSeeds.dust, + dustOptions, + cacheFilePath: dustCacheFilePath, + skipCache: opts.skipWalletCache === true, + }); + + type CreateFacadeArgs = Parameters; + const walletFacade: WalletFacade = await WalletFactory.createWalletFacade( + config as CreateFacadeArgs[0], + shieldedWallet as CreateFacadeArgs[1], + unshieldedWallet, + dustWallet, + ); + + const provider = await MidnightWalletProvider.withWallet( + logger, + env, + walletFacade, + ZswapSecretKeys.fromSeed(walletSeeds.shielded), + DustSecretKey.fromSeed(walletSeeds.dust), + unshieldedKeystore as Parameters< + typeof MidnightWalletProvider.withWallet + >[5], + ); + + return new WalletHandler( + provider, + unshieldedKeystore, + logger, + shieldedCacheFilePath, + dustCacheFilePath, + ); + } + + /** + * Snapshot the shielded + dust sub-wallets to disk. Best-effort and + * independent per sub-wallet. Both are cached because on real + * networks both are slow on first sync (shielded trial-decrypts every + * note; dust streams the global unfiltered ledger event log). + */ + async saveCache(): Promise { + await Promise.allSettled([ + this.#saveSubWalletCache( + this.#shieldedCacheFilePath, + this.provider.wallet.shielded, + 'shielded', + ), + this.#saveSubWalletCache( + this.#dustCacheFilePath, + this.provider.wallet.dust, + 'dust', + ), + ]); + } + + async #saveSubWalletCache( + filePath: string, + subWallet: unknown, + label: string, + ): Promise { + try { + const dir = pathDir(filePath); + const filename = pathBase(filePath); + // `seed` param only feeds the default filename; we pass an + // explicit one, so the empty string is fine. + const saver = new WalletSaveStateProvider( + this.#logger, + '', + dir, + filename, + ); + await saver.save(subWallet as Parameters[0]); + } catch (e) { + this.#logger.warn( + { err: (e as Error).message, label, filePath }, + 'Wallet sub-wallet cache save failed; continuing', + ); + } + } + + async [Symbol.asyncDispose](): Promise { + try { + await this.provider.stop(); + } catch (e) { + this.#logger.warn({ err: (e as Error).message }, 'Wallet stop failed'); + } + } +} + +// --------------------------------------------------------------------------- +// Internals +// --------------------------------------------------------------------------- + +/** Opaque testkit-js `FluentWalletBuilder.config` (not exported by testkit). */ +type ConfigShape = unknown; + +/** + * Drop a user-supplied wallet-state file into `.states/` under the + * seed-derived filename so the existing restore path picks it up. + * Detects gzip via magic bytes (`0x1f 0x8b`); raw JSON is gzipped on + * the way in. Throws {@link WalletError} on read failure so a bad path + * fails fast instead of silently doing a cold sync from genesis. + * + * Safety guarantees: + * 1. **Source is read-only.** Only `readFileSync` touches `srcPath`. + * 2. **Backup is preserved forever.** If the target `.gz` already + * exists, it is `copyFileSync`'d to `.bak` *before* any + * write — never deleted, never overwritten by this helper. A user + * who hits a bad-format import can roll back with + * `mv .bak ` and re-run. + * 3. **Write is atomic.** New bytes land in `.tmp` first, + * then `rename(2)` over the final path. POSIX rename is atomic + * within the same filesystem, so a mid-write crash can never leave + * the existing cache half-overwritten. A stale `.tmp` left by a + * failed rename is harmless (cache load only scans `.gz`) and gets + * overwritten by the next import attempt. + */ +function importSeedCache( + logger: Logger, + srcPath: string, + targetPath: string, + kind: 'shielded' | 'dust', +): void { + const absoluteSrc = resolve(process.cwd(), srcPath); + let bytes: Buffer; + try { + bytes = readFileSync(absoluteSrc); + } catch (e) { + throw new WalletError( + `--seed-cache-from-${kind}: cannot read ${absoluteSrc}: ${(e as Error).message}`, + ); + } + const isGzipped = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b; + const payload = isGzipped ? bytes : gzipSync(bytes); + const backupPath = `${targetPath}.bak`; + const tempPath = `${targetPath}.tmp`; + mkdirSync(pathDir(targetPath), { recursive: true }); + // Preserve the previous cache forever as `.bak` so a + // bad-format import is always recoverable by hand. We use copy (not + // rename) so the live `` keeps its bytes throughout the + // window before the atomic rename below. + const backedUp = existsSync(targetPath); + if (backedUp) { + copyFileSync(targetPath, backupPath); + } + writeFileSync(tempPath, payload); + renameSync(tempPath, targetPath); + logger.info( + `Imported ${kind} cache: ${absoluteSrc} → ${targetPath}${ + backedUp ? ` (previous cache backed up to ${backupPath})` : '' + }`, + ); +} + +/** + * `--.gz`. Per-kind suffix prevents + * shielded/dust cross-load (different state schemas). Don't reuse + * testkit's helper: it embeds the seed verbatim and gates the + * network on env vars instead of runtime ID. + */ +function computeCacheFilePath( + env: EnvironmentConfiguration, + seed: Uint8Array, + kind: 'shielded' | 'dust', +): string { + const seedHash = createHash('sha256') + .update(seed) + .digest('hex') + .slice(0, 16); + const filename = `${env.walletNetworkId}-${seedHash}-${kind}.gz`; + return resolve(process.cwd(), DEFAULT_WALLET_STATE_DIRECTORY, filename); +} + +/** + * Restore dust wallet from cache, else build fresh. Routes through + * `DustWallet(config).restore(...)` because testkit doesn't expose a + * `WalletFactory.restoreDustWallet`. Caching turns preprod's 1 h+ + * first-run dust sync into seconds on subsequent boots. + */ +async function loadOrCreateDustWallet(args: { + logger: Logger; + config: ConfigShape; + seed: Uint8Array; + dustOptions: DustWalletOptions; + cacheFilePath: string; + skipCache: boolean; +}): Promise { + const { logger, config, seed, dustOptions, cacheFilePath, skipCache } = args; + + if (!skipCache && existsSync(cacheFilePath)) { + try { + const dir = pathDir(cacheFilePath); + const filename = pathBase(cacheFilePath); + const loader = new WalletSaveStateProvider(logger, '', dir, filename); + const serializedState = await loader.load(); + // `costParameters` is runtime state on the builder, not baked + // into the snapshot. Re-apply `dustOptions` so the restored + // wallet honours our `additionalFeeOverhead` override. + const dustConfig = buildDustConfig(config, dustOptions); + const dustClass = DustWallet( + dustConfig as Parameters[0], + ); + const restored = dustClass.restore(serializedState); + logger.info(`Restored dust wallet state from ${cacheFilePath}`); + return restored as unknown as DustWalletAPI; + } catch (e) { + logger.warn( + { err: (e as Error).message, cacheFilePath }, + 'Dust wallet cache restore failed; falling back to fresh sync', + ); + } + } else if (skipCache) { + logger.info('Dust wallet cache disabled (--no-cache); doing fresh sync'); + } + + return WalletFactory.createDustWallet( + config as Parameters[0], + seed, + dustOptions, + ); +} + +async function loadOrCreateShieldedWallet(args: { + logger: Logger; + config: ConfigShape; + seed: Uint8Array; + cacheFilePath: string; + skipCache: boolean; +}): Promise { + const { logger, config, seed, cacheFilePath, skipCache } = args; + + if (!skipCache && existsSync(cacheFilePath)) { + try { + const dir = pathDir(cacheFilePath); + const filename = pathBase(cacheFilePath); + const loader = new WalletSaveStateProvider(logger, '', dir, filename); + const serializedState = await loader.load(); + const restored = await WalletFactory.restoreShieldedWallet( + config as Parameters[0], + serializedState, + ); + logger.info(`Restored wallet state from ${cacheFilePath}`); + return restored as ShieldedWalletAPI; + } catch (e) { + logger.warn( + { err: (e as Error).message, cacheFilePath }, + 'Wallet cache restore failed; falling back to fresh sync', + ); + } + } else if (skipCache) { + logger.info('Wallet cache disabled (--no-cache); doing fresh sync'); + } + + return WalletFactory.createShieldedWallet( + config as Parameters[0], + seed, + ) as ShieldedWalletAPI; +} + +/** Layer `dustOptions` onto the base config so cache-restored wallets honour `additionalFeeOverhead`. */ +function buildDustConfig( + config: ConfigShape, + dustOptions: DustWalletOptions, +): ConfigShape { + return { + ...(config as Record), + costParameters: { + ledgerParams: dustOptions.ledgerParams, + additionalFeeOverhead: dustOptions.additionalFeeOverhead, + feeBlocksMargin: dustOptions.feeBlocksMargin, + }, + } as ConfigShape; +} + +function pathDir(p: string): string { + const i = p.lastIndexOf('/'); + return i === -1 ? '.' : p.slice(0, i); +} + +function pathBase(p: string): string { + const i = p.lastIndexOf('/'); + return i === -1 ? p : p.slice(i + 1); +} diff --git a/packages/deployer/src/wallet/keystore.test.ts b/packages/deployer/src/wallet/keystore.test.ts new file mode 100644 index 0000000..393a28f --- /dev/null +++ b/packages/deployer/src/wallet/keystore.test.ts @@ -0,0 +1,151 @@ +import { mkdtempSync, rmSync, statSync } from 'node:fs'; +import { readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { WalletError } from '../errors.ts'; +import { Keystore, type MidnightKeystore } from './keystore.ts'; + +const FAST_OPTS = { scryptN: 1024, scryptR: 8, scryptP: 1, dklen: 32 }; +const SEED = 'deadbeef'.repeat(8); + +describe('Keystore', () => { + let tmp: string; + + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'keystore-test-')); + }); + + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + describe('encrypt / decrypt', () => { + it('should round-trip a seed through encrypt → decrypt', () => { + const ks = Keystore.encrypt(SEED, 'hunter2', FAST_OPTS); + const json = ks.toJSON(); + expect(json.version).toBe('midnight-1'); + expect(json.crypto.cipher).toBe('aes-128-ctr'); + expect(json.crypto.kdf).toBe('scrypt'); + expect(ks.decrypt('hunter2')).toBe(SEED); + }); + + it('should accept a 0x-prefixed hex seed and round-trip back to unprefixed hex', () => { + const ks = Keystore.encrypt(`0x${SEED}`, 'pw', FAST_OPTS); + expect(ks.decrypt('pw')).toBe(SEED); + }); + + it('should reject a non-hex seed', () => { + expect(() => Keystore.encrypt('not hex!', 'pw', FAST_OPTS)).toThrow( + WalletError, + ); + }); + + it('should reject an odd-length hex seed', () => { + expect(() => Keystore.encrypt('abc', 'pw', FAST_OPTS)).toThrow( + WalletError, + ); + }); + + it('should reject a wrong passphrase with MAC mismatch', () => { + const ks = Keystore.encrypt(SEED, 'hunter2', FAST_OPTS); + expect(() => ks.decrypt('wrong')).toThrow(/MAC mismatch/); + }); + + it('should produce a different ciphertext on each encryption (random salt/iv)', () => { + const a = Keystore.encrypt(SEED, 'pp', FAST_OPTS).toJSON(); + const b = Keystore.encrypt(SEED, 'pp', FAST_OPTS).toJSON(); + expect(a.crypto.ciphertext).not.toBe(b.crypto.ciphertext); + expect(a.crypto.kdfparams.salt).not.toBe(b.crypto.kdfparams.salt); + }); + }); + + describe('toJSON', () => { + it('should expose the full on-disk shape with all crypto fields', () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS); + const json = ks.toJSON(); + expect(json.version).toBe('midnight-1'); + expect(typeof json.id).toBe('string'); + expect(json.crypto.cipher).toBe('aes-128-ctr'); + expect(json.crypto.kdf).toBe('scrypt'); + expect(typeof json.crypto.ciphertext).toBe('string'); + expect(typeof json.crypto.mac).toBe('string'); + expect(typeof json.crypto.cipherparams.iv).toBe('string'); + expect(json.crypto.kdfparams).toMatchObject({ + dklen: 32, + n: 1024, + p: 1, + r: 8, + }); + expect(typeof json.crypto.kdfparams.salt).toBe('string'); + }); + }); + + describe('writeToFile', () => { + it('should write JSON to disk with mode 0o600', async () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS); + const path = join(tmp, 'wallet.json'); + await ks.writeToFile(path); + const st = statSync(path); + // mask out file-type bits, only check perm bits + expect(st.mode & 0o777).toBe(0o600); + const parsed = JSON.parse(await readFile(path, 'utf8')); + expect(parsed.version).toBe('midnight-1'); + }); + }); + + describe('readFromFile', () => { + it('should round-trip through writeToFile + readFromFile + decrypt', async () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS); + const path = join(tmp, 'wallet.json'); + await ks.writeToFile(path); + const loaded = await Keystore.readFromFile(path); + expect(loaded.decrypt('pw')).toBe(SEED); + }); + + it('should wrap fs errors as WalletError', async () => { + await expect( + Keystore.readFromFile(join(tmp, 'does-not-exist.json')), + ).rejects.toThrow(/Failed to read keystore at/); + }); + + it('should reject invalid JSON with WalletError', async () => { + const path = join(tmp, 'bad.json'); + await writeFile(path, '{ not valid json'); + await expect(Keystore.readFromFile(path)).rejects.toThrow( + /Invalid JSON in keystore/, + ); + }); + }); + + describe('fromJSON validation', () => { + it('should reject an unsupported version', () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS); + const tampered = { + ...ks.toJSON(), + version: 'eth-3', + } as unknown as MidnightKeystore; + expect(() => Keystore.fromJSON(tampered)).toThrow( + /Unsupported keystore version/, + ); + }); + + it('should reject an unsupported KDF', () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS).toJSON(); + const tampered = { + ...ks, + crypto: { ...ks.crypto, kdf: 'pbkdf2' }, + } as unknown as MidnightKeystore; + expect(() => Keystore.fromJSON(tampered)).toThrow(/Unsupported KDF/); + }); + + it('should reject an unsupported cipher', () => { + const ks = Keystore.encrypt(SEED, 'pw', FAST_OPTS).toJSON(); + const tampered = { + ...ks, + crypto: { ...ks.crypto, cipher: 'aes-256-gcm' }, + } as unknown as MidnightKeystore; + expect(() => Keystore.fromJSON(tampered)).toThrow(/Unsupported cipher/); + }); + }); +}); diff --git a/packages/deployer/src/wallet/keystore.ts b/packages/deployer/src/wallet/keystore.ts new file mode 100644 index 0000000..9a2a2de --- /dev/null +++ b/packages/deployer/src/wallet/keystore.ts @@ -0,0 +1,202 @@ +/** + * Web3 Secret Storage v3-shaped JSON keystore (scrypt + AES-128-CTR + + * SHA-256 MAC) with a `version: "midnight-1"` marker so future schema + * bumps don't collide with Ethereum tooling that reads v3. + */ + +import { + createCipheriv, + createDecipheriv, + createHash, + randomBytes, + randomUUID, + scryptSync, +} from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { WalletError } from '../errors.ts'; + +const VERSION = 'midnight-1'; + +/** On-disk JSON shape. Exported so consumers can transport keystores verbatim. */ +export interface MidnightKeystore { + version: typeof VERSION; + id: string; + crypto: { + cipher: 'aes-128-ctr'; + ciphertext: string; + cipherparams: { iv: string }; + kdf: 'scrypt'; + kdfparams: { dklen: number; n: number; p: number; r: number; salt: string }; + mac: string; + }; +} + +export interface KeystoreCreateOptions { + scryptN?: number; + scryptP?: number; + scryptR?: number; + dklen?: number; +} + +const DEFAULTS: Required = { + scryptN: 1 << 17, + scryptP: 1, + scryptR: 8, + dklen: 32, +}; + +/** Encrypted wallet-seed wrapper; invariants enforced at construction. */ +export class Keystore { + readonly #data: MidnightKeystore; + + private constructor(data: MidnightKeystore) { + this.#data = data; + } + + /** Encrypt a 32-byte hex seed (with or without `0x`) under `passphrase`. Override {@link DEFAULTS} only for tests that need fast scrypt. */ + static encrypt( + seedHex: string, + passphrase: string, + opts: KeystoreCreateOptions = {}, + ): Keystore { + const seed = seedFromHex(seedHex); + const { scryptN, scryptP, scryptR, dklen } = { ...DEFAULTS, ...opts }; + + const salt = randomBytes(32); + const iv = randomBytes(16); + const derived = scryptSync(Buffer.from(passphrase, 'utf8'), salt, dklen, { + N: scryptN, + p: scryptP, + r: scryptR, + maxmem: 512 * 1024 * 1024, + }); + + const encKey = derived.subarray(0, 16); + const macKey = derived.subarray(16, 32); + + const cipher = createCipheriv('aes-128-ctr', encKey, iv); + const ciphertext = Buffer.concat([cipher.update(seed), cipher.final()]); + const mac = createHash('sha256') + .update(Buffer.concat([macKey, ciphertext])) + .digest(); + + return new Keystore({ + version: VERSION, + id: randomUUID(), + crypto: { + cipher: 'aes-128-ctr', + ciphertext: ciphertext.toString('hex'), + cipherparams: { iv: iv.toString('hex') }, + kdf: 'scrypt', + kdfparams: { + dklen, + n: scryptN, + p: scryptP, + r: scryptR, + salt: salt.toString('hex'), + }, + mac: mac.toString('hex'), + }, + }); + } + + /** Read + parse a JSON keystore file. Validates via {@link Keystore.fromJSON}. */ + static async readFromFile(path: string): Promise { + let raw: string; + try { + raw = await readFile(path, 'utf8'); + } catch (e) { + throw new WalletError( + `Failed to read keystore at ${path}: ${(e as Error).message}`, + ); + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (e) { + throw new WalletError( + `Invalid JSON in keystore ${path}: ${(e as Error).message}`, + ); + } + return Keystore.fromJSON(parsed as MidnightKeystore); + } + + /** Wrap parsed keystore JSON; validates version/cipher/KDF eagerly. */ + static fromJSON(data: MidnightKeystore): Keystore { + if (data.version !== VERSION) { + throw new WalletError( + `Unsupported keystore version: ${data.version} (expected ${VERSION})`, + ); + } + if (data.crypto.kdf !== 'scrypt') { + throw new WalletError( + `Unsupported KDF: ${data.crypto.kdf} (expected scrypt)`, + ); + } + if (data.crypto.cipher !== 'aes-128-ctr') { + throw new WalletError( + `Unsupported cipher: ${data.crypto.cipher} (expected aes-128-ctr)`, + ); + } + return new Keystore(data); + } + + /** Recover the hex-encoded seed. Throws {@link WalletError} on MAC mismatch. */ + decrypt(passphrase: string): string { + const { kdfparams, ciphertext, cipherparams, mac } = this.#data.crypto; + const derived = scryptSync( + Buffer.from(passphrase, 'utf8'), + Buffer.from(kdfparams.salt, 'hex'), + kdfparams.dklen, + { + N: kdfparams.n, + p: kdfparams.p, + r: kdfparams.r, + maxmem: 512 * 1024 * 1024, + }, + ); + const encKey = derived.subarray(0, 16); + const macKey = derived.subarray(16, 32); + + const cipherBytes = Buffer.from(ciphertext, 'hex'); + const expectedMac = createHash('sha256') + .update(Buffer.concat([macKey, cipherBytes])) + .digest('hex'); + if (expectedMac !== mac) { + throw new WalletError( + 'Keystore MAC mismatch (wrong passphrase or corrupted file)', + ); + } + + const decipher = createDecipheriv( + 'aes-128-ctr', + encKey, + Buffer.from(cipherparams.iv, 'hex'), + ); + const plain = Buffer.concat([ + decipher.update(cipherBytes), + decipher.final(), + ]); + return plain.toString('hex'); + } + + /** Write to disk as pretty JSON with mode `0o600`. */ + async writeToFile(path: string): Promise { + await writeFile(path, `${JSON.stringify(this.#data, null, 2)}\n`, { + mode: 0o600, + }); + } + + /** Return the on-disk JSON shape. */ + toJSON(): MidnightKeystore { + return this.#data; + } +} + +function seedFromHex(hex: string): Buffer { + const stripped = hex.startsWith('0x') ? hex.slice(2) : hex; + if (!/^[0-9a-fA-F]+$/.test(stripped) || stripped.length % 2 !== 0) { + throw new WalletError('Seed must be hex-encoded'); + } + return Buffer.from(stripped, 'hex'); +} diff --git a/packages/deployer/src/wallet/seeds.test.ts b/packages/deployer/src/wallet/seeds.test.ts new file mode 100644 index 0000000..709a955 --- /dev/null +++ b/packages/deployer/src/wallet/seeds.test.ts @@ -0,0 +1,269 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { CompactConfig } from '../config/compact-config.ts'; +import type { NetworkConfig } from '../config/schema.ts'; +import { WalletError } from '../errors.ts'; +import { Keystore } from './keystore.ts'; +import { + classifySeed, + LOCAL_PREFUNDED_SEEDS, + localPrefundedSeed, + resolveSeed, +} from './seeds.ts'; + +vi.mock('./keystore.ts', () => ({ + Keystore: { readFromFile: vi.fn() }, +})); + +const HEX_SEED = 'aa'.repeat(32); +const MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +function fakeConfig(rootDir: string, keystore?: string): CompactConfig { + return { + rootDir, + wallet: keystore ? { keystore } : undefined, + } as unknown as CompactConfig; +} + +function fakeNetwork(opts: { local?: { index?: number } } = {}): NetworkConfig { + return { + wallet: opts.local ? { source: 'local', index: opts.local.index ?? 0 } : undefined, + } as unknown as NetworkConfig; +} + +describe('classifySeed', () => { + it('should classify a 64-char hex string as hex (lowercased)', () => { + const hex = 'A'.repeat(64); + expect(classifySeed(hex)).toEqual({ kind: 'hex', value: 'a'.repeat(64) }); + }); + + it('should classify a 128-char hex string as hex', () => { + const hex = `${'0'.repeat(127)}1`; + expect(classifySeed(hex)).toEqual({ kind: 'hex', value: hex }); + }); + + it('should classify a valid BIP39 mnemonic as mnemonic (no conversion)', () => { + expect(classifySeed(MNEMONIC)).toEqual({ + kind: 'mnemonic', + value: MNEMONIC, + }); + }); + + it('should reject empty input', () => { + expect(() => classifySeed(' ')).toThrow(WalletError); + }); + + it('should reject an invalid hex length', () => { + expect(() => classifySeed('abc123')).toThrow(WalletError); + }); + + it('should reject gibberish that is neither hex nor BIP39', () => { + expect(() => classifySeed('this is definitely not valid')).toThrow( + WalletError, + ); + }); +}); + +describe('localPrefundedSeed', () => { + it('should return the prefunded seed at the given index', () => { + for (let i = 0; i < LOCAL_PREFUNDED_SEEDS.length; i++) { + expect(localPrefundedSeed(i)).toBe(LOCAL_PREFUNDED_SEEDS[i]); + } + }); + + it('should throw RangeError when the index is out of range', () => { + expect(() => localPrefundedSeed(99)).toThrow(RangeError); + }); +}); + +describe('resolveSeed', () => { + let rootDir: string; + const originalEnvSeed = process.env.MN_DEPLOYER_SEED; + + beforeEach(() => { + rootDir = mkdtempSync(join(tmpdir(), 'seeds-resolve-')); + delete process.env.MN_DEPLOYER_SEED; + }); + + afterEach(() => { + rmSync(rootDir, { recursive: true, force: true }); + vi.clearAllMocks(); + if (originalEnvSeed === undefined) { + delete process.env.MN_DEPLOYER_SEED; + } else { + process.env.MN_DEPLOYER_SEED = originalEnvSeed; + } + }); + + describe('--seed-file branch', () => { + it('should read seed from a relative seedFile path under rootDir', async () => { + writeFileSync(join(rootDir, 'seed.hex'), `${HEX_SEED}\n`); + const result = await resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + seedFile: 'seed.hex', + }); + expect(result).toEqual({ + seed: { kind: 'hex', value: HEX_SEED }, + origin: 'cli', + }); + }); + + it('should read seed from an absolute seedFile path unchanged', async () => { + const abs = join(rootDir, 'abs-seed.hex'); + writeFileSync(abs, HEX_SEED); + const result = await resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + seedFile: abs, + }); + expect(result.origin).toBe('cli'); + expect(result.seed).toEqual({ kind: 'hex', value: HEX_SEED }); + }); + + it('should wrap fs errors as WalletError with the --seed-file label', async () => { + await expect( + resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + seedFile: 'does-not-exist.hex', + }), + ).rejects.toThrow(/Failed to read --seed-file/); + }); + }); + + describe('MN_DEPLOYER_SEED branch', () => { + it('should return env seed with origin=env when set', async () => { + process.env.MN_DEPLOYER_SEED = HEX_SEED; + const result = await resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + }); + expect(result).toEqual({ + seed: { kind: 'hex', value: HEX_SEED }, + origin: 'env', + }); + }); + + it('should ignore a whitespace-only env value', async () => { + process.env.MN_DEPLOYER_SEED = ' '; + await expect( + resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + }), + ).rejects.toThrow(WalletError); + }); + }); + + describe('keystore branch', () => { + it('should throw WalletError when the keystore file does not exist', async () => { + await expect( + resolveSeed({ + config: fakeConfig(rootDir, 'missing-keystore.json'), + networkName: 'testnet', + network: fakeNetwork(), + promptPassphrase: async () => 'pw', + }), + ).rejects.toThrow(/Keystore file not found:/); + }); + + it('should throw WalletError when keystore is configured but no passphrase prompt provided', async () => { + const ksPath = join(rootDir, 'keystore.json'); + writeFileSync(ksPath, '{}'); + await expect( + resolveSeed({ + config: fakeConfig(rootDir, 'keystore.json'), + networkName: 'testnet', + network: fakeNetwork(), + }), + ).rejects.toThrow(/no passphrase prompt provided/); + }); + + it('should decrypt the keystore and return origin=keystore on the happy path', async () => { + const ksPath = join(rootDir, 'keystore.json'); + writeFileSync(ksPath, '{}'); + const decrypt = vi.fn(() => HEX_SEED); + vi.mocked(Keystore.readFromFile).mockResolvedValue({ + decrypt, + } as unknown as Keystore); + const prompt = vi.fn(async () => 'hunter2'); + const result = await resolveSeed({ + config: fakeConfig(rootDir, 'keystore.json'), + networkName: 'testnet', + network: fakeNetwork(), + promptPassphrase: prompt, + }); + expect(Keystore.readFromFile).toHaveBeenCalledWith(ksPath); + expect(prompt).toHaveBeenCalledWith(ksPath); + expect(decrypt).toHaveBeenCalledWith('hunter2'); + expect(result).toEqual({ + seed: { kind: 'hex', value: HEX_SEED }, + origin: 'keystore', + }); + }); + }); + + describe('local prefunded branch', () => { + it('should return the indexed local prefunded seed when networkName=local and source=local', async () => { + const result = await resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'local', + network: fakeNetwork({ local: { index: 2 } }), + }); + expect(result.origin).toBe('local'); + // index 2 is a 64-char hex seed in LOCAL_PREFUNDED_SEEDS + expect(result.seed.kind).toBe('hex'); + expect(result.seed.value).toBe(LOCAL_PREFUNDED_SEEDS[2]); + }); + + it('should default to index 0 (the mnemonic) when no index is configured', async () => { + const result = await resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'local', + network: fakeNetwork({ local: {} }), + }); + expect(result.origin).toBe('local'); + expect(result.seed.kind).toBe('mnemonic'); + }); + }); + + describe('no source available', () => { + it('should throw WalletError with the help message when no seed source is configured', async () => { + await expect( + resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'testnet', + network: fakeNetwork(), + }), + ).rejects.toThrow( + /Provide --seed-file, set MN_DEPLOYER_SEED, or configure \[wallet\].keystore/, + ); + }); + + it('should NOT fall into the local branch when networkName is local but no wallet source is set', async () => { + await expect( + resolveSeed({ + config: fakeConfig(rootDir), + networkName: 'local', + network: fakeNetwork(), + }), + ).rejects.toThrow(WalletError); + }); + }); +}); diff --git a/packages/deployer/src/wallet/seeds.ts b/packages/deployer/src/wallet/seeds.ts new file mode 100644 index 0000000..535f767 --- /dev/null +++ b/packages/deployer/src/wallet/seeds.ts @@ -0,0 +1,141 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { isAbsolute, resolve } from 'node:path'; +import { TEST_MNEMONIC } from '@midnight-ntwrk/testkit-js'; +import { validateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import type { CompactConfig } from '../config/compact-config.ts'; +import type { NetworkConfig } from '../config/schema.ts'; +import { WalletError } from '../errors.ts'; +import { Keystore } from './keystore.ts'; + +// --- Local prefunded seeds (dev-preset midnight-node) --- + +/** + * Prefunded wallets on `midnight-node --preset=dev`. Slot 0 is the testkit-js + * `TEST_MNEMONIC`; slots 1..4 are the hex seeds from `LocalTestEnvironment`. + */ +export const LOCAL_PREFUNDED_SEEDS: readonly string[] = [ + TEST_MNEMONIC, + '0000000000000000000000000000000000000000000000000000000000000001', + '0000000000000000000000000000000000000000000000000000000000000002', + '0000000000000000000000000000000000000000000000000000000000000003', + '0000000000000000000000000000000000000000000000000000000000000004', +] as const; + +export function localPrefundedSeed(index: number): string { + const seed = LOCAL_PREFUNDED_SEEDS[index]; + if (!seed) { + throw new RangeError( + `local wallet index ${index} out of range (0..${LOCAL_PREFUNDED_SEEDS.length - 1})`, + ); + } + return seed; +} + +// --- Classify: raw string → discriminated WalletSeed --- + +/** + * The wallet builder derives *different* wallets from `.withSeed(hex)` vs + * `.withMnemonic(phrase)` for the same entropy, so we keep the kind + * explicit through the resolve chain. + */ +export type WalletSeed = + | { kind: 'hex'; value: string } + | { kind: 'mnemonic'; value: string }; + +export function classifySeed(input: string): WalletSeed { + const trimmed = input.trim(); + if (!trimmed) { + throw new WalletError('Seed cannot be empty'); + } + if ( + /^[0-9a-fA-F]+$/.test(trimmed) && + (trimmed.length === 64 || trimmed.length === 128) + ) { + return { kind: 'hex', value: trimmed.toLowerCase() }; + } + if (validateMnemonic(trimmed, wordlist)) { + return { kind: 'mnemonic', value: trimmed }; + } + throw new WalletError( + 'Invalid seed: expected a 64/128-char hex string or a valid BIP39 mnemonic (12 or 24 words).', + ); +} + +// --- Resolve: pick a seed from the precedence chain --- + +/** + * Precedence: `--seed-file` > `MN_DEPLOYER_SEED` > `[wallet].keystore` + * (passphrase-prompted) > `[networks.local].wallet.source = "local"`. + * Throws {@link WalletError} when none match. + */ +export interface SeedResolution { + seed: WalletSeed; + origin: 'cli' | 'env' | 'keystore' | 'local'; +} + +export interface ResolveOptions { + config: CompactConfig; + networkName: string; + network: NetworkConfig; + seedFile?: string; + promptPassphrase?: (path: string) => Promise; +} + +export async function resolveSeed( + opts: ResolveOptions, +): Promise { + const { rootDir } = opts.config; + if (opts.seedFile) { + const path = absoluteUnder(rootDir, opts.seedFile); + const raw = await safeRead(path, '--seed-file'); + return { seed: classifySeed(raw), origin: 'cli' }; + } + + const envSeed = process.env.MN_DEPLOYER_SEED; + if (envSeed?.trim()) { + return { seed: classifySeed(envSeed), origin: 'env' }; + } + + const keystorePath = opts.config.wallet?.keystore; + if (keystorePath) { + const path = absoluteUnder(rootDir, keystorePath); + if (!existsSync(path)) { + throw new WalletError(`Keystore file not found: ${path}`); + } + if (!opts.promptPassphrase) { + throw new WalletError( + 'Keystore configured but no passphrase prompt provided', + ); + } + const ks = await Keystore.readFromFile(path); + const passphrase = await opts.promptPassphrase(path); + return { seed: classifySeed(ks.decrypt(passphrase)), origin: 'keystore' }; + } + + if (opts.networkName === 'local' && opts.network.wallet?.source === 'local') { + return { + seed: classifySeed(localPrefundedSeed(opts.network.wallet.index ?? 0)), + origin: 'local', + }; + } + + throw new WalletError( + `No deployer seed for network "${opts.networkName}". Provide --seed-file, set MN_DEPLOYER_SEED, or configure [wallet].keystore in compact.toml.`, + ); +} + +function absoluteUnder(root: string, p: string): string { + return isAbsolute(p) ? p : resolve(root, p); +} + +async function safeRead(path: string, label: string): Promise { + try { + return await readFile(path, 'utf8'); + } catch (e) { + throw new WalletError( + `Failed to read ${label} (${path}): ${(e as Error).message}`, + ); + } +} diff --git a/packages/deployer/tsconfig.json b/packages/deployer/tsconfig.json new file mode 100644 index 0000000..d46d4e6 --- /dev/null +++ b/packages/deployer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"], + "declaration": true, + "skipLibCheck": true, + "sourceMap": true, + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/packages/deployer/vitest.config.ts b/packages/deployer/vitest.config.ts new file mode 100644 index 0000000..8b2eb8b --- /dev/null +++ b/packages/deployer/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + reporters: 'verbose', + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'json-summary'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/index.ts'], + thresholds: { + statements: 95, + branches: 90, + functions: 100, + lines: 95, + }, + }, + }, +}); diff --git a/tests/integrations/.gitignore b/tests/integrations/.gitignore new file mode 100644 index 0000000..e3222f7 --- /dev/null +++ b/tests/integrations/.gitignore @@ -0,0 +1,3 @@ +fixtures/artifacts/ +deployments/ +logs/ diff --git a/tests/integrations/README.md b/tests/integrations/README.md new file mode 100644 index 0000000..23febc7 --- /dev/null +++ b/tests/integrations/README.md @@ -0,0 +1,81 @@ +# compact-tools — integration tests + +End-to-end tests for `@openzeppelin/compact-deployer` against a real local Midnight stack (proof-server + indexer + node, Docker). + +## Layout + +``` +tests/integrations/ + local-env.yml # Docker compose: proof-server + indexer + node + vitest.config.ts # Vitest config (forks pool, long timeouts) + compact.toml # Deployer config; paths resolve to this dir + _harness/ # Shared setup: walletPool, network, paths, … + fixtures/ + Counter.compact # Minimal one-circuit fixture + PrivateCounter.compact # Witness + private-state fixture + signingkeys/ # Per-contract CMA keys (test-only) + initstates/ # init_private_state JSON seeds + witnesses/ # TS witness modules (resolved at deploy time) + artifacts/ # Output of compact-compiler (gitignored) + specs/ + deploy/ # deploy, dry-run, history rotation/isolation, + # proof-server auto, async-dispose, PrivateCounter + wallet/ # wallet pool, lifecycle, keystore+passphrase + errors/ # config-error surface +``` + +All orchestration (env-up / env-down / compile / test-integration) lives in +the top-level `/Makefile`; this directory holds only the test sources, +fixtures, and config. + +This is **not** a workspace package. The root `package.json` adds `@openzeppelin/compact-deployer` as a dev dep (resolved via yarn workspaces), and the root `test:integration` script invokes vitest pointed at this folder. + +## Run + +From the repo root (`compact-tools/`): + +```bash +make build # build compact-deployer +make test-integration # env-up → compile → test → env-down +``` + +`make test-integration` is fully self-contained: it brings the docker +stack up, compiles the fixture contracts, runs the specs, and tears the +stack down at the end. Teardown is wired via a `trap … EXIT INT TERM` +inside the Makefile recipe so it fires even when the tests fail or +you `Ctrl+C` out. + +`yarn test:integration` is kept as a thin wrapper around the same +Make target so the CI invocation surface stays consistent with the +other yarn scripts. + +### Iterative dev (skip the up/down cycle) + +For fast inner-loop work (editing a spec and re-running) the up/down +dance is wasted time. Bring the stack up once, then call vitest +directly: + +```bash +make env-up # one-time +make compile # idempotent; no-op if sources unchanged +yarn vitest run --config tests/integrations/vitest.config.ts +make env-down # when you're done iterating +``` + +## What's covered + +- **dry-run** — loads + validates the config without submitting a tx. +- **deploy** — deploys Counter to the local stack; verifies returned address, txHash, blockHeight, signingKey, and the persisted `deployments/compact/local.json` record. +- **history rotation** — redeploying rotates the previous head into `local.history.json`. +- **proof_server auto** — `proof_server = "auto"` (or `--proof-server auto`) boots a `DynamicProofServerContainer` for the duration of the deploy and disposes it on exit. +- **async-dispose cleanup** — a failure mid-prepare (after the proof server starts) is unwound via `AsyncDisposableStack`; the next deploy still works. +- **wallet lifecycle** — `Deployer.prepare` doesn't call `wallet.stop()` on dispose when `walletProvider` is injected (caller-owned). +- **history isolation** — Counter and SecondaryCounter share an artifact but maintain independent head/history slots per contract name. +- **keystore + passphrase** — `[wallet].keystore` configured in `compact.toml` resolves the seed via the `promptPassphrase` callback; wrong/missing passphrase fails with `WalletError`. +- **PrivateCounter** — exercises the `init_private_state` and `witnesses = { module, export }` resolution paths end-to-end. + +## Notes + +- Uses the canonical genesis-funded seed `0x…0001` via `[networks.local].wallet = { source = "local", index = 0 }`. +- The CMA signing key in `fixtures/signingkeys/Counter.signingkey` is a fixed test value. Never use it for real deploys. +- The `deployments/` directory is wiped between test runs to keep specs hermetic. diff --git a/tests/integrations/_harness/deployer.ts b/tests/integrations/_harness/deployer.ts new file mode 100644 index 0000000..d309f3b --- /dev/null +++ b/tests/integrations/_harness/deployer.ts @@ -0,0 +1,61 @@ +import { + inMemoryPrivateStateProvider, + syncWallet, +} from '@midnight-ntwrk/testkit-js'; +import { Deployer, type DeployResult } from '@openzeppelin/compact-deployer'; +import { testLogger } from './logger.ts'; +import { localNetworkConfig, setupLocalNetwork } from './network.ts'; +import { CONFIG_PATH } from './paths.ts'; +import { getSharedPool, type PoolAlias } from './walletPool.ts'; + +/** + * Fresh `inMemoryPrivateStateProvider` per call so each integration + * deploy gets an isolated private-state store. Avoids the fcntl LOCK + * contention `levelPrivateStateProvider` causes when the wallet pool + * keeps multiple testkit-js wallets alive in the same process — they + * already share the `midnight-level-db/` dir, and adding a deploy-side + * Level handle on top reliably triggers `LEVEL_LOCKED`. + */ +export function harnessPrivateStateProvider() { + return inMemoryPrivateStateProvider(); +} + +/** + * Deploy `Counter` against the local stack using the wallet at `alias`. + * + * Each spec is expected to call `deployFixture` with its own alias so + * the Deployer always reuses the same wallet for multiple deploys + * within that spec. Sharing one wallet across multiple `deploy` calls + * keeps its UTXO view internally consistent — a fresh + * `WalletHandler.build` per deploy syncs from the indexer (which may + * lag) and can occasionally see an already-spent dust UTXO, producing + * a `DustDoubleSpend` rejection on submission. + * + * Wallet lifecycle is owned by the shared pool: built and started on + * first use, stopped via `resetSharedPool()` once at end-of-suite. + */ +export async function deployFixture( + contract: 'Counter' | 'SecondaryCounter' | 'PrivateCounter', + alias: PoolAlias, + overrides: { dryRun?: boolean; proofServer?: string } = {}, +): Promise { + setupLocalNetwork(); + const wallet = await getSharedPool(localNetworkConfig()).signerFor(alias); + // Wait for the wallet's UTXO view to catch up to the chain head before + // submitting another deploy. Without this, rapid back-to-back deploys + // with the same alias (e.g. spec A → spec B both using BOB) see + // already-spent dust UTXOs and fail with `SubmissionError`. The wallet + // pool keeps one wallet per alias alive for the whole suite, so its + // sync state drifts as other specs deploy. + await syncWallet(wallet.wallet); + await using deployer = await Deployer.prepare({ + contract, + network: 'local', + configPath: CONFIG_PATH, + logger: testLogger(), + walletProvider: wallet, + proofServer: overrides.proofServer, + privateStateProvider: harnessPrivateStateProvider(), + }); + return overrides.dryRun ? deployer.dryRun() : deployer.deploy(); +} diff --git a/tests/integrations/_harness/logger.ts b/tests/integrations/_harness/logger.ts new file mode 100644 index 0000000..5bb20a4 --- /dev/null +++ b/tests/integrations/_harness/logger.ts @@ -0,0 +1,15 @@ +import pino, { type Logger } from 'pino'; + +let sharedLogger: Logger | undefined; + +/** + * Process-shared pino logger, level controlled by `LOG_LEVEL` (defaults to + * `warn` to keep test output clean). Specs that want chatty output can run + * `LOG_LEVEL=debug yarn test:integration`. + */ +export function testLogger(): Logger { + if (!sharedLogger) { + sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); + } + return sharedLogger; +} diff --git a/tests/integrations/_harness/network.ts b/tests/integrations/_harness/network.ts new file mode 100644 index 0000000..9e5c708 --- /dev/null +++ b/tests/integrations/_harness/network.ts @@ -0,0 +1,42 @@ +import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import type { EnvironmentConfiguration } from '@midnight-ntwrk/testkit-js'; + +/** + * Local-stack network identifier. The dev-preset `midnight-node` boots with + * this id; every wallet/provider in the suite must agree. + */ +export const LOCAL_NETWORK_ID = 'undeployed'; + +/** + * Endpoints for the local stack brought up by `make env-up`. Each one is + * overridable via a `MIDNIGHT_*` env var so the same harness can be pointed + * at a relocated stack (e.g. a remote CI runner). + */ +export function localNetworkConfig(): EnvironmentConfiguration { + return { + walletNetworkId: LOCAL_NETWORK_ID, + networkId: LOCAL_NETWORK_ID, + indexer: + process.env.MIDNIGHT_INDEXER_URL ?? + 'http://127.0.0.1:8088/api/v4/graphql', + indexerWS: + process.env.MIDNIGHT_INDEXER_WS_URL ?? + 'ws://127.0.0.1:8088/api/v4/graphql/ws', + node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', + nodeWS: process.env.MIDNIGHT_NODE_WS_URL ?? 'ws://127.0.0.1:9944', + proofServer: + process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', + faucet: undefined, + }; +} + +/** + * Set the process-wide network id once before any provider/wallet is built. + * Idempotent. + */ +let networkIdSet = false; +export function setupLocalNetwork(): void { + if (networkIdSet) return; + setNetworkId(LOCAL_NETWORK_ID); + networkIdSet = true; +} diff --git a/tests/integrations/_harness/paths.ts b/tests/integrations/_harness/paths.ts new file mode 100644 index 0000000..7067248 --- /dev/null +++ b/tests/integrations/_harness/paths.ts @@ -0,0 +1,46 @@ +import { existsSync, rmSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HARNESS_DIR = dirname(fileURLToPath(import.meta.url)); +const INTEGRATION_DIR = resolve(HARNESS_DIR, '..'); + +export const CONFIG_PATH = resolve(INTEGRATION_DIR, 'compact.toml'); +export const ARTIFACT_DIR = resolve( + INTEGRATION_DIR, + 'fixtures/artifacts/Counter', +); +export const PRIVATE_COUNTER_ARTIFACT_DIR = resolve( + INTEGRATION_DIR, + 'fixtures/artifacts/PrivateCounter', +); +export const DEPLOYMENTS_DIR = resolve(INTEGRATION_DIR, 'deployments/compact'); + +/** Throw with a helpful hint if the fixture hasn't been compiled yet. */ +export function requireFixtureArtifact(): void { + if (existsSync(ARTIFACT_DIR)) return; + throw new Error( + `Missing compiled artifact at ${ARTIFACT_DIR}.\n` + + 'Run `make -C tests/integrations compile` first.', + ); +} + +/** + * Same hint shape as {@link requireFixtureArtifact} but for the + * `PrivateCounter` fixture used by the private-state and + * witnesses-module specs. + */ +export function requirePrivateCounterArtifact(): void { + if (existsSync(PRIVATE_COUNTER_ARTIFACT_DIR)) return; + throw new Error( + `Missing compiled artifact at ${PRIVATE_COUNTER_ARTIFACT_DIR}.\n` + + 'Run `make -C tests/integrations compile` first.', + ); +} + +/** Reset the deployments directory between specs. */ +export function wipeDeployments(): void { + if (existsSync(DEPLOYMENTS_DIR)) { + rmSync(DEPLOYMENTS_DIR, { recursive: true, force: true }); + } +} diff --git a/tests/integrations/_harness/teardown.ts b/tests/integrations/_harness/teardown.ts new file mode 100644 index 0000000..16248fc --- /dev/null +++ b/tests/integrations/_harness/teardown.ts @@ -0,0 +1,12 @@ +import { resetSharedPool } from './walletPool.ts'; + +/** + * Vitest `globalSetup` hook. The returned function runs once after the + * suite, stopping every wallet built across all specs so the process + * exits cleanly. + */ +export default function globalSetup(): () => Promise { + return async () => { + await resetSharedPool(); + }; +} diff --git a/tests/integrations/_harness/walletPool.ts b/tests/integrations/_harness/walletPool.ts new file mode 100644 index 0000000..ca145f7 --- /dev/null +++ b/tests/integrations/_harness/walletPool.ts @@ -0,0 +1,100 @@ +import { + type EnvironmentConfiguration, + type MidnightWalletProvider, + TEST_MNEMONIC, +} from '@midnight-ntwrk/testkit-js'; +import { classifySeed, WalletHandler } from '@openzeppelin/compact-deployer'; +import { testLogger } from './logger.ts'; + +/** + * Aliases mapped to seeds prefunded by `midnight-node --preset=dev`. + * + * - `DEPLOYER` uses `TEST_MNEMONIC`, the canonical `abandon × 23 diesel` + * BIP39 phrase recognised by the dev preset as the genesis-funded + * account. Routed through `FluentWalletBuilder.withMnemonic`. + * - `ALICE`/`BOB`/`CHARLIE`/`DAVE` map to the hex seeds the standalone + * testkit exposes via `LocalTestEnvironment.genesisMintWalletSeed`. + * Routed through `FluentWalletBuilder.withSeed`. + */ +export const PREFUNDED_SEEDS = { + DEPLOYER: TEST_MNEMONIC, + ALICE: '0000000000000000000000000000000000000000000000000000000000000001', + BOB: '0000000000000000000000000000000000000000000000000000000000000002', + CHARLIE: '0000000000000000000000000000000000000000000000000000000000000003', + DAVE: '0000000000000000000000000000000000000000000000000000000000000004', +} as const; + +export type PoolAlias = keyof typeof PREFUNDED_SEEDS; + +/** + * Process-shared pool of test wallets keyed by alias. + * + * Wallet startup (`build` + sync) is the slowest part of the suite, so the + * pool caches one promise per alias. `signerFor()` is safe to call from + * `beforeAll` in every spec — repeated calls return the same warm wallet. + * Specs that need wallet isolation can construct their own pool instance. + */ +export class WalletPool { + private cache = new Map>(); + + constructor(private readonly env: EnvironmentConfiguration) {} + + async signerFor(alias: PoolAlias): Promise { + return (await this.ownedFor(alias)).provider; + } + + private ownedFor(alias: PoolAlias): Promise { + const seedString = PREFUNDED_SEEDS[alias]; + if (seedString === undefined) { + throw new Error( + `WalletPool: unknown alias '${alias}'. Available: ${Object.keys(PREFUNDED_SEEDS).join(', ')}`, + ); + } + const cached = this.cache.get(alias); + if (cached) return cached; + + const built = (async () => { + const owned = await WalletHandler.build( + testLogger(), + this.env, + classifySeed(seedString), + ); + await owned.provider.start(true); + return owned; + })(); + this.cache.set(alias, built); + return built; + } + + /** Stop every cached wallet and clear the cache. Call from `afterAll()`. */ + async reset(): Promise { + const entries = Array.from(this.cache.values()); + this.cache.clear(); + await Promise.all( + entries.map(async (p) => { + try { + await (await p)[Symbol.asyncDispose](); + } catch { + /* ignore stop errors during teardown */ + } + }), + ); + } +} + +let sharedPool: WalletPool | undefined; + +/** + * Process-singleton pool. First call builds it against `env`; subsequent + * calls return the cached instance. Reset via `resetSharedPool()`. + */ +export function getSharedPool(env: EnvironmentConfiguration): WalletPool { + if (!sharedPool) sharedPool = new WalletPool(env); + return sharedPool; +} + +export async function resetSharedPool(): Promise { + if (!sharedPool) return; + await sharedPool.reset(); + sharedPool = undefined; +} diff --git a/tests/integrations/compact.toml b/tests/integrations/compact.toml new file mode 100644 index 0000000..366a494 --- /dev/null +++ b/tests/integrations/compact.toml @@ -0,0 +1,47 @@ +# Integration-test deployer config. All paths resolve against this file's dir. + +[profile] +default_network = "local" +artifacts_dir = "fixtures/artifacts" +deployments_dir = "deployments/compact" + +[networks.local] +network_id = "undeployed" +indexer = "http://127.0.0.1:8088/api/v4/graphql" +indexer_ws = "ws://127.0.0.1:8088/api/v4/graphql/ws" +node = "http://127.0.0.1:9944" +node_ws = "ws://127.0.0.1:9944" +proof_server = "http://127.0.0.1:6300" +wallet = { source = "local", index = 0 } +faucet = false + +[contracts.Counter] +artifact = "Counter" +signing_key_file = "fixtures/signingkeys/Counter.signingkey" + +# Alias used by the history-isolation spec: reuses the Counter artifact +# but registers under a distinct name so deployments/.json can +# show the two contracts maintain independent head/history slots. +[contracts.SecondaryCounter] +artifact = "Counter" +signing_key_file = "fixtures/signingkeys/SecondaryCounter.signingkey" + +# Intentionally broken artifact reference used by the async-dispose +# spec: prepare passes config + signing-key validation and boots the +# proof server, then fails at `Artifact.load` so the test can prove the +# `AsyncDisposableStack` cleans up everything acquired so far. +[contracts.MissingArtifact] +artifact = "DoesNotExist" +signing_key_file = "fixtures/signingkeys/Counter.signingkey" + +# Exercises the deployer's `init_private_state` + `witnesses = { module, export }` +# resolution paths end-to-end. The JSON file seeds the initial private +# state; the TS module supplies the witness implementations resolved via +# Node's dynamic `import()`. Requires `make compile` to produce +# `fixtures/artifacts/PrivateCounter/`. +[contracts.PrivateCounter] +artifact = "PrivateCounter" +signing_key_file = "fixtures/signingkeys/PrivateCounter.signingkey" +private_state_id = "private-counter-state" +init_private_state = { file = "fixtures/initstates/PrivateCounter.json" } +witnesses = { module = "fixtures/witnesses/PrivateCounter.witness.ts", export = "PrivateCounterWitnesses" } diff --git a/tests/integrations/fixtures/Counter.compact b/tests/integrations/fixtures/Counter.compact new file mode 100644 index 0000000..4a5959a --- /dev/null +++ b/tests/integrations/fixtures/Counter.compact @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Integration-test fixture for @openzeppelin/compact-deploy. +// Minimal single-circuit counter. + +pragma language_version >= 0.21.0; + +import CompactStandardLibrary; + +export ledger round: Counter; + +export circuit increment(): [] { + round.increment(1); +} diff --git a/tests/integrations/fixtures/PrivateCounter.compact b/tests/integrations/fixtures/PrivateCounter.compact new file mode 100644 index 0000000..3c4a7f8 --- /dev/null +++ b/tests/integrations/fixtures/PrivateCounter.compact @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Integration-test fixture for @openzeppelin/compact-deployer. +// +// Demonstrates two compact features the deployer must support end-to-end +// that the minimal Counter fixture does not exercise: +// +// 1. **Witnesses.** The `.compact` declares only the signature; the +// implementation lives in a TS module, resolved at deploy time via +// `[contracts.PrivateCounter].witnesses = { module, export }` in +// `compact.toml`. +// +// 2. **Private state.** The witness's TS impl reads from +// `context.privateState`, seeded at deploy time by the deployer's +// `init_private_state = { file = "..." }` path. +// +// Single circuit `applyDelta()`: reads the secret delta from private +// state and adds it to the on-chain `publicSum` counter. `disclose()` +// is required to write a witness value to the public ledger. + +pragma language_version >= 0.21.0; + +import CompactStandardLibrary; + +export ledger publicSum: Counter; + +// `Counter.increment` from the standard library expects `Uint<16>`, so +// the witness must match. The JSON seed value (7n) fits trivially. +witness secret_delta(): Uint<16>; + +export circuit applyDelta(): [] { + const delta = secret_delta(); + publicSum.increment(disclose(delta)); +} diff --git a/tests/integrations/fixtures/initstates/PrivateCounter.json b/tests/integrations/fixtures/initstates/PrivateCounter.json new file mode 100644 index 0000000..ffeea05 --- /dev/null +++ b/tests/integrations/fixtures/initstates/PrivateCounter.json @@ -0,0 +1,3 @@ +{ + "delta": "7n" +} diff --git a/tests/integrations/fixtures/signingkeys/Counter.signingkey b/tests/integrations/fixtures/signingkeys/Counter.signingkey new file mode 100644 index 0000000..9c442d4 --- /dev/null +++ b/tests/integrations/fixtures/signingkeys/Counter.signingkey @@ -0,0 +1 @@ +0000000000000000000000000000000000000000000000000000000000000042 diff --git a/tests/integrations/fixtures/signingkeys/PrivateCounter.signingkey b/tests/integrations/fixtures/signingkeys/PrivateCounter.signingkey new file mode 100644 index 0000000..e1d63f1 --- /dev/null +++ b/tests/integrations/fixtures/signingkeys/PrivateCounter.signingkey @@ -0,0 +1 @@ +0000000000000000000000000000000000000000000000000000000000000044 diff --git a/tests/integrations/fixtures/signingkeys/SecondaryCounter.signingkey b/tests/integrations/fixtures/signingkeys/SecondaryCounter.signingkey new file mode 100644 index 0000000..d6c00af --- /dev/null +++ b/tests/integrations/fixtures/signingkeys/SecondaryCounter.signingkey @@ -0,0 +1 @@ +0000000000000000000000000000000000000000000000000000000000000043 diff --git a/tests/integrations/fixtures/witnesses/PrivateCounter.witness.ts b/tests/integrations/fixtures/witnesses/PrivateCounter.witness.ts new file mode 100644 index 0000000..b7d1fb1 --- /dev/null +++ b/tests/integrations/fixtures/witnesses/PrivateCounter.witness.ts @@ -0,0 +1,33 @@ +/** + * Witness module for the `PrivateCounter` fixture, resolved at deploy + * time via `[contracts.PrivateCounter].witnesses = { module, export }` + * in `compact.toml`. Mirrors the pattern in + * `packages/simulator/test/fixtures/sample-contracts/witnesses/`. + * + * The deployer's loader calls `PrivateCounterWitnesses()` (with no + * type-argument; generics are erased at runtime) and uses the returned + * object to satisfy the `secret_delta` declaration in + * `PrivateCounter.compact`. Each witness returns + * `[updatedPrivateState, value]` per Compact's witness ABI. + */ +import type { WitnessContext } from '@midnight-ntwrk/compact-runtime'; + +export type PrivateCounterState = { + /** Secret value the circuit reads via `secret_delta()`. */ + delta: bigint; +}; + +export interface IPrivateCounterWitnesses { + secret_delta(context: WitnessContext): [P, bigint]; +} + +export const PrivateCounterWitnesses = (): IPrivateCounterWitnesses< + L, + PrivateCounterState +> => ({ + secret_delta( + context: WitnessContext, + ): [PrivateCounterState, bigint] { + return [context.privateState, context.privateState.delta]; + }, +}); diff --git a/tests/integrations/local-env.yml b/tests/integrations/local-env.yml new file mode 100644 index 0000000..3969782 --- /dev/null +++ b/tests/integrations/local-env.yml @@ -0,0 +1,61 @@ +# WARNING: Insecure default credentials below. For local development only — do not use in production. +services: + proof-server: + image: 'midnightntwrk/proof-server:latest' + command: ['midnight-proof-server -v'] + ports: + - '6300:6300' + environment: + RUST_BACKTRACE: 'full' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + + indexer: + image: 'midnightntwrk/indexer-standalone:latest' + ports: + - '8088:8088' + environment: + RUST_LOG: 'indexer=info,chain_indexer=info,indexer_api=info,wallet_indexer=info,indexer_common=info,info' + APP__INFRA__NODE__URL: 'ws://node:9944' + APP__APPLICATION__NETWORK_ID: 'undeployed' + APP__INFRA__STORAGE__PASSWORD: 'indexer' + APP__INFRA__PUB_SUB__PASSWORD: 'indexer' + APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' + APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' + healthcheck: + test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + depends_on: + node: + condition: service_healthy + logging: + driver: local + options: + max-size: '10m' + max-file: '3' + + node: + image: 'midnightntwrk/midnight-node:0.22.2' + ports: + - '9944:9944' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] + interval: 2s + timeout: 5s + retries: 20 + start_period: 5s + environment: + CFG_PRESET: 'dev' + SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' + logging: + driver: local + options: + max-size: '10m' + max-file: '3' diff --git a/tests/integrations/specs/deploy/asyncDisposeCleanup.spec.ts b/tests/integrations/specs/deploy/asyncDisposeCleanup.spec.ts new file mode 100644 index 0000000..603c50f --- /dev/null +++ b/tests/integrations/specs/deploy/asyncDisposeCleanup.spec.ts @@ -0,0 +1,72 @@ +import { + ArtifactNotFoundError, + Deployer, +} from '@openzeppelin/compact-deployer'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { testLogger } from '../../_harness/logger.ts'; +import { + localNetworkConfig, + setupLocalNetwork, +} from '../../_harness/network.ts'; +import { + CONFIG_PATH, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; +import { getSharedPool } from '../../_harness/walletPool.ts'; + +/** + * Spec: `Deployer.prepare` accumulates owned resources into a local + * `AsyncDisposableStack`. On failure mid-prepare — here, the + * `MissingArtifact` contract whose artifact directory doesn't exist — + * the stack must dispose everything acquired so far (notably the + * `"auto"` proof-server container). + * + * Externally we verify two things: + * 1. The expected `ArtifactNotFoundError` propagates out. + * 2. A subsequent successful deploy works against an `auto` proof + * server, which would fail if the previous container were stuck + * on the underlying port. + * + * SKIPPED — same upstream `testkit-js` issue as `proofServerAuto.spec.ts`: + * the underlying `DynamicProofServerContainer.start` never gets past + * its log-wait strategy, so the test fails with that error before + * reaching the `ArtifactNotFoundError` we want to assert. Re-enable + * once `testkit-js`'s wait strategy is updated. + */ +describe.skip('compact-deploy — resource cleanup on mid-prepare failure', () => { + beforeAll(() => { + setupLocalNetwork(); + requireFixtureArtifact(); + wipeDeployments(); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should throw ArtifactNotFoundError when the artifact directory is missing', async () => { + const wallet = await getSharedPool(localNetworkConfig()).signerFor('DAVE'); + + await expect( + Deployer.prepare({ + contract: 'MissingArtifact', + network: 'local', + configPath: CONFIG_PATH, + logger: testLogger(), + walletProvider: wallet, + proofServer: 'auto', + }), + ).rejects.toThrow(ArtifactNotFoundError); + }, 240_000); + + it('should leave the proof-server slot reusable for the next deploy', async () => { + // If the auto container leaked, this would either fail to start a + // fresh container or the deploy would hang waiting on the dead one. + const result = await deployFixture('Counter', 'DAVE', { + proofServer: 'auto', + }); + expect(result.address).toMatch(/^[0-9a-f]+$/i); + }, 240_000); +}); diff --git a/tests/integrations/specs/deploy/deploy.spec.ts b/tests/integrations/specs/deploy/deploy.spec.ts new file mode 100644 index 0000000..063dce9 --- /dev/null +++ b/tests/integrations/specs/deploy/deploy.spec.ts @@ -0,0 +1,54 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + DEPLOYMENTS_DIR, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: a fresh `compact-deploy` invocation puts Counter on the local + * chain and writes a complete deployment record. Exercises the full + * pipeline end-to-end against the live Midnight stack. + */ +describe('compact-deploy — Counter deploys to local stack', () => { + beforeAll(() => { + requireFixtureArtifact(); + wipeDeployments(); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should return an address, txHash, signingKey, and block height', async () => { + const result = await deployFixture('Counter', 'DEPLOYER'); + + expect(result.dryRun).toBe(false); + expect(result.contractName).toBe('Counter'); + expect(result.network).toBe('local'); + expect(result.address).toMatch(/^[0-9a-f]+$/i); + expect(result.txId).toMatch(/^[0-9a-f]+$/i); + expect(result.txHash).toMatch(/^[0-9a-f]+$/i); + expect(result.blockHeight).toBeGreaterThan(0); + expect(result.signingKey).toMatch(/^[0-9a-f]{64}$/); + expect(result.deployer).toBeTruthy(); + }); + + it('should persist the deployment record at deployments/compact/local.json', async () => { + const headPath = resolve(DEPLOYMENTS_DIR, 'local.json'); + expect(existsSync(headPath)).toBe(true); + + const head = JSON.parse(await readFile(headPath, 'utf8')); + expect(head.Counter).toBeDefined(); + expect(head.Counter.address).toMatch(/^[0-9a-f]+$/i); + expect(head.Counter.txHash).toMatch(/^[0-9a-f]+$/i); + expect(head.Counter.signingKey).toMatch(/^[0-9a-f]{64}$/); + expect(head.Counter.deployer).toBeTruthy(); + expect(head.Counter.artifact).toBe('Counter'); + expect(head.Counter.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); +}); diff --git a/tests/integrations/specs/deploy/dryRun.spec.ts b/tests/integrations/specs/deploy/dryRun.spec.ts new file mode 100644 index 0000000..64c9158 --- /dev/null +++ b/tests/integrations/specs/deploy/dryRun.spec.ts @@ -0,0 +1,42 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + DEPLOYMENTS_DIR, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: `--dry-run` performs every validation step (config, artifact, + * wallet seed, providers) without submitting a transaction. No + * deployments file should be written. + */ +describe('compact-deploy — --dry-run validates without submitting', () => { + beforeAll(() => { + requireFixtureArtifact(); + wipeDeployments(); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should return dryRun=true and an empty address', async () => { + const result = await deployFixture('Counter', 'ALICE', { dryRun: true }); + + expect(result.dryRun).toBe(true); + expect(result.address).toBe(''); + expect(result.contractName).toBe('Counter'); + expect(result.network).toBe('local'); + expect(result.signingKey).toMatch(/^[0-9a-f]{64}$/); + }); + + it('should not write a deployments file', () => { + expect(existsSync(resolve(DEPLOYMENTS_DIR, 'local.json'))).toBe(false); + expect(existsSync(resolve(DEPLOYMENTS_DIR, 'local.history.json'))).toBe( + false, + ); + }); +}); diff --git a/tests/integrations/specs/deploy/historyIsolation.spec.ts b/tests/integrations/specs/deploy/historyIsolation.spec.ts new file mode 100644 index 0000000..f18fe32 --- /dev/null +++ b/tests/integrations/specs/deploy/historyIsolation.spec.ts @@ -0,0 +1,69 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + DEPLOYMENTS_DIR, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: `Deployments.record` rotates the head into history per + * **(contract, network)** pair. Deploying a different contract on the + * same network must not touch the first contract's history. + * + * Counter and SecondaryCounter share the same compiled artifact but + * register under distinct names in `compact.toml`, so they have + * independent head/history slots in `local.json` / `local.history.json`. + */ +describe('compact-deploy — history rotates per contract, not per network', () => { + let firstCounterAddress: string; + let secondaryAddress: string; + let secondCounterAddress: string; + + beforeAll(async () => { + requireFixtureArtifact(); + wipeDeployments(); + + firstCounterAddress = (await deployFixture('Counter', 'ALICE')).address; + secondaryAddress = (await deployFixture('SecondaryCounter', 'ALICE')) + .address; + secondCounterAddress = (await deployFixture('Counter', 'ALICE')).address; + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should produce distinct addresses for each deploy', () => { + const seen = new Set([ + firstCounterAddress, + secondaryAddress, + secondCounterAddress, + ]); + expect(seen.size).toBe(3); + }); + + it('should keep both contracts at the head of local.json', async () => { + const headPath = resolve(DEPLOYMENTS_DIR, 'local.json'); + expect(existsSync(headPath)).toBe(true); + + const head = JSON.parse(await readFile(headPath, 'utf8')); + expect(head.Counter.address).toBe(secondCounterAddress); + expect(head.SecondaryCounter.address).toBe(secondaryAddress); + }); + + it('should rotate only Counter into history, leaving SecondaryCounter untouched', async () => { + const historyPath = resolve(DEPLOYMENTS_DIR, 'local.history.json'); + expect(existsSync(historyPath)).toBe(true); + + const history = JSON.parse(await readFile(historyPath, 'utf8')); + expect(Array.isArray(history.Counter)).toBe(true); + expect(history.Counter.length).toBe(1); + expect(history.Counter[0].address).toBe(firstCounterAddress); + + expect(history.SecondaryCounter).toBeUndefined(); + }); +}); diff --git a/tests/integrations/specs/deploy/historyRotation.spec.ts b/tests/integrations/specs/deploy/historyRotation.spec.ts new file mode 100644 index 0000000..f52dd2c --- /dev/null +++ b/tests/integrations/specs/deploy/historyRotation.spec.ts @@ -0,0 +1,52 @@ +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + DEPLOYMENTS_DIR, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: redeploying the same contract rotates the previous head into + * `.history.json`. Verifies the persist module's append-to-history + * behaviour against real deploy results. + */ +describe('compact-deploy — redeploy rotates head into history', () => { + let firstAddress: string; + let secondAddress: string; + + beforeAll(async () => { + requireFixtureArtifact(); + wipeDeployments(); + firstAddress = (await deployFixture('Counter', 'BOB')).address; + secondAddress = (await deployFixture('Counter', 'BOB')).address; + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should produce distinct addresses on each deploy', () => { + expect(firstAddress).not.toBe(secondAddress); + expect(firstAddress).toMatch(/^[0-9a-f]+$/i); + expect(secondAddress).toMatch(/^[0-9a-f]+$/i); + }); + + it('should keep the latest deployment at the head', async () => { + const head = JSON.parse( + await readFile(resolve(DEPLOYMENTS_DIR, 'local.json'), 'utf8'), + ); + expect(head.Counter.address).toBe(secondAddress); + }); + + it('should move the previous head into .history.json', async () => { + const history = JSON.parse( + await readFile(resolve(DEPLOYMENTS_DIR, 'local.history.json'), 'utf8'), + ); + expect(Array.isArray(history.Counter)).toBe(true); + expect(history.Counter.length).toBeGreaterThanOrEqual(1); + expect(history.Counter[0].address).toBe(firstAddress); + }); +}); diff --git a/tests/integrations/specs/deploy/privateCounter.spec.ts b/tests/integrations/specs/deploy/privateCounter.spec.ts new file mode 100644 index 0000000..3c04e1e --- /dev/null +++ b/tests/integrations/specs/deploy/privateCounter.spec.ts @@ -0,0 +1,67 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import type { DeployResult } from '@openzeppelin/compact-deployer'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + DEPLOYMENTS_DIR, + requirePrivateCounterArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: the `PrivateCounter` fixture exercises two deploy-pipeline + * paths the minimal `Counter` doesn't: + * + * 1. **`init_private_state`** — the deployer's + * `executeDeploy` includes `privateStateId` + `initialPrivateState` + * in the contract-deploy options. The JSON file ships + * `{ delta: 7n }` (bigint-revived) as the seed. + * + * 2. **Witnesses-module resolution** — `compact.toml` references + * `witnesses = { module = "...PrivateCounter.witness.ts", export = + * "PrivateCounterWitnesses" }`. `Artifact.load` resolves the export + * via Node's dynamic `import()`, calls the factory, and threads the + * impls into the compiled contract. + * + * Both are implicit: a successful deploy means both code paths ran. We + * also re-read the on-disk deployment record to lock in the persistence + * contract for a private-state contract. + * + * Prereq: `make -C tests/integrations compile` must have produced the + * `PrivateCounter` artifact directory. + */ +describe('compact-deploy — PrivateCounter exercises private-state + witnesses-module paths', () => { + let result: DeployResult; + + beforeAll(async () => { + requirePrivateCounterArtifact(); + wipeDeployments(); + result = await deployFixture('PrivateCounter', 'CHARLIE'); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should deploy successfully with init_private_state + witnesses-module configured', () => { + expect(result.dryRun).toBe(false); + expect(result.contractName).toBe('PrivateCounter'); + expect(result.network).toBe('local'); + expect(result.address).toMatch(/^[0-9a-f]+$/i); + expect(result.txHash).toMatch(/^[0-9a-f]+$/i); + expect(result.blockHeight).toBeGreaterThan(0); + expect(result.signingKey).toMatch(/^[0-9a-f]{64}$/); + }, 240_000); + + it('should record the deployment under PrivateCounter in local.json', async () => { + const headPath = resolve(DEPLOYMENTS_DIR, 'local.json'); + expect(existsSync(headPath)).toBe(true); + + const head = JSON.parse(await readFile(headPath, 'utf8')); + expect(head.PrivateCounter).toBeDefined(); + expect(head.PrivateCounter.address).toBe(result.address); + expect(head.PrivateCounter.artifact).toBe('PrivateCounter'); + }); +}); diff --git a/tests/integrations/specs/deploy/proofServerAuto.spec.ts b/tests/integrations/specs/deploy/proofServerAuto.spec.ts new file mode 100644 index 0000000..1e684a4 --- /dev/null +++ b/tests/integrations/specs/deploy/proofServerAuto.spec.ts @@ -0,0 +1,54 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { deployFixture } from '../../_harness/deployer.ts'; +import { + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; + +/** + * Spec: `proof_server = "auto"` (or CLI `--proof-server auto`) boots a + * `DynamicProofServerContainer` for the duration of the deploy and + * disposes it on `Deployer[Symbol.asyncDispose]`. + * + * The deploy succeeding end-to-end is sufficient proof: prepare boots + * the container, the deploy submits through it, then `await using` + * stops it. A leaked container would surface in a later run as a + * health-check failure or port collision. + * + * SKIPPED — upstream `testkit-js` issue: + * `DynamicProofServerContainer.start` waits for a log line matching + * the regex `.*Started.*` that the current + * `midnightntwrk/proof-server:latest` image no longer emits. The + * container exits without ever producing that marker, so + * `testcontainers` throws "Log stream ended and message was not + * received". Re-enable once `testkit-js`'s wait strategy is updated + * (or once we override it locally). + */ +describe.skip('compact-deploy — proof_server = "auto" boots and disposes a container', () => { + beforeAll(() => { + requireFixtureArtifact(); + wipeDeployments(); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should boot a dynamic proof-server container and deploy successfully', async () => { + const result = await deployFixture('Counter', 'CHARLIE', { + proofServer: 'auto', + }); + + expect(result.dryRun).toBe(false); + expect(result.address).toMatch(/^[0-9a-f]+$/i); + expect(result.txHash).toMatch(/^[0-9a-f]+$/i); + expect(result.blockHeight).toBeGreaterThan(0); + }, 240_000); + + it('should leave no zombie container — a subsequent "auto" deploy still works', async () => { + const result = await deployFixture('Counter', 'CHARLIE', { + proofServer: 'auto', + }); + expect(result.address).toMatch(/^[0-9a-f]+$/i); + }, 240_000); +}); diff --git a/tests/integrations/specs/errors/errors.spec.ts b/tests/integrations/specs/errors/errors.spec.ts new file mode 100644 index 0000000..1e29fc2 --- /dev/null +++ b/tests/integrations/specs/errors/errors.spec.ts @@ -0,0 +1,47 @@ +import { ConfigError, Deployer } from '@openzeppelin/compact-deployer'; +import { describe, expect, it } from 'vitest'; +import { testLogger } from '../../_harness/logger.ts'; +import { CONFIG_PATH, requireFixtureArtifact } from '../../_harness/paths.ts'; + +/** + * Spec: Deployer.prepare surfaces typed `ConfigError`s for foreseeable + * user mistakes, with messages that name the offending key/value. These + * run against the live stack but never get past the config-validation + * phase, so they're fast. + */ +describe('compact-deploy — config errors are typed and actionable', () => { + it('should reject an unknown contract name', async () => { + requireFixtureArtifact(); + await expect( + Deployer.prepare({ + contract: 'Nonexistent', + network: 'local', + configPath: CONFIG_PATH, + logger: testLogger(), + }), + ).rejects.toThrow(ConfigError); + }); + + it('should reject an unknown network name', async () => { + requireFixtureArtifact(); + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'unknown-network', + configPath: CONFIG_PATH, + logger: testLogger(), + }), + ).rejects.toThrow(ConfigError); + }); + + it('should reject a missing compact.toml path', async () => { + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: '/nonexistent/compact.toml', + logger: testLogger(), + }), + ).rejects.toThrow(ConfigError); + }); +}); diff --git a/tests/integrations/specs/wallet/keystorePassphrase.spec.ts b/tests/integrations/specs/wallet/keystorePassphrase.spec.ts new file mode 100644 index 0000000..fd429bf --- /dev/null +++ b/tests/integrations/specs/wallet/keystorePassphrase.spec.ts @@ -0,0 +1,133 @@ +import { + mkdtempSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Deployer, Keystore, WalletError } from '@openzeppelin/compact-deployer'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { harnessPrivateStateProvider } from '../../_harness/deployer.ts'; +import { testLogger } from '../../_harness/logger.ts'; +import { + localNetworkConfig, + setupLocalNetwork, +} from '../../_harness/network.ts'; + +/** + * Spec: the `[wallet].keystore` path in `compact.toml` resolves the + * deployer seed via an encrypted JSON keystore. `Deployer.prepare` + * invokes the user-supplied `promptPassphrase` callback exactly once + * with the keystore's absolute path; the decrypted seed builds the + * wallet just like a `--seed-file` would. + * + * Every other integration spec injects `walletProvider` and skips + * `resolveSeed` — this is the only spec that exercises the + * keystore-resolution path end-to-end against the live stack. + */ +const HARNESS_DIR = dirname(fileURLToPath(import.meta.url)); +const INTEGRATION_DIR = resolve(HARNESS_DIR, '..', '..'); +const FIXTURES_ARTIFACTS = resolve(INTEGRATION_DIR, 'fixtures/artifacts'); +const FIXTURES_SIGNING_KEY = resolve( + INTEGRATION_DIR, + 'fixtures/signingkeys/Counter.signingkey', +); + +// ALICE's prefunded seed — picked so the wallet built from the keystore +// has the dev-preset's genesis balance and can submit a deploy. +const ALICE_SEED = + '0000000000000000000000000000000000000000000000000000000000000001'; +const PASSPHRASE = 'hunter2-keystore-spec'; +// Scrypt parameters relaxed for test speed; the real CLI uses defaults +// (~1s derivation). Matches the convention from +// `wallet/keystore.test.ts`. +const FAST_SCRYPT = { scryptN: 1024, scryptR: 8, scryptP: 1, dklen: 32 }; + +describe('compact-deploy — [wallet].keystore resolves via promptPassphrase', () => { + let tmpRoot: string; + let tomlPath: string; + let keystorePath: string; + + beforeAll(() => { + setupLocalNetwork(); + tmpRoot = mkdtempSync(join(tmpdir(), 'keystore-spec-')); + keystorePath = join(tmpRoot, 'wallet.keystore.json'); + + const ks = Keystore.encrypt(ALICE_SEED, PASSPHRASE, FAST_SCRYPT); + writeFileSync(keystorePath, JSON.stringify(ks.toJSON())); + + const net = localNetworkConfig(); + tomlPath = join(tmpRoot, 'compact.toml'); + writeFileSync( + tomlPath, + [ + '[profile]', + 'default_network = "local"', + `artifacts_dir = "${FIXTURES_ARTIFACTS}"`, + `deployments_dir = "${join(tmpRoot, 'deployments')}"`, + '', + '[networks.local]', + 'network_id = "undeployed"', + `indexer = "${net.indexer}"`, + `indexer_ws = "${net.indexerWS}"`, + `node = "${net.node}"`, + `node_ws = "${net.nodeWS}"`, + `proof_server = "${net.proofServer}"`, + 'faucet = false', + '', + '[wallet]', + 'keystore = "wallet.keystore.json"', + '', + '[contracts.Counter]', + 'artifact = "Counter"', + `signing_key_file = "${FIXTURES_SIGNING_KEY}"`, + '', + ].join('\n'), + ); + }); + + afterAll(() => { + if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true }); + }); + + it('should invoke promptPassphrase with the absolute keystore path', async () => { + const promptPassphrase = vi.fn(async () => PASSPHRASE); + + await using deployer = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: tomlPath, + logger: testLogger(), + promptPassphrase, + privateStateProvider: harnessPrivateStateProvider(), + }); + + expect(deployer.contractName).toBe('Counter'); + expect(promptPassphrase).toHaveBeenCalledOnce(); + expect(promptPassphrase).toHaveBeenCalledWith(keystorePath); + }, 240_000); + + it('should reject when the keystore is configured but no promptPassphrase is provided', async () => { + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: tomlPath, + logger: testLogger(), + }), + ).rejects.toThrow(WalletError); + }, 60_000); + + it('should reject when the passphrase is wrong (MAC mismatch)', async () => { + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: tomlPath, + logger: testLogger(), + promptPassphrase: async () => 'definitely-not-the-passphrase', + }), + ).rejects.toThrow(/MAC mismatch/); + }, 60_000); +}); diff --git a/tests/integrations/specs/wallet/walletLifecycle.spec.ts b/tests/integrations/specs/wallet/walletLifecycle.spec.ts new file mode 100644 index 0000000..c98bc0b --- /dev/null +++ b/tests/integrations/specs/wallet/walletLifecycle.spec.ts @@ -0,0 +1,82 @@ +import { Deployer } from '@openzeppelin/compact-deployer'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { + deployFixture, + harnessPrivateStateProvider, +} from '../../_harness/deployer.ts'; +import { testLogger } from '../../_harness/logger.ts'; +import { + localNetworkConfig, + setupLocalNetwork, +} from '../../_harness/network.ts'; +import { + CONFIG_PATH, + requireFixtureArtifact, + wipeDeployments, +} from '../../_harness/paths.ts'; +import { getSharedPool } from '../../_harness/walletPool.ts'; + +/** + * Spec: when `walletProvider` is injected into `Deployer.prepare`, the + * deployer treats the wallet as caller-owned — no `wallet.start()` at + * acquire-time, no `wallet.stop()` on dispose. This is the contract the + * integration suite relies on so a single pool wallet can drive many + * back-to-back deploys without losing UTXO continuity. + */ +describe('compact-deploy — injected wallets are not touched by the deployer', () => { + beforeAll(() => { + setupLocalNetwork(); + requireFixtureArtifact(); + wipeDeployments(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + afterAll(() => { + wipeDeployments(); + }); + + it('should not call wallet.stop() when the deployer is disposed', async () => { + const wallet = await getSharedPool(localNetworkConfig()).signerFor('ALICE'); + const stopSpy = vi.spyOn(wallet, 'stop'); + + { + await using deployer = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: CONFIG_PATH, + logger: testLogger(), + walletProvider: wallet, + privateStateProvider: harnessPrivateStateProvider(), + }); + // Deploy isn't needed to verify the lifecycle contract — preparing + // and disposing is enough. We just want to confirm dispose doesn't + // tear down the caller-owned wallet. + expect(deployer.contractName).toBe('Counter'); + } + + expect(stopSpy).not.toHaveBeenCalled(); + }, 240_000); + + it('should leave the injected wallet usable after dispose', async () => { + const wallet = await getSharedPool(localNetworkConfig()).signerFor('BOB'); + + { + await using deployer = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: CONFIG_PATH, + logger: testLogger(), + walletProvider: wallet, + privateStateProvider: harnessPrivateStateProvider(), + }); + expect(deployer.contractName).toBe('Counter'); + } + + expect(typeof wallet.getCoinPublicKey()).toBe('string'); + const followUp = await deployFixture('Counter', 'BOB'); + expect(followUp.address).toMatch(/^[0-9a-f]+$/i); + }, 240_000); +}); diff --git a/tests/integrations/specs/wallet/walletPool.spec.ts b/tests/integrations/specs/wallet/walletPool.spec.ts new file mode 100644 index 0000000..494233b --- /dev/null +++ b/tests/integrations/specs/wallet/walletPool.spec.ts @@ -0,0 +1,60 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { + localNetworkConfig, + setupLocalNetwork, +} from '../../_harness/network.ts'; +import { + getSharedPool, + type PoolAlias, + PREFUNDED_SEEDS, + resetSharedPool, +} from '../../_harness/walletPool.ts'; + +/** + * Spec: every alias in `PREFUNDED_SEEDS` (DEPLOYER via TEST_MNEMONIC, the + * four hex-seed accounts) is genesis-funded on the dev-preset node, so the + * pool can hand out a synced wallet for each without needing the faucet. + * + * This is the property `compact-deploy`'s local resolution depends on — + * if it breaks (e.g. a new node release changes the prefunded set), every + * other spec will start failing with InsufficientFunds. + */ +describe('compact-deploy — prefunded wallet pool', () => { + beforeAll(() => { + setupLocalNetwork(); + }); + + afterAll(async () => { + await resetSharedPool(); + }); + + const aliases = Object.keys(PREFUNDED_SEEDS) as PoolAlias[]; + + it.each( + aliases, + )('should build a synced, funded wallet for %s', async (alias) => { + const pool = getSharedPool(localNetworkConfig()); + const wallet = await pool.signerFor(alias); + + const coinPublicKey = wallet.getCoinPublicKey(); + expect(typeof coinPublicKey).toBe('string'); + expect((coinPublicKey as unknown as string).length).toBeGreaterThan(0); + + const encryptionPublicKey = wallet.getEncryptionPublicKey(); + expect(typeof encryptionPublicKey).toBe('string'); + }, 180_000); + + it('should return the same wallet instance for repeated `signerFor` calls', async () => { + const pool = getSharedPool(localNetworkConfig()); + const a = await pool.signerFor('ALICE'); + const b = await pool.signerFor('ALICE'); + expect(a).toBe(b); + }); + + it('should produce distinct addresses for distinct aliases', async () => { + const pool = getSharedPool(localNetworkConfig()); + const alice = await pool.signerFor('ALICE'); + const bob = await pool.signerFor('BOB'); + expect(alice.getCoinPublicKey()).not.toBe(bob.getCoinPublicKey()); + }); +}); diff --git a/tests/integrations/vitest.config.ts b/tests/integrations/vitest.config.ts new file mode 100644 index 0000000..0874945 --- /dev/null +++ b/tests/integrations/vitest.config.ts @@ -0,0 +1,18 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: dirname(fileURLToPath(import.meta.url)), + test: { + include: ['specs/**/*.spec.ts'], + testTimeout: 240_000, + hookTimeout: 300_000, + teardownTimeout: 60_000, + pool: 'forks', + poolOptions: { + forks: { singleFork: true }, + }, + globalSetup: ['./_harness/teardown.ts'], + }, +}); diff --git a/turbo.json b/turbo.json index 08dd369..4baf411 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,19 @@ "outputs": [], "cache": false }, + "coverage": { + "dependsOn": ["^build"], + "env": ["COMPACT_HOME"], + "inputs": [ + "src/**/*.ts", + "src/**/*.compact", + "test/**/*.ts", + "vitest.config.ts", + "package.json" + ], + "outputs": ["coverage/**"], + "cache": false + }, "build": { "dependsOn": ["^build"], "env": ["COMPACT_HOME"], diff --git a/yarn.lock b/yarn.lock index 9352729..d58675a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,86 @@ __metadata: version: 8 cacheKey: 10 +"@apollo/client@npm:^4.1.6": + version: 4.2.0 + resolution: "@apollo/client@npm:4.2.0" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@wry/caches": "npm:^1.0.0" + "@wry/equality": "npm:^0.5.6" + "@wry/trie": "npm:^0.5.0" + graphql-tag: "npm:^2.12.6" + optimism: "npm:^0.18.0" + tslib: "npm:^2.3.0" + peerDependencies: + graphql: ^16.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + rxjs: ^7.3.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: 10/fceef4fdeb0780fe91dd95ccd9fea9b56710698b154a1bc6b843ade8df39475d5133eb918705152459ec54f618bd420008e1a0c46012ae8ae7d62bc1e619359e + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-string-parser@npm:7.27.1" + checksum: 10/0ae29cc2005084abdae2966afdb86ed14d41c9c37db02c3693d5022fba9f5d59b011d039380b8e537c34daf117c549f52b452398f576e908fb9db3c7abbb3a00 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 10/8e5d9b0133702cfacc7f368bf792f0f8ac0483794877c6dca5fcb73810ee138e27527701826fb58a40a004f3a5ec0a2f3c3dd5e326d262530b119918f3132ba7 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.29.3": + version: 7.29.3 + resolution: "@babel/parser@npm:7.29.3" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/10e8f34e0fdaa495b9db8be71f4eb29b16d8a57e0818c1bb1c4084015b0383803fd77812ed41597760cbf3d9ab3ae9f4af54f39ff5e5d8e081ba43593232f0ca + languageName: node + linkType: hard + +"@babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10/bfc2b211210f3894dcd7e6a33b2d1c32c93495dc1e36b547376aa33441abe551ab4bc1640d4154ee2acd8e46d3bbc925c7224caae02fcaf0e6a771e97fccc661 + languageName: node + linkType: hard + +"@balena/dockerignore@npm:^1.0.2": + version: 1.0.2 + resolution: "@balena/dockerignore@npm:1.0.2" + checksum: 10/13d654fdd725008577d32e721c720275bdc48f72bce612326363d5bed449febbed856c517a0b23c7c40d87cb531e63432804550b4ecc13e365d26fee38fb6c8a + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: 10/46600b2dde460269b07a8e4f12b72e418eae1337b85c979f43af3336c9a1c65b04e42508ab6b245f1e0e3c64328e1c38d8cd733e4a7cebc4fbf9cf65c6e59937 + languageName: node + linkType: hard + "@biomejs/biome@npm:2.3.8": version: 2.3.8 resolution: "@biomejs/biome@npm:2.3.8" @@ -105,6 +185,32 @@ __metadata: languageName: node linkType: hard +"@effect/platform@npm:^0.95.0": + version: 0.95.0 + resolution: "@effect/platform@npm:0.95.0" + dependencies: + find-my-way-ts: "npm:^0.1.6" + msgpackr: "npm:^1.11.4" + multipasta: "npm:^0.2.7" + peerDependencies: + effect: ^3.20.0 + checksum: 10/ae3f3bd441f77bb0f3bb71f954d3a06be2565e4d924eba8c7d5c898da32d893f42c4af0e5c6fee5a1ba087ab7d2d1dae8734a4b1e830baeb654fcccd63c996bb + languageName: node + linkType: hard + +"@effect/platform@npm:^0.96.0": + version: 0.96.1 + resolution: "@effect/platform@npm:0.96.1" + dependencies: + find-my-way-ts: "npm:^0.1.6" + msgpackr: "npm:^1.11.10" + multipasta: "npm:^0.2.7" + peerDependencies: + effect: ^3.21.2 + checksum: 10/36d8b1d43d636be02f9119e0e6d981565a88801ec097bda1cae0ed65bea9fb1963226140b3f6b714f4ea9091a248bc20bf2cc0d775b238a9ef4010ddea48fa65 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.27.7": version: 0.27.7 resolution: "@esbuild/aix-ppc64@npm:0.27.7" @@ -287,17 +393,57 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" +"@graphql-typed-document-node/core@npm:^3.1.1, @graphql-typed-document-node/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + +"@grpc/grpc-js@npm:^1.11.1": + version: 1.14.3 + resolution: "@grpc/grpc-js@npm:1.14.3" dependencies: - string-width: "npm:^5.1.2" - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: "npm:^7.0.1" - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: "npm:^8.1.0" - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + "@grpc/proto-loader": "npm:^0.8.0" + "@js-sdsl/ordered-map": "npm:^4.4.2" + checksum: 10/bb9bfe2f749179ae5ac7774d30486dfa2e0b004518c28de158b248e0f6f65f40138f01635c48266fa540670220f850216726e3724e1eb29d078817581c96e4db + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.7.13": + version: 0.7.15 + resolution: "@grpc/proto-loader@npm:0.7.15" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.2.5" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10/2e2b33ace8bc34211522751a9e654faf9ac997577a9e9291b1619b4c05d7878a74d2101c3bc43b2b2b92bca7509001678fb191d4eb100684cc2910d66f36c373 + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.8.0": + version: 0.8.1 + resolution: "@grpc/proto-loader@npm:0.8.1" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.5.5" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10/d9ef734a43fa3003b9fea4ad9392137f353b79d62b6452b68f8f6b1d8f97947139141d111108ba3e858642989e966e4aa1211012a657d1e41f80a9c7540070ec + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^9.0.0": + version: 9.0.0 + resolution: "@isaacs/cliui@npm:9.0.0" + checksum: 10/8ea3d1009fd29071419209bb91ede20cf27e6e2a1630c5e0702d8b3f47f9e1a3f1c5a587fa2cb96d22d18219790327df49db1bcced573346bbaf4577cf46b643 languageName: node linkType: hard @@ -310,14 +456,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.0.3": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10/97106439d750a409c22c8bff822d648f6a71f3aa9bc8e5129efdc36343cd3096ddc4eeb1c62d2fe48e9bdd4db37b05d4646a17114ecebd3bbcacfa2de51c3c1d languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -334,6 +480,37 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.31": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/da0283270e691bdb5543806077548532791608e52386cfbbf3b9e8fb00457859d1bd01d512851161c886eb3a2f3ce6fd9bcf25db8edf3bddedd275bd4a88d606 + languageName: node + linkType: hard + +"@js-sdsl/ordered-map@npm:^4.4.2": + version: 4.4.2 + resolution: "@js-sdsl/ordered-map@npm:4.4.2" + checksum: 10/ac64e3f0615ecc015461c9f527f124d2edaa9e68de153c1e270c627e01e83d046522d7e872692fd57a8c514578b539afceff75831c0d8b2a9a7a347fbed35af4 + languageName: node + linkType: hard + +"@midnight-ntwrk/compact-js@npm:2.5.1": + version: 2.5.1 + resolution: "@midnight-ntwrk/compact-js@npm:2.5.1" + dependencies: + "@effect/platform": "npm:^0.95.0" + "@midnight-ntwrk/compact-runtime": "npm:0.16.0" + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/platform-js": "npm:^2.2.4" + effect: "npm:^3.20.0" + tslib: "npm:^2.8.1" + checksum: 10/ee041b88d8fd43dc63f8cbb6b02f2eb0d6445921633b032e1dd3e909c75be8ca8f311cee70d16a9d06795bbb94172d2a6797799ee3870f29a8aa42e7e3e153b6 + languageName: node + linkType: hard + "@midnight-ntwrk/compact-runtime@npm:0.14.0": version: 0.14.0 resolution: "@midnight-ntwrk/compact-runtime@npm:0.14.0" @@ -345,6 +522,24 @@ __metadata: languageName: node linkType: hard +"@midnight-ntwrk/compact-runtime@npm:0.16.0": + version: 0.16.0 + resolution: "@midnight-ntwrk/compact-runtime@npm:0.16.0" + dependencies: + "@midnight-ntwrk/onchain-runtime-v3": "npm:^3.0.0" + "@types/object-inspect": "npm:^1.8.1" + object-inspect: "npm:^1.12.3" + checksum: 10/ef0c68d53bba6a04f336094c82c26b781082d7ce4ee09f0539009fb108776b36ea24b9a774292d9bbf9722b8a78d47254b5f80a613d4010a7f7d108514243023 + languageName: node + linkType: hard + +"@midnight-ntwrk/dapp-connector-api@npm:4.0.1": + version: 4.0.1 + resolution: "@midnight-ntwrk/dapp-connector-api@npm:4.0.1" + checksum: 10/b5a2fe117390ea40d5d1030a600351400624532169f2beeaa2fa130935c27110e3743fb8f9028d2541ae466e6e168a79efcfd45aaf1bf87a8ca8340bbcf53814 + languageName: node + linkType: hard + "@midnight-ntwrk/ledger-v7@npm:^7.0.0": version: 7.0.2 resolution: "@midnight-ntwrk/ledger-v7@npm:7.0.2" @@ -352,210 +547,1262 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/onchain-runtime-v2@npm:^2.0.0": - version: 2.0.1 - resolution: "@midnight-ntwrk/onchain-runtime-v2@npm:2.0.1" - checksum: 10/40ffba7809ecbf9e7e4fd98e7e025922ba72ff667d15f7737b9a2b913558688f19552ef40a63a1379b348a4e5c85e4257f6f485d6b09d15c2b5e4ca0149613b0 +"@midnight-ntwrk/ledger-v8@npm:8.0.3": + version: 8.0.3 + resolution: "@midnight-ntwrk/ledger-v8@npm:8.0.3" + checksum: 10/93d24ddeff967a5f5d566a7e8fc0c5586f309e954adf56761fff4ab67874b846c2a4f3f2aede4f51a9e1445d01f52a7446da121473f0120793bc622feeeed207 languageName: node linkType: hard -"@npmcli/agent@npm:^3.0.0": - version: 3.0.0 - resolution: "@npmcli/agent@npm:3.0.0" +"@midnight-ntwrk/midnight-js-compact@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-compact@npm:4.1.0" dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10/775c9a7eb1f88c195dfb3bce70c31d0fe2a12b28b754e25c08a3edb4bc4816bfedb7ac64ef1e730579d078ca19dacf11630e99f8f3c3e0fd7b23caa5fd6d30a6 + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + bin: + fetch-compactc: dist/fetch-compact.mjs + run-compactc: dist/run-compactc.cjs + checksum: 10/230da503784e600151c6749d54bc719f32f5e24a85911087f871385b2996ad646b094cfe00bb3ee1c285cda177743efc7c27502da5d7c0dffd23b4bfc6dbd13d languageName: node linkType: hard -"@npmcli/fs@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/fs@npm:4.0.0" +"@midnight-ntwrk/midnight-js-contracts@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-contracts@npm:4.1.0" dependencies: - semver: "npm:^7.3.5" - checksum: 10/405c4490e1ff11cf299775449a3c254a366a4b1ffc79d87159b0ee7d5558ac9f6a2f8c0735fd6ff3873cef014cb1a44a5f9127cb6a1b2dbc408718cca9365b5a + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + checksum: 10/29c807ed2a62f6186bb3337848740ba090f2b0c50353e30e808d7912b6b630353a3b63f83e7103e3d191b1a99fc90d75680f72fab1fd526d0abeaa6eb845db0b languageName: node linkType: hard -"@openzeppelin/compact-builder@workspace:^, @openzeppelin/compact-builder@workspace:packages/builder": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-builder@workspace:packages/builder" +"@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:4.1.0" dependencies: - "@tsconfig/node24": "npm:^24.0.3" - "@types/node": "npm:24.10.1" - "@types/shell-quote": "npm:^1.7.5" - chalk: "npm:^5.6.2" - log-symbols: "npm:^7.0.0" - ora: "npm:^9.0.0" - shell-quote: "npm:^1.8.3" - typescript: "npm:^5.9.3" - vitest: "npm:^4.0.15" - languageName: unknown - linkType: soft + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + cross-fetch: "npm:^4.1.0" + fetch-retry: "npm:^6.0.0" + checksum: 10/0a4c90e0a7988c5e08b670573b49f2b083c4260858f02bf80908cf8fd3bc67c78bee029764a4a64f8b1363d29c1f4857dbe438352c2b4c1e2f8f183e0a5be066 + languageName: node + linkType: hard -"@openzeppelin/compact-cli@workspace:packages/cli": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-cli@workspace:packages/cli" +"@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:4.1.0" dependencies: - "@openzeppelin/compact-builder": "workspace:^" - "@tsconfig/node24": "npm:^24.0.3" - "@types/node": "npm:24.10.1" - chalk: "npm:^5.6.2" - ora: "npm:^9.0.0" - typescript: "npm:^5.9.3" - vitest: "npm:^4.0.15" - bin: - compact-builder: dist/runBuilder.js - compact-compiler: dist/runCompiler.js - languageName: unknown - linkType: soft - -"@openzeppelin/compact-simulator@workspace:packages/simulator": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-simulator@workspace:packages/simulator" + "@apollo/client": "npm:^4.1.6" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + buffer: "npm:^6.0.3" + cross-fetch: "npm:^4.1.0" + graphql: "npm:^16.13.2" + graphql-ws: "npm:^6.0.8" + isomorphic-ws: "npm:^5.0.0" + rxjs: "npm:^7.5.0" + ws: "npm:^8.20.0" + checksum: 10/529ce6f5eb910f1db6b8405f5b5c216afefd8bf4fe8c672019a540c6b8ffd0d59d45c78107def6fe50f74e566af622294f099d36424658d4b951cc59aea96a29 + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-level-private-state-provider@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-level-private-state-provider@npm:4.1.0" dependencies: - "@midnight-ntwrk/compact-runtime": "npm:0.14.0" - "@midnight-ntwrk/ledger-v7": "npm:^7.0.0" - "@tsconfig/node24": "npm:^24.0.3" - "@types/node": "npm:24.10.1" - fast-check: "npm:^4.5.2" - typescript: "npm:^5.8.2" - vitest: "npm:^4.0.15" - languageName: unknown - linkType: soft - -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@noble/ciphers": "npm:^2.0.0" + "@noble/hashes": "npm:^2.0.0" + abstract-level: "npm:^3.0.0" + buffer: "npm:^6.0.3" + level: "npm:^10.0.0" + superjson: "npm:^2.0.0" + checksum: 10/52f90ea4695fd630c5b52d918c7b52a4636928ce8cf172503b34debc50f1a21b24c3932b2604a83f34bf537c19c9695a09df6a84a3f0ebe053dacf5a784a0815 languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.60.3" - conditions: os=android & cpu=arm +"@midnight-ntwrk/midnight-js-network-id@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-network-id@npm:4.1.0" + checksum: 10/34c3ec96126db9e44380eb47b7bff2755b6809e4da491c63743f95b644df77bf49b6d3788a37248608ab657fd56ce22088189254ffebb2ca188d36a7ae85b376 languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-android-arm64@npm:4.60.3" - conditions: os=android & cpu=arm64 +"@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:4.1.0" + dependencies: + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + checksum: 10/e437458920b867a415e9717743391f1d376bbe9842965dabbb03377364108b72a27c6c0651ebe85349f0d1f507b58c3011be6df5be2f861e9a12ace82910cba6 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-darwin-arm64@npm:4.60.3" - conditions: os=darwin & cpu=arm64 +"@midnight-ntwrk/midnight-js-protocol@file:./vendor/midnight-js-protocol-4.1.0.tgz::locator=compact-tools-monorepo%40workspace%3A.": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-protocol@file:./vendor/midnight-js-protocol-4.1.0.tgz#./vendor/midnight-js-protocol-4.1.0.tgz::hash=d3f5ac&locator=compact-tools-monorepo%40workspace%3A." + dependencies: + "@midnight-ntwrk/compact-js": "npm:2.5.1" + "@midnight-ntwrk/compact-runtime": "npm:0.16.0" + "@midnight-ntwrk/ledger-v8": "npm:8.0.3" + "@midnight-ntwrk/onchain-runtime-v3": "npm:3.0.0" + "@midnight-ntwrk/platform-js": "npm:2.2.4" + checksum: 10/ffe843c7c234b18a098e6197704c16b3509171e958970deff05c39614d85b8f0cb4c010b248330c3e4174df89df22920251cab7b676c7fa1e219d63d278f3726 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-darwin-x64@npm:4.60.3" - conditions: os=darwin & cpu=x64 +"@midnight-ntwrk/midnight-js-types@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-types@npm:4.1.0" + dependencies: + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + rxjs: "npm:^7.5.0" + checksum: 10/1dce63152741e9c47703bb0bebe716e724731826c35c9c88c8b54f3eb63b9561a7e371d76451bc71a5bc1c2b44d2a4a1d440e08d744706dbfc64775b5666bff5 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.60.3" - conditions: os=freebsd & cpu=arm64 +"@midnight-ntwrk/midnight-js-utils@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-utils@npm:4.1.0" + dependencies: + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + checksum: 10/032560a0b2e34b60d5eda39be19041a5817997cd9318ce91fab0fe431b7f3885285eb5eb7f06bf26fca7c2a4018774863b78b3afd3b46300d515b230ebfffc36 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-freebsd-x64@npm:4.60.3" - conditions: os=freebsd & cpu=x64 +"@midnight-ntwrk/onchain-runtime-v2@npm:^2.0.0": + version: 2.0.1 + resolution: "@midnight-ntwrk/onchain-runtime-v2@npm:2.0.1" + checksum: 10/40ffba7809ecbf9e7e4fd98e7e025922ba72ff667d15f7737b9a2b913558688f19552ef40a63a1379b348a4e5c85e4257f6f485d6b09d15c2b5e4ca0149613b0 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.60.3" - conditions: os=linux & cpu=arm & libc=glibc +"@midnight-ntwrk/onchain-runtime-v3@npm:3.0.0, @midnight-ntwrk/onchain-runtime-v3@npm:^3.0.0": + version: 3.0.0 + resolution: "@midnight-ntwrk/onchain-runtime-v3@npm:3.0.0" + checksum: 10/873aeb9e631c3678373c62b5aef847de454de94427028fb3d3f28bfdc8b2c02a3c770bd79d9bfef183eb9db6fb8c23e6826636f2e512ffd6eacbcf7cc0651c5d languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.60.3" - conditions: os=linux & cpu=arm & libc=musl +"@midnight-ntwrk/platform-js@npm:2.2.4, @midnight-ntwrk/platform-js@npm:^2.2.4": + version: 2.2.4 + resolution: "@midnight-ntwrk/platform-js@npm:2.2.4" + dependencies: + "@effect/platform": "npm:^0.95.0" + effect: "npm:^3.20.0" + tslib: "npm:^2.8.1" + checksum: 10/1650bb7e54a64740aaaf27f7e84b7bffdb08611c994bbf54208db43a0a11d10ea8994f05d82e848d60d6fcee8a9b3a5db770d306262b99547e71185d52614825 languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.60.3" - conditions: os=linux & cpu=arm64 & libc=glibc +"@midnight-ntwrk/testkit-js@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/testkit-js@npm:4.1.0" + dependencies: + "@midnight-ntwrk/dapp-connector-api": "npm:4.0.1" + "@midnight-ntwrk/midnight-js-compact": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + "@midnight-ntwrk/wallet-sdk": "npm:1.0.0" + "@midnight-ntwrk/zkir-v2": "npm:2.1.0" + buffer: "npm:^6.0.3" + cross-fetch: "npm:^4.0.0" + rxjs: "npm:^7.8.1" + ws: "npm:^8.20.0" + checksum: 10/f1a7f66d7c17f07cd21b69c300cf9317c3742a046b2d8596b3062643c55648e8c8a171577ce284c0166f5f575f3ae9e677d50b7d42024d3b8d89aa2d055fe345 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-abstractions@npm:2.1.0, @midnight-ntwrk/wallet-sdk-abstractions@npm:^2.1.0": + version: 2.1.0 + resolution: "@midnight-ntwrk/wallet-sdk-abstractions@npm:2.1.0" + dependencies: + effect: "npm:^3.19.19" + checksum: 10/acd476877ab4d32a2580d0b8c4a22a4458a9f5f3bd61b3220fc8a9da63a5cc61ccb5fd95d47506fe47999e708ade7a37d4eca74707cffe9a6b9b648c9ed28596 languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.60.3" - conditions: os=linux & cpu=arm64 & libc=musl +"@midnight-ntwrk/wallet-sdk-address-format@npm:3.1.1, @midnight-ntwrk/wallet-sdk-address-format@npm:^3.1.1": + version: 3.1.1 + resolution: "@midnight-ntwrk/wallet-sdk-address-format@npm:3.1.1" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@scure/base": "npm:^2.0.0" + "@subsquid/scale-codec": "npm:^4.0.1" + checksum: 10/d92eb47928ae9dfc93bd8b549ba9c32b54b43eaae34ed7031c46b6654a55c92173eed47732f170307a4b372ed692bf3637d0b78fc58fdc3f5635d97bb782be4a languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.60.3" - conditions: os=linux & cpu=loong64 & libc=glibc +"@midnight-ntwrk/wallet-sdk-capabilities@npm:3.3.0, @midnight-ntwrk/wallet-sdk-capabilities@npm:^3.3.0": + version: 3.3.0 + resolution: "@midnight-ntwrk/wallet-sdk-capabilities@npm:3.3.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.1" + "@midnight-ntwrk/wallet-sdk-node-client": "npm:^1.1.1" + "@midnight-ntwrk/wallet-sdk-prover-client": "npm:^1.2.1" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.1.1" + "@midnight-ntwrk/zkir-v2": "npm:^2.1.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/dab6a7c2862a0181e16b1e94f882e9de655de16644a5476cb784d503847febe682cb8a565defdd90eef50247f5363a0eb1c8cd4a0702c279831c4a9e62b7e5a7 languageName: node linkType: hard -"@rollup/rollup-linux-loong64-musl@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-loong64-musl@npm:4.60.3" - conditions: os=linux & cpu=loong64 & libc=musl +"@midnight-ntwrk/wallet-sdk-dust-wallet@npm:4.0.0, @midnight-ntwrk/wallet-sdk-dust-wallet@npm:^4.0.0": + version: 4.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-dust-wallet@npm:4.0.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:3.1.1" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:3.3.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:1.2.1" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:1.0.3" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/f8da07b8e4b1be2603747f6c17afe1362265d292d21bdb1b4984c9049aa5c99ac289ba6eabe212625be200b3bf502b504508df6febf3d3bc78b5cc44128ebb94 languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.60.3" - conditions: os=linux & cpu=ppc64 & libc=glibc +"@midnight-ntwrk/wallet-sdk-facade@npm:4.0.0, @midnight-ntwrk/wallet-sdk-facade@npm:^4.0.0": + version: 4.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-facade@npm:4.0.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.1" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.0" + "@midnight-ntwrk/wallet-sdk-dust-wallet": "npm:^4.0.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.1" + "@midnight-ntwrk/wallet-sdk-shielded": "npm:^3.0.0" + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "npm:^3.0.0" + rxjs: "npm:^7.8.2" + checksum: 10/4884866470ce22b190d9f8f0aa79f423f7818670743103ddedf316830367a8b7dafa5bde3229570a6c49276c34421e4e57e468d7a8c097e0920098f67be4eb6c languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-musl@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.60.3" - conditions: os=linux & cpu=ppc64 & libc=musl +"@midnight-ntwrk/wallet-sdk-hd@npm:3.0.2, @midnight-ntwrk/wallet-sdk-hd@npm:^3.0.2": + version: 3.0.2 + resolution: "@midnight-ntwrk/wallet-sdk-hd@npm:3.0.2" + dependencies: + "@scure/bip32": "npm:^2.0.1" + "@scure/bip39": "npm:^2.0.1" + checksum: 10/697361dfa33bbb32f9eef6bed7aa13591af60405fa0f7caaf90b772148dd543e75b2500f8b2208105ed71b53e9d4c650b25b0ef5e5460628d0ff6f1235f8fd22 languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.60.3" - conditions: os=linux & cpu=riscv64 & libc=glibc +"@midnight-ntwrk/wallet-sdk-indexer-client@npm:1.2.1, @midnight-ntwrk/wallet-sdk-indexer-client@npm:^1.2.1": + version: 1.2.1 + resolution: "@midnight-ntwrk/wallet-sdk-indexer-client@npm:1.2.1" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.2.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + effect: "npm:^3.19.19" + graphql: "npm:^16.13.0" + graphql-http: "npm:^1.22.4" + graphql-ws: "npm:^6.0.7" + checksum: 10/419c9fe66e100659a4ae958ea7b55d885f2e201d8ef67ce49ad3802be7e606419f4909b3c7c0b1892cbf065a21263bff05b216f99b007af17a132c11757dfdbf languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.60.3" - conditions: os=linux & cpu=riscv64 & libc=musl +"@midnight-ntwrk/wallet-sdk-node-client@npm:^1.1.1": + version: 1.1.1 + resolution: "@midnight-ntwrk/wallet-sdk-node-client@npm:1.1.1" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:2.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + "@polkadot/api": "npm:^16.5.4" + "@polkadot/types": "npm:^16.5.4" + "@polkadot/util": "npm:^14.0.1" + "@types/bn.js": "npm:^5.2.0" + bn.js: "npm:^5.2.3" + effect: "npm:^3.19.19" + checksum: 10/e2c32fbfc4a475891f31ff786887a20b33a315c005b231aa66da8eb54d923728c113fa7bf629c5f328a92aadb821feabefcf51fb18c21b161440991caa15cf9d languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.60.3": - version: 4.60.3 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.60.3" - conditions: os=linux & cpu=s390x & libc=glibc +"@midnight-ntwrk/wallet-sdk-prover-client@npm:^1.2.1": + version: 1.2.1 + resolution: "@midnight-ntwrk/wallet-sdk-prover-client@npm:1.2.1" + dependencies: + "@effect/platform": "npm:^0.96.0" + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + "@midnight-ntwrk/zkir-v2": "npm:2.1.0" + effect: "npm:^3.19.19" + web-worker: "npm:^1.5.0" + checksum: 10/ec5c0cf6d5ab382d342655d4cd2dc08fa0d74969d63bcfc781cc13c187340db3744b9fcb0dbd95cf92966752c20334e668aba9ef1ef6a7059d97f374defda0a8 languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.60.3": - version: 4.60.3 +"@midnight-ntwrk/wallet-sdk-runtime@npm:1.0.3": + version: 1.0.3 + resolution: "@midnight-ntwrk/wallet-sdk-runtime@npm:1.0.3" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:2.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/b1b2cff5fd3814e5b8a8400e2b0f347a58fc4f1ed3405a628e690d06095dcbb4b8fead017c8cc199e319e77c165090106967b266a953815bd33c2d5cad819425 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-shielded@npm:3.0.0, @midnight-ntwrk/wallet-sdk-shielded@npm:^3.0.0": + version: 3.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-shielded@npm:3.0.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:3.1.1" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:3.3.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:1.2.1" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:1.0.3" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/e52e4f3d2c1722e401454686312aca9189d6cd37d5f14f61f14937e8109b4af4c695c4a6710a651031bcfdd1d7ce2a3018a25a14b8762783ab8c03364a1dd936 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-unshielded-wallet@npm:3.0.0, @midnight-ntwrk/wallet-sdk-unshielded-wallet@npm:^3.0.0": + version: 3.0.0 + resolution: "@midnight-ntwrk/wallet-sdk-unshielded-wallet@npm:3.0.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.0.3" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:3.1.1" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:3.3.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:1.2.1" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:1.0.3" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:1.1.1" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/cab6e5d9071544b20946ba1da9014a4b7da824291fe493d634063d346b046288094b7e33c37ff3373ed28fa2660689a8c2836027895614bd44752f9daeb5081d + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-utilities@npm:1.1.1, @midnight-ntwrk/wallet-sdk-utilities@npm:^1.1.1": + version: 1.1.1 + resolution: "@midnight-ntwrk/wallet-sdk-utilities@npm:1.1.1" + dependencies: + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/1775ac559ba003274fde80b839f296d5e1bba8c580cd6aae31db9df97f8ab5682ead4b76adbd3db01ad3af051fb81d0e24be2567cffdce51d1d55e864c6104a8 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk@npm:1.0.0": + version: 1.0.0 + resolution: "@midnight-ntwrk/wallet-sdk@npm:1.0.0" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.1" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.0" + "@midnight-ntwrk/wallet-sdk-dust-wallet": "npm:^4.0.0" + "@midnight-ntwrk/wallet-sdk-facade": "npm:^4.0.0" + "@midnight-ntwrk/wallet-sdk-hd": "npm:^3.0.2" + "@midnight-ntwrk/wallet-sdk-shielded": "npm:^3.0.0" + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "npm:^3.0.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.1.1" + checksum: 10/2c70429c4b1cd54d60b29807412dacf2ce326ae63cc0a03f092731b0e76225cd496fc3bb68eae4f51c799a691a2f6843664cb1beca9e13619daae054918cbb66 + languageName: node + linkType: hard + +"@midnight-ntwrk/zkir-v2@npm:2.1.0, @midnight-ntwrk/zkir-v2@npm:^2.1.0": + version: 2.1.0 + resolution: "@midnight-ntwrk/zkir-v2@npm:2.1.0" + checksum: 10/c16761489c3abbf858a4b7c2c4dd99d498f40554b5f1a57a93534b21c66390d4c6b0035dee8923fb5972418c75ac1f80e2e0675d8f3eb2a96dce7e7555fb2b7d + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@noble/ciphers@npm:^2.0.0": + version: 2.2.0 + resolution: "@noble/ciphers@npm:2.2.0" + checksum: 10/d75348aa682b41ad3e24cdd0a56c6d9ca033fb629ab93f37d6690be41c4882359b27598a11af0f5439ba82df4f9e3875dea1f875064310f68fef63cf24e3481a + languageName: node + linkType: hard + +"@noble/curves@npm:2.2.0": + version: 2.2.0 + resolution: "@noble/curves@npm:2.2.0" + dependencies: + "@noble/hashes": "npm:2.2.0" + checksum: 10/f9545e55bb8b6cdf2618c936870b9229339c90b25f129fc368b4b534e723f274e5c0daf8abca2f891bcf0a59c3b49c5ac5205899aec07f5251f545ec616e3aa9 + languageName: node + linkType: hard + +"@noble/curves@npm:^1.3.0, @noble/curves@npm:~1.9.2": + version: 1.9.7 + resolution: "@noble/curves@npm:1.9.7" + dependencies: + "@noble/hashes": "npm:1.8.0" + checksum: 10/3cfe2735ea94972988ca9e217e0ebb2044372a7160b2079bf885da789492a6291fc8bf76ca3d8bf8dee477847ee2d6fac267d1e6c4f555054059f5e8c4865d44 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:~1.8.0": + version: 1.8.0 + resolution: "@noble/hashes@npm:1.8.0" + checksum: 10/474b7f56bc6fb2d5b3a42132561e221b0ea4f91e590f4655312ca13667840896b34195e2b53b7f097ec080a1fdd3b58d902c2a8d0fbdf51d2e238b53808a177e + languageName: node + linkType: hard + +"@noble/hashes@npm:2.2.0, @noble/hashes@npm:^2.0.0": + version: 2.2.0 + resolution: "@noble/hashes@npm:2.2.0" + checksum: 10/b1b78bedc2a01394be047429f3d888905015fe8a09f1b7e43e0b5736b54133df62f73dcc73ede43af38e96e86156afb45b86973fdeaa95d9f0880333c3fc0907 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^3.0.0": + version: 3.0.0 + resolution: "@npmcli/agent@npm:3.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/775c9a7eb1f88c195dfb3bce70c31d0fe2a12b28b754e25c08a3edb4bc4816bfedb7ac64ef1e730579d078ca19dacf11630e99f8f3c3e0fd7b23caa5fd6d30a6 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/fs@npm:4.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/405c4490e1ff11cf299775449a3c254a366a4b1ffc79d87159b0ee7d5558ac9f6a2f8c0735fd6ff3873cef014cb1a44a5f9127cb6a1b2dbc408718cca9365b5a + languageName: node + linkType: hard + +"@openzeppelin/compact-builder@workspace:^, @openzeppelin/compact-builder@workspace:packages/builder": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-builder@workspace:packages/builder" + dependencies: + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:24.10.1" + "@types/shell-quote": "npm:^1.7.5" + chalk: "npm:^5.6.2" + log-symbols: "npm:^7.0.0" + ora: "npm:^9.0.0" + shell-quote: "npm:^1.8.3" + typescript: "npm:^5.9.3" + vitest: "npm:^4.0.15" + languageName: unknown + linkType: soft + +"@openzeppelin/compact-cli@workspace:^, @openzeppelin/compact-cli@workspace:packages/cli": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-cli@workspace:packages/cli" + dependencies: + "@openzeppelin/compact-builder": "workspace:^" + "@openzeppelin/compact-deployer": "workspace:^" + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:24.10.1" + "@types/ws": "npm:^8.5.10" + chalk: "npm:^5.6.2" + ora: "npm:^9.0.0" + pino: "npm:^9.7.0" + pino-pretty: "npm:^13.0.0" + typescript: "npm:^5.9.3" + vitest: "npm:^4.0.15" + ws: "npm:^8.16.0" + bin: + compact-builder: dist/runBuilder.js + compact-compiler: dist/runCompiler.js + compact-deploy: dist/runDeploy.js + languageName: unknown + linkType: soft + +"@openzeppelin/compact-deployer@workspace:^, @openzeppelin/compact-deployer@workspace:packages/deployer": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-deployer@workspace:packages/deployer" + dependencies: + "@midnight-ntwrk/compact-js": "npm:2.5.1" + "@midnight-ntwrk/compact-runtime": "npm:0.16.0" + "@midnight-ntwrk/ledger-v8": "npm:8.0.3" + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.0" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.0" + "@midnight-ntwrk/testkit-js": "npm:4.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:3.1.1" + "@midnight-ntwrk/wallet-sdk-dust-wallet": "npm:4.0.0" + "@midnight-ntwrk/wallet-sdk-facade": "npm:4.0.0" + "@midnight-ntwrk/wallet-sdk-hd": "npm:3.0.2" + "@midnight-ntwrk/wallet-sdk-shielded": "npm:3.0.0" + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "npm:3.0.0" + "@scure/bip39": "npm:^1.2.1" + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:24.10.1" + axios: "npm:^1.12.0" + pino: "npm:^9.7.0" + rxjs: "npm:^7.8.1" + smol-toml: "npm:^1.3.4" + testcontainers: "npm:^10.28.0" + typescript: "npm:^5.9.3" + vitest: "npm:^4.0.15" + zod: "npm:^3.23.8" + languageName: unknown + linkType: soft + +"@openzeppelin/compact-simulator@workspace:packages/simulator": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-simulator@workspace:packages/simulator" + dependencies: + "@midnight-ntwrk/compact-runtime": "npm:0.14.0" + "@midnight-ntwrk/ledger-v7": "npm:^7.0.0" + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:24.10.1" + fast-check: "npm:^4.5.2" + typescript: "npm:^5.8.2" + vitest: "npm:^4.0.15" + languageName: unknown + linkType: soft + +"@pinojs/redact@npm:^0.4.0": + version: 0.4.0 + resolution: "@pinojs/redact@npm:0.4.0" + checksum: 10/2210ffb6b38357853d47239fd0532cc9edb406325270a81c440a35cece22090127c30c2ead3eefa3e608f2244087485308e515c431f4f69b6bd2e16cbd32812b + languageName: node + linkType: hard + +"@polkadot-api/json-rpc-provider-proxy@npm:^0.1.0": + version: 0.1.0 + resolution: "@polkadot-api/json-rpc-provider-proxy@npm:0.1.0" + checksum: 10/1a232337a4f6f32f3ec0350d5aaceaab21547ccee3cca63318d4b9238982efa5ff2406b033c320318c72d067b73508c0a1af21eb47acabaff714c1c21477bafa + languageName: node + linkType: hard + +"@polkadot-api/json-rpc-provider@npm:0.0.1, @polkadot-api/json-rpc-provider@npm:^0.0.1": + version: 0.0.1 + resolution: "@polkadot-api/json-rpc-provider@npm:0.0.1" + checksum: 10/1f315bdadcba7def7145011132e6127b983c6f91f976be217ad7d555bb96a67f3a270fe4a46e427531822c5d54d353d84a6439d112a99cdfc07013d3b662ee3c + languageName: node + linkType: hard + +"@polkadot-api/metadata-builders@npm:0.3.2": + version: 0.3.2 + resolution: "@polkadot-api/metadata-builders@npm:0.3.2" + dependencies: + "@polkadot-api/substrate-bindings": "npm:0.6.0" + "@polkadot-api/utils": "npm:0.1.0" + checksum: 10/874b38e1fb92beea99b98b889143f25671f137e54113767aeabb79ff5cdf7d61cadb0121f08c7a9a40718b924d7c9a1dd700f81e7e287bc55923b0129e2a6160 + languageName: node + linkType: hard + +"@polkadot-api/observable-client@npm:^0.3.0": + version: 0.3.2 + resolution: "@polkadot-api/observable-client@npm:0.3.2" + dependencies: + "@polkadot-api/metadata-builders": "npm:0.3.2" + "@polkadot-api/substrate-bindings": "npm:0.6.0" + "@polkadot-api/utils": "npm:0.1.0" + peerDependencies: + "@polkadot-api/substrate-client": 0.1.4 + rxjs: ">=7.8.0" + checksum: 10/91b95a06e3ddd477c2489110d7cffdcfaf87a222054b437013c701dc43eac6a5d30438b1ac8fb130166ba039a67808e6199ccb3b2eaac7dcf8d2ef7a835f047b + languageName: node + linkType: hard + +"@polkadot-api/substrate-bindings@npm:0.6.0": + version: 0.6.0 + resolution: "@polkadot-api/substrate-bindings@npm:0.6.0" + dependencies: + "@noble/hashes": "npm:^1.3.1" + "@polkadot-api/utils": "npm:0.1.0" + "@scure/base": "npm:^1.1.1" + scale-ts: "npm:^1.6.0" + checksum: 10/01926a9083f608514a55c3d23563ebef139e2963d4adbebe7dcd99b65e1a08f1551fc0e147e787a31c749402767333c96eb1399f85a6c71654cfa1cc9d26e445 + languageName: node + linkType: hard + +"@polkadot-api/substrate-client@npm:^0.1.2": + version: 0.1.4 + resolution: "@polkadot-api/substrate-client@npm:0.1.4" + dependencies: + "@polkadot-api/json-rpc-provider": "npm:0.0.1" + "@polkadot-api/utils": "npm:0.1.0" + checksum: 10/e7172696db404676d297cd5661b195de110593769f9ce37f32bdb5576ca00c56d32fcb04172a91102986fdda27a13962d909ad9466869a2991611d658ee6ac92 + languageName: node + linkType: hard + +"@polkadot-api/utils@npm:0.1.0": + version: 0.1.0 + resolution: "@polkadot-api/utils@npm:0.1.0" + checksum: 10/c557daea91ddb03e16b93c7c5a75533495c7b77cbbbdc2b4f5e97af0c1e1132a47e434c9c729a08241bd7b3624b6644ac0950f914aa8b29a0f419bf0fd224c7c + languageName: node + linkType: hard + +"@polkadot/api-augment@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/api-augment@npm:16.5.6" + dependencies: + "@polkadot/api-base": "npm:16.5.6" + "@polkadot/rpc-augment": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-augment": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/155e90fb8b11ae9d6fc1db1108ddb231187764ab5f42f0b2dca0c0d2a5e8ac5f833a7a32cfb9f401dea4395b631af99354e312432b41973281358e7fa05c5a26 + languageName: node + linkType: hard + +"@polkadot/api-base@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/api-base@npm:16.5.6" + dependencies: + "@polkadot/rpc-core": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.8.1" + checksum: 10/28c238896a3150f3cd405c7d204992b70e9704b04075e7bee440b590701ed025f5baa5a25d81c7396aa0e2d77a63ed7c17a489451d758edd75183198b4552a69 + languageName: node + linkType: hard + +"@polkadot/api-derive@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/api-derive@npm:16.5.6" + dependencies: + "@polkadot/api": "npm:16.5.6" + "@polkadot/api-augment": "npm:16.5.6" + "@polkadot/api-base": "npm:16.5.6" + "@polkadot/rpc-core": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + "@polkadot/util-crypto": "npm:^14.0.3" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.8.1" + checksum: 10/493be1bfa7807d6c39f8bef9569f1d5ae9e87e2330bd561a2dcf59a3bfec71c2cd260e33005c752d17a6e24195184e18db7a1a80309af9738bb0070a7f3b90db + languageName: node + linkType: hard + +"@polkadot/api@npm:16.5.6, @polkadot/api@npm:^16.5.4": + version: 16.5.6 + resolution: "@polkadot/api@npm:16.5.6" + dependencies: + "@polkadot/api-augment": "npm:16.5.6" + "@polkadot/api-base": "npm:16.5.6" + "@polkadot/api-derive": "npm:16.5.6" + "@polkadot/keyring": "npm:^14.0.3" + "@polkadot/rpc-augment": "npm:16.5.6" + "@polkadot/rpc-core": "npm:16.5.6" + "@polkadot/rpc-provider": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-augment": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/types-create": "npm:16.5.6" + "@polkadot/types-known": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + "@polkadot/util-crypto": "npm:^14.0.3" + eventemitter3: "npm:^5.0.1" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.8.1" + checksum: 10/bfd3c7d8f4e69fa405eafcc437abfe7d69754301f280459c4665cc4bb2d55e62741967cd72bfbec15dbbacc343c261f9480e073fd5d534da24aabc013be0b7da + languageName: node + linkType: hard + +"@polkadot/keyring@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/keyring@npm:14.0.3" + dependencies: + "@polkadot/util": "npm:14.0.3" + "@polkadot/util-crypto": "npm:14.0.3" + tslib: "npm:^2.8.0" + peerDependencies: + "@polkadot/util": 14.0.3 + "@polkadot/util-crypto": 14.0.3 + checksum: 10/69f9f776363f8327d72b43794262ae709fc2824182637e499ed6e9ca94315645d78005bf1f25bdfb7305e5d79879cb932c114e6612467ddf21a760117834e8a2 + languageName: node + linkType: hard + +"@polkadot/networks@npm:14.0.3, @polkadot/networks@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/networks@npm:14.0.3" + dependencies: + "@polkadot/util": "npm:14.0.3" + "@substrate/ss58-registry": "npm:^1.51.0" + tslib: "npm:^2.8.0" + checksum: 10/eb006f537f103b0d417e52966d0098b528326d1ebbae84e4c7834627bb3e863b7b849856992aa58c4a0aeb0ed1e1838a9619aeba7610d0e7c75e99ffcc6c9ecd + languageName: node + linkType: hard + +"@polkadot/rpc-augment@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/rpc-augment@npm:16.5.6" + dependencies: + "@polkadot/rpc-core": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/77abf8d1ced793a489a6b0888f190ac0d3b1fe03f310ec34f2f2dc5b646bd23606cf6dd93e660cb7383995931672a36e1e9ab642e9c8010d60fab83ccdd0ac42 + languageName: node + linkType: hard + +"@polkadot/rpc-core@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/rpc-core@npm:16.5.6" + dependencies: + "@polkadot/rpc-augment": "npm:16.5.6" + "@polkadot/rpc-provider": "npm:16.5.6" + "@polkadot/types": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.8.1" + checksum: 10/795d504e109367d1bf41f27e90b440968e06f5b86c1ef9e5806d98bd38036cc1dd5bbe9aeb539b1e81865d78a0957a22341b9397372c0e6b748cdc51ca79ea30 + languageName: node + linkType: hard + +"@polkadot/rpc-provider@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/rpc-provider@npm:16.5.6" + dependencies: + "@polkadot/keyring": "npm:^14.0.3" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-support": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + "@polkadot/util-crypto": "npm:^14.0.3" + "@polkadot/x-fetch": "npm:^14.0.3" + "@polkadot/x-global": "npm:^14.0.3" + "@polkadot/x-ws": "npm:^14.0.3" + "@substrate/connect": "npm:0.8.11" + eventemitter3: "npm:^5.0.1" + mock-socket: "npm:^9.3.1" + nock: "npm:^13.5.5" + tslib: "npm:^2.8.1" + dependenciesMeta: + "@substrate/connect": + optional: true + checksum: 10/06913cb6887652896a47aef6fef3cb811d9bed577a4d13c570baa0c8df401ecfcaec58f27d338d0d6c6319acbfc3b6a4b4a837679fae089dcec0bd1babd9e418 + languageName: node + linkType: hard + +"@polkadot/types-augment@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/types-augment@npm:16.5.6" + dependencies: + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/b2b300af0cac2394d1b95a907e25b1f78d3af7502186c6bc2f3eef51928c6638d6db8e55de57a6ddbef0b621d5d6a36311aefa1820f23d61bd86f3a6d20108c8 + languageName: node + linkType: hard + +"@polkadot/types-codec@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/types-codec@npm:16.5.6" + dependencies: + "@polkadot/util": "npm:^14.0.3" + "@polkadot/x-bigint": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/80cd00315e19d5521732ee0c676444dbf7081ff056ccd070b665064cda0d364a7b434c39a23a68af89c20e2020b93ce281eef8d4a7db28161ce88ee92ce7dd07 + languageName: node + linkType: hard + +"@polkadot/types-create@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/types-create@npm:16.5.6" + dependencies: + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/553c023d34fefdac5461cdc8c8d451a669dfbc15c2bd1f24b0836a68829ad06b5329487091a21bd7d557f76b2fb364a53f33a32f9da1ae8e3474a32f2da61127 + languageName: node + linkType: hard + +"@polkadot/types-known@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/types-known@npm:16.5.6" + dependencies: + "@polkadot/networks": "npm:^14.0.3" + "@polkadot/types": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/types-create": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/6681e5189e0f16127379981c44d6abb35829e2731961ed6996c06bfc8c5f811fc26010f4213ea2e1f06c36b174576ef2f64f783bebd7e38c735cc06445ee557f + languageName: node + linkType: hard + +"@polkadot/types-support@npm:16.5.6": + version: 16.5.6 + resolution: "@polkadot/types-support@npm:16.5.6" + dependencies: + "@polkadot/util": "npm:^14.0.3" + tslib: "npm:^2.8.1" + checksum: 10/d43b902392af367adde8d9492161ca7a5ae6acc7d3c9b87e9633896b25d3ba783a96e5a00436a137e55c231d1465ae9c5d15472ec674051c917401106655de80 + languageName: node + linkType: hard + +"@polkadot/types@npm:16.5.6, @polkadot/types@npm:^16.5.4": + version: 16.5.6 + resolution: "@polkadot/types@npm:16.5.6" + dependencies: + "@polkadot/keyring": "npm:^14.0.3" + "@polkadot/types-augment": "npm:16.5.6" + "@polkadot/types-codec": "npm:16.5.6" + "@polkadot/types-create": "npm:16.5.6" + "@polkadot/util": "npm:^14.0.3" + "@polkadot/util-crypto": "npm:^14.0.3" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.8.1" + checksum: 10/85c3ad043d16216f9b49fbb613d17c0af70ba817f20c3fa287e0ff628d3a5338ce4e7505e74a59610f1eb0b4f26b2a8701c3f25c1e90f7c95f2e3bde1fc5391b + languageName: node + linkType: hard + +"@polkadot/util-crypto@npm:14.0.3, @polkadot/util-crypto@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/util-crypto@npm:14.0.3" + dependencies: + "@noble/curves": "npm:^1.3.0" + "@noble/hashes": "npm:^1.3.3" + "@polkadot/networks": "npm:14.0.3" + "@polkadot/util": "npm:14.0.3" + "@polkadot/wasm-crypto": "npm:^7.5.3" + "@polkadot/wasm-util": "npm:^7.5.3" + "@polkadot/x-bigint": "npm:14.0.3" + "@polkadot/x-randomvalues": "npm:14.0.3" + "@scure/base": "npm:^1.1.7" + "@scure/sr25519": "npm:^0.2.0" + tslib: "npm:^2.8.0" + peerDependencies: + "@polkadot/util": 14.0.3 + checksum: 10/e8f2da806cb81d3c014415bdd633f0fc5871132ce790ca892f65899010386d64fa25f7c047574cc96402afa03b5ff77e4dff904e69b90e714a7150e18ef0f507 + languageName: node + linkType: hard + +"@polkadot/util@npm:14.0.3, @polkadot/util@npm:^14.0.1, @polkadot/util@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/util@npm:14.0.3" + dependencies: + "@polkadot/x-bigint": "npm:14.0.3" + "@polkadot/x-global": "npm:14.0.3" + "@polkadot/x-textdecoder": "npm:14.0.3" + "@polkadot/x-textencoder": "npm:14.0.3" + "@types/bn.js": "npm:^5.1.6" + bn.js: "npm:^5.2.1" + tslib: "npm:^2.8.0" + checksum: 10/7731f26f363696a2e313fdd44d870d711924e8d24200e1c5e88769e02c220af99382460372caa1715511548753e1e3d5c1466a02308b0d4dec0700ec0ab4e88b + languageName: node + linkType: hard + +"@polkadot/wasm-bridge@npm:7.5.4": + version: 7.5.4 + resolution: "@polkadot/wasm-bridge@npm:7.5.4" + dependencies: + "@polkadot/wasm-util": "npm:7.5.4" + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + "@polkadot/x-randomvalues": "*" + checksum: 10/64db5db90a82396032c31e6745b2e77817b8e9258841b72e506370ecf3ac63497efc654ca113419baf3c9b5fabda86bb21b29e1b508f192ab4e07beab8ef6d04 + languageName: node + linkType: hard + +"@polkadot/wasm-crypto-asmjs@npm:7.5.4": + version: 7.5.4 + resolution: "@polkadot/wasm-crypto-asmjs@npm:7.5.4" + dependencies: + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + checksum: 10/9e03f052b871bc9e33268b01025fe43789f2af40e4aabbe3b7d8348a0752001cd137c20ba66c58ee7d692e798d957024c7cbd0cbf1a8cf3e6baebbe67696e781 + languageName: node + linkType: hard + +"@polkadot/wasm-crypto-init@npm:7.5.4": + version: 7.5.4 + resolution: "@polkadot/wasm-crypto-init@npm:7.5.4" + dependencies: + "@polkadot/wasm-bridge": "npm:7.5.4" + "@polkadot/wasm-crypto-asmjs": "npm:7.5.4" + "@polkadot/wasm-crypto-wasm": "npm:7.5.4" + "@polkadot/wasm-util": "npm:7.5.4" + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + "@polkadot/x-randomvalues": "*" + checksum: 10/c1077a74156bd6356487043b23a849b214274c74fc44f1e2c203ec58f152c47c577f9da920ebf79ef746cfdfd2f246b1dd6a97c5796556f1c00e63d795eb896f + languageName: node + linkType: hard + +"@polkadot/wasm-crypto-wasm@npm:7.5.4": + version: 7.5.4 + resolution: "@polkadot/wasm-crypto-wasm@npm:7.5.4" + dependencies: + "@polkadot/wasm-util": "npm:7.5.4" + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + checksum: 10/338b5d4b347116efa09aba7f27f1d13e84a4ef62680ab02e2c47bbd43180844434cf49f8c954528cbb8bebef69bdf101be33e3a6fe093efd3f5ab2245f5e7faf + languageName: node + linkType: hard + +"@polkadot/wasm-crypto@npm:^7.5.3": + version: 7.5.4 + resolution: "@polkadot/wasm-crypto@npm:7.5.4" + dependencies: + "@polkadot/wasm-bridge": "npm:7.5.4" + "@polkadot/wasm-crypto-asmjs": "npm:7.5.4" + "@polkadot/wasm-crypto-init": "npm:7.5.4" + "@polkadot/wasm-crypto-wasm": "npm:7.5.4" + "@polkadot/wasm-util": "npm:7.5.4" + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + "@polkadot/x-randomvalues": "*" + checksum: 10/d4edce7bc9e8fa8387abe1d3fa4433937ab40faf4889a949a5a64c42f852837e3da96c00a73fb383fc8ef3fe177ac40dc85a13bcd43b059f2d04bab52f537801 + languageName: node + linkType: hard + +"@polkadot/wasm-util@npm:7.5.4, @polkadot/wasm-util@npm:^7.5.3": + version: 7.5.4 + resolution: "@polkadot/wasm-util@npm:7.5.4" + dependencies: + tslib: "npm:^2.7.0" + peerDependencies: + "@polkadot/util": "*" + checksum: 10/4dda837f3ac84705d709a2e62fc0f9ec54518dbae88d3bf9dc68b65f17f50eadf7fff4289f3deaf51f93d79d5ac0631ecf57ad572d55f98a11149beaa3b2bcc4 + languageName: node + linkType: hard + +"@polkadot/x-bigint@npm:14.0.3, @polkadot/x-bigint@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-bigint@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + tslib: "npm:^2.8.0" + checksum: 10/82017c7046c9d65af15cead3ebbaea08e07992e7fb081f7cc9175dae61988a0a352d923da57da5ee86fb8d671ab5449f6e630798b889002ea8b899d7e3d1b5d3 + languageName: node + linkType: hard + +"@polkadot/x-fetch@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-fetch@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + node-fetch: "npm:^3.3.2" + tslib: "npm:^2.8.0" + checksum: 10/cf9add8a351d8021ea9728ea648ad34d3244de2848cf90cb08037d73b16b63251577beb4590669dcff1bd1f64c99b62cb059831b333ea07a047bc0b33f79a0e7 + languageName: node + linkType: hard + +"@polkadot/x-global@npm:14.0.3, @polkadot/x-global@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-global@npm:14.0.3" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10/5d75b2097ae7f279efdc49c02e7f4deb5ffa131250f25439bcf7f1a334e3ae525467520521424cca62a198f396ee9f5c321f591cb9b55f1b2aeaf69cd129c829 + languageName: node + linkType: hard + +"@polkadot/x-randomvalues@npm:14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-randomvalues@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + tslib: "npm:^2.8.0" + peerDependencies: + "@polkadot/util": 14.0.3 + "@polkadot/wasm-util": "*" + checksum: 10/03aa905b34f2eefc038d1a8edaf41a631aef36e229235d40d965a460ca127c027753bad0954ca889967877ba7d13d1fc5b49dc86d6637c1f98596c9ad600cb04 + languageName: node + linkType: hard + +"@polkadot/x-textdecoder@npm:14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-textdecoder@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + tslib: "npm:^2.8.0" + checksum: 10/3ec2210f9d3b0f5cab0a2b39575dd3d0393aed141e8cb9cc743573b17ea201d08c6f28aebc6acafd9eae9362ad6b223091486131a53409b684a3ddecbce19250 + languageName: node + linkType: hard + +"@polkadot/x-textencoder@npm:14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-textencoder@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + tslib: "npm:^2.8.0" + checksum: 10/541fd458433e153683ac41e8d6c060a2e46dd29ff5638abf992dd5ea7838a3514b4ee1d9ca11d50b384d3d001fb1347f01e176531cca10bfc4840b4736cdd474 + languageName: node + linkType: hard + +"@polkadot/x-ws@npm:^14.0.3": + version: 14.0.3 + resolution: "@polkadot/x-ws@npm:14.0.3" + dependencies: + "@polkadot/x-global": "npm:14.0.3" + tslib: "npm:^2.8.0" + ws: "npm:^8.18.0" + checksum: 10/c66b7f9c5857884ec94abe5796372816d1029e2f81078f026eef12456ef0971f59e2d678fec347f3bdf6f755834a41074b4b6177f10ec2a7b56a19d35825ac8b + languageName: node + linkType: hard + +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10/8a938d84fe4889411296db66b29287bd61ea3c14c2d23e7a8325f46a2b8ce899857c5f038d65d7641805e6c1d06b495525c7faf00c44f85a7ee6476649034969 + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10/c71b100daeb3c9bdccab5cbc29495b906ba0ae22ceedc200e1ba49717d9c4ab15a6256839cebb6f9c6acae4ed7c25c67e0a95e734f612b258261d1a3098fe342 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.5": + version: 2.0.5 + resolution: "@protobufjs/codegen@npm:2.0.5" + checksum: 10/290335fa114f26202abc0695f279d53e2fd516b01cfd8298923591e0bda011295ff40e3582a1cda0a0f27cbc5039a0292082d5ad08872bb5d6243a614ac15c88 + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/eventemitter@npm:1.1.0" + checksum: 10/03af3e99f17ad421283d054c88a06a30a615922a817741b43ca1b13e7c6b37820a37f6eba9980fb5150c54dba6e26cb6f7b64a6f7d8afa83596fafb3afa218c3 + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/fetch@npm:1.1.1" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + checksum: 10/427cf2da8c69b494b0df3b2fb1f43c97f0f71ca2c8ef8232dac7e44f2527ad0cc9cecb243eda14a918e86018bfa6d54d92252240d2b37ed205b13adb5506fa1d + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10/634c2c989da0ef2f4f19373d64187e2a79f598c5fb7991afb689d29a2ea17c14b796b29725945fa34b9493c17fb799e08ac0a7ccaae460ee1757d3083ed35187 + languageName: node + linkType: hard + +"@protobufjs/inquire@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/inquire@npm:1.1.2" + checksum: 10/259756489c75a751552df60d18f82503d2534855646397b96b91cf15807fa852e99bd9eb73dabb64da37aec7913844032ecb031a4326d82aae622f5e4c2f8a17 + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10/bb709567935fd385a86ad1f575aea98131bbd719c743fb9b6edd6b47ede429ff71a801cecbd64fc72deebf4e08b8f1bd8062793178cdaed3713b8d15771f9b83 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10/b9c7047647f6af28e92aac54f6f7c1f7ff31b201b4bfcc7a415b2861528854fce3ec666d7e7e10fd744da905f7d4aef2205bbcc8944ca0ca7a82e18134d00c46 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/utf8@npm:1.1.1" + checksum: 10/ed0c3f9ff1afd602a0aed54c4c03a0b8f641686a5587d8949e088dcac653fb2019d15691ed92eef23dfdf9f4293249532d0508ecd15cef810acf026917719a19 + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.60.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-android-arm64@npm:4.60.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.60.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.60.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-arm64@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.60.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-x64@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.60.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.60.3" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.60.3" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.60.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.60.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.60.3" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.60.3" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.60.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.60.3" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.60.3" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-musl@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.60.3" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.60.3": + version: 4.60.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.60.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.60.3": + version: 4.60.3 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.60.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node @@ -610,6 +1857,61 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:2.2.0, @scure/base@npm:^2.0.0": + version: 2.2.0 + resolution: "@scure/base@npm:2.2.0" + checksum: 10/b52ec9cd54bad77e22f881b6924ccab692dc1c6dd10287d1787bf263e9f1e560d6d2bda906538fb9a39615d61a1b5c2f53f57a511667fd10e93b9cdaa6fb5d2a + languageName: node + linkType: hard + +"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.7, @scure/base@npm:~1.2.5": + version: 1.2.6 + resolution: "@scure/base@npm:1.2.6" + checksum: 10/c1a7bd5e0b0c8f94c36fbc220f4a67cc832b00e2d2065c7d8a404ed81ab1c94c5443def6d361a70fc382db3496e9487fb9941728f0584782b274c18a4bed4187 + languageName: node + linkType: hard + +"@scure/bip32@npm:^2.0.1": + version: 2.2.0 + resolution: "@scure/bip32@npm:2.2.0" + dependencies: + "@noble/curves": "npm:2.2.0" + "@noble/hashes": "npm:2.2.0" + "@scure/base": "npm:2.2.0" + checksum: 10/595875bdfdd153621a35d71b73bb77e1406b5d659bbd20fc4db3fed697d72d39a62c8a6b2bb9816ce4e50199200252008ae203cd637f3acf1e0821180755cd3d + languageName: node + linkType: hard + +"@scure/bip39@npm:^1.2.1": + version: 1.6.0 + resolution: "@scure/bip39@npm:1.6.0" + dependencies: + "@noble/hashes": "npm:~1.8.0" + "@scure/base": "npm:~1.2.5" + checksum: 10/63e60c40fa1bda2c1b50351546fee6d7b0947cc814aa7a4209dcedd3693b5053302c8fca28292f5f50735e11c613265359acdc019127393dbab17e53489fc449 + languageName: node + linkType: hard + +"@scure/bip39@npm:^2.0.1": + version: 2.2.0 + resolution: "@scure/bip39@npm:2.2.0" + dependencies: + "@noble/hashes": "npm:2.2.0" + "@scure/base": "npm:2.2.0" + checksum: 10/f8f05c9f1337f694e1b490dcc795ac0da87e3cb4e5377889c19caa910c46567aa6b4071f2fc102fffb76020c221e09ffe9e1dde471728224335713c55cbfb182 + languageName: node + linkType: hard + +"@scure/sr25519@npm:^0.2.0": + version: 0.2.0 + resolution: "@scure/sr25519@npm:0.2.0" + dependencies: + "@noble/curves": "npm:~1.9.2" + "@noble/hashes": "npm:~1.8.0" + checksum: 10/3c47b474811642b43fd8c96f7846c9d88c9a06eefa7d6360b6421ebdfb6cf582e1e8fdce9ae4708b088a0e323cd6519c883c3a33a284c2fad592414b02f19049 + languageName: node + linkType: hard + "@standard-schema/spec@npm:^1.0.0": version: 1.0.0 resolution: "@standard-schema/spec@npm:1.0.0" @@ -617,6 +1919,82 @@ __metadata: languageName: node linkType: hard +"@subsquid/scale-codec@npm:^4.0.1": + version: 4.0.1 + resolution: "@subsquid/scale-codec@npm:4.0.1" + dependencies: + "@subsquid/util-internal-hex": "npm:^1.2.2" + "@subsquid/util-internal-json": "npm:^1.2.2" + checksum: 10/d0c81f43c6c93d6885baa0992dd170c94e8259b2eb500694b62b8ca25624c78bb7e4815b1120bbb7f3ed0e7eda02cd02233e1d8b5bac903322731ff3c9fb42bc + languageName: node + linkType: hard + +"@subsquid/util-internal-hex@npm:^1.2.2": + version: 1.2.3 + resolution: "@subsquid/util-internal-hex@npm:1.2.3" + checksum: 10/d3feeb16e130d7a5281bbd98c0ddc9a44d3c49f2655766d4e97d16407c8466b3b246bbefecfb397580f2402dc62b45065c8e62ce986b14935246b1252e66d347 + languageName: node + linkType: hard + +"@subsquid/util-internal-json@npm:^1.2.2": + version: 1.2.3 + resolution: "@subsquid/util-internal-json@npm:1.2.3" + dependencies: + "@subsquid/util-internal-hex": "npm:^1.2.2" + checksum: 10/9a518c8fc56066778b0535ed243024e17f958d9020d99d5444657fd877d7da3adc1f34b3f0e621cb8365729bc9e10aeb63bb24b91e579eb413ef8cbbab66c81d + languageName: node + linkType: hard + +"@substrate/connect-extension-protocol@npm:^2.0.0": + version: 2.2.2 + resolution: "@substrate/connect-extension-protocol@npm:2.2.2" + checksum: 10/b5427526dafcbd0ec45d3ce7ef7a3d1018496cae7d8ef60f545d4e143420b3e51fe37af966f493e73f4cb9383bc78af756cdc19294e633240c8a86c620b3d8b5 + languageName: node + linkType: hard + +"@substrate/connect-known-chains@npm:^1.1.5": + version: 1.10.3 + resolution: "@substrate/connect-known-chains@npm:1.10.3" + checksum: 10/b0b4e2914a9c8c0576196ff78f7d0a1ccaf3ee2a02f0b710ee5e79153fdcd4be36e5b7a58998ea72d13f9251dc13d448967114da14efc6aa1891eda284d066bb + languageName: node + linkType: hard + +"@substrate/connect@npm:0.8.11": + version: 0.8.11 + resolution: "@substrate/connect@npm:0.8.11" + dependencies: + "@substrate/connect-extension-protocol": "npm:^2.0.0" + "@substrate/connect-known-chains": "npm:^1.1.5" + "@substrate/light-client-extension-helpers": "npm:^1.0.0" + smoldot: "npm:2.0.26" + checksum: 10/380ba85aa3aec4439fae2ee42173376615ca60262d9c37e6e43d1d65d0d0f63f38c009bb476e9a612b0b9985c1b5808c4d9a75aff9e1828c77e75c8b7584d824 + languageName: node + linkType: hard + +"@substrate/light-client-extension-helpers@npm:^1.0.0": + version: 1.0.0 + resolution: "@substrate/light-client-extension-helpers@npm:1.0.0" + dependencies: + "@polkadot-api/json-rpc-provider": "npm:^0.0.1" + "@polkadot-api/json-rpc-provider-proxy": "npm:^0.1.0" + "@polkadot-api/observable-client": "npm:^0.3.0" + "@polkadot-api/substrate-client": "npm:^0.1.2" + "@substrate/connect-extension-protocol": "npm:^2.0.0" + "@substrate/connect-known-chains": "npm:^1.1.5" + rxjs: "npm:^7.8.1" + peerDependencies: + smoldot: 2.x + checksum: 10/ca0726e8271aa9eb4f1edbb13e7f6986d45c9a4ae9a73a1a14aa9a41552821ca291a33459b7e8fc1ec1bde1ead9336a8bca4fb8781c060d5cbdd7e59ca96cb2d + languageName: node + linkType: hard + +"@substrate/ss58-registry@npm:^1.51.0": + version: 1.51.0 + resolution: "@substrate/ss58-registry@npm:1.51.0" + checksum: 10/34eb21292f543a8be7c62ad3bcdae89d61c8a51e35a0be4687b6b4e955b5180a90a7691a9e6779f7509f8dfcfdfa372d8278087a9668521b9c501adb85c915b6 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -652,6 +2030,15 @@ __metadata: languageName: node linkType: hard +"@types/bn.js@npm:^5.1.6, @types/bn.js@npm:^5.2.0": + version: 5.2.0 + resolution: "@types/bn.js@npm:5.2.0" + dependencies: + "@types/node": "npm:*" + checksum: 10/06c93841f74e4a5e5b81b74427d56303b223c9af36389b4cd3c562bda93f43c425c7e241aee1b0b881dde57238dc2e07f21d30d412b206a7dae4435af4c054e8 + languageName: node + linkType: hard + "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -669,6 +2056,27 @@ __metadata: languageName: node linkType: hard +"@types/docker-modem@npm:*": + version: 3.0.6 + resolution: "@types/docker-modem@npm:3.0.6" + dependencies: + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10/cc58e8189f6ec5a2b8ca890207402178a97ddac8c80d125dc65d8ab29034b5db736de15e99b91b2d74e66d14e26e73b6b8b33216613dd15fd3aa6b82c11a83ed + languageName: node + linkType: hard + +"@types/dockerode@npm:^3.3.35": + version: 3.3.47 + resolution: "@types/dockerode@npm:3.3.47" + dependencies: + "@types/docker-modem": "npm:*" + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10/b840ae7872398a3b02e5789006a69d0cf5bb7ec6c0eb714c7ca04ca093add8de4cd06204ecd8f01388e347e62927cf4c599e8b7dba53e81c1350910da766d517 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -676,6 +2084,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*, @types/node@npm:>=13.7.0": + version: 25.9.0 + resolution: "@types/node@npm:25.9.0" + dependencies: + undici-types: "npm:>=7.24.0 <7.24.7" + checksum: 10/8725e4e3191ba81626b322cfb80b62064c687d5da2983d7318068069f940a9c019e6f342a674ccc4ad26ef6f0a5dcbc7451a81610155ca2c6d5202800b144a19 + languageName: node + linkType: hard + "@types/node@npm:24.10.1": version: 24.10.1 resolution: "@types/node@npm:24.10.1" @@ -685,6 +2102,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.130 + resolution: "@types/node@npm:18.19.130" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/ebb85c6edcec78df926de27d828ecbeb1b3d77c165ceef95bfc26e171edbc1924245db4eb2d7d6230206fe6b1a1f7665714fe1c70739e9f5980d8ce31af6ef82 + languageName: node + linkType: hard + "@types/object-inspect@npm:^1.8.1": version: 1.13.0 resolution: "@types/object-inspect@npm:1.13.0" @@ -699,6 +2125,68 @@ __metadata: languageName: node linkType: hard +"@types/ssh2-streams@npm:*": + version: 0.1.13 + resolution: "@types/ssh2-streams@npm:0.1.13" + dependencies: + "@types/node": "npm:*" + checksum: 10/182c9de8384e11fcfed04e447c3c1d37f898ed4e7f0be0cc58b3bd5b23e22957c17939b68f709092cece758a4befa92913dd967115f643fa0e2dc629fc2e2383 + languageName: node + linkType: hard + +"@types/ssh2@npm:*": + version: 1.15.5 + resolution: "@types/ssh2@npm:1.15.5" + dependencies: + "@types/node": "npm:^18.11.18" + checksum: 10/dd6f29f4e96ea43aa61d29a4a3ad87ad8d11bf1bef637b2848958abd94b05d28754cc611eac13f52d43bd1f51afe7c660cd1c8533ae06878b5739888f4ea0d99 + languageName: node + linkType: hard + +"@types/ssh2@npm:^0.5.48": + version: 0.5.52 + resolution: "@types/ssh2@npm:0.5.52" + dependencies: + "@types/node": "npm:*" + "@types/ssh2-streams": "npm:*" + checksum: 10/fc2584af091da49da9d6628dd8a5e851b217bb9b1b732b0361903894f2730ab3fdf8634f954be34c5a513f7eb0b2772d059d64062bcf6b4a0eb73bfc83c4b858 + languageName: node + linkType: hard + +"@types/ws@npm:^8.5.10": + version: 8.18.1 + resolution: "@types/ws@npm:8.18.1" + dependencies: + "@types/node": "npm:*" + checksum: 10/1ce05e3174dcacf28dae0e9b854ef1c9a12da44c7ed73617ab6897c5cbe4fccbb155a20be5508ae9a7dde2f83bd80f5cf3baa386b934fc4b40889ec963e94f3a + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/coverage-v8@npm:4.0.15" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.0.15" + ast-v8-to-istanbul: "npm:^0.3.8" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.1" + obug: "npm:^2.1.1" + std-env: "npm:^3.10.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + "@vitest/browser": 4.0.15 + vitest: 4.0.15 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10/cdf5d26ba7f6f3895f72662549298e216f810a6cfce8a337d81d8b738df62f0766e0bb5c74f44b09d1282d4a83e14ac63e65c95cef461ac066f4b348c228f9a6 + languageName: node + linkType: hard + "@vitest/expect@npm:4.0.15": version: 4.0.15 resolution: "@vitest/expect@npm:4.0.15" @@ -755,27 +2243,63 @@ __metadata: version: 4.0.15 resolution: "@vitest/snapshot@npm:4.0.15" dependencies: - "@vitest/pretty-format": "npm:4.0.15" - magic-string: "npm:^0.30.21" - pathe: "npm:^2.0.3" - checksum: 10/f881257fc1c520541131296f9762d627ad61eb167a3d7129942a5c2dce46e870af1a8446fbf94d2fcdc5a31ab787ffff113f2b8dbd75b15d0494fe43db649682 + "@vitest/pretty-format": "npm:4.0.15" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10/f881257fc1c520541131296f9762d627ad61eb167a3d7129942a5c2dce46e870af1a8446fbf94d2fcdc5a31ab787ffff113f2b8dbd75b15d0494fe43db649682 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/spy@npm:4.0.15" + checksum: 10/700b06beb4fd33c1430bc5061e7c3055df9ad1e64500a0a02edba6a52e37ba3bf800eadfda1f617e1eeca53d7ab6941a69ba2812980347fcc3c3b736c5ae5a56 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/utils@npm:4.0.15" + dependencies: + "@vitest/pretty-format": "npm:4.0.15" + tinyrainbow: "npm:^3.0.3" + checksum: 10/54d3fd272e05ad43913d842a25dce705eb71db8591511f28fa4a6d0c28fd5eb109c580072e9f8dbc0f431425c890b74494c9d0b14f78d0be18ab87071f06d020 + languageName: node + linkType: hard + +"@wry/caches@npm:^1.0.0": + version: 1.0.1 + resolution: "@wry/caches@npm:1.0.1" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/055f592ee52b5fd9aa86e274e54e4a8b2650f619000bf6f61880ce14aaf47eb2ab34f3ada2eab964fe8b2f19bf8097ecacddcea4638fcc64c3d3a0a512aaa07c + languageName: node + linkType: hard + +"@wry/context@npm:^0.7.0": + version: 0.7.4 + resolution: "@wry/context@npm:0.7.4" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/70d648949a97a035b2be2d6ddb716d4162113e850ab2c4c86331b2da94a7e826204080ce04eee2a95665bd3a0b245bf2ea3aae9adfa57b004ae0d2d49bdb5c8f languageName: node linkType: hard -"@vitest/spy@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/spy@npm:4.0.15" - checksum: 10/700b06beb4fd33c1430bc5061e7c3055df9ad1e64500a0a02edba6a52e37ba3bf800eadfda1f617e1eeca53d7ab6941a69ba2812980347fcc3c3b736c5ae5a56 +"@wry/equality@npm:^0.5.6": + version: 0.5.7 + resolution: "@wry/equality@npm:0.5.7" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/69dccf33c0c41fd7ec5550f5703b857c6484a949412ad747001da941270ea436648c3ab988a2091765304249585ac30c7b417fad8be9a7ce19c1221f71548e35 languageName: node linkType: hard -"@vitest/utils@npm:4.0.15": - version: 4.0.15 - resolution: "@vitest/utils@npm:4.0.15" +"@wry/trie@npm:^0.5.0": + version: 0.5.0 + resolution: "@wry/trie@npm:0.5.0" dependencies: - "@vitest/pretty-format": "npm:4.0.15" - tinyrainbow: "npm:^3.0.3" - checksum: 10/54d3fd272e05ad43913d842a25dce705eb71db8591511f28fa4a6d0c28fd5eb109c580072e9f8dbc0f431425c890b74494c9d0b14f78d0be18ab87071f06d020 + tslib: "npm:^2.3.0" + checksum: 10/578a08f3a96256c9b163230337183d9511fd775bdfe147a30561ccaacedc9ce33b9731ee6e591bb1f5f53e41b26789e519b47dff5100c7bf4e1cd2df3062f797 languageName: node linkType: hard @@ -786,6 +2310,29 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10/ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40 + languageName: node + linkType: hard + +"abstract-level@npm:^3.0.0, abstract-level@npm:^3.1.0": + version: 3.1.1 + resolution: "abstract-level@npm:3.1.1" + dependencies: + buffer: "npm:^6.0.3" + is-buffer: "npm:^2.0.5" + level-supports: "npm:^6.2.0" + level-transcoder: "npm:^1.0.1" + maybe-combine-errors: "npm:^1.0.0" + module-error: "npm:^1.0.1" + checksum: 10/1a4d19efac7a8781972aa5e8a57dce39b3ada75a15c1ee25c8dce5978d72b5f9e2bc8d7fbfabafdc49b5941c5b1913465331864b3061fd0d0ed351a397624b46 + languageName: node + linkType: hard + "acorn-walk@npm:^8.1.1": version: 8.3.4 resolution: "acorn-walk@npm:8.3.4" @@ -804,6 +2351,15 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:6": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: 10/21fb903e0917e5cb16591b4d0ef6a028a54b83ac30cd1fca58dece3d4e0990512a8723f9f83130d88a41e2af8b1f7be1386fda3ea2d181bb1a62155e75e95e23 + languageName: node + linkType: hard + "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.4 resolution: "agent-base@npm:7.1.4" @@ -834,10 +2390,33 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.3 - resolution: "ansi-styles@npm:6.2.3" - checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 +"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": + version: 5.0.2 + resolution: "archiver-utils@npm:5.0.2" + dependencies: + glob: "npm:^10.0.0" + graceful-fs: "npm:^4.2.0" + is-stream: "npm:^2.0.1" + lazystream: "npm:^1.0.0" + lodash: "npm:^4.17.15" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 10/9dde4aa3f0cb1bdfe0b3d4c969f82e6cca9ae76338b7fee6f0071a14a2a38c0cdd1c41ecd3e362466585aa6cc5d07e9e435abea8c94fd9c7ace35f184abef9e4 + languageName: node + linkType: hard + +"archiver@npm:^7.0.1": + version: 7.0.1 + resolution: "archiver@npm:7.0.1" + dependencies: + archiver-utils: "npm:^5.0.2" + async: "npm:^3.2.4" + buffer-crc32: "npm:^1.0.0" + readable-stream: "npm:^4.0.0" + readdir-glob: "npm:^1.1.2" + tar-stream: "npm:^3.0.0" + zip-stream: "npm:^6.0.1" + checksum: 10/81c6102db99d7ffd5cb2aed02a678f551c6603991a059ca66ef59249942b835a651a3d3b5240af4f8bec4e61e13790357c9d1ad4a99982bd2cc4149575c31d67 languageName: node linkType: hard @@ -848,6 +2427,15 @@ __metadata: languageName: node linkType: hard +"asn1@npm:^0.2.6": + version: 0.2.6 + resolution: "asn1@npm:0.2.6" + dependencies: + safer-buffer: "npm:~2.1.0" + checksum: 10/cf629291fee6c1a6f530549939433ebf32200d7849f38b810ff26ee74235e845c0c12b2ed0f1607ac17383d19b219b69cefa009b920dab57924c5c544e495078 + languageName: node + linkType: hard + "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -855,6 +2443,83 @@ __metadata: languageName: node linkType: hard +"ast-v8-to-istanbul@npm:^0.3.8": + version: 0.3.12 + resolution: "ast-v8-to-istanbul@npm:0.3.12" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10/a457149c1f3acd0a99ba0b1add4e34787f0a20e453e1e44df45e15f4aebc878a42bb2aa3d9902c3a87cea48a6d279ab05c5d4edd4d4f6a3c221b0b673140f33b + languageName: node + linkType: hard + +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10/3d49e7acbeee9e84537f4cb0e0f91893df8eba976759875ae8ee9e3d3c82f6ecdebdb347c2fad9926b92596d93cdfc78ecc988bcdf407e40433e8e8e6fe5d78e + languageName: node + linkType: hard + +"async-lock@npm:^1.4.1": + version: 1.4.1 + resolution: "async-lock@npm:1.4.1" + checksum: 10/80d55ac95f920e880a865968b799963014f6d987dd790dd08173fae6e1af509d8cd0ab45a25daaca82e3ef8e7c939f5d128cd1facfcc5c647da8ac2409e20ef9 + languageName: node + linkType: hard + +"async@npm:^3.2.4": + version: 3.2.6 + resolution: "async@npm:3.2.6" + checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10/3ce727cbc78f69d6a4722517a58ee926c8c21083633b1d3fdf66fd688f6c127a53a592141bd4866f9b63240a86e9d8e974b13919450bd17fa33c2d22c4558ad8 + languageName: node + linkType: hard + +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10/3ab6d2cf46b31394b4607e935ec5c1c3c4f60f3e30f0913d35ea74b51b3585e84f590d09e58067f11762eec71c87d25314ce859030983dc0e4397eed21daa12e + languageName: node + linkType: hard + +"axios@npm:^1.12.0": + version: 1.16.1 + resolution: "axios@npm:1.16.1" + dependencies: + follow-redirects: "npm:^1.16.0" + form-data: "npm:^4.0.5" + https-proxy-agent: "npm:^5.0.1" + proxy-from-env: "npm:^2.1.0" + checksum: 10/9b6218cf96321cfbbf8f160658d695367114bcf4fb62492bdc1ccd647f184b5c71ae400e5ecaaf41079bc561de2ecbaf1fec63f398b3ec53389beff7694df64c + languageName: node + linkType: hard + +"b4a@npm:^1.6.4": + version: 1.8.1 + resolution: "b4a@npm:1.8.1" + peerDependencies: + react-native-b4a: "*" + peerDependenciesMeta: + react-native-b4a: + optional: true + checksum: 10/8536650b525f9f916e8fff9f5976fbeba2fc3238f047cad52e91073cf9825306ce7a68d0077ba2d06e3d20c95b445dccc2ab97ed45773331244d82251329cf8d + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -862,7 +2527,124 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^2.0.2": +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"bare-events@npm:^2.5.4, bare-events@npm:^2.7.0": + version: 2.8.3 + resolution: "bare-events@npm:2.8.3" + peerDependencies: + bare-abort-controller: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + checksum: 10/704252793362d4a422959f3b5d134a3f893f020b515cccf55965c8076941d6e7fd8c23268560693f2300270378a00384156237e4390edda2d4ca0e641bfe774e + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1, bare-fs@npm:^4.5.5": + version: 4.7.1 + resolution: "bare-fs@npm:4.7.1" + dependencies: + bare-events: "npm:^2.5.4" + bare-path: "npm:^3.0.0" + bare-stream: "npm:^2.6.4" + bare-url: "npm:^2.2.2" + fast-fifo: "npm:^1.3.2" + peerDependencies: + bare-buffer: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + checksum: 10/bb873bf8d22c45fd14444b0f9731315a77b696c9387b09cc0df9975b998d1b5db9f4c88aa4b264ce59edeade573689ba9e0ba172003cc8900b2c2ad803f9275b + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.9.1 + resolution: "bare-os@npm:3.9.1" + checksum: 10/2a106aca9eeb1cf41e30403410c9fa81a9e13c25818debc21444f2485158e01e65f10daff37acab0cbf9460c00e64e6bcaedef07b25a9171ec1e45485213ff50 + languageName: node + linkType: hard + +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" + dependencies: + bare-os: "npm:^3.0.1" + checksum: 10/712d90e9cd8c3263cc11b0e0d386d1531a452706d7840c081ee586b34b00d72544e65df7a40013d47c1b177277495225deeede65cb2984db88a979cb65aaa2ff + languageName: node + linkType: hard + +"bare-stream@npm:^2.6.4": + version: 2.13.1 + resolution: "bare-stream@npm:2.13.1" + dependencies: + streamx: "npm:^2.25.0" + teex: "npm:^1.0.1" + peerDependencies: + bare-abort-controller: "*" + bare-buffer: "*" + bare-events: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + checksum: 10/50aa90a7005d71c1af8fafcc84f378bd4d7c2dd293a581ffe3899bee39b0d2eb07c47e1092f581fa5b199a63c0ad2618b150c0ab716658727e3fcc7fd7d1e401 + languageName: node + linkType: hard + +"bare-url@npm:^2.2.2": + version: 2.4.3 + resolution: "bare-url@npm:2.4.3" + dependencies: + bare-path: "npm:^3.0.0" + checksum: 10/e2c16dd57e0c4b974813d9acd626b96e83a8894e19b0bf780de4bef40a7000c697984a47c398c8f612aa7991974bfb97f1c3c3fd410085a55fa5db15d1ba6309 + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"bcrypt-pbkdf@npm:^1.0.2": + version: 1.0.2 + resolution: "bcrypt-pbkdf@npm:1.0.2" + dependencies: + tweetnacl: "npm:^0.14.3" + checksum: 10/13a4cde058250dbf1fa77a4f1b9a07d32ae2e3b9e28e88a0c7a1827835bc3482f3e478c4a0cfd4da6ff0c46dae07da1061123a995372b32cc563d9975f975404 + languageName: node + linkType: hard + +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 + languageName: node + linkType: hard + +"bn.js@npm:^5.2.1, bn.js@npm:^5.2.3": + version: 5.2.3 + resolution: "bn.js@npm:5.2.3" + checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": version: 2.1.0 resolution: "brace-expansion@npm:2.1.0" dependencies: @@ -871,6 +2653,65 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.5": + version: 5.0.6 + resolution: "brace-expansion@npm:5.0.6" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/a7acf120fefa79e9d7c9c92898114f57c07596a3920197f3c5917e6a628b04220a5f7f9618c30bdd973a6576a32113b99f9c3f1c8245ccc399dd2a9a718d81d8 + languageName: node + linkType: hard + +"browser-level@npm:^3.0.0": + version: 3.0.0 + resolution: "browser-level@npm:3.0.0" + dependencies: + abstract-level: "npm:^3.1.0" + checksum: 10/719e9aa36fb85ed7bd9d06267961c7b151866422e4ff4e97cc82966c6fdefcc13a19bbd2cefe151d57af21bf7d2e2419e758f8646af445dca47d8ab191e7236b + languageName: node + linkType: hard + +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 10/ef3b7c07622435085c04300c9a51e850ec34a27b2445f758eef69b859c7827848c2282f3840ca6c1eef3829145a1580ce540cab03ccf4433827a2b95d3b09ca7 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10/b6bc68237ebf29bdacae48ce60e5e28fc53ae886301f2ad9496618efac49427ed79096750033e7eab1897a4f26ae374ace49106a5758f38fb70c78c9fda2c3b1 + languageName: node + linkType: hard + +"buildcheck@npm:~0.0.6": + version: 0.0.7 + resolution: "buildcheck@npm:0.0.7" + checksum: 10/cca174bcc917ee9dc00b1be404b4f22656d9c243d439d3456e6bd52263f05ad5f5d3c77e62a1f6ccaf1d36cb65efc5ee3bb30ed10e1675f22a1abdfad99eb9b3 + languageName: node + linkType: hard + +"byline@npm:^5.0.0": + version: 5.0.0 + resolution: "byline@npm:5.0.0" + checksum: 10/737ca83e8eda2976728dae62e68bc733aea095fab08db4c6f12d3cee3cf45b6f97dce45d1f6b6ff9c2c947736d10074985b4425b31ce04afa1985a4ef3d334a7 + languageName: node + linkType: hard + "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -891,6 +2732,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 + languageName: node + linkType: hard + "chai@npm:^6.2.1": version: 6.2.1 resolution: "chai@npm:6.2.1" @@ -905,6 +2756,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -912,6 +2770,19 @@ __metadata: languageName: node linkType: hard +"classic-level@npm:^3.0.0": + version: 3.0.0 + resolution: "classic-level@npm:3.0.0" + dependencies: + abstract-level: "npm:^3.1.0" + module-error: "npm:^1.0.1" + napi-macros: "npm:^2.2.2" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/96c07b0ca6f38dc5535c040804fdb845f728dcabd12838dafbcb379ca4b4cce906fb14c4ab8d871b3798f0e27a7815b9f584be535d1e00089f1104da97e44f95 + languageName: node + linkType: hard + "cli-cursor@npm:^5.0.0": version: 5.0.0 resolution: "cli-cursor@npm:5.0.0" @@ -928,6 +2799,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -944,12 +2826,40 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^2.0.7": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10/2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4 + languageName: node + linkType: hard + +"compact-deployer-example-fungible-token@workspace:examples/fungible-token": + version: 0.0.0-use.local + resolution: "compact-deployer-example-fungible-token@workspace:examples/fungible-token" + dependencies: + "@openzeppelin/compact-cli": "workspace:^" + "@openzeppelin/compact-deployer": "workspace:^" + languageName: unknown + linkType: soft + "compact-tools-monorepo@workspace:.": version: 0.0.0-use.local resolution: "compact-tools-monorepo@workspace:." dependencies: "@biomejs/biome": "npm:2.3.8" + "@openzeppelin/compact-deployer": "workspace:^" "@types/node": "npm:24.10.1" + "@vitest/coverage-v8": "npm:4.0.15" + pino: "npm:^9.7.0" ts-node: "npm:^10.9.2" turbo: "npm:^2.6.1" typescript: "npm:^5.9.3" @@ -957,6 +2867,65 @@ __metadata: languageName: unknown linkType: soft +"compress-commons@npm:^6.0.2": + version: 6.0.2 + resolution: "compress-commons@npm:6.0.2" + dependencies: + crc-32: "npm:^1.2.0" + crc32-stream: "npm:^6.0.0" + is-stream: "npm:^2.0.1" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 10/78e3ba10aeef919a1c5bbac21e120f3e1558a31b2defebbfa1635274fc7f7e8a3a0ee748a06249589acd0b33a0d58144b8238ff77afc3220f8d403a96fcc13aa + languageName: node + linkType: hard + +"copy-anything@npm:^4": + version: 4.0.5 + resolution: "copy-anything@npm:4.0.5" + dependencies: + is-what: "npm:^5.2.0" + checksum: 10/1ee7e6f55c1016a47871ecd09aa765ca825c1ec89c46e6f58686016c80c6fe3d36452a6010d8498c766ea5d60bc5d892d9511b41310a7355b48ac10b39c90c9a + languageName: node + linkType: hard + +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 10/9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + +"cpu-features@npm:~0.0.10": + version: 0.0.10 + resolution: "cpu-features@npm:0.0.10" + dependencies: + buildcheck: "npm:~0.0.6" + nan: "npm:^2.19.0" + node-gyp: "npm:latest" + checksum: 10/941b828ffe77582b2bdc03e894c913e2e2eeb5c6043ccb01338c34446d026f6888dc480ecb85e684809f9c3889d245f3648c7907eb61a92bdfc6aed039fcda8d + languageName: node + linkType: hard + +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: 10/824f696a5baaf617809aa9cd033313c8f94f12d15ebffa69f10202480396be44aef9831d900ab291638a8022ed91c360696dd5b1ba691eb3f34e60be8835b7c3 + languageName: node + linkType: hard + +"crc32-stream@npm:^6.0.0": + version: 6.0.0 + resolution: "crc32-stream@npm:6.0.0" + dependencies: + crc-32: "npm:^1.2.0" + readable-stream: "npm:^4.0.0" + checksum: 10/e6edc2f81bc387daef6d18b2ac18c2ffcb01b554d3b5c7d8d29b177505aafffba574658fdd23922767e8dab1183d1962026c98c17e17fb272794c33293ef607c + languageName: node + linkType: hard + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -964,6 +2933,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.0.0, cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: "npm:^2.7.0" + checksum: 10/07624940607b64777d27ec9c668ddb6649e8c59ee0a5a10e63a51ce857e2bbb1294a45854a31c10eccb91b65909a5b199fcb0217339b44156f85900a7384f489 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -975,7 +2953,21 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4": +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10/5c149c91bf9ce2142c89f84eee4c585f0cb1f6faf2536b1af89873f862666a28529d1ccafc44750aa01384da2197c4f76f4e149a3cc0c1cb2c46f5cc45f2bcb5 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.3.5": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -987,17 +2979,81 @@ __metadata: languageName: node linkType: hard -"diff@npm:^4.0.1": - version: 4.0.2 - resolution: "diff@npm:4.0.2" - checksum: 10/ec09ec2101934ca5966355a229d77afcad5911c92e2a77413efda5455636c4cf2ce84057e2d7715227a2eeeda04255b849bd3ae3a4dd22eb22e86e76456df069 +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10/46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.1": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10/ec09ec2101934ca5966355a229d77afcad5911c92e2a77413efda5455636c4cf2ce84057e2d7715227a2eeeda04255b849bd3ae3a4dd22eb22e86e76456df069 + languageName: node + linkType: hard + +"docker-compose@npm:^0.24.8": + version: 0.24.8 + resolution: "docker-compose@npm:0.24.8" + dependencies: + yaml: "npm:^2.2.2" + checksum: 10/2b8526f9797a55c819ff2d7dcea57085b012b3a3d77bc2e1a6b45c3fc9e82196312f5298cbe8299966462454a5ac8f68814bb407736b4385e0d226a2a39e877a + languageName: node + linkType: hard + +"docker-modem@npm:^5.0.7": + version: 5.0.7 + resolution: "docker-modem@npm:5.0.7" + dependencies: + debug: "npm:^4.1.1" + readable-stream: "npm:^3.5.0" + split-ca: "npm:^1.0.1" + ssh2: "npm:^1.15.0" + checksum: 10/8c0dc9908e10fbc91c35b187fc6a67a0dcbe4b33a2198dfa67cd8304e0f2452325e1639215674d6e441731d0bf27f06339550f6c3767585b877601d2f16e43e2 + languageName: node + linkType: hard + +"dockerode@npm:^4.0.5": + version: 4.0.12 + resolution: "dockerode@npm:4.0.12" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + "@grpc/grpc-js": "npm:^1.11.1" + "@grpc/proto-loader": "npm:^0.7.13" + docker-modem: "npm:^5.0.7" + protobufjs: "npm:^7.3.2" + tar-fs: "npm:^2.1.4" + uuid: "npm:^10.0.0" + checksum: 10/e08b15ba2ba41e93e61cac472e525efff48851b0eaaba75e5075cf540760099658f57883b08334ccc3fee021c4ca286013c76a00890b5d0716892b8ff678b2d1 + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 languageName: node linkType: hard -"eastasianwidth@npm:^0.2.0": - version: 0.2.0 - resolution: "eastasianwidth@npm:0.2.0" - checksum: 10/9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 +"effect@npm:^3.19.19, effect@npm:^3.20.0": + version: 3.21.2 + resolution: "effect@npm:3.21.2" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + fast-check: "npm:^3.23.1" + checksum: 10/e1bf90d9010e6b4d8389937e80e96884e49164b8b1658230cf2aaf9d2a3844d1698a6854fd8183a82a0335bdcbc37879d9af84491b52a57bf16ab52052cf6f46 languageName: node linkType: hard @@ -1008,13 +3064,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^9.2.2": - version: 9.2.2 - resolution: "emoji-regex@npm:9.2.2" - checksum: 10/915acf859cea7131dac1b2b5c9c8e35c4849e325a1d114c30adb8cd615970f6dca0e27f64f3a4949d7d6ed86ecd79a1c5c63f02e697513cddd7b5835c90948b8 - languageName: node - linkType: hard - "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -1024,6 +3073,15 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10/1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -1038,6 +3096,20 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 + languageName: node + linkType: hard + "es-module-lexer@npm:^1.7.0": version: 1.7.0 resolution: "es-module-lexer@npm:1.7.0" @@ -1045,6 +3117,27 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f + languageName: node + linkType: hard + "esbuild@npm:^0.27.0": version: 0.27.7 resolution: "esbuild@npm:0.27.7" @@ -1134,6 +3227,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 + languageName: node + linkType: hard + "estree-walker@npm:^3.0.3": version: 3.0.3 resolution: "estree-walker@npm:3.0.3" @@ -1143,6 +3243,36 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10/49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 + languageName: node + linkType: hard + +"eventemitter3@npm:^5.0.1": + version: 5.0.4 + resolution: "eventemitter3@npm:5.0.4" + checksum: 10/54f5c8c543650d65f92d03dbef1bb73a682a920490c44699ad8f863a6b19bbca42fb7409aa09ca09cb98a44149d9a7bc1dffd55ca88a740bd928c7be0ad666a0 + languageName: node + linkType: hard + +"events-universal@npm:^1.0.0": + version: 1.0.1 + resolution: "events-universal@npm:1.0.1" + dependencies: + bare-events: "npm:^2.7.0" + checksum: 10/71b2e6079b4dc030c613ef73d99f1acb369dd3ddb6034f49fd98b3e2c6632cde9f61c15fb1351004339d7c79672252a4694ecc46a6124dc794b558be50a83867 + languageName: node + linkType: hard + +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be + languageName: node + linkType: hard + "expect-type@npm:^1.2.2": version: 1.2.2 resolution: "expect-type@npm:1.2.2" @@ -1157,6 +3287,15 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:^3.23.1": + version: 3.23.2 + resolution: "fast-check@npm:3.23.2" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10/dab344146b778e8bc2973366ea55528d1b58d3e3037270262b877c54241e800c4d744957722c24705c787020d702aece11e57c9e3dbd5ea19c3e10926bf1f3fe + languageName: node + linkType: hard + "fast-check@npm:^4.5.2": version: 4.5.2 resolution: "fast-check@npm:4.5.2" @@ -1166,6 +3305,27 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^4.0.0": + version: 4.0.3 + resolution: "fast-copy@npm:4.0.3" + checksum: 10/1e74e8b18a83f125b697b0dc7d802b4c73ec2aba7b181458e5e72d46a261faefcdee22ad9fa682c77f4606133451342f95de9835c2c804c481472585fa6ded26 + languageName: node + linkType: hard + +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -1178,7 +3338,41 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0": +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b + languageName: node + linkType: hard + +"fetch-retry@npm:^6.0.0": + version: 6.0.0 + resolution: "fetch-retry@npm:6.0.0" + checksum: 10/0c8d3082e2d76fff2df75adef6280bc854bc36fd3ef38506674f0216d0d819e2efd14da7477d3f1732415aea1d2cfde7cd3e1aeae46f45f2adbfc5133296e8de + languageName: node + linkType: hard + +"find-my-way-ts@npm:^0.1.6": + version: 0.1.6 + resolution: "find-my-way-ts@npm:0.1.6" + checksum: 10/b95bf644011f0d341e5963aa4cac55b2ee59e2435d3f65ae5cf9ee80e52f0fc7db0cee9a55e7420a62a2cec7d8bec7538399dada45e024c05488daa754451bcc + languageName: node + linkType: hard + +"follow-redirects@npm:^1.16.0": + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd + languageName: node + linkType: hard + +"foreground-child@npm:^3.3.1": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" dependencies: @@ -1188,6 +3382,35 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10/52ecd6e927c8c4e215e68a7ad5e0f7c1031397439672fd9741654b4a94722c4182e74cc815b225dcb5be3f4180f36428f67c6dd39eaa98af0dcfdd26c00c19cd + languageName: node + linkType: hard + +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -1216,6 +3439,27 @@ __metadata: languageName: node linkType: hard +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10/185e20d20f10c8d661d59aac0f3b63b31132d492e1b11fcc2a93cb2c47257ebaee7407c38513efd2b35cafdf972d9beb2ea4593c1e0f3bf8f2744836928d7454 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-east-asian-width@npm:^1.3.0": version: 1.4.0 resolution: "get-east-asian-width@npm:1.4.0" @@ -1223,29 +3467,166 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": - version: 10.5.0 - resolution: "glob@npm:10.5.0" +"get-intrinsic@npm:^1.2.6": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10/bb579dda84caa4a3a41611bdd483dade7f00f246f2a7992eb143c5861155290df3fdb48a8406efa3dfb0b434e2c8fafa4eebd469e409d0439247f85fc3fa2cc1 + languageName: node + linkType: hard + +"get-port@npm:^7.1.0": + version: 7.2.0 + resolution: "get-port@npm:7.2.0" + checksum: 10/f8785ccdcc52b1e03f1b1de3fcd46dbc41fe4079e234f2727c3e154ca76bb94318fb0d341daa28a6c87eff24ad4016eaa8b1b4e26eff0d6a2196dd1c1ffc63a1 + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + +"glob@npm:^11.0.0": + version: 11.1.0 + resolution: "glob@npm:11.1.0" dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" + foreground-child: "npm:^3.3.1" + jackspeak: "npm:^4.1.1" + minimatch: "npm:^10.1.1" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" + path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 + checksum: 10/da4501819633daff8822c007bb3f93d5c4d2cbc7b15a8e886660f4497dd251a1fb4f53a85fba1e760b31704eff7164aeb2c7a82db10f9f2c362d12c02fe52cf3 languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard +"graphql-http@npm:^1.22.4": + version: 1.22.4 + resolution: "graphql-http@npm:1.22.4" + peerDependencies: + graphql: ">=0.11 <=16" + checksum: 10/ef81c3d86ac75743509d225aaf88a79262adee8801035712e5af655deedd5755afb0060e68306ca54aa54067c4ef0a382a03b2ecde016e0fb43454b73184a04d + languageName: node + linkType: hard + +"graphql-tag@npm:^2.12.6": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: "npm:^2.1.0" + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10/23a2bc1d3fbeae86444204e0ac08522e09dc369559ba75768e47421a7321b59f352fb5b2c9a5c37d3cf6de890dca4e5ac47e740c7cc622e728572ecaa649089e + languageName: node + linkType: hard + +"graphql-ws@npm:^6.0.7, graphql-ws@npm:^6.0.8": + version: 6.0.8 + resolution: "graphql-ws@npm:6.0.8" + peerDependencies: + "@fastify/websocket": ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + ws: ^8 + peerDependenciesMeta: + "@fastify/websocket": + optional: true + crossws: + optional: true + ws: + optional: true + checksum: 10/503d581c7dab4b9a884dad844fa9642a896803161aa1f1c8d3f12619e4e428f43cb39fe06a198c30bb685a521689d525b2870539c07bd68bb4bf704d039bdd9a + languageName: node + linkType: hard + +"graphql@npm:^16.13.0, graphql@npm:^16.13.2": + version: 16.14.0 + resolution: "graphql@npm:16.14.0" + checksum: 10/019bed00a1d62c90d38bd8971f827af9be479bd1935ac990b62edce8dbe5d9e1d93cae72e986199fdeb7108ee83e3f73c7492989ec08fcaf446b6bd79d533741 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10/261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10/959385c98696ebbca51e7534e0dc723ada325efa3475350951363cce216d27373e0259b63edb599f72eb94d6cde8577b4b2375f080b303947e560f85692834fa + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: "npm:^1.0.3" + checksum: 10/c74c5f5ceee3c8a5b8bc37719840dc3749f5b0306d818974141dda2471a1a2ca6c8e46b9d6ac222c5345df7a901c9b6f350b1e6d62763fec877e26609a401bfe + languageName: node + linkType: hard + +"hasown@npm:^2.0.2": + version: 2.0.3 + resolution: "hasown@npm:2.0.3" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10/619526379cda755409d856cbf3c65b82ea342151719a0a550920cf7d6a7f58f7cf079e5a78f3acd162324fc784a3d3d6f6f61aff613b47a0163c16fbe09ea89f + languageName: node + linkType: hard + +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10/034d74029dcca544a34fb6135e98d427acd73019796ffc17383eaa3ec2fe1c0471dcbbc8f8ed39e46e86d43ccd753a160631615e4048285e313569609b66d5b7 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -1263,6 +3644,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/f0dce7bdcac5e8eaa0be3c7368bb8836ed010fb5b6349ffb412b172a203efe8f807d9a6681319105ea1b6901e1972c7b5ea899672a7b9aad58309f766dcbe0df + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -1282,6 +3673,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -1289,6 +3687,13 @@ __metadata: languageName: node linkType: hard +"inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 + languageName: node + linkType: hard + "ip-address@npm:^10.0.1": version: 10.2.0 resolution: "ip-address@npm:10.2.0" @@ -1296,6 +3701,13 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:^2.0.5": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 10/3261a8b858edcc6c9566ba1694bf829e126faa88911d1c0a747ea658c5d81b14b6955e3a702d59dabadd58fdd440c01f321aa71d6547105fd21d03f94d0597e7 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -1310,37 +3722,174 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": - version: 2.1.0 - resolution: "is-unicode-supported@npm:2.1.0" - checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 +"is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + +"is-what@npm:^5.2.0": + version: 5.5.0 + resolution: "is-what@npm:5.5.0" + checksum: 10/d53a6ea1aebf953f3bcf711a28e8463bfe79fc0e4e87575d77c692a30fd3d98f87b88d4c006c06753bf85f771c9d2c1d05b2c6b03c246883261fe190526195d9 + languageName: node + linkType: hard + +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10/7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: 10/e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10/40bbdd1e937dfd8c830fa286d0f665e81b7a78bdabcd4565f6d5667c99828bda3db7fb7ac6b96a3e2e8a2461ddbc5452d9f8bc7d00cb00075fa6a3e99f5b6a81 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10/86a83421ca1cf2109a9f6d193c06c31ef04a45e72a74579b11060b1e7bb9b6337a4e6f04abfb8857e2d569c271273c65e855ee429376a0d7c91ad91db42accd1 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.2.0": + version: 3.2.0 + resolution: "istanbul-reports@npm:3.2.0" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10/6773a1d5c7d47eeec75b317144fe2a3b1da84a44b6282bebdc856e09667865e58c9b025b75b3d87f5bc62939126cbba4c871ee84254537d934ba5da5d4c4ec4e + languageName: node + linkType: hard + +"jackspeak@npm:^4.1.1": + version: 4.2.3 + resolution: "jackspeak@npm:4.2.3" + dependencies: + "@isaacs/cliui": "npm:^9.0.0" + checksum: 10/b88e3fe5fa04d34f0f939a15b7cef4a8589999b7a366ef89a3e0f2c45d2a7666066b67cbf46d57c3a4796a76d27b9d869b23d96a803dd834200d222c2a70de7e + languageName: node + linkType: hard + +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 + languageName: node + linkType: hard + +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10/88f536ec89f076fc230d29df255b3c55531237669d746d1868fca716b1e3f5f2e4abf8e5b8701903216e3f00d2dc3918d078b35da87772d433ab6a513c3bf76d + languageName: node + linkType: hard + +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 10/59169a081e4eeb6f9559ae1f938f656191c000e0512aa6df9f3c8b2437a4ab1823819c6b9fd1818a4e39593ccfd72e9a051fdd3e2d1e340ed913679e888ded8c + languageName: node + linkType: hard + +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: 10/35f8cf8b5799c76570b211b079d4d706a20cbf13a4936d44cc7dbdacab1de6b346ab339ed3e3805f4693155ee5bbebbda4050fa2b666d61956e89a573089e3d4 + languageName: node + linkType: hard + +"level-supports@npm:^6.2.0": + version: 6.2.0 + resolution: "level-supports@npm:6.2.0" + checksum: 10/450c04839cf42ac7c73085b4928f1c1c51d9ab179aac9102cc8ef2389faf2d06cebaf57df2d025da89d78465004ccf29bfd972a04b0b35d5d423fa3f4516f906 + languageName: node + linkType: hard + +"level-transcoder@npm:^1.0.1": + version: 1.0.1 + resolution: "level-transcoder@npm:1.0.1" + dependencies: + buffer: "npm:^6.0.3" + module-error: "npm:^1.0.1" + checksum: 10/2fb41a1d8037fc279f851ead8cdc3852b738f1f935ac2895183cd606aae3e57008e085c7c2bd2b2d43cfd057333108cfaed604092e173ac2abdf5ab1b8333f9e languageName: node linkType: hard -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 10/7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 +"level@npm:^10.0.0": + version: 10.0.0 + resolution: "level@npm:10.0.0" + dependencies: + abstract-level: "npm:^3.1.0" + browser-level: "npm:^3.0.0" + classic-level: "npm:^3.0.0" + checksum: 10/c04a81530e0472b7dbcd061ee32fb498675574b45e1121ec3ed8407734ed45a7b4ca7ef72a70a710c53b35a3d77223fc90092877e807e9f21a557c5219e9d54b languageName: node linkType: hard -"isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10/c301cc379310441dc73cd6cebeb91fb254bea74e6ad3027f9346fc43b4174385153df420ffa521654e502fd34c40ef69ca4e7d40ee7129a99e06f306032bfc65 languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/96f8786eaab98e4bf5b2a5d6d9588ea46c4d06bbc4f2eb861fdd7b6b182b16f71d8a70e79820f335d52653b16d4843b29dd9cdcf38ae80406756db9199497cf3 +"lodash@npm:^4.17.15": + version: 4.18.1 + resolution: "lodash@npm:4.18.1" + checksum: 10/306fea53dfd39dad1f03d45ba654a2405aebd35797b673077f401edb7df2543623dc44b9effbb98f69b32152295fff725a4cec99c684098947430600c6af0c3f languageName: node linkType: hard @@ -1354,13 +3903,27 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": +"long@npm:^5.0.0, long@npm:^5.3.2": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10/b6b55ddae56fcce2864d37119d6b02fe28f6dd6d9e44fd22705f86a9254b9321bd69e9ffe35263b4846d54aba197c64882adcb8c543f2383c1e41284b321ea64 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.4.0 + resolution: "lru-cache@npm:11.4.0" + checksum: 10/c6bb5bb7cd1938c6a96ec70e8cae4b2181bca3852013b51b64c3a40dadb14271f1a3337d5f34350d03d9506970e73be5161eddcf7df524fdf4ad0e390e7d534c + languageName: node + linkType: hard + "magic-string@npm:^0.30.21": version: 0.30.21 resolution: "magic-string@npm:0.30.21" @@ -1370,6 +3933,26 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.1": + version: 0.5.3 + resolution: "magicast@npm:0.5.3" + dependencies: + "@babel/parser": "npm:^7.29.3" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10/436ad518726b691cf9ac1a14ab14705784f28075892a092b06e8b17ac7303fe57e8a2789989c68b560653a909a8df49d1582bb73f9bdad4bcbab892201251049 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10/bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -1396,6 +3979,36 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd + languageName: node + linkType: hard + +"maybe-combine-errors@npm:^1.0.0": + version: 1.0.0 + resolution: "maybe-combine-errors@npm:1.0.0" + checksum: 10/16bb6d3dcf79fc61f5a04abe948c4c81cae0da6ee5da9a1d8196f1723b069d6ab60f752bc208e18481e2b82de146e068bc462558c65ecdf96fed0d021a1aa6ab + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10/89aa9651b67644035de2784a6e665fc685d79aba61857e02b9c8758da874a754aed4a9aced9265f5ed1171fd934331e5516b84a7f0218031b6fa0270eca1e51a + languageName: node + linkType: hard + "mimic-function@npm:^5.0.0": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" @@ -1403,12 +4016,28 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.4": - version: 9.0.9 - resolution: "minimatch@npm:9.0.9" +"minimatch@npm:^10.1.1": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" + dependencies: + brace-expansion: "npm:^5.0.5" + checksum: 10/19e87a931aff60ee7b9d80f39f817b8bfc54f61f8356ee3549fbf636dbccacacfec8d803eac73293955c4527cd085247dfc064bce4a5e349f8f3b85e2bf5da0f + languageName: node + linkType: hard + +"minimatch@npm:^5.1.0": + version: 5.1.9 + resolution: "minimatch@npm:5.1.9" dependencies: - brace-expansion: "npm:^2.0.2" - checksum: 10/b91fad937deaffb68a45a2cb731ff3cff1c3baf9b6469c879477ed16f15c8f4ce39d63a3f75c2455107c2fdff0f3ab597d97dc09e2e93b883aafcf926ef0c8f9 + brace-expansion: "npm:^2.0.1" + checksum: 10/23b4feb64dcb77ba93b70a72be551eb2e2677ac02178cf1ed3d38836cc4cd84802d90b77f60ef87f2bac64d270d2d8eba242e428f0554ea4e36bfdb7e9d25d0c + languageName: node + linkType: hard + +"minimist@npm:^1.2.6": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f languageName: node linkType: hard @@ -1472,7 +4101,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 @@ -1488,6 +4117,36 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.4": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 + languageName: node + linkType: hard + +"mock-socket@npm:^9.3.1": + version: 9.3.1 + resolution: "mock-socket@npm:9.3.1" + checksum: 10/c5c07568f2859db6926d79cb61580c07e67958b5cd6b52d1270fdfa17ae066d7f74a18a4208fc4386092eea4e1ee001aa23f015c88a1774265994e4fae34d18e + languageName: node + linkType: hard + +"module-error@npm:^1.0.1": + version: 1.0.2 + resolution: "module-error@npm:1.0.2" + checksum: 10/5d653e35bd55b3e95f8aee2cdac108082ea892e71b8f651be92cde43e4ee86abee4fa8bd7fc3fe5e68b63926d42f63c54cd17b87a560c31f18739295575a3962 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -1495,6 +4154,65 @@ __metadata: languageName: node linkType: hard +"msgpackr-extract@npm:^3.0.2": + version: 3.0.3 + resolution: "msgpackr-extract@npm:3.0.3" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.3" + node-gyp: "npm:latest" + node-gyp-build-optional-packages: "npm:5.2.2" + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: 10/4bfe45cf6968310570765951691f1b8e85b6a837e5197b8232fc9285eef4b457992e73118d9d07c92a52cc23f9e837897b135e17ea0f73e3604540434051b62f + languageName: node + linkType: hard + +"msgpackr@npm:^1.11.10, msgpackr@npm:^1.11.4": + version: 1.11.12 + resolution: "msgpackr@npm:1.11.12" + dependencies: + msgpackr-extract: "npm:^3.0.2" + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 10/8077d7ebf661df831ba119a277588b7e00149d25b6f5630e311c2415504553ce695347a351a7198cdf1f596feaaf91121adc3181e483f7d2c9822484b73babf2 + languageName: node + linkType: hard + +"multipasta@npm:^0.2.7": + version: 0.2.7 + resolution: "multipasta@npm:0.2.7" + checksum: 10/244a7194ff508b3c5c1724f11c303f1c446cf6142cdbe82e57d5e59c44abb4942b1b983dd8c0d9c63080e684b2a8fa10f511df70d42dbef4d215ed7d41e76fcc + languageName: node + linkType: hard + +"nan@npm:^2.19.0, nan@npm:^2.23.0": + version: 2.27.0 + resolution: "nan@npm:2.27.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/bdce0630e417740501394c412bd9f0ed1c287825e3b8f9b7efb95cc3acd3ef69de60479b5f00a2d039b79321e5ce29b672b0b263cfe0e4d8f47c8f810a24a5ee + languageName: node + linkType: hard + "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -1504,6 +4222,13 @@ __metadata: languageName: node linkType: hard +"napi-macros@npm:^2.2.2": + version: 2.2.2 + resolution: "napi-macros@npm:2.2.2" + checksum: 10/2cdb9c40ad4b424b14fbe5e13c5329559e2b511665acf41cdcda172fd2270202dc747a2d288b687c72bc70f654c797bc24a93adb67631128d62461588d7cc070 + languageName: node + linkType: hard + "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -1511,6 +4236,73 @@ __metadata: languageName: node linkType: hard +"nock@npm:^13.5.5": + version: 13.5.6 + resolution: "nock@npm:13.5.6" + dependencies: + debug: "npm:^4.1.0" + json-stringify-safe: "npm:^5.0.1" + propagate: "npm:^2.0.0" + checksum: 10/a57c265b75e5f7767e2f8baf058773cdbf357c31c5fea2761386ec03a008a657f9df921899fe2a9502773b47145b708863b32345aef529b3c45cba4019120f88 + languageName: node + linkType: hard + +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + +"node-gyp-build-optional-packages@npm:5.2.2": + version: 5.2.2 + resolution: "node-gyp-build-optional-packages@npm:5.2.2" + dependencies: + detect-libc: "npm:^2.0.1" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: 10/f448a328cf608071dc8cc4426ac5be0daec4788e4e1759e9f7ffcd286822cc799384edce17a8c79e610c4bbfc8e3aff788f3681f1d88290e0ca7aaa5342a090f + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/6a7d62289d1afc419fc8fc9bd00aa4e554369e50ca0acbc215cb91446148b75ff7e2a3b53c2c5b2c09a39d416d69f3d3237937860373104b5fe429bf30ad9ac5 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 11.5.0 resolution: "node-gyp@npm:11.5.0" @@ -1542,6 +4334,13 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 + languageName: node + linkType: hard + "object-inspect@npm:^1.12.3": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -1556,6 +4355,22 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10/cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + "onetime@npm:^7.0.0": version: 7.0.0 resolution: "onetime@npm:7.0.0" @@ -1565,6 +4380,18 @@ __metadata: languageName: node linkType: hard +"optimism@npm:^0.18.0": + version: 0.18.1 + resolution: "optimism@npm:0.18.1" + dependencies: + "@wry/caches": "npm:^1.0.0" + "@wry/context": "npm:^0.7.0" + "@wry/trie": "npm:^0.5.0" + tslib: "npm:^2.3.0" + checksum: 10/d805f5995d61a417d4fd49a923749db1aa310d1ae8de084ec3a5f589f8b185d9a41b7b4422d33ee75ce43115c264e14bca086f8be2bb182c76448ad08997213a + languageName: node + linkType: hard + "ora@npm:^9.0.0": version: 9.0.0 resolution: "ora@npm:9.0.0" @@ -1603,69 +4430,299 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1": - version: 1.11.1 - resolution: "path-scurry@npm:1.11.1" +"path-scurry@npm:^2.0.0": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 + languageName: node + linkType: hard + +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d + languageName: node + linkType: hard + +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + +"picomatch@npm:^4.0.3": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce + languageName: node + linkType: hard + +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/e5699ecb06c7121055978e988e5cecea5b6892fc2589c64f1f86df5e7386bbbfd2ada268839e911b021c6b3123428aed7c6be3ac7940eee139556c75324c7e83 + languageName: node + linkType: hard + +"pino-abstract-transport@npm:^3.0.0": + version: 3.0.0 + resolution: "pino-abstract-transport@npm:3.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/f42b85b2663c8520839124a55b27801e88c89c65e9569384b49bb4c81b022ae24860020c2375b92a03db699113969007cc155e1fb2dfe53754403920c1cbe18c + languageName: node + linkType: hard + +"pino-pretty@npm:^13.0.0": + version: 13.1.3 + resolution: "pino-pretty@npm:13.1.3" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^4.0.0" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^3.0.0" + pump: "npm:^3.0.0" + secure-json-parse: "npm:^4.0.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^5.0.2" + bin: + pino-pretty: bin.js + checksum: 10/4bb721e1ece378c1c9000457e4fe4a914ea5b8e036551608f5681ca58c8fbacc6b8a31807e93bc0c66d17fb5d96e74b3e4051fb53152955dc51ac58848428e27 + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.1.0 + resolution: "pino-std-serializers@npm:7.1.0" + checksum: 10/6e27f6f885927b6df3b424ddb8a9e0e9854f3b59f4abd51afa74e1c2cf33436a505277b004bb00ce61884a962c8fdfd977391205c7baab885d6afb35fce7396a + languageName: node + linkType: hard + +"pino@npm:^9.7.0": + version: 9.14.0 + resolution: "pino@npm:9.14.0" + dependencies: + "@pinojs/redact": "npm:^0.4.0" + atomic-sleep: "npm:^1.0.0" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10/918e1fc764885150cb2b4fae8249a0ece53275020a7ca389f994fa2fbbb17b6353cd736c2db3a3794fbac0351f8e3d58411fabe127e875e24151a8fa4cd0b2b5 + languageName: node + linkType: hard + +"postcss@npm:^8.5.6": + version: 8.5.14 + resolution: "postcss@npm:8.5.14" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/2e3f4dea69692918fe9df5402beb0e54df84499995a094f2fbf63d1a9e38bc1b7a42854df47f09e02593213e01a5eb0627b1d1bd6d1b0ea90767b2e072f7167c + languageName: node + linkType: hard + +"proc-log@npm:^5.0.0": + version: 5.0.0 + resolution: "proc-log@npm:5.0.0" + checksum: 10/35610bdb0177d3ab5d35f8827a429fb1dc2518d9e639f2151ac9007f01a061c30e0c635a970c9b00c39102216160f6ec54b62377c92fac3b7bfc2ad4b98d195c + languageName: node + linkType: hard + +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10/1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + +"process-warning@npm:^5.0.0": + version: 5.0.0 + resolution: "process-warning@npm:5.0.0" + checksum: 10/10f3e00ac9fc1943ec4566ff41fff2b964e660f853c283e622257719839d340b4616e707d62a02d6aa0038761bb1fa7c56bc7308d602d51bd96f05f9cd305dcd + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: 10/dbaa7e8d1d5cf375c36963ff43116772a989ef2bb47c9bdee20f38fd8fc061119cf38140631cf90c781aca4d3f0f0d2c834711952b728953f04fd7d238f59f5b + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + languageName: node + linkType: hard + +"propagate@npm:^2.0.0": + version: 2.0.1 + resolution: "propagate@npm:2.0.1" + checksum: 10/8c761c16e8232f82f6d015d3e01e8bd4109f47ad804f904d950f6fe319813b448ca112246b6bfdc182b400424b155b0b7c4525a9bb009e6fa950200157569c14 + languageName: node + linkType: hard + +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: "npm:^4.2.4" + retry: "npm:^0.12.0" + signal-exit: "npm:^3.0.2" + checksum: 10/000a4875f543f591872b36ca94531af8a6463ddb0174f41c0b004d19e231d7445268b422ff1ea595e43d238655c702250cd3d27f408e7b9d97b56f1533ba26bf + languageName: node + linkType: hard + +"properties-reader@npm:^2.3.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: "npm:^1.0.4" + checksum: 10/0b41eb4136dc278ae0d97968ccce8de2d48d321655b319192e31f2424f1c6e052182204671e65aa8967216360cb3e7cbd9129830062e058fe9d6a1d74964c29a + languageName: node + linkType: hard + +"protobufjs@npm:^7.2.5, protobufjs@npm:^7.3.2, protobufjs@npm:^7.5.5": + version: 7.6.0 + resolution: "protobufjs@npm:7.6.0" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.5" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.1" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.2" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.1" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.3.2" + checksum: 10/2becdf429fa148b2f3c9ee5e52c7b8249d2b775d158ce9e5bcf82d2f9d979bf95667818f5c70487636f775e5712aecf20775ac6e86a019e146fb95ed4063dfdc + languageName: node + linkType: hard + +"proxy-from-env@npm:^2.1.0": + version: 2.1.0 + resolution: "proxy-from-env@npm:2.1.0" + checksum: 10/fbbaf4dab2a6231dc9e394903a5f66f20475e36b734335790b46feb9da07c37d6b32e2c02e3e2ea4d4b23774c53d8562e5b7cc73282cb43f4a597b7eacaee2ee + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.4 + resolution: "pump@npm:3.0.4" dependencies: - lru-cache: "npm:^10.2.0" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10/5e8845c159261adda6f09814d7725683257fcc85a18f329880ab4d7cc1d12830967eae5d5894e453f341710d5484b8fdbbd4d75181b4d6e1eb2f4dc7aeadc434 + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/d043c3e710c56ffd280711e98a94e863ab334f79ea43cee0fb70e1349b2355ffd2ff287c7522e4c960a247699d5b7825f00fa090b85d6179c973be13f78a6c49 languageName: node linkType: hard -"pathe@npm:^2.0.3": - version: 2.0.3 - resolution: "pathe@npm:2.0.3" - checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d +"pure-rand@npm:^6.1.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4 languageName: node linkType: hard -"picocolors@npm:^1.1.1": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 languageName: node linkType: hard -"picomatch@npm:^4.0.3": +"quick-format-unescaped@npm:^4.0.3": version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7 languageName: node linkType: hard -"postcss@npm:^8.5.6": - version: 8.5.14 - resolution: "postcss@npm:8.5.14" +"readable-stream@npm:^2.0.5": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" dependencies: - nanoid: "npm:^3.3.11" - picocolors: "npm:^1.1.1" - source-map-js: "npm:^1.2.1" - checksum: 10/2e3f4dea69692918fe9df5402beb0e54df84499995a094f2fbf63d1a9e38bc1b7a42854df47f09e02593213e01a5eb0627b1d1bd6d1b0ea90767b2e072f7167c + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10/8500dd3a90e391d6c5d889256d50ec6026c059fadee98ae9aa9b86757d60ac46fff24fafb7a39fa41d54cb39d8be56cc77be202ebd4cd8ffcf4cb226cbaa40d4 languageName: node linkType: hard -"proc-log@npm:^5.0.0": - version: 5.0.0 - resolution: "proc-log@npm:5.0.0" - checksum: 10/35610bdb0177d3ab5d35f8827a429fb1dc2518d9e639f2151ac9007f01a061c30e0c635a970c9b00c39102216160f6ec54b62377c92fac3b7bfc2ad4b98d195c +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 languageName: node linkType: hard -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" +"readable-stream@npm:^4.0.0": + version: 4.7.0 + resolution: "readable-stream@npm:4.7.0" dependencies: - err-code: "npm:^2.0.2" - retry: "npm:^0.12.0" - checksum: 10/96e1a82453c6c96eef53a37a1d6134c9f2482f94068f98a59145d0986ca4e497bf110a410adf73857e588165eab3899f0ebcf7b3890c1b3ce802abc0d65967d4 + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10/bdf096c8ff59452ce5d08f13da9597f9fcfe400b4facfaa88e74ec057e5ad1fdfa140ffe28e5ed806cf4d2055f0b812806e962bca91dce31bc4cef08e53be3a4 languageName: node linkType: hard -"pure-rand@npm:^7.0.0": - version: 7.0.1 - resolution: "pure-rand@npm:7.0.1" - checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 +"readdir-glob@npm:^1.1.2": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: "npm:^5.1.0" + checksum: 10/ca3a20aa1e715d671302d4ec785a32bf08e59d6d0dd25d5fc03e9e5a39f8c612cdf809ab3e638a79973db7ad6868492edf38504701e313328e767693671447d6 + languageName: node + linkType: hard + +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf languageName: node linkType: hard @@ -1776,13 +4833,57 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": +"rxjs@npm:^7.5.0, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d + languageName: node + linkType: hard + +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a + languageName: node + linkType: hard + +"safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 languageName: node linkType: hard +"scale-ts@npm:^1.6.0": + version: 1.6.1 + resolution: "scale-ts@npm:1.6.1" + checksum: 10/f1f9bf1d9abfcfcaf8ae2ae326270beca5c2456cc72f6b6b8230aa175a30bdcd6387678746a4d873c834efbba9c8e015698d42ee67bd71b70f7adfe2e0ba1d39 + languageName: node + linkType: hard + +"secure-json-parse@npm:^4.0.0": + version: 4.1.0 + resolution: "secure-json-parse@npm:4.1.0" + checksum: 10/1025c6fd0b8fa0e8c6ac7225fc0b79ecc528b2e51a8446e4bb73bfc47a2450b9e9e9813b84bc9e6735ce30c947b52e5b9d90771521aa9bb2ec216afd24c2da4e + languageName: node + linkType: hard + "semver@npm:^7.3.5": version: 7.7.3 resolution: "semver@npm:7.7.3" @@ -1792,6 +4893,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.3": + version: 7.8.1 + resolution: "semver@npm:7.8.1" + bin: + semver: bin/semver.js + checksum: 10/3244f6c4cb3f8126fea0426d353829ed4967e41e1f4696337c6fdcad87426466fe2badaf49d7dc85849acfc496ea0599432a4aecc33802d2d774e723acfa30e6 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -1822,6 +4932,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -1836,6 +4953,22 @@ __metadata: languageName: node linkType: hard +"smol-toml@npm:^1.3.4": + version: 1.6.1 + resolution: "smol-toml@npm:1.6.1" + checksum: 10/9a0d86cc7f8abef429c915b373b9a1f369fe57a87efbbec46b967fb41dc28af753a2fa62c9c4848907c3b47c282be15c8854aa4e2942ef1fa86ff95a76d13856 + languageName: node + linkType: hard + +"smoldot@npm:2.0.26": + version: 2.0.26 + resolution: "smoldot@npm:2.0.26" + dependencies: + ws: "npm:^8.8.1" + checksum: 10/b975c8ef16e2286b2eddc8c19c18080bd528f27e9abc0e2731304823e67ebe1fc71b01bed2c070d00da1f7e2f69e25c159c976d27eb1796de4a978362dae701e + languageName: node + linkType: hard + "socks-proxy-agent@npm:^8.0.3": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" @@ -1857,6 +4990,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.1 + resolution: "sonic-boom@npm:4.2.1" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/161af46b3e6debc4ad3865b0db47f37289741a0b3005b8cf056f93a4e0e1a347e24ca1a2d8ccc864f7f19caa6185a766797f8382cdbfd2f3d046a0323d73a542 + languageName: node + linkType: hard + "source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -1864,6 +5006,47 @@ __metadata: languageName: node linkType: hard +"split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 10/1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f + languageName: node + linkType: hard + +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab + languageName: node + linkType: hard + +"ssh-remote-port-forward@npm:^1.0.4": + version: 1.0.4 + resolution: "ssh-remote-port-forward@npm:1.0.4" + dependencies: + "@types/ssh2": "npm:^0.5.48" + ssh2: "npm:^1.4.0" + checksum: 10/c6c04c5ddfde7cb06e9a8655a152bd28fe6771c6fe62ff0bc08be229491546c410f30b153c968b8d6817a57d38678a270c228f30143ec0fe1be546efc4f6b65a + languageName: node + linkType: hard + +"ssh2@npm:^1.15.0, ssh2@npm:^1.4.0": + version: 1.17.0 + resolution: "ssh2@npm:1.17.0" + dependencies: + asn1: "npm:^0.2.6" + bcrypt-pbkdf: "npm:^1.0.2" + cpu-features: "npm:~0.0.10" + nan: "npm:^2.23.0" + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: 10/5a7e911f234f73c4332f2b436cc6131c164962d2eac71f463ab401b54c4b8627875d9c9be1c55e0bfd1a0eae108cfa33217bc73939287e4a5e81f34f532b1036 + languageName: node + linkType: hard + "ssri@npm:^12.0.0": version: 12.0.0 resolution: "ssri@npm:12.0.0" @@ -1894,7 +5077,18 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.25.0": + version: 2.25.0 + resolution: "streamx@npm:2.25.0" + dependencies: + events-universal: "npm:^1.0.0" + fast-fifo: "npm:^1.3.2" + text-decoder: "npm:^1.1.0" + checksum: 10/d00dd38a1b73e4dac5225344aee421eb12ba9dded3f0ee3427d358d663677af185bc2310f46cb85ff3da31e032a50514d6f66348ba756154fe8a89b845273a3c + languageName: node + linkType: hard + +"string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -1905,17 +5099,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.1, string-width@npm:^5.1.2": - version: 5.1.2 - resolution: "string-width@npm:5.1.2" - dependencies: - eastasianwidth: "npm:^0.2.0" - emoji-regex: "npm:^9.2.2" - strip-ansi: "npm:^7.0.1" - checksum: 10/7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 - languageName: node - linkType: hard - "string-width@npm:^8.1.0": version: 8.1.0 resolution: "string-width@npm:8.1.0" @@ -1926,7 +5109,25 @@ __metadata: languageName: node linkType: hard -"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10/54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10/7c41c17ed4dea105231f6df208002ebddd732e8e9e2d619d133cecd8e0087ddfd9587d2feb3c8caf3213cbd841ada6d057f5142cae68a4e62d3540778d9819b4 + languageName: node + linkType: hard + +"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -1935,7 +5136,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": +"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": version: 7.1.2 resolution: "strip-ansi@npm:7.1.2" dependencies: @@ -1944,6 +5145,85 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:^5.0.2": + version: 5.0.3 + resolution: "strip-json-comments@npm:5.0.3" + checksum: 10/3ccbf26f278220f785e4b71f8a719a6a063d72558cc63cb450924254af258a4f4c008b8c9b055373a680dc7bd525be9e543ad742c177f8a7667e0b726258e0e4 + languageName: node + linkType: hard + +"superjson@npm:^2.0.0": + version: 2.2.6 + resolution: "superjson@npm:2.2.6" + dependencies: + copy-anything: "npm:^4" + checksum: 10/7bb6446b70e8a37ec9aa2f2d08295ae4e7e8268b86c89d83a306b3798cd0cc60d89016c0c5fa83b558db23e8de8863c585a4cf52d18c4834c48bad7d2b6ee25b + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10/c8bb7afd564e3b26b50ca6ee47572c217526a1389fe018d00345856d4a9b08ffbd61fadaf283a87368d94c3dcdb8f5ffe2650a5a65863e21ad2730ca0f05210a + languageName: node + linkType: hard + +"tar-fs@npm:^2.1.4": + version: 2.1.4 + resolution: "tar-fs@npm:2.1.4" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10/bdf7e3cb039522e39c6dae3084b1bca8d7bcc1de1906eae4a1caea6a2250d22d26dcc234118bf879b345d91ebf250a744b196e379334a4abcbb109a78db7d3be + languageName: node + linkType: hard + +"tar-fs@npm:^3.0.7": + version: 3.1.2 + resolution: "tar-fs@npm:3.1.2" + dependencies: + bare-fs: "npm:^4.0.1" + bare-path: "npm:^3.0.0" + pump: "npm:^3.0.0" + tar-stream: "npm:^3.1.5" + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: 10/b358fb7061eebb42bfa6f122cf62d1bdd40dc619117863f3b59eeaa4f880dc03707014905bdb592e77176703d9045956d1ba27adda4458805f9f7cbf62015cbd + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + languageName: node + linkType: hard + +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": + version: 3.2.0 + resolution: "tar-stream@npm:3.2.0" + dependencies: + b4a: "npm:^1.6.4" + bare-fs: "npm:^4.5.5" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10/ce57a81521de73ae7a3b7d55a08da50d6771427c249bfa89a208518e48faf5254c8fa7201a8f5419ab8bde9601a74e6dd512b31a13ec89774aec96178f99a8d3 + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.5.14 resolution: "tar@npm:7.5.14" @@ -1957,6 +5237,56 @@ __metadata: languageName: node linkType: hard +"teex@npm:^1.0.1": + version: 1.0.1 + resolution: "teex@npm:1.0.1" + dependencies: + streamx: "npm:^2.12.5" + checksum: 10/36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 + languageName: node + linkType: hard + +"testcontainers@npm:^10.28.0": + version: 10.28.0 + resolution: "testcontainers@npm:10.28.0" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + "@types/dockerode": "npm:^3.3.35" + archiver: "npm:^7.0.1" + async-lock: "npm:^1.4.1" + byline: "npm:^5.0.0" + debug: "npm:^4.3.5" + docker-compose: "npm:^0.24.8" + dockerode: "npm:^4.0.5" + get-port: "npm:^7.1.0" + proper-lockfile: "npm:^4.1.2" + properties-reader: "npm:^2.3.0" + ssh-remote-port-forward: "npm:^1.0.4" + tar-fs: "npm:^3.0.7" + tmp: "npm:^0.2.3" + undici: "npm:^5.29.0" + checksum: 10/434d3677e10a114805420f2420831a8eae4091acdaf242787fb100a8755140af0e11eab3932cdb29267f0869af22d0b572532f72ee5450d60f63f3fed30d098c + languageName: node + linkType: hard + +"text-decoder@npm:^1.1.0": + version: 1.2.7 + resolution: "text-decoder@npm:1.2.7" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10/151f89339a497353ad579b32536be94bf90a0785fd2aa2dc0a5ec8a4b71ed59998f4adb872201bdc536805425aa8c5cf8f4a936c449be614c1d3c4527688b3d0 + languageName: node + linkType: hard + +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10/ea2d816c4f6077a7062fac5414a88e82977f807c82ee330938fb9691fe11883bb03f078551c0518bb649c239e47ba113d44014fcbb5db42c5abd5996f35e4213 + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -1988,6 +5318,20 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.3": + version: 0.2.5 + resolution: "tmp@npm:0.2.5" + checksum: 10/dd4b78b32385eab4899d3ae296007b34482b035b6d73e1201c4a9aede40860e90997a1452c65a2d21aee73d53e93cd167d741c3db4015d90e63b6d568a93d7ec + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 + languageName: node + linkType: hard + "ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -2026,6 +5370,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 + languageName: node + linkType: hard + "turbo-darwin-64@npm:2.6.1": version: 2.6.1 resolution: "turbo-darwin-64@npm:2.6.1" @@ -2097,6 +5448,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^0.14.3": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 10/04ee27901cde46c1c0a64b9584e04c96c5fe45b38c0d74930710751ea991408b405747d01dfae72f80fc158137018aea94f9c38c651cb9c318f0861a310c3679 + languageName: node + linkType: hard + "typescript@npm:^5.8.2, typescript@npm:^5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -2117,6 +5475,20 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:>=7.24.0 <7.24.7": + version: 7.24.6 + resolution: "undici-types@npm:7.24.6" + checksum: 10/defc9538b952e3c15b8526596c591f7c1f0c7605ad27a2b7feddbea7ef2e3003f3eda2cdb051a3cb1a2185e3893100fd9cb925c799db99d48131ea63b5233d10 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + "undici-types@npm:~7.16.0": version: 7.16.0 resolution: "undici-types@npm:7.16.0" @@ -2124,6 +5496,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.24.0": + version: 6.25.0 + resolution: "undici@npm:6.25.0" + checksum: 10/a475e45da3e1d1073283bb70531666f09a432eabff2b857bd7063d469a1ee1486192ff61dc0dadbb526673ce1120fee14d66a59b6b17d1e0bd3a4d5f0a52d0a6 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -2142,6 +5521,22 @@ __metadata: languageName: node linkType: hard +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"uuid@npm:^13.0.0": + version: 13.0.2 + resolution: "uuid@npm:13.0.2" + bin: + uuid: dist-node/bin/uuid + checksum: 10/567dddca18a8520796dd3cd1e4513f4c7c522f25602c15381615395d60c7892f330366680fc21373f19fb83c991f3da8413f57dbd85bf976069cf0818aa6c61c + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -2263,6 +5658,37 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + +"web-worker@npm:^1.5.0": + version: 1.5.0 + resolution: "web-worker@npm:1.5.0" + checksum: 10/1209461e2c731fe8e8297c95a8a324c6dd00fd9f3c489ed79d18a15592731324762b7b06c8b6bc404596259aa13cd413119e0153e12a80f47a7f374960461e0d + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -2297,7 +5723,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -2308,14 +5734,47 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.1.0": - version: 8.1.0 - resolution: "wrap-ansi@npm:8.1.0" - dependencies: - ansi-styles: "npm:^6.1.0" - string-width: "npm:^5.0.1" - strip-ansi: "npm:^7.0.1" - checksum: 10/7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10/159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^8.16.0, ws@npm:^8.18.0, ws@npm:^8.8.1": + version: 8.20.1 + resolution: "ws@npm:8.20.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/8c4d2b06dc65381b6bfab1f2e584275dabd30a99a5ce058b4dc76f3d03fad1921cef3a21d8f53127d30a808cfd1864aa2fe6890a5d43359f682457315baec873 + languageName: node + linkType: hard + +"ws@npm:^8.20.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/088411956432c8f876158409d5a285cb9ad1382f593391f51d3a599bd0a5b277f876609ebd00fc3596321c4a4c9064d6fffe1ebad960e8ea7fd9ae25324f35c2 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10/5f1b5f95e3775de4514edbb142398a2c37849ccfaf04a015be5d75521e9629d3be29bd4432d23c57f37e5b61ade592fb0197022e9993f81a06a5afbdcda9346d languageName: node linkType: hard @@ -2333,6 +5792,37 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.2": + version: 2.9.0 + resolution: "yaml@npm:2.9.0" + bin: + yaml: bin.mjs + checksum: 10/9a95e8e08651c3d292ab6a5befeb5f57b76801caa097c75bb45c9a70ce19c1b11f57e87a6ef84a579ea070ed2c2c8ac541c88c0ae684d544d5f42c7e77d11b7b + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10/9dc2c217ea3bf8d858041252d43e074f7166b53f3d010a8c711275e09cd3d62a002969a39858b92bbda2a6a63a585c7127014534a560b9c69ed2d923d113406e + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10/abb3e37678d6e38ea85485ed86ebe0d1e3464c640d7d9069805ea0da12f69d5a32df8e5625e370f9c96dd1c2dc088ab2d0a4dd32af18222ef3c4224a19471576 + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -2346,3 +5836,21 @@ __metadata: checksum: 10/6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 languageName: node linkType: hard + +"zip-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "zip-stream@npm:6.0.1" + dependencies: + archiver-utils: "npm:^5.0.0" + compress-commons: "npm:^6.0.2" + readable-stream: "npm:^4.0.0" + checksum: 10/aa5abd6a89590eadeba040afbc375f53337f12637e5e98330012a12d9886cde7a3ccc28bd91aafab50576035bbb1de39a9a316eecf2411c8b9009c9f94f0db27 + languageName: node + linkType: hard + +"zod@npm:^3.23.8": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard