diff --git a/.gitignore b/.gitignore index 075ffa5..b1f6384 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 @@ -22,10 +29,34 @@ result dist/ gen/ managed/ +# Compiler output. Regenerated by `compact-compiler` on every build, so it +# never gets committed — including under examples/, where the walkthrough +# expects you to compile the contract yourself before deploying. artifacts/ 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/package.json b/package.json index 52160b2..bddb68d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "turbo run build --log-prefix=none", "test": "turbo run test --log-prefix=none", + "coverage": "turbo run coverage --log-prefix=none", "lint": "biome check .", "lint:fix": "biome check . --write", "lint:ci": "biome ci . --no-errors-on-unmatched", @@ -17,10 +18,20 @@ }, "devDependencies": { "@biomejs/biome": "2.4.16", + "@openzeppelin/compact-deployer": "workspace:^", "@types/node": "25.9.1", + "@vitest/coverage-v8": "4.1.8", + "pino": "^9.7.0", "ts-node": "^10.9.2", "turbo": "^2.9.14", "typescript": "^6.0.3", "vitest": "^4.1.6" + }, + "resolutions": { + "@midnight-ntwrk/ledger-v8": "8.0.3", + "@midnight-ntwrk/midnight-js-protocol": "4.1.0", + "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 0408b5d..c1b0486 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.2", "keywords": [ "compact", "cli", "compiler", "builder", + "deployer", "testing" ], "author": "OpenZeppelin Community ", @@ -19,7 +20,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", @@ -27,27 +29,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": "25.9.1", + "@types/ws": "^8.5.10", "typescript": "^6.0.3", "vitest": "^4.1.6" }, "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..70d809b --- /dev/null +++ b/packages/cli/src/logger.ts @@ -0,0 +1,70 @@ +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 to STDERR, no + * transports — STDOUT is reserved for the single result object), 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) { + // fd 2 = STDERR; keeps STDOUT carrying only the final JSON result. + return pino( + { level: opts.verbose ? 'debug' : 'info' }, + pino.destination(2), + ); + } + + 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..4ecc486 --- /dev/null +++ b/packages/cli/src/prompt.ts @@ -0,0 +1,70 @@ +import { stderr, stdin } from 'node:process'; + +/** + * Prompt for a keystore passphrase with terminal echo suppressed; falls back + * to plain line-read off a TTY. Prompt text and the trailing newline go to + * STDERR so `--json` (and any piped) callers keep a clean single-object STDOUT. + */ +export async function promptPassphrase(label: string): Promise { + stderr.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); + stdin.removeListener('end', onEnd); + stdin.removeListener('error', onError); + stderr.write('\n'); + }; + + // Without these, a non-interactive stdin that closes without a trailing + // newline (e.g. piped input, or a closed pipe) would never settle the + // promise and the CLI would hang. + const onEnd = () => { + cleanup(); + if (buffer.length > 0) resolveFn(buffer); + else rejectFn(new Error('Aborted')); + }; + + const onError = (err: Error) => { + cleanup(); + rejectFn(err); + }; + + 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); + stdin.on('end', onEnd); + stdin.on('error', onError); + }); +} 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..7609aef --- /dev/null +++ b/packages/cli/src/runDeploy.ts @@ -0,0 +1,341 @@ +#!/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; + syncBatchSize?: 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; + } + case '--sync-batch-size': { + const raw = expectValue(argv, ++i, '--sync-batch-size'); + const size = Number.parseInt(raw, 10); + if (!Number.isFinite(size) || size <= 0) { + throw new Error( + `--sync-batch-size requires a positive integer; got "${raw}"`, + ); + } + out.syncBatchSize = size; + 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, + syncBatchSize: args.syncBatchSize, + 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( + ' --sync-batch-size Dust/shielded sync batch size (default 5000)', + ), + ); + 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'), + ); + console.log( + chalk.yellow( + '\nNote: a first sync on a long-history network (e.g. preprod) can exceed', + ), + ); + console.log( + chalk.yellow( + " Node's default heap. On 'JavaScript heap out of memory', raise it:", + ), + ); + console.log( + chalk.yellow( + ' NODE_OPTIONS=--max-old-space-size=8192 compact-deploy …', + ), + ); +} + +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..7694819 --- /dev/null +++ b/packages/cli/test/logger.test.ts @@ -0,0 +1,193 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockMkdirSync, mockPino, mockTransport, mockDestination, 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 })), + mockDestination: vi.fn((fd: number) => ({ __destination: fd })), + 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; + (pinoFn as unknown as { destination: typeof mockDestination }).destination = + mockDestination; + 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 routed to STDERR when verbose false', () => { + const logger = createLogger({ verbose: false, json: true }); + + expect(mockPino).toHaveBeenCalledTimes(1); + // fd 2 = STDERR; STDOUT stays reserved for the single JSON result. + expect(mockDestination).toHaveBeenCalledWith(2); + expect(mockPino).toHaveBeenCalledWith( + { level: 'info' }, + { __destination: 2 }, + ); + expect(mockTransport).not.toHaveBeenCalled(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(logger).toBe(fakeLogger); + }); + + it('should return logger at debug level routed to STDERR when verbose true', () => { + const logger = createLogger({ verbose: true, json: true }); + + expect(mockDestination).toHaveBeenCalledWith(2); + expect(mockPino).toHaveBeenCalledWith( + { level: 'debug' }, + { __destination: 2 }, + ); + 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: Record[] } }, + ]; + 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..42bde24 --- /dev/null +++ b/packages/cli/test/prompt.test.ts @@ -0,0 +1,171 @@ +import type { EventEmitter } from 'node:events'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockStdin, mockStderr } = 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 stderr = { write: vi.fn() }; + return { mockStdin: stdin, mockStderr: stderr }; +}); + +vi.mock('node:process', () => ({ + stdin: mockStdin, + stderr: mockStderr, +})); + +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(); + mockStderr.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(mockStderr.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(mockStderr.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'); + }); + }); + + describe('stream close path', () => { + it('should reject with "Aborted" when stdin ends with an empty buffer', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('end'); + await expect(promise).rejects.toThrow('Aborted'); + }); + + it('should resolve with the buffer when stdin ends without a trailing newline', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('data', Buffer.from('piped-secret')); + mockStdin.emit('end'); + await expect(promise).resolves.toBe('piped-secret'); + }); + + it('should reject when stdin emits an error', async () => { + const promise = promptPassphrase('label'); + mockStdin.emit('error', new Error('stream boom')); + await expect(promise).rejects.toThrow('stream boom'); + }); + }); +}); 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..865977a --- /dev/null +++ b/packages/cli/test/runDeploy.test.ts @@ -0,0 +1,560 @@ +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', + '--sync-batch-size', + '5000', + '--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.syncBatchSize).toBe(5000); + 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 leave syncBatchSize undefined when --sync-batch-size is omitted', async () => { + await runMain(['Token']); + const opts = mockPrepare.mock.calls[0]?.[0] as Record; + expect(opts.syncBatchSize).toBeUndefined(); + }); + + it('should reject non-numeric --sync-batch-size', async () => { + await runMain(['Token', '--sync-batch-size', 'abc']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + '--sync-batch-size requires a positive integer', + ), + ); + expect(mockExit).toHaveBeenCalledWith(2); + }); + + it('should reject zero/negative --sync-batch-size', async () => { + await runMain(['Token', '--sync-batch-size', '0']); + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + '--sync-batch-size 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..132aa5e --- /dev/null +++ b/packages/deployer/README.md @@ -0,0 +1,188 @@ +# @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`. + +## Install & run + +The `compact-deploy` bin ships in `@openzeppelin/compact-cli`. Install it as a dev dependency of the project that holds your compiled artifacts, then run it with `npx`: + +```bash +npm i -D @openzeppelin/compact-cli # or pnpm/yarn +npx compact-deploy Token --network local # resolves the local install +``` + +`npx` prefers the local `node_modules/.bin`, so the deployer and your compiled contracts share **one** `@midnight-ntwrk/compact-runtime` copy. A real deploy requires this: the deploy-tx builder creates a `ContractMaintenanceAuthority` from the runtime and hands it to your contract, and the WASM check rejects it unless both sides are the same physical copy. + +> **Ephemeral `npx @openzeppelin/compact-cli compact-deploy …` (no local install)** is fine for `--help`, `--version`, and `--dry-run` / validation, but **not reliable for a real deploy**: npx fetches the CLI into its own cache tree, so its `compact-runtime` differs from your project's and the submit fails with `expected instance of ContractMaintenanceAuthority`. Install it locally for real deploys. + +## 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) + --sync-batch-size dust/shielded sync batch size (default 5000) + --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"`. +- **Tune the sync batch size**: `--sync-batch-size ` (default 5000). Larger replays a long dust history faster but uses more memory per batch; lower it on a memory-constrained host. +- **Persist the sync knobs in TOML**: set `sync_timeout` (seconds) and/or `sync_batch_size` under `[networks.X]` so you don't pass the flags every run. The matching CLI flag overrides the TOML value when both are present (CLI > TOML > default). +- **Tip gate is tolerant**: sync completes once every sub-wallet is within 50 events of the tip, not at an exact gap of 0. On a live network the global dust stream advances continuously, so an exact-match gate would never fire. +- **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" +sync_timeout = 5400 # seconds; overridden by --sync-timeout +sync_batch_size = 5000 # dust/shielded batch; overridden by --sync-batch-size + +# ---------- 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.** The deployer now raises the dust/shielded sync batch size (`batchUpdates = { size: 5000, … }`) so the replay no longer OOMs mid-stream on `wallet-sdk-dust-wallet@4.0.0` ([midnightntwrk/midnight-wallet#425](https://github.com/midnightntwrk/midnight-wallet/issues/425)). The restored dust tree plus shielded trial-decryption can still spike past V8's ~2 GB default old-space on a first preprod sync, so set `NODE_OPTIONS="--max-old-space-size=8192"` for that run. 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..579962d --- /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.1.6" + }, + "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..86d086d --- /dev/null +++ b/packages/deployer/src/config/schema.test.ts @@ -0,0 +1,300 @@ +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 — network sync tuning', () => { + it('should accept sync_timeout and sync_batch_size as positive integers', () => { + const parsed = configSchema.parse({ + ...baseConfig, + networks: { + testnet: { ...validNetwork, sync_timeout: 3600, sync_batch_size: 5000 }, + }, + }); + expect(parsed.networks.testnet.sync_timeout).toBe(3600); + expect(parsed.networks.testnet.sync_batch_size).toBe(5000); + }); + + it('should leave sync_timeout and sync_batch_size undefined when omitted', () => { + const parsed = configSchema.parse(baseConfig); + expect(parsed.networks.testnet.sync_timeout).toBeUndefined(); + expect(parsed.networks.testnet.sync_batch_size).toBeUndefined(); + }); + + it('should reject a non-positive sync_batch_size', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { testnet: { ...validNetwork, sync_batch_size: 0 } }, + }), + ).toThrow(); + }); + + it('should reject a fractional sync_timeout', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + networks: { testnet: { ...validNetwork, sync_timeout: 1.5 } }, + }), + ).toThrow(); + }); +}); + +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', + }); + }); + + it('should reject an ambiguous ref carrying both file and module', () => { + expect(() => + configSchema.parse({ + ...baseConfig, + contracts: { + Counter: { + ...validContract, + args: { file: 'args.json', module: 'args.ts' }, + }, + }, + }), + ).toThrow(); + }); +}); + +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..495280d --- /dev/null +++ b/packages/deployer/src/config/schema.ts @@ -0,0 +1,114 @@ +/** + * 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(), + // Optional sync tuning, per network. `sync_timeout` is the max seconds to + // wait for the wallet to reach chain tip; `sync_batch_size` is the + // dust/shielded sync batch size (raise for long-history networks like + // preprod, default 5000). The matching CLI flags (`--sync-timeout`, + // `--sync-batch-size`) override these when set. + sync_timeout: z.number().int().positive().optional(), + sync_batch_size: z.number().int().positive().optional(), +}); + +const walletObjectSchema = z.object({ + keystore: z.string().optional(), +}); +const walletSchema = walletObjectSchema.optional(); + +const fileRefSchema = z.object({ file: z.string().min(1) }).strict(); +const moduleRefSchema = z + .object({ + module: z.string().min(1), + export: z.string().default('default'), + }) + .strict(); +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..ff1e52c --- /dev/null +++ b/packages/deployer/src/deployer.test.ts @@ -0,0 +1,1012 @@ +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< + typeof import('@midnight-ntwrk/midnight-js-network-id') + >(); + 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, + isCompleteWithin: () => true, + }, + }, + }, + unshielded: { + balances: anyKeyHasBalance, + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => 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, + isCompleteWithin: () => true, + }, + }, + }, + unshielded: { + balances: unshielded, + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => 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; + syncTimeout?: number; + syncBatchSize?: number; + } = {}, +): Fixture { + const rootDir = mkdtempSync(join(tmpdir(), 'deployer-test-')); + const explorerLine = opts.explorer ? `explorer = "${opts.explorer}"\n` : ''; + const syncTimeoutLine = + opts.syncTimeout !== undefined + ? `sync_timeout = ${opts.syncTimeout}\n` + : ''; + const syncBatchLine = + opts.syncBatchSize !== undefined + ? `sync_batch_size = ${opts.syncBatchSize}\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}${syncTimeoutLine}${syncBatchLine} +[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 reaches chain tip', 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 complete sync when sub-wallets are within the gap but not strictly complete', async () => { + // Live-chain shape (issue #115): the global dust stream keeps + // advancing, so dust is never strictly complete (gap 0) but settles + // within the tolerated gap. The old strict `isSynced` gate would hang + // here forever; the tolerant `isCompleteWithin` gate must proceed. + const anyBal = new Proxy({} as Record, { + get: () => 1n, + }); + const liveTipState = { + isSynced: false, + shielded: { + balances: anyBal, + state: { + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + }, + unshielded: { + balances: anyBal, + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => false, + isCompleteWithin: () => true, + }, + }, + balance: () => 1n, + }, + }; + const built = fakeOwnedFromProvider( + fakeProviderWithState(Rx.of(liveTipState as unknown), '0xLIVE-TIP'), + ); + 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('0xLIVE-TIP'); + }); + + it('should NOT complete sync while any sub-wallet is outside the gap', async () => { + // Dust still outside the tolerated gap: the gate must keep waiting and + // ultimately time out rather than deploy against a half-synced wallet. + const anyBal = new Proxy({} as Record, { + get: () => 1n, + }); + const laggingState = { + isSynced: false, + shielded: { + balances: anyBal, + state: { + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + }, + unshielded: { + balances: anyBal, + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => false, + isCompleteWithin: () => false, + }, + }, + balance: () => 1n, + }, + }; + // Emit the lagging state, then hang: the gate filters it out and must + // keep waiting (not complete the sequence) so the timeout can fire. + const built = fakeOwnedFromProvider( + fakeProviderWithState( + Rx.concat(Rx.of(laggingState as unknown), Rx.NEVER), + '0xLAGGING', + ), + ); + 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('wallet build options', () => { + it('should forward syncBatchSize to WalletHandler.build', async () => { + const built = fakeOwnedWallet('0xBATCH'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: fx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + syncBatchSize: 2500, + }); + expect(d.deployer).toBe('0xBATCH'); + expect(WalletHandler.build).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ syncBatchSize: 2500 }), + ); + }); + + it('should use the TOML [networks.X].sync_batch_size when no option is passed', async () => { + const customFx = writeFixture({ syncBatchSize: 1234 }); + try { + const built = fakeOwnedWallet('0xTOML-BATCH'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + }); + expect(d.deployer).toBe('0xTOML-BATCH'); + expect(WalletHandler.build).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ syncBatchSize: 1234 }), + ); + } finally { + customFx.cleanup(); + } + }); + + it('should let the syncBatchSize option override the TOML value', async () => { + const customFx = writeFixture({ syncBatchSize: 1234 }); + try { + const built = fakeOwnedWallet('0xOVERRIDE'); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await using d = await Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + syncTimeoutMs: 1000, + syncBatchSize: 9999, + }); + expect(d.deployer).toBe('0xOVERRIDE'); + expect(WalletHandler.build).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ syncBatchSize: 9999 }), + ); + } finally { + customFx.cleanup(); + } + }); + + it('should apply the TOML [networks.X].sync_timeout (seconds) as the sync ceiling', async () => { + // 1s TOML timeout against a never-syncing wallet must trip at 1000ms. + const customFx = writeFixture({ syncTimeout: 1 }); + try { + const built = fakeOwnedFromProvider( + fakeProviderWithState(Rx.NEVER, '0xTOML-TIMEOUT'), + ); + vi.mocked(WalletHandler.build).mockResolvedValueOnce(built.owned); + await expect( + Deployer.prepare({ + contract: 'Counter', + network: 'local', + configPath: customFx.configPath, + logger: silentLogger, + }), + ).rejects.toThrow(/Wallet sync timeout after 1000ms/); + } finally { + customFx.cleanup(); + } + }); + }); + + 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' }); + (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' }); + (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 (still short of the tip) that drives the progress + // subscription's "else" branch (highest > 0). Then a follow-up + // tip-reached state lets the `isCompleteWithin` gate resolve so the + // prepare call terminates. + const midState = { + isSynced: false, + shielded: { + balances: {} as Record, + state: { + progress: { + isStrictlyComplete: () => false, + isCompleteWithin: () => false, + appliedIndex: 10n, + highestIndex: 100n, + isConnected: true, + }, + }, + }, + unshielded: { + balances: {} as Record, + progress: { + isStrictlyComplete: () => false, + isCompleteWithin: () => false, + appliedId: 5n, + highestTransactionId: 50n, + isConnected: true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => false, + isCompleteWithin: () => 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, + isCompleteWithin: () => true, + }, + }, + }, + unshielded: { + balances: anyKeyHasBalance, + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => true, + }, + }, + dust: { + state: { + progress: { + isStrictlyComplete: () => true, + isCompleteWithin: () => 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..30ee36b --- /dev/null +++ b/packages/deployer/src/deployer.ts @@ -0,0 +1,745 @@ +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, + 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; + +/** + * Tolerated sync gap, in events, for the chain-tip gate. + * `FacadeState.isSynced` requires every sub-wallet to be *strictly* + * complete (gap 0). On a live network the global dust stream advances + * continuously, so the dust wallet sits a few events behind the tip + * indefinitely and strict `isSynced` never flips. The gate would then time + * out on a wallet that is in fact fully usable. `isCompleteWithin(50)` is the + * SDK default (and what the unshielded wallet uses internally): it treats + * "within 50 events of tip" as synced, which a live wallet reaches and + * holds. See OpenZeppelin/compact-tools#115. + */ +const SYNC_MAX_GAP = 50n; + +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). Precedence: this value > `[networks.X].sync_timeout` + * (seconds, from TOML) > {@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; + /** + * Sync batch size for the shielded + dust sub-wallets. Precedence: this + * value > `[networks.X].sync_batch_size` (TOML) > 5000. Raise it to replay + * a long dust history faster (more memory per batch); lower it on a + * memory-constrained host. Ignored when {@link walletProvider} is injected. + * Argv: `--sync-batch-size`. + */ + syncBatchSize?: number; +} + +/** 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'); + } + // Sync tuning precedence: CLI/programmatic option > [networks.X] TOML + // value > built-in default. `sync_batch_size` falls through to + // WalletHandler's 5000 default when neither is set. + const syncBatchSize = opts.syncBatchSize ?? network.sync_batch_size; + const syncTimeoutMs = + opts.syncTimeoutMs ?? + (network.sync_timeout !== undefined + ? network.sync_timeout * 1000 + : DEFAULT_SYNC_TIMEOUT_MS); + const owned = await WalletHandler.build( + logger, + env, + seedResolution.seed, + { + skipWalletCache: opts.skipWalletCache, + seedCacheDust: opts.seedCacheDust, + seedCacheShielded: opts.seedCacheShielded, + syncBatchSize, + }, + ); + 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: syncTimeoutMs, + 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. Gates on each + * sub-wallet being within {@link SYNC_MAX_GAP} events of the tip rather + * than strictly complete: on a live network the global dust stream never + * settles to gap 0, so strict `FacadeState.isSynced` never fires and the + * gate times out on a perfectly usable wallet (#115). The gate still waits + * on all three sub-wallets; dropping the dust/shielded wait entirely + * 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( + // Tolerant tip gate: all three sub-wallets within SYNC_MAX_GAP + // events of the tip. Strict `s.isSynced` never fires on a live + // chain because the global dust stream keeps advancing (#115). + Rx.filter( + (s: FacadeState) => + s.shielded.state.progress.isCompleteWithin(SYNC_MAX_GAP) && + s.dust.state.progress.isCompleteWithin(SYNC_MAX_GAP) && + s.unshielded.progress.isCompleteWithin(SYNC_MAX_GAP), + ), + 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..e3db135 --- /dev/null +++ b/packages/deployer/src/deployments.ts @@ -0,0 +1,121 @@ +import { randomUUID } from 'node:crypto'; +import { existsSync } from 'node:fs'; +import { mkdir, readFile, rename, 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; +} + +// Write atomically: a crash mid-write would otherwise leave a truncated +// `*.json`, breaking subsequent reads and losing durable deploy state. +// Write to a sibling temp file, then rename it into place (atomic on the +// same filesystem). +async function writeJson(path: string, value: unknown): Promise { + await mkdir(dirname(path), { recursive: true }); + const tmp = `${path}.${randomUUID()}.tmp`; + await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`); + await rename(tmp, path); +} 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..8b99ede --- /dev/null +++ b/packages/deployer/src/loaders/args.test.ts @@ -0,0 +1,174 @@ +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..b376e23 --- /dev/null +++ b/packages/deployer/src/loaders/args.ts @@ -0,0 +1,103 @@ +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..dbf3969 --- /dev/null +++ b/packages/deployer/src/loaders/artifact.test.ts @@ -0,0 +1,314 @@ +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..43206c8 --- /dev/null +++ b/packages/deployer/src/loaders/artifact.ts @@ -0,0 +1,194 @@ +import { existsSync, readdirSync } from 'node:fs'; +import { dirname, 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 entry = findEntry(resolve(artifactPath, 'contract'), artifactPath); + if (!entry) { + throw new ArtifactNotFoundError( + `${artifactPath} (no contract/index.{cjs,js} or index.{cjs,js} found)`, + ); + } + // Bind compiled assets to the directory the entry actually lives in: + // `findEntry` may resolve a top-level `index.{cjs,js}` rather than the + // `contract/` subdir, in which case the hardcoded path would be wrong. + const contractDir = dirname(entry); + + 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..773c7a1 --- /dev/null +++ b/packages/deployer/src/loaders/constructor-meta.test.ts @@ -0,0 +1,85 @@ +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/, + ); + }); + + it('accepts an empty named object for a no-arg constructor', () => { + expect(reorderNamedArgs({}, [])).toEqual([]); + }); + + it('rejects a non-empty named object for a no-arg constructor', () => { + expect(() => reorderNamedArgs({ a: 1 }, [])).toThrow( + /unknown constructor parameter\(s\): a/, + ); + }); +}); diff --git a/packages/deployer/src/loaders/constructor-meta.ts b/packages/deployer/src/loaders/constructor-meta.ts new file mode 100644 index 0000000..f446b97 --- /dev/null +++ b/packages/deployer/src/loaders/constructor-meta.ts @@ -0,0 +1,106 @@ +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'); + // A no-arg constructor yields `[]`. `reorderNamedArgs` then accepts an + // empty named-args object and rejects any unexpected keys, so we don't + // need to special-case the empty result here. + return parseConstructorParamNames(source); +} + +/** 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..917dad5 --- /dev/null +++ b/packages/deployer/src/loaders/contract-resolve.test.ts @@ -0,0 +1,181 @@ +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..66c0af4 --- /dev/null +++ b/packages/deployer/src/loaders/contract-resolve.ts @@ -0,0 +1,92 @@ +import { existsSync } from 'node:fs'; +import { isAbsolute, 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 (isAbsolute(artifact)) 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..0a4c3ad --- /dev/null +++ b/packages/deployer/src/loaders/ref-resolver.test.ts @@ -0,0 +1,111 @@ +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..eaa6a4c --- /dev/null +++ b/packages/deployer/src/providers/build.test.ts @@ -0,0 +1,241 @@ +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< + typeof buildProviders + >[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..372f0f7 --- /dev/null +++ b/packages/deployer/src/providers/build.ts @@ -0,0 +1,62 @@ +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..ee7dedf --- /dev/null +++ b/packages/deployer/src/providers/network.test.ts @@ -0,0 +1,78 @@ +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', + ])('should accept known network id %s', (id) => { + expect(() => + applyNetwork({ ...baseNetwork, network_id: id }, 'http://ps'), + ).not.toThrow(); + expect(setNetworkId).toHaveBeenLastCalledWith(id); + }); + + it('should reject mainnet while the deployer is testnet/preview-only', () => { + expect(() => + applyNetwork({ ...baseNetwork, network_id: 'mainnet' }, 'http://ps'), + ).toThrow(ConfigError); + expect(setNetworkId).not.toHaveBeenCalled(); + }); + + 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..58a3306 --- /dev/null +++ b/packages/deployer/src/providers/network.ts @@ -0,0 +1,52 @@ +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', +]); + +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..2582dda --- /dev/null +++ b/packages/deployer/src/providers/private-state-password.test.ts @@ -0,0 +1,61 @@ +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..03644c5 --- /dev/null +++ b/packages/deployer/src/providers/proof-server.test.ts @@ -0,0 +1,207 @@ +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('(4) should reject a partially-numeric PROOF_SERVER_PORT (e.g. "6300abc")', async () => { + process.env.PROOF_SERVER_PORT = '6300abc'; + await expect( + ProofServer.start({ network: baseNetwork, logger: makeLogger() }), + ).rejects.toThrow(ConfigError); + expect(StaticProofServerContainer).not.toHaveBeenCalled(); + }); + + it('(4) should reject an out-of-range PROOF_SERVER_PORT', async () => { + process.env.PROOF_SERVER_PORT = '70000'; + await expect( + ProofServer.start({ network: baseNetwork, logger: makeLogger() }), + ).rejects.toThrow(ConfigError); + expect(StaticProofServerContainer).not.toHaveBeenCalled(); + }); + + 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..c9983c1 --- /dev/null +++ b/packages/deployer/src/providers/proof-server.ts @@ -0,0 +1,114 @@ +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) { + if (!/^\d+$/.test(port)) { + throw new ConfigError(`Invalid PROOF_SERVER_PORT: ${port}`); + } + const parsed = Number(port); + if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { + 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..8fa37ba --- /dev/null +++ b/packages/deployer/src/runDeploy.test.ts @@ -0,0 +1,359 @@ +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') + .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' }); + + expect((fakeDep.dryRun as any).mock.calls.length).toBe(1); + expect( + (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( + (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 = (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 = (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..da4210c --- /dev/null +++ b/packages/deployer/src/runDeploy.ts @@ -0,0 +1,405 @@ +import { type Logger, pino } 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..33bed77 --- /dev/null +++ b/packages/deployer/src/wallet/handler.test.ts @@ -0,0 +1,585 @@ +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('sync batching', () => { + it('should default the batchUpdates size to 5000 on the shared config for both sub-wallets', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build(logger, fakeEnv('preprod'), { + kind: 'hex', + value: '00', + }); + // The shared config is mutated in place and threaded into every + // sub-wallet factory, so asserting on the dust + shielded calls proves + // the OOM workaround (issue #115) covers both. Default SDK batch size + // is 10, which OOMs replaying preprod's ~1M-event dust stream. + const withBatch = expect.objectContaining({ + batchUpdates: { size: 5000, timeout: 1, spacing: 4 }, + }); + expect(WalletFactory.createDustWallet).toHaveBeenCalledWith( + withBatch, + expect.any(Uint8Array), + expect.anything(), + ); + expect(WalletFactory.createShieldedWallet).toHaveBeenCalledWith( + withBatch, + expect.any(Uint8Array), + ); + }); + + it('should honour a caller-supplied syncBatchSize override', async () => { + wireTestkitChain(fakeProvider()); + await WalletHandler.build( + logger, + fakeEnv('preprod'), + { kind: 'hex', value: '00' }, + { syncBatchSize: 1000 }, + ); + // timeout/spacing stay at the validated values; only size changes. + const withBatch = expect.objectContaining({ + batchUpdates: { size: 1000, timeout: 1, spacing: 4 }, + }); + expect(WalletFactory.createDustWallet).toHaveBeenCalledWith( + withBatch, + expect.any(Uint8Array), + expect.anything(), + ); + expect(WalletFactory.createShieldedWallet).toHaveBeenCalledWith( + withBatch, + expect.any(Uint8Array), + ); + }); + }); + + 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..3d9867c --- /dev/null +++ b/packages/deployer/src/wallet/handler.ts @@ -0,0 +1,499 @@ +import { createHash } from 'node:crypto'; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + writeFileSync, +} from 'node:fs'; +import { basename, dirname, 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'; + +/** + * Subset of the wallet-SDK sync `batchUpdates` knob we set. The SDK only + * exports it from an unstable deep path (`wallet-sdk-/dist/v1/Sync`), + * so we declare the shape we use locally. + */ +interface BatchUpdatesConfig { + size?: number; + timeout?: number; + spacing?: number; +} + +/** + * Default sync batch size for the shielded + dust sub-wallets, overridable + * per build via {@link WalletHandlerBuildOptions.syncBatchSize}. The SDK + * default (size 10) exhausts the V8 heap replaying preprod's ~1M-event + * global dust stream on `wallet-sdk-dust-wallet@4.0.0` + * (midnightntwrk/midnight-wallet#425, "Ineffective mark-compacts near heap + * limit"). A larger batch streams the replay in bigger chunks; size 5000 + * was validated at ~146 MB peak, ~1000 events/sec. Harmless on the + * upstream-fixed 4.1.0, which no longer needs the workaround. + */ +const DEFAULT_SYNC_BATCH_SIZE = 5000; + +/** `timeout`/`spacing` companions to the batch size; left at the validated values. */ +const SYNC_BATCH_TIMING = { timeout: 1, spacing: 4 } as const; + +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; + /** + * Sync batch size for the shielded + dust sub-wallets. Defaults to + * {@link DEFAULT_SYNC_BATCH_SIZE} (5000). Raise it to replay a long dust + * history faster (more memory per batch); lower it on a memory-constrained + * host. The SDK default of 10 OOMs on preprod's ~1M-event dust stream. + */ + syncBatchSize?: number; +} + +/** + * 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; + + // Raise the sync batch size on the *shared* config so it applies to + // every sub-wallet built off it: shielded and dust, fresh-sync and + // cache-restore alike. `buildDustConfig` spreads this config, so the + // restore path inherits it too. Setting it only on the fresh-build + // path would be bypassed the moment an on-disk snapshot exists, which + // is exactly when preprod's dust replay OOMs. See + // {@link DEFAULT_SYNC_BATCH_SIZE} and OpenZeppelin/compact-tools#115. + (config as { batchUpdates?: BatchUpdatesConfig }).batchUpdates = { + size: opts.syncBatchSize ?? DEFAULT_SYNC_BATCH_SIZE, + ...SYNC_BATCH_TIMING, + }; + + 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 { + return dirname(p); +} + +function pathBase(p: string): string { + return basename(p); +} diff --git a/packages/deployer/src/wallet/keystore.test.ts b/packages/deployer/src/wallet/keystore.test.ts new file mode 100644 index 0000000..29b19d0 --- /dev/null +++ b/packages/deployer/src/wallet/keystore.test.ts @@ -0,0 +1,162 @@ +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/); + }); + + it('should reject a non-object with WalletError (not a raw TypeError)', () => { + expect(() => Keystore.fromJSON(null)).toThrow(WalletError); + expect(() => Keystore.fromJSON('nope')).toThrow(/expected an object/); + }); + + it('should reject JSON missing the crypto section with WalletError', () => { + expect(() => Keystore.fromJSON({ version: 'midnight-1' })).toThrow( + /missing crypto section/, + ); + }); + }); +}); diff --git a/packages/deployer/src/wallet/keystore.ts b/packages/deployer/src/wallet/keystore.ts new file mode 100644 index 0000000..deb71f0 --- /dev/null +++ b/packages/deployer/src/wallet/keystore.ts @@ -0,0 +1,210 @@ +/** + * 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); + } + + /** Wrap parsed keystore JSON; validates shape + version/cipher/KDF eagerly. */ + static fromJSON(data: unknown): Keystore { + if (!data || typeof data !== 'object') { + throw new WalletError('Invalid keystore: expected an object'); + } + const d = data as Partial; + if (!d.crypto || typeof d.crypto !== 'object') { + throw new WalletError('Invalid keystore: missing crypto section'); + } + const crypto = d.crypto as MidnightKeystore['crypto']; + if (d.version !== VERSION) { + throw new WalletError( + `Unsupported keystore version: ${String(d.version)} (expected ${VERSION})`, + ); + } + if (crypto.kdf !== 'scrypt') { + throw new WalletError( + `Unsupported KDF: ${String(crypto.kdf)} (expected scrypt)`, + ); + } + if (crypto.cipher !== 'aes-128-ctr') { + throw new WalletError( + `Unsupported cipher: ${String(crypto.cipher)} (expected aes-128-ctr)`, + ); + } + return new Keystore(d as MidnightKeystore); + } + + /** 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..3da18d2 --- /dev/null +++ b/packages/deployer/src/wallet/seeds.test.ts @@ -0,0 +1,264 @@ +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/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 1777295..dc2eab8 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.4.16": version: 2.4.16 resolution: "@biomejs/biome@npm:2.4.16" @@ -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 + "@emnapi/core@npm:1.10.0": version: 1.10.0 resolution: "@emnapi/core@npm:1.10.0" @@ -133,6 +239,60 @@ __metadata: languageName: node linkType: hard +"@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: + "@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 + "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -142,14 +302,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 @@ -166,6 +326,37 @@ __metadata: languageName: node linkType: hard +"@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.16.0": version: 0.16.0 resolution: "@midnight-ntwrk/compact-runtime@npm:0.16.0" @@ -177,657 +368,3261 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/ledger-v8@npm:8.1.0": - version: 8.1.0 - resolution: "@midnight-ntwrk/ledger-v8@npm:8.1.0" - checksum: 10/10d56076b0333a502f157c816f8cfebefc8d50221cb20c6db15abcbf2d0092bdaf7e9bc1bd19a6d9f51455547c713c916cb16d4a7d18e83cba0e172ad6e2a507 +"@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/onchain-runtime-v3@npm:^3.0.0": - version: 3.0.0 - resolution: "@midnight-ntwrk/onchain-runtime-v3@npm:3.0.0" - checksum: 10/873aeb9e631c3678373c62b5aef847de454de94427028fb3d3f28bfdc8b2c02a3c770bd79d9bfef183eb9db6fb8c23e6826636f2e512ffd6eacbcf7cc0651c5d +"@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 -"@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.4 - resolution: "@napi-rs/wasm-runtime@npm:1.1.4" +"@midnight-ntwrk/midnight-js-compact@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-compact@npm:4.1.0" dependencies: - "@tybys/wasm-util": "npm:^0.10.1" - peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + "@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 -"@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:25.9.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.4" - typescript: "npm:^6.0.3" - vitest: "npm:^4.0.15" - languageName: unknown - linkType: soft - -"@openzeppelin/compact-cli@workspace:packages/cli": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-cli@workspace:packages/cli" +"@midnight-ntwrk/midnight-js-contracts@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-contracts@npm:4.1.0" dependencies: - "@openzeppelin/compact-builder": "workspace:^" - "@tsconfig/node24": "npm:^24.0.3" - "@types/node": "npm:25.9.1" - chalk: "npm:^5.6.2" - ora: "npm:^9.0.0" - typescript: "npm:^6.0.3" - vitest: "npm:^4.1.6" - bin: - compact-builder: dist/runBuilder.js - compact-compiler: dist/runCompiler.js - languageName: unknown - linkType: soft + "@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-simulator@workspace:packages/simulator": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-simulator@workspace:packages/simulator" +"@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: - "@midnight-ntwrk/compact-runtime": "npm:0.16.0" - "@midnight-ntwrk/ledger-v8": "npm:8.1.0" - "@tsconfig/node24": "npm:^24.0.3" - "@types/node": "npm:25.9.1" - fast-check: "npm:^4.5.2" - typescript: "npm:^6.0.3" - vitest: "npm:^4.1.6" - languageName: unknown - linkType: soft - -"@oxc-project/types@npm:=0.132.0": - version: 0.132.0 - resolution: "@oxc-project/types@npm:0.132.0" - checksum: 10/e0694a3c24746006ad774a1cab34efac3ccad5b519234063bcde17e9afe3475680749357e9f90164a222326414cb9510da1b8da350edc0cd35612fd05147c218 + "@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 -"@rolldown/binding-android-arm64@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-android-arm64@npm:1.0.2" - conditions: os=android & cpu=arm64 +"@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: + "@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/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 -"@rolldown/binding-darwin-arm64@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.2" - conditions: os=darwin & cpu=arm64 +"@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 -"@rolldown/binding-darwin-x64@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.2" - conditions: os=darwin & cpu=x64 +"@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 -"@rolldown/binding-freebsd-x64@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.2" - conditions: os=freebsd & cpu=x64 +"@midnight-ntwrk/midnight-js-protocol@npm:4.1.0": + version: 4.1.0 + resolution: "@midnight-ntwrk/midnight-js-protocol@npm:4.1.0" + 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 -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2" - conditions: os=linux & cpu=arm +"@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 -"@rolldown/binding-linux-arm64-gnu@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.2" - conditions: os=linux & cpu=arm64 & libc=glibc +"@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 -"@rolldown/binding-linux-arm64-musl@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.2" - conditions: os=linux & cpu=arm64 & libc=musl +"@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 -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.2" - conditions: os=linux & cpu=ppc64 & libc=glibc +"@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 -"@rolldown/binding-linux-s390x-gnu@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.2" - conditions: os=linux & cpu=s390x & 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 -"@rolldown/binding-linux-x64-gnu@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.2" - conditions: os=linux & cpu=x64 & libc=glibc +"@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 -"@rolldown/binding-linux-x64-musl@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.2" - conditions: os=linux & cpu=x64 & libc=musl +"@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 -"@rolldown/binding-openharmony-arm64@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.2" - conditions: os=openharmony & cpu=arm64 +"@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 -"@rolldown/binding-wasm32-wasi@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.2" +"@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: - "@emnapi/core": "npm:1.10.0" - "@emnapi/runtime": "npm:1.10.0" - "@napi-rs/wasm-runtime": "npm:^1.1.4" - conditions: cpu=wasm32 + "@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 -"@rolldown/binding-win32-arm64-msvc@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.2" - conditions: os=win32 & cpu=arm64 +"@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 -"@rolldown/binding-win32-x64-msvc@npm:1.0.2": - version: 1.0.2 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.2" - conditions: os=win32 & cpu=x64 +"@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 -"@rolldown/pluginutils@npm:^1.0.0": - version: 1.0.1 - resolution: "@rolldown/pluginutils@npm:1.0.1" - checksum: 10/4e95cf9ce23d75e5aa03ea0249cd86f7d1e21f83fbf6f8520e4edd8a251ba1b82c4ba9bc13cd24b6c4661daec6225b06e6d35c64c604e731b230b2a49af47d05 +"@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 -"@standard-schema/spec@npm:^1.1.0": - version: 1.1.0 - resolution: "@standard-schema/spec@npm:1.1.0" - checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 +"@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 -"@tsconfig/node10@npm:^1.0.7": - version: 1.0.12 - resolution: "@tsconfig/node10@npm:1.0.12" - checksum: 10/27e2f989dbb20f773aa121b609a5361a473b7047ff286fce7c851e61f5eec0c74f0bdb38d5bd69c8a06f17e60e9530188f2219b1cbeabeac91f0a5fd348eac2a +"@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 -"@tsconfig/node12@npm:^1.0.7": - version: 1.0.11 - resolution: "@tsconfig/node12@npm:1.0.11" - checksum: 10/5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a +"@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 -"@tsconfig/node14@npm:^1.0.0": - version: 1.0.3 - resolution: "@tsconfig/node14@npm:1.0.3" - checksum: 10/19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d +"@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 -"@tsconfig/node16@npm:^1.0.2": - version: 1.0.4 - resolution: "@tsconfig/node16@npm:1.0.4" - checksum: 10/202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff +"@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 -"@tsconfig/node24@npm:^24.0.3": - version: 24.0.4 - resolution: "@tsconfig/node24@npm:24.0.4" - checksum: 10/e3c011e4589d1d3f8774e6587434bf6d5655ca31c848ac1d173de7949e2f2e1c3dd0be5f97869201e46c4a6dda03003659e1c5078efc580ecefce1d706b8b6a1 +"@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 -"@turbo/darwin-64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/darwin-64@npm:2.9.14" - conditions: os=darwin & cpu=x64 +"@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 -"@turbo/darwin-arm64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/darwin-arm64@npm:2.9.14" +"@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 -"@turbo/linux-64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/linux-64@npm:2.9.14" - conditions: os=linux & cpu=x64 +"@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 -"@turbo/linux-arm64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/linux-arm64@npm:2.9.14" +"@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 -"@turbo/windows-64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/windows-64@npm:2.9.14" - conditions: os=win32 & cpu=x64 +"@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 -"@turbo/windows-arm64@npm:2.9.14": - version: 2.9.14 - resolution: "@turbo/windows-arm64@npm:2.9.14" - conditions: os=win32 & cpu=arm64 +"@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 -"@tybys/wasm-util@npm:^0.10.1": - version: 0.10.2 - resolution: "@tybys/wasm-util@npm:0.10.2" +"@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 + +"@napi-rs/wasm-runtime@npm:^1.1.4": + version: 1.1.4 + resolution: "@napi-rs/wasm-runtime@npm:1.1.4" + dependencies: + "@tybys/wasm-util": "npm:^0.10.1" + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + 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:25.9.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.4" + typescript: "npm:^6.0.3" + vitest: "npm:^4.0.15" + languageName: unknown + linkType: soft + +"@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:25.9.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:^6.0.3" + vitest: "npm:^4.1.6" + 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.1.6" + 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.16.0" + "@midnight-ntwrk/ledger-v8": "npm:8.1.0" + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:25.9.1" + fast-check: "npm:^4.5.2" + typescript: "npm:^6.0.3" + vitest: "npm:^4.1.6" + languageName: unknown + linkType: soft + +"@oxc-project/types@npm:=0.133.0": + version: 0.133.0 + resolution: "@oxc-project/types@npm:0.133.0" + checksum: 10/de44f653a9e0c0267309122f1f184120c6869af4382218a6bf4a320c5150743eb00b5e8641b04917666281995ed0fe6381561922a48a28082a75bb122acf3ac6 + languageName: node + linkType: hard + +"@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 + +"@rolldown/binding-android-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-android-arm64@npm:1.0.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.3" + dependencies: + "@emnapi/core": "npm:1.10.0" + "@emnapi/runtime": "npm:1.10.0" + "@napi-rs/wasm-runtime": "npm:^1.1.4" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/pluginutils@npm:^1.0.0": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10/4e95cf9ce23d75e5aa03ea0249cd86f7d1e21f83fbf6f8520e4edd8a251ba1b82c4ba9bc13cd24b6c4661daec6225b06e6d35c64c604e731b230b2a49af47d05 + 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" + checksum: 10/aee780cc1431888ca4b9aba9b24ffc8f3073fc083acc105e3951481478a2f4dc957796931b2da9e2d8329584cf211e4542275f188296c1cdff3ed44fd93a8bc8 + languageName: node + linkType: hard + +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 + 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" + checksum: 10/51fe47d55fe1b80ec35e6e5ed30a13665fd3a531945350aa74a14a1e82875fb60b350c2f2a5e72a64831b1b6bc02acb6760c30b3738b54954ec2dea82db7a267 + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10/5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10/19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10/202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + +"@tsconfig/node24@npm:^24.0.3": + version: 24.0.3 + resolution: "@tsconfig/node24@npm:24.0.3" + checksum: 10/02318441e4fc1a493be0ea4299813f952cc564e8eac7b23508f22a1774692e7b362ba58b9dddd9ffb052d1c9fadc4f1937f5590bc581584fe662ae83512feb8d + languageName: node + linkType: hard + +"@turbo/darwin-64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/darwin-64@npm:2.9.16" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@turbo/darwin-arm64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/darwin-arm64@npm:2.9.16" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@turbo/linux-64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/linux-64@npm:2.9.16" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@turbo/linux-arm64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/linux-arm64@npm:2.9.16" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@turbo/windows-64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/windows-64@npm:2.9.16" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@turbo/windows-arm64@npm:2.9.16": + version: 2.9.16 + resolution: "@turbo/windows-arm64@npm:2.9.16" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@tybys/wasm-util@npm:^0.10.1": + version: 0.10.2 + resolution: "@tybys/wasm-util@npm:0.10.2" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/d12f1dafe12d7a573c406b35ffef0038042b9cc9fbcc74d657267eb635499b956276afc05eebdbd81bea582e1c4c921421a1dd7243a93daaa8c8216b19395c23 + 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" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + 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.0": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 + 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" + dependencies: + undici-types: "npm:~7.16.0" + checksum: 10/ddac8c97be5f7401e31ea0e9316c6e864993c6cd06689b7f9874ecfb576ef8349f2d14298248a08b94a6dd029570a46a285cddc4d50e524817f1a3730b29a86e + languageName: node + linkType: hard + +"@types/node@npm:25.9.1": + version: 25.9.1 + resolution: "@types/node@npm:25.9.1" + dependencies: + undici-types: "npm:>=7.24.0 <7.24.7" + checksum: 10/8a1ccf60f0c0ca856d3324a690ee35776f26dfc1d51c3763aecdf246a3246a7971a0156bf6eb3239aa22dfa940eb361d048212f5c3204264d31ef4c41d17416a + 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" + checksum: 10/8caf52c815947540b5246e0b5b2d455a2183791fe9427537eab8a40b465392400cee6ce50beaeb35465e167e9cb405ccfde90eb5317ee2c9df85af7508f0a320 + languageName: node + linkType: hard + +"@types/shell-quote@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/shell-quote@npm:1.7.5" + checksum: 10/32b4d697c7d23dbadf40713692c47f1595f083a3b3deea76cb18e30a05d197aa9205d2b87f6d92edb60cda120b51e35d32bda96ed9b0a7e32921eed2deb4559e + 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.1.8": + version: 4.1.8 + resolution: "@vitest/coverage-v8@npm:4.1.8" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/utils": "npm:4.1.8" + ast-v8-to-istanbul: "npm:^1.0.0" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.2" + obug: "npm:^2.1.1" + std-env: "npm:^4.0.0-rc.1" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + "@vitest/browser": 4.1.8 + vitest: 4.1.8 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10/08d9ea65ca4cc007a1f1cdc85ea36d51bfa91a9b2f0e9ad27436b777629b4138e33dba2f68c8e68b01343310bf9d5624ad1d6d24553a5b289b66da51561259eb + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/expect@npm:4.1.8" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10/cb7d78e250ec77b7e180ac3e5f543501488c69b237d7ed97ffe9196c5e946b0e4a37be05a2ec38af7ce7750c1a98286480acdd247286a29c239b08a13b085d4b + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/mocker@npm:4.1.8" + dependencies: + "@vitest/spy": "npm:4.1.8" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/fc977703b07d950aa170bafdef988bc7ba88f0a80159d1563ce95696763729ec1f6d015012aad36cf4e1b522d327b205292c56d76692d2a9f72285d694ed3cba + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/pretty-format@npm:4.1.8" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10/56a4b685cdf9f2e9708025f17dab8c0fa990ab06e5b38606a1ddde52a09830a099843da6a1b127ee48217ab023bad7bd23c49eb4969d77dff07df363fad0bb0e + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/runner@npm:4.1.8" + dependencies: + "@vitest/utils": "npm:4.1.8" + pathe: "npm:^2.0.3" + checksum: 10/278d1482123877343731b3bb822d0280af928252ee263aab73ca189c39de3bb767ce715581870b2e1eb408f7cba01106a6989406cb2ada1332f181912558a3c1 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/snapshot@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10/162ca0eccb72db02081b04307d21ac8d14c8fcd4a840872459274f589b1665f108bd4119dff19d5a2150a0e26b90531791ebec7ee74f0c2c5285b491cebbcfcb + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/spy@npm:4.1.8" + checksum: 10/53e948d8f5e229e969e704dc8a54fd42ad715b2b18f401592f4bba97dcf33bd4cf01d11af577d4efe42dc2d90c9e6574ec991531fd8f1bdfee916a1dd0828547 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/utils@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10/13250b9e7825d425cc9a3d22aeb2e8d117c93e96a192138e93d76bfe7d5a391ab3888c5aa9e0394b0314bdff41e441ad7a32b0c0caa00cd202223b88087dcc78 + 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 + +"@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 + +"@wry/trie@npm:^0.5.0": + version: 0.5.0 + resolution: "@wry/trie@npm:0.5.0" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/578a08f3a96256c9b163230337183d9511fd775bdfe147a30561ccaacedc9ce33b9731ee6e591bb1f5f53e41b26789e519b47dff5100c7bf4e1cd2df3062f797 + languageName: node + linkType: hard + +"abbrev@npm:^3.0.0": + version: 3.0.1 + resolution: "abbrev@npm:3.0.1" + checksum: 10/ebd2c149dda6f543b66ce3779ea612151bb3aa9d0824f169773ee9876f1ca5a4e0adbcccc7eed048c04da7998e1825e2aa76fcca92d9e67dea50ac2b0a58dc2e + 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" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10/871386764e1451c637bb8ab9f76f4995d408057e9909be6fb5ad68537ae3375d85e6a6f170b98989f44ab3ff6c74ad120bc2779a3d577606e7a0cd2b4efcaf77 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" + bin: + acorn: bin/acorn + checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 + 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" + checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10/2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10/b4494dfbfc7e4591b4711a396bd27e540f8153914123dccb4cdbbcb514015ada63a3809f362b9d8d4f6b17a706f1d7bea3c6f974b15fa5ae76b5b502070889ff + languageName: node + linkType: hard + +"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 + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 + 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" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + +"ast-v8-to-istanbul@npm:^1.0.0": + version: 1.0.3 + resolution: "ast-v8-to-istanbul@npm:1.0.3" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10/7b9079a87d311f4a4dd13c6a3bd2cc76c70b094ab7a8359eaf8314bf8a5841b38db0ea26f12ae5ee08abf0654fc46778db6562c1e8ff6145739ba659ee139744 + 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" + checksum: 10/9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"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: + balanced-match: "npm:^1.0.0" + checksum: 10/c77a7a64aabf94b8d5913955adb4f36957917565374461355bb4276830c027a313d981f32410cea9e38f52573e7eb776d02fe05091c3a79a061958d97e4d2b43 + 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" + dependencies: + "@npmcli/fs": "npm:^4.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^12.0.0" + tar: "npm:^7.4.3" + unique-filename: "npm:^4.0.0" + checksum: 10/ea026b27b13656330c2bbaa462a88181dcaa0435c1c2e705db89b31d9bdf7126049d6d0445ba746dca21454a0cfdf1d6f47fd39d34c8c8435296b30bc5738a13 + 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.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f + languageName: node + linkType: hard + +"chalk@npm:^5.6.2": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 + 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" + checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c + 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" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + languageName: node + linkType: hard + +"cli-spinners@npm:^3.2.0": + version: 3.3.0 + resolution: "cli-spinners@npm:3.3.0" + checksum: 10/d95f69f4a6a4efab2104ca5d4723c9f6fae9a4006df7fdcc1f79ea6539324e274b85bf6f5931146d84296b0f71814f4c1ff1acc158f2e1107c0c9797c1291bcc + 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" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10/fa00c91b4332b294de06b443923246bccebe9fab1b253f7fe1772d37b06a2269b4039a85e309abe1fe11b267b11c08d1d0473fda3badd6167f57313af2887a64 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + 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-tools-monorepo@workspace:.": + version: 0.0.0-use.local + resolution: "compact-tools-monorepo@workspace:." + dependencies: + "@biomejs/biome": "npm:2.4.16" + "@openzeppelin/compact-deployer": "workspace:^" + "@types/node": "npm:25.9.1" + "@vitest/coverage-v8": "npm:4.1.8" + pino: "npm:^9.7.0" + ts-node: "npm:^10.9.2" + turbo: "npm:^2.9.14" + typescript: "npm:^6.0.3" + vitest: "npm:^4.1.6" + 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 + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10/c987be3ec061348cdb3c2bfb924bec86dea1eacad10550a85ca23edb0fe3556c3a61c7399114f3331ccb3499d7fd0285ab24566e5745929412983494c3926e15 + 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" + checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + 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" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10/0d52657d7ae36eb130999dffff1168ec348687b48dd38e2ff59992ed916c88d328cf1d07ff4a4a10bc78de5e1c23f04b306d569e42f7a2293915c081e4dfee86 + languageName: node + linkType: hard + +"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: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad + languageName: node + linkType: hard + +"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, detect-libc@npm:^2.0.3": + 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 + +"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 + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10/c72d67a6821be15ec11997877c437491c313d924306b8da5d87d2a2bcc2cec9903cb5b04ee1a088460501d8e5b44f10df82fdc93c444101a7610b80c8b6938e1 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10/bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + 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" + checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10/1d20d825cdcce8d811bfbe86340f4755c02655a7feb2f13f8c880566d9d72a3f6c92c192a6867632e490d6da67b678271f46e01044996a6443e870331100dfdd + 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:^2.0.0": + version: 2.1.0 + resolution: "es-module-lexer@npm:2.1.0" + checksum: 10/554c4374e78a812a1fa3673871ce7d42236438c414ea80c2ec35521cd9bb26d1d9155287529057d07431fd91df50d6a26d9bee5afd755fb7f6f7c81905a03956 + 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: - tslib: "npm:^2.4.0" - checksum: 10/d12f1dafe12d7a573c406b35ffef0038042b9cc9fbcc74d657267eb635499b956276afc05eebdbd81bea582e1c4c921421a1dd7243a93daaa8c8216b19395c23 + es-errors: "npm:^1.3.0" + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 languageName: node linkType: hard -"@types/chai@npm:^5.2.2": - version: 5.2.3 - resolution: "@types/chai@npm:5.2.3" +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" dependencies: - "@types/deep-eql": "npm:*" - assertion-error: "npm:^2.0.1" - checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + 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 -"@types/deep-eql@npm:*": - version: 4.0.2 - resolution: "@types/deep-eql@npm:4.0.2" - checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 languageName: node linkType: hard -"@types/estree@npm:^1.0.0": - version: 1.0.9 - resolution: "@types/estree@npm:1.0.9" - checksum: 10/16aabfa703b5bdac83f719b07ce92a11b2d3c9b8628eacc92889d8af46cab2d78fc45c7b5378de383d0500585cea5c2f79125eeddfe5fbc6bd6a27eb0c8ccee5 +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af languageName: node linkType: hard -"@types/node@npm:25.9.1": - version: 25.9.1 - resolution: "@types/node@npm:25.9.1" +"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: - undici-types: "npm:>=7.24.0 <7.24.7" - checksum: 10/8a1ccf60f0c0ca856d3324a690ee35776f26dfc1d51c3763aecdf246a3246a7971a0156bf6eb3239aa22dfa940eb361d048212f5c3204264d31ef4c41d17416a + bare-events: "npm:^2.7.0" + checksum: 10/71b2e6079b4dc030c613ef73d99f1acb369dd3ddb6034f49fd98b3e2c6632cde9f61c15fb1351004339d7c79672252a4694ecc46a6124dc794b558be50a83867 languageName: node linkType: hard -"@types/object-inspect@npm:^1.8.1": - version: 1.13.0 - resolution: "@types/object-inspect@npm:1.13.0" - checksum: 10/8caf52c815947540b5246e0b5b2d455a2183791fe9427537eab8a40b465392400cee6ce50beaeb35465e167e9cb405ccfde90eb5317ee2c9df85af7508f0a320 +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be languageName: node linkType: hard -"@types/shell-quote@npm:^1.7.5": - version: 1.7.5 - resolution: "@types/shell-quote@npm:1.7.5" - checksum: 10/32b4d697c7d23dbadf40713692c47f1595f083a3b3deea76cb18e30a05d197aa9205d2b87f6d92edb60cda120b51e35d32bda96ed9b0a7e32921eed2deb4559e +"expect-type@npm:^1.3.0": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10/a5fada3d0c621649261f886e7d93e6bf80ce26d8a86e5d517e38301b8baec8450ab2cb94ba6e7a0a6bf2fc9ee55f54e1b06938ef1efa52ddcfeffbfa01acbbcc + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10/ca25962b4bbab943b7c4ed0b5228e263833a5063c65e1cdeac4be9afad350aae5466e8e619b5051f4f8d37b2144a2d6e8fcc771b6cc82934f7dade2f964f652c languageName: node linkType: hard -"@vitest/expect@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/expect@npm:4.1.7" +"fast-check@npm:^3.23.1": + version: 3.23.2 + resolution: "fast-check@npm:3.23.2" dependencies: - "@standard-schema/spec": "npm:^1.1.0" - "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.7" - "@vitest/utils": "npm:4.1.7" - chai: "npm:^6.2.2" - tinyrainbow: "npm:^3.1.0" - checksum: 10/a609af6c0497cd510ce8aed099f18faf6d6642bc8eb3432b688f2b39d7354a04d1c4ee9dc28bcfb9d4be701ceac88384d586592a520a324b3773ea43e8a1e677 + pure-rand: "npm:^6.1.0" + checksum: 10/dab344146b778e8bc2973366ea55528d1b58d3e3037270262b877c54241e800c4d744957722c24705c787020d702aece11e57c9e3dbd5ea19c3e10926bf1f3fe languageName: node linkType: hard -"@vitest/mocker@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/mocker@npm:4.1.7" +"fast-check@npm:^4.5.2": + version: 4.5.2 + resolution: "fast-check@npm:4.5.2" dependencies: - "@vitest/spy": "npm:4.1.7" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" + pure-rand: "npm:^7.0.0" + checksum: 10/20193f70f4087641f82ca8aede1a395fb974fe170c317f589bf2614e2bb98ef16873b27091e8359c2cc79d59f9c32f042e84b1eef13053068ee94150bf08a0bb + 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" peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + picomatch: ^3 || ^4 peerDependenciesMeta: - msw: + picomatch: optional: true - vite: + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + +"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/124d0ec9cc099fde1fca4b065b81a389e9ba2204ecba9729751a0a022d0ffaa34609d9dc60c1f8494ee972c2209035a4476ff1dddc1790e07d1ca28a1103b30d + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/pretty-format@npm:4.1.7" +"foreground-child@npm:^3.3.1": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" dependencies: - tinyrainbow: "npm:^3.1.0" - checksum: 10/79c86c39173577250955744c3444d8c0c9304c95c7d351b91a916229252c3733a0e969741a8f3441a5c4777b5a4371707ecb747ea4bfd2c07e72ddf1ef621293 + cross-spawn: "npm:^7.0.6" + signal-exit: "npm:^4.0.1" + checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 languageName: node linkType: hard -"@vitest/runner@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/runner@npm:4.1.7" +"form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" dependencies: - "@vitest/utils": "npm:4.1.7" - pathe: "npm:^2.0.3" - checksum: 10/429f1e0cc93f66a681d8acc816e21ac41258b07550f9139d004aab103bb06be53e3d91fc66886cef1ba1460a120f5fe4b12d6fe32dafdb1b06740dd119d70f7e + 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 -"@vitest/snapshot@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/snapshot@npm:4.1.7" +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" dependencies: - "@vitest/pretty-format": "npm:4.1.7" - "@vitest/utils": "npm:4.1.7" - magic-string: "npm:^0.30.21" - pathe: "npm:^2.0.3" - checksum: 10/ef7001add6724c025772891616338e6081ecdb11a92c084ca1d09c4662cf632e5877bec4cb38056aabc311f29fbe149c89fbf332975829087f3817554fe92cde + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f languageName: node linkType: hard -"@vitest/spy@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/spy@npm:4.1.7" - checksum: 10/49a9959c615f45ec593379a6d1a238190d08524857a6c4819b724134ce8a1a96d94e20144723d245941ce1ada54d8b00552573810d629880ecb8c3ff03b6d1ad +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d languageName: node linkType: hard -"@vitest/utils@npm:4.1.7": - version: 4.1.7 - resolution: "@vitest/utils@npm:4.1.7" +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" dependencies: - "@vitest/pretty-format": "npm:4.1.7" - convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.1.0" - checksum: 10/9cc729618dade24de3ad6862c288c22e9daac3fda5cae0abc9b6ce87035cc8e7efa2b66c3c124ae08beef462b36761b062e792bbc619798b832a7ea9382ed12a + minipass: "npm:^7.0.3" + checksum: 10/af143246cf6884fe26fa281621d45cfe111d34b30535a475bfa38dafe343dadb466c047a924ffc7d6b7b18265df4110224ce3803806dbb07173bf2087b648d7f languageName: node linkType: hard -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10/4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0 + conditions: os=darwin languageName: node linkType: hard -"acorn-walk@npm:^8.1.1": - version: 8.3.5 - resolution: "acorn-walk@npm:8.3.5" +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: - acorn: "npm:^8.11.0" - checksum: 10/f52a158a1c1f00c82702c7eb9b8ae8aad79748a7689241dcc2d797dce680f1dcb15c78f312f687eeacdfb3a4cac4b87d04af470f0201bd56c6661fca6f94b195 + node-gyp: "npm:latest" + conditions: os=darwin languageName: node linkType: hard -"acorn@npm:^8.11.0, acorn@npm:^8.4.1": - version: 8.16.0 - resolution: "acorn@npm:8.16.0" +"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" + checksum: 10/c9ae85bfc2feaf4cc71cdb236e60f1757ae82281964c206c6aa89a25f1987d326ddd8b0de9f9ccd56e37711b9fcd988f7f5137118b49b0b45e19df93c3be8f45 + languageName: node + linkType: hard + +"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.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:^2.0.0" bin: - acorn: bin/acorn - checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b + glob: dist/esm/bin.mjs + checksum: 10/da4501819633daff8822c007bb3f93d5c4d2cbc7b15a8e886660f4497dd251a1fb4f53a85fba1e760b31704eff7164aeb2c7a82db10f9f2c362d12c02fe52cf3 languageName: node linkType: hard -"ansi-regex@npm:^6.2.2": - version: 6.2.2 - resolution: "ansi-regex@npm:6.2.2" - checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 languageName: node linkType: hard -"arg@npm:^4.1.0": - version: 4.1.3 - resolution: "arg@npm:4.1.3" - checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 +"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 -"assertion-error@npm:^2.0.1": - version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" - checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 +"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 -"chai@npm:^6.2.2": - version: 6.2.2 - resolution: "chai@npm:6.2.2" - checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f +"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 -"chalk@npm:^5.6.2": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 +"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 -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c +"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 -"cli-cursor@npm:^5.0.0": +"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: "cli-cursor@npm: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" + checksum: 10/4efd2dfcfeea9d5e88c84af450b9980be8a43c2c8179508b1c57c7b4421c855f3e8efe92fa53e0b3f4a43c85824ada930eabbc306d1b3beab750b6dcc5187693 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: - restore-cursor: "npm:^5.0.0" - checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090 + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 languageName: node linkType: hard -"cli-spinners@npm:^3.2.0": - version: 3.4.0 - resolution: "cli-spinners@npm:3.4.0" - checksum: 10/6a4021c1999011fc34ae714f055dcdafb56309abc1f8fb021ea7d9370dfc524485fe8684226015e5fe6053dd30544e74270184ff7edc3fa4d37043b8efd0a054 +"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 -"compact-tools-monorepo@workspace:.": - version: 0.0.0-use.local - resolution: "compact-tools-monorepo@workspace:." +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" dependencies: - "@biomejs/biome": "npm:2.4.16" - "@types/node": "npm:25.9.1" - ts-node: "npm:^10.9.2" - turbo: "npm:^2.9.14" - typescript: "npm:^6.0.3" - vitest: "npm:^4.1.6" - languageName: unknown - linkType: soft + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10/24e3292dd3dadaa81d065c6f8c41b274a47098150d444b96e5f53b4638a9a71482921ea6a91a1f59bb71d9796de25e04afd05919fa64c360347ba65d3766f10f + 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" + checksum: 10/2d30b157a91fe1c1d7c6f653cbf263f039be6c5bfa959245a16d4ee191fc0f2af86c08545b6e6beeb041c56b574d2d5b9f95343d378ab49c0f37394d541e7fc8 + 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" + checksum: 10/12fec399e1af5753ac322e47a6d81a50d3a528b3abb17c09525b2a2edcaedcca628c40520706f7037bc4d8e951b0296c47e7b86d0a8e6e2335c8f0ba4afcfac1 + 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" + checksum: 10/44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard -"convert-source-map@npm:^2.0.0": +"is-interactive@npm:^2.0.0": version: 2.0.0 - resolution: "convert-source-map@npm:2.0.0" - checksum: 10/c987be3ec061348cdb3c2bfb924bec86dea1eacad10550a85ca23edb0fe3556c3a61c7399114f3331ccb3499d7fd0285ab24566e5745929412983494c3926e15 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c languageName: node linkType: hard -"create-require@npm:^1.1.0": - version: 1.1.1 - resolution: "create-require@npm:1.1.1" - checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff +"is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 languageName: node linkType: hard -"detect-libc@npm:^2.0.3": - version: 2.1.2 - resolution: "detect-libc@npm:2.1.2" - checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 +"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 -"diff@npm:^4.0.1": - version: 4.0.4 - resolution: "diff@npm:4.0.4" - checksum: 10/5019b3f5ae124ea9e95137119e1a83a59c252c75ddac873cc967832fd7a834570a58a4d58b941bdbd07832ebf98dcb232b27c561b7f5584357da6dae59bcac62 +"is-what@npm:^5.2.0": + version: 5.5.0 + resolution: "is-what@npm:5.5.0" + checksum: 10/d53a6ea1aebf953f3bcf711a28e8463bfe79fc0e4e87575d77c692a30fd3d98f87b88d4c006c06753bf85f771c9d2c1d05b2c6b03c246883261fe190526195d9 languageName: node linkType: hard -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab languageName: node linkType: hard -"es-module-lexer@npm:^2.0.0": - version: 2.1.0 - resolution: "es-module-lexer@npm:2.1.0" - checksum: 10/554c4374e78a812a1fa3673871ce7d42236438c414ea80c2ec35521cd9bb26d1d9155287529057d07431fd91df50d6a26d9bee5afd755fb7f6f7c81905a03956 +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10/7c9f715c03aff08f35e98b1fadae1b9267b38f0615d501824f9743f3aab99ef10e303ce7db3f186763a0b70a19de5791ebfc854ff884d5a8c4d92211f642ec92 languageName: node linkType: hard -"estree-walker@npm:^3.0.3": - version: 3.0.3 - resolution: "estree-walker@npm:3.0.3" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e languageName: node linkType: hard -"expect-type@npm:^1.3.0": - version: 1.3.0 - resolution: "expect-type@npm:1.3.0" - checksum: 10/a5fada3d0c621649261f886e7d93e6bf80ce26d8a86e5d517e38301b8baec8450ab2cb94ba6e7a0a6bf2fc9ee55f54e1b06938ef1efa52ddcfeffbfa01acbbcc +"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 -"exponential-backoff@npm:^3.1.1": - version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" - checksum: 10/ca25962b4bbab943b7c4ed0b5228e263833a5063c65e1cdeac4be9afad350aae5466e8e619b5051f4f8d37b2144a2d6e8fcc771b6cc82934f7dade2f964f652c +"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 -"fast-check@npm:^4.5.2": - version: 4.8.0 - resolution: "fast-check@npm:4.8.0" +"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: - pure-rand: "npm:^8.0.0" - checksum: 10/32101d27e69f7f0a3ea486f1a6feee6b34c824ba14bf3efce2ad1741fcd412817ac3ed14f53ef57a55d6fe2e00038e399c4a9c62444446a6e7a1a4b04bc6b88d + 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 -"fdir@npm:^6.5.0": - version: 6.5.0 - resolution: "fdir@npm:6.5.0" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 +"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 -"fsevents@npm:~2.3.3": - version: 2.3.3 - resolution: "fsevents@npm:2.3.3" +"jackspeak@npm:^4.1.1": + version: 4.2.3 + resolution: "jackspeak@npm:4.2.3" dependencies: - node-gyp: "npm:latest" - checksum: 10/4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0 - conditions: os=darwin + "@isaacs/cliui": "npm:^9.0.0" + checksum: 10/b88e3fe5fa04d34f0f939a15b7cef4a8589999b7a366ef89a3e0f2c45d2a7666066b67cbf46d57c3a4796a76d27b9d869b23d96a803dd834200d222c2a70de7e languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 languageName: node linkType: hard -"get-east-asian-width@npm:^1.5.0": - version: 1.6.0 - resolution: "get-east-asian-width@npm:1.6.0" - checksum: 10/3e5370b5df1f0020db711d8a3f9ee2cbfc9c7542daa99a699e9d7b9acf66e7868b89084741565a45d30d80afedf6e1218e0fb8bef7a583924a449c2816777380 +"js-tokens@npm:^10.0.0": + version: 10.0.0 + resolution: "js-tokens@npm:10.0.0" + checksum: 10/88f536ec89f076fc230d29df255b3c55531237669d746d1868fca716b1e3f5f2e4abf8e5b8701903216e3f00d2dc3918d078b35da87772d433ab6a513c3bf76d languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 +"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 -"is-interactive@npm:^2.0.0": - version: 2.0.0 - resolution: "is-interactive@npm:2.0.0" - checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c +"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 -"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 +"level-supports@npm:^6.2.0": + version: 6.2.0 + resolution: "level-supports@npm:6.2.0" + checksum: 10/450c04839cf42ac7c73085b4928f1c1c51d9ab179aac9102cc8ef2389faf2d06cebaf57df2d025da89d78465004ccf29bfd972a04b0b35d5d423fa3f4516f906 languageName: node linkType: hard -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10/2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 +"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 + +"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 @@ -925,118 +3720,491 @@ __metadata: lightningcss-win32-arm64-msvc: "npm:1.32.0" lightningcss-win32-x64-msvc: "npm:1.32.0" dependenciesMeta: - lightningcss-android-arm64: - optional: true - lightningcss-darwin-arm64: - optional: true - lightningcss-darwin-x64: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10/098e61007f0d0ec8b5c50884e33b543b551d1ff21bc7b062434b6638fd0b8596858f823b60dfc2a4aa756f3cb120ad79f2b7f4a55b1bda2c0269ab8cf476f114 + languageName: node + linkType: hard + +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10/c301cc379310441dc73cd6cebeb91fb254bea74e6ad3027f9346fc43b4174385153df420ffa521654e502fd34c40ef69ca4e7d40ee7129a99e06f306032bfc65 + languageName: node + linkType: hard + +"lodash@npm:^4.17.15": + version: 4.18.1 + resolution: "lodash@npm:4.18.1" + checksum: 10/306fea53dfd39dad1f03d45ba654a2405aebd35797b673077f401edb7df2543623dc44b9effbb98f69b32152295fff725a4cec99c684098947430600c6af0c3f + languageName: node + linkType: hard + +"log-symbols@npm:^7.0.0, log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: "npm:^2.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + languageName: node + linkType: hard + +"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" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + +"magicast@npm:^0.5.2": + 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" + checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^14.0.3": + version: 14.0.3 + resolution: "make-fetch-happen@npm:14.0.3" + dependencies: + "@npmcli/agent": "npm:^3.0.0" + cacache: "npm:^19.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^4.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^5.0.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^12.0.0" + checksum: 10/fce0385840b6d86b735053dfe941edc2dd6468fda80fe74da1eeff10cbd82a75760f406194f2bc2fa85b99545b2bc1f84c08ddf994b21830775ba2d1a87e8bdf + 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" + checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c + languageName: node + linkType: hard + +"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.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 + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^4.0.0": + version: 4.0.1 + resolution: "minipass-fetch@npm:4.0.1" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^3.0.1" + dependenciesMeta: + encoding: + optional: true + checksum: 10/7ddfebdbb87d9866e7b5f7eead5a9e3d9d507992af932a11d275551f60006cf7d9178e66d586dbb910894f3e3458d27c0ddf93c76e94d49d0a54a541ddc1263d + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/40982d8d836a52b0f37049a0a7e5d0f089637298e6d9b45df9c115d4f0520682a78258905e5c8b180fb41b593b0a82cc1361d2c74b45f7ada66334f84d1ecfdd + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10/a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 + languageName: node + linkType: hard + +"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 + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 + 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" + checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + 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 - lightningcss-freebsd-x64: - optional: true - lightningcss-linux-arm-gnueabihf: - optional: true - lightningcss-linux-arm64-gnu: + "@msgpackr-extract/msgpackr-extract-darwin-x64": optional: true - lightningcss-linux-arm64-musl: + "@msgpackr-extract/msgpackr-extract-linux-arm": optional: true - lightningcss-linux-x64-gnu: + "@msgpackr-extract/msgpackr-extract-linux-arm64": optional: true - lightningcss-linux-x64-musl: + "@msgpackr-extract/msgpackr-extract-linux-x64": optional: true - lightningcss-win32-arm64-msvc: + "@msgpackr-extract/msgpackr-extract-win32-x64": optional: true - lightningcss-win32-x64-msvc: + 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/098e61007f0d0ec8b5c50884e33b543b551d1ff21bc7b062434b6638fd0b8596858f823b60dfc2a4aa756f3cb120ad79f2b7f4a55b1bda2c0269ab8cf476f114 + checksum: 10/8077d7ebf661df831ba119a277588b7e00149d25b6f5630e311c2415504553ce695347a351a7198cdf1f596feaaf91121adc3181e483f7d2c9822484b73babf2 languageName: node linkType: hard -"log-symbols@npm:^7.0.0, log-symbols@npm:^7.0.1": - version: 7.0.1 - resolution: "log-symbols@npm:7.0.1" +"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: - is-unicode-supported: "npm:^2.0.0" - yoctocolors: "npm:^2.1.1" - checksum: 10/0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + node-gyp: "npm:latest" + checksum: 10/bdce0630e417740501394c412bd9f0ed1c287825e3b8f9b7efb95cc3acd3ef69de60479b5f00a2d039b79321e5ce29b672b0b263cfe0e4d8f47c8f810a24a5ee languageName: node linkType: hard -"magic-string@npm:^0.30.21": - version: 0.30.21 - resolution: "magic-string@npm:0.30.21" +"nanoid@npm:^3.3.12": + version: 3.3.12 + resolution: "nanoid@npm:3.3.12" + bin: + nanoid: bin/nanoid.cjs + checksum: 10/6eec280694e2088d18fb802b1e3bfc4578e27b665b7ecfbe36c7356612fea2f814277056e671e2a1529dff551588a652efdc0bfa39f8a3185bc2247be311872e + 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" + checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 + languageName: node + linkType: hard + +"nock@npm:^13.5.5": + version: 13.5.6 + resolution: "nock@npm:13.5.6" dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.5" - checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + debug: "npm:^4.1.0" + json-stringify-safe: "npm:^5.0.1" + propagate: "npm:^2.0.0" + checksum: 10/a57c265b75e5f7767e2f8baf058773cdbf357c31c5fea2761386ec03a008a657f9df921899fe2a9502773b47145b708863b32345aef529b3c45cba4019120f88 languageName: node linkType: hard -"make-error@npm:^1.1.1": - version: 1.3.6 - resolution: "make-error@npm:1.3.6" - checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 languageName: node linkType: hard -"mimic-function@npm:^5.0.0": - version: 5.0.1 - resolution: "mimic-function@npm:5.0.1" - checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c +"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 -"minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.3 - resolution: "minipass@npm:7.1.3" - checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 +"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 -"minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" +"node-gyp-build-optional-packages@npm:5.2.2": + version: 5.2.2 + resolution: "node-gyp-build-optional-packages@npm:5.2.2" dependencies: - minipass: "npm:^7.1.2" - checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 + 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 -"nanoid@npm:^3.3.12": - version: 3.3.12 - resolution: "nanoid@npm:3.3.12" +"node-gyp-build@npm:^4.3.0": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" bin: - nanoid: bin/nanoid.cjs - checksum: 10/6eec280694e2088d18fb802b1e3bfc4578e27b665b7ecfbe36c7356612fea2f814277056e671e2a1529dff551588a652efdc0bfa39f8a3185bc2247be311872e + 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: 12.3.0 - resolution: "node-gyp@npm:12.3.0" + version: 11.5.0 + resolution: "node-gyp@npm:11.5.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" graceful-fs: "npm:^4.2.6" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" semver: "npm:^7.3.5" - tar: "npm:^7.5.4" + tar: "npm:^7.4.3" tinyglobby: "npm:^0.2.12" - undici: "npm:^6.25.0" - which: "npm:^6.0.0" + which: "npm:^5.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/cd97bf17f0f3e6288c42cc23a6db8528a98e7530abdb72ab558272906d603362e4558069f99f8a5250bc78f65ff305b1438caca4f1b31c81904a8798c242603e + checksum: 10/15a600b626116e1e528c49f73027c5ff84dbf6986df77b0fb61d6eb079ab4230c39f245295cb67f0590e6541a848cbd267e00c5769e8fb8bf88a5cca3701b551 languageName: node linkType: hard -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" +"nopt@npm:^8.0.0": + version: 8.1.0 + resolution: "nopt@npm:8.1.0" dependencies: - abbrev: "npm:^4.0.0" + abbrev: "npm:^3.0.0" bin: nopt: bin/nopt.js - checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda + checksum: 10/26ab456c51a96f02a9e5aa8d1b80ef3219f2070f3f3528a040e32fb735b1e651e17bdf0f1476988d3a46d498f35c65ed662d122f340d38ce4a7e71dd7b20c4bc + 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 @@ -1054,6 +4222,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" @@ -1063,9 +4247,21 @@ __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.4.0 - resolution: "ora@npm:9.4.0" + version: 9.0.0 + resolution: "ora@npm:9.0.0" dependencies: chalk: "npm:^5.6.2" cli-cursor: "npm:^5.0.0" @@ -1073,9 +4269,41 @@ __metadata: is-interactive: "npm:^2.0.0" is-unicode-supported: "npm:^2.1.0" log-symbols: "npm:^7.0.1" - stdin-discarder: "npm:^0.3.2" + stdin-discarder: "npm:^0.2.2" string-width: "npm:^8.1.0" - checksum: 10/48fe48f98764d1132a77d845862fab1b1f8d7aacd4c38c39ad42a874a55686c5949194eeb836a7ee4c43027a471ffb93937eff69f4db7353d6800b678073de55 + strip-ansi: "npm:^7.1.2" + checksum: 10/b6074c9cec4a39c1b4f41c2ce2741982a99c53c86bd6f07a28fb6274857263af7fe1a340136629939934b553af35b03fc62ca2a88baa6803b2f9bfdf269fb850 + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10/2ef48ccfc6dd387253d71bf502604f7893ed62090b2c9d73387f10006c342606b05233da0e4f29388227b61eb5aeface6197e166520c465c234552eeab2fe633 + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10/55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"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 @@ -1100,6 +4328,75 @@ __metadata: 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.15": version: 8.5.15 resolution: "postcss@npm:8.5.15" @@ -1111,17 +4408,188 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^6.0.0": +"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: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/d043c3e710c56ffd280711e98a94e863ab334f79ea43cee0fb70e1349b2355ffd2ff287c7522e4c960a247699d5b7825f00fa090b85d6179c973be13f78a6c49 + languageName: node + linkType: hard + +"pure-rand@npm:^6.1.0": version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 + resolution: "pure-rand@npm:6.1.0" + checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4 + languageName: node + linkType: hard + +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 + languageName: node + linkType: hard + +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7 + languageName: node + linkType: hard + +"readable-stream@npm:^2.0.5": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + 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 + +"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 + +"readable-stream@npm:^4.0.0": + version: 4.7.0 + resolution: "readable-stream@npm:4.7.0" + dependencies: + 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 + +"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 -"pure-rand@npm:^8.0.0": - version: 8.4.0 - resolution: "pure-rand@npm:8.4.0" - checksum: 10/baaee81b6647c89e426458fa0a6fcf51a42315db3db6a2c1c48d676813dd7cebf46dc56d352ce352c3bdfacd5a965b9c7783ba2a92f38cf2e64a9a8186469c3e +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf languageName: node linkType: hard @@ -1135,26 +4603,33 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.2": - version: 1.0.2 - resolution: "rolldown@npm:1.0.2" - dependencies: - "@oxc-project/types": "npm:=0.132.0" - "@rolldown/binding-android-arm64": "npm:1.0.2" - "@rolldown/binding-darwin-arm64": "npm:1.0.2" - "@rolldown/binding-darwin-x64": "npm:1.0.2" - "@rolldown/binding-freebsd-x64": "npm:1.0.2" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.2" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.2" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.2" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.2" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.2" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.2" - "@rolldown/binding-linux-x64-musl": "npm:1.0.2" - "@rolldown/binding-openharmony-arm64": "npm:1.0.2" - "@rolldown/binding-wasm32-wasi": "npm:1.0.2" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.2" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.2" +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 + languageName: node + linkType: hard + +"rolldown@npm:1.0.3": + version: 1.0.3 + resolution: "rolldown@npm:1.0.3" + dependencies: + "@oxc-project/types": "npm:=0.133.0" + "@rolldown/binding-android-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-x64": "npm:1.0.3" + "@rolldown/binding-freebsd-x64": "npm:1.0.3" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.3" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.3" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-musl": "npm:1.0.3" + "@rolldown/binding-openharmony-arm64": "npm:1.0.3" + "@rolldown/binding-wasm32-wasi": "npm:1.0.3" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.3" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.3" "@rolldown/pluginutils": "npm:^1.0.0" dependenciesMeta: "@rolldown/binding-android-arm64": @@ -1189,16 +4664,92 @@ __metadata: optional: true bin: rolldown: ./bin/cli.mjs - checksum: 10/2e51f0b2332eef4001262dad360886ca11376558ce270fbddad6182870395200b123ad75d412e60cb4328650d1df2cb74ae374e79edf930c030bfb693c9b1891 + checksum: 10/4dbe2c055104c47c15c051b713068cf4660acd473841904d3f7118f730922b2e498176610a45826cbc1ffe36842a29a076385d3bfcd5acb0f7ef8ad06b8feefb + languageName: node + linkType: hard + +"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" + bin: + semver: bin/semver.js + checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 + 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 -"semver@npm:^7.3.5": - version: 7.8.0 - resolution: "semver@npm:7.8.0" - bin: - semver: bin/semver.js - checksum: 10/039a8f68a581c03c1ac17c990316da57a79a93af9b109b712739c50cd4d464079f7e3fee31c008b472e390c7ba48a11ed2b86e91d8602bf06059d4a266db1426 +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10/6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10/1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 languageName: node linkType: hard @@ -1216,13 +4767,73 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.1.0": +"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" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f languageName: node linkType: hard +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10/927484aa0b1640fd9473cee3e0a0bcad6fce93fd7bbc18bac9ad0c33686f5d2e2c422fba24b5899c184524af01e11dd2bd051c2bf2b07e47aff8ca72cbfc60d2 + 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" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: "npm:^10.0.1" + smart-buffer: "npm:^4.2.0" + checksum: 10/d19366c95908c19db154f329bbe94c2317d315dc933a7c2b5101e73f32a555c84fb199b62174e1490082a593a4933d8d5a9b297bde7d1419c14a11a965f51356 + 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" @@ -1230,6 +4841,56 @@ __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" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/7024c1a6e39b3f18aa8f1c8290e884fe91b0f9ca5a6c6d410544daad54de0ba664db879afe16412e187c6c292fd60b937f047ee44292e5c2af2dcc6d8e1a9b48 + languageName: node + linkType: hard + "stackback@npm:0.0.2": version: 0.0.2 resolution: "stackback@npm:0.0.2" @@ -1244,42 +4905,220 @@ __metadata: languageName: node linkType: hard -"stdin-discarder@npm:^0.3.2": - version: 0.3.2 - resolution: "stdin-discarder@npm:0.3.2" - checksum: 10/63c6912146efe079fd048ecc02e5c3bf5aaa4cb268ad4e365603d845444dd3048daa45868c2690c5fe2d020ba47273c8a20df684a8c424fb4bd7f359c795c2f5 +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 10/642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + +"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: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10/e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb languageName: node linkType: hard "string-width@npm:^8.1.0": - version: 8.2.1 - resolution: "string-width@npm:8.2.1" + version: 8.1.0 + resolution: "string-width@npm:8.1.0" dependencies: - get-east-asian-width: "npm:^1.5.0" - strip-ansi: "npm:^7.1.2" - checksum: 10/cfadcc454b357d1a2ef88afb85068c7605900c9920362a16df9b4c320cf411983cee51b9832b70772d138674c2851d506f39c7e669c961a1cdd1258207580805 + get-east-asian-width: "npm:^1.3.0" + strip-ansi: "npm:^7.1.0" + checksum: 10/51ee97c4ffee7b94f8a2ee785fac14f81ec9809b9fcec9a4db44e25c717c263af0cc4387c111aef76195c0718dc43766f3678c07fb542294fb0244f7bfbde883 + languageName: node + linkType: hard + +"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: + ansi-regex: "npm:^5.0.1" + checksum: 10/ae3b5436d34fadeb6096367626ce987057713c566e1e7768818797e00ac5d62023d0f198c4e681eae9e20701721980b26a64a8f5b91238869592a9c6800719a2 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": + version: 7.1.2 + resolution: "strip-ansi@npm:7.1.2" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b + 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 -"strip-ansi@npm:^7.1.2": +"supports-color@npm:^7.1.0": version: 7.2.0 - resolution: "strip-ansi@npm: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: - ansi-regex: "npm:^6.2.2" - checksum: 10/96da3bc6d73cfba1218625a3d66cf7d37a69bf0920d8735b28f9eeaafcdb6c1fe8440e1ae9eb1ba0ca355dbe8702da872e105e2e939fa93e7851b3cb5dd7d316 + 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.5.4": - version: 7.5.15 - resolution: "tar@npm:7.5.15" +"tar@npm:^7.4.3": + version: 7.5.14 + resolution: "tar@npm:7.5.14" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/b4cb6acd822159867f81ebda8d765c6941ec8292f1cf2f870d3713f4933c14bf0ed7bf4a92338143c31e8815ca0a1fdd62aa03ddb48a42ae187f7ef696583ffe + checksum: 10/4399a2cedf42cf8fbc24b1d732a0bc709ed5cda6bef5bd8b60ccbff94d213bf33f81afb5b99e849dd51626954c048062631b23f6619255be9199a3ceb5950cad + 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 @@ -1291,19 +5130,29 @@ __metadata: linkType: hard "tinyexec@npm:^1.0.2": - version: 1.1.2 - resolution: "tinyexec@npm:1.1.2" - checksum: 10/2bbe37f9001c6f5723ab39eb8dc1e88f77e830d7cf2e8f34bb75019eb505fcfe3b061b4799c502ff31fa63aa1a9adc649add5ff1e17b7fbd8c16e1afb75d0b9e + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" +"tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" dependencies: fdir: "npm:^6.5.0" picomatch: "npm:^4.0.4" - checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 + checksum: 10/f85e8a217d675c3f78d5f0ad25ea4557e7e023ed13ddc2b014da10bd0312eea53a34cd52356af07ccdff777f1243012547656282a4ca70936f68bf5065fbaa71 languageName: node linkType: hard @@ -1314,6 +5163,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" @@ -1352,7 +5215,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0": +"tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.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 @@ -1360,15 +5223,15 @@ __metadata: linkType: hard "turbo@npm:^2.9.14": - version: 2.9.14 - resolution: "turbo@npm:2.9.14" - dependencies: - "@turbo/darwin-64": "npm:2.9.14" - "@turbo/darwin-arm64": "npm:2.9.14" - "@turbo/linux-64": "npm:2.9.14" - "@turbo/linux-arm64": "npm:2.9.14" - "@turbo/windows-64": "npm:2.9.14" - "@turbo/windows-arm64": "npm:2.9.14" + version: 2.9.16 + resolution: "turbo@npm:2.9.16" + dependencies: + "@turbo/darwin-64": "npm:2.9.16" + "@turbo/darwin-arm64": "npm:2.9.16" + "@turbo/linux-64": "npm:2.9.16" + "@turbo/linux-arm64": "npm:2.9.16" + "@turbo/windows-64": "npm:2.9.16" + "@turbo/windows-arm64": "npm:2.9.16" dependenciesMeta: "@turbo/darwin-64": optional: true @@ -1384,7 +5247,24 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/99ce1f10c79ec840b542cc628d44f6febc18fcff8fed08f7f966a983a71b03c7febcf3e16066667a01f9d8d7faca133ebd541c57555bd509364f669ceb46807f + checksum: 10/a151be0f24bd66802d36b0303eb5ed21bbe05077265831ea457554fb7b859b7cfdd35938e3b3812c5aa86721935032c9f1d03c247b17786a2ea7aecb65b82e1d + 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.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/c089d9d3da2729fd4ac517f9b0e0485914c4b3c26f80dc0cffcb5de1719a17951e92425d55db59515c1a7ddab65808466debb864d0d56dcf43f27007d0709594 languageName: node linkType: hard @@ -1398,6 +5278,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/696e1b017bc2635f4e0c94eb4435357701008e2f272f553d06e35b494b8ddc60aa221145e286c28ace0c89ee32827a28c2040e3a69bdc108b1a5dc8fb40b72e3 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^6.0.3#optional!builtin": version: 6.0.3 resolution: "typescript@patch:typescript@npm%3A6.0.3#optional!builtin::version=6.0.3&hash=5786d5" @@ -1415,13 +5305,61 @@ __metadata: languageName: node linkType: hard -"undici@npm:^6.25.0": +"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" + checksum: 10/db43439f69c2d94cc29f75cbfe9de86df87061d6b0c577ebe9bb3255f49b22c50162a7d7eb413b0458b6510b8ca299ac7cff38c3a29fbd31af9f504bcf7fbc0d + 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" + dependencies: + unique-slug: "npm:^5.0.0" + checksum: 10/6a62094fcac286b9ec39edbd1f8f64ff92383baa430af303dfed1ffda5e47a08a6b316408554abfddd9730c78b6106bef4ca4d02c1231a735ddd56ced77573df + languageName: node + linkType: hard + +"unique-slug@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-slug@npm:5.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10/beafdf3d6f44990e0a5ce560f8f881b4ee811be70b6ba0db25298c31c8cf525ed963572b48cd03be1c1349084f9e339be4241666d7cf1ebdad20598d3c652b27 + 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" @@ -1430,15 +5368,15 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.0.14 - resolution: "vite@npm:8.0.14" + version: 8.0.16 + resolution: "vite@npm:8.0.16" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" postcss: "npm:^8.5.15" - rolldown: "npm:1.0.2" - tinyglobby: "npm:^0.2.16" + rolldown: "npm:1.0.3" + tinyglobby: "npm:^0.2.17" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 "@vitejs/devtools": ^0.1.18 @@ -1482,21 +5420,21 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/3747c9b9dabdfa5b840630c39b2c764afb3c3762816f3148afe7d516edc1889b60b666adeb4e98761c26fb8ed5ba3a9770df5c0450443daf4cdfac110bc6df1c + checksum: 10/a5d91d26f6110672a292a06ca161af9a58279fe9d27106c8c0afb725a942b0b47091c440c3b1e7ebc8e0fe901f64ac6a2ffee3cdae2f899339686dbecd0c0266 languageName: node linkType: hard "vitest@npm:^4.0.15, vitest@npm:^4.1.6": - version: 4.1.7 - resolution: "vitest@npm:4.1.7" - dependencies: - "@vitest/expect": "npm:4.1.7" - "@vitest/mocker": "npm:4.1.7" - "@vitest/pretty-format": "npm:4.1.7" - "@vitest/runner": "npm:4.1.7" - "@vitest/snapshot": "npm:4.1.7" - "@vitest/spy": "npm:4.1.7" - "@vitest/utils": "npm:4.1.7" + version: 4.1.8 + resolution: "vitest@npm:4.1.8" + dependencies: + "@vitest/expect": "npm:4.1.8" + "@vitest/mocker": "npm:4.1.8" + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/runner": "npm:4.1.8" + "@vitest/snapshot": "npm:4.1.8" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -1514,12 +5452,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.7 - "@vitest/browser-preview": 4.1.7 - "@vitest/browser-webdriverio": 4.1.7 - "@vitest/coverage-istanbul": 4.1.7 - "@vitest/coverage-v8": 4.1.7 - "@vitest/ui": 4.1.7 + "@vitest/browser-playwright": 4.1.8 + "@vitest/browser-preview": 4.1.8 + "@vitest/browser-webdriverio": 4.1.8 + "@vitest/coverage-istanbul": 4.1.8 + "@vitest/coverage-v8": 4.1.8 + "@vitest/ui": 4.1.8 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1550,18 +5488,60 @@ __metadata: optional: false bin: vitest: vitest.mjs - checksum: 10/23ce0ce8bf81856c1acf983c6138efda5d01b60cbdc5734abd0948f3b39cde14ea7bf0981a2ec8a6b05fe7f3658b211116997fd658fcd20c2f5740b5465502ca + checksum: 10/b9f1308436717da9558b36e149cac6bab8e3730aa7e90b49f9d7a84ba853e353d8afba7d406dc0abec731fb2a9ea9e92b89aba06b94b1a2802203048b43468af languageName: node linkType: hard -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" +"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" dependencies: - isexe: "npm:^4.0.0" + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10/4782f8a1d6b8fc12c65e968fea49f59752bf6302dc43036c3bf87da718a80710f61a062516e9764c70008b487929a73546125570acea95c5b5dcc8ac3052c70f + languageName: node + linkType: hard + +"which@npm:^5.0.0": + version: 5.0.0 + resolution: "which@npm:5.0.0" + dependencies: + isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 10/dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + checksum: 10/6ec99e89ba32c7e748b8a3144e64bfc74aa63e2b2eacbb61a0060ad0b961eb1a632b08fb1de067ed59b002cec3e21de18299216ebf2325ef0f78e0f121e14e90 languageName: node linkType: hard @@ -1577,6 +5557,68 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10/cebdaeca3a6880da410f75209e68cd05428580de5ad24535f22696d7d9cab134d1f8498599f344c3cf0fb37c1715807a183778d8c648d6cc0cb5ff2bb4236540 + languageName: node + linkType: hard + +"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 + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10/4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd + languageName: node + linkType: hard + "yallist@npm:^5.0.0": version: 5.0.0 resolution: "yallist@npm:5.0.0" @@ -1584,6 +5626,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" @@ -1597,3 +5670,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