diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a98f2c663..14b47c863 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,11 @@ We really appreciate and value contributions to OpenZeppelin Contracts for Compa * [Pull Requests](#pull-requests) * [Opening an Issue](#opening-an-issue) +[Running Tests](#running-tests) + +* [Unit Tests](#unit-tests) +* [Live Tests](#live-tests) + [Styleguides](#styleguides) * [Git Commit Messages](#git-commit-messages) @@ -152,6 +157,76 @@ A maintainer will re-run the status check for you. If we conclude that the failu While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. +## Running Tests + +Run all commands from the repository root. Enable Corepack once (`corepack enable`) so `yarn` resolves to the version pinned in `package.json`. + +### Unit Tests + +Unit tests run against an in-process mock backend (no network, ZK proving skipped): + +```bash +yarn test +``` + +### Live Tests + +Live tests run against a local Midnight network (node, indexer, and proof server) defined in [`local-env.yml`](./local-env.yml). They require [Docker](https://docs.docker.com/get-docker/) and a completed `yarn install`. + +One command runs everything — it compiles, resets the stack, runs a quick harness smoke, then each live-ready category sequentially on a freshly reset node: + +```bash +yarn test:live +``` + +Currently `multisig` is the only live-ready category; the others still assume dry-only semantics and are skipped (listed in the run banner). Each category joins the run — with its own `test:live:` script — as its specs are refactored for the live backend. + +If any files fail, a second round re-runs just those files on a fresh node with one worker, to separate a real failure from an environment flake: + +* Fails round 1, passes round 2 → **FLAKY** (exit 0, reported loudly). +* Fails both rounds → **REAL** (exit non-zero). + +Scope the same mechanism to one category, or to a single file within it. The +first argument names the category; any further argument is a filename substring +(vitest matches it), so pass a spec name to run just that file on the live +backend — the fast loop while iterating on one feature, instead of waiting for +the whole category: + +```bash +yarn test:live multisig # the whole category +yarn test:live multisig ShieldedTreasury # just that one file +yarn test:live multisig Forwarder # any file matching "Forwarder" +``` + +The two-round flake check still applies to a single-file run, so a green result +means the same thing it does for the full suite. + +Stop the network when done: `yarn env:down`. (No manual `env:up` is needed — the runner resets the stack itself.) + +> **Note:** The live tests all run against one shared node, so state left by an earlier run can make a later one fail. Two rules keep them reliable, both enforced by a guard that fails fast, before any wallet build: +> +> 1. **Start from a fresh node.** State left by a previous run makes shielded spends fail with node `Custom error: 103`. The guard aborts if it finds any shielded coin event beyond genesis. The `test:live*` runner resets for you; reset manually with `yarn env:up`. +> 2. **One live run at a time.** A pid-stamped lock (`contracts/logs/.live-run.lock`) makes a second concurrent run abort. + +Environment knobs: + +| Variable | Default | Effect | +| --- | --- | --- | +| `MIDNIGHT_LIVE_WORKERS` | 3 | Parallel spec files (max 3 — one genesis-funded deployer each). | +| `MIDNIGHT_LIVE_ALLOW_DIRTY` | unset | `1` skips the freshness check (run against a dirty node). | +| `MIDNIGHT_LIVE_MAX_COIN_EVENTS` | 0 | Coin events beyond genesis tolerated before "not fresh". | +| `MIDNIGHT_LIVE_MAX_SCAN_BLOCKS` | 3600 | Above this indexer head, the guard asks you to `env:up` rather than scan. | + +`unit-live` runs up to 3 workers in parallel, so their output interleaves. It is tagged per worker: a `▶ live worker N/3 ready` banner when a worker's wallets are funded, a `[wN] ❯ ` line as each spec file starts, and a `[wN] ✓ () [done/total]` line per test — showing the worker, the result, and overall progress through the run. Each worker also writes a detailed log to `logs/live-harness-wN.log`. + +> **Tip:** to save the run to a colored, readable log, force color and pipe to `tee`. Piping (stdout is no longer a TTY) makes vitest print one clean line per result instead of an animated spinner, and `FORCE_COLOR=1` keeps the color. Write it to a `.ansi` file: +> +> ```bash +> FORCE_COLOR=1 yarn test:live multisig 2>&1 | tee logs/live-multisig.ansi +> ``` +> +> The file stores ANSI color codes, so render them rather than reading them raw. In VS Code, an ANSI extension such as [`iliazeus.vscode-ansi`](https://marketplace.visualstudio.com/items?itemName=iliazeus.vscode-ansi) renders a `.ansi` file via **"ANSI Text: Open Preview"**. In a terminal, use `less -R logs/live-multisig.ansi`. On Linux, prefix `systemd-inhibit --why="live tests"` for a long run. + ## Styleguides ### TypeScript Styleguide diff --git a/Makefile b/Makefile index 09b8cf28a..a9007936c 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,10 @@ SERVICES := proof-server indexer node ## Start local environment and stream logs to logs/ env-up: env-down - docker compose -f $(COMPOSE_FILE) up -d --wait + docker compose -f $(COMPOSE_FILE) up -d + @# proof-server has no healthcheck, so a blanket `--wait` is not portable + @# across Docker Compose versions; wait only on the services that expose one. + docker compose -f $(COMPOSE_FILE) up -d --wait node indexer @mkdir -p $(LOGS_DIR) @for svc in $(SERVICES); do \ docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ diff --git a/contracts/package.json b/contracts/package.json index 72d20a643..95e0d519e 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -21,11 +21,9 @@ "homepage": "https://docs.openzeppelin.com/contracts-compact/", "type": "module", "imports": { - "#test-utils/address.js": "./test-utils/address.js", - "#test-utils/zswap.js": "./test-utils/zswap.js" + "#test-utils/*": "./test-utils/*" }, "scripts": { - "compact": "compact-compiler --exclude '*/archive/*'", "compact:access": "compact-compiler --dir access", "compact:archive": "compact-compiler --dir archive", "compact:crypto": "compact-compiler --dir crypto", @@ -33,12 +31,13 @@ "compact:security": "compact-compiler --dir security", "compact:token": "compact-compiler --dir token", "compact:utils": "compact-compiler --dir utils", + "compact:integration": "SKIP_ZK=true compact-compiler --src test/integration/_mocks", "build": "compact-builder --hierarchical --out dist --clean-dist --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete", - "test": "SKIP_ZK=true yarn run compact && vitest run", - "test:coverage": "SKIP_ZK=true yarn run compact && vitest run --coverage", - "test:live": "yarn run compact && MIDNIGHT_BACKEND=live vitest run --config vitest.live.config.ts", - "compact:integration": "SKIP_ZK=true compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && SKIP_ZK=true compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens", - "test:integration": "yarn run compact:integration && vitest run --config vitest.integration.config.ts", + "test": "vitest run --project unit", + "test:coverage": "vitest run --project unit --coverage", + "test:integration": "vitest run --project integration", + "test:harness": "vitest run --project harness", + "test:harness:live": "MIDNIGHT_BACKEND=live vitest run --project harness-live", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, @@ -49,6 +48,17 @@ "@openzeppelin/compact-cli": "^0.0.2" }, "devDependencies": { + "@midnight-ntwrk/compact-js": "2.5.1", + "@midnight-ntwrk/compact-runtime": "0.16.0", + "@midnight-ntwrk/ledger-v8": "8.1.0", + "@midnight-ntwrk/midnight-js-contracts": "4.1.1", + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.1.1", + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.1", + "@midnight-ntwrk/midnight-js-network-id": "4.1.1", + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.1.1", + "@midnight-ntwrk/midnight-js-protocol": "4.1.1", + "@midnight-ntwrk/midnight-js-types": "4.1.1", + "@midnight-ntwrk/testkit-js": "4.1.1", "@openzeppelin/compact-simulator": "^0.2.0", "@tsconfig/node24": "^24.0.4", "@types/node": "25.9.3", diff --git a/contracts/src/access/test/AccessControl.test.ts b/contracts/src/access/test/AccessControl.test.ts index 8060d3365..70c334cd2 100644 --- a/contracts/src/access/test/AccessControl.test.ts +++ b/contracts/src/access/test/AccessControl.test.ts @@ -5,7 +5,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { AccessControlSimulator } from './simulators/AccessControlSimulator.js'; // Helpers diff --git a/contracts/src/access/test/Ownable.test.ts b/contracts/src/access/test/Ownable.test.ts index 72219b7d4..6c44140a9 100644 --- a/contracts/src/access/test/Ownable.test.ts +++ b/contracts/src/access/test/Ownable.test.ts @@ -4,7 +4,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { OwnableSimulator } from './simulators/OwnableSimulator.js'; // Helpers diff --git a/contracts/src/access/test/ZOwnablePK.test.ts b/contracts/src/access/test/ZOwnablePK.test.ts index 6269cdacf..7714c9408 100644 --- a/contracts/src/access/test/ZOwnablePK.test.ts +++ b/contracts/src/access/test/ZOwnablePK.test.ts @@ -5,7 +5,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.js'; import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; import { ZOwnablePKPrivateState } from './witnesses/ZOwnablePKWitnesses.js'; @@ -133,7 +133,9 @@ describe('ZOwnablePK', () => { }); it('should allow pure computeOwnerId', async () => { - const eitherOwner = utils.createEitherTestUser('OWNER'); + const eitherOwner = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('OWNER'), + ); const ownerId = await ownable._computeOwnerId( eitherOwner, @@ -418,17 +420,23 @@ describe('ZOwnablePK', () => { const testCases = [ ...Array.from({ length: 10 }, (_, i) => ({ label: `User${i}`, - eitherOwner: utils.createEitherTestUser(`User${i}`), + eitherOwner: utils.eitherUserFromCoinPublicKey( + utils.toHexPadded(`User${i}`), + ), nonce: new Uint8Array(32).fill(i), })), { label: 'All-zero nonce', - eitherOwner: utils.createEitherTestUser('ZeroUser'), + eitherOwner: utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('ZeroUser'), + ), nonce: new Uint8Array(32).fill(0), }, { label: 'Max nonce', - eitherOwner: utils.createEitherTestUser('MaxUser'), + eitherOwner: utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('MaxUser'), + ), nonce: new Uint8Array(32).fill(255), }, ]; diff --git a/contracts/src/multisig/presets/ShieldedMultiSig.compact b/contracts/src/multisig/presets/ShieldedMultiSig.compact index f335d240c..4faacf7ff 100644 --- a/contracts/src/multisig/presets/ShieldedMultiSig.compact +++ b/contracts/src/multisig/presets/ShieldedMultiSig.compact @@ -187,17 +187,12 @@ export circuit getProposal(id: Uint<64>): Proposal_Proposal { return Proposal_getProposal(id); } -export circuit getProposalRecipient(id: Uint<64>): Proposal_Recipient { - return Proposal_getProposalRecipient(id); -} - -export circuit getProposalAmount(id: Uint<64>): Uint<128> { - return Proposal_getProposalAmount(id); -} - -export circuit getProposalColor(id: Uint<64>): Bytes<32> { - return Proposal_getProposalColor(id); -} +// NOTE: the per-field proposal getters (getProposalRecipient / getProposalAmount +// / getProposalColor) and getReceivedMinusSent were dropped to bring the deploy +// transaction under the block-weight limit (the full 19-circuit deploy is +// rejected with "Transaction would exhaust the block limits"). They are all +// redundant: read the proposal fields via `getProposal(id).to / .amount / .color`, +// and the net balance via `getReceivedTotal(color) - getSentTotal(color)`. export circuit getProposalStatus(id: Uint<64>): Proposal_ProposalStatus { return Proposal_getProposalStatus(id); @@ -217,10 +212,6 @@ export circuit getSentTotal(color: Bytes<32>): Uint<128> { return Treasury_getSentTotal(color); } -export circuit getReceivedMinusSent(color: Bytes<32>): Uint<128> { - return Treasury_getReceivedMinusSent(color); -} - // ISignerManager export circuit getSignerCount(): Uint<8> { diff --git a/contracts/src/multisig/test/EmptyWitnesses.ts b/contracts/src/multisig/test/EmptyWitnesses.ts index f22171cf6..267c017ab 100644 --- a/contracts/src/multisig/test/EmptyWitnesses.ts +++ b/contracts/src/multisig/test/EmptyWitnesses.ts @@ -2,9 +2,9 @@ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/test/EmptyWitnesses.ts) /** - * Shared empty private state and witnesses for forwarder contracts, none - * of which declare any witnesses. Imported by their simulators in place - * of a per-contract witness module. + * Shared empty private state and witnesses for the multisig test simulators. + * None of the multisig contracts declare witnesses, so every simulator imports + * this in place of a per-contract witness module. */ export type EmptyPrivateState = Record; export const EmptyPrivateState: EmptyPrivateState = {}; diff --git a/contracts/src/multisig/test/Forwarder.test.ts b/contracts/src/multisig/test/Forwarder.test.ts index e9fa63eb2..5679a277b 100644 --- a/contracts/src/multisig/test/Forwarder.test.ts +++ b/contracts/src/multisig/test/Forwarder.test.ts @@ -1,5 +1,11 @@ +import { isLiveBackend } from '@openzeppelin/compact-simulator'; import { describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js'; import { MockForwarderShieldedSimulator } from './simulators/MockForwarderShieldedSimulator.js'; import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnshieldedSimulator.js'; @@ -11,19 +17,31 @@ import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnsh // future CMA circuit upgrade can add contract support without a state // migration; `initialize` stores the supported arm (shielded → `left`, // unshielded → `right`), which is what `getParent` reads back. -const SHIELDED_PARENT = utils.createEitherTestUser('PARENT').left; +// +// Live: the shielded parent is the deployer's own key (the forward sends the +// coin to it, so its encryption key must resolve on-chain). The unshielded +// parent stays synthetic — an unshielded recipient is a public address, no +// encryption key needed. +const SHIELDED_PARENT = shieldedTestParentKey(); const SHIELDED_ZERO = utils.ZERO_KEY.left; const UNSHIELDED_PARENT = utils.createEitherTestUserAddress('PARENT').right; const UNSHIELDED_ZERO = utils.ZERO_USER_ADDRESS.right; -const COLOR = new Uint8Array(32).fill(1); + +// Shielded color: genesis-funded (`0x00…01`) so a live forward has funds to +// draw; `fill(1)` would be unfunded on live. Unshielded color: on live the +// deployer wallet only holds the native unshielded token (`0x00…00`), so the +// forward draws that; on dry any color mints freely. +const SHIELDED_COLOR = + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; +const UNSHIELDED_COLOR = isLiveBackend() + ? new Uint8Array(32) + : new Uint8Array(32).fill(1); const AMOUNT = 1000n; +// Live gets a fresh random nonce per run (the node persists nullifiers); dry +// uses zero for reproducibility. function makeCoin(color: Uint8Array, value: bigint, nonce?: Uint8Array) { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - }; + return encodeShieldedCoinInfo(color, value, nonce); } describe('ForwarderShielded module', () => { @@ -55,9 +73,9 @@ describe('ForwarderShielded module', () => { SHIELDED_PARENT, false, ); - await expect(mock.deposit(makeCoin(COLOR, AMOUNT))).rejects.toThrow( - 'ForwarderShielded: contract not initialized', - ); + await expect( + mock.deposit(makeCoin(SHIELDED_COLOR, AMOUNT)), + ).rejects.toThrow('ForwarderShielded: contract not initialized'); }); }); @@ -67,7 +85,7 @@ describe('ForwarderShielded module', () => { SHIELDED_PARENT, true, ); - await mock.deposit(makeCoin(COLOR, AMOUNT)); + await mock.deposit(makeCoin(SHIELDED_COLOR, AMOUNT)); }); }); }); @@ -101,7 +119,7 @@ describe('ForwarderUnshielded module', () => { UNSHIELDED_PARENT, false, ); - await expect(mock.deposit(COLOR, AMOUNT)).rejects.toThrow( + await expect(mock.deposit(UNSHIELDED_COLOR, AMOUNT)).rejects.toThrow( 'ForwarderUnshielded: contract not initialized', ); }); @@ -113,7 +131,7 @@ describe('ForwarderUnshielded module', () => { UNSHIELDED_PARENT, true, ); - await mock.deposit(COLOR, AMOUNT); + await mock.deposit(UNSHIELDED_COLOR, AMOUNT); }); }); }); diff --git a/contracts/src/multisig/test/ForwarderPrivate.test.ts b/contracts/src/multisig/test/ForwarderPrivate.test.ts index 23b2cacb5..46c33e5dc 100644 --- a/contracts/src/multisig/test/ForwarderPrivate.test.ts +++ b/contracts/src/multisig/test/ForwarderPrivate.test.ts @@ -1,12 +1,23 @@ +import type { EncodedQualifiedShieldedCoinInfo } from '@midnight-ntwrk/compact-runtime'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; import fc from 'fast-check'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js'; import { bytesToHex, isNonceSpent, zswapDelta, zswapSnapshot, -} from '#test-utils/zswap.js'; +} from '#test-utils/fixtures/zswap.js'; +import { + contractOwner, + getQualifiedShieldedCoinInfo, +} from '#test-utils/harness/NativeShieldedTokenTracker.js'; import { MockForwarderPrivateSimulator } from './simulators/MockForwarderPrivateSimulator.js'; // The drain parent is a `ZswapCoinPublicKey` (coin public key only). A contract @@ -14,26 +25,40 @@ import { MockForwarderPrivateSimulator } from './simulators/MockForwarderPrivate // publishes the contract address in cleartext, which would defeat the // private-parent guarantee (confirmed on preprod). The commitment is over the // parent key's raw 32 bytes (`_calculateParentCommitment(parent.bytes, opSecret)`). -const PARENT_BYTES = utils.createEitherTestUser('PARENT').left.bytes; -const WRONG_BYTES = utils.createEitherTestUser('WRONG').left.bytes; +// +// Live: the parent is the deployer's own coin public key — the drain sends the +// note to it, so its encryption key must resolve on-chain (a fabricated key has +// none). Published by the live setup, so safe to read at module scope. `WRONG` +// stays a synthetic key: it only exercises the commitment gate, which rejects it +// before any send. +const PARENT_BYTES = shieldedTestParentKey().bytes; +const WRONG_BYTES = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('WRONG'), +).left.bytes; const OP_SECRET = new Uint8Array(32).fill(0xaa); const WRONG_OP_SECRET = new Uint8Array(32).fill(0xbb); const ZERO = new Uint8Array(32); -const COLOR = new Uint8Array(32).fill(1); +// A shielded token type the deployer wallet holds on live (genesis-minted); +// `fill(1)` would be unfunded on live. On dry the color is arbitrary. +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; const MAX_U64 = (1n << 64n) - 1n; +// A non-zero deploy address so a change output routed to self carries a +// recognizable address rather than the zero `dummyContractAddress()` default +// (asserted in the dry Zswap-I/O block; ignored on live, which deploys for real). +const FORWARDER_ADDRESS = '3e'.repeat(32); + /** A coin-public-key parent: `ZswapCoinPublicKey` is `{ bytes }`. */ function key(bytes: Uint8Array): { bytes: Uint8Array } { return { bytes }; } +// Backend-aware coin builder: live gets a fresh random nonce per run (the node +// persists nullifiers, so a fixed nonce would replay a spent coin); dry uses +// `nonce` (else zero) for reproducibility. function makeCoin(color: Uint8Array, value: bigint, nonce?: Uint8Array) { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - }; + return encodeShieldedCoinInfo(color, value, nonce); } function makeQualifiedCoin( @@ -57,17 +82,33 @@ function commitment(parent: Uint8Array, opSecret: Uint8Array): Uint8Array { ); } -/** Initialized forwarder committed to `committedBytes`, with one coin deposited. */ +/** + * An initialized forwarder committed to `committedBytes`, with one coin deposited + * and qualified for spending. On live the coin's real `mt_index` is recovered + * from the coin tracker (the contract keeps no record of it); on dry a + * placeholder the in-memory runtime ignores. `contractAddress` sets a + * recognizable self-address for the dry change-output assertions. + */ async function freshMock( committedBytes: Uint8Array, opSecret: Uint8Array = OP_SECRET, -): Promise { + contractAddress?: string, +): Promise<{ + mock: MockForwarderPrivateSimulator; + coin: EncodedQualifiedShieldedCoinInfo; +}> { const mock = await MockForwarderPrivateSimulator.create( commitment(committedBytes, opSecret), true, + contractAddress ? { contractAddress } : {}, ); - await mock.deposit(makeCoin(COLOR, AMOUNT)); - return mock; + const deposited = makeCoin(COLOR, AMOUNT); + await mock.deposit(deposited); + const coin = await getQualifiedShieldedCoinInfo( + contractOwner(mock), + deposited, + ); + return { mock, coin }; } describe('ForwarderPrivate module', () => { @@ -110,6 +151,8 @@ describe('ForwarderPrivate module', () => { }); it('should fail drain when not initialized', async () => { + // The init guard rejects before any coin is spent, so the placeholder + // `mt_index` is never used here. await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), @@ -150,14 +193,15 @@ describe('ForwarderPrivate module', () => { // (INV-6/27), value sufficiency (INV-7), and the change-coin pattern (INV-22). describe('drain', () => { let mock: MockForwarderPrivateSimulator; + let coin: EncodedQualifiedShieldedCoinInfo; beforeEach(async () => { - mock = await freshMock(PARENT_BYTES); + ({ mock, coin } = await freshMock(PARENT_BYTES)); }); it('should succeed drain with correct (parent, opSecret)', async () => { const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), + coin, key(PARENT_BYTES), OP_SECRET, AMOUNT, @@ -167,51 +211,31 @@ describe('ForwarderPrivate module', () => { it('should fail drain with wrong parent key', async () => { await expect( - mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(WRONG_BYTES), - OP_SECRET, - AMOUNT, - ), + mock.drain(coin, key(WRONG_BYTES), OP_SECRET, AMOUNT), ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); it('should fail drain with wrong opSecret', async () => { await expect( - mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - WRONG_OP_SECRET, - AMOUNT, - ), + mock.drain(coin, key(PARENT_BYTES), WRONG_OP_SECRET, AMOUNT), ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); it('should fail drain with both wrong', async () => { await expect( - mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(WRONG_BYTES), - WRONG_OP_SECRET, - AMOUNT, - ), + mock.drain(coin, key(WRONG_BYTES), WRONG_OP_SECRET, AMOUNT), ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); it('should fail drain with value > coin.value', async () => { await expect( - mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - AMOUNT + 1n, - ), + mock.drain(coin, key(PARENT_BYTES), OP_SECRET, AMOUNT + 1n), ).rejects.toThrow(); }); it('should produce no change when drain value equals coin value', async () => { const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), + coin, key(PARENT_BYTES), OP_SECRET, AMOUNT, @@ -220,24 +244,14 @@ describe('ForwarderPrivate module', () => { }); it('should produce a change coin when drain value is less than coin value', async () => { - const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - 400n, - ); + const result = await mock.drain(coin, key(PARENT_BYTES), OP_SECRET, 400n); expect(result.change.is_some).toBe(true); expect(result.change.value.value).toEqual(AMOUNT - 400n); expect(result.change.value.color).toEqual(COLOR); }); it('should produce a sent coin of exactly value on partial drain', async () => { - const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - 400n, - ); + const result = await mock.drain(coin, key(PARENT_BYTES), OP_SECRET, 400n); expect(result.sent.value).toEqual(400n); expect(result.sent.color).toEqual(COLOR); }); @@ -246,14 +260,9 @@ describe('ForwarderPrivate module', () => { // INV-34: a zero parent key is rejected before the commitment gate. describe('drain — rejects a zero parent', () => { it('should reject a zero parent key', async () => { - const mock = await freshMock(PARENT_BYTES); + const { mock, coin } = await freshMock(PARENT_BYTES); await expect( - mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(ZERO), - OP_SECRET, - AMOUNT, - ), + mock.drain(coin, key(ZERO), OP_SECRET, AMOUNT), ).rejects.toThrow('ForwarderPrivate: zero parent'); }); }); @@ -271,16 +280,10 @@ describe('ForwarderPrivate module', () => { describe('drain — residual public surface (INV-12 / INV-17 / INV-25)', () => { it('should not mutate the parent commitment on a successful drain', async () => { const c = commitment(PARENT_BYTES, OP_SECRET); - const mock = await MockForwarderPrivateSimulator.create(c, true); - await mock.deposit(makeCoin(COLOR, AMOUNT)); + const { mock, coin } = await freshMock(PARENT_BYTES); const before = await mock.getParentCommitment(); - await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - AMOUNT, - ); + await mock.drain(coin, key(PARENT_BYTES), OP_SECRET, AMOUNT); const after = await mock.getParentCommitment(); expect(after).toStrictEqual(before); @@ -295,166 +298,264 @@ describe('ForwarderPrivate module', () => { // coin a double spend the node rejects on the next drain. The dry simulator // does not enforce nullifiers, so these tests read the recorded Zswap I/O: the // change coin's nonce must not appear among the spent inputs. - describe('drain — change coin is spendable (no double spend)', () => { - // A non-zero deploy address so the change output (routed to self for future - // drains) carries a recognizable address rather than the zero - // `dummyContractAddress()` default. - const FORWARDER_ADDRESS = '3e'.repeat(32); - let mock: MockForwarderPrivateSimulator; - - beforeEach(async () => { - mock = await MockForwarderPrivateSimulator.create( - commitment(PARENT_BYTES, OP_SECRET), - true, - { contractAddress: FORWARDER_ADDRESS }, - ); - await mock.deposit(makeCoin(COLOR, AMOUNT)); - }); - - it('should send the note to the parent and route the change back to itself on a partial drain', async () => { - const snap = zswapSnapshot(mock); - const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - 400n, - ); - const { inputs, outputs } = zswapDelta(mock, snap); - - // One coin consumed: the drained coin (nonce 0, full value). - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(AMOUNT); - expect(inputs[0].color).toStrictEqual(COLOR); - expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); - - // Two coins produced: the note to the parent key (`left` arm) and the - // change back to this contract (`right`/self arm). - expect(outputs).toHaveLength(2); - const toParent = outputs.filter((o) => o.recipient.is_left); - const toSelf = outputs.filter((o) => !o.recipient.is_left); - expect(toParent).toHaveLength(1); - expect(toSelf).toHaveLength(1); - - // Note: 400 of COLOR to the parent key; equals result.sent. - expect(toParent[0].coinInfo.value).toBe(400n); - expect(toParent[0].coinInfo).toStrictEqual(result.sent); - expect(toParent[0].recipient.left.bytes).toStrictEqual(PARENT_BYTES); - - // Change: the remainder back to THIS contract's address (for future - // drains), identical to the returned change coin, and NOT spent in this - // same tx. - expect(result.change.is_some).toBe(true); - expect(toSelf[0].coinInfo.value).toBe(AMOUNT - 400n); - expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); - expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( - FORWARDER_ADDRESS, - ); - expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); - }); - - it('should spend the coin and produce only the note when draining in full', async () => { - const snap = zswapSnapshot(mock); - const result = await mock.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - AMOUNT, - ); - const { inputs, outputs } = zswapDelta(mock, snap); + // + // Dry-only: `zswapSnapshot`/`zswapDelta` read the dry sim's Zswap local state, + // which does not exist on the live backend. The live counterpart follows. + describe.skipIf(isLiveBackend())( + 'drain — change coin is spendable, via Zswap I/O (dry only, no double spend)', + () => { + it('should send the note to the parent and route the change back to itself on a partial drain', async () => { + const { mock, coin } = await freshMock( + PARENT_BYTES, + OP_SECRET, + FORWARDER_ADDRESS, + ); + const snap = zswapSnapshot(mock); + const result = await mock.drain( + coin, + key(PARENT_BYTES), + OP_SECRET, + 400n, + ); + const { inputs, outputs } = zswapDelta(mock, snap); + + // One coin consumed: the drained coin (nonce 0, full value). + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(AMOUNT); + expect(inputs[0].color).toStrictEqual(COLOR); + expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); + + // Two coins produced: the note to the parent key (`left` arm) and the + // change back to this contract (`right`/self arm). + expect(outputs).toHaveLength(2); + const toParent = outputs.filter((o) => o.recipient.is_left); + const toSelf = outputs.filter((o) => !o.recipient.is_left); + expect(toParent).toHaveLength(1); + expect(toSelf).toHaveLength(1); + + // Note: 400 of COLOR to the parent key; equals result.sent. + expect(toParent[0].coinInfo.value).toBe(400n); + expect(toParent[0].coinInfo).toStrictEqual(result.sent); + expect(toParent[0].recipient.left.bytes).toStrictEqual(PARENT_BYTES); + + // Change: the remainder back to THIS contract's address (for future + // drains), identical to the returned change coin, and NOT spent in this + // same tx. + expect(result.change.is_some).toBe(true); + expect(toSelf[0].coinInfo.value).toBe(AMOUNT - 400n); + expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); + expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( + FORWARDER_ADDRESS, + ); + expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); + }); + + it('should spend the coin and produce only the note when draining in full', async () => { + const { mock, coin } = await freshMock( + PARENT_BYTES, + OP_SECRET, + FORWARDER_ADDRESS, + ); + const snap = zswapSnapshot(mock); + const result = await mock.drain( + coin, + key(PARENT_BYTES), + OP_SECRET, + AMOUNT, + ); + const { inputs, outputs } = zswapDelta(mock, snap); + + // No change: one input (the drained coin), one output (the note to parent). + expect(result.change.is_some).toBe(false); + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(AMOUNT); + expect(outputs).toHaveLength(1); + expect(outputs[0].recipient.is_left).toBe(true); + expect(outputs[0].recipient.left.bytes).toStrictEqual(PARENT_BYTES); + expect(outputs[0].coinInfo.value).toBe(AMOUNT); + expect(outputs[0].coinInfo).toStrictEqual(result.sent); + }); + }, + ); - // No change: one input (the drained coin), one output (the note to parent). - expect(result.change.is_some).toBe(false); - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(AMOUNT); - expect(outputs).toHaveLength(1); - expect(outputs[0].recipient.is_left).toBe(true); - expect(outputs[0].recipient.left.bytes).toStrictEqual(PARENT_BYTES); - expect(outputs[0].coinInfo.value).toBe(AMOUNT); - expect(outputs[0].coinInfo).toStrictEqual(result.sent); - }); - }); + // Live counterpart of the block above: the SAME two cases, asserted on what a + // node exposes. Zswap I/O is not readable on live, so the no-double-spend + // proof is the node's own nullifier enforcement — the change coin routed back + // to the contract is spent in a follow-up drain, which the node would reject + // (Custom error 103) if the first drain had already nullified it, so the + // second drain SUCCEEDING is the proof. + describe.runIf(isLiveBackend())( + 'drain — change coin is spendable on live (no double spend)', + () => { + it('should send the note to the parent and route the change back to itself on a partial drain', async () => { + const { mock, coin } = await freshMock(PARENT_BYTES); + const result = await mock.drain( + coin, + key(PARENT_BYTES), + OP_SECRET, + 400n, + ); + + // Note: 400 of COLOR to the parent. + expect(result.sent.value).toBe(400n); + expect(result.sent.color).toStrictEqual(COLOR); + + // Change: the remainder handed back as a live, spendable coin. + expect(result.change.is_some).toBe(true); + expect(result.change.value.value).toBe(AMOUNT - 400n); + + // Spend that retained change in a follow-up drain: recover its index, + // then drain it. A double spend would be rejected, so this succeeding + // proves the change stayed spendable. + const change = await getQualifiedShieldedCoinInfo( + contractOwner(mock), + result.change.value, + ); + const second = await mock.drain( + change, + key(PARENT_BYTES), + OP_SECRET, + AMOUNT - 400n, + ); + expect(second.sent.value).toBe(AMOUNT - 400n); + expect(second.change.is_some).toBe(false); + }); + + it('should spend the coin and produce only the note when draining in full', async () => { + const { mock, coin } = await freshMock(PARENT_BYTES); + const result = await mock.drain( + coin, + key(PARENT_BYTES), + OP_SECRET, + AMOUNT, + ); + expect(result.sent.value).toBe(AMOUNT); + expect(result.change.is_some).toBe(false); + }); + }, + ); // Tests that `_drain` (inner call in the mock `drainAndRouteChange`) returns a // live, unspent change coin, so an implementing contract can spend it onward to // a different recipient in the same tx (the only reason to re-spend change, // since `sendShielded` already routes it to self). This would be impossible if // `_drain` handed back a coin it had already spent. - describe('drain — implementing contract routes the change onward', () => { - const CHANGE_DEST = utils.createEitherTestUser('CHANGE_DEST'); - let mock: MockForwarderPrivateSimulator; - - beforeEach(async () => { - mock = await freshMock(PARENT_BYTES); - }); - - it('should let the caller send the change to a different recipient using the drain result', async () => { - const snap = zswapSnapshot(mock); - const routed = await mock.drainAndRouteChange( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - 400n, - CHANGE_DEST, - ); - const { inputs, outputs } = zswapDelta(mock, snap); - - // The onward send delivered the whole change to `changeRecipient`, so - // nothing is left over. - expect(routed.change.is_some).toBe(false); - expect(routed.sent.value).toBe(AMOUNT - 400n); - - // Two coins are consumed: the drained coin, then the change coin (spent - // to route it onward — spending it here is correct, unlike the keep-change - // path). - expect(inputs).toHaveLength(2); - - // The note still went to the parent for the drained `value`... - const toParent = outputs.filter( - (o) => - o.recipient.is_left && - bytesToHex(o.recipient.left.bytes) === bytesToHex(PARENT_BYTES), - ); - expect(toParent).toHaveLength(1); - expect(toParent[0].coinInfo.value).toBe(400n); - - // ...and the change was routed onward to `changeRecipient`, matching the - // returned coin. - const toChangeDest = outputs.filter( - (o) => - o.recipient.is_left && - bytesToHex(o.recipient.left.bytes) === - bytesToHex(CHANGE_DEST.left.bytes), + // + // Dry-only: reads the recorded Zswap I/O; the live counterpart below asserts + // the functional outcome (recipient arms are indistinguishable on live — both + // resolve to the deployer key). + describe.skipIf(isLiveBackend())( + 'drain — implementing contract routes the change onward (dry only)', + () => { + const CHANGE_DEST = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('CHANGE_DEST'), ); - expect(toChangeDest).toHaveLength(1); - expect(toChangeDest[0].coinInfo.value).toBe(AMOUNT - 400n); - expect(toChangeDest[0].coinInfo).toStrictEqual(routed.sent); - }); - }); - describe('property: change arithmetic', () => { - it('should preserve change.value == coin.value - drain.value on partial drain', async () => { - await fc.assert( - fc.asyncProperty( - fc.bigInt({ min: 2n, max: MAX_U64 - 1n }), - fc.bigInt({ min: 1n, max: MAX_U64 - 1n }), - async (coinVal, drainVal) => { - fc.pre(drainVal < coinVal); - const mock = await MockForwarderPrivateSimulator.create( - commitment(PARENT_BYTES, OP_SECRET), - true, - ); - await mock.deposit(makeCoin(COLOR, coinVal)); - const result = await mock.drain( - makeQualifiedCoin(COLOR, coinVal, 0n), - key(PARENT_BYTES), - OP_SECRET, - drainVal, - ); - expect(result.change.value.value).toEqual(coinVal - drainVal); + it('should let the caller send the change to a different recipient using the drain result', async () => { + const { mock, coin } = await freshMock(PARENT_BYTES); + const snap = zswapSnapshot(mock); + const routed = await mock.drainAndRouteChange( + coin, + key(PARENT_BYTES), + OP_SECRET, + 400n, + CHANGE_DEST, + ); + const { inputs, outputs } = zswapDelta(mock, snap); + + // The onward send delivered the whole change to `changeRecipient`, so + // nothing is left over. + expect(routed.change.is_some).toBe(false); + expect(routed.sent.value).toBe(AMOUNT - 400n); + + // Two coins are consumed: the drained coin, then the change coin (spent + // to route it onward — spending it here is correct, unlike the keep-change + // path). + expect(inputs).toHaveLength(2); + + // The note still went to the parent for the drained `value`... + const toParent = outputs.filter( + (o) => + o.recipient.is_left && + bytesToHex(o.recipient.left.bytes) === bytesToHex(PARENT_BYTES), + ); + expect(toParent).toHaveLength(1); + expect(toParent[0].coinInfo.value).toBe(400n); + + // ...and the change was routed onward to `changeRecipient`, matching the + // returned coin. + const toChangeDest = outputs.filter( + (o) => + o.recipient.is_left && + bytesToHex(o.recipient.left.bytes) === + bytesToHex(CHANGE_DEST.left.bytes), + ); + expect(toChangeDest).toHaveLength(1); + expect(toChangeDest[0].coinInfo.value).toBe(AMOUNT - 400n); + expect(toChangeDest[0].coinInfo).toStrictEqual(routed.sent); + }); + }, + ); + + // Live counterpart: every deliverable target must be a node-resolvable key (so + // the parent and the change recipient are both the deployer here), so this + // asserts only the functional outcome — `_drain` returned a live change coin + // that was routed fully onward, with nothing retained. + describe.runIf(isLiveBackend())( + 'drain — implementing contract routes the change onward on live', + () => { + it('should let the caller send the change to a different recipient using the drain result', async () => { + const { mock, coin } = await freshMock(PARENT_BYTES); + const routed = await mock.drainAndRouteChange( + coin, + key(PARENT_BYTES), + OP_SECRET, + 400n, + // Reuse the parent (deployer) key so the onward send is deliverable. + { + is_left: true, + left: key(PARENT_BYTES), + right: utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('CHANGE_DEST'), + ).right, }, - ), - { numRuns: 25 }, - ); - }); - }); + ); + expect(routed.change.is_some).toBe(false); + expect(routed.sent.value).toBe(AMOUNT - 400n); + }); + }, + ); + + // Dry-only: fast-check fuzzes coin/drain values up to `MAX_U64`, which exceeds + // the deployer wallet's genesis balance and would spawn a deploy per run on a + // real node. The concrete partial/full drains above cover the live path. + describe.skipIf(isLiveBackend())( + 'property: change arithmetic (dry only)', + () => { + it('should preserve change.value == coin.value - drain.value on partial drain', async () => { + await fc.assert( + fc.asyncProperty( + fc.bigInt({ min: 2n, max: MAX_U64 - 1n }), + fc.bigInt({ min: 1n, max: MAX_U64 - 1n }), + async (coinVal, drainVal) => { + fc.pre(drainVal < coinVal); + const mock = await MockForwarderPrivateSimulator.create( + commitment(PARENT_BYTES, OP_SECRET), + true, + ); + await mock.deposit(makeCoin(COLOR, coinVal)); + const result = await mock.drain( + makeQualifiedCoin(COLOR, coinVal, 0n), + key(PARENT_BYTES), + OP_SECRET, + drainVal, + ); + expect(result.change.value.value).toEqual(coinVal - drainVal); + }, + ), + { numRuns: 25 }, + ); + }); + }, + ); }); diff --git a/contracts/src/multisig/test/ProposalManager.test.ts b/contracts/src/multisig/test/ProposalManager.test.ts index b93c0d48a..2b44a4ad0 100644 --- a/contracts/src/multisig/test/ProposalManager.test.ts +++ b/contracts/src/multisig/test/ProposalManager.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import * as utils from '#test-utils/fixtures/address.js'; import { ProposalManagerSimulator } from './simulators/ProposalManagerSimulator.js'; // Enum values matching ProposalStatus and RecipientKind @@ -16,12 +16,16 @@ const Z_CONTRACT_RECIPIENT = utils.encodeToAddress('CONTRACT_RECIPIENT'); let contract: ProposalManagerSimulator; -describe('ProposalManager', () => { - beforeEach(async () => { - contract = await ProposalManagerSimulator.create(); - }); +// A fresh ProposalManager. Mutating groups build one per test (`beforeEach`); +// read-only groups build one per group (`beforeAll`) to save a live deploy tx. +const fresh = () => ProposalManagerSimulator.create(); +describe('ProposalManager', () => { describe('recipient helpers (pure)', () => { + beforeAll(async () => { + contract = await fresh(); + }); + it('should create shielded user recipient', () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); expect(recipient.kind).toEqual(RecipientKind.ShieldedUser); @@ -89,6 +93,10 @@ describe('ProposalManager', () => { }); describe('_createProposal', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should create a proposal and return id', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); @@ -135,6 +143,10 @@ describe('ProposalManager', () => { }); describe('assertProposalExists', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should pass for existing proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); @@ -149,6 +161,10 @@ describe('ProposalManager', () => { }); describe('assertProposalActive', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should pass for active proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); @@ -181,6 +197,10 @@ describe('ProposalManager', () => { }); describe('_cancelProposal', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should cancel an active proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); @@ -230,6 +250,10 @@ describe('ProposalManager', () => { }); describe('_markExecuted', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should mark an active proposal as executed', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); @@ -270,7 +294,9 @@ describe('ProposalManager', () => { describe('view circuits', () => { let proposalId: bigint; - beforeEach(async () => { + // All read-only, so deploy once and seed a single proposal for the group. + beforeAll(async () => { + contract = await fresh(); const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); proposalId = await contract._createProposal(recipient, COLOR, AMOUNT); }); @@ -324,6 +350,10 @@ describe('ProposalManager', () => { }); describe('lifecycle transitions', () => { + beforeEach(async () => { + contract = await fresh(); + }); + it('should handle create -> cancel flow', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); const id = await contract._createProposal(recipient, COLOR, AMOUNT); diff --git a/contracts/src/multisig/test/ShieldedMultiSig.test.ts b/contracts/src/multisig/test/ShieldedMultiSig.test.ts index d1ee95b80..f4ec21e83 100644 --- a/contracts/src/multisig/test/ShieldedMultiSig.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSig.test.ts @@ -1,22 +1,39 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { + shieldedTestParentKey, + shieldedTestSigner, +} from '#test-utils/fixtures/shieldedKey.js'; import { ShieldedMultiSigSimulator } from './simulators/ShieldedMultiSigSimulator.js'; const ProposalStatus = { Inactive: 0, Active: 1, Executed: 2, Cancelled: 3 }; const RecipientKind = { ShieldedUser: 0, UnshieldedUser: 1, Contract: 2 }; const THRESHOLD = 2n; -const COLOR = new Uint8Array(32).fill(1); +// A shielded token type the deployer wallet holds on live (genesis-minted); +// `fill(1)` would be unfunded on live. On dry the color is arbitrary. +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; const PROPOSAL_AMOUNT = 400n; -const [, Z_SIGNER1] = utils.generateEitherPubKeyPair('SIGNER1'); -const [, Z_SIGNER2] = utils.generateEitherPubKeyPair('SIGNER2'); -const [, Z_SIGNER3] = utils.generateEitherPubKeyPair('SIGNER3'); +// Signer identities. On live each resolves to a distinct prefunded wallet's coin +// public key (the harness pool), so `.as('SIGNER1')` submits from that wallet and +// `ownPublicKey()` matches — the only way to exercise multi-signer authorization +// on a real node. On dry these are the deterministic synthetic keys `.as(...)` +// resolves to. `OTHER` is not a pooled wallet, so it acts as a non-signer. +const Z_SIGNER1 = shieldedTestSigner('SIGNER1'); +const Z_SIGNER2 = shieldedTestSigner('SIGNER2'); +const Z_SIGNER3 = shieldedTestSigner('SIGNER3'); const SIGNERS = [Z_SIGNER1, Z_SIGNER2, Z_SIGNER3]; -const [, Z_NON_SIGNER] = utils.generateEitherPubKeyPair('OTHER'); -const [, Z_RECIPIENT_PK] = utils.generatePubKeyPair('RECIPIENT'); +const Z_NON_SIGNER = shieldedTestSigner('OTHER'); +// Proposal payout recipient. On live `executeShieldedProposal` sends the treasury +// coins here, so it must be a node-resolvable key (the deployer's own); on dry a +// synthetic key. +const Z_RECIPIENT_PK = shieldedTestParentKey('RECIPIENT'); function makeRecipient(pk: { bytes: Uint8Array }): { kind: number; @@ -25,20 +42,23 @@ function makeRecipient(pk: { bytes: Uint8Array }): { return { kind: RecipientKind.ShieldedUser, address: pk.bytes }; } +// Backend-aware coin builder: live gets a fresh random nonce per run (the node +// persists nullifiers); dry uses `nonce` (else zero) for reproducibility. function makeCoin( color: Uint8Array, value: bigint, nonce?: Uint8Array, ): { nonce: Uint8Array; color: Uint8Array; value: bigint } { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - }; + return encodeShieldedCoinInfo(color, value, nonce); } let multisig: ShieldedMultiSigSimulator; +// A fresh 2-of-3 multisig. Mutating groups deploy one per test (`beforeEach`); +// read-only groups deploy one per group (`beforeAll`) to save a live deploy tx. +const freshMultisig = () => + ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); + describe('ShieldedMultiSig', () => { describe('constructor', () => { it('should initialize with signers and threshold', async () => { @@ -73,11 +93,11 @@ describe('ShieldedMultiSig', () => { }); describe('when initialized', () => { - beforeEach(async () => { - multisig = await ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); - }); - describe('deposit', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should accept deposits', async () => { await multisig.deposit(makeCoin(COLOR, AMOUNT)); expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); @@ -99,325 +119,11 @@ describe('ShieldedMultiSig', () => { }); }); - describe('createShieldedProposal', () => { - it('should allow signer to create proposal', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect(id).toEqual(1n); - }); - - it('should store proposal data correctly', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - - const proposal = await multisig.getProposal(id); - expect(proposal.status).toEqual(ProposalStatus.Active); - expect(proposal.amount).toEqual(PROPOSAL_AMOUNT); - expect(proposal.color).toEqual(COLOR); - }); - - it('should fail for non-signer', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - await expect( - multisig - .as('OTHER') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), - ).rejects.toThrow('SignerManager: not a signer'); - }); - - it('should fail with zero amount', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - await expect( - multisig.as('SIGNER1').createShieldedProposal(to, COLOR, 0n), - ).rejects.toThrow('ProposalManager: zero amount'); - }); - - it('should reject UnshieldedUser recipient kind', async () => { - const to = { - kind: RecipientKind.UnshieldedUser, - address: Z_RECIPIENT_PK.bytes, - }; - await expect( - multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), - ).rejects.toThrow( - 'ShieldedMultiSig: recipient must be a shielded user or contract', - ); - }); - - it('should accept Contract recipient kind', async () => { - const to = { - kind: RecipientKind.Contract, - address: new Uint8Array(32).fill(7), - }; - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect(id).toEqual(1n); - expect((await multisig.getProposalRecipient(id)).kind).toEqual( - RecipientKind.Contract, - ); - }); - }); - - describe('approveProposal', () => { - let proposalId: bigint; - - beforeEach(async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - }); - - it('should allow signer to approve', async () => { - await multisig.as('SIGNER1').approveProposal(proposalId); - expect( - await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), - ).toEqual(true); - expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); - }); - - it('should allow multiple signers to approve', async () => { - await multisig.as('SIGNER1').approveProposal(proposalId); - await multisig.as('SIGNER2').approveProposal(proposalId); - expect(await multisig.getApprovalCount(proposalId)).toEqual(2n); - }); - - it('should fail for non-signer', async () => { - await expect( - multisig.as('OTHER').approveProposal(proposalId), - ).rejects.toThrow('SignerManager: not a signer'); - }); - - it('should fail for double approval', async () => { - await multisig.as('SIGNER1').approveProposal(proposalId); - await expect( - multisig.as('SIGNER1').approveProposal(proposalId), - ).rejects.toThrow('Multisig: already approved'); - }); - - it('should fail for non-existing proposal', async () => { - await expect( - multisig.as('SIGNER1').approveProposal(999n), - ).rejects.toThrow('ProposalManager: proposal not found'); - }); - - it('should fail for executed proposal', async () => { - await multisig.deposit(makeCoin(COLOR, AMOUNT)); - await multisig.as('SIGNER1').approveProposal(proposalId); - await multisig.as('SIGNER2').approveProposal(proposalId); - await multisig.executeShieldedProposal(proposalId); - - await expect( - multisig.as('SIGNER3').approveProposal(proposalId), - ).rejects.toThrow('ProposalManager: proposal not active'); - }); - }); - - describe('revokeApproval', () => { - let proposalId: bigint; - - beforeEach(async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - await multisig.as('SIGNER1').approveProposal(proposalId); - }); - - it('should allow signer to revoke their approval', async () => { - await multisig.as('SIGNER1').revokeApproval(proposalId); - expect( - await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), - ).toEqual(false); - expect(await multisig.getApprovalCount(proposalId)).toEqual(0n); - }); - - it('should fail for non-signer', async () => { - await expect( - multisig.as('OTHER').revokeApproval(proposalId), - ).rejects.toThrow('SignerManager: not a signer'); - }); - - it('should fail if not yet approved', async () => { - await expect( - multisig.as('SIGNER2').revokeApproval(proposalId), - ).rejects.toThrow('Multisig: not approved'); - }); - - it('should allow re-approval after revoke', async () => { - await multisig.as('SIGNER1').revokeApproval(proposalId); - await multisig.as('SIGNER1').approveProposal(proposalId); - expect( - await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), - ).toEqual(true); - expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); - }); - - it('should fail for executed proposal', async () => { - await multisig.deposit(makeCoin(COLOR, AMOUNT)); - await multisig.as('SIGNER2').approveProposal(proposalId); - await multisig.executeShieldedProposal(proposalId); - - await expect( - multisig.as('SIGNER1').revokeApproval(proposalId), - ).rejects.toThrow('ProposalManager: proposal not active'); - }); - }); - - describe('executeShieldedProposal', () => { - let proposalId: bigint; - - beforeEach(async () => { - // Fund the treasury - await multisig.deposit(makeCoin(COLOR, AMOUNT)); - - // Create and approve proposal to threshold - const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - await multisig.as('SIGNER1').approveProposal(proposalId); - await multisig.as('SIGNER2').approveProposal(proposalId); - }); - - it('should execute when threshold is met', async () => { - await multisig.executeShieldedProposal(proposalId); - expect(await multisig.getProposalStatus(proposalId)).toEqual( - ProposalStatus.Executed, - ); - }); - - it('should return sent coin and change in result', async () => { - const result = await multisig.executeShieldedProposal(proposalId); - expect(result.sent.value).toEqual(PROPOSAL_AMOUNT); - expect(result.sent.color).toEqual(COLOR); - expect(result.change.is_some).toEqual(true); - expect(result.change.value.value).toEqual(AMOUNT - PROPOSAL_AMOUNT); - expect(result.change.value.color).toEqual(COLOR); - }); - - it('should return no change when sending full balance', async () => { - // Create proposal for the full amount - const to = makeRecipient(Z_RECIPIENT_PK); - const fullId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, AMOUNT); - await multisig.as('SIGNER1').approveProposal(fullId); - await multisig.as('SIGNER2').approveProposal(fullId); - - const result = await multisig.executeShieldedProposal(fullId); - expect(result.sent.value).toEqual(AMOUNT); - expect(result.change.is_some).toEqual(false); - }); - - it('should deduct from treasury balance', async () => { - await multisig.executeShieldedProposal(proposalId); - expect(await multisig.getTokenBalance(COLOR)).toEqual( - AMOUNT - PROPOSAL_AMOUNT, - ); - }); - - it('should track sent total', async () => { - await multisig.executeShieldedProposal(proposalId); - expect(await multisig.getSentTotal(COLOR)).toEqual(PROPOSAL_AMOUNT); - }); - - it('should fail when threshold is not met', async () => { - // Create a new proposal with only 1 approval - const to = makeRecipient(Z_RECIPIENT_PK); - const id2 = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, 100n); - await multisig.as('SIGNER1').approveProposal(id2); - - await expect(multisig.executeShieldedProposal(id2)).rejects.toThrow( - 'SignerManager: threshold not met', - ); - }); - - it('should fail for non-existing proposal', async () => { - await expect(multisig.executeShieldedProposal(999n)).rejects.toThrow( - 'ProposalManager: proposal not found', - ); - }); - - it('should fail when executed twice', async () => { - await multisig.executeShieldedProposal(proposalId); - await expect( - multisig.executeShieldedProposal(proposalId), - ).rejects.toThrow('ProposalManager: proposal not active'); - }); - - it('should fail with insufficient treasury balance', async () => { - // Create proposal for more than treasury holds - const to = makeRecipient(Z_RECIPIENT_PK); - const bigId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, AMOUNT + 1n); - await multisig.as('SIGNER1').approveProposal(bigId); - await multisig.as('SIGNER2').approveProposal(bigId); - - await expect(multisig.executeShieldedProposal(bigId)).rejects.toThrow( - 'ShieldedTreasury: coin value insufficient', - ); - }); - }); - - describe('view - approvals', () => { - it('should return false for unapproved signer', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect( - await multisig.isProposalApprovedBySigner(id, Z_SIGNER1), - ).toEqual(false); - }); - - it('should return 0 approval count for new proposal', async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect(await multisig.getApprovalCount(id)).toEqual(0n); - }); - }); - - describe('view - proposal delegation', () => { - let proposalId: bigint; - - beforeEach(async () => { - const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - }); - - it('getProposalRecipient should return recipient', async () => { - const recipient = await multisig.getProposalRecipient(proposalId); - expect(recipient.kind).toEqual(RecipientKind.ShieldedUser); - expect(recipient.address).toEqual(Z_RECIPIENT_PK.bytes); - }); - - it('getProposalAmount should return amount', async () => { - expect(await multisig.getProposalAmount(proposalId)).toEqual( - PROPOSAL_AMOUNT, - ); - }); - - it('getProposalColor should return color', async () => { - expect(await multisig.getProposalColor(proposalId)).toEqual(COLOR); + describe('view - signer manager delegation', () => { + beforeAll(async () => { + multisig = await freshMultisig(); }); - }); - describe('view - signer manager delegation', () => { it('getSignerCount should match initial count', async () => { expect(await multisig.getSignerCount()).toEqual(BigInt(SIGNERS.length)); }); @@ -436,7 +142,8 @@ describe('ShieldedMultiSig', () => { }); describe('view - treasury delegation', () => { - beforeEach(async () => { + beforeAll(async () => { + multisig = await freshMultisig(); await multisig.deposit(makeCoin(COLOR, AMOUNT)); }); @@ -457,83 +164,426 @@ describe('ShieldedMultiSig', () => { }); }); - describe('full lifecycle', () => { - it('should handle deposit -> propose -> approve -> execute', async () => { - // Deposit - await multisig.deposit(makeCoin(COLOR, AMOUNT)); - expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); + // Caller-gated flows: authorization is resolved via `ownPublicKey()` (a + // witness the dry sim sets from `.as('SIGNER1')`). On live each alias is + // backed by its own prefunded wallet (the harness pool), so `.as('SIGNER1')` + // submits from that wallet and `ownPublicKey()` is that signer's key — the + // synthetic set is replaced by the pooled wallets' keys (see `shieldedTestSigner`), + // so these run on both backends. + describe('caller-gated proposal flows', () => { + describe('createShieldedProposal', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + + it('should allow signer to create proposal', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + expect(id).toEqual(1n); + }); - // Propose - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - - // Approve to threshold - await multisig.as('SIGNER1').approveProposal(id); - await multisig.as('SIGNER2').approveProposal(id); - expect(await multisig.getApprovalCount(id)).toEqual(THRESHOLD); - - // Execute - await multisig.executeShieldedProposal(id); - expect(await multisig.getProposalStatus(id)).toEqual( - ProposalStatus.Executed, - ); - expect(await multisig.getTokenBalance(COLOR)).toEqual( - AMOUNT - PROPOSAL_AMOUNT, - ); - expect(await multisig.getReceivedMinusSent(COLOR)).toEqual( - AMOUNT - PROPOSAL_AMOUNT, - ); + it('should store proposal data correctly', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + + const proposal = await multisig.getProposal(id); + expect(proposal.status).toEqual(ProposalStatus.Active); + expect(proposal.amount).toEqual(PROPOSAL_AMOUNT); + expect(proposal.color).toEqual(COLOR); + }); + + it('should fail for non-signer', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + await expect( + multisig + .as('OTHER') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), + ).rejects.toThrow('SignerManager: not a signer'); + }); + + it('should fail with zero amount', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + await expect( + multisig.as('SIGNER1').createShieldedProposal(to, COLOR, 0n), + ).rejects.toThrow('ProposalManager: zero amount'); + }); + + it('should reject UnshieldedUser recipient kind', async () => { + const to = { + kind: RecipientKind.UnshieldedUser, + address: Z_RECIPIENT_PK.bytes, + }; + await expect( + multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), + ).rejects.toThrow( + 'ShieldedMultiSig: recipient must be a shielded user or contract', + ); + }); + + it('should accept Contract recipient kind', async () => { + const to = { + kind: RecipientKind.Contract, + address: new Uint8Array(32).fill(7), + }; + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + expect(id).toEqual(1n); + expect((await multisig.getProposalRecipient(id)).kind).toEqual( + RecipientKind.Contract, + ); + }); }); - it('should handle multiple proposals concurrently', async () => { - await multisig.deposit(makeCoin(COLOR, AMOUNT)); + describe('approveProposal', () => { + let proposalId: bigint; - const to = makeRecipient(Z_RECIPIENT_PK); - const id1 = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, 200n); - const id2 = await multisig - .as('SIGNER2') - .createShieldedProposal(to, COLOR, 300n); - - // Approve and execute first - await multisig.as('SIGNER1').approveProposal(id1); - await multisig.as('SIGNER2').approveProposal(id1); - await multisig.executeShieldedProposal(id1); - - // Approve and execute second - await multisig.as('SIGNER1').approveProposal(id2); - await multisig.as('SIGNER3').approveProposal(id2); - await multisig.executeShieldedProposal(id2); - - expect(await multisig.getTokenBalance(COLOR)).toEqual( - AMOUNT - 200n - 300n, - ); + beforeEach(async () => { + multisig = await freshMultisig(); + const to = makeRecipient(Z_RECIPIENT_PK); + proposalId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + }); + + it('should allow signer to approve', async () => { + await multisig.as('SIGNER1').approveProposal(proposalId); + expect( + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + ).toEqual(true); + expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); + }); + + it('should allow multiple signers to approve', async () => { + await multisig.as('SIGNER1').approveProposal(proposalId); + await multisig.as('SIGNER2').approveProposal(proposalId); + expect(await multisig.getApprovalCount(proposalId)).toEqual(2n); + }); + + it('should fail for non-signer', async () => { + await expect( + multisig.as('OTHER').approveProposal(proposalId), + ).rejects.toThrow('SignerManager: not a signer'); + }); + + it('should fail for double approval', async () => { + await multisig.as('SIGNER1').approveProposal(proposalId); + await expect( + multisig.as('SIGNER1').approveProposal(proposalId), + ).rejects.toThrow('Multisig: already approved'); + }); + + it('should fail for non-existing proposal', async () => { + await expect( + multisig.as('SIGNER1').approveProposal(999n), + ).rejects.toThrow('ProposalManager: proposal not found'); + }); + + it('should fail for executed proposal', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + await multisig.as('SIGNER1').approveProposal(proposalId); + await multisig.as('SIGNER2').approveProposal(proposalId); + await multisig.executeShieldedProposal(proposalId); + + await expect( + multisig.as('SIGNER3').approveProposal(proposalId), + ).rejects.toThrow('ProposalManager: proposal not active'); + }); + }); + + describe('revokeApproval', () => { + let proposalId: bigint; + + beforeEach(async () => { + multisig = await freshMultisig(); + const to = makeRecipient(Z_RECIPIENT_PK); + proposalId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + await multisig.as('SIGNER1').approveProposal(proposalId); + }); + + it('should allow signer to revoke their approval', async () => { + await multisig.as('SIGNER1').revokeApproval(proposalId); + expect( + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + ).toEqual(false); + expect(await multisig.getApprovalCount(proposalId)).toEqual(0n); + }); + + it('should fail for non-signer', async () => { + await expect( + multisig.as('OTHER').revokeApproval(proposalId), + ).rejects.toThrow('SignerManager: not a signer'); + }); + + it('should fail if not yet approved', async () => { + await expect( + multisig.as('SIGNER2').revokeApproval(proposalId), + ).rejects.toThrow('Multisig: not approved'); + }); + + it('should allow re-approval after revoke', async () => { + await multisig.as('SIGNER1').revokeApproval(proposalId); + await multisig.as('SIGNER1').approveProposal(proposalId); + expect( + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + ).toEqual(true); + expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); + }); + + it('should fail for executed proposal', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + await multisig.as('SIGNER2').approveProposal(proposalId); + await multisig.executeShieldedProposal(proposalId); + + await expect( + multisig.as('SIGNER1').revokeApproval(proposalId), + ).rejects.toThrow('ProposalManager: proposal not active'); + }); + }); + + describe('executeShieldedProposal', () => { + let proposalId: bigint; + + beforeEach(async () => { + multisig = await freshMultisig(); + // Fund the treasury + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + + // Create and approve proposal to threshold + const to = makeRecipient(Z_RECIPIENT_PK); + proposalId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + await multisig.as('SIGNER1').approveProposal(proposalId); + await multisig.as('SIGNER2').approveProposal(proposalId); + }); + + it('should execute when threshold is met', async () => { + await multisig.executeShieldedProposal(proposalId); + expect(await multisig.getProposalStatus(proposalId)).toEqual( + ProposalStatus.Executed, + ); + }); + + it('should return sent coin and change in result', async () => { + const result = await multisig.executeShieldedProposal(proposalId); + expect(result.sent.value).toEqual(PROPOSAL_AMOUNT); + expect(result.sent.color).toEqual(COLOR); + expect(result.change.is_some).toEqual(true); + expect(result.change.value.value).toEqual(AMOUNT - PROPOSAL_AMOUNT); + expect(result.change.value.color).toEqual(COLOR); + }); + + it('should return no change when sending full balance', async () => { + // Create proposal for the full amount + const to = makeRecipient(Z_RECIPIENT_PK); + const fullId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, AMOUNT); + await multisig.as('SIGNER1').approveProposal(fullId); + await multisig.as('SIGNER2').approveProposal(fullId); + + const result = await multisig.executeShieldedProposal(fullId); + expect(result.sent.value).toEqual(AMOUNT); + expect(result.change.is_some).toEqual(false); + }); + + it('should deduct from treasury balance', async () => { + await multisig.executeShieldedProposal(proposalId); + expect(await multisig.getTokenBalance(COLOR)).toEqual( + AMOUNT - PROPOSAL_AMOUNT, + ); + }); + + it('should track sent total', async () => { + await multisig.executeShieldedProposal(proposalId); + expect(await multisig.getSentTotal(COLOR)).toEqual(PROPOSAL_AMOUNT); + }); + + it('should fail when threshold is not met', async () => { + // Create a new proposal with only 1 approval + const to = makeRecipient(Z_RECIPIENT_PK); + const id2 = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, 100n); + await multisig.as('SIGNER1').approveProposal(id2); + + await expect(multisig.executeShieldedProposal(id2)).rejects.toThrow( + 'SignerManager: threshold not met', + ); + }); + + it('should fail for non-existing proposal', async () => { + await expect(multisig.executeShieldedProposal(999n)).rejects.toThrow( + 'ProposalManager: proposal not found', + ); + }); + + it('should fail when executed twice', async () => { + await multisig.executeShieldedProposal(proposalId); + await expect( + multisig.executeShieldedProposal(proposalId), + ).rejects.toThrow('ProposalManager: proposal not active'); + }); + + it('should fail with insufficient treasury balance', async () => { + // Create proposal for more than treasury holds + const to = makeRecipient(Z_RECIPIENT_PK); + const bigId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, AMOUNT + 1n); + await multisig.as('SIGNER1').approveProposal(bigId); + await multisig.as('SIGNER2').approveProposal(bigId); + + await expect(multisig.executeShieldedProposal(bigId)).rejects.toThrow( + 'ShieldedTreasury: coin value insufficient', + ); + }); }); - it('should handle approve -> revoke -> re-approve -> execute', async () => { - await multisig.deposit(makeCoin(COLOR, AMOUNT)); - const to = makeRecipient(Z_RECIPIENT_PK); - const id = await multisig - .as('SIGNER1') - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - - // Approve then revoke - await multisig.as('SIGNER1').approveProposal(id); - await multisig.as('SIGNER1').revokeApproval(id); - expect(await multisig.getApprovalCount(id)).toEqual(0n); - - // Re-approve with enough signers - await multisig.as('SIGNER2').approveProposal(id); - await multisig.as('SIGNER3').approveProposal(id); - expect(await multisig.getApprovalCount(id)).toEqual(2n); - - await multisig.executeShieldedProposal(id); - expect(await multisig.getProposalStatus(id)).toEqual( - ProposalStatus.Executed, - ); + describe('view - approvals', () => { + beforeAll(async () => { + multisig = await freshMultisig(); + }); + + it('should return false for unapproved signer', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + expect( + await multisig.isProposalApprovedBySigner(id, Z_SIGNER1), + ).toEqual(false); + }); + + it('should return 0 approval count for new proposal', async () => { + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + expect(await multisig.getApprovalCount(id)).toEqual(0n); + }); + }); + + describe('view - proposal delegation', () => { + let proposalId: bigint; + + beforeAll(async () => { + multisig = await freshMultisig(); + const to = makeRecipient(Z_RECIPIENT_PK); + proposalId = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + }); + + it('getProposalRecipient should return recipient', async () => { + const recipient = await multisig.getProposalRecipient(proposalId); + expect(recipient.kind).toEqual(RecipientKind.ShieldedUser); + expect(recipient.address).toEqual(Z_RECIPIENT_PK.bytes); + }); + + it('getProposalAmount should return amount', async () => { + expect(await multisig.getProposalAmount(proposalId)).toEqual( + PROPOSAL_AMOUNT, + ); + }); + + it('getProposalColor should return color', async () => { + expect(await multisig.getProposalColor(proposalId)).toEqual(COLOR); + }); + }); + + // TODO: move to integration tests + describe('full lifecycle', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + + it('should handle deposit -> propose -> approve -> execute', async () => { + // Deposit + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); + + // Propose + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + + // Approve to threshold + await multisig.as('SIGNER1').approveProposal(id); + await multisig.as('SIGNER2').approveProposal(id); + expect(await multisig.getApprovalCount(id)).toEqual(THRESHOLD); + + // Execute + await multisig.executeShieldedProposal(id); + expect(await multisig.getProposalStatus(id)).toEqual( + ProposalStatus.Executed, + ); + expect(await multisig.getTokenBalance(COLOR)).toEqual( + AMOUNT - PROPOSAL_AMOUNT, + ); + expect(await multisig.getReceivedMinusSent(COLOR)).toEqual( + AMOUNT - PROPOSAL_AMOUNT, + ); + }); + + it('should handle multiple proposals concurrently', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + + const to = makeRecipient(Z_RECIPIENT_PK); + const id1 = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, 200n); + const id2 = await multisig + .as('SIGNER2') + .createShieldedProposal(to, COLOR, 300n); + + // Approve and execute first + await multisig.as('SIGNER1').approveProposal(id1); + await multisig.as('SIGNER2').approveProposal(id1); + await multisig.executeShieldedProposal(id1); + + // Approve and execute second + await multisig.as('SIGNER1').approveProposal(id2); + await multisig.as('SIGNER3').approveProposal(id2); + await multisig.executeShieldedProposal(id2); + + expect(await multisig.getTokenBalance(COLOR)).toEqual( + AMOUNT - 200n - 300n, + ); + }); + + it('should handle approve -> revoke -> re-approve -> execute', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + const to = makeRecipient(Z_RECIPIENT_PK); + const id = await multisig + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); + + // Approve then revoke + await multisig.as('SIGNER1').approveProposal(id); + await multisig.as('SIGNER1').revokeApproval(id); + expect(await multisig.getApprovalCount(id)).toEqual(0n); + + // Re-approve with enough signers + await multisig.as('SIGNER2').approveProposal(id); + await multisig.as('SIGNER3').approveProposal(id); + expect(await multisig.getApprovalCount(id)).toEqual(2n); + + await multisig.executeShieldedProposal(id); + expect(await multisig.getProposalStatus(id)).toEqual( + ProposalStatus.Executed, + ); + }); }); }); }); diff --git a/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts b/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts index 3dea44c72..680b12ec2 100644 --- a/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts @@ -1,10 +1,16 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, + encodeShieldedCoinInfo as makeCoin, +} from '#test-utils/fixtures/nativeShieldedToken.js'; import { ShieldedMultiSigV2Simulator } from './simulators/ShieldedMultiSigV2Simulator.js'; const RecipientKind = { ShieldedUser: 0, UnshieldedUser: 1, Contract: 2 }; const INSTANCE_SALT = new Uint8Array(32).fill(0xaa); -const COLOR = new Uint8Array(32).fill(1); +// A shielded token type the deployer wallet holds on live (genesis-minted); +// `fill(1)` would be unfunded on live. On dry the color is arbitrary. +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; const PK1 = new Uint8Array(64).fill(0x11); @@ -26,6 +32,10 @@ const COMMITMENT3 = ShieldedMultiSigV2Simulator.calculateSignerId( ); const SIGNER_COMMITMENTS = [COMMITMENT1, COMMITMENT2, COMMITMENT3]; +// ECDSA verification is stubbed in the contract (`stubVerifySignature` returns +// true), so any 64-byte value passes. Authorization is enforced only by +// signer-commitment membership and duplicate detection — both caller-agnostic, +// so this spec runs unchanged on live (no `ownPublicKey`-based caller identity). const DUMMY_SIG = new Uint8Array(64).fill(0xff); function makeRecipient(address: Uint8Array): { @@ -35,18 +45,6 @@ function makeRecipient(address: Uint8Array): { return { kind: RecipientKind.ShieldedUser, address }; } -function makeCoin( - color: Uint8Array, - value: bigint, - nonce?: Uint8Array, -): { nonce: Uint8Array; color: Uint8Array; value: bigint } { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - }; -} - function makeQualifiedCoin( color: Uint8Array, value: bigint, @@ -68,6 +66,11 @@ function makeQualifiedCoin( let multisig: ShieldedMultiSigV2Simulator; +// A fresh 2-of-3 stateless multisig. Mutating groups build one per test +// (`beforeEach`); the read-only `view` group shares one deploy (`beforeAll`). +const freshMultisig = () => + ShieldedMultiSigV2Simulator.create(INSTANCE_SALT, SIGNER_COMMITMENTS, 2n); + describe('ShieldedMultiSigV2', () => { describe('constructor', () => { it('should initialize with 2-of-3 threshold', async () => { @@ -137,15 +140,11 @@ describe('ShieldedMultiSigV2', () => { }); describe('when initialized', () => { - beforeEach(async () => { - multisig = await ShieldedMultiSigV2Simulator.create( - INSTANCE_SALT, - SIGNER_COMMITMENTS, - 2n, - ); - }); - describe('view', () => { + beforeAll(async () => { + multisig = await freshMultisig(); + }); + it('getNonce should start at 0', async () => { expect(await multisig.getNonce()).toEqual(0n); }); @@ -160,12 +159,20 @@ describe('ShieldedMultiSigV2', () => { }); describe('deposit', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should accept deposits without reverting', async () => { await multisig.deposit(makeCoin(COLOR, AMOUNT)); }); }); describe('execute', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should reject duplicate signer', async () => { const to = makeRecipient(new Uint8Array(32).fill(7)); const coin = makeQualifiedCoin(COLOR, AMOUNT, 0n); diff --git a/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts b/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts index dcf3900b6..fded63c35 100644 --- a/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts @@ -1,5 +1,7 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import * as utils from '#test-utils/fixtures/address.js'; +import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js'; import { calculateSignerId, ShieldedMultiSigV3Simulator, @@ -12,6 +14,10 @@ const INIT_COIN_NONCE = new Uint8Array(32).fill(0xbb); const TOKEN_DOMAIN = new Uint8Array(32); Buffer.from('smt:token:').copy(TOKEN_DOMAIN); +// Signer identity is a commitment (`calculateSignerId(pk, salt)`) passed to +// `mint`/`burn` explicitly, and ECDSA verification is stubbed (`DUMMY_SIG` +// passes), so authorization is caller-agnostic — this spec's signer logic runs +// unchanged on live (no `ownPublicKey`-based identity). const PK1 = new Uint8Array(64).fill(0x11); const PK2 = new Uint8Array(64).fill(0x22); const PK3 = new Uint8Array(64).fill(0x33); @@ -24,9 +30,17 @@ const SIGNER_COMMITMENTS = [COMMITMENT1, COMMITMENT2, COMMITMENT3]; const DUMMY_SIG = new Uint8Array(64).fill(0xff); -const USER_RECIPIENT = utils.createEitherTestUser('ALICE'); +// A contract recipient for `mint`. Dry-only: minting to a non-participating +// contract publishes an output no one claims, which a live node rejects (the +// same unclaimed-output limit that blocks atomic contract-recipient sends). const CONTRACT_RECIPIENT = utils.createEitherTestContractAddress('TARGET'); +// The user recipient for `mint`. Assigned in `beforeEach` after `create()`: on +// live it resolves to the deployer's own coin public key (whose encryption key +// the node can resolve), so the minted coin is deliverable; dry → a synthetic +// user. +let USER_RECIPIENT: ReturnType; + function makeQualifiedCoin( color: Uint8Array, value: bigint, @@ -48,6 +62,17 @@ function makeQualifiedCoin( let multisig: ShieldedMultiSigV3Simulator; +// A fresh multisig-token instance. Mutating groups build one per test +// (`beforeEach`); read-only groups build one per group (`beforeAll`) to save a +// live deploy tx. +const freshMultisig = () => + ShieldedMultiSigV3Simulator.create( + INSTANCE_SALT, + INIT_COIN_NONCE, + TOKEN_DOMAIN, + SIGNER_COMMITMENTS, + ); + describe('ShieldedMultiSigV3', () => { describe('constructor', () => { it('should initialize', async () => { @@ -110,13 +135,13 @@ describe('ShieldedMultiSigV3', () => { }); describe('when initialized', () => { - beforeEach(async () => { - multisig = await ShieldedMultiSigV3Simulator.create( - INSTANCE_SALT, - INIT_COIN_NONCE, - TOKEN_DOMAIN, - SIGNER_COMMITMENTS, - ); + // USER_RECIPIENT is stable (deployer key on live, synthetic on dry), so + // resolve it once after the first deploy. The read-only `view` and + // `_calculateSignerId` groups run first and reuse this shared deploy; + // mutating groups below build their own fresh instance per test. + beforeAll(async () => { + multisig = await freshMultisig(); + USER_RECIPIENT = shieldedTestRecipient(); }); describe('view', () => { @@ -177,6 +202,10 @@ describe('ShieldedMultiSigV3', () => { }); describe('mint', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should mint to a user recipient with signers 0 and 1', async () => { await multisig.mint( 100n, @@ -204,14 +233,19 @@ describe('ShieldedMultiSigV3', () => { ); }); - it('should mint to a contract recipient', async () => { - await multisig.mint( - 100n, - CONTRACT_RECIPIENT, - [PK1, PK2], - [DUMMY_SIG, DUMMY_SIG], - ); - }); + // Live: a mint to a non-participating contract leaves an unclaimed output + // the node rejects (no atomic cross-contract receive today). + it.skipIf(isLiveBackend())( + 'should mint to a contract recipient', + async () => { + await multisig.mint( + 100n, + CONTRACT_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + }, + ); it('should reject duplicate signer', async () => { await expect( @@ -261,7 +295,7 @@ describe('ShieldedMultiSigV3', () => { ); await multisig.mint( 300n, - CONTRACT_RECIPIENT, + USER_RECIPIENT, [PK2, PK3], [DUMMY_SIG, DUMMY_SIG], ); @@ -296,30 +330,58 @@ describe('ShieldedMultiSigV3', () => { }); }); + // A successful burn spends a real coin of the contract's own token. Its + // nonce is derived inside the mint circuit, so the spec cannot reconstruct + // it to recover the coin's `mt_index` on live (that is the wallet SDK's + // ciphertext-discovery job, out of scope for the coin tracker). The + // rejection paths below throw before the receive/spend, so they run on both + // backends; the success paths are dry-only. describe('burn', () => { - it('should burn with valid coin and signers 0 and 1', async () => { - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }); - - it('should burn with signers 0 and 2', async () => { - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 100n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); - }); - - it('should burn with signers 1 and 2', async () => { - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 100n, [PK2, PK3], [DUMMY_SIG, DUMMY_SIG]); - }); - - it('should burn partial amount', async () => { - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 50n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }); + beforeEach(async () => { + multisig = await freshMultisig(); + }); + + // Happy-path burns execute a real spend, so they are dry-only until the + // live harness can fund and track the burned coin. + describe.skipIf(isLiveBackend())('happy path (dry only)', () => { + it('should burn with valid coin and signers 0 and 1', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); + }); + + it('should burn with signers 0 and 2', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 100n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); + }); + + it('should burn with signers 1 and 2', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 100n, [PK2, PK3], [DUMMY_SIG, DUMMY_SIG]); + }); + + it('should burn partial amount', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 50n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); + }); + + it('should handle zero burn amount', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 0n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); + }); + + it('should share nonce across mint and burn', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + expect(await multisig.getNonce()).toEqual(1n); - it('should handle zero burn amount', async () => { - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 0n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await multisig.burn(coin, 50n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); + expect(await multisig.getNonce()).toEqual(2n); + }); }); it('should reject duplicate signer', async () => { @@ -362,23 +424,15 @@ describe('ShieldedMultiSigV3', () => { multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]), ).rejects.toThrow('Multisig: insufficient coin value'); }); - - it('should share nonce across mint and burn', async () => { - await multisig.mint( - 100n, - USER_RECIPIENT, - [PK1, PK2], - [DUMMY_SIG, DUMMY_SIG], - ); - expect(await multisig.getNonce()).toEqual(1n); - - const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); - await multisig.burn(coin, 50n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); - expect(await multisig.getNonce()).toEqual(2n); - }); }); describe('domain separation', () => { + // Read-only on `multisig`, but runs after the mutating groups above, so + // deploy a clean shared instance for the group. + beforeAll(async () => { + multisig = await freshMultisig(); + }); + it('should isolate signers across instances with different salts', async () => { const salt2 = new Uint8Array(32).fill(0xcc); const c1 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); @@ -404,6 +458,10 @@ describe('ShieldedMultiSigV3', () => { }); describe('nonce', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should start at 0', async () => { expect(await multisig.getNonce()).toEqual(0n); }); @@ -422,6 +480,10 @@ describe('ShieldedMultiSigV3', () => { }); describe('cross-instance replay', () => { + beforeEach(async () => { + multisig = await freshMultisig(); + }); + it('should derive different message hashes for different instances', async () => { const instance2 = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, diff --git a/contracts/src/multisig/test/ShieldedTreasury.test.ts b/contracts/src/multisig/test/ShieldedTreasury.test.ts index 017723fd8..37a1e5adb 100644 --- a/contracts/src/multisig/test/ShieldedTreasury.test.ts +++ b/contracts/src/multisig/test/ShieldedTreasury.test.ts @@ -1,15 +1,24 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js'; import { bytesToHex, isNonceSpent, zswapDelta, zswapSnapshot, -} from '#test-utils/zswap.js'; +} from '#test-utils/fixtures/zswap.js'; import { ShieldedTreasurySimulator } from './simulators/ShieldedTreasurySimulator.js'; -const COLOR = new Uint8Array(32).fill(1); -const COLOR2 = new Uint8Array(32).fill(2); +// Genesis-funded shielded colors (`0x00…01` / `0x00…02`): on the live backend a +// `_deposit` / `_send` can only draw a color the deployer wallet holds, so specs +// must use these. `new Uint8Array(32).fill(1)` (`0x0101…01`) is unfunded on live +// (`Wallet.InsufficientFunds`); on dry any color mints freely. See nativeShieldedToken.ts. +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; +const COLOR2 = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken2; const AMOUNT = 1000n; // A non-zero deploy address so the change output (routed to self via @@ -18,27 +27,38 @@ const AMOUNT = 1000n; // THIS contract, not merely to "some contract arm". const TREASURY_ADDRESS = '7a'.repeat(32); -const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); +// Assigned in `beforeAll` after `create()` syncs the wallet: on live the harness +// then publishes MIDNIGHT_DEPLOYER_COIN_PK, so this resolves to the deployer's own +// coin public key (an encryption key the node can resolve); dry → a synthetic user. +let Z_RECIPIENT: ReturnType; +// Delegates to the backend-aware builder: on live every coin gets a fresh random +// nonce (the local node persists nullifiers across runs, so a fixed nonce would +// replay an already-spent coin — Custom error 103); on dry it uses `nonce` (else +// zero) so assertions stay reproducible. function makeCoin( color: Uint8Array, value: bigint, nonce?: Uint8Array, ): { nonce: Uint8Array; color: Uint8Array; value: bigint } { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - }; + return encodeShieldedCoinInfo(color, value, nonce); } let treasury: ShieldedTreasurySimulator; +// A fresh treasury at the fixed deploy address. Mutating groups build one per +// test (`beforeEach`); the read-only `initial state` group shares one deploy. +const freshTreasury = () => + ShieldedTreasurySimulator.create({ contractAddress: TREASURY_ADDRESS }); + describe('ShieldedTreasury', () => { - beforeEach(async () => { - treasury = await ShieldedTreasurySimulator.create({ - contractAddress: TREASURY_ADDRESS, - }); + // Z_RECIPIENT is stable (the deployer's own coin key on live, a synthetic key + // on dry), so resolve it once after the first deploy; mutating groups then + // only need a fresh treasury per test. The read-only `initial state` group + // reuses this shared deploy. + beforeAll(async () => { + treasury = await freshTreasury(); + Z_RECIPIENT = shieldedTestRecipient(); }); describe('initial state', () => { @@ -60,6 +80,10 @@ describe('ShieldedTreasury', () => { }); describe('_deposit', () => { + beforeEach(async () => { + treasury = await freshTreasury(); + }); + it('should deposit and update balance', async () => { await treasury._deposit(makeCoin(COLOR, AMOUNT)); expect(await treasury.getTokenBalance(COLOR)).toEqual(AMOUNT); @@ -102,6 +126,7 @@ describe('ShieldedTreasury', () => { describe('_send', () => { beforeEach(async () => { + treasury = await freshTreasury(); await treasury._deposit(makeCoin(COLOR, AMOUNT)); }); @@ -146,93 +171,161 @@ describe('ShieldedTreasury', () => { // rejects (`Zswap(NullifierAlreadyPresent)`). The dry simulator does not // enforce nullifiers, so these tests read the recorded Zswap I/O: a re-spend // of the change coin would show up as an extra input carrying its nonce. - describe('_send — change coin is spendable (no double spend)', () => { - beforeEach(async () => { - await treasury._deposit(makeCoin(COLOR, 400n)); - }); - - it('should spend the stored coin and route the change back to itself on a partial send', async () => { - const snap = zswapSnapshot(treasury); - const result = await treasury._send(Z_RECIPIENT, COLOR, 150n); - const { inputs, outputs } = zswapDelta(treasury, snap); - - // Exactly one coin is consumed: the stored 400 balance (nonce 0). - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(400n); - expect(inputs[0].color).toStrictEqual(COLOR); - expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); - - // Two coins are produced: the payment (to the recipient key, `left` arm) - // and the change (back to this contract, `right`/self arm). - expect(outputs).toHaveLength(2); - const toRecipient = outputs.filter((o) => o.recipient.is_left); - const toSelf = outputs.filter((o) => !o.recipient.is_left); - expect(toRecipient).toHaveLength(1); - expect(toSelf).toHaveLength(1); - - // Payment: 150 of COLOR to the intended recipient key; equals result.sent. - expect(toRecipient[0].coinInfo.value).toBe(150n); - expect(toRecipient[0].coinInfo.color).toStrictEqual(COLOR); - expect(toRecipient[0].coinInfo).toStrictEqual(result.sent); - expect(toRecipient[0].recipient.left.bytes).toStrictEqual( - Z_RECIPIENT.left.bytes, - ); + // + // Dry-only: `zswapSnapshot`/`zswapDelta` read the dry sim's Zswap local state, + // which does not exist on the live backend. The live counterpart is the block + // that follows. + describe.skipIf(isLiveBackend())( + '_send — change coin is spendable, via Zswap I/O (dry only, no double spend)', + () => { + it('should spend the stored coin and route the change back to itself on a partial send', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const snap = zswapSnapshot(treasury); + const result = await treasury._send(Z_RECIPIENT, COLOR, 150n); + const { inputs, outputs } = zswapDelta(treasury, snap); + + // Exactly one coin is consumed: the stored 400 balance (nonce 0). + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(400n); + expect(inputs[0].color).toStrictEqual(COLOR); + expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); + + // Two coins are produced: the payment (to the recipient key, `left` arm) + // and the change (back to this contract, `right`/self arm). + expect(outputs).toHaveLength(2); + const toRecipient = outputs.filter((o) => o.recipient.is_left); + const toSelf = outputs.filter((o) => !o.recipient.is_left); + expect(toRecipient).toHaveLength(1); + expect(toSelf).toHaveLength(1); + + // Payment: 150 of COLOR to the intended recipient key; equals result.sent. + expect(toRecipient[0].coinInfo.value).toBe(150n); + expect(toRecipient[0].coinInfo.color).toStrictEqual(COLOR); + expect(toRecipient[0].coinInfo).toStrictEqual(result.sent); + expect(toRecipient[0].recipient.left.bytes).toStrictEqual( + Z_RECIPIENT.left.bytes, + ); + + // Change: 250 of COLOR routed back to THIS contract's address; identical + // to the returned change coin, and crucially NOT spent in this same tx. + expect(result.change.is_some).toBe(true); + expect(toSelf[0].coinInfo.value).toBe(250n); + expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); + expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( + TREASURY_ADDRESS, + ); + expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); + }); + + it('should spend exactly the stored change coin on a follow-up spend', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const first = await treasury._send(Z_RECIPIENT, COLOR, 150n); // 250 change stored + const storedChange = first.change.value; + + const snap = zswapSnapshot(treasury); + const second = await treasury._send(Z_RECIPIENT, COLOR, 250n); // spend the change + const { inputs, outputs } = zswapDelta(treasury, snap); + + // A node would reject this as a double spend if the 250 change coin had + // already been nullified by the first send. The single input must be + // exactly that stored change coin (same nonce/value/color). + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(250n); + expect(inputs[0].color).toStrictEqual(COLOR); + expect(inputs[0].nonce).toStrictEqual(storedChange.nonce); + + // Full spend of the change: one output to the recipient, no further change. + expect(second.change.is_some).toBe(false); + expect(outputs).toHaveLength(1); + expect(outputs[0].recipient.is_left).toBe(true); + expect(outputs[0].coinInfo.value).toBe(250n); + expect(await treasury.getTokenBalance(COLOR)).toBe(0n); + }); + + it('should spend the balance and produce only the payment when sending in full', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const snap = zswapSnapshot(treasury); + const result = await treasury._send(Z_RECIPIENT, COLOR, 400n); + const { inputs, outputs } = zswapDelta(treasury, snap); + + // No change: one input (the 400 balance), one output (the payment). + expect(result.change.is_some).toBe(false); + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(400n); + expect(outputs).toHaveLength(1); + expect(outputs[0].recipient.is_left).toBe(true); + expect(outputs[0].recipient.left.bytes).toStrictEqual( + Z_RECIPIENT.left.bytes, + ); + expect(outputs[0].coinInfo.value).toBe(400n); + expect(outputs[0].coinInfo).toStrictEqual(result.sent); + }); + }, + ); + + // Live counterpart of the block above: the SAME three cases, asserted on what a + // node exposes. Zswap I/O is not readable on live, so instead of inspecting the + // spent/produced coins we rely on the node's own nullifier enforcement — a + // re-spent (nullified) change coin is rejected with `Zswap(NullifierAlreadyPresent)` + // / substrate Custom error 103, so a later spend of the retained change SUCCEEDING + // is the no-double-spend proof — plus the recorded ledger balance / sent total. + describe.runIf(isLiveBackend())( + '_send — change coin is spendable on live (no double spend)', + () => { + it('should spend the stored coin and route the change back to itself on a partial send', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const result = await treasury._send(Z_RECIPIENT, COLOR, 150n); + + // Payment: 150 of COLOR to the recipient. + expect(result.sent.value).toBe(150n); + expect(result.sent.color).toStrictEqual(COLOR); + + // Change: 250 of COLOR routed back to the treasury and retained as the new + // balance (the recipient arm/address isn't observable on live, so assert + // on the recorded balance instead). + expect(result.change.is_some).toBe(true); + expect(result.change.value.value).toBe(250n); + expect(await treasury.getTokenBalance(COLOR)).toBe(250n); + }); + + it('should spend exactly the stored change coin on a follow-up spend', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const first = await treasury._send(Z_RECIPIENT, COLOR, 150n); // 250 change stored + expect(first.change.is_some).toBe(true); + + // The node rejects a double spend if the 250 change coin had already been + // nullified by the first send. This second send SUCCEEDING is the proof + // the retained change stayed spendable. + const second = await treasury._send(Z_RECIPIENT, COLOR, 250n); // spend the change + expect(second.sent.value).toBe(250n); + expect(second.change.is_some).toBe(false); + expect(await treasury.getTokenBalance(COLOR)).toBe(0n); + expect(await treasury.getSentTotal(COLOR)).toBe(400n); + }); + + it('should spend the balance and produce only the payment when sending in full', async () => { + treasury = await freshTreasury(); + await treasury._deposit(makeCoin(COLOR, 400n)); + const result = await treasury._send(Z_RECIPIENT, COLOR, 400n); + + // No change: the full balance is sent and nothing is retained. + expect(result.sent.value).toBe(400n); + expect(result.change.is_some).toBe(false); + expect(await treasury.getTokenBalance(COLOR)).toBe(0n); + }); + }, + ); - // Change: 250 of COLOR routed back to THIS contract's address; identical - // to the returned change coin, and crucially NOT spent in this same tx. - expect(result.change.is_some).toBe(true); - expect(toSelf[0].coinInfo.value).toBe(250n); - expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); - expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( - TREASURY_ADDRESS, - ); - expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); - }); - - it('should spend exactly the stored change coin on a follow-up spend', async () => { - const first = await treasury._send(Z_RECIPIENT, COLOR, 150n); // 250 change stored - const storedChange = first.change.value; - - const snap = zswapSnapshot(treasury); - const second = await treasury._send(Z_RECIPIENT, COLOR, 250n); // spend the change - const { inputs, outputs } = zswapDelta(treasury, snap); - - // A node would reject this as a double spend if the 250 change coin had - // already been nullified by the first send. The single input must be - // exactly that stored change coin (same nonce/value/color). - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(250n); - expect(inputs[0].color).toStrictEqual(COLOR); - expect(inputs[0].nonce).toStrictEqual(storedChange.nonce); - - // Full spend of the change: one output to the recipient, no further change. - expect(second.change.is_some).toBe(false); - expect(outputs).toHaveLength(1); - expect(outputs[0].recipient.is_left).toBe(true); - expect(outputs[0].coinInfo.value).toBe(250n); - expect(await treasury.getTokenBalance(COLOR)).toBe(0n); - }); - - it('should spend the balance and produce only the payment when sending in full', async () => { - const snap = zswapSnapshot(treasury); - const result = await treasury._send(Z_RECIPIENT, COLOR, 400n); - const { inputs, outputs } = zswapDelta(treasury, snap); - - // No change: one input (the 400 balance), one output (the payment). - expect(result.change.is_some).toBe(false); - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(400n); - expect(outputs).toHaveLength(1); - expect(outputs[0].recipient.is_left).toBe(true); - expect(outputs[0].recipient.left.bytes).toStrictEqual( - Z_RECIPIENT.left.bytes, - ); - expect(outputs[0].coinInfo.value).toBe(400n); - expect(outputs[0].coinInfo).toStrictEqual(result.sent); + describe('accounting consistency', () => { + beforeEach(async () => { + treasury = await freshTreasury(); }); - }); - describe('accounting consistency', () => { it('should keep receivedMinusSent equal to balance', async () => { await treasury._deposit(makeCoin(COLOR, 500n)); await treasury._send(Z_RECIPIENT, COLOR, 200n); diff --git a/contracts/src/multisig/test/ShieldedTreasuryStateless.test.ts b/contracts/src/multisig/test/ShieldedTreasuryStateless.test.ts index c7edaa7fa..42426f765 100644 --- a/contracts/src/multisig/test/ShieldedTreasuryStateless.test.ts +++ b/contracts/src/multisig/test/ShieldedTreasuryStateless.test.ts @@ -1,57 +1,68 @@ +import type { EncodedQualifiedShieldedCoinInfo } from '@midnight-ntwrk/compact-runtime'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js'; import { bytesToHex, isNonceSpent, zswapDelta, zswapSnapshot, -} from '#test-utils/zswap.js'; +} from '#test-utils/fixtures/zswap.js'; +import { + contractOwner, + getQualifiedShieldedCoinInfo, +} from '#test-utils/harness/NativeShieldedTokenTracker.js'; import { MockShieldedTreasuryStatelessSimulator } from './simulators/MockShieldedTreasuryStatelessSimulator.js'; -const COLOR = new Uint8Array(32).fill(1); +// Genesis-funded shielded color (`0x00…01`): on the live backend a `_deposit` / +// `_send` can only draw a color the deployer wallet holds. +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; // A non-zero deploy address so the change output (routed to self) carries a // recognizable address rather than the zero `dummyContractAddress()` default. const TREASURY_ADDRESS = '5c'.repeat(32); -const Z_RECIPIENT = utils.createEitherTestUser('RECIPIENT'); +// Assigned in `beforeEach` after `create()` syncs the wallet: on live this +// resolves to the deployer's own coin public key (an encryption key the node can +// resolve); dry → a synthetic user. +let Z_RECIPIENT: ReturnType; +// Delegates to the backend-aware builder: live gets a fresh random nonce per run +// (the local node persists nullifiers, so a fixed nonce would replay a spent +// coin — Custom error 103); dry uses `nonce` (else zero) for reproducibility. function makeCoin(color: Uint8Array, value: bigint, nonce?: Uint8Array) { - return { nonce: nonce ?? new Uint8Array(32).fill(0), color, value }; -} - -function makeQualifiedCoin( - color: Uint8Array, - value: bigint, - mtIndex: bigint, - nonce?: Uint8Array, -) { - return { - nonce: nonce ?? new Uint8Array(32).fill(0), - color, - value, - mt_index: mtIndex, - }; + return encodeShieldedCoinInfo(color, value, nonce); } let treasury: MockShieldedTreasuryStatelessSimulator; +// The coin deposited in `beforeEach`, qualified with its coin-commitment-tree +// index: recovered from the chain's event stream on live (the stateless treasury +// keeps no record of it), a `0n` placeholder the in-memory runtime ignores on dry. +let coin: EncodedQualifiedShieldedCoinInfo; describe('ShieldedTreasuryStateless', () => { beforeEach(async () => { treasury = await MockShieldedTreasuryStatelessSimulator.create({ contractAddress: TREASURY_ADDRESS, }); - await treasury._deposit(makeCoin(COLOR, AMOUNT)); + Z_RECIPIENT = shieldedTestRecipient(); + const deposited = makeCoin(COLOR, AMOUNT); + await treasury._deposit(deposited); + coin = await getQualifiedShieldedCoinInfo( + contractOwner(treasury), + deposited, + ); }); describe('_send', () => { it('should send the requested amount and return change', async () => { - const result = await treasury._send( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - 400n, - ); + const result = await treasury._send(coin, Z_RECIPIENT, 400n); expect(result.sent.value).toBe(400n); expect(result.sent.color).toStrictEqual(COLOR); expect(result.change.is_some).toBe(true); @@ -60,21 +71,13 @@ describe('ShieldedTreasuryStateless', () => { }); it('should produce no change when sending the full value', async () => { - const result = await treasury._send( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - AMOUNT, - ); + const result = await treasury._send(coin, Z_RECIPIENT, AMOUNT); expect(result.change.is_some).toBe(false); }); it('should fail when amount exceeds coin value', async () => { await expect( - treasury._send( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - AMOUNT + 1n, - ), + treasury._send(coin, Z_RECIPIENT, AMOUNT + 1n), ).rejects.toThrow(); }); }); @@ -87,118 +90,185 @@ describe('ShieldedTreasuryStateless', () => { // the next spend. The dry simulator does not enforce nullifiers, so these // tests read the recorded Zswap I/O: the returned change coin's nonce must not // appear among the spent inputs. - describe('_send — change coin is spendable (no double spend)', () => { - it('should spend the supplied coin and route the change back to itself on a partial send', async () => { - const snap = zswapSnapshot(treasury); - const result = await treasury._send( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - 400n, - ); - const { inputs, outputs } = zswapDelta(treasury, snap); - - // One coin consumed: the supplied coin (nonce 0, full value). - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(AMOUNT); - expect(inputs[0].color).toStrictEqual(COLOR); - expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); - - // Two coins produced: the payment (recipient key, `left` arm) and the - // change (back to this contract, `right`/self arm). - expect(outputs).toHaveLength(2); - const toRecipient = outputs.filter((o) => o.recipient.is_left); - const toSelf = outputs.filter((o) => !o.recipient.is_left); - expect(toRecipient).toHaveLength(1); - expect(toSelf).toHaveLength(1); - - // Payment: 400 of COLOR to the recipient key; equals result.sent. - expect(toRecipient[0].coinInfo.value).toBe(400n); - expect(toRecipient[0].coinInfo).toStrictEqual(result.sent); - expect(toRecipient[0].recipient.left.bytes).toStrictEqual( - Z_RECIPIENT.left.bytes, - ); + // + // Dry-only: `zswapSnapshot`/`zswapDelta` read the dry sim's Zswap local state, + // which does not exist on the live backend. The live counterpart follows. + describe.skipIf(isLiveBackend())( + '_send — change coin is spendable, via Zswap I/O (dry only, no double spend)', + () => { + it('should spend the supplied coin and route the change back to itself on a partial send', async () => { + const snap = zswapSnapshot(treasury); + const result = await treasury._send(coin, Z_RECIPIENT, 400n); + const { inputs, outputs } = zswapDelta(treasury, snap); - // Change: the remainder back to THIS contract's address, identical to - // the returned change coin, and NOT spent in this same tx. - expect(result.change.is_some).toBe(true); - expect(toSelf[0].coinInfo.value).toBe(AMOUNT - 400n); - expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); - expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( - TREASURY_ADDRESS, - ); - expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); - }); + // One coin consumed: the supplied coin (nonce 0, full value). + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(AMOUNT); + expect(inputs[0].color).toStrictEqual(COLOR); + expect(inputs[0].nonce).toStrictEqual(new Uint8Array(32).fill(0)); - it('should spend the coin and produce only the payment when sending in full', async () => { - const snap = zswapSnapshot(treasury); - const result = await treasury._send( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - AMOUNT, - ); - const { inputs, outputs } = zswapDelta(treasury, snap); + // Two coins produced: the payment (recipient key, `left` arm) and the + // change (back to this contract, `right`/self arm). + expect(outputs).toHaveLength(2); + const toRecipient = outputs.filter((o) => o.recipient.is_left); + const toSelf = outputs.filter((o) => !o.recipient.is_left); + expect(toRecipient).toHaveLength(1); + expect(toSelf).toHaveLength(1); - // No change: one input (the supplied coin), one output (the payment). - expect(result.change.is_some).toBe(false); - expect(inputs).toHaveLength(1); - expect(inputs[0].value).toBe(AMOUNT); - expect(outputs).toHaveLength(1); - expect(outputs[0].recipient.is_left).toBe(true); - expect(outputs[0].recipient.left.bytes).toStrictEqual( - Z_RECIPIENT.left.bytes, - ); - expect(outputs[0].coinInfo.value).toBe(AMOUNT); - expect(outputs[0].coinInfo).toStrictEqual(result.sent); - }); - }); + // Payment: 400 of COLOR to the recipient key; equals result.sent. + expect(toRecipient[0].coinInfo.value).toBe(400n); + expect(toRecipient[0].coinInfo).toStrictEqual(result.sent); + expect(toRecipient[0].recipient.left.bytes).toStrictEqual( + Z_RECIPIENT.left.bytes, + ); + + // Change: the remainder back to THIS contract's address, identical to + // the returned change coin, and NOT spent in this same tx. + expect(result.change.is_some).toBe(true); + expect(toSelf[0].coinInfo.value).toBe(AMOUNT - 400n); + expect(toSelf[0].coinInfo).toStrictEqual(result.change.value); + expect(bytesToHex(toSelf[0].recipient.right.bytes)).toBe( + TREASURY_ADDRESS, + ); + expect(isNonceSpent(inputs, result.change.value.nonce)).toBe(false); + }); + + it('should spend the coin and produce only the payment when sending in full', async () => { + const snap = zswapSnapshot(treasury); + const result = await treasury._send(coin, Z_RECIPIENT, AMOUNT); + const { inputs, outputs } = zswapDelta(treasury, snap); + + // No change: one input (the supplied coin), one output (the payment). + expect(result.change.is_some).toBe(false); + expect(inputs).toHaveLength(1); + expect(inputs[0].value).toBe(AMOUNT); + expect(outputs).toHaveLength(1); + expect(outputs[0].recipient.is_left).toBe(true); + expect(outputs[0].recipient.left.bytes).toStrictEqual( + Z_RECIPIENT.left.bytes, + ); + expect(outputs[0].coinInfo.value).toBe(AMOUNT); + expect(outputs[0].coinInfo).toStrictEqual(result.sent); + }); + }, + ); + + // Live counterpart of the block above: the SAME two cases, asserted on what a + // node exposes. Zswap I/O is not readable on live, so the no-double-spend + // proof is the node's own nullifier enforcement — the returned change coin is + // spent in a follow-up `_send` and the node would reject it (Custom error 103) + // if the first send had already nullified it, so the second send SUCCEEDING is + // the proof. + describe.runIf(isLiveBackend())( + '_send — change coin is spendable on live (no double spend)', + () => { + it('should spend the supplied coin and route the change back to itself on a partial send', async () => { + const result = await treasury._send(coin, Z_RECIPIENT, 400n); + + // Payment: 400 of COLOR to the recipient. + expect(result.sent.value).toBe(400n); + expect(result.sent.color).toStrictEqual(COLOR); + + // Change: the remainder handed back as a live, spendable coin. + expect(result.change.is_some).toBe(true); + expect(result.change.value.value).toBe(AMOUNT - 400n); + + // Spend that change coin: recover its index, then send it. A double + // spend would be rejected, so this succeeding proves it stayed spendable. + const change = await getQualifiedShieldedCoinInfo( + contractOwner(treasury), + result.change.value, + ); + const second = await treasury._send(change, Z_RECIPIENT, AMOUNT - 400n); + expect(second.sent.value).toBe(AMOUNT - 400n); + expect(second.change.is_some).toBe(false); + }); + + it('should spend the coin and produce only the payment when sending in full', async () => { + const result = await treasury._send(coin, Z_RECIPIENT, AMOUNT); + expect(result.sent.value).toBe(AMOUNT); + expect(result.change.is_some).toBe(false); + }); + }, + ); // `_send` hands back a live, unspent change coin, so an implementing contract // can spend it onward to a different recipient in the same tx (the only reason // to re-spend change, since `sendShielded` already routes it to self). If // `_send` returned a coin it had already spent, this same-tx re-spend would be // a double spend and fail. - describe('_send — implementing contract routes the change onward', () => { - const CHANGE_DEST = utils.createEitherTestUser('CHANGE_DEST'); - - it('should let the caller send the change to a different recipient using the send result', async () => { - const snap = zswapSnapshot(treasury); - const routed = await treasury._sendAndRouteChange( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - Z_RECIPIENT, - 400n, - CHANGE_DEST, - ); - const { inputs, outputs } = zswapDelta(treasury, snap); - - // The onward send delivered the whole change to `changeRecipient`. - expect(routed.change.is_some).toBe(false); - expect(routed.sent.value).toBe(AMOUNT - 400n); - - // Two coins consumed: the supplied coin, then the change coin (spent to - // route it onward — correct here, unlike the keep-change path). - expect(inputs).toHaveLength(2); - - // The payment still went to the original recipient for `amount`... - const toRecipient = outputs.filter( - (o) => - o.recipient.is_left && - bytesToHex(o.recipient.left.bytes) === - bytesToHex(Z_RECIPIENT.left.bytes), + // + // Dry-only: reads the recorded Zswap I/O (no live counterpart to it below, + // where recipient arms are indistinguishable — both resolve to the deployer). + describe.skipIf(isLiveBackend())( + '_send — implementing contract routes the change onward (dry only)', + () => { + const CHANGE_DEST = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('CHANGE_DEST'), ); - expect(toRecipient).toHaveLength(1); - expect(toRecipient[0].coinInfo.value).toBe(400n); - - // ...and the change was routed onward to `changeRecipient`, matching the - // returned coin. - const toChangeDest = outputs.filter( - (o) => - o.recipient.is_left && - bytesToHex(o.recipient.left.bytes) === - bytesToHex(CHANGE_DEST.left.bytes), - ); - expect(toChangeDest).toHaveLength(1); - expect(toChangeDest[0].coinInfo.value).toBe(AMOUNT - 400n); - expect(toChangeDest[0].coinInfo).toStrictEqual(routed.sent); - }); - }); + + it('should let the caller send the change to a different recipient using the send result', async () => { + const snap = zswapSnapshot(treasury); + const routed = await treasury._sendAndRouteChange( + coin, + Z_RECIPIENT, + 400n, + CHANGE_DEST, + ); + const { inputs, outputs } = zswapDelta(treasury, snap); + + // The onward send delivered the whole change to `changeRecipient`. + expect(routed.change.is_some).toBe(false); + expect(routed.sent.value).toBe(AMOUNT - 400n); + + // Two coins consumed: the supplied coin, then the change coin (spent to + // route it onward — correct here, unlike the keep-change path). + expect(inputs).toHaveLength(2); + + // The payment still went to the original recipient for `amount`... + const toRecipient = outputs.filter( + (o) => + o.recipient.is_left && + bytesToHex(o.recipient.left.bytes) === + bytesToHex(Z_RECIPIENT.left.bytes), + ); + expect(toRecipient).toHaveLength(1); + expect(toRecipient[0].coinInfo.value).toBe(400n); + + // ...and the change was routed onward to `changeRecipient`, matching the + // returned coin. + const toChangeDest = outputs.filter( + (o) => + o.recipient.is_left && + bytesToHex(o.recipient.left.bytes) === + bytesToHex(CHANGE_DEST.left.bytes), + ); + expect(toChangeDest).toHaveLength(1); + expect(toChangeDest[0].coinInfo.value).toBe(AMOUNT - 400n); + expect(toChangeDest[0].coinInfo).toStrictEqual(routed.sent); + }); + }, + ); + + // Live counterpart: the recipient arm/address is not observable on live and + // every deliverable target must be a node-resolvable key (so both the payment + // and change targets are the deployer), so this asserts only the functional + // outcome — the returned change was live enough to be routed fully onward, with + // nothing retained. + describe.runIf(isLiveBackend())( + '_send — implementing contract routes the change onward on live', + () => { + it('should let the caller send the change to a different recipient using the send result', async () => { + const changeDest = shieldedTestRecipient(); + const routed = await treasury._sendAndRouteChange( + coin, + Z_RECIPIENT, + 400n, + changeDest, + ); + expect(routed.change.is_some).toBe(false); + expect(routed.sent.value).toBe(AMOUNT - 400n); + }); + }, + ); }); diff --git a/contracts/src/multisig/test/Signer.test.ts b/contracts/src/multisig/test/Signer.test.ts index da5af3328..95c465b30 100644 --- a/contracts/src/multisig/test/Signer.test.ts +++ b/contracts/src/multisig/test/Signer.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { SignerSimulator } from './simulators/SignerSimulator.js'; const THRESHOLD = 2n; @@ -14,6 +14,11 @@ const OTHER2 = new Uint8Array(32).fill(5); let contract: SignerSimulator; +// A fresh initialized 2-of-3 Signer. Mutating groups build one per test +// (`beforeEach`); read-only groups build one per group (`beforeAll`) to save a +// live deploy tx. +const freshInit = () => SignerSimulator.create(SIGNERS, THRESHOLD, IS_INIT); + describe('Signer', () => { describe('when not initialized', () => { beforeEach(async () => { @@ -92,11 +97,11 @@ describe('Signer', () => { }); }); - beforeEach(async () => { - contract = await SignerSimulator.create(SIGNERS, THRESHOLD, IS_INIT); - }); - describe('assertSigner', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should pass with good signer', async () => { await contract.assertSigner(SIGNER); }); @@ -109,6 +114,10 @@ describe('Signer', () => { }); describe('assertThresholdMet', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should pass when approvals equal threshold', async () => { await contract.assertThresholdMet(THRESHOLD); }); @@ -131,6 +140,12 @@ describe('Signer', () => { }); describe('getSignerCount', () => { + // Mixed: one read plus two mutating (`_addSigner`/`_removeSigner`) tests, so + // each test needs its own fresh contract. + beforeEach(async () => { + contract = await freshInit(); + }); + it('should return the initial signer count', async () => { expect(await contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); }); @@ -151,6 +166,12 @@ describe('Signer', () => { }); describe('getThreshold', () => { + // Mixed: one read plus two mutating (`_changeThreshold`/`_setThreshold`) + // tests, so each test needs its own fresh contract. + beforeEach(async () => { + contract = await freshInit(); + }); + it('should return the initial threshold', async () => { expect(await contract.getThreshold()).toEqual(THRESHOLD); }); @@ -167,6 +188,10 @@ describe('Signer', () => { }); describe('isSigner', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should return true for an active signer', async () => { expect(await contract.isSigner(SIGNER)).toEqual(true); }); @@ -177,6 +202,10 @@ describe('Signer', () => { }); describe('_addSigner', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should add a new signer', async () => { await contract._addSigner(OTHER); @@ -217,6 +246,10 @@ describe('Signer', () => { }); describe('_removeSigner', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should remove an existing signer', async () => { await contract._removeSigner(SIGNER3); @@ -267,6 +300,10 @@ describe('Signer', () => { }); describe('_changeThreshold', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should update the threshold', async () => { await contract._changeThreshold(3n); diff --git a/contracts/src/multisig/test/SignerManager.test.ts b/contracts/src/multisig/test/SignerManager.test.ts index 9ecd24684..5c3cbf2d5 100644 --- a/contracts/src/multisig/test/SignerManager.test.ts +++ b/contracts/src/multisig/test/SignerManager.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import * as utils from '#test-utils/fixtures/address.js'; import { SignerManagerSimulator, type SignerSet, @@ -16,6 +16,11 @@ const [_OTHER2, Z_OTHER2] = utils.generateEitherPubKeyPair('OTHER2'); let contract: SignerManagerSimulator; +// A fresh 2-of-3 SignerManager. Mutating groups build one per test +// (`beforeEach`); read-only groups build one per group (`beforeAll`) to save a +// live deploy tx. +const freshInit = () => SignerManagerSimulator.create(SIGNERS, THRESHOLD); + describe('SigningManager', () => { describe('initialization', () => { it('should fail with a threshold of zero', async () => { @@ -45,11 +50,11 @@ describe('SigningManager', () => { }); }); - beforeEach(async () => { - contract = await SignerManagerSimulator.create(SIGNERS, THRESHOLD); - }); - describe('assertSigner', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should pass with good signer', async () => { await contract.assertSigner(Z_SIGNER); }); @@ -62,6 +67,10 @@ describe('SigningManager', () => { }); describe('assertThresholdMet', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should pass when approvals equal threshold', async () => { await contract.assertThresholdMet(THRESHOLD); }); @@ -84,6 +93,10 @@ describe('SigningManager', () => { }); describe('isSigner', () => { + beforeAll(async () => { + contract = await freshInit(); + }); + it('should return true for an active signer', async () => { expect(await contract.isSigner(Z_SIGNER)).toEqual(true); }); @@ -94,6 +107,10 @@ describe('SigningManager', () => { }); describe('_addSigner', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should add a new signer', async () => { await contract._addSigner(Z_OTHER); @@ -122,6 +139,10 @@ describe('SigningManager', () => { }); describe('_removeSigner', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should remove an existing signer', async () => { await contract._removeSigner(Z_SIGNER3); @@ -160,6 +181,10 @@ describe('SigningManager', () => { }); describe('_changeThreshold', () => { + beforeEach(async () => { + contract = await freshInit(); + }); + it('should update the threshold', async () => { await contract._changeThreshold(3n); diff --git a/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts b/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts index ba87b3fd3..245c436a2 100644 --- a/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts @@ -1,26 +1,29 @@ import { describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import { + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, + encodeShieldedCoinInfo as makeCoin, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js'; +import { + contractOwner, + getQualifiedShieldedCoinInfo, +} from '#test-utils/harness/NativeShieldedTokenTracker.js'; import { ForwarderPrivateSimulator } from '../simulators/presets/ForwarderPrivateSimulator.js'; -const PARENT_BYTES = utils.createEitherTestUser('PARENT').left.bytes; +// The drain parent is a `ZswapCoinPublicKey` (`{ bytes }`); the commitment is +// over its raw 32 bytes (`calculateParentCommitment(parent.bytes, opSecret)`). +// On live it is the deployer's own key (the drain sends the note to it, so its +// encryption key must resolve on-chain). +const PARENT_BYTES = shieldedTestParentKey().bytes; const OP_SECRET = new Uint8Array(32).fill(0xaa); -const COLOR = new Uint8Array(32).fill(1); +// A shielded token type the deployer wallet holds on live (genesis-minted). +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; -// The drain parent is a `ZswapCoinPublicKey` (`{ bytes }`); the commitment is -// over its raw 32 bytes (`calculateParentCommitment(parent.bytes, opSecret)`). function key(bytes: Uint8Array) { return { bytes }; } -function makeCoin(color: Uint8Array, value: bigint) { - return { nonce: new Uint8Array(32), color, value }; -} - -function makeQualifiedCoin(color: Uint8Array, value: bigint, mtIndex: bigint) { - return { nonce: new Uint8Array(32), color, value, mt_index: mtIndex }; -} - function commitment(parent: Uint8Array, opSecret: Uint8Array): Uint8Array { return ForwarderPrivateSimulator.calculateParentCommitment(parent, opSecret); } @@ -43,13 +46,15 @@ describe('ForwarderPrivate preset', () => { const fwd = await ForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), ); - await fwd.deposit(makeCoin(COLOR, AMOUNT)); - const result = await fwd.drain( - makeQualifiedCoin(COLOR, AMOUNT, 0n), - key(PARENT_BYTES), - OP_SECRET, - AMOUNT, + // Deposit a real coin, then recover its `mt_index` from the coin tracker + // (the contract keeps no record of it) before spending it. + const deposited = makeCoin(COLOR, AMOUNT); + await fwd.deposit(deposited); + const coin = await getQualifiedShieldedCoinInfo( + contractOwner(fwd), + deposited, ); + const result = await fwd.drain(coin, key(PARENT_BYTES), OP_SECRET, AMOUNT); expect(result.sent.value).toEqual(AMOUNT); }); diff --git a/contracts/src/multisig/test/presets/ForwarderShielded.test.ts b/contracts/src/multisig/test/presets/ForwarderShielded.test.ts index 144450726..533dd1612 100644 --- a/contracts/src/multisig/test/presets/ForwarderShielded.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderShielded.test.ts @@ -1,20 +1,25 @@ import { describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; +import { + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, + encodeShieldedCoinInfo as makeCoin, +} from '#test-utils/fixtures/nativeShieldedToken.js'; +import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js'; import { ForwarderShieldedSimulator } from '../simulators/presets/ForwarderShieldedSimulator.js'; // The constructor takes a `ZswapCoinPublicKey` (the supported arm). The // `_parent` ledger field stays a generic `Either`; `initialize` stores the key // in the `left` arm, which is what `getParent` reads back. A contract-address // parent is not expressible today (see the module header). -const PARENT = utils.createEitherTestUser('PARENT').left; +// +// Live: the parent is the deployer's own key (the deposit forwards the coin to +// it, so its encryption key must resolve on-chain). +const PARENT = shieldedTestParentKey(); const ZERO_KEY = utils.ZERO_KEY.left; -const COLOR = new Uint8Array(32).fill(1); +// A shielded token type the deployer wallet holds on live (genesis-minted). +const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1; const AMOUNT = 1000n; -function makeCoin(color: Uint8Array, value: bigint) { - return { nonce: new Uint8Array(32), color, value }; -} - describe('ForwarderShielded preset', () => { it('should store the parent passed to the constructor in the left arm', async () => { const fwd = await ForwarderShieldedSimulator.create(PARENT); diff --git a/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts b/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts index 0f3ade64b..48be56965 100644 --- a/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts @@ -1,14 +1,18 @@ +import { isLiveBackend } from '@openzeppelin/compact-simulator'; import { describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { ForwarderUnshieldedSimulator } from '../simulators/presets/ForwarderUnshieldedSimulator.js'; // The constructor takes a `UserAddress` (the supported arm). The `_parent` // ledger field stays a generic `Either`; `initialize` stores the address in the // `right` arm, which is what `getParent` reads back. A contract-address parent -// is not expressible today (see the module header). +// is not expressible today (see the module header). An unshielded recipient is +// a public address, so the parent stays synthetic on live (no encryption key). const PARENT = utils.createEitherTestUserAddress('PARENT').right; const ZERO_ADDR = utils.ZERO_USER_ADDRESS.right; -const COLOR = new Uint8Array(32).fill(1); +// On live the deployer wallet only holds the native unshielded token +// (`0x00…00`), so the forward has funds to draw; dry mints any color freely. +const COLOR = isLiveBackend() ? new Uint8Array(32) : new Uint8Array(32).fill(1); const AMOUNT = 1000n; describe('ForwarderUnshielded preset', () => { diff --git a/contracts/src/multisig/test/simulators/MockShieldedTreasuryStatelessSimulator.ts b/contracts/src/multisig/test/simulators/MockShieldedTreasuryStatelessSimulator.ts index dfff1ad68..f7fe4383c 100644 --- a/contracts/src/multisig/test/simulators/MockShieldedTreasuryStatelessSimulator.ts +++ b/contracts/src/multisig/test/simulators/MockShieldedTreasuryStatelessSimulator.ts @@ -6,10 +6,7 @@ import { ledger, Contract as MockShieldedTreasuryStateless, } from '../../../../artifacts/MockShieldedTreasuryStateless/contract/index.js'; -import { - ShieldedTreasuryPrivateState, - ShieldedTreasuryWitnesses, -} from '../witnesses/ShieldedTreasuryWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type ShieldedCoinInfo = { nonce: Uint8Array; color: Uint8Array; value: bigint }; type QualifiedShieldedCoinInfo = ShieldedCoinInfo & { mt_index: bigint }; @@ -21,26 +18,26 @@ type ShieldedSendResult = { type ShieldedTreasuryStatelessArgs = readonly []; const MockShieldedTreasuryStatelessSimulatorBase = createSimulator< - ShieldedTreasuryPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - MockShieldedTreasuryStateless, + ReturnType, + MockShieldedTreasuryStateless, ShieldedTreasuryStatelessArgs >({ contractFactory: (witnesses) => - new MockShieldedTreasuryStateless(witnesses), - defaultPrivateState: () => ShieldedTreasuryPrivateState, + new MockShieldedTreasuryStateless(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: () => [], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedTreasuryWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'MockShieldedTreasuryStateless', }); export class MockShieldedTreasuryStatelessSimulator extends MockShieldedTreasuryStatelessSimulatorBase { static async create( options: SimulatorOptions< - ShieldedTreasuryPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts b/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts index 98c97436e..8f57d22f5 100644 --- a/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts +++ b/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts @@ -7,10 +7,7 @@ import { Contract as MockProposalManager, pureCircuits, } from '../../../../artifacts/MockProposalManager/contract/index.js'; -import { - ProposalManagerPrivateState, - ProposalManagerWitnesses, -} from '../witnesses/ProposalManagerWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type Recipient = { kind: number; address: Uint8Array }; type Proposal = { @@ -23,26 +20,26 @@ type Proposal = { type ProposalManagerArgs = readonly []; const ProposalManagerSimulatorBase = createSimulator< - ProposalManagerPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - MockProposalManager, + ReturnType, + MockProposalManager, ProposalManagerArgs >({ contractFactory: (witnesses) => - new MockProposalManager(witnesses), - defaultPrivateState: () => ProposalManagerPrivateState, + new MockProposalManager(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: () => [], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ProposalManagerWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'MockProposalManager', }); export class ProposalManagerSimulator extends ProposalManagerSimulatorBase { static async create( options: SimulatorOptions< - ProposalManagerPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts index a58035f4c..f4f8b6224 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts @@ -7,10 +7,7 @@ import { ledger, Contract as ShieldedMultiSig, } from '../../../../artifacts/ShieldedMultiSig/contract/index.js'; -import { - ShieldedMultiSigPrivateState, - ShieldedMultiSigWitnesses, -} from '../witnesses/ShieldedMultiSigWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type EitherPKAddress = { is_left: boolean; @@ -36,18 +33,18 @@ type ShieldedMultiSigArgs = readonly [ ]; const ShieldedMultiSigSimulatorBase = createSimulator< - ShieldedMultiSigPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - ShieldedMultiSig, + ReturnType, + ShieldedMultiSig, ShieldedMultiSigArgs >({ contractFactory: (witnesses) => - new ShieldedMultiSig(witnesses), - defaultPrivateState: () => ShieldedMultiSigPrivateState, + new ShieldedMultiSig(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: (signers, thresh) => [signers, thresh], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedMultiSigWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'ShieldedMultiSig', }); @@ -56,8 +53,8 @@ export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { signers: EitherPKAddress[], thresh: bigint, options: SimulatorOptions< - ShieldedMultiSigPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` @@ -110,16 +107,19 @@ export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { return this.circuits.impure.getProposal(id); } - public getProposalRecipient(id: bigint): Promise { - return this.circuits.impure.getProposalRecipient(id); + // getProposalRecipient / getProposalAmount / getProposalColor were dropped from + // the contract (redundant with getProposal; removed to fit the deploy block + // limit). Derive them here so specs are unchanged. + public async getProposalRecipient(id: bigint): Promise { + return (await this.getProposal(id)).to; } - public getProposalAmount(id: bigint): Promise { - return this.circuits.impure.getProposalAmount(id); + public async getProposalAmount(id: bigint): Promise { + return (await this.getProposal(id)).amount; } - public getProposalColor(id: bigint): Promise { - return this.circuits.impure.getProposalColor(id); + public async getProposalColor(id: bigint): Promise { + return (await this.getProposal(id)).color; } public getProposalStatus(id: bigint): Promise { @@ -139,8 +139,15 @@ export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { return this.circuits.impure.getSentTotal(color); } - public getReceivedMinusSent(color: Uint8Array): Promise { - return this.circuits.impure.getReceivedMinusSent(color); + // getReceivedMinusSent was dropped from the contract (redundant; removed to fit + // the deploy block limit). Derive it from the two tracked totals. + public async getReceivedMinusSent(color: Uint8Array): Promise { + // Await sequentially: on live each impure getter submits a tx, and two + // concurrent submissions balance against the same wallet snapshot and + // trigger a stale-UTXO rejection. + const received = await this.getReceivedTotal(color); + const sent = await this.getSentTotal(color); + return received - sent; } // View - Signers diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts index c03078bd9..22fdfd0e5 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts @@ -8,10 +8,7 @@ import { pureCircuits, Contract as ShieldedMultiSigV2, } from '../../../../artifacts/ShieldedMultiSigV2/contract/index.js'; -import { - ShieldedMultiSigV2PrivateState, - ShieldedMultiSigV2Witnesses, -} from '../witnesses/ShieldedMultiSigV2Witnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type Recipient = { kind: number; address: Uint8Array }; type ShieldedCoinInfo = { nonce: Uint8Array; color: Uint8Array; value: bigint }; @@ -33,22 +30,22 @@ type ShieldedMultiSigV2Args = readonly [ ]; const ShieldedMultiSigV2SimulatorBase = createSimulator< - ShieldedMultiSigV2PrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - ShieldedMultiSigV2, + ReturnType, + ShieldedMultiSigV2, ShieldedMultiSigV2Args >({ contractFactory: (witnesses) => - new ShieldedMultiSigV2(witnesses), - defaultPrivateState: () => ShieldedMultiSigV2PrivateState, + new ShieldedMultiSigV2(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: (instanceSalt, signerCommitments, thresh) => [ instanceSalt, signerCommitments, thresh, ], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedMultiSigV2Witnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'ShieldedMultiSigV2', }); @@ -58,8 +55,8 @@ export class ShieldedMultiSigV2Simulator extends ShieldedMultiSigV2SimulatorBase signerCommitments: Uint8Array[], thresh: bigint, options: SimulatorOptions< - ShieldedMultiSigV2PrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts index afba649fd..072e7875b 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts @@ -10,10 +10,7 @@ import { Contract as ShieldedMultiSigV3Contract, type ZswapCoinPublicKey, } from '../../../../artifacts/ShieldedMultiSigV3/contract/index.js'; -import { - ShieldedMultiSigV3PrivateState, - ShieldedMultiSigV3Witnesses, -} from '../witnesses/ShieldedMultiSigV3Witnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type ShieldedMultiSigV3Args = readonly [ instanceSalt: Uint8Array, @@ -23,15 +20,15 @@ type ShieldedMultiSigV3Args = readonly [ ]; const ShieldedMultiSigV3SimulatorBase = createSimulator< - ShieldedMultiSigV3PrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - ShieldedMultiSigV3Contract, + ReturnType, + ShieldedMultiSigV3Contract, ShieldedMultiSigV3Args >({ contractFactory: (witnesses) => - new ShieldedMultiSigV3Contract(witnesses), - defaultPrivateState: () => ShieldedMultiSigV3PrivateState, + new ShieldedMultiSigV3Contract(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: ( instanceSalt, initCoinNonce, @@ -39,7 +36,7 @@ const ShieldedMultiSigV3SimulatorBase = createSimulator< signerCommitments, ) => [instanceSalt, initCoinNonce, tokenDomain, signerCommitments], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedMultiSigV3Witnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'ShieldedMultiSigV3', }); @@ -50,8 +47,8 @@ export class ShieldedMultiSigV3Simulator extends ShieldedMultiSigV3SimulatorBase tokenDomain: Uint8Array, signerCommitments: Uint8Array[], options: SimulatorOptions< - ShieldedMultiSigV3PrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts b/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts index 210972d25..001c94e98 100644 --- a/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts @@ -6,10 +6,7 @@ import { ledger, Contract as MockShieldedTreasury, } from '../../../../artifacts/MockShieldedTreasury/contract/index.js'; -import { - ShieldedTreasuryPrivateState, - ShieldedTreasuryWitnesses, -} from '../witnesses/ShieldedTreasuryWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; type ShieldedCoinInfo = { nonce: Uint8Array; color: Uint8Array; value: bigint }; type ShieldedSendResult = { @@ -20,26 +17,26 @@ type ShieldedSendResult = { type ShieldedTreasuryArgs = readonly []; const ShieldedTreasurySimulatorBase = createSimulator< - ShieldedTreasuryPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - MockShieldedTreasury, + ReturnType, + MockShieldedTreasury, ShieldedTreasuryArgs >({ contractFactory: (witnesses) => - new MockShieldedTreasury(witnesses), - defaultPrivateState: () => ShieldedTreasuryPrivateState, + new MockShieldedTreasury(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: () => [], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ShieldedTreasuryWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'MockShieldedTreasury', }); export class ShieldedTreasurySimulator extends ShieldedTreasurySimulatorBase { static async create( options: SimulatorOptions< - ShieldedTreasuryPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts b/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts index be5ee9aac..2b135ef12 100644 --- a/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts +++ b/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts @@ -9,10 +9,7 @@ import { Contract as MockSignerManager, type ZswapCoinPublicKey, } from '../../../../artifacts/MockSignerManager/contract/index.js'; -import { - SignerManagerPrivateState, - SignerManagerWitnesses, -} from '../witnesses/SignerManagerWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; /** * A fixed set of exactly three signers, matching the @@ -31,18 +28,18 @@ export type SignerSet = readonly [ type SignerManagerArgs = readonly [signers: SignerSet, thresh: bigint]; const SignerManagerSimulatorBase = createSimulator< - SignerManagerPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - MockSignerManager, + ReturnType, + MockSignerManager, SignerManagerArgs >({ contractFactory: (witnesses) => - new MockSignerManager(witnesses), - defaultPrivateState: () => SignerManagerPrivateState, + new MockSignerManager(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: (signers, thresh) => [signers, thresh], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => SignerManagerWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'MockSignerManager', }); @@ -54,8 +51,8 @@ export class SignerManagerSimulator extends SignerManagerSimulatorBase { signers: SignerSet, thresh: bigint, options: SimulatorOptions< - SignerManagerPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/simulators/SignerSimulator.ts b/contracts/src/multisig/test/simulators/SignerSimulator.ts index ef3f3948a..bbd245706 100644 --- a/contracts/src/multisig/test/simulators/SignerSimulator.ts +++ b/contracts/src/multisig/test/simulators/SignerSimulator.ts @@ -6,10 +6,7 @@ import { ledger, Contract as MockSigner, } from '../../../../artifacts/MockSigner/contract/index.js'; -import { - SignerPrivateState, - SignerWitnesses, -} from '../witnesses/SignerWitnesses.js'; +import { EmptyPrivateState, emptyWitnesses } from '../EmptyWitnesses.js'; /** * Type constructor args @@ -21,17 +18,17 @@ type SignerArgs = readonly [ ]; const SignerSimulatorBase = createSimulator< - SignerPrivateState, + EmptyPrivateState, ReturnType, - ReturnType, - MockSigner, + ReturnType, + MockSigner, SignerArgs >({ - contractFactory: (witnesses) => new MockSigner(witnesses), - defaultPrivateState: () => SignerPrivateState, + contractFactory: (witnesses) => new MockSigner(witnesses), + defaultPrivateState: () => EmptyPrivateState, contractArgs: (signers, thresh, isInit) => [signers, thresh, isInit], ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => SignerWitnesses(), + witnessesFactory: () => emptyWitnesses(), artifactName: 'MockSigner', }); @@ -44,8 +41,8 @@ export class SignerSimulator extends SignerSimulatorBase { thresh: bigint, isInit: boolean, options: SimulatorOptions< - SignerPrivateState, - ReturnType + EmptyPrivateState, + ReturnType > = {}, ): Promise { // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` diff --git a/contracts/src/multisig/test/witnesses/ProposalManagerWitnesses.ts b/contracts/src/multisig/test/witnesses/ProposalManagerWitnesses.ts deleted file mode 100644 index 7676d0d1a..000000000 --- a/contracts/src/multisig/test/witnesses/ProposalManagerWitnesses.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/ProposalManagerWitnesses.ts) - -export type ProposalManagerPrivateState = Record; -export const ProposalManagerPrivateState: ProposalManagerPrivateState = {}; -export const ProposalManagerWitnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/ShieldedMultiSigV2Witnesses.ts b/contracts/src/multisig/test/witnesses/ShieldedMultiSigV2Witnesses.ts deleted file mode 100644 index 2df105c95..000000000 --- a/contracts/src/multisig/test/witnesses/ShieldedMultiSigV2Witnesses.ts +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/ShieldedMultiSigV2Witnesses.ts) - -export type ShieldedMultiSigV2PrivateState = Record; -export const ShieldedMultiSigV2PrivateState: ShieldedMultiSigV2PrivateState = - {}; -export const ShieldedMultiSigV2Witnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/ShieldedMultiSigV3Witnesses.ts b/contracts/src/multisig/test/witnesses/ShieldedMultiSigV3Witnesses.ts deleted file mode 100644 index f726fb96c..000000000 --- a/contracts/src/multisig/test/witnesses/ShieldedMultiSigV3Witnesses.ts +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (multisig/witnesses/ShieldedMultiSigV3Witnesses.ts) - -export type ShieldedMultiSigV3PrivateState = Record; -export const ShieldedMultiSigV3PrivateState: ShieldedMultiSigV3PrivateState = - {}; -export const ShieldedMultiSigV3Witnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/ShieldedMultiSigWitnesses.ts b/contracts/src/multisig/test/witnesses/ShieldedMultiSigWitnesses.ts deleted file mode 100644 index d18cd56ff..000000000 --- a/contracts/src/multisig/test/witnesses/ShieldedMultiSigWitnesses.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/ShieldedMultiSigWitnesses.ts) - -export type ShieldedMultiSigPrivateState = Record; -export const ShieldedMultiSigPrivateState: ShieldedMultiSigPrivateState = {}; -export const ShieldedMultiSigWitnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/ShieldedTreasuryWitnesses.ts b/contracts/src/multisig/test/witnesses/ShieldedTreasuryWitnesses.ts deleted file mode 100644 index 5c09ac230..000000000 --- a/contracts/src/multisig/test/witnesses/ShieldedTreasuryWitnesses.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/ShieldedTreasuryWitnesses.ts) - -export type ShieldedTreasuryPrivateState = Record; -export const ShieldedTreasuryPrivateState: ShieldedTreasuryPrivateState = {}; -export const ShieldedTreasuryWitnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/SignerManagerWitnesses.ts b/contracts/src/multisig/test/witnesses/SignerManagerWitnesses.ts deleted file mode 100644 index 087a78e0d..000000000 --- a/contracts/src/multisig/test/witnesses/SignerManagerWitnesses.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/SignerManagerWitnesses.ts) - -export type SignerManagerPrivateState = Record; -export const SignerManagerPrivateState: SignerManagerPrivateState = {}; -export const SignerManagerWitnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/SignerWitnesses.ts b/contracts/src/multisig/test/witnesses/SignerWitnesses.ts deleted file mode 100644 index ea8d11c12..000000000 --- a/contracts/src/multisig/test/witnesses/SignerWitnesses.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/SignerWitnesses.ts) - -export type SignerPrivateState = Record; -export const SignerPrivateState: SignerPrivateState = {}; -export const SignerWitnesses = () => ({}); diff --git a/contracts/src/multisig/test/witnesses/UnshieldedTreasuryWitnesses.ts b/contracts/src/multisig/test/witnesses/UnshieldedTreasuryWitnesses.ts deleted file mode 100644 index bd2b0e566..000000000 --- a/contracts/src/multisig/test/witnesses/UnshieldedTreasuryWitnesses.ts +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/witnesses/UnshieldedTreasuryWitnesses.ts) - -export type UnshieldedTreasuryPrivateState = Record; -export const UnshieldedTreasuryPrivateState: UnshieldedTreasuryPrivateState = - {}; -export const UnshieldedTreasuryWitnesses = () => ({}); diff --git a/contracts/src/token/test/FungibleToken.test.ts b/contracts/src/token/test/FungibleToken.test.ts index 108de8b3e..51924e5fb 100644 --- a/contracts/src/token/test/FungibleToken.test.ts +++ b/contracts/src/token/test/FungibleToken.test.ts @@ -4,7 +4,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { FungibleTokenSimulator } from './simulators/FungibleTokenSimulator.js'; // Helpers diff --git a/contracts/src/token/test/MultiToken.test.ts b/contracts/src/token/test/MultiToken.test.ts index a7376e5ee..77087b797 100644 --- a/contracts/src/token/test/MultiToken.test.ts +++ b/contracts/src/token/test/MultiToken.test.ts @@ -4,7 +4,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import type { Maybe } from '../../../artifacts/MockMultiToken/contract/index.js'; import { MultiTokenSimulator } from './simulators/MultiTokenSimulator.js'; diff --git a/contracts/src/token/test/NativeShieldedToken.test.ts b/contracts/src/token/test/NativeShieldedToken.test.ts index 962e2fe0f..04dbcb2a0 100644 --- a/contracts/src/token/test/NativeShieldedToken.test.ts +++ b/contracts/src/token/test/NativeShieldedToken.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { NativeShieldedTokenSimulator, type NativeShieldedTokenSimulator as Sim, @@ -13,9 +13,13 @@ const b32 = (label: string): Uint8Array => { }; // Users / recipients -const RECIPIENT = utils.createEitherTestUser('RECIPIENT'); +const RECIPIENT = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('RECIPIENT'), +); const RECIPIENT_CONTRACT = utils.createEitherTestContractAddress('RECIPIENT_C'); -const REFUND_TO = utils.createEitherTestUser('REFUND_TO'); +const REFUND_TO = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('REFUND_TO'), +); const { ZERO_KEY, ZERO_ADDRESS } = utils; // Metadata diff --git a/contracts/src/token/test/NativeShieldedTokenCore.test.ts b/contracts/src/token/test/NativeShieldedTokenCore.test.ts index ea8734e6e..8573053ed 100644 --- a/contracts/src/token/test/NativeShieldedTokenCore.test.ts +++ b/contracts/src/token/test/NativeShieldedTokenCore.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { NativeShieldedTokenCoreSimulator, type NativeShieldedTokenCoreSimulator as Sim, @@ -11,8 +11,12 @@ const b32 = (label: string): Uint8Array => { return u; }; -const RECIPIENT = utils.createEitherTestUser('RECIPIENT'); -const REFUND_TO = utils.createEitherTestUser('REFUND_TO'); +const RECIPIENT = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('RECIPIENT'), +); +const REFUND_TO = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('REFUND_TO'), +); const { ZERO_KEY, ZERO_ADDRESS } = utils; const NAME = 'Core Token'; diff --git a/contracts/src/token/test/NativeShieldedTokenFamily.test.ts b/contracts/src/token/test/NativeShieldedTokenFamily.test.ts index db5ba0cc5..ae63ba74d 100644 --- a/contracts/src/token/test/NativeShieldedTokenFamily.test.ts +++ b/contracts/src/token/test/NativeShieldedTokenFamily.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { NativeShieldedTokenFamilySimulator, type NativeShieldedTokenFamilySimulator as Sim, @@ -11,8 +11,12 @@ const b32 = (label: string): Uint8Array => { return u; }; -const RECIPIENT = utils.createEitherTestUser('RECIPIENT'); -const REFUND_TO = utils.createEitherTestUser('REFUND_TO'); +const RECIPIENT = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('RECIPIENT'), +); +const REFUND_TO = utils.eitherUserFromCoinPublicKey( + utils.toHexPadded('REFUND_TO'), +); const { ZERO_KEY, ZERO_ADDRESS } = utils; const NAME = 'Family Token'; diff --git a/contracts/src/token/test/nonFungibleToken.test.ts b/contracts/src/token/test/nonFungibleToken.test.ts index 83a6c0a43..d4786fc3d 100644 --- a/contracts/src/token/test/nonFungibleToken.test.ts +++ b/contracts/src/token/test/nonFungibleToken.test.ts @@ -4,7 +4,7 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as utils from '#test-utils/address.js'; +import * as utils from '#test-utils/fixtures/address.js'; import { NonFungibleTokenSimulator } from './simulators/NonFungibleTokenSimulator.js'; // Helpers diff --git a/contracts/src/utils/test/utils.test.ts b/contracts/src/utils/test/utils.test.ts index ed2db8097..8e44282a6 100644 --- a/contracts/src/utils/test/utils.test.ts +++ b/contracts/src/utils/test/utils.test.ts @@ -4,11 +4,15 @@ import { persistentHash, } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as contractUtils from '#test-utils/address.js'; +import * as contractUtils from '#test-utils/fixtures/address.js'; import { UtilsSimulator } from './simulators/UtilsSimulator.js'; -const Z_SOME_KEY = contractUtils.createEitherTestUser('SOME_KEY'); -const Z_OTHER_KEY = contractUtils.createEitherTestUser('OTHER_KEY'); +const Z_SOME_KEY = contractUtils.eitherUserFromCoinPublicKey( + contractUtils.toHexPadded('SOME_KEY'), +); +const Z_OTHER_KEY = contractUtils.eitherUserFromCoinPublicKey( + contractUtils.toHexPadded('OTHER_KEY'), +); const SOME_CONTRACT = contractUtils.createEitherTestContractAddress('SOME_CONTRACT'); const OTHER_CONTRACT = diff --git a/contracts/test-utils/address.ts b/contracts/test-utils/fixtures/address.ts similarity index 87% rename from contracts/test-utils/address.ts rename to contracts/test-utils/fixtures/address.ts index e2ae9b0aa..126608a78 100644 --- a/contracts/test-utils/address.ts +++ b/contracts/test-utils/fixtures/address.ts @@ -53,16 +53,19 @@ export const encodeToAddress = (str: string): EncodedContractAddress => { }; /** - * @description Generates an Either object for ZswapCoinPublicKey for testing. - * For use when an Either argument is expected. - * @param str String to hexify and encode. - * @returns Defined Either object for ZswapCoinPublicKey. + * @description Builds an `Either` bound to a + * real coin public key (a 64-char hex string, e.g. a live wallet's + * `getCoinPublicKey()`) instead of a hashed test string. The live backend uses + * this so shielded sends target a recipient whose encryption key the node can + * resolve (a fabricated key from a hashed test label has none). + * @param coinPublicKey 64-char hex coin public key. + * @returns Defined Either object for the given ZswapCoinPublicKey. */ -export const createEitherTestUser = ( - str: string, +export const eitherUserFromCoinPublicKey = ( + coinPublicKey: string, ): Either => ({ is_left: true, - left: encodeToPK(str), + left: { bytes: encodeCoinPublicKey(coinPublicKey) }, right: encodeToAddress(''), }); @@ -86,7 +89,9 @@ const baseGeneratePubKeyPair = ( ZswapCoinPublicKey | Either, ] => { const pk = toHexPadded(str); - const zpk = asEither ? createEitherTestUser(str) : encodeToPK(str); + const zpk = asEither + ? eitherUserFromCoinPublicKey(toHexPadded(str)) + : encodeToPK(str); return [pk, zpk]; }; diff --git a/contracts/test-utils/fixtures/nativeShieldedToken.ts b/contracts/test-utils/fixtures/nativeShieldedToken.ts new file mode 100644 index 000000000..ad77157f3 --- /dev/null +++ b/contracts/test-utils/fixtures/nativeShieldedToken.ts @@ -0,0 +1,67 @@ +import { randomBytes } from 'node:crypto'; +// Compact's byte-encoded coin representation (the one the compiled circuits +// accept) — distinct from the runtime `ShieldedCoinInfo`, whose fields are +// `type`/`string`. +import type { EncodedShieldedCoinInfo } from '@midnight-ntwrk/compact-runtime'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; + +/** + * Backend-aware native-shielded-token fixtures, so one spec runs unchanged on + * both `MIDNIGHT_BACKEND=dry` and `=live`. The backend is read via the + * simulator's `isLiveBackend()` (no live-harness import), so a dry spec importing + * this stays lean. Terminology follows MIP-0011 (Native Shielded Token Standard): + * a token's identifier is its `tokenColor` (`tokenType(domain, contractAddress)`). + * Exports keep the `NativeShieldedToken` prefix so a future unshielded analog can + * add parallel `NativeUnshieldedToken*` fixtures without name clashes. + * See {@link shieldedKey} for the matching recipient-key fixtures. + */ + +/** + * A shielded token color the dev-preset genesis mints to every deployer wallet + * (seeds `0x..0001`–`0x..0004`): colors `0x..01` and `0x..02` each carry ~5e13. + * On the live backend `_deposit` / `_send` can only fund a color the wallet + * actually holds, so specs must build coins with these — `new Uint8Array(32).fill(1)` + * (`0x0101..01`) is a different, unfunded color. On dry the color is arbitrary, so + * the same spec passes. + * + * @param lastByte The final byte of the 32-byte color (`1` → `0x..01`). + */ +export const genesisNativeShieldedTokenColor = ( + lastByte: number, +): Uint8Array => { + const bytes = new Uint8Array(32); + bytes[31] = lastByte; + return bytes; +}; + +/** + * The canonical set of shielded token colors the dev-preset genesis actually funds + * on every deployer wallet (seeds `0x..0001`–`0x..0004`). These are the only + * colors a live `_deposit` / `_send` can draw on — any other color is unfunded + * and reverts — so specs pick from here instead of guessing a `lastByte`. + */ +export const GENESIS_NATIVE_SHIELDED_TOKEN_COLORS = { + /** Funded shielded token color carrying ~5e13, `0x..01`. */ + nativeShieldedToken1: genesisNativeShieldedTokenColor(1), + /** Funded shielded token color carrying ~5e13, `0x..02`. */ + nativeShieldedToken2: genesisNativeShieldedTokenColor(2), +} as const; + +/** + * Builds an `EncodedShieldedCoinInfo` for a spec that runs on both backends. On live, + * every coin gets a unique random nonce per run: the local node persists + * nullifiers across runs, so a fixed nonce would replay an already-spent coin + * (`Custom error: 103`) and mask real failures. On dry the nonce is + * deterministic (the passed `nonce`, else zero) so assertions stay reproducible. + */ +export const encodeShieldedCoinInfo = ( + color: Uint8Array, + value: bigint, + nonce?: Uint8Array, +): EncodedShieldedCoinInfo => ({ + nonce: isLiveBackend() + ? Uint8Array.from(randomBytes(32)) + : (nonce ?? new Uint8Array(32).fill(0)), + color, + value, +}); diff --git a/contracts/test-utils/fixtures/shieldedKey.ts b/contracts/test-utils/fixtures/shieldedKey.ts new file mode 100644 index 000000000..e80f2a52c --- /dev/null +++ b/contracts/test-utils/fixtures/shieldedKey.ts @@ -0,0 +1,79 @@ +// Compact's byte-encoded recipient representation (the one the compiled circuits +// accept) — distinct from the runtime `Recipient`, whose fields are +// `type`/`string`. `EncodedRecipient` is exactly +// `Either`. +import { + type EncodedRecipient, + encodeCoinPublicKey, +} from '@midnight-ntwrk/compact-runtime'; +import { + eitherUserFromCoinPublicKey, + encodeToPK, + toHexPadded, +} from './address.js'; + +/** + * Backend-aware shielded recipient/signer-key fixtures, so one spec runs + * unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. The live keys are + * threaded in WITHOUT importing the live harness (which pulls in testkit / + * midnight-js): each key arrives through a `MIDNIGHT_*_COIN_PK` env var the + * harness publishes once the wallet has synced. So a dry spec importing this + * stays lean. See {@link nativeShieldedToken} for the matching coin fixtures. + */ + +/** + * The recipient for a shielded send in a spec that runs on both backends. On + * live it must be a key whose encryption key the node can resolve, so it is the + * deployer wallet's own coin public key — published by the live harness as + * `MIDNIGHT_DEPLOYER_COIN_PK` once the wallet has synced. Call this AFTER + * `Sim.create()` (e.g. in `beforeEach`), since that is what triggers the sync. + * On dry a fabricated key works, so `label` is hashed into a synthetic recipient. + * + * @param label Distinguishes synthetic recipients on the dry backend. + */ +export const shieldedTestRecipient = ( + label = 'RECIPIENT', +): EncodedRecipient => { + const deployerPk = process.env.MIDNIGHT_DEPLOYER_COIN_PK; + return deployerPk + ? eitherUserFromCoinPublicKey(deployerPk) + : eitherUserFromCoinPublicKey(toHexPadded(label)); +}; + +/** + * A bare `ZswapCoinPublicKey` for a spec that targets a coin public key directly + * (not an `Either`) — e.g. a private forwarder's drain parent. Like + * {@link shieldedTestRecipient}, on live it is the deployer wallet's own key + * (whose encryption key the node can resolve; a fabricated key has none) and on + * dry a synthetic key. The deployer key is published by the live setup before + * any spec loads, so this is safe to read at module scope. + * + * @param label Distinguishes synthetic keys on the dry backend. + */ +export const shieldedTestParentKey = ( + label = 'PARENT', +): { bytes: Uint8Array } => { + const deployerPk = process.env.MIDNIGHT_DEPLOYER_COIN_PK; + return deployerPk + ? { bytes: encodeCoinPublicKey(deployerPk) } + : encodeToPK(label); +}; + +/** + * The `Either` for a named signer alias, for + * specs that register a multisig signer set. On live it resolves to the pooled + * wallet's own coin public key — published by the harness as + * `MIDNIGHT__COIN_PK` — so `.as(alias)` submits from that wallet and the + * circuit's `ownPublicKey()` matches this key. On dry it is the deterministic + * synthetic key `eitherUserFromCoinPublicKey(toHexPadded(alias))`, which is exactly what the dry + * backend resolves `.as(alias)` to. Read at module scope: the keys are published + * by the live setup before any spec loads. + * + * @param alias The signer alias (e.g. `SIGNER1`); must be a pooled wallet on live. + */ +export const shieldedTestSigner = (alias: string): EncodedRecipient => { + const pk = process.env[`MIDNIGHT_${alias}_COIN_PK`]; + return pk + ? eitherUserFromCoinPublicKey(pk) + : eitherUserFromCoinPublicKey(toHexPadded(alias)); +}; diff --git a/contracts/test-utils/fixtures/test/address.test.ts b/contracts/test-utils/fixtures/test/address.test.ts new file mode 100644 index 000000000..bad671c61 --- /dev/null +++ b/contracts/test-utils/fixtures/test/address.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from 'vitest'; +import { + createEitherTestContractAddress, + createEitherTestUnshieldedContract, + createEitherTestUserAddress, + eitherUserFromCoinPublicKey, + encodeToAddress, + encodeToPK, + encodeToUserAddress, + generateEitherPubKeyPair, + generatePubKeyPair, + toHexPadded, + ZERO_ADDRESS, + ZERO_KEY, + ZERO_UNSHIELDED_CONTRACT, + ZERO_USER_ADDRESS, + zeroUint8Array, +} from '../address.js'; + +/** + * The pure address/key encoding fixtures. Every encoder here is a deterministic + * hex→bytes transform (an ASCII string is hexified, left-padded to 32 bytes, and + * decoded), so a single ASCII char lands in the final byte. That lets each case + * assert a fully pinned `Uint8Array` rather than a recomputed value. + */ + +/** A 32-byte zero array — the padded encoding of the empty string. */ +const zeros = (n = 32) => new Uint8Array(n); + +/** A 32-byte array whose only non-zero byte is the last (a single ASCII char). */ +const lastByte = (byte: number) => { + const b = new Uint8Array(32); + b[31] = byte; + return b; +}; + +const A = 0x41; // ASCII 'A' + +describe('address fixtures', () => { + describe('toHexPadded', () => { + it('should hexify ASCII and left-pad to 64 chars by default', () => { + expect(toHexPadded('AB')).toBe(`${'0'.repeat(60)}4142`); + }); + + it('should pad to a custom length', () => { + expect(toHexPadded('A', 4)).toBe('0041'); + }); + + it('should not truncate input longer than the target length', () => { + expect(toHexPadded('AAAA', 2)).toBe('41414141'); + }); + + it('should encode the empty string as all-zero padding', () => { + expect(toHexPadded('')).toBe('0'.repeat(64)); + }); + }); + + describe('encodeToPK', () => { + it('should encode the empty string to a 32-byte zero key', () => { + expect(encodeToPK('')).toStrictEqual({ bytes: zeros() }); + }); + + it('should place a single ASCII char in the final byte', () => { + expect(encodeToPK('A')).toStrictEqual({ bytes: lastByte(A) }); + }); + + it('should be deterministic for the same input', () => { + expect(encodeToPK('A')).toStrictEqual(encodeToPK('A')); + }); + + it('should produce distinct keys for distinct inputs', () => { + expect(encodeToPK('A')).not.toStrictEqual(encodeToPK('B')); + }); + }); + + describe('encodeToAddress', () => { + it('should encode the empty string to a 32-byte zero address', () => { + expect(encodeToAddress('')).toStrictEqual({ bytes: zeros() }); + }); + + it('should place a single ASCII char in the final byte', () => { + expect(encodeToAddress('A')).toStrictEqual({ bytes: lastByte(A) }); + }); + + it('should throw when the input is not a valid contract address', () => { + // A 40-char string hexifies to 80 chars — too long to be a 32-byte address. + expect(() => encodeToAddress('x'.repeat(40))).toThrow( + 'must be a valid `ContractAddress`', + ); + }); + }); + + describe('encodeToUserAddress', () => { + it('should place a single ASCII char in the final byte', () => { + expect(encodeToUserAddress('A')).toStrictEqual({ bytes: lastByte(A) }); + }); + + it('should throw when the encoded value is not exactly 32 bytes', () => { + expect(() => encodeToUserAddress('x'.repeat(40))).toThrow( + 'must be exactly 32 bytes', + ); + }); + }); + + describe('eitherUserFromCoinPublicKey', () => { + it('should build a left (coin-public-key) Either from a raw hex key', () => { + const pk = 'ab'.repeat(32); // 64 hex chars → 32 bytes of 0xab + expect(eitherUserFromCoinPublicKey(pk)).toStrictEqual({ + is_left: true, + left: { bytes: new Uint8Array(32).fill(0xab) }, + right: { bytes: zeros() }, + }); + }); + }); + + describe('createEitherTestContractAddress', () => { + it('should build a right Either bound to the contract address', () => { + expect(createEitherTestContractAddress('A')).toStrictEqual({ + is_left: false, + left: { bytes: zeros() }, + right: { bytes: lastByte(A) }, + }); + }); + }); + + describe('createEitherTestUserAddress', () => { + it('should build a right Either bound to the user address', () => { + expect(createEitherTestUserAddress('A')).toStrictEqual({ + is_left: false, + left: { bytes: zeros() }, + right: { bytes: lastByte(A) }, + }); + }); + }); + + describe('createEitherTestUnshieldedContract', () => { + it('should build a left Either bound to the contract address', () => { + expect(createEitherTestUnshieldedContract('A')).toStrictEqual({ + is_left: true, + left: { bytes: lastByte(A) }, + right: { bytes: zeros() }, + }); + }); + }); + + describe('generatePubKeyPair', () => { + it('should return the padded hex alongside the encoded key', () => { + expect(generatePubKeyPair('A')).toStrictEqual([ + `${'0'.repeat(62)}41`, + { bytes: lastByte(A) }, + ]); + }); + }); + + describe('generateEitherPubKeyPair', () => { + it('should return the padded hex alongside the Either-wrapped key', () => { + expect(generateEitherPubKeyPair('A')).toStrictEqual([ + `${'0'.repeat(62)}41`, + { + is_left: true, + left: { bytes: lastByte(A) }, + right: { bytes: zeros() }, + }, + ]); + }); + }); + + describe('zeroUint8Array', () => { + it('should return 32 zero bytes by default', () => { + expect(zeroUint8Array()).toStrictEqual(zeros()); + }); + + it('should return the requested number of zero bytes', () => { + expect(zeroUint8Array(16)).toStrictEqual(zeros(16)); + }); + }); + + describe('zero constants', () => { + it('should expose a zero coin-public-key Either', () => { + expect(ZERO_KEY).toStrictEqual({ + is_left: true, + left: { bytes: zeros() }, + right: { bytes: zeros() }, + }); + }); + + it('should expose a zero contract-address Either', () => { + expect(ZERO_ADDRESS).toStrictEqual({ + is_left: false, + left: { bytes: zeros() }, + right: { bytes: zeros() }, + }); + }); + + it('should expose a zero user-address Either', () => { + expect(ZERO_USER_ADDRESS).toStrictEqual({ + is_left: false, + left: { bytes: zeros() }, + right: { bytes: zeros() }, + }); + }); + + it('should expose a zero unshielded-contract Either', () => { + expect(ZERO_UNSHIELDED_CONTRACT).toStrictEqual({ + is_left: true, + left: { bytes: zeros() }, + right: { bytes: zeros() }, + }); + }); + }); +}); diff --git a/contracts/test-utils/fixtures/test/nativeShieldedToken.test.ts b/contracts/test-utils/fixtures/test/nativeShieldedToken.test.ts new file mode 100644 index 000000000..459a3de5c --- /dev/null +++ b/contracts/test-utils/fixtures/test/nativeShieldedToken.test.ts @@ -0,0 +1,85 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + encodeShieldedCoinInfo, + GENESIS_NATIVE_SHIELDED_TOKEN_COLORS, + genesisNativeShieldedTokenColor, +} from '../nativeShieldedToken.js'; + +/** + * The backend-aware shielded-coin fixtures. `encodeShieldedCoinInfo` branches on + * `isLiveBackend()` (`MIDNIGHT_BACKEND=live`), so the tests drive both arms by + * stubbing the environment. The color builders are pure. + */ + +const color = new Uint8Array(32).fill(1); + +describe('native shielded token fixtures', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('genesisNativeShieldedTokenColor', () => { + it('should set the requested last byte of a 32-byte color', () => { + const expected = new Uint8Array(32); + expected[31] = 1; + expect(genesisNativeShieldedTokenColor(1)).toStrictEqual(expected); + }); + + it('should be all zeros for the native token byte', () => { + expect(genesisNativeShieldedTokenColor(0)).toStrictEqual( + new Uint8Array(32), + ); + }); + }); + + describe('GENESIS_NATIVE_SHIELDED_TOKEN_COLORS', () => { + it('should map each funded color to its genesis type byte', () => { + expect(GENESIS_NATIVE_SHIELDED_TOKEN_COLORS).toStrictEqual({ + nativeShieldedToken1: genesisNativeShieldedTokenColor(1), + nativeShieldedToken2: genesisNativeShieldedTokenColor(2), + }); + }); + }); + + describe('encodeShieldedCoinInfo (dry backend)', () => { + it('should default the nonce to 32 zero bytes', () => { + vi.stubEnv('MIDNIGHT_BACKEND', 'dry'); + expect(encodeShieldedCoinInfo(color, 500n)).toStrictEqual({ + nonce: new Uint8Array(32), + color, + value: 500n, + }); + }); + + it('should use the provided nonce verbatim', () => { + vi.stubEnv('MIDNIGHT_BACKEND', 'dry'); + const nonce = new Uint8Array(32).fill(9); + expect(encodeShieldedCoinInfo(color, 500n, nonce)).toStrictEqual({ + nonce, + color, + value: 500n, + }); + }); + }); + + describe('encodeShieldedCoinInfo (live backend)', () => { + it('should generate a fresh 32-byte random nonce, passing color and value through', () => { + vi.stubEnv('MIDNIGHT_BACKEND', 'live'); + const first = encodeShieldedCoinInfo(color, 500n); + const second = encodeShieldedCoinInfo(color, 500n); + expect(first.nonce).toHaveLength(32); + expect(first.color).toBe(color); + expect(first.value).toBe(500n); + // Random per call: two coins must not share a nonce (would replay on live). + expect(first.nonce).not.toStrictEqual(second.nonce); + }); + + it('should ignore a provided nonce on the live backend', () => { + vi.stubEnv('MIDNIGHT_BACKEND', 'live'); + const nonce = new Uint8Array(32).fill(9); + expect( + encodeShieldedCoinInfo(color, 500n, nonce).nonce, + ).not.toStrictEqual(nonce); + }); + }); +}); diff --git a/contracts/test-utils/fixtures/test/shieldedKey.test.ts b/contracts/test-utils/fixtures/test/shieldedKey.test.ts new file mode 100644 index 000000000..e6ae527f8 --- /dev/null +++ b/contracts/test-utils/fixtures/test/shieldedKey.test.ts @@ -0,0 +1,77 @@ +import { encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + eitherUserFromCoinPublicKey, + encodeToPK, + toHexPadded, +} from '../address.js'; +import { + shieldedTestParentKey, + shieldedTestRecipient, + shieldedTestSigner, +} from '../shieldedKey.js'; + +/** + * The backend-aware shielded recipient/signer-key fixtures. Each helper branches + * on a published `MIDNIGHT_*_COIN_PK` env var, so the tests drive both arms by + * stubbing the environment. The dry arm is asserted against the pure builders it + * delegates to; the live arm (real wallet keys) is exercised by the live path, so + * here we only assert the branch selection. + */ + +/** A valid 64-hex coin public key (32 bytes of 0xab). */ +const PK = 'ab'.repeat(32); + +describe('shielded key fixtures', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('shieldedTestRecipient', () => { + it('should build a synthetic recipient when no deployer key is published', () => { + vi.stubEnv('MIDNIGHT_DEPLOYER_COIN_PK', undefined); + expect(shieldedTestRecipient('BOB')).toStrictEqual( + eitherUserFromCoinPublicKey(toHexPadded('BOB')), + ); + }); + + it('should bind to the deployer coin public key when published', () => { + vi.stubEnv('MIDNIGHT_DEPLOYER_COIN_PK', PK); + expect(shieldedTestRecipient('BOB')).toStrictEqual( + eitherUserFromCoinPublicKey(PK), + ); + }); + }); + + describe('shieldedTestParentKey', () => { + it('should build a synthetic key when no deployer key is published', () => { + vi.stubEnv('MIDNIGHT_DEPLOYER_COIN_PK', undefined); + expect(shieldedTestParentKey('PARENT')).toStrictEqual( + encodeToPK('PARENT'), + ); + }); + + it('should bind to the deployer coin public key when published', () => { + vi.stubEnv('MIDNIGHT_DEPLOYER_COIN_PK', PK); + expect(shieldedTestParentKey('PARENT')).toStrictEqual({ + bytes: encodeCoinPublicKey(PK), + }); + }); + }); + + describe('shieldedTestSigner', () => { + it("should build a synthetic key from the alias when the signer's key is unpublished", () => { + vi.stubEnv('MIDNIGHT_SIGNER1_COIN_PK', undefined); + expect(shieldedTestSigner('SIGNER1')).toStrictEqual( + eitherUserFromCoinPublicKey(toHexPadded('SIGNER1')), + ); + }); + + it("should bind to the signer's published coin public key", () => { + vi.stubEnv('MIDNIGHT_SIGNER1_COIN_PK', PK); + expect(shieldedTestSigner('SIGNER1')).toStrictEqual( + eitherUserFromCoinPublicKey(PK), + ); + }); + }); +}); diff --git a/contracts/test-utils/fixtures/test/zswap.test.ts b/contracts/test-utils/fixtures/test/zswap.test.ts new file mode 100644 index 000000000..8f3d9dadc --- /dev/null +++ b/contracts/test-utils/fixtures/test/zswap.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from 'vitest'; +import { + bytesToHex, + isNonceSpent, + type ZswapInput, + type ZswapLocalState, + type ZswapOutput, + zswapDelta, + zswapLocalState, + zswapSnapshot, +} from '../zswap.js'; + +/** + * The Zswap-introspection fixtures. `bytesToHex` and `isNonceSpent` are pure; + * `zswapLocalState` / `zswapSnapshot` / `zswapDelta` read the accumulated + * `currentZswapLocalState` off a dry simulator, which is reproduced here with a + * plain object shaped like the backend the helpers dig into. + */ + +/** A minimal coin body for building inputs/outputs. */ +const coin = (nonceByte: number, value = 1000n) => ({ + nonce: new Uint8Array(32).fill(nonceByte), + color: new Uint8Array(32).fill(1), + value, +}); + +const input = (nonceByte: number, mt_index = 0n): ZswapInput => ({ + ...coin(nonceByte), + mt_index, +}); + +const output = (nonceByte: number): ZswapOutput => ({ + coinInfo: coin(nonceByte), + recipient: { + is_left: true, + left: { bytes: new Uint8Array(32) }, + right: { bytes: new Uint8Array(32) }, + }, +}); + +/** Wraps a local state in the nested shape `zswapLocalState` reads from. */ +const mockSim = (state: ZswapLocalState) => ({ + _backend: { sim: { circuitContext: { currentZswapLocalState: state } } }, +}); + +describe('zswap fixtures', () => { + describe('bytesToHex', () => { + it('should hex-encode a byte string', () => { + expect(bytesToHex(Uint8Array.from([0xde, 0xad, 0xbe, 0xef]))).toBe( + 'deadbeef', + ); + }); + + it('should encode an empty array as an empty string', () => { + expect(bytesToHex(new Uint8Array(0))).toBe(''); + }); + }); + + describe('zswapLocalState', () => { + it('should read the accumulated local state off the simulator', () => { + const state: ZswapLocalState = { + inputs: [input(7)], + outputs: [output(8)], + }; + expect(zswapLocalState(mockSim(state))).toBe(state); + }); + + it('should throw when the simulator does not expose the local state', () => { + expect(() => zswapLocalState({})).toThrow( + 'Could not read currentZswapLocalState', + ); + }); + }); + + describe('zswapSnapshot', () => { + it('should capture the current input and output counts', () => { + const state: ZswapLocalState = { + inputs: [input(1), input(2)], + outputs: [output(3)], + }; + expect(zswapSnapshot(mockSim(state))).toStrictEqual({ + inputs: 2, + outputs: 1, + }); + }); + }); + + describe('zswapDelta', () => { + it('should return only the entries appended since the snapshot', () => { + const kept = input(1); + const spentSince = input(2); + const madeSince = output(3); + const state: ZswapLocalState = { + inputs: [kept, spentSince], + outputs: [madeSince], + }; + expect( + zswapDelta(mockSim(state), { inputs: 1, outputs: 0 }), + ).toStrictEqual({ inputs: [spentSince], outputs: [madeSince] }); + }); + + it('should return empty deltas when nothing was appended', () => { + const state: ZswapLocalState = { + inputs: [input(1)], + outputs: [output(2)], + }; + expect( + zswapDelta(mockSim(state), { inputs: 1, outputs: 1 }), + ).toStrictEqual({ inputs: [], outputs: [] }); + }); + }); + + describe('isNonceSpent', () => { + const nonce7 = new Uint8Array(32).fill(7); + + it('should be true when a coin with the nonce was spent', () => { + expect(isNonceSpent([input(1), input(7)], nonce7)).toBe(true); + }); + + it('should match by value, not by array identity', () => { + // A fresh array with the same bytes must still count as spent. + expect(isNonceSpent([input(7)], new Uint8Array(32).fill(7))).toBe(true); + }); + + it('should be false when no input carries the nonce', () => { + expect(isNonceSpent([input(1), input(2)], nonce7)).toBe(false); + }); + + it('should be false for an empty input set', () => { + expect(isNonceSpent([], nonce7)).toBe(false); + }); + }); +}); diff --git a/contracts/test-utils/zswap.ts b/contracts/test-utils/fixtures/zswap.ts similarity index 100% rename from contracts/test-utils/zswap.ts rename to contracts/test-utils/fixtures/zswap.ts diff --git a/contracts/test-utils/harness/FundedWallet.ts b/contracts/test-utils/harness/FundedWallet.ts new file mode 100644 index 000000000..9c773b3da --- /dev/null +++ b/contracts/test-utils/harness/FundedWallet.ts @@ -0,0 +1,132 @@ +import { DustSecretKey, ZswapSecretKeys } from '@midnight-ntwrk/ledger-v8'; +import { unshieldedToken } from '@midnight-ntwrk/midnight-js-protocol/ledger'; +import { + DEFAULT_DUST_OPTIONS, + FluentWalletBuilder, + type LocalTestConfiguration, + MidnightWalletProvider, + syncWallet, + waitForFunds, +} from '@midnight-ntwrk/testkit-js'; +import { MIN_WALLET_NIGHT, UNDEPLOYED_FEE_OVERHEAD } from './dust.js'; +import type { PooledWallet } from './WalletPool.js'; + +/** The pino logger testkit's providers expect (withWallet's first parameter). */ +type LiveLogger = Parameters[0]; + +/** + * One dust-funded wallet built from a raw seed — the concrete + * {@link PooledWallet} the live harness injects into the pool. + * + * Responsibility: apply the `undeployed` fee overhead (the knob + * `MidnightWalletProvider.build()` hides) via the lower-level + * `FluentWalletBuilder`, then run an asserting funds wait so an unfunded or + * unsynced seed is spotted in seconds rather than hanging ~1h on its first tx. + * + * A wallet "can pay fees" when it holds spendable NIGHT (a genesis grant, still + * unregistered) OR generated dust (NIGHT that has been registered for dust + * generation — which zeroes the plain NIGHT balance). {@link build} does not + * itself gate on this: it reports both balances via {@link isFunded} so the + * composition root can top up an unfunded signer from the deployer before the + * gate (see `funding.ts`). The deployer itself must be genesis-funded. + */ +export class FundedWallet implements PooledWallet { + private constructor( + readonly alias: string, + readonly provider: MidnightWalletProvider, + public nightBalance: bigint, + public dustBalance: bigint, + ) {} + + /** The wallet's coin public key, encoded for `MIDNIGHT__COIN_PK`. */ + get coinPublicKey(): string { + return String(this.provider.getCoinPublicKey()); + } + + /** Whether the wallet can pay tx fees: spendable NIGHT or generated dust. */ + get isFunded(): boolean { + return this.nightBalance >= MIN_WALLET_NIGHT || this.dustBalance > 0n; + } + + stop(): Promise { + return this.provider.stop(); + } + + /** Re-sync and refresh {@link nightBalance} / {@link dustBalance} (e.g. after a top-up). */ + async refresh(): Promise { + const state = await syncWallet(this.provider.wallet); + this.nightBalance = nightOf(state); + this.dustBalance = state.dust.balance(new Date()); + } + + static async build( + env: LocalTestConfiguration, + alias: string, + walletSeed: string, + logger: LiveLogger, + ): Promise { + // `MidnightWalletProvider.build()` exposes no dust options, so go one layer + // down to set `additionalFeeOverhead` on the dust wallet. + const { wallet, seeds, keystore } = + await FluentWalletBuilder.forEnvironment(env) + .withSeed(walletSeed) + .withDustOptions({ + ...DEFAULT_DUST_OPTIONS, + additionalFeeOverhead: UNDEPLOYED_FEE_OVERHEAD, + }) + .buildWithoutStarting(); + + const provider = await MidnightWalletProvider.withWallet( + logger, + env, + wallet, + ZswapSecretKeys.fromSeed(seeds.shielded), + DustSecretKey.fromSeed(seeds.dust), + keystore, + ); + + // Start the facade WITHOUT the provider's fire-and-forget fund wait, then run + // our own asserting wait so a zero-NIGHT/unsynced seed is spotted fast (the + // plain `start(true)` logs a `0` balance and proceeds, which is the ~1h hang). + // `waitForFunds` also registers any NIGHT UTXOs for dust generation. + // Stop the provider on any init failure so a rejected build doesn't leak the + // started connection (WalletPool.reset only stops wallets it recorded). + try { + await provider.start(false); + const nightBalance = await waitForFunds(wallet, env, true, keystore); + const dustBalance = (await syncWallet(wallet)).dust.balance(new Date()); + logger.info( + `live wallet '${alias}' built — NIGHT ${nightBalance}, dust ${dustBalance}`, + ); + + // Re-sync the wallet before every tx build. Consecutive submissions from + // the same signer must balance against state that already reflects the + // prior tx; otherwise the second call reuses a dust/coin UTXO the first + // call already spent (the wallet observes the spend only on its next synced + // emission) and the node rejects it with a bare "Transaction submission + // error". `syncWallet` waits for a strictly-complete shielded/unshielded/ + // dust state — i.e. the wallet has caught up to the latest block. The cost + // is negligible next to proving. Guarded on a real `balanceTx`: the + // unit-test mock provider may omit it. + if (typeof provider.balanceTx === 'function') { + const balanceTx = provider.balanceTx.bind(provider); + provider.balanceTx = (async (...args: Parameters) => { + await syncWallet(wallet); + return balanceTx(...args); + }) as typeof provider.balanceTx; + } + + return new FundedWallet(alias, provider, nightBalance, dustBalance); + } catch (error) { + await provider.stop().catch(() => {}); + throw error; + } + } +} + +/** The wallet's spendable NIGHT (the native unshielded token) in a synced state. + * A wallet that has registered its NIGHT for dust generation reports it here as + * absent (0n) — its balance has moved into dust. */ +function nightOf(state: Awaited>): bigint { + return state.unshielded.balances[unshieldedToken().raw] ?? 0n; +} diff --git a/contracts/test-utils/harness/LiveSimulatorBackend.ts b/contracts/test-utils/harness/LiveSimulatorBackend.ts new file mode 100644 index 000000000..54634bf0e --- /dev/null +++ b/contracts/test-utils/harness/LiveSimulatorBackend.ts @@ -0,0 +1,304 @@ +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + CompiledContract, + type Contract as ContractNs, +} from '@midnight-ntwrk/compact-js'; +import { deployContract } from '@midnight-ntwrk/midnight-js-contracts'; +import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; +import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; +import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; +import type { + MidnightProviders, + PrivateStateId, +} from '@midnight-ntwrk/midnight-js-types'; +import { + inMemoryPrivateStateProvider, + type LocalTestConfiguration, +} from '@midnight-ntwrk/testkit-js'; +import { + createLiveContext, + type LiveBackendRequest, + type LiveContext, + registerLiveBackend, +} from '@openzeppelin/compact-simulator'; +import type { WalletPool } from './WalletPool.js'; + +/** + * Bridges the `@openzeppelin/compact-simulator` live backend to the local stack: + * on each `Sim.create()` it deploys the requested artifact (signed by the + * deployer) and returns a `LiveContext` whose per-alias providers route each + * caller's calls through that signer's wallet. + * + * Deploy-per-`create()` gives each test a fresh contract, matching the unit + * specs' `beforeEach`-fresh-state assumption. + */ + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); + +/** Absolute path to `contracts/artifacts//` (the ZK keys + zkir root). */ +function moduleRootPath(name: string): string { + // this harness lives at contracts/test-utils/harness/; + // artifacts live at contracts/artifacts// + return path.resolve(currentDir, '..', '..', 'artifacts', name); +} + +/** + * The contract deployed here is chosen at runtime (by `artifactName`), so its + * concrete type is unknowable at compile time. We model it as the library's own + * "any contract" type and pin every piece (compiled contract, providers, deploy + * options) to it, so `deployContract` infers `C = Contract.Any` consistently — + * `CompiledContract` is invariant in `C`, so provider and compiled types must + * agree exactly. + */ +type AnyContract = ContractNs.Any; +type CircuitId = ContractNs.ProvableCircuitId; +type PrivateState = ContractNs.PrivateState; + +/** The compiled artifact's module shape — its generated `Contract` constructor. */ +type ContractModule = { Contract: new (...args: unknown[]) => AnyContract }; + +/** Loads a compiled contract module by artifact name. Injectable for tests. */ +export type LoadContract = (name: string) => Promise; + +/** Default loader: dynamic-import the artifact's generated `contract/index.js`. */ +const importArtifact: LoadContract = (name) => + import( + pathToFileURL(path.join(moduleRootPath(name), 'contract', 'index.js')).href + ) as Promise; + +/** The midnight-js provider bundle, pinned to the runtime-chosen contract. */ +type Providers = MidnightProviders; + +/** The providers that don't depend on the caller (everything but the wallet). */ +type SharedProviders = Omit; + +/** Resolves the provider bundle for a caller alias (unknown → deployer). */ +type ProvidersFor = (alias?: string | null) => Providers; + +/** The compiled + witness-bound contract handle deploy/createLiveContext consume. */ +type CompiledArtifact = ReturnType; + +/** The artifact name to deploy, or throw if the spec set none. */ +function requireArtifactName(req: LiveBackendRequest): string { + const name = req.config.artifactName; + if (!name) { + throw new Error( + 'live backend: SimulatorConfig.artifactName is required to deploy on live', + ); + } + return name; +} + +/** Bind the artifact's constructor to its witnesses and compiled-file assets. */ +function compileArtifact( + name: string, + ctor: ContractModule['Contract'], + witnesses: unknown, +) { + return CompiledContract.make(name, ctor).pipe( + // The first `.pipe` combinator sees the compiled contract's full unresolved + // requirement union (witnesses + assets path), which the effect builder's + // phantom-context type narrows to `never`; the assets step below then reads + // cleanly. This single cast is intrinsic to the builder's typing. + CompiledContract.withWitnesses((witnesses ?? {}) as never), + CompiledContract.withCompiledFileAssets( + path.join(moduleRootPath(name), 'contract'), + ), + ); +} + +/** Reads on-chain public state + tx status from the indexer. */ +function makePublicDataProvider(env: LocalTestConfiguration) { + return indexerPublicDataProvider(env.indexer, env.indexerWS); +} + +/** Loads the artifact's proving keys + zkir from `contracts/artifacts//`. */ +function makeZkConfigProvider(name: string) { + return new NodeZkConfigProvider(moduleRootPath(name)); +} + +/** Proves transactions against the local proof server. */ +function makeProofProvider( + env: LocalTestConfiguration, + zkConfigProvider: ReturnType, +) { + return httpClientProofProvider(env.proofServer, zkConfigProvider); +} + +/** + * A single in-memory private-state store, shared across every caller alias so + * the deploy's initial private state is visible to each `.as(alias)`. + * + * testkit's default (a per-provider on-disk LevelDB) cannot serve this: every + * provider opens the same DB directory (only one handle allowed) AND scopes + * state by the wallet's coin public key, so a non-deployer signer both fought + * over the lock and never saw the deployed state. In-memory sidesteps both and + * keeps the run hermetic — no disk, no stale state across runs. + */ +function makePrivateStateProvider() { + return inMemoryPrivateStateProvider(); +} + +/** + * A per-alias provider resolver over one set of {@link SharedProviders}: each + * alias reuses the shared providers and swaps in its own wallet, so + * `.as('SIGNER1')` submits + pays from SIGNER1 (its `ownPublicKey()`) while + * reading the same private state. An unknown alias falls back to the deployer. + */ +function makeProvidersFor( + pool: WalletPool, + shared: SharedProviders, +): ProvidersFor { + const cache = new Map(); + return (alias) => { + const key = pool.isKnownAlias(alias) ? (alias as string) : 'deployer'; + let providers = cache.get(key); + if (!providers) { + const wallet = pool.walletFor(key); + providers = { + ...shared, + walletProvider: wallet, + midnightProvider: wallet, + }; + cache.set(key, providers); + } + return providers; + }; +} + +const DETERMINISTIC_REJECTION = /1010: Invalid Transaction/; + +/** + * Whether `err` is a deterministic node rejection (RPC 1010 "Invalid + * Transaction") that would fail identically on a retry — e.g. a shielded spend + * the ledger rejects against stale node state (`Custom error: 103`). Retrying + * only doubles the proving cost and the log noise. + * + * The 1010 text can hide behind an effect `FiberFailure` (which keeps its cause + * chain behind a Symbol and only renders it via `toString()`) or a plain + * `cause` / `AggregateError.errors` chain, so walk both: test `String(e)` (picks + * up a custom `toString`) and each `Error`'s `message`, following `cause` and + * `errors`. Cycle-safe. + */ +function isDeterministicRejection(err: unknown): boolean { + const seen = new Set(); + const queue: unknown[] = [err]; + while (queue.length > 0) { + const e = queue.pop(); + if (e == null || seen.has(e)) continue; + seen.add(e); + if (DETERMINISTIC_REJECTION.test(String(e))) return true; + if (e instanceof Error) { + if (DETERMINISTIC_REJECTION.test(e.message)) return true; + queue.push(e.cause); + const { errors } = e as { errors?: unknown }; + if (Array.isArray(errors)) queue.push(...errors); + } + } + return false; +} + +/** + * Deploy the compiled contract with `providers` and return its address. + * + * Retried once on failure with a jittered backoff. With parallel workers, several + * deploys can contend for one block and the node may bounce a submission + * ("Transaction submission error"); a single retry absorbs that race (and the + * occasional transient submit flake). The jitter keeps contending workers from + * retrying in lockstep. + * + * A deterministic node rejection (RPC 1010 "Invalid Transaction") is NOT retried + * — it would fail identically. See {@link isDeterministicRejection}. + */ +async function deployArtifact( + providers: Providers, + compiled: CompiledArtifact, + privateStateId: string, + req: LiveBackendRequest, +): Promise { + const initialPrivateState = + req.options.privateState ?? req.config.defaultPrivateState(); + const args = req.config.contractArgs(...req.contractArgs); + const deploy = () => + deployContract(providers, { + compiledContract: compiled, + privateStateId, + initialPrivateState, + args, + }); + const deployed = await deploy().catch(async (err: unknown) => { + if (isDeterministicRejection(err)) throw err; + await new Promise((resolve) => { + setTimeout(resolve, 500 + Math.floor(Math.random() * 1000)); + }); + return deploy(); + }); + return deployed.deployTxData.public.contractAddress; +} + +export class LiveSimulatorBackend { + private registered = false; + + constructor( + private readonly pool: WalletPool, + private readonly env: LocalTestConfiguration, + // Seam for tests: how a contract module is loaded from its artifact name. + private readonly loadContract: LoadContract = importArtifact, + ) {} + + /** Register with the simulator. Idempotent per worker. */ + register(): void { + if (this.registered) return; + this.registered = true; + registerLiveBackend((req) => this.buildContext(req)); + } + + /** Build the caller-independent providers once for one deployment. */ + private sharedProviders(name: string): SharedProviders { + const zkConfigProvider = makeZkConfigProvider(name); + return { + publicDataProvider: makePublicDataProvider(this.env), + zkConfigProvider, + proofProvider: makeProofProvider(this.env, zkConfigProvider), + privateStateProvider: makePrivateStateProvider(), + }; + } + + /** Deploy the requested artifact and assemble its `LiveContext`. */ + private async buildContext( + req: LiveBackendRequest, + ): Promise> { + const name = requireArtifactName(req); + // The loaded constructor is the one genuinely-untyped value; the loader + // pins it to a precise constructor type so `Contract.Any` flows from here. + const { Contract: ctor } = await this.loadContract(name); + const compiled = compileArtifact(name, ctor, req.config.witnessesFactory()); + + await this.pool.ensureReady(); + const privateStateId = `${name}-ps`; + const shared = this.sharedProviders(name); + const providersFor = makeProvidersFor(this.pool, shared); + + // The deploy is always signed by the deployer. + const providers = providersFor('deployer'); + const contractAddress = await deployArtifact( + providers, + compiled, + privateStateId, + req, + ); + + // The simulator assembles the LiveContext: per-alias `findDeployedContract` + // handle cache, indexer-lag-absorbing public read, private-state read. Each + // alias routes to its own wallet's providers so caller identity varies. + return createLiveContext({ + contractAddress, + providersFor, + compiledContract: compiled, + privateStateId, + publicDataProvider: shared.publicDataProvider, + privateStateProvider: shared.privateStateProvider, + }); + } +} diff --git a/contracts/test-utils/harness/NativeShieldedTokenTracker.ts b/contracts/test-utils/harness/NativeShieldedTokenTracker.ts new file mode 100644 index 000000000..346122a2e --- /dev/null +++ b/contracts/test-utils/harness/NativeShieldedTokenTracker.ts @@ -0,0 +1,150 @@ +import { + decodeShieldedCoinInfo, + type EncodedQualifiedShieldedCoinInfo, + type EncodedShieldedCoinInfo, +} from '@midnight-ntwrk/compact-runtime'; +import { coinCommitment, ZswapOutput } from '@midnight-ntwrk/ledger-v8'; +import { isLiveBackend } from '@openzeppelin/compact-simulator'; +import { fetchCoinEvents, indexerHead } from './ledgerEvents.js'; + +/** + * The owner of a shielded coin, for resolving its commitment: a contract (by + * address) or a wallet (by coin public key). Both arms use the same global + * event index — the owner only decides how the coin's commitment is computed. + */ +export type ShieldedOwner = + | { readonly kind: 'contract'; readonly address: string } + | { readonly kind: 'wallet'; readonly coinPublicKey: string }; + +/** + * Resolves a shielded coin — contract- or wallet-owned — to its + * `QualifiedShieldedCoinInfo`: the coin plus its `mt_index`, the coin-commitment + * tree position required to spend it on-chain. + * + * It reads the index (never computes it) by indexing the indexer's global zswap + * ledger-event stream, whose output events carry each coin's owner and index. So + * it follows every coin the chain emits — across any transaction — not one offer. + * + * Resolves coins whose fields (nonce/color/value) are known. Auto-discovering an + * unknown wallet coin (ciphertext-only) is the wallet SDK's job. + */ +export class NativeShieldedTokenTracker { + private readonly outputs = new Map< + string, + { contract: string | undefined; mtIndex: bigint } + >(); + private lastHeight: number; + + private constructor( + private readonly url: string, + head: number, + ) { + this.lastHeight = head; + } + + /** Builds a tracker anchored at the current chain head. */ + static async create(url: string): Promise { + return new NativeShieldedTokenTracker(url, await indexerHead(url)); + } + + /** + * Pulls new ledger events up to the current head into the index. Re-reads the + * anchor block (idempotent) so a coin inserted in the same block the tracker + * was created in is never missed. + */ + private async sync(): Promise { + const head = await indexerHead(this.url); + if (head < this.lastHeight) return; + for (const event of await fetchCoinEvents( + this.url, + this.lastHeight, + head, + )) { + this.outputs.set(event.commitment, { + contract: event.contract, + mtIndex: event.mtIndex, + }); + } + this.lastHeight = head + 1; + } + + /** The on-chain commitment for `coin` under `owner`. */ + private commitmentFor( + owner: ShieldedOwner, + coin: EncodedShieldedCoinInfo, + ): string { + const runtimeCoin = decodeShieldedCoinInfo(coin); + return owner.kind === 'contract' + ? ZswapOutput.newContractOwned(runtimeCoin, undefined, owner.address) + .commitment + : coinCommitment(runtimeCoin, owner.coinPublicKey); + } + + /** + * Syncs (retrying to absorb indexer lag) until `coin`'s output is indexed, then + * returns it with its `mt_index`. Call after the transaction that created the + * coin. Throws if it never appears (never created on-chain, or wrong fields). + */ + async resolve( + owner: ShieldedOwner, + coin: EncodedShieldedCoinInfo, + { + retries = 20, + delayMs = 500, + }: { retries?: number; delayMs?: number } = {}, + ): Promise { + const commitment = this.commitmentFor(owner, coin); + for (let attempt = 0; attempt < retries; attempt++) { + await this.sync(); + const record = this.outputs.get(commitment); + if (record) return { ...coin, mt_index: record.mtIndex }; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + throw new Error( + `coin tracker: no indexed commitment for coin under ${owner.kind} (synced through height ${this.lastHeight - 1}) — was it created on-chain?`, + ); + } +} + +let trackerPromise: Promise | undefined; + +/** + * The process-wide {@link NativeShieldedTokenTracker}, anchored at the chain head on + * first use. The indexer URL is derived from `MIDNIGHT_INDEXER_PORT` (default + * `8088`), matching the harness. Depends only on the indexer over `fetch`, so it + * is decoupled from the testkit live harness (a dry spec can import this module + * without pulling in testkit — it just never calls this on dry). + */ +export function getShieldedCoinTracker(): Promise { + if (!trackerPromise) { + const port = process.env.MIDNIGHT_INDEXER_PORT ?? '8088'; + trackerPromise = NativeShieldedTokenTracker.create( + `http://127.0.0.1:${port}/api/v4/graphql`, + ); + } + return trackerPromise; +} + +/** A {@link ShieldedOwner} for a deployed simulator (a contract), read from its + * backend address. Works on both backends (dry has an address too). */ +export const contractOwner = (sim: { + readonly _backend: { readonly contractAddress: string }; +}): ShieldedOwner => ({ + kind: 'contract', + address: sim._backend.contractAddress, +}); + +/** + * Gets the qualified coin — the coin plus its `mt_index` (the position needed to + * spend it) — for a coin the spec just created under `owner`, so one spec runs on + * both backends. Dry: a placeholder `0n` the in-memory runtime ignores. Live: the + * real global index, looked up from the tracker's index of the indexer's event + * stream. Call after the transaction that created the coin. + */ +export async function getQualifiedShieldedCoinInfo( + owner: ShieldedOwner, + coin: EncodedShieldedCoinInfo, +): Promise { + if (!isLiveBackend()) return { ...coin, mt_index: 0n }; + return (await getShieldedCoinTracker()).resolve(owner, coin); +} diff --git a/contracts/test-utils/harness/WalletPool.ts b/contracts/test-utils/harness/WalletPool.ts new file mode 100644 index 000000000..3a1df2897 --- /dev/null +++ b/contracts/test-utils/harness/WalletPool.ts @@ -0,0 +1,149 @@ +import type { MidnightWalletProvider } from '@midnight-ntwrk/testkit-js'; + +/** + * The pooled test wallets for the live backend: build each seed once, publish + * its coin public key, and resolve one wallet per caller alias. + * + * This module is deliberately free of any testkit *runtime* dependency (it only + * imports testkit types, which are erased). Building a wallet — the part that + * touches testkit / the node — is injected as a {@link WalletBuilder}, so the + * pool's orchestration can be unit-tested with a fake builder and no node. The + * real builder ({@link FundedWallet.build}) is wired in by `live.setup.ts`. + */ + +/** The wallet surface the pool needs from whatever the builder returns. */ +export interface PooledWallet { + readonly provider: MidnightWalletProvider; + /** Encoded coin public key, published as `MIDNIGHT__COIN_PK`. */ + readonly coinPublicKey: string; + stop(): Promise; +} + +/** Builds (and funds) one pooled wallet for an alias. Injected for testability. */ +export type WalletBuilder = ( + alias: string, + seed: string, +) => Promise; + +const seed = (lastByte: number): string => + `${'0'.repeat(62)}${lastByte.toString(16).padStart(2, '0')}`; + +/** + * How many live workers the wallet partition supports. Bounded by the + * genesis-funded deployer seeds `midnight-node --preset=dev` provides + * (`0x..01`–`0x..03`): each worker's deployer must be a genesis seed, because + * only those carry the NIGHT + genesis shielded coins the deposit specs spend. + */ +export const MAX_LIVE_WORKERS = 3; + +/** + * The pooled wallet seeds for one live worker `w` (1-based). Each worker gets a + * disjoint set so up to {@link MAX_LIVE_WORKERS} `unit-live` files can run + * concurrently against the shared node without sharing a wallet's UTXOs/nonces: + * + * - `deployer` is genesis seed `0x..0w` — one of the three seeds the dev + * preset funds with NIGHT + the genesis shielded coins deposit specs spend + * (so it MUST be a genesis seed; hence the 3-worker cap). It pays for every + * deploy and is the default caller. Worker 1's deployer honours + * `MIDNIGHT_WALLET_SEED`. + * - `SIGNER1`–`SIGNER3` are derived seeds `0x..(0x10·w + slot)` (worker 1: + * `0x11`–`0x13`, worker 2: `0x21`–`0x23`, worker 3: `0x31`–`0x33`) — disjoint + * from the genesis seeds (`0x01`–`0x04`) and from other workers'. They start + * empty and are topped up from the worker's deployer at live setup (see + * `funding.ts`); signers only pay fees, never spend shielded coins, so they + * need no genesis grant. They back distinct on-chain identities, so a + * multisig spec's `.as('SIGNER1')` submits from a wallet whose + * `ownPublicKey()` differs from the others (the only way to exercise + * multi-signer authorization on live — one wallet can't impersonate three). + */ +export function walletSeedsFor( + worker: number, +): Readonly> { + const deployer = + worker === 1 + ? (process.env.MIDNIGHT_WALLET_SEED ?? seed(worker)) + : seed(worker); + return { + deployer, + SIGNER1: seed(0x10 * worker + 1), + SIGNER2: seed(0x10 * worker + 2), + SIGNER3: seed(0x10 * worker + 3), + }; +} + +/** + * Worker 1's pooled seeds — the default single-worker pool. A stable symbol for + * callers that don't partition by worker; the live setup builds its pool from + * {@link walletSeedsFor} keyed on the worker's `VITEST_POOL_ID`. + */ +export const WALLET_SEEDS: Readonly> = walletSeedsFor(1); + +/** The env var carrying an alias's coin public key (e.g. `MIDNIGHT_SIGNER1_COIN_PK`, + * `MIDNIGHT_DEPLOYER_COIN_PK`). Specs read these to build a live signer set. */ +export const coinPkEnv = (alias: string): string => + `MIDNIGHT_${alias === 'deployer' ? 'DEPLOYER' : alias}_COIN_PK`; + +/** + * Owns the pooled wallets for a worker. Builds each seed once (concurrently) via + * the injected builder, publishes each coin public key, resolves a wallet per + * caller alias (unknown alias → deployer fallback), and tears them all down. + */ +export class WalletPool { + private readonly wallets = new Map(); + private ready?: Promise; + + constructor( + private readonly seeds: Readonly>, + private readonly buildWallet: WalletBuilder, + ) {} + + /** + * Build every pooled wallet once, sequentially, and publish each coin public + * key. Serial (not `Promise.all`): the live builder funds empty signers from + * the shared deployer, so concurrent builds would balance several funding + * transactions against the same deployer snapshot and reintroduce the + * stale-UTXO race the harness exists to prevent. Idempotent; a no-op-cost + * await after the first call. + */ + ensureReady(): Promise { + if (!this.ready) { + this.ready = (async () => { + for (const [alias, walletSeed] of Object.entries(this.seeds)) { + const wallet = await this.buildWallet(alias, walletSeed); + process.env[coinPkEnv(alias)] = wallet.coinPublicKey; + this.wallets.set(alias, wallet); + } + })(); + } + return this.ready; + } + + /** Whether `alias` names a pooled wallet (vs. falling back to the deployer). */ + isKnownAlias(alias: string | null | undefined): boolean { + return !!alias && this.seeds[alias] !== undefined; + } + + /** + * The wallet provider for a caller alias (requires {@link ensureReady}). An + * unknown alias falls back to the deployer, so `.as('OTHER')` acts as a funded + * non-signer caller rather than erroring. + */ + walletFor(alias: string | null | undefined): MidnightWalletProvider { + const key = this.isKnownAlias(alias) ? (alias as string) : 'deployer'; + const wallet = this.wallets.get(key) ?? this.wallets.get('deployer'); + if (!wallet) { + throw new Error( + 'live wallets not initialized — call ensureReady() in the test:live setup', + ); + } + return wallet.provider; + } + + /** Stop every built wallet and clear the pool (for a `globalTeardown`). */ + async reset(): Promise { + const all = Array.from(this.wallets.values()); + this.wallets.clear(); + this.ready = undefined; + await Promise.all(all.map((w) => w.stop())); + } +} diff --git a/contracts/test-utils/harness/dust.ts b/contracts/test-utils/harness/dust.ts new file mode 100644 index 000000000..1ab03117a --- /dev/null +++ b/contracts/test-utils/harness/dust.ts @@ -0,0 +1,50 @@ +/** + * Dust policy for the local `undeployed` devnet — pure and dependency-free, so + * it can be unit-tested without loading testkit or touching a node. + * + * "Dust" is what pays tx fees; it is generated by the NIGHT a wallet holds. On + * the local devnet a wallet must be built with fee headroom or it cannot balance + * its first tx, and a wallet holding zero NIGHT can never pay at all. + */ + +/** + * Fee headroom the local `undeployed` devnet needs to balance a tx. testkit's + * `MidnightWalletProvider.build()` hard-codes `additionalFeeOverhead: 0n` + * (`DEFAULT_DUST_OPTIONS`), which leaves a signer wallet unable to pay for its + * first tx — the balancer retries for ~1h and then WASM-traps. This mirrors the + * value PR #632's `OwnWalletProvider` uses for `undeployed`. + */ +export const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; // 5e17 + +/** + * Minimum NIGHT a pooled wallet must report before it is usable. NIGHT generates + * the dust that pays fees; a zero-NIGHT seed can never pay, so we reject it up + * front rather than hang on its first tx. + */ +export const MIN_WALLET_NIGHT = 1n; + +/** + * A pooled wallet failed the funds gate — it holds no spendable NIGHT, so it can + * never pay tx fees on the local devnet. Thrown at build time (fast) instead of + * letting the wallet hang on its first unpayable tx. + */ +export class DustFundingError extends Error { + constructor( + readonly alias: string, + readonly nightBalance: bigint, + ) { + super( + `live wallet '${alias}' has no spendable dust (NIGHT balance ${nightBalance}). ` + + `On the local 'undeployed' devnet this seed cannot pay tx fees — check that ` + + 'midnight-node --preset=dev funds its seed, or rotate the pool to a funded one.', + ); + this.name = 'DustFundingError'; + } +} + +/** Throw {@link DustFundingError} unless `nightBalance` clears {@link MIN_WALLET_NIGHT}. */ +export function assertFunded(alias: string, nightBalance: bigint): void { + if (nightBalance < MIN_WALLET_NIGHT) { + throw new DustFundingError(alias, nightBalance); + } +} diff --git a/contracts/test-utils/harness/funding.ts b/contracts/test-utils/harness/funding.ts new file mode 100644 index 000000000..6829f2f6b --- /dev/null +++ b/contracts/test-utils/harness/funding.ts @@ -0,0 +1,133 @@ +import { unshieldedToken } from '@midnight-ntwrk/midnight-js-protocol/ledger'; +import { + type LocalTestConfiguration, + type MidnightWalletProvider, + syncWallet, + waitForFunds, +} from '@midnight-ntwrk/testkit-js'; +import { UnshieldedAddress } from '@midnight-ntwrk/wallet-sdk-address-format'; + +/** + * Deployer-funds-signer top-up for the local `undeployed` devnet. + * + * Each worker's three signers are derived seeds the dev preset never funds, so + * they start with zero NIGHT and cannot pay tx fees. This transfers NIGHT from + * the worker's (genesis-funded) deployer to a signer, registers it for dust + * generation, and waits for the dust to appear — after which the signer can pay + * for its own multisig transactions. Verified end-to-end on the live stack: dust + * bootstraps within one block of the registration. On-chain funding persists, so + * only the first spec file per worker pays the top-up; later files skip it. + * + * Only the testkit-aware layer uses this; the pool and its unit tests stay + * testkit-free (the funder is wired in by `live.setup.ts`). + */ + +/** The pino logger testkit's providers expect. */ +type LiveLogger = Parameters[0]; + +/** + * NIGHT moved to a topped-up signer. Sized well above the fee overhead so the + * signer generates ample dust for a spec's worth of proofs, while leaving the + * deployer the bulk of its genesis grant for the deploys it keeps paying for. + */ +export const SIGNER_TOPUP_NIGHT = 50_000_000_000_000n; // 5e13 + +/** The NIGHT raw token type (the native unshielded token). */ +const nightRaw = () => unshieldedToken().raw; + +/** The target provider's unshielded address as the SDK's `UnshieldedAddress`. */ +function receiverAddressOf(target: MidnightWalletProvider): UnshieldedAddress { + const bech32 = target.unshieldedKeystore.getBech32Address(); + return bech32.decode( + UnshieldedAddress, + (bech32 as unknown as { network: string }).network, + ); +} + +/** Transfer `amount` NIGHT from `from` to `to`'s unshielded address; returns the tx id. */ +async function transferNight( + from: MidnightWalletProvider, + to: MidnightWalletProvider, + amount: bigint, +): Promise { + const ttl = new Date(Date.now() + 30 * 60 * 1000); + const recipe = await from.wallet.transferTransaction( + [ + { + type: 'unshielded', + outputs: [ + { type: nightRaw(), receiverAddress: receiverAddressOf(to), amount }, + ], + }, + ], + { + shieldedSecretKeys: from.zswapSecretKeys, + dustSecretKey: from.dustSecretKey, + }, + { ttl, payFees: true }, + ); + // Spending unshielded UTXOs needs the owner's signature before finalizing. + const signed = await from.wallet.signRecipe(recipe, (payload) => + from.unshieldedKeystore.signData(payload), + ); + const finalized = await from.wallet.finalizeRecipe(signed); + return from.wallet.submitTransaction(finalized); +} + +// Transfers from the shared deployer are serialized so concurrent top-ups (one +// per unfunded signer, built in parallel) don't race on the deployer's UTXOs. +let transferQueue: Promise = Promise.resolve(); +function serialize(fn: () => Promise): Promise { + const run = transferQueue.then(fn, fn); + transferQueue = run.catch(() => undefined); + return run; +} + +async function nightBalance(p: MidnightWalletProvider): Promise { + return (await syncWallet(p.wallet)).unshielded.balances[nightRaw()] ?? 0n; +} +async function dustBalance(p: MidnightWalletProvider): Promise { + return (await syncWallet(p.wallet)).dust.balance(new Date()); +} + +/** + * Fund `target` from `deployer` and wait until it holds spendable dust. Transfers + * {@link SIGNER_TOPUP_NIGHT}, waits for the UTXO to arrive, registers it for dust + * generation (via `waitForFunds`), then polls until the dust balance is non-zero. + * Returns the target's dust balance. Throws if the dust never appears. + */ +export async function fundFromDeployer( + deployer: MidnightWalletProvider, + target: MidnightWalletProvider, + env: LocalTestConfiguration, + logger: LiveLogger, + { + amount = SIGNER_TOPUP_NIGHT, + retries = 30, + delayMs = 2000, + }: { amount?: bigint; retries?: number; delayMs?: number } = {}, +): Promise { + const txId = await serialize(() => transferNight(deployer, target, amount)); + logger.info(`top-up: sent ${amount} NIGHT to a signer (tx ${txId})`); + + // Wait for the NIGHT to land before registering it for dust. + for (let i = 0; i < retries; i++) { + if ((await nightBalance(target)) > 0n) break; + await new Promise((r) => setTimeout(r, delayMs)); + } + // Registers the freshly-received NIGHT UTXO for dust generation. + await waitForFunds(target.wallet, env, true, target.unshieldedKeystore); + + // Dust appears a block after registration; poll until it does. + for (let i = 0; i < retries; i++) { + const dust = await dustBalance(target); + if (dust > 0n) { + logger.info(`top-up: signer now holds dust ${dust}`); + return dust; + } + await new Promise((r) => setTimeout(r, delayMs)); + } + throw new Error( + `top-up: signer received NIGHT but generated no dust within ${(retries * delayMs) / 1000}s`, + ); +} diff --git a/contracts/test-utils/harness/ledgerEvents.ts b/contracts/test-utils/harness/ledgerEvents.ts new file mode 100644 index 000000000..a421900f4 --- /dev/null +++ b/contracts/test-utils/harness/ledgerEvents.ts @@ -0,0 +1,106 @@ +import { Event, type EventDetails } from '@midnight-ntwrk/ledger-v8'; + +/** + * Live transport for the indexer's global zswap ledger-event stream — the source + * of truth for a coin's real `mt_index` and its owner. Deliberately isolated: the + * one query the tracker depends on lives here, so a schema tweak after a live + * probe is a one-file change. Uses `fetch` only (no websocket, no new deps). + * + * Each `ZswapLedgerEvent.raw` is a hex `Event`; we keep the `zswapOutput` variant + * `{ commitment, contract?, mtIndex }` — a coin entering the tree, with its owner + * (`contract` set → contract-owned) and global index. + */ + +/** A coin-commitment output: an owned coin at a known global index. */ +export interface CoinOutputEvent { + readonly commitment: string; + readonly contract: string | undefined; + readonly mtIndex: bigint; +} + +interface GqlBlockEventsData { + block: { + transactions: ReadonlyArray<{ + zswapLedgerEvents: ReadonlyArray<{ raw: string }>; + }>; + } | null; +} + +const HEAD_QUERY = 'query Head { block { height } }'; + +const BLOCK_EVENTS_QUERY = `query BlockEvents($offset: BlockOffset) { + block(offset: $offset) { + transactions { zswapLedgerEvents { raw } } + } +}`; + +async function gql( + url: string, + query: string, + variables: Record, +): Promise { + const res = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + if (!res.ok) { + throw new Error(`indexer ${url}: HTTP ${res.status}`); + } + const body = (await res.json()) as { data?: T; errors?: unknown }; + if (body.errors) { + throw new Error(`indexer gql errors: ${JSON.stringify(body.errors)}`); + } + if (!body.data) { + throw new Error('indexer gql: empty data'); + } + return body.data; +} + +/** The current chain head height as the indexer sees it (0 if none yet). */ +export async function indexerHead(url: string): Promise { + const data = await gql<{ block: { height: number } | null }>( + url, + HEAD_QUERY, + {}, + ); + return data.block?.height ?? 0; +} + +function decodeOutput(rawHex: string): CoinOutputEvent | undefined { + // `EventDetails` ends in a `{ tag: string }` catch-all, so a `tag` check alone + // does not narrow field access — pin the concrete variant with `Extract`. + const content = Event.deserialize(Buffer.from(rawHex, 'hex')).content; + if (content.tag !== 'zswapOutput') return undefined; + const output = content as Extract; + return { + commitment: output.commitment, + contract: output.contract, + mtIndex: output.mtIndex, + }; +} + +/** + * All zswap coin-commitment outputs in blocks `[fromHeight, toHeight]` + * (inclusive), in block order. Re-reading a block is safe: downstream indexing + * is idempotent by commitment. + */ +export async function fetchCoinEvents( + url: string, + fromHeight: number, + toHeight: number, +): Promise { + const events: CoinOutputEvent[] = []; + for (let height = Math.max(0, fromHeight); height <= toHeight; height++) { + const data = await gql(url, BLOCK_EVENTS_QUERY, { + offset: { height }, + }); + for (const tx of data.block?.transactions ?? []) { + for (const event of tx.zswapLedgerEvents) { + const output = decodeOutput(event.raw); + if (output) events.push(output); + } + } + } + return events; +} diff --git a/contracts/test-utils/harness/live.globalSetup.ts b/contracts/test-utils/harness/live.globalSetup.ts new file mode 100644 index 000000000..e9b35859a --- /dev/null +++ b/contracts/test-utils/harness/live.globalSetup.ts @@ -0,0 +1,219 @@ +import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { fetchCoinEvents, indexerHead } from './ledgerEvents.js'; + +/** + * Vitest `globalSetup` for the live tests — runs once in the main process, + * before any worker builds a wallet, so a bad environment fails in ~1s instead + * of after a slow wallet build. Gated on `MIDNIGHT_BACKEND === 'live'` so a dry + * `vitest run` that happens to glob the live tests is a no-op. + * + * It guards two things: + * - **Freshness.** The live tests are not isolated from one another: they all + * run against the same node, so shielded-coin state left by an earlier run + * changes a later run's outcome (a coin re-spent against stale state is + * rejected with node `Custom error: 103`). Genesis records the funded + * seeds' shielded coins in block 0 (measured: 28 events); any coin a test + * creates on-chain is recorded in a later block. So a coin event at or + * beyond block {@link SCAN_FROM} means an earlier run left state behind — + * abort and tell the dev to `env:up`. + * - **Single run.** Two live runs against one node corrupt each other's coin + * state. A pid-stamped lock file makes the second run fail fast. + * + * Escape hatch: `MIDNIGHT_LIVE_ALLOW_DIRTY=1` skips the freshness check (the + * lock is still taken). Thresholds: `MIDNIGHT_LIVE_MAX_COIN_EVENTS` (default 0), + * `MIDNIGHT_LIVE_MAX_SCAN_BLOCKS` (default 3600 ≈ 6h of idle blocks). + */ + +const LOGS_DIR = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../logs', +); +const LOCK_PATH = path.join(LOGS_DIR, '.live-run.lock'); + +const INDEXER_PORT = Number(process.env.MIDNIGHT_INDEXER_PORT ?? 8088); +const INDEXER_URL = `http://127.0.0.1:${INDEXER_PORT}/api/v4/graphql`; + +// Genesis coins are block 0; real deposits/drains are block 1+. +const SCAN_FROM = 1; +const MAX_COIN_EVENTS = Number(process.env.MIDNIGHT_LIVE_MAX_COIN_EVENTS ?? 0); +const MAX_SCAN_BLOCKS = Number( + process.env.MIDNIGHT_LIVE_MAX_SCAN_BLOCKS ?? 3600, +); +const SCAN_CHUNK = 32; +const SCAN_CONCURRENCY = 8; + +const ENV_UP_HINT = "run 'yarn env:up' to reset the local stack"; + +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +// --- lock ------------------------------------------------------------------ + +export interface LockInfo { + readonly pid: number; + readonly startedAt: string; +} + +/** Whether a pid names a live process (EPERM means alive but not ours to signal). */ +function pidAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (e) { + return (e as NodeJS.ErrnoException).code === 'EPERM'; + } +} + +/** + * What an existing lock means for this process: `reentrant` when we already hold + * it (one vitest process can run several live projects, each with its own + * globalSetup), `held` when a live process owns it, `stale` otherwise. + */ +export function lockHolderState( + info: LockInfo, + ownPid: number, + isAlive: (pid: number) => boolean, +): 'reentrant' | 'held' | 'stale' { + if (info.pid === ownPid) return 'reentrant'; + return isAlive(info.pid) ? 'held' : 'stale'; +} + +function readLock(): LockInfo | undefined { + try { + return JSON.parse(readFileSync(LOCK_PATH, 'utf8')) as LockInfo; + } catch { + return undefined; + } +} + +function removeLock(): void { + try { + unlinkSync(LOCK_PATH); + } catch { + // already gone + } +} + +/** Take the lock, or throw if another live process holds it. */ +function acquireLock(): { reentrant: boolean } { + mkdirSync(LOGS_DIR, { recursive: true }); + const stamp = JSON.stringify({ + pid: process.pid, + startedAt: new Date().toISOString(), + }); + for (let attempt = 0; attempt < 2; attempt++) { + try { + writeFileSync(LOCK_PATH, stamp, { flag: 'wx' }); + return { reentrant: false }; + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e; + const info = readLock(); + const state = info + ? lockHolderState(info, process.pid, pidAlive) + : 'stale'; + if (state === 'reentrant') return { reentrant: true }; + if (state === 'held') { + throw new Error( + `another live run is already in progress (pid ${info?.pid}, ` + + `started ${info?.startedAt}). Wait for it to finish, or remove ` + + `${LOCK_PATH} if it is stale.`, + ); + } + removeLock(); // stale or unreadable — reclaim and retry + } + } + throw new Error(`could not acquire the live-run lock at ${LOCK_PATH}`); +} + +/** Release only if we still own the lock (guards against a stale takeover). */ +function releaseLock(reentrant: boolean): void { + if (reentrant) return; + const info = readLock(); + if (info?.pid === process.pid) removeLock(); +} + +// --- freshness ------------------------------------------------------------- + +/** + * Count coin-commitment events in `[from, head]`, scanning in parallel chunks + * and stopping as soon as the count exceeds `threshold` (so a dirty node is + * caught in the first chunk; a clean node scans everything). `fetchWindow` + * returns the event count for an inclusive block range — injected for testing. + */ +export async function countCoinEvents( + fetchWindow: (from: number, to: number) => Promise, + from: number, + head: number, + threshold: number, + { chunk = SCAN_CHUNK, concurrency = SCAN_CONCURRENCY } = {}, +): Promise { + const windows: [number, number][] = []; + for (let lo = from; lo <= head; lo += chunk) { + windows.push([lo, Math.min(lo + chunk - 1, head)]); + } + let count = 0; + for (let i = 0; i < windows.length; i += concurrency) { + const batch = windows.slice(i, i + concurrency); + const counts = await Promise.all( + batch.map(([lo, hi]) => fetchWindow(lo, hi)), + ); + for (const c of counts) count += c; + if (count > threshold) return count; + } + return count; +} + +async function indexerHeadWithRetry(): Promise { + let lastError: unknown; + for (let attempt = 0; attempt < 10; attempt++) { + try { + return await indexerHead(INDEXER_URL); + } catch (e) { + lastError = e; + await sleep(1000); + } + } + throw new Error( + `live indexer not reachable at ${INDEXER_URL} (${String(lastError)}). ` + + `Is the stack up? ${ENV_UP_HINT}.`, + ); +} + +async function assertFreshNode(): Promise { + const head = await indexerHeadWithRetry(); + if (head > MAX_SCAN_BLOCKS) { + throw new Error( + 'live stack has been up too long to verify freshness (indexer head ' + + `${head} > ${MAX_SCAN_BLOCKS} blocks) — ${ENV_UP_HINT}, or set ` + + 'MIDNIGHT_LIVE_ALLOW_DIRTY=1 to run against it anyway.', + ); + } + const count = await countCoinEvents( + (from, to) => fetchCoinEvents(INDEXER_URL, from, to).then((e) => e.length), + SCAN_FROM, + head, + MAX_COIN_EVENTS, + ); + if (count > MAX_COIN_EVENTS) { + throw new Error( + `live stack is not fresh: found ${count} shielded coin event(s) beyond ` + + `genesis (block ${SCAN_FROM}+), so a previous run left state on the ` + + `node. This makes shielded spends fail with node "Custom error: 103". ` + + `${ENV_UP_HINT}, or set MIDNIGHT_LIVE_ALLOW_DIRTY=1 to run anyway.`, + ); + } +} + +export default async function setup(): Promise<() => void> { + if (process.env.MIDNIGHT_BACKEND !== 'live') return () => {}; + const { reentrant } = acquireLock(); + try { + if (process.env.MIDNIGHT_LIVE_ALLOW_DIRTY !== '1') await assertFreshNode(); + } catch (e) { + releaseLock(reentrant); + throw e; + } + return () => releaseLock(reentrant); +} diff --git a/contracts/test-utils/harness/live.setup.ts b/contracts/test-utils/harness/live.setup.ts new file mode 100644 index 000000000..fb31cc09c --- /dev/null +++ b/contracts/test-utils/harness/live.setup.ts @@ -0,0 +1,114 @@ +import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import { createLogger } from '@midnight-ntwrk/testkit-js'; +import { beforeAll, beforeEach, expect } from 'vitest'; +import { assertFunded } from './dust.js'; +import { FundedWallet } from './FundedWallet.js'; +import { fundFromDeployer } from './funding.js'; +import { LiveSimulatorBackend } from './LiveSimulatorBackend.js'; +import { localEnv } from './network.js'; +import { + MAX_LIVE_WORKERS, + type WalletBuilder, + WalletPool, + walletSeedsFor, +} from './WalletPool.js'; + +/** + * Composition root for the live backend. Runs once per worker before the unit + * specs when `test:live` is used (on the dry path this file is not loaded). + * + * It owns the wiring — everything else is split by responsibility: + * - {@link WalletPool} builds the pooled wallets (testkit-free orchestration). + * - {@link FundedWallet} builds one dust-funded wallet from a seed. + * - {@link fundFromDeployer} tops up a signer the dev preset didn't fund. + * - {@link LiveSimulatorBackend} deploys per `Sim.create()` and assembles the + * simulator `LiveContext`. + * + * Each vitest worker drives a disjoint wallet partition keyed on its + * `VITEST_POOL_ID` (see {@link walletSeedsFor}), so parallel `unit-live` files + * never share a wallet's UTXOs. Every worker's deployer is a genesis-funded + * seed; its three signers start empty and are topped up from it before the + * pool's readiness gate. + * + * Order matters: pin the process network id first (deployContract and the + * indexer provider read it globally), register the backend, then build the + * wallets up front so specs can read each published `MIDNIGHT__COIN_PK` + * at module load (e.g. a forwarder's parent key, or a multisig's live signer + * set). Top-level await; setup files are awaited before test files. + */ + +setNetworkId('undeployed'); + +// The forks pool sets `VITEST_POOL_ID` to 1..maxWorkers before setup files +// load; it is this worker's partition index. Defaults to 1 on a non-forked run. +const worker = Number(process.env.VITEST_POOL_ID ?? '1'); +if (!Number.isInteger(worker) || worker < 1 || worker > MAX_LIVE_WORKERS) { + throw new Error( + `live setup: VITEST_POOL_ID='${process.env.VITEST_POOL_ID}' is outside ` + + `1..${MAX_LIVE_WORKERS}. The wallet partition only has ${MAX_LIVE_WORKERS} ` + + 'genesis-funded deployers — lower MIDNIGHT_LIVE_WORKERS, and check that ' + + "VITEST_MAX_WORKERS isn't overriding the configured worker count.", + ); +} + +// Total configured live workers (resolved and published by vitest.config). +// Shown in the per-worker banner and the per-file tag below. +const totalWorkers = Number( + process.env.MIDNIGHT_LIVE_WORKERS ?? MAX_LIVE_WORKERS, +); + +// Per-file worker pointer: tag each spec file with the worker running it, so +// interleaved output from parallel workers stays attributable. A setup-file +// `beforeAll` fires once before each spec file's suite. +beforeAll(() => { + const testPath = (expect.getState?.().testPath ?? '') as string; + const file = testPath ? (testPath.split('/').pop() ?? testPath) : '(spec)'; + console.log(`[w${worker}] ❯ ${file}`); +}); + +// Stamp this worker's id onto each test's metadata so the live progress +// reporter (main process) can tag every result line with its worker — the +// worker is the only place that knows its `VITEST_POOL_ID`. A setup-file +// `beforeEach` fires in the worker before each test. See `liveProgressReporter`. +beforeEach((ctx) => { + (ctx.task.meta as { workerId?: number }).workerId = worker; +}); + +const seeds = walletSeedsFor(worker); +const logger = createLogger(`logs/live-harness-w${worker}.log`); +const env = localEnv(); + +// The deployer pays for every deploy and funds the signers, so it must be +// genesis-funded — fail fast if its seed carries no NIGHT. +const deployer = await FundedWallet.build( + env, + 'deployer', + seeds.deployer, + logger, +); +assertFunded('deployer', deployer.nightBalance); + +// Reuse the prebuilt deployer; build each signer and top it up from the deployer +// if the preset left it unfunded, so `.as('SIGNERn')` can pay its own fees. +const buildWallet: WalletBuilder = async (alias, walletSeed) => { + if (alias === 'deployer') return deployer; + const wallet = await FundedWallet.build(env, alias, walletSeed, logger); + if (!wallet.isFunded) { + await fundFromDeployer(deployer.provider, wallet.provider, env, logger); + await wallet.refresh(); + if (!wallet.isFunded) assertFunded(alias, wallet.nightBalance); + } + return wallet; +}; + +const pool = new WalletPool(seeds, buildWallet); +const backend = new LiveSimulatorBackend(pool, env); + +backend.register(); +await pool.ensureReady(); + +// Worker ready: wallets funded, backend registered. Printed after the (slow) +// wallet build, before any spec in this worker runs — a "we're live" pointer. +console.log( + `▶ live worker ${worker}/${totalWorkers} ready — deployer 0x…${seeds.deployer.slice(-4)}`, +); diff --git a/contracts/test-utils/harness/liveProgressReporter.ts b/contracts/test-utils/harness/liveProgressReporter.ts new file mode 100644 index 000000000..dfa4b9dfe --- /dev/null +++ b/contracts/test-utils/harness/liveProgressReporter.ts @@ -0,0 +1,45 @@ +import type { Reporter, TestCase, TestModule } from 'vitest/node'; + +/** + * Prints one worker-tagged, globally-counted line per test, e.g. + * [w2] ✓ Signer > assertThresholdMet > should fail… (15ms) [118/210] + * + * Runs alongside the built-in `default` reporter, which still owns the per-file + * lines, failure details, and final summary — this reporter only adds the + * per-test progress line. The worker id comes from `task.meta`, stamped by each + * worker in `live.setup` (the only place that knows its `VITEST_POOL_ID`). The + * total accrues as modules are collected, so the first few lines may show a + * smaller denominator until collection finishes. + */ +const MARKS: Record = { + passed: '✓', + failed: '✗', + skipped: '↓', +}; + +export default class LiveProgressReporter implements Reporter { + private total = 0; + private done = 0; + + onTestRunStart(): void { + this.total = 0; + this.done = 0; + } + + onTestModuleCollected(module: TestModule): void { + this.total += [...module.children.allTests()].length; + } + + onTestCaseResult(testCase: TestCase): void { + const { state } = testCase.result(); + if (state === 'pending') return; // not finished yet + this.done += 1; + const worker = (testCase.meta() as { workerId?: number }).workerId ?? '?'; + const mark = MARKS[state] ?? '·'; + const ms = Math.round(testCase.diagnostic()?.duration ?? 0); + console.log( + `[w${worker}] ${mark} ${testCase.fullName} (${ms}ms) ` + + `[${this.done}/${this.total}]`, + ); + } +} diff --git a/contracts/test-utils/harness/network.ts b/contracts/test-utils/harness/network.ts new file mode 100644 index 000000000..0f8777f5b --- /dev/null +++ b/contracts/test-utils/harness/network.ts @@ -0,0 +1,29 @@ +import { LocalTestConfiguration } from '@midnight-ntwrk/testkit-js'; + +/** Parse a port override, failing with the responsible env var name so a + * malformed value (empty, non-numeric, out of range) is caught at load time + * instead of surfacing later as a bad `NaN`/`0` URL. */ +function port(name: string, fallback: number): number { + const value = Number(process.env[name] ?? fallback); + if (!Number.isInteger(value) || value < 1 || value > 65_535) { + throw new Error(`${name} must be an integer between 1 and 65535`); + } + return value; +} + +/** + * Endpoints of the local stack (`local-env.yml`), shared by the live setup and + * the live harness smoke. `LocalTestConfiguration` builds the `127.0.0.1` + * `/api/v4/graphql` URLs from these ports; override per port via the + * `MIDNIGHT_*_PORT` env vars for a relocated stack. + */ +export const PORTS = { + indexer: port('MIDNIGHT_INDEXER_PORT', 8088), + node: port('MIDNIGHT_NODE_PORT', 9944), + proofServer: port('MIDNIGHT_PROOF_SERVER_PORT', 6300), +}; + +/** A testkit environment config pointed at the local stack. */ +export function localEnv(): LocalTestConfiguration { + return new LocalTestConfiguration(PORTS); +} diff --git a/contracts/test-utils/harness/test/FundedWallet.test.ts b/contracts/test-utils/harness/test/FundedWallet.test.ts new file mode 100644 index 000000000..01450db5f --- /dev/null +++ b/contracts/test-utils/harness/test/FundedWallet.test.ts @@ -0,0 +1,164 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { UNDEPLOYED_FEE_OVERHEAD } from '../dust.js'; + +const NIGHT_RAW = 'night-raw-token'; + +// Mock testkit + ledger-v8 so we can drive FundedWallet.build with no node and +// assert the wiring that matters: the dust fee overhead is applied, the wallet +// starts without the fire-and-forget fund wait, and both balances are reported. +const m = vi.hoisted(() => { + const start = vi.fn(async (_wait?: boolean) => {}); + const stop = vi.fn(async () => {}); + const balanceTx = vi.fn(async (_tx: unknown) => 'balanced-tx'); + // A fresh provider object per `withWallet` call, as production does (each + // `FundedWallet.build` constructs a new provider). It reuses the shared fns + // so tests can assert against them, but its own `balanceTx` slot is distinct + // per call — so `build`'s wrapping doesn't stack across tests. + const makeProvider = () => ({ + start, + stop, + balanceTx, + getCoinPublicKey: () => 'coin-pk-xyz', + wallet: { id: 'facade' }, + }); + const builder = { + withSeed: vi.fn().mockReturnThis(), + withDustOptions: vi.fn().mockReturnThis(), + buildWithoutStarting: vi.fn(async () => ({ + wallet: { id: 'facade' }, + seeds: { shielded: new Uint8Array([1]), dust: new Uint8Array([2]) }, + keystore: { id: 'keystore' }, + })), + }; + // A synced-state stub whose NIGHT + dust balances tests can vary. + const syncState = { night: 250_000_000_000_000n, dust: 0n }; + const syncWallet = vi.fn(async () => ({ + unshielded: { balances: { [NIGHT_RAW]: syncState.night } }, + dust: { balance: (_d: Date) => syncState.dust }, + })); + return { + start, + stop, + balanceTx, + builder, + syncState, + withWallet: vi.fn(async () => makeProvider()), + waitForFunds: vi.fn(async () => 250_000_000_000_000n), + syncWallet, + }; +}); + +vi.mock('@midnight-ntwrk/testkit-js', () => ({ + FluentWalletBuilder: { forEnvironment: vi.fn(() => m.builder) }, + MidnightWalletProvider: { withWallet: m.withWallet }, + DEFAULT_DUST_OPTIONS: { + ledgerParams: {}, + additionalFeeOverhead: 0n, + feeBlocksMargin: 5, + }, + waitForFunds: m.waitForFunds, + syncWallet: m.syncWallet, +})); + +vi.mock('@midnight-ntwrk/midnight-js-protocol/ledger', () => ({ + unshieldedToken: () => ({ raw: NIGHT_RAW }), +})); + +vi.mock('@midnight-ntwrk/ledger-v8', () => ({ + ZswapSecretKeys: { fromSeed: vi.fn(() => ({ kind: 'zswap' })) }, + DustSecretKey: { fromSeed: vi.fn(() => ({ kind: 'dust' })) }, +})); + +import { FundedWallet } from '../FundedWallet.js'; + +const ENV = {} as never; // LocalTestConfiguration is type-only in FundedWallet +const LOGGER = { info: vi.fn() } as never; +const build = () => FundedWallet.build(ENV, 'SIGNER1', 'seed-hex', LOGGER); + +describe('FundedWallet.build', () => { + beforeEach(() => { + vi.clearAllMocks(); + m.syncState.night = 250_000_000_000_000n; + m.syncState.dust = 0n; + m.waitForFunds.mockResolvedValue(250_000_000_000_000n); + }); + + it('should apply the undeployed dust fee overhead', async () => { + await build(); + expect(m.builder.withDustOptions).toHaveBeenCalledWith( + expect.objectContaining({ + additionalFeeOverhead: UNDEPLOYED_FEE_OVERHEAD, + }), + ); + }); + + it('should start the wallet without the fire-and-forget fund wait', async () => { + await build(); + expect(m.start).toHaveBeenCalledWith(false); + }); + + it('should expose the provider coin public key', async () => { + const wallet = await build(); + expect(wallet.coinPublicKey).toBe('coin-pk-xyz'); + }); + + it('should delegate stop to the provider', async () => { + const wallet = await build(); + await wallet.stop(); + expect(m.stop).toHaveBeenCalledTimes(1); + }); + + it('should report funded and carry both balances for a NIGHT-holding seed', async () => { + m.waitForFunds.mockResolvedValueOnce(250_000_000_000_000n); + m.syncState.dust = 0n; + const wallet = await build(); + expect(wallet.nightBalance).toBe(250_000_000_000_000n); + expect(wallet.isFunded).toBe(true); + }); + + it('should report unfunded when the seed holds neither NIGHT nor dust', async () => { + m.waitForFunds.mockResolvedValueOnce(0n); + m.syncState.dust = 0n; + const wallet = await build(); + expect(wallet.isFunded).toBe(false); + }); + + it('should report funded on dust alone (NIGHT registered for dust generation)', async () => { + m.waitForFunds.mockResolvedValueOnce(0n); + m.syncState.dust = 4_600_000_000_000_000_000n; + const wallet = await build(); + expect(wallet.nightBalance).toBe(0n); + expect(wallet.isFunded).toBe(true); + }); + + it('should re-sync the wallet before balancing a tx', async () => { + const wallet = await build(); + m.syncWallet.mockClear(); + + const balanced = await wallet.provider.balanceTx('unbalanced' as never); + + // The original balanceTx result flows through unchanged... + expect(balanced).toBe('balanced-tx'); + // ...but a fresh sync runs first, so the tx balances against post-prior-tx + // state (the guard against consecutive same-signer UTXO reuse). + expect(m.syncWallet).toHaveBeenCalledTimes(1); + expect(m.syncWallet.mock.invocationCallOrder[0]).toBeLessThan( + m.balanceTx.mock.invocationCallOrder[0], + ); + }); + + it('should refresh both balances after a top-up', async () => { + m.waitForFunds.mockResolvedValueOnce(0n); + m.syncState.dust = 0n; + const wallet = await build(); + expect(wallet.isFunded).toBe(false); + + // A top-up landed: NIGHT registered for dust, dust now present. + m.syncState.night = 0n; + m.syncState.dust = 4_600_000_000_000_000_000n; + await wallet.refresh(); + expect(wallet.nightBalance).toBe(0n); + expect(wallet.dustBalance).toBe(4_600_000_000_000_000_000n); + expect(wallet.isFunded).toBe(true); + }); +}); diff --git a/contracts/test-utils/harness/test/LiveSimulatorBackend.test.ts b/contracts/test-utils/harness/test/LiveSimulatorBackend.test.ts new file mode 100644 index 000000000..88bc0d7df --- /dev/null +++ b/contracts/test-utils/harness/test/LiveSimulatorBackend.test.ts @@ -0,0 +1,201 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Spies for everything buildContext delegates to, so it runs with no node and +// no artifact on disk. +const { registerSpy, createContextSpy, deploySpy } = vi.hoisted(() => ({ + registerSpy: vi.fn(), + createContextSpy: vi.fn(() => ({ liveContext: true })), + deploySpy: vi.fn(async () => ({ + deployTxData: { public: { contractAddress: 'abc123' } }, + })), +})); + +vi.mock('@openzeppelin/compact-simulator', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + registerLiveBackend: registerSpy, + createLiveContext: createContextSpy, + }; +}); +vi.mock('@midnight-ntwrk/compact-js', () => ({ + CompiledContract: { + make: vi.fn(() => ({ pipe: vi.fn(() => ({ compiled: true })) })), + withWitnesses: vi.fn(() => 'with-witnesses'), + withCompiledFileAssets: vi.fn(() => 'with-assets'), + }, +})); +vi.mock('@midnight-ntwrk/midnight-js-contracts', () => ({ + deployContract: deploySpy, +})); +vi.mock('@midnight-ntwrk/testkit-js', () => ({ + inMemoryPrivateStateProvider: vi.fn(() => ({ inMemory: true })), +})); +vi.mock('@midnight-ntwrk/midnight-js-indexer-public-data-provider', () => ({ + indexerPublicDataProvider: vi.fn(() => ({ publicData: true })), +})); +vi.mock('@midnight-ntwrk/midnight-js-http-client-proof-provider', () => ({ + httpClientProofProvider: vi.fn(() => ({ proof: true })), +})); +vi.mock('@midnight-ntwrk/midnight-js-node-zk-config-provider', () => ({ + NodeZkConfigProvider: class { + constructor(readonly dir: string) {} + }, +})); + +import { LiveSimulatorBackend } from '../LiveSimulatorBackend.js'; + +// register() + the artifactName guard use neither the pool, env, nor loader. +const guardBackend = () => + new LiveSimulatorBackend(undefined as never, undefined as never); + +// A fake pool + injected loader so the deploy path touches no node/artifact. +const fakePool = { + ensureReady: vi.fn(async () => {}), + isKnownAlias: (a?: string | null) => a === 'SIGNER1' || a === 'deployer', + walletFor: (a?: string | null) => ({ wallet: a }), +}; +const loadContract = vi.fn(async () => ({ Contract: class {} })); +const deployBackend = () => + new LiveSimulatorBackend( + fakePool as never, + {} as never, + loadContract as never, + ); + +const REQUEST = { + config: { + artifactName: 'Probe', + witnessesFactory: () => ({}), + defaultPrivateState: () => 'ps0', + contractArgs: (...a: unknown[]) => a, + }, + options: {}, + contractArgs: [] as unknown[], +}; + +/** register the backend and return the callback it handed the simulator. */ +function capturedBuildContext( + b: LiveSimulatorBackend, +): (req: unknown) => Promise { + b.register(); + return registerSpy.mock.calls.at(-1)?.[0] as ( + req: unknown, + ) => Promise; +} + +describe('LiveSimulatorBackend', () => { + beforeEach(() => vi.clearAllMocks()); + + describe('register', () => { + it('should register with the live backend on the first call', () => { + guardBackend().register(); + expect(registerSpy).toHaveBeenCalledTimes(1); + expect(typeof registerSpy.mock.calls[0][0]).toBe('function'); + }); + + it('should not register again on a second call (idempotent)', () => { + const b = guardBackend(); + b.register(); + b.register(); + expect(registerSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('buildContext', () => { + it('should reject a request that is missing an artifactName', async () => { + const buildContext = capturedBuildContext(guardBackend()); + await expect( + buildContext({ config: { artifactName: undefined } }), + ).rejects.toThrow(/artifactName is required/); + }); + + it('should deploy with the deployer wallet and return the live context', async () => { + const buildContext = capturedBuildContext(deployBackend()); + const ctx = await buildContext(REQUEST); + + expect(deploySpy).toHaveBeenCalledTimes(1); + const deployProviders = deploySpy.mock.calls[0][0] as { + walletProvider: { wallet: string }; + }; + expect(deployProviders.walletProvider).toEqual({ wallet: 'deployer' }); + + expect(createContextSpy).toHaveBeenCalledWith( + expect.objectContaining({ contractAddress: 'abc123' }), + ); + expect(ctx).toEqual({ liveContext: true }); + }); + + it('should route an unknown caller alias to the deployer wallet', async () => { + const buildContext = capturedBuildContext(deployBackend()); + await buildContext(REQUEST); + + const { providersFor } = createContextSpy.mock.calls[0][0] as { + providersFor: (a?: string | null) => { + walletProvider: { wallet: string }; + }; + }; + expect(providersFor('OTHER').walletProvider).toEqual({ + wallet: 'deployer', + }); + expect(providersFor('SIGNER1').walletProvider).toEqual({ + wallet: 'SIGNER1', + }); + }); + }); + + describe('deploy retry', () => { + it('should retry once on a transient submission error', async () => { + vi.useFakeTimers(); + try { + deploySpy + .mockRejectedValueOnce(new Error('Transaction submission error')) + .mockResolvedValueOnce({ + deployTxData: { public: { contractAddress: 'retried-ok' } }, + }); + const buildContext = capturedBuildContext(deployBackend()); + const pending = buildContext(REQUEST); + await vi.advanceTimersByTimeAsync(1500); // cover the jittered backoff + await pending; + expect(deploySpy).toHaveBeenCalledTimes(2); + } finally { + vi.useRealTimers(); + } + }); + + it('should not retry a deterministic node rejection (RPC 1010)', async () => { + const rejection = new Error( + '1010: Invalid Transaction: Custom error: 103', + ); + deploySpy.mockRejectedValueOnce(rejection); + const buildContext = capturedBuildContext(deployBackend()); + await expect(buildContext(REQUEST)).rejects.toBe(rejection); + expect(deploySpy).toHaveBeenCalledTimes(1); + }); + + it('should not retry when 1010 is nested in the cause chain', async () => { + const rpc = new Error('1010: Invalid Transaction: Custom error: 103'); + const inner = new Error('Transaction submission failed', { cause: rpc }); + const top = new Error('Transaction submission error', { cause: inner }); + deploySpy.mockRejectedValueOnce(top); + const buildContext = capturedBuildContext(deployBackend()); + await expect(buildContext(REQUEST)).rejects.toBe(top); + expect(deploySpy).toHaveBeenCalledTimes(1); + }); + + it('should not retry a FiberFailure whose 1010 is only in toString()', async () => { + // effect's FiberFailure hides its cause behind a Symbol; the 1010 text is + // reachable only via toString(), not `.message` or `.cause`. + const fiberFailure = { + message: 'Transaction submission error', + toString: () => + 'FiberFailure: 1010: Invalid Transaction: Custom error: 103', + }; + deploySpy.mockRejectedValueOnce(fiberFailure); + const buildContext = capturedBuildContext(deployBackend()); + await expect(buildContext(REQUEST)).rejects.toBe(fiberFailure); + expect(deploySpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/contracts/test-utils/harness/test/NativeShieldedTokenTracker.test.ts b/contracts/test-utils/harness/test/NativeShieldedTokenTracker.test.ts new file mode 100644 index 000000000..ae1e2c269 --- /dev/null +++ b/contracts/test-utils/harness/test/NativeShieldedTokenTracker.test.ts @@ -0,0 +1,55 @@ +import { isLiveBackend } from '@openzeppelin/compact-simulator'; +import { describe, expect, it } from 'vitest'; +import { + contractOwner, + getQualifiedShieldedCoinInfo, + type ShieldedOwner, +} from '../NativeShieldedTokenTracker.js'; + +/** + * Dry surface of the coin tracker — the backend-aware helpers specs call. + * + * `contractOwner` and the dry branch of `getQualifiedShieldedCoinInfo` are pure + * (no indexer). The live index recovery (`decodeOutput` / `resolve` against the + * indexer's event stream) needs a real serialized event and a running node, so + * it's exercised by the live path, not here. + */ +describe('coin tracker (dry surface)', () => { + describe('contractOwner', () => { + it('should map a deployed simulator to its contract-address owner', () => { + const owner = contractOwner({ + _backend: { contractAddress: 'deadbeef' }, + }); + expect(owner).toStrictEqual({ kind: 'contract', address: 'deadbeef' }); + }); + }); + + // These assert the *dry* passthrough (a placeholder `mt_index` of `0n`, no + // indexer). On the live backend `getQualifiedShieldedCoinInfo` instead resolves + // a real commitment, so the dry assertions don't apply — the live path is + // exercised by the contract live specs, not here. + describe.skipIf(isLiveBackend())( + 'getQualifiedShieldedCoinInfo (dry backend)', + () => { + const coin = { + nonce: new Uint8Array(32).fill(7), + color: new Uint8Array(32).fill(1), + value: 1000n, + }; + + it('should return the coin with a placeholder mt_index of 0n', async () => { + const owner: ShieldedOwner = { kind: 'contract', address: 'abc' }; + const qualified = await getQualifiedShieldedCoinInfo(owner, coin); + expect(qualified).toStrictEqual({ ...coin, mt_index: 0n }); + }); + + it('should not consult the indexer for a wallet owner on the dry backend', async () => { + // No indexer is running here; a dry resolve must be a pure passthrough. + const owner: ShieldedOwner = { kind: 'wallet', coinPublicKey: 'pk' }; + const qualified = await getQualifiedShieldedCoinInfo(owner, coin); + expect(qualified.mt_index).toBe(0n); + expect(qualified.value).toBe(1000n); + }); + }, + ); +}); diff --git a/contracts/test-utils/harness/test/WalletPool.test.ts b/contracts/test-utils/harness/test/WalletPool.test.ts new file mode 100644 index 000000000..d02e43664 --- /dev/null +++ b/contracts/test-utils/harness/test/WalletPool.test.ts @@ -0,0 +1,301 @@ +import { isLiveBackend } from '@openzeppelin/compact-simulator'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from 'vitest'; +import { DustFundingError } from '../dust.js'; +import { + coinPkEnv, + MAX_LIVE_WORKERS, + type PooledWallet, + WALLET_SEEDS, + type WalletBuilder, + WalletPool, + walletSeedsFor, +} from '../WalletPool.js'; + +// A testkit-free stand-in for a built wallet. `provider` is a sentinel we can +// identify by alias so `walletFor` resolution is checkable without a node. +interface FakeWallet extends PooledWallet { + stopped: boolean; +} +function fakeWallet(alias: string): FakeWallet { + return { + provider: { alias } as unknown as PooledWallet['provider'], + coinPublicKey: `pk-${alias}`, + stopped: false, + stop() { + this.stopped = true; + return Promise.resolve(); + }, + }; +} + +/** A builder that records what it built, so tests can assert dedup + teardown. */ +function recordingBuilder(): { + build: WalletBuilder; + built: string[]; + created: Map; +} { + const built: string[] = []; + const created = new Map(); + const build: WalletBuilder = async (alias) => { + built.push(alias); + const w = fakeWallet(alias); + created.set(alias, w); + return w; + }; + return { build, built, created }; +} + +const providerAlias = (p: PooledWallet['provider']): string => + (p as unknown as { alias: string }).alias; + +const SEEDS = { deployer: 's-dep', SIGNER1: 's-1', SIGNER2: 's-2' }; + +// The pool publishes MIDNIGHT__COIN_PK into process.env — clear them so +// tests don't leak the fake keys into each other or the live projects. +afterEach(() => { + for (const alias of Object.keys(SEEDS)) delete process.env[coinPkEnv(alias)]; +}); + +describe('WalletPool', () => { + describe('ensureReady', () => { + it('should build every seed once and publish each coin public key', async () => { + const { build, built } = recordingBuilder(); + await new WalletPool(SEEDS, build).ensureReady(); + + expect([...built].sort()).toEqual(['SIGNER1', 'SIGNER2', 'deployer']); + expect(process.env[coinPkEnv('deployer')]).toBe('pk-deployer'); + expect(process.env[coinPkEnv('SIGNER1')]).toBe('pk-SIGNER1'); + expect(process.env[coinPkEnv('SIGNER2')]).toBe('pk-SIGNER2'); + }); + + it('should not rebuild wallets on a second call', async () => { + const { build, built } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + await pool.ensureReady(); + await pool.ensureReady(); + expect(built.length).toBe(3); + }); + + it('should propagate a builder failure such as an unfunded seed', async () => { + // The real builder throws DustFundingError when a seed can't pay fees; + // ensureReady must surface it so the live setup fails fast. + const build: WalletBuilder = (alias) => { + if (alias === 'SIGNER1') throw new DustFundingError('SIGNER1', 0n); + return Promise.resolve(fakeWallet(alias)); + }; + await expect(new WalletPool(SEEDS, build).ensureReady()).rejects.toThrow( + DustFundingError, + ); + }); + }); + + describe('walletFor', () => { + it('should resolve a known alias to its own wallet', async () => { + const { build } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + await pool.ensureReady(); + expect(providerAlias(pool.walletFor('SIGNER1'))).toBe('SIGNER1'); + }); + + it('should fall back to the deployer for an unknown or empty alias', async () => { + const { build } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + await pool.ensureReady(); + expect(providerAlias(pool.walletFor('OTHER'))).toBe('deployer'); + expect(providerAlias(pool.walletFor(null))).toBe('deployer'); + expect(providerAlias(pool.walletFor(undefined))).toBe('deployer'); + }); + + it('should throw before the pool is ready', () => { + const { build } = recordingBuilder(); + expect(() => new WalletPool(SEEDS, build).walletFor('SIGNER1')).toThrow( + /not initialized/, + ); + }); + }); + + describe('isKnownAlias', () => { + it('should recognize a pooled seed', () => { + const { build } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + expect(pool.isKnownAlias('SIGNER1')).toBe(true); + expect(pool.isKnownAlias('deployer')).toBe(true); + }); + + it('should not recognize an unknown or empty alias', () => { + const { build } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + expect(pool.isKnownAlias('OTHER')).toBe(false); + expect(pool.isKnownAlias(null)).toBe(false); + expect(pool.isKnownAlias(undefined)).toBe(false); + }); + }); + + describe('reset', () => { + it('should stop every wallet and empty the pool', async () => { + const { build, created } = recordingBuilder(); + const pool = new WalletPool(SEEDS, build); + await pool.ensureReady(); + + await pool.reset(); + + for (const w of created.values()) expect(w.stopped).toBe(true); + expect(() => pool.walletFor('SIGNER1')).toThrow(/not initialized/); + }); + }); + + describe('walletSeedsFor / WALLET_SEEDS', () => { + // A 64-hex seed with the given last byte(s), mirroring WalletPool's `seed`. + const s = (hex: string): string => '0'.repeat(64 - hex.length) + hex; + // The exact-value assertions assume the deployer isn't pinned; save/restore + // any real override so the suite stays hermetic. + const savedSeed = process.env.MIDNIGHT_WALLET_SEED; + beforeEach(() => { + delete process.env.MIDNIGHT_WALLET_SEED; + }); + afterAll(() => { + if (savedSeed === undefined) delete process.env.MIDNIGHT_WALLET_SEED; + else process.env.MIDNIGHT_WALLET_SEED = savedSeed; + }); + + it('should give each worker a genesis deployer and derived signers', () => { + expect(walletSeedsFor(1)).toStrictEqual({ + deployer: s('01'), + SIGNER1: s('11'), + SIGNER2: s('12'), + SIGNER3: s('13'), + }); + expect(walletSeedsFor(2)).toStrictEqual({ + deployer: s('02'), + SIGNER1: s('21'), + SIGNER2: s('22'), + SIGNER3: s('23'), + }); + expect(walletSeedsFor(3)).toStrictEqual({ + deployer: s('03'), + SIGNER1: s('31'), + SIGNER2: s('32'), + SIGNER3: s('33'), + }); + }); + + it('should produce 64-hex seeds disjoint across workers', () => { + const all: string[] = []; + for (let w = 1; w <= MAX_LIVE_WORKERS; w++) { + for (const value of Object.values(walletSeedsFor(w))) { + expect(value).toMatch(/^[0-9a-f]{64}$/); + all.push(value); + } + } + expect(new Set(all).size).toBe(all.length); + }); + + it('should keep signer seeds out of the genesis set', () => { + const genesis = new Set(['01', '02', '03', '04'].map(s)); + for (let w = 1; w <= MAX_LIVE_WORKERS; w++) { + const { deployer: _deployer, ...signers } = walletSeedsFor(w); + for (const value of Object.values(signers)) { + expect(genesis.has(value)).toBe(false); + } + } + }); + + it('should override only worker 1 deployer via MIDNIGHT_WALLET_SEED', () => { + const pinned = 'ff'.repeat(32); + process.env.MIDNIGHT_WALLET_SEED = pinned; + expect(walletSeedsFor(1).deployer).toBe(pinned); + expect(walletSeedsFor(2).deployer).toBe(s('02')); + expect(walletSeedsFor(3).deployer).toBe(s('03')); + }); + + it('WALLET_SEEDS should be the worker-1 pool', () => { + expect(Object.keys(WALLET_SEEDS).sort()).toEqual([ + 'SIGNER1', + 'SIGNER2', + 'SIGNER3', + 'deployer', + ]); + const w1 = walletSeedsFor(1); + expect(WALLET_SEEDS.SIGNER1).toBe(w1.SIGNER1); + expect(WALLET_SEEDS.SIGNER2).toBe(w1.SIGNER2); + expect(WALLET_SEEDS.SIGNER3).toBe(w1.SIGNER3); + expect(WALLET_SEEDS.deployer).toMatch(/^[0-9a-f]{64}$/); + }); + + it('should map the deployer alias to the DEPLOYER env var', () => { + expect(coinPkEnv('deployer')).toBe('MIDNIGHT_DEPLOYER_COIN_PK'); + expect(coinPkEnv('SIGNER1')).toBe('MIDNIGHT_SIGNER1_COIN_PK'); + }); + }); +}); + +// Live smoke against the local stack (`make env-up`): builds the real pooled +// wallets and proves the dust fix end-to-end (the gate passes or names the +// unfunded seed). Runs only on `MIDNIGHT_BACKEND=live`; skipped on the dry run. +describe.runIf(isLiveBackend())('WalletPool (live smoke)', () => { + let pool: WalletPool; + let published: Record; + + beforeAll(async () => { + // Load the live-only deps here so the dry run never imports testkit. + const [{ setNetworkId }, { createLogger }, { FundedWallet }, { localEnv }] = + await Promise.all([ + import('@midnight-ntwrk/midnight-js-network-id'), + import('@midnight-ntwrk/testkit-js'), + import('../FundedWallet.js'), + import('../network.js'), + ]); + setNetworkId('undeployed'); + const env = localEnv(); + // Mirror the live setup: build this worker's own disjoint partition. + const worker = Number(process.env.VITEST_POOL_ID ?? '1'); + const seeds = walletSeedsFor(worker); + const logger = createLogger(`logs/live-harness-w${worker}.log`); + pool = new WalletPool(seeds, (alias, walletSeed) => + FundedWallet.build(env, alias, walletSeed, logger), + ); + // Throws DustFundingError (naming the alias) if a seed can't pay fees. + await pool.ensureReady(); + // Snapshot the published keys now, before the dry afterEach clears any. + published = Object.fromEntries( + Object.keys(seeds).map((alias) => [alias, process.env[coinPkEnv(alias)]]), + ); + }); + + afterAll(async () => { + await pool?.reset(); + }); + + it('should publish a distinct, non-empty coin public key for every pooled wallet', () => { + const keys = Object.values(published); + for (const key of keys) { + expect(typeof key).toBe('string'); + expect((key as string).length).toBeGreaterThan(0); + } + expect(new Set(keys).size).toBe(keys.length); + }); + + it('should resolve each signer to its own funded wallet matching the published key', () => { + for (const alias of ['SIGNER1', 'SIGNER2', 'SIGNER3']) { + expect(String(pool.walletFor(alias).getCoinPublicKey())).toBe( + published[alias], + ); + } + }); + + it('should fall back an unknown alias to the deployer wallet', () => { + expect(pool.walletFor('OTHER')).toBe(pool.walletFor('deployer')); + }); + + it('should resolve a known signer to a wallet distinct from the deployer', () => { + expect(pool.walletFor('SIGNER1')).not.toBe(pool.walletFor('deployer')); + }); +}); diff --git a/contracts/test-utils/harness/test/dust.test.ts b/contracts/test-utils/harness/test/dust.test.ts new file mode 100644 index 000000000..b1ec5aaf4 --- /dev/null +++ b/contracts/test-utils/harness/test/dust.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { assertFunded, DustFundingError, MIN_WALLET_NIGHT } from '../dust.js'; + +describe('dust policy', () => { + describe('DustFundingError', () => { + it('should carry the alias and balance and name both in the message', () => { + const err = new DustFundingError('SIGNER2', 0n); + expect(err).toBeInstanceOf(Error); + expect(err.name).toBe('DustFundingError'); + expect(err.alias).toBe('SIGNER2'); + expect(err.nightBalance).toBe(0n); + expect(err.message).toContain('SIGNER2'); + expect(err.message).toContain('0'); + }); + }); + + describe('assertFunded', () => { + it('should throw for a zero-NIGHT wallet', () => { + expect(() => assertFunded('SIGNER1', 0n)).toThrow(DustFundingError); + }); + + it('should throw just below the minimum', () => { + expect(() => assertFunded('SIGNER1', MIN_WALLET_NIGHT - 1n)).toThrow( + DustFundingError, + ); + }); + + it('should not throw exactly at the minimum', () => { + expect(() => assertFunded('deployer', MIN_WALLET_NIGHT)).not.toThrow(); + }); + + it('should not throw for a well-funded wallet', () => { + expect(() => + assertFunded('deployer', 250_000_000_000_000n), + ).not.toThrow(); + }); + }); +}); diff --git a/contracts/test-utils/harness/test/liveGlobalSetup.test.ts b/contracts/test-utils/harness/test/liveGlobalSetup.test.ts new file mode 100644 index 000000000..d54619c1c --- /dev/null +++ b/contracts/test-utils/harness/test/liveGlobalSetup.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from 'vitest'; +import { countCoinEvents, lockHolderState } from '../live.globalSetup.js'; + +describe('live.globalSetup', () => { + describe('lockHolderState', () => { + const alive = () => true; + const dead = () => false; + + it('is reentrant when the lock holds our own pid', () => { + expect(lockHolderState({ pid: 42, startedAt: 'x' }, 42, dead)).toBe( + 'reentrant', + ); + }); + + it('is held when a live process owns the lock', () => { + expect(lockHolderState({ pid: 7, startedAt: 'x' }, 42, alive)).toBe( + 'held', + ); + }); + + it('is stale when the owning pid is gone', () => { + expect(lockHolderState({ pid: 7, startedAt: 'x' }, 42, dead)).toBe( + 'stale', + ); + }); + }); + + describe('countCoinEvents', () => { + it('scans every block when the node is clean', async () => { + const fetchWindow = vi.fn(async () => 0); + const count = await countCoinEvents(fetchWindow, 1, 100, 0, { + chunk: 10, + concurrency: 4, + }); + expect(count).toBe(0); + expect(fetchWindow).toHaveBeenCalledTimes(10); // 100 blocks / chunk 10 + }); + + it('early-exits as soon as the count exceeds the threshold', async () => { + const fetchWindow = vi.fn(async (from: number) => (from === 1 ? 3 : 0)); + const count = await countCoinEvents(fetchWindow, 1, 1000, 0, { + chunk: 10, + concurrency: 1, + }); + expect(count).toBe(3); + expect(fetchWindow).toHaveBeenCalledTimes(1); // stopped after batch 1 + }); + + it('does not trip when the count only reaches the threshold', async () => { + const fetchWindow = vi.fn(async (from: number) => (from === 1 ? 2 : 0)); + const count = await countCoinEvents(fetchWindow, 1, 30, 2, { + chunk: 10, + concurrency: 1, + }); + expect(count).toBe(2); // == threshold, no early exit + expect(fetchWindow).toHaveBeenCalledTimes(3); + }); + + it('returns 0 for an empty range (head before from)', async () => { + const fetchWindow = vi.fn(async () => 0); + expect(await countCoinEvents(fetchWindow, 1, 0, 0)).toBe(0); + expect(fetchWindow).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/contracts/test-utils/harness/test/network.test.ts b/contracts/test-utils/harness/test/network.test.ts new file mode 100644 index 000000000..09249f402 --- /dev/null +++ b/contracts/test-utils/harness/test/network.test.ts @@ -0,0 +1,50 @@ +import { LocalTestConfiguration } from '@midnight-ntwrk/testkit-js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { localEnv } from '../network.js'; + +describe('network config', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + describe('PORTS', () => { + it('should default to the local stack ports', async () => { + // Clear any real overrides so a dev/CI env with MIDNIGHT_*_PORT set does + // not fail this default assertion; re-import so PORTS re-evaluates. + vi.stubEnv('MIDNIGHT_INDEXER_PORT', undefined); + vi.stubEnv('MIDNIGHT_NODE_PORT', undefined); + vi.stubEnv('MIDNIGHT_PROOF_SERVER_PORT', undefined); + vi.resetModules(); + const { PORTS: defaults } = await import('../network.js'); + expect(defaults).toStrictEqual({ + indexer: 8088, + node: 9944, + proofServer: 6300, + }); + }); + + it('should override each port from its env var', async () => { + vi.stubEnv('MIDNIGHT_INDEXER_PORT', '18088'); + vi.stubEnv('MIDNIGHT_NODE_PORT', '19944'); + vi.stubEnv('MIDNIGHT_PROOF_SERVER_PORT', '16300'); + vi.resetModules(); + const { PORTS: overridden } = await import('../network.js'); + expect(overridden).toStrictEqual({ + indexer: 18088, + node: 19944, + proofServer: 16300, + }); + }); + }); + + describe('localEnv', () => { + it('should build a testkit LocalTestConfiguration', () => { + expect(localEnv()).toBeInstanceOf(LocalTestConfiguration); + }); + + it('should return a fresh config each call', () => { + expect(localEnv()).not.toBe(localEnv()); + }); + }); +}); diff --git a/contracts/vitest.config.ts b/contracts/vitest.config.ts index 7df70de2e..cc0bb332f 100644 --- a/contracts/vitest.config.ts +++ b/contracts/vitest.config.ts @@ -1,11 +1,62 @@ import { configDefaults, defineConfig } from 'vitest/config'; +/** + * One config, one project per test flavour. Select with `--project `: + * + * - `unit` — dry simulator run of the per-module specs (`src/**`). + * - `unit-live` — the same specs against the local stack (`make env-up`), + * via the live backend registered in `live.setup`. Driven + * by `MIDNIGHT_BACKEND=live` (set by the `test:live` script). + * - `integration` — composed-contract specs (`test/integration/specs`). + * - `harness` — dry unit tests for the live harness itself (`test-utils`). + * - `harness-live` — live smoke that the real wallet pool funds + resolves on + * the node, before the expensive contract live specs. + * + * Coverage is a root-level concern (applies to whichever project runs with + * `--coverage`); the `unit` project is the one gated in CI. + */ + +const NODE = { globals: true, environment: 'node' as const }; +const ARCHIVE_EXCLUDE = [...configDefaults.exclude, 'src/archive/**']; + +// Generous timeouts every live project shares (real proofs + on-chain finality +// are slow). Split out from the sequential/parallel knobs below. +const LIVE_TIMEOUTS = { + testTimeout: 600_000, + hookTimeout: 300_000, +} as const; + +// Fully-sequential live run: no file parallelism, no concurrent tests. Used by +// `harness-live` (a single shared node, no per-worker wallet partition). +const LIVE_SEQUENTIAL = { + ...LIVE_TIMEOUTS, + fileParallelism: false, + sequence: { concurrent: false }, +} as const; + +// `unit-live` parallelism. Each vitest worker drives a disjoint wallet +// partition (see WalletPool.walletSeedsFor), so up to MIDNIGHT_LIVE_WORKERS +// spec files run concurrently against the shared node. Tests WITHIN a file stay +// sequential (`sequence.concurrent: false`). Bounded by the 3 genesis-funded +// deployer seeds; forced to 1 when MIDNIGHT_WALLET_SEED pins a custom deployer +// (it would collide across workers). +const MAX_LIVE_WORKERS = 3; +const liveWorkers = process.env.MIDNIGHT_WALLET_SEED + ? 1 + : Math.min( + MAX_LIVE_WORKERS, + Math.max( + 1, + Number(process.env.MIDNIGHT_LIVE_WORKERS ?? MAX_LIVE_WORKERS) || 1, + ), + ); + +// Publish the resolved count so `live.setup` can print `w/` in its +// per-worker banner. Workers inherit this env at fork time. +process.env.MIDNIGHT_LIVE_WORKERS = String(liveWorkers); + export default defineConfig({ test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts'], - exclude: [...configDefaults.exclude, 'src/archive/**'], reporters: 'verbose', coverage: { provider: 'v8', @@ -39,5 +90,60 @@ export default defineConfig({ }, }, }, + projects: [ + { + test: { + ...NODE, + name: 'unit', + include: ['src/**/*.test.ts'], + exclude: ARCHIVE_EXCLUDE, + }, + }, + { + test: { + ...NODE, + ...LIVE_TIMEOUTS, + name: 'unit-live', + include: ['src/**/*.test.ts'], + exclude: ARCHIVE_EXCLUDE, + // Fail fast (before any wallet build) if the node is dirty or another + // live run holds the lock. See `live.globalSetup`. + globalSetup: ['./test-utils/harness/live.globalSetup.ts'], + setupFiles: ['./test-utils/harness/live.setup.ts'], + // Run up to `liveWorkers` spec files concurrently (fileParallelism + // defaults to true). `groupOrder` keeps this project in its own + // scheduler group: a multi-project run otherwise rejects two projects + // that share a group but differ in `maxWorkers`. + maxWorkers: liveWorkers, + sequence: { concurrent: false, groupOrder: 1 }, + }, + }, + { + test: { + ...NODE, + name: 'integration', + include: ['test/integration/specs/**/*.spec.ts'], + }, + }, + { + test: { + ...NODE, + name: 'harness', + include: ['test-utils/**/*.test.ts'], + }, + }, + { + // Same files as `harness`; `MIDNIGHT_BACKEND=live` (set by the + // `test:harness:live` script) flips the `isLiveBackend()`-gated blocks + // (e.g. the WalletPool live smoke) on. LIVE timeouts + sequential. + test: { + ...NODE, + ...LIVE_SEQUENTIAL, + name: 'harness-live', + include: ['test-utils/**/*.test.ts'], + globalSetup: ['./test-utils/harness/live.globalSetup.ts'], + }, + }, + ], }, }); diff --git a/contracts/vitest.integration.config.ts b/contracts/vitest.integration.config.ts deleted file mode 100644 index 24a7d3d85..000000000 --- a/contracts/vitest.integration.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -// Integration specs compose multiple production modules into a single contract -// and drive them through the simulator. Kept separate from the unit `test` -// config (which scans `src/**/*.test.ts`). -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['test/integration/specs/**/*.spec.ts'], - reporters: 'verbose', - }, -}); diff --git a/contracts/vitest.live.config.ts b/contracts/vitest.live.config.ts deleted file mode 100644 index 52377aad3..000000000 --- a/contracts/vitest.live.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { configDefaults, defineConfig } from 'vitest/config'; - -// Live-backend run of the unit specs against the local stack (`make env-up`). -// Same spec files as the default dry `test`; only the backend (via -// `MIDNIGHT_BACKEND=live`) and this config differ — each `await Sim.create()` -// deploys + attaches a real contract through the registered live harness. -// -// Single fork + no parallelism: every deploy is signed by the one genesis-funded -// account, so specs must run sequentially to avoid nonce races. Generous -// timeouts: each deploy + impure call is a real proof + on-chain tx. -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts'], - exclude: [...configDefaults.exclude, 'src/archive/**'], - setupFiles: ['./test/integration/_harness/live.setup.ts'], - reporters: 'verbose', - testTimeout: 180_000, - hookTimeout: 300_000, - fileParallelism: false, - sequence: { concurrent: false }, - }, -}); diff --git a/local-env.yml b/local-env.yml index e933d0748..ce4f135f1 100644 --- a/local-env.yml +++ b/local-env.yml @@ -7,12 +7,14 @@ services: - '6300:6300' environment: RUST_BACKTRACE: 'full' + # No healthcheck: this image is a minimal Nix build with no shell and no + # curl/wget, so any in-container HTTP probe fails with "executable not found" + # and `docker compose up --wait` aborts even though the server is serving + # (`GET :6300/version` returns 200 from the host). The server starts + # immediately and is ready long before node/indexer (whose healthchecks do + # work) finish; the harness/midnight-js also retry proof requests. healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] - interval: 10s - timeout: 5s - retries: 20 - start_period: 10s + disable: true indexer: # Pinned: `latest` drifted to the Cardano-bridge indexer line, whose config diff --git a/package.json b/package.json index fdf9ee283..2286d6640 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,26 @@ "compact:access": "turbo run compact:access --filter=@openzeppelin/compact-contracts --log-prefix=none", "compact:archive": "turbo run compact:archive --filter=@openzeppelin/compact-contracts --log-prefix=none", "compact:crypto": "turbo run compact:crypto --filter=@openzeppelin/compact-contracts --log-prefix=none", + "compact:multisig": "turbo run compact:multisig --filter=@openzeppelin/compact-contracts --log-prefix=none", "compact:security": "turbo run compact:security --filter=@openzeppelin/compact-contracts --log-prefix=none", "compact:token": "turbo run compact:token --filter=@openzeppelin/compact-contracts --log-prefix=none", "compact:utils": "turbo run compact:utils --filter=@openzeppelin/compact-contracts --log-prefix=none", + "compact:integration": "turbo run compact:integration --filter=@openzeppelin/compact-contracts --log-prefix=none", "build": "turbo run build --log-prefix=none", - "test": "turbo run test --filter=@openzeppelin/compact-contracts --log-prefix=none", + "test": "SKIP_ZK=true turbo run test --filter=@openzeppelin/compact-contracts --env-mode=loose --log-prefix=none", + "test:coverage": "SKIP_ZK=true turbo run test:coverage --filter=@openzeppelin/compact-contracts --env-mode=loose --log-prefix=none", + "test:live": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/test-live.ts", + "test:integration": "SKIP_ZK=true turbo run test:integration --filter=@openzeppelin/compact-contracts --env-mode=loose --log-prefix=none", + "test:harness": "turbo run test:harness --filter=@openzeppelin/compact-contracts --env-mode=loose --log-prefix=none", + "test:harness:live": "turbo run test:harness:live --filter=@openzeppelin/compact-contracts --env-mode=loose --log-prefix=none", "fmt-and-lint": "biome check .", "fmt-and-lint:fix": "biome check . --write", "fmt-and-lint:ci": "biome ci . --no-errors-on-unmatched", + "env:up": "make env-up", + "env:down": "make env-down", + "env:status": "make env-status", + "env:logs": "make env-logs", + "env:logs:clean": "make env-logs-clean", "types": "turbo run types", "clean": "turbo run clean" }, @@ -30,7 +42,8 @@ "minimatch": "9.0.6", "postcss": "8.5.10", "picomatch": "4.0.4", - "ip-address": "10.1.1" + "ip-address": "10.1.1", + "@midnight-ntwrk/wallet-sdk-facade": "4.0.1" }, "dependencies": { "@midnight-ntwrk/compact-runtime": "0.16.0" diff --git a/scripts/keyIntegrity.ts b/scripts/keyIntegrity.ts new file mode 100644 index 000000000..f2eb5a48e --- /dev/null +++ b/scripts/keyIntegrity.ts @@ -0,0 +1,92 @@ +import { existsSync, readdirSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** Recursively collect `` basenames of every `.compact` under `root`. + * The compiler names each artifact dir after the source file's basename, so this + * is the set of contract names the current tree can legitimately produce. */ +function compactContractNames(root: string): Set { + const names = new Set(); + if (!existsSync(root)) return names; + const walk = (dir: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) walk(p); + else if (entry.name.endsWith('.compact')) + names.add(entry.name.slice(0, -'.compact'.length)); + } + }; + walk(root); + return names; +} + +function collectEmptyKeys(dir: string, out: string[]): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectEmptyKeys(p, out); + } else if ( + (entry.name.endsWith('.verifier') || entry.name.endsWith('.prover')) && + statSync(p).size === 0 + ) { + out.push(p); + } + } +} + +/** + * Find 0-byte ZK key files (`*.verifier` / `*.prover`) under `artifactsRoot`. + * + * Compile can report success (a turbo cache hit, or a compiler that exits 0) + * while leaving a truncated key on disk — an interrupted/killed compile, or a + * turbo cache-restore racing a concurrent compile over the shared `artifacts/` + * tree (OpenZeppelin/compact-contracts#675). A 0-byte `_deposit.verifier` makes a + * real deploy fail in `beforeAll`, which vitest turns into a silent whole-suite + * skip. Callers check this before starting the live stack. + * + * When `sourceRoot` is given, only contracts that still have a `.compact` source + * under it are checked, so stale orphan artifact dirs (source deleted, keys never + * rebuilt) do not false-positive. Omit it to scan every contract dir. + * + * @param artifactsRoot - artifact tree to scan (e.g. `contracts/artifacts`) + * @param sourceRoot - optional source tree to scope by (e.g. `contracts/src`) + * @returns absolute paths of empty key files; empty array means all good + */ +export function emptyKeyArtifacts( + artifactsRoot: string, + sourceRoot?: string, +): string[] { + if (!existsSync(artifactsRoot)) return []; + const live = sourceRoot ? compactContractNames(sourceRoot) : undefined; + const empty: string[] = []; + for (const contract of readdirSync(artifactsRoot, { withFileTypes: true })) { + if (!contract.isDirectory()) continue; + if (live && !live.has(contract.name)) continue; // skip stale orphans + collectEmptyKeys(path.join(artifactsRoot, contract.name), empty); + } + return empty; +} + +// Standalone CLI: `node scripts/keyIntegrity.ts` checks the repo's artifacts +// against its sources and exits 1 if any live contract has a truncated key. +const selfPath = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === selfPath) { + const repoRoot = path.resolve(path.dirname(selfPath), '..'); + const contracts = path.join(repoRoot, 'contracts'); + const bad = emptyKeyArtifacts( + path.join(contracts, 'artifacts'), + path.join(contracts, 'src'), + ); + if (bad.length === 0) { + console.log('ZK keys OK — no truncated (0-byte) .verifier/.prover files.'); + } else { + console.log('Truncated (0-byte) ZK key(s) found:'); + for (const k of bad) console.log(` ✗ ${path.relative(repoRoot, k)}`); + console.log( + '\nDrain the turbo cache and recompile serially — a parallel recompile ' + + 'can re-poison the cache (OpenZeppelin/compact-contracts#675):\n' + + ' rm -rf .turbo/cache && yarn compact --concurrency=1', + ); + } + process.exit(bad.length === 0 ? 0 : 1); +} diff --git a/scripts/test-live.ts b/scripts/test-live.ts new file mode 100644 index 000000000..18808bb54 --- /dev/null +++ b/scripts/test-live.ts @@ -0,0 +1,478 @@ +import { spawnSync } from 'node:child_process'; +import { + appendFileSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { emptyKeyArtifacts } from './keyIntegrity.ts'; + +/** + * Live-test orchestrator: runs each category (`src/`) sequentially, + * each on a freshly reset node, then verifies any failures with a second round. + * + * The live tests all run against one shared node, so state left by an earlier + * test can make a later one fail (a coin re-spent against stale node state is + * rejected with node "Custom error: 103"). A file that fails during a busy full + * run may therefore pass in isolation on a fresh node. Hence two rounds: + * + * Round 1: compile + harness smoke once, then per category: reset the stack + * and run that category's files (parallel workers). Collect the + * files that failed from the JSON reporter. + * Round 2: for each failed file, reset the stack and re-run just that file + * on its own (one worker), so no earlier round-2 file can dirty the + * node under a later one. + * + * A file that fails round 1 but passes round 2 is FLAKY (an environment + * artifact); one that fails both — or never reports in round 2 — is a REAL + * failure. Exit 0 unless there is a real failure, so an env flake never turns + * the run red — but it is reported loudly. + * + * Why a script and not turbo tasks: turbo models a DAG of stateless, + * cacheable tasks, and a live run needs stateful orchestration that a task + * graph cannot express: + * - the two-round flake classification above (re-run failures, classify, + * exit 0 on flaky-only); + * - docker lifecycle between categories and rounds (`make env-up`) against + * ONE shared node — parallel turbo tasks would race over it; + * - ZK-key integrity self-heal (turbo's own poisoned cache, #675); + * - infra-vs-test exit codes (2 vs 1), the pid lock, CI verdict summaries. + * Turbo still runs where the DAG helps: the compile and harness-smoke steps + * below go through it (cached keygen, dependency ordering). + * + * Usage (via the root package.json scripts): + * yarn test:live # every live-ready category + * yarn test:live multisig # one category + * yarn test:live multisig Forwarder # files within a category + * yarn test:live --list # live-ready categories (JSON) + * + * Node runs this .ts directly (type stripping); only `node:` builtins. + */ + +const REPO_ROOT = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const CONTRACTS = path.join(REPO_ROOT, 'contracts'); +const SRC = path.join(CONTRACTS, 'src'); +const LOGS = path.join(REPO_ROOT, 'logs'); +const VITEST = path.join(REPO_ROOT, 'node_modules', '.bin', 'vitest'); +const PROGRESS_REPORTER = path.join( + CONTRACTS, + 'test-utils/harness/liveProgressReporter.ts', +); +const VERIFY_LOCK = path.join(LOGS, '.live-verify.lock'); + +// `archive` is excluded from the unit/unit-live projects (see vitest.config). +const EXCLUDED_CATEGORIES = new Set(['archive']); + +// Categories whose specs have been refactored for the live backend. The others +// still assume dry-only semantics (e.g. `.as()` identities derived from alias +// labels, which the live wallet pool cannot impersonate) and join this list as +// they are refactored, PR by PR. +const LIVE_READY = new Set(['multisig']); + +interface JsonTestResult { + readonly name: string; + readonly status: string; +} +interface JsonReport { + readonly testResults?: readonly JsonTestResult[]; +} +interface LockInfo { + readonly pid: number; + readonly startedAt: string; +} + +const rel = (abs: string): string => path.relative(REPO_ROOT, abs); +const r1Json = (category: string): string => + path.join(LOGS, `live-r1-${category}.json`); +const r2Json = (file: string): string => + path.join( + LOGS, + `live-r2-${path.basename(file).replace(/\.test\.ts$/, '')}.json`, + ); + +function banner(message: string): void { + const rule = '═'.repeat(64); + console.log(`\n${rule}\n${message}\n${rule}`); +} + +/** Append markdown to the GitHub Actions job summary (no-op outside CI). */ +function appendJobSummary(markdown: string): void { + const summaryPath = process.env.GITHUB_STEP_SUMMARY; + if (!summaryPath) return; + appendFileSync(summaryPath, `${markdown}\n`); +} + +/** Emit a GitHub Actions warning annotation (no-op outside CI). */ +function ciWarn(file: string, message: string): void { + if (process.env.GITHUB_ACTIONS !== 'true') return; + console.log(`::warning file=${file}::${message}`); +} + +/** `src/` subdirectories that contain test files (future categories join + * automatically; no hardcoded list to maintain). */ +function liveCategories(): string[] { + const hasTests = (dir: string): boolean => + readdirSync(dir, { withFileTypes: true }).some((entry) => + entry.isDirectory() + ? hasTests(path.join(dir, entry.name)) + : entry.name.endsWith('.test.ts'), + ); + return readdirSync(SRC, { withFileTypes: true }) + .filter((d) => d.isDirectory() && !EXCLUDED_CATEGORIES.has(d.name)) + .map((d) => d.name) + .filter((name) => hasTests(path.join(SRC, name))) + .sort(); +} + +/** Run a command with inherited stdio (streams live). Returns its exit status. */ +function run( + cmd: string, + args: string[], + env: NodeJS.ProcessEnv = process.env, + cwd: string = REPO_ROOT, +): number { + const res = spawnSync(cmd, args, { cwd, env, stdio: 'inherit' }); + if (res.error) { + console.log(`could not run ${cmd}: ${res.error.message}`); + return 1; + } + return res.status ?? 1; +} + +function runLiveVitest( + jsonPath: string, + fileFilters: string[], + extraEnv: Record, +): number { + return run( + VITEST, + [ + 'run', + '--project', + 'unit-live', + // A category filtered down to zero matching files is a pass, not an error. + '--passWithNoTests', + // `default` prints one line per file (piped) plus failures/summary; the + // progress reporter adds the worker-tagged, counted per-test line. + '--reporter=default', + `--reporter=${PROGRESS_REPORTER}`, + '--reporter=json', + `--outputFile.json=${jsonPath}`, + ...fileFilters, + ], + { ...process.env, MIDNIGHT_BACKEND: 'live', ...extraEnv }, + CONTRACTS, + ); +} + +/** name → status for every file in the report, or undefined if none exists. */ +function fileStatuses(jsonPath: string): Map | undefined { + if (!existsSync(jsonPath)) return undefined; + const report = JSON.parse(readFileSync(jsonPath, 'utf8')) as JsonReport; + return new Map( + (report.testResults ?? []).map((r) => [r.name, r.status] as const), + ); +} + +function pidAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (e) { + return (e as NodeJS.ErrnoException).code === 'EPERM'; + } +} + +function readLock(): LockInfo | undefined { + try { + return JSON.parse(readFileSync(VERIFY_LOCK, 'utf8')) as LockInfo; + } catch { + return undefined; + } +} + +function acquireVerifyLock(): void { + mkdirSync(LOGS, { recursive: true }); + const stamp = JSON.stringify({ + pid: process.pid, + startedAt: new Date().toISOString(), + }); + try { + writeFileSync(VERIFY_LOCK, stamp, { flag: 'wx' }); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e; + const info = readLock(); + if (info && pidAlive(info.pid)) { + throw new Error( + `another test:live run is already in progress (pid ${info.pid}, ` + + `started ${info.startedAt}). Wait for it, or remove ${VERIFY_LOCK}.`, + ); + } + writeFileSync(VERIFY_LOCK, stamp); // stale — reclaim + } +} + +function releaseVerifyLock(): void { + if (readLock()?.pid === process.pid) { + try { + unlinkSync(VERIFY_LOCK); + } catch { + // already gone + } + } +} + +const truncatedKeys = (): string[] => + emptyKeyArtifacts(path.join(CONTRACTS, 'artifacts'), SRC); + +/** + * Compile, then verify no truncated (0-byte) ZK key was left behind. A killed + * compile (or machine crash) can poison the turbo cache so that every later + * cache hit re-extracts a truncated key, and a concurrent compile racing this + * one over the shared `artifacts/` tree can truncate keys directly + * (OpenZeppelin/compact-contracts#675). Both repairs are mechanical, so + * self-heal once — drain the cache and recompile serially (a parallel + * recompile can re-poison it) — and only abort if keys are still truncated + * after the rebuild. + */ +function compileVerified(): boolean { + if (run('yarn', ['compact']) !== 0) { + console.log('compile failed — a compile error is real, not a flake.'); + return false; + } + const empty = truncatedKeys(); + if (empty.length === 0) return true; + + console.log( + '\ncompile reported success but left truncated (0-byte) ZK key(s):', + ); + for (const k of empty) console.log(` ✗ ${rel(k)}`); + console.log( + '\nPoisoned turbo cache or artifact tree ' + + '(OpenZeppelin/compact-contracts#675) — draining the cache and ' + + 'recompiling serially...', + ); + rmSync(path.join(REPO_ROOT, '.turbo', 'cache'), { + recursive: true, + force: true, + }); + if (run('yarn', ['compact', '--concurrency=1']) !== 0) { + console.log('serial recompile failed.'); + return false; + } + const stillEmpty = truncatedKeys(); + if (stillEmpty.length === 0) { + console.log('recovered — ZK keys intact after the serial recompile.'); + return true; + } + console.log( + '\nstill truncated after a serial recompile — needs investigation:', + ); + for (const k of stillEmpty) console.log(` ✗ ${rel(k)}`); + return false; +} + +function reportVerdict(flaky: string[], real: string[]): number { + const headline = + real.length === 0 + ? `VERDICT: PASSED${flaky.length ? ` (with ${flaky.length} flaky file(s))` : ''}` + : `VERDICT: FAILED — ${real.length} real failure(s), ${flaky.length} flaky`; + banner(headline); + if (flaky.length > 0) { + console.log('\nFLAKY (failed round 1, passed round 2 on a fresh node):'); + for (const f of flaky) console.log(` ~ ${rel(f)}`); + } + if (real.length > 0) { + console.log('\nREAL (failed both rounds — investigate):'); + for (const f of real) console.log(` ✗ ${rel(f)}`); + } + // A flaky-only run exits 0, so without these a green CI run would swallow + // the flake report entirely. + for (const f of flaky) { + ciWarn( + rel(f), + 'flaky live spec — failed round 1, passed round 2 on a fresh node', + ); + } + appendJobSummary( + [ + `### ${headline}`, + ...(flaky.length > 0 + ? [ + '', + 'Flaky (failed round 1, passed round 2 on a fresh node):', + ...flaky.map((f) => `- ~ \`${rel(f)}\``), + ] + : []), + ...(real.length > 0 + ? [ + '', + 'Real failures (failed both rounds — investigate):', + ...real.map((f) => `- ✗ \`${rel(f)}\``), + ] + : []), + ].join('\n'), + ); + return real.length === 0 ? 0 : 1; +} + +async function main(): Promise { + // `--list` prints the live-ready categories as JSON and exits — CI derives + // its per-category matrix from this, so LIVE_READY stays the single source + // of truth. + if (process.argv.includes('--list')) { + console.log( + JSON.stringify(liveCategories().filter((c) => LIVE_READY.has(c))), + ); + return 0; + } + const args = process.argv.slice(2).filter((a) => a !== '--'); + const allCategories = liveCategories(); + // First arg naming a category (the test:live: scripts pass one) + // scopes the run; everything else is a vitest file filter. + const scoped = args.length > 0 && allCategories.includes(args[0]); + if (scoped && !LIVE_READY.has(args[0])) { + console.log( + `'${args[0]}' is not live-ready yet — its specs still assume dry-only ` + + `semantics. Ready categories: ${[...LIVE_READY].join(', ')}.`, + ); + return 2; + } + const categories = scoped + ? [args[0]] + : allCategories.filter((c) => LIVE_READY.has(c)); + const skipped = scoped ? [] : allCategories.filter((c) => !LIVE_READY.has(c)); + const fileFilters = scoped ? args.slice(1) : args; + + acquireVerifyLock(); + try { + for (const c of categories) rmSync(r1Json(c), { force: true }); + if (existsSync(LOGS)) { + for (const f of readdirSync(LOGS)) { + if (f.startsWith('live-r2-') && f.endsWith('.json')) { + rmSync(path.join(LOGS, f), { force: true }); + } + } + } + + banner( + `ROUND 1 — categories: ${categories.join(', ')}` + + (fileFilters.length ? ` (filter: ${fileFilters.join(' ')})` : ''), + ); + if (skipped.length > 0) { + console.log(`skipped (not yet live-ready): ${skipped.join(', ')}`); + } + if (!compileVerified()) return 2; + if (run('make', ['env-up']) !== 0) { + console.log('env-up failed — cannot start the live stack.'); + return 2; + } + if (run('yarn', ['test:harness:live']) !== 0) { + console.log( + '\nlive harness smoke failed — this is an infrastructure problem, ' + + 'not a spec flake. Fix the stack and retry.', + ); + return 2; + } + + // Each category gets a freshly reset node: smaller coin tree, no + // cross-category state interactions. The smoke above already validated the + // stack, and its only on-chain footprint (NIGHT/dust) does not trip the + // freshness guard — so the first category reuses its node. + const failed: string[] = []; + for (const [i, category] of categories.entries()) { + banner(`ROUND 1 · ${category} (${i + 1}/${categories.length})`); + if (i > 0 && run('make', ['env-up']) !== 0) { + console.log(`env-up failed before '${category}'.`); + return 2; + } + // vitest ORs positional filters, so passing the category dir *and* a name + // filter would match the whole category (every file is under the dir). + // Use the name filters when given — they scope to the matching files; + // otherwise the category dir runs the whole set. + const round1Filters = + fileFilters.length > 0 ? fileFilters : [`src/${category}`]; + const status = runLiveVitest(r1Json(category), round1Filters, {}); + const statuses = fileStatuses(r1Json(category)); + if (statuses === undefined) { + console.log( + `\n'${category}' produced no results file — the run was blocked ` + + '(dirty node / lock) or crashed before finishing.', + ); + return 2; + } + const categoryFailed = [...statuses.entries()] + .filter(([, s]) => s === 'failed') + .map(([name]) => name); + if (status !== 0 && categoryFailed.length === 0) { + console.log( + `\n'${category}' exited non-zero without reporting failing files — ` + + 'aborting to be safe.', + ); + return 2; + } + failed.push(...categoryFailed); + console.log( + `\n${category}: ${statuses.size} file(s), ${categoryFailed.length} failed`, + ); + } + + if (failed.length === 0) { + banner('VERDICT: PASSED — all live specs green on the first run.'); + appendJobSummary( + '### VERDICT: PASSED — all live specs green on the first run.', + ); + return 0; + } + + banner(`ROUND 1 found ${failed.length} failing file(s)`); + for (const f of failed) console.log(` ✗ ${rel(f)}`); + + banner('ROUND 2 — re-run each failed file alone on a fresh node'); + // Reset the node before each file so state left by an earlier round-2 file + // can never fail a later one (which would misclassify a flake as REAL). + const round2 = new Map(); + for (const [i, file] of failed.entries()) { + banner(`ROUND 2 · ${rel(file)} (${i + 1}/${failed.length})`); + if (run('make', ['env-up']) !== 0) { + console.log(`env-up failed before round 2 of '${rel(file)}'.`); + return 2; + } + const jsonPath = r2Json(file); + runLiveVitest(jsonPath, [file], { MIDNIGHT_LIVE_WORKERS: '1' }); + const statuses = fileStatuses(jsonPath); + if (statuses === undefined) { + console.log( + `\nround 2 produced no results for '${rel(file)}' — cannot classify.`, + ); + return 2; + } + // No entry means the file crashed without reporting; treat as not-passed. + round2.set(file, statuses.get(file) ?? 'failed'); + } + + // Only an explicit round-2 pass demotes a failure to FLAKY; a file that + // failed again — or never reported (crashed) — stays REAL. + const flaky = failed.filter((f) => round2.get(f) === 'passed'); + const real = failed.filter((f) => round2.get(f) !== 'passed'); + return reportVerdict(flaky, real); + } finally { + releaseVerifyLock(); + } +} + +main() + .then((code) => process.exit(code)) + .catch((e) => { + console.log(e instanceof Error ? e.message : String(e)); + process.exit(2); + }); diff --git a/turbo.json b/turbo.json index efac8ec02..5266bdf19 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://turbo.build/schema.json", + "globalPassThroughEnv": ["GITHUB_ACTIONS", "GITHUB_STEP_SUMMARY"], "tasks": { "compact:crypto": { "dependsOn": ["^build"], @@ -50,6 +51,13 @@ "outputLogs": "new-only", "outputs": ["artifacts/**/"] }, + "compact:integration": { + "dependsOn": ["^build"], + "env": ["COMPACT_HOME", "SKIP_ZK"], + "inputs": ["test/integration/**/*.compact"], + "outputLogs": "new-only", + "outputs": ["artifacts/**/"] + }, "compact": { "dependsOn": [ "compact:crypto", @@ -58,21 +66,33 @@ "compact:access", "compact:multisig", "compact:token" - ], - "env": ["COMPACT_HOME", "SKIP_ZK"], - "inputs": ["src/**/*.compact", "test/**/*.compact"], - "outputLogs": "new-only", - "outputs": ["artifacts/**"] + ] }, "test": { - "dependsOn": ["^build"], + "dependsOn": ["compact"], + "env": ["COMPACT_HOME"], + "outputs": [], + "cache": false + }, + "test:coverage": { + "dependsOn": ["compact"], + "env": ["COMPACT_HOME"], + "outputs": [], + "cache": false + }, + "test:integration": { + "dependsOn": ["compact:integration"], + "env": ["COMPACT_HOME"], + "outputs": [], + "cache": false + }, + "test:harness": { + "outputs": [], + "cache": false + }, + "test:harness:live": { + "dependsOn": ["test:harness"], "env": ["COMPACT_HOME"], - "inputs": [ - "src/**/*.ts", - "src/**/*.compact", - "vitest.config.ts", - "package.json" - ], "outputs": [], "cache": false }, @@ -82,6 +102,7 @@ "inputs": [ "src/**/*.ts", "!src/**/*.test.ts", + "src/**/*.compact", "tsconfig.json", "tsconfig.build.json", ".env" diff --git a/yarn.lock b/yarn.lock index fc35a6274..ea9467604 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,37 @@ __metadata: version: 8 cacheKey: 10 +"@apollo/client@npm:^4.2.0": + version: 4.2.5 + resolution: "@apollo/client@npm:4.2.5" + 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 || ^17.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/4e19e03b352d488fce184f3e4639f51f4d7876041041006190a96d0c9886237390fe6e23b0450ffe3298feb9efb4067de235e73c2385fa4cd34cc471ac22d059 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-string-parser@npm:7.29.7" @@ -40,6 +71,13 @@ __metadata: 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" @@ -147,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.2 + resolution: "@effect/platform@npm:0.96.2" + dependencies: + find-my-way-ts: "npm:^0.1.6" + msgpackr: "npm:^1.11.10" + multipasta: "npm:^0.2.7" + peerDependencies: + effect: ^3.21.4 + checksum: 10/d10d0265f9b8392e9013af6db4e4ccce42523962360e8b99f8bdfb660aa32d5c0b661a0fe45b1f705bc2fc43cf5545da6cf6b530e3c02c25ac95a7f849b30db9 + languageName: node + linkType: hard + "@emnapi/core@npm:1.10.0": version: 1.10.0 resolution: "@emnapi/core@npm:1.10.0" @@ -175,6 +239,53 @@ __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.4 + resolution: "@grpc/grpc-js@npm:1.14.4" + dependencies: + "@grpc/proto-loader": "npm:^0.8.0" + "@js-sdsl/ordered-map": "npm:^4.4.2" + checksum: 10/f9cdbd81e7388dc784c57274fcf6f4f4484da8968dd0975b97a14708d3fb117ae4a7bc2848e1bd1cc8b8ed9ee7a80ff131bfe728c85260da90a4e0b170e31ca9 + 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:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -232,6 +343,36 @@ __metadata: 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 + +"@kwsites/file-exists@npm:^1.1.1": + version: 1.1.1 + resolution: "@kwsites/file-exists@npm:1.1.1" + dependencies: + debug: "npm:^4.1.1" + checksum: 10/4ff945de7293285133aeae759caddc71e73c4a44a12fac710fdd4f574cce2671a3f89d8165fdb03d383cfc97f3f96f677d8de3c95133da3d0e12a123a23109fe + 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" @@ -243,20 +384,416 @@ __metadata: languageName: node linkType: hard -"@midnight-ntwrk/ledger-v8@npm:8.1.0": +"@midnight-ntwrk/dapp-connector-api@npm:4.0.1": + version: 4.0.1 + resolution: "@midnight-ntwrk/dapp-connector-api@npm:4.0.1" + checksum: 10/b5a2fe117390ea40d5d1030a600351400624532169f2beeaa2fa130935c27110e3743fb8f9028d2541ae466e6e168a79efcfd45aaf1bf87a8ca8340bbcf53814 + languageName: node + linkType: hard + +"@midnight-ntwrk/ledger-v8@npm:8.1.0, @midnight-ntwrk/ledger-v8@npm:^8.0.3, @midnight-ntwrk/ledger-v8@npm:^8.1.0": version: 8.1.0 resolution: "@midnight-ntwrk/ledger-v8@npm:8.1.0" checksum: 10/10d56076b0333a502f157c816f8cfebefc8d50221cb20c6db15abcbf2d0092bdaf7e9bc1bd19a6d9f51455547c713c916cb16d4a7d18e83cba0e172ad6e2a507 languageName: node linkType: hard -"@midnight-ntwrk/onchain-runtime-v3@npm:^3.0.0": +"@midnight-ntwrk/midnight-js-compact@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-compact@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + bin: + fetch-compactc: dist/fetch-compact.mjs + run-compactc: dist/run-compactc.cjs + checksum: 10/7446faf9bb885756ecb766235f7378804d82a011718d3e1e9ce797f96f82cf593808192a1ddbdb4c7e4589b5ac3f051ba5fb4547c6c569f31fe28d0b0997abe8 + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-contracts@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-contracts@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + checksum: 10/93791613419dd914cbd4db6c0bc75cebb093962e4162a391d298e2ae705e53c2e87a40fa923fd9a9a6a31cdb91bede0836673beea14d4a594e15da250a1b4feb + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-http-client-proof-provider@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + cross-fetch: "npm:^4.1.0" + fetch-retry: "npm:^6.0.0" + checksum: 10/f2dc4acb733d3a87ef1c18ab225c081adfc42e3d7b626ac64ff858e931a2df873ab2161b13110522490c246ef5154a821db5fc24d0fb8ff43fa7e3745aa1defb + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-indexer-public-data-provider@npm:4.1.1" + dependencies: + "@apollo/client": "npm:^4.2.0" + "@graphql-typed-document-node/core": "npm:^3.2.0" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + buffer: "npm:^6.0.3" + cross-fetch: "npm:^4.1.0" + graphql: "npm:^16.14.0" + graphql-ws: "npm:^6.0.8" + isomorphic-ws: "npm:^5.0.0" + rxjs: "npm:^7.8.2" + ws: "npm:^8.20.0" + checksum: 10/89974d6d7e8588b1992ad552a15d7f512a437bfd2fd3e1192e5c52a1663902e6af84e2189ef2a3626f860acf1178303bb971e39b696215a0995a984f6397eef8 + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-level-private-state-provider@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-level-private-state-provider@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + "@noble/ciphers": "npm:^2.2.0" + "@noble/hashes": "npm:^2.2.0" + abstract-level: "npm:^3.1.1" + buffer: "npm:^6.0.3" + level: "npm:^10.0.0" + superjson: "npm:^2.2.6" + checksum: 10/f28e1b8d53da55d4088f1937d4f30d2029349407119adf78850c0fa997c538f9201d2ad6423bbd203d15ae5656d547f000e805dfe999100f2f30428d5206898f + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-network-id@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-network-id@npm:4.1.1" + checksum: 10/ac2f06da0d3bdec6ee83fe84312d8d012b398dad8e23896727c8de10df51eefeeff8eff2de29079c534e090f7cc8520f6e0763a324b560dfe1a0f1d55f2ca2a3 + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-node-zk-config-provider@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + checksum: 10/716d38a7ff0b6b007a5502364bb66b41b8cb1009a2b927b911861f1a7700860b59de5ebe50b83a0f61af9d40ca7af885ece371918b9e45a37d2b151965c5f80f + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-protocol@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-protocol@npm:4.1.1" + dependencies: + "@midnight-ntwrk/compact-js": "npm:2.5.1" + "@midnight-ntwrk/compact-runtime": "npm:0.16.0" + "@midnight-ntwrk/ledger-v8": "npm:8.1.0" + "@midnight-ntwrk/onchain-runtime-v3": "npm:3.0.0" + "@midnight-ntwrk/platform-js": "npm:2.2.4" + checksum: 10/bfd6195e90b8c0fbc178b32ff9f8223f377d48a55a4a6fdedba424ede77c2234eac75555263b73a61f3ca14d2cc27ed935b3efae15daa6a475a00db4a696e7a1 + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-types@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-types@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + effect: "npm:^3.20.0" + pino: "npm:^10.3.1" + rxjs: "npm:^7.8.2" + checksum: 10/a11a994f0838968954f01146c9f5f65f29c8e30fe305401a3f0cef15b6a75802078d2c8c4abe6ef743585a12a05b4d9d30dbe1bb7177699cd2623302fb2b619e + languageName: node + linkType: hard + +"@midnight-ntwrk/midnight-js-utils@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/midnight-js-utils@npm:4.1.1" + dependencies: + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.0" + checksum: 10/73f3ab682bd0ea37b988bc34e8036bc03d748b1df534ea57764c7fc656a235ea3287ca55481d57be956455ff4a8334e805c99d87d95c998f1879b5b7743dc18c + languageName: node + linkType: hard + +"@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 +"@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 + +"@midnight-ntwrk/testkit-js@npm:4.1.1": + version: 4.1.1 + resolution: "@midnight-ntwrk/testkit-js@npm:4.1.1" + dependencies: + "@midnight-ntwrk/dapp-connector-api": "npm:4.0.1" + "@midnight-ntwrk/midnight-js-compact": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-level-private-state-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1" + "@midnight-ntwrk/wallet-sdk": "npm:1.1.0" + "@midnight-ntwrk/wallet-sdk-prover-client": "npm:^1.2.1" + "@midnight-ntwrk/zkir-v2": "npm:2.1.0" + "@scure/bip39": "npm:^2.2.0" + axios: "npm:^1.16.1" + buffer: "npm:^6.0.3" + cross-fetch: "npm:^4.1.0" + pino: "npm:^10.3.1" + pino-pretty: "npm:^13.1.3" + rxjs: "npm:^7.8.2" + testcontainers: "npm:^12.0.0" + ws: "npm:^8.20.0" + checksum: 10/54950d9f88ae6aac3cbdcb1edab1de1cba8fb7791d50e6e173e9db64ca93bd6a3eab7fe1489fc296100345b651e22f0f082fcc83962a2014b23b0dacef8399a3 + languageName: node + linkType: hard + +"@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 + +"@midnight-ntwrk/wallet-sdk-address-format@npm:^3.1.0, @midnight-ntwrk/wallet-sdk-address-format@npm:^3.1.2": + version: 3.1.2 + resolution: "@midnight-ntwrk/wallet-sdk-address-format@npm:3.1.2" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@scure/base": "npm:^2.0.0" + "@subsquid/scale-codec": "npm:^4.0.1" + checksum: 10/f3e2374c1dd8e31310aa464fa2afecca3cca92b923a999bfcba2922225b907e5387b94a70e6a8c06e8fb9d51fd9140a08c827c13f8a9191fd18d597cb5ab7b0c + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-capabilities@npm:^3.3.1": + version: 3.3.1 + resolution: "@midnight-ntwrk/wallet-sdk-capabilities@npm:3.3.1" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-node-client": "npm:^1.1.2" + "@midnight-ntwrk/wallet-sdk-prover-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + "@midnight-ntwrk/zkir-v2": "npm:^2.1.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/95e37f71b8f991287e3036940c9ccfd06a545aa3f8f2f34542591a77fb59e6860c96c2ada96b5cf16e71e7270783e7e88e292fc2ed11fe8ffdef96716f38907a + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-dust-wallet@npm:^4.1.0": + version: 4.2.0 + resolution: "@midnight-ntwrk/wallet-sdk-dust-wallet@npm:4.2.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.2" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.1" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.3" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:^1.0.5" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/a48226ec58a84c9f1a7c2244df5ed1c154ccde4b473a0e318d6664dcf706dfe372aabe4bfe00f2f834f363fa8ef0b1d505e60b085e66b45d96c7aa7b3587f59f + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-facade@npm:4.0.1": + version: 4.0.1 + resolution: "@midnight-ntwrk/wallet-sdk-facade@npm:4.0.1" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.2" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.1" + "@midnight-ntwrk/wallet-sdk-dust-wallet": "npm:^4.1.0" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-shielded": "npm:^3.0.1" + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "npm:^3.1.0" + rxjs: "npm:^7.8.2" + checksum: 10/615e1bb2b4703941a0c67289271e126af9714a170c4f54ba8df7a54b2a7a4fd17a8786a6e9d9a25c77c6c4d26eb5b0eb3a60bdd6e377703e58c5f2e69757ebf6 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-hd@npm:^3.0.2": + version: 3.0.3 + resolution: "@midnight-ntwrk/wallet-sdk-hd@npm:3.0.3" + dependencies: + "@scure/bip32": "npm:^2.0.1" + "@scure/bip39": "npm:^2.0.1" + checksum: 10/21804c08b074e93ef6298d90cd1d3f1d00822b3202072aca2a04edf91f4c23e980f1901f970920f5115516816d4d2b8ee7a4a70f58f8e07f3f0a7014aa91bc79 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-indexer-client@npm:^1.2.2, @midnight-ntwrk/wallet-sdk-indexer-client@npm:^1.2.3": + version: 1.2.3 + resolution: "@midnight-ntwrk/wallet-sdk-indexer-client@npm:1.2.3" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.2.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + effect: "npm:^3.19.19" + graphql: "npm:^16.13.0" + graphql-http: "npm:^1.22.4" + graphql-ws: "npm:^6.0.7" + checksum: 10/5b777c9e87bf459b7dba00247d052315d607b12f239ed5f1154824fae19e450b02ce99e6591a3c18b5901a254a2be3ce04e3bc9bc53580b5226f917a4b31c833 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-node-client@npm:^1.1.2": + version: 1.1.3 + resolution: "@midnight-ntwrk/wallet-sdk-node-client@npm:1.1.3" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + "@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" + peerDependencies: + "@midnight-ntwrk/ledger-v8": ^8.1.0 + "@midnight-ntwrk/wallet-sdk-prover-client": ^1.2.3 + peerDependenciesMeta: + "@midnight-ntwrk/ledger-v8": + optional: true + "@midnight-ntwrk/wallet-sdk-prover-client": + optional: true + checksum: 10/68a846042185b136d580565fc643376dd060a3eb42473f12400107dfe982f81302de8c4fa33657432cf18f3d9dfe34dde3f4bdcc3eda78b6b6c1ccd835160163 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-prover-client@npm:^1.2.1, @midnight-ntwrk/wallet-sdk-prover-client@npm:^1.2.2": + version: 1.2.3 + resolution: "@midnight-ntwrk/wallet-sdk-prover-client@npm:1.2.3" + dependencies: + "@effect/platform": "npm:^0.96.0" + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + "@midnight-ntwrk/zkir-v2": "npm:^2.1.0" + effect: "npm:^3.19.19" + web-worker: "npm:^1.5.0" + checksum: 10/c99f8b8bbc4c2885f8d2575504fc080dacca00af8a85f7010b1ae768b5e109f144757ff8b895046831241da597849ec8e7b97cebf3029dc6c61d6389362321d1 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-runtime@npm:^1.0.4, @midnight-ntwrk/wallet-sdk-runtime@npm:^1.0.5": + version: 1.0.5 + resolution: "@midnight-ntwrk/wallet-sdk-runtime@npm:1.0.5" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/7315ea2191f9f81596773360d080980367a478d326f7b45524907db854dea8caecdd16a2ece6f70d72ee42c68ed607f6da624384048361625b368f226373371f + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-shielded@npm:^3.0.1": + version: 3.0.2 + resolution: "@midnight-ntwrk/wallet-sdk-shielded@npm:3.0.2" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.2" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.1" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.3" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:^1.0.5" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/ce98e211f9f8ba43a648523eb53991f1864e10fab95a157d2378934140e3ecbdb5981bd8059095c5cc5b7a97b622c14b95e3af58de70bc74a8369e8e544367e8 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-unshielded-wallet@npm:^3.1.0": + version: 3.1.0 + resolution: "@midnight-ntwrk/wallet-sdk-unshielded-wallet@npm:3.1.0" + dependencies: + "@midnight-ntwrk/ledger-v8": "npm:^8.1.0" + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.2" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.1" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:^1.0.4" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/21cca2a1119e3a4a19d74babb3acdd8690be48a58c4f4a191a8e0dcd79c07d8de11646ad05c02267e4a470ab17f4d43768691b44e0ed67d9317c1c752be0a6c2 + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk-utilities@npm:^1.2.0": + version: 1.2.0 + resolution: "@midnight-ntwrk/wallet-sdk-utilities@npm:1.2.0" + dependencies: + effect: "npm:^3.19.19" + rxjs: "npm:^7.8.2" + checksum: 10/369a626afbb25f80a54b945b2ffc5a20bbaca761b580a288eaf572e149d976567b8e646a72b16b613c10c6b5f6ee319e9a0829d8c76baa4e7e38e92b77c9093c + languageName: node + linkType: hard + +"@midnight-ntwrk/wallet-sdk@npm:1.1.0": + version: 1.1.0 + resolution: "@midnight-ntwrk/wallet-sdk@npm:1.1.0" + dependencies: + "@midnight-ntwrk/wallet-sdk-abstractions": "npm:^2.1.0" + "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.2" + "@midnight-ntwrk/wallet-sdk-capabilities": "npm:^3.3.1" + "@midnight-ntwrk/wallet-sdk-dust-wallet": "npm:^4.1.0" + "@midnight-ntwrk/wallet-sdk-facade": "npm:^4.0.1" + "@midnight-ntwrk/wallet-sdk-hd": "npm:^3.0.2" + "@midnight-ntwrk/wallet-sdk-indexer-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-node-client": "npm:^1.1.2" + "@midnight-ntwrk/wallet-sdk-prover-client": "npm:^1.2.2" + "@midnight-ntwrk/wallet-sdk-runtime": "npm:^1.0.4" + "@midnight-ntwrk/wallet-sdk-shielded": "npm:^3.0.1" + "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "npm:^3.1.0" + "@midnight-ntwrk/wallet-sdk-utilities": "npm:^1.2.0" + checksum: 10/2763ae543a0728a2680df9928e4dfac6a31d871d52a4c927a2d392abc3639eb250b64cefaec27628172f42515fbbaa0cb017058a03bab9103ab393e6da1eed43 + languageName: node + linkType: hard + +"@midnight-ntwrk/zkir-v2@npm:2.1.0, @midnight-ntwrk/zkir-v2@npm:^2.1.0": + version: 2.1.0 + resolution: "@midnight-ntwrk/zkir-v2@npm:2.1.0" + checksum: 10/c16761489c3abbf858a4b7c2c4dd99d498f40554b5f1a57a93534b21c66390d4c6b0035dee8923fb5972418c75ac1f80e2e0675d8f3eb2a96dce7e7555fb2b7d + languageName: node + linkType: hard + "@midnight-ntwrk/zswap@npm:^4.0.0": version: 4.0.0 resolution: "@midnight-ntwrk/zswap@npm:4.0.0" @@ -264,6 +801,48 @@ __metadata: languageName: node linkType: hard +"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4": + version: 3.0.4 + resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^1.1.4": version: 1.1.5 resolution: "@napi-rs/wasm-runtime@npm:1.1.5" @@ -276,6 +855,45 @@ __metadata: languageName: node linkType: hard +"@noble/ciphers@npm:^2.2.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.2.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" @@ -328,6 +946,17 @@ __metadata: version: 0.0.0-use.local resolution: "@openzeppelin/compact-contracts@workspace:contracts" dependencies: + "@midnight-ntwrk/compact-js": "npm:2.5.1" + "@midnight-ntwrk/compact-runtime": "npm:0.16.0" + "@midnight-ntwrk/ledger-v8": "npm:8.1.0" + "@midnight-ntwrk/midnight-js-contracts": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1" + "@midnight-ntwrk/midnight-js-types": "npm:4.1.1" + "@midnight-ntwrk/testkit-js": "npm:4.1.1" "@openzeppelin/compact-cli": "npm:^0.0.2" "@openzeppelin/compact-simulator": "npm:^0.2.0" "@tsconfig/node24": "npm:^24.0.4" @@ -365,6 +994,13 @@ __metadata: 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 + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -372,57 +1008,600 @@ __metadata: 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 +"@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 -"@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 +"@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 -"@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 +"@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 -"@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 +"@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 -"@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 +"@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 -"@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 +"@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 -"@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 +"@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 -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": - version: 1.0.3 +"@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.1": + version: 1.1.1 + resolution: "@protobufjs/eventemitter@npm:1.1.1" + checksum: 10/a54dc1aff4475ffad4fdf3235c71a553f5e40e3b4cf6a2e217151895a61cb4eb0be20d63791db22441ca25e594671f1021977133f9939540750231ff7d8e9dd6 + 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/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.2 + resolution: "@protobufjs/utf8@npm:1.1.2" + checksum: 10/ff759348d60e8f65137d3a7a16e00cf69abd9a6dede75e50ec377c6aebb4ac400a4a70af2e77eb2bf75e2accf30abbea81803e9403004b1922ea70776bfdc3aa + 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 @@ -488,13 +1667,134 @@ __metadata: languageName: node linkType: hard -"@standard-schema/spec@npm:^1.1.0": +"@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": + 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:^2.0.1, @scure/bip39@npm:^2.2.0": + 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, @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, @subsquid/util-internal-hex@npm:^1.2.3": + 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.4 + resolution: "@subsquid/util-internal-json@npm:1.2.4" + dependencies: + "@subsquid/util-internal-hex": "npm:^1.2.3" + checksum: 10/89868acd9c3ecdcf39eacac94882a7605dee546cb8d212e89a3f0851304b17c6dcd20d301d52ab690f7991225eaf210bacbe1a1a3e036a63fc932ef30cdf980f + 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" @@ -581,6 +1881,15 @@ __metadata: languageName: node linkType: hard +"@types/bn.js@npm:^5.1.6, @types/bn.js@npm:^5.2.0": + version: 5.2.0 + resolution: "@types/bn.js@npm:5.2.0" + dependencies: + "@types/node": "npm:*" + checksum: 10/06c93841f74e4a5e5b81b74427d56303b223c9af36389b4cd3c562bda93f43c425c7e241aee1b0b881dde57238dc2e07f21d30d412b206a7dae4435af4c054e8 + languageName: node + linkType: hard + "@types/chai@npm:^5.2.2": version: 5.2.2 resolution: "@types/chai@npm:5.2.2" @@ -597,6 +1906,27 @@ __metadata: languageName: node linkType: hard +"@types/docker-modem@npm:*": + version: 3.0.6 + resolution: "@types/docker-modem@npm:3.0.6" + dependencies: + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10/cc58e8189f6ec5a2b8ca890207402178a97ddac8c80d125dc65d8ab29034b5db736de15e99b91b2d74e66d14e26e73b6b8b33216613dd15fd3aa6b82c11a83ed + languageName: node + linkType: hard + +"@types/dockerode@npm:^4.0.1": + version: 4.0.1 + resolution: "@types/dockerode@npm:4.0.1" + dependencies: + "@types/docker-modem": "npm:*" + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10/d16b3a69a20fac269b2317a978442a6752dea158729a264511a3812dcb4c756e0ee079b39b61068cee182f268661e27e32aa7a28815262f0e088ffeb9f2f48c5 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.0": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -604,6 +1934,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*, @types/node@npm:>=13.7.0": + version: 26.1.0 + resolution: "@types/node@npm:26.1.0" + dependencies: + undici-types: "npm:~8.3.0" + checksum: 10/fee720e8ddf8aa66a22bc5cf3209347f8dcc07cbf7475f966761684f04fb34b819739a311edbf87271d6c7f9f50b1f015d3ff9e36d67d04264aafe18ac9a3796 + languageName: node + linkType: hard + "@types/node@npm:25.9.3": version: 25.9.3 resolution: "@types/node@npm:25.9.3" @@ -613,6 +1952,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.130 + resolution: "@types/node@npm:18.19.130" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/ebb85c6edcec78df926de27d828ecbeb1b3d77c165ceef95bfc26e171edbc1924245db4eb2d7d6230206fe6b1a1f7665714fe1c70739e9f5980d8ce31af6ef82 + languageName: node + linkType: hard + "@types/object-inspect@npm:^1.8.1": version: 1.13.0 resolution: "@types/object-inspect@npm:1.13.0" @@ -620,6 +1968,34 @@ __metadata: languageName: node linkType: hard +"@types/ssh2-streams@npm:*": + version: 0.1.13 + resolution: "@types/ssh2-streams@npm:0.1.13" + dependencies: + "@types/node": "npm:*" + checksum: 10/182c9de8384e11fcfed04e447c3c1d37f898ed4e7f0be0cc58b3bd5b23e22957c17939b68f709092cece758a4befa92913dd967115f643fa0e2dc629fc2e2383 + languageName: node + linkType: hard + +"@types/ssh2@npm:*": + version: 1.15.5 + resolution: "@types/ssh2@npm:1.15.5" + dependencies: + "@types/node": "npm:^18.11.18" + checksum: 10/dd6f29f4e96ea43aa61d29a4a3ad87ad8d11bf1bef637b2848958abd94b05d28754cc611eac13f52d43bd1f51afe7c660cd1c8533ae06878b5739888f4ea0d99 + languageName: node + linkType: hard + +"@types/ssh2@npm:^0.5.48": + version: 0.5.52 + resolution: "@types/ssh2@npm:0.5.52" + dependencies: + "@types/node": "npm:*" + "@types/ssh2-streams": "npm:*" + checksum: 10/fc2584af091da49da9d6628dd8a5e851b217bb9b1b732b0361903894f2730ab3fdf8634f954be34c5a513f7eb0b2772d059d64062bcf6b4a0eb73bfc83c4b858 + languageName: node + linkType: hard + "@vitest/coverage-v8@npm:^4.1.9": version: 4.1.9 resolution: "@vitest/coverage-v8@npm:4.1.9" @@ -726,6 +2102,42 @@ __metadata: 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" @@ -733,92 +2145,390 @@ __metadata: 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 +"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.1.0, abstract-level@npm:^3.1.1": + 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 + +"ansi-styles@npm:^6.1.0": + version: 6.2.3 + resolution: "ansi-styles@npm:6.2.3" + checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 + 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 + +"ast-v8-to-istanbul@npm:^1.0.0": + version: 1.0.2 + resolution: "ast-v8-to-istanbul@npm:1.0.2" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" + estree-walker: "npm:^3.0.3" + js-tokens: "npm:^10.0.0" + checksum: 10/640494e7170d3b36079da24c35f132bbac51c7e63289d418d3054a085dc84e3e5f7c3e56136647f302a3d48c64acf8f4230d7d13acb45ff9ac5f50d398512f8c + 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.16.1": + version: 1.18.1 + resolution: "axios@npm:1.18.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/c4cdced3ee0a9bf7dcae189fbc74a124aa079d4f04dbd995e602d39c1419476e8d021f87ee39ac8d8398e5a55e6d0b986238b3645f0d9177ac80de87c2a73f18 + languageName: node + linkType: hard + +"b4a@npm:^1.6.4, b4a@npm:^1.8.1": + 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:^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.9.1 + resolution: "bare-events@npm:2.9.1" + peerDependencies: + bare-abort-controller: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + checksum: 10/0692be1767f4f326e39c8b1ec76f88e9f566d96fce4fe23ff10c1a6e69843237d1eee0d14a52d9aa4b89c8efa6511fe6c0e640d96fa8586241bfa877c4f7bb46 + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1, bare-fs@npm:^4.5.5": + version: 4.7.3 + resolution: "bare-fs@npm:4.7.3" + 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/9b8cf1e4977bf0c88179580e005d2e1c4c57d2c6665f5505ce06259b536fe07b5699a6c83058d8328a1b0f17d49d40b1d267846106cea960f07b9e1f540825fa + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.9.3 + resolution: "bare-os@npm:3.9.3" + checksum: 10/f45bcaaacfb36d38868afefa604cb72b0f20241c111189c07c5e68c57eea4f1197d95b3c11c209677a0798599a9b2248b0991d6be699fce777b1d7eec49ab394 + languageName: node + linkType: hard + +"bare-path@npm:^3.0.0": + version: 3.0.1 + resolution: "bare-path@npm:3.0.1" + dependencies: + bare-os: "npm:^3.0.1" + checksum: 10/278e4bee8cff9c6b7d20e63c2f57832f813536489e70f48f9adf1cfb8402e4672425a764605cb666090878ff46b2020488c3c9a1b967fe205f962b08d70300f8 + languageName: node + linkType: hard + +"bare-stream@npm:^2.6.4": + version: 2.13.3 + resolution: "bare-stream@npm:2.13.3" + dependencies: + b4a: "npm:^1.8.1" + 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/771f0f5a05af4a1bc33e86ba18c5afd90c73496bfa2b457d71146ce78837c7c4cdef4b980f7889108434ceab57ac29e219a3a3cc39892db81a5cae2c54c91c38 + languageName: node + linkType: hard + +"bare-url@npm:^2.2.2": + version: 2.4.5 + resolution: "bare-url@npm:2.4.5" + dependencies: + bare-path: "npm:^3.0.0" + checksum: 10/65d7a906dae5051ba37d8b96fd3cd1ba8b8e1a7f74340a363c6b45123908d707a4d58c4e21c97f6735c4dfcd6c499d353a67cc8a4ff638d4e8b46a1d9984ad60 + 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 -"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 +"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 -"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 +"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 -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 10/2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b +"bn.js@npm:^5.2.1, bn.js@npm:^5.2.3": + version: 5.2.4 + resolution: "bn.js@npm:5.2.4" + checksum: 10/30af6d4e5930dee835c692d6225f2b0a018535647c9d854fb780cda26d9ec9dc920fdcc815f44e312f47223f73eaf58d03074a19fd1593fb33107bb8d9720a5f languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": - version: 6.2.2 - resolution: "ansi-regex@npm:6.2.2" - checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f +"brace-expansion@npm:^5.0.2": + 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 -"ansi-styles@npm:^4.0.0": - version: 4.3.0 - resolution: "ansi-styles@npm:4.3.0" +"browser-level@npm:^3.0.0": + version: 3.0.0 + resolution: "browser-level@npm:3.0.0" dependencies: - color-convert: "npm:^2.0.1" - checksum: 10/b4494dfbfc7e4591b4711a396bd27e540f8153914123dccb4cdbbcb514015ada63a3809f362b9d8d4f6b17a706f1d7bea3c6f974b15fa5ae76b5b502070889ff + abstract-level: "npm:^3.1.0" + checksum: 10/719e9aa36fb85ed7bd9d06267961c7b151866422e4ff4e97cc82966c6fdefcc13a19bbd2cefe151d57af21bf7d2e2419e758f8646af445dca47d8ab191e7236b languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.3 - resolution: "ansi-styles@npm:6.2.3" - checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 10/ef3b7c07622435085c04300c9a51e850ec34a27b2445f758eef69b859c7827848c2282f3840ca6c1eef3829145a1580ce540cab03ccf4433827a2b95d3b09ca7 languageName: node linkType: hard -"arg@npm:^4.1.0": - version: 4.1.3 - resolution: "arg@npm:4.1.3" - checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 +"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 -"ast-v8-to-istanbul@npm:^1.0.0": - version: 1.0.2 - resolution: "ast-v8-to-istanbul@npm:1.0.2" +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.31" - estree-walker: "npm:^3.0.3" - js-tokens: "npm:^10.0.0" - checksum: 10/640494e7170d3b36079da24c35f132bbac51c7e63289d418d3054a085dc84e3e5f7c3e56136647f302a3d48c64acf8f4230d7d13acb45ff9ac5f50d398512f8c + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10/b6bc68237ebf29bdacae48ce60e5e28fc53ae886301f2ad9496618efac49427ed79096750033e7eab1897a4f26ae374ace49106a5758f38fb70c78c9fda2c3b1 languageName: node linkType: hard -"balanced-match@npm:^4.0.2": - version: 4.0.4 - resolution: "balanced-match@npm:4.0.4" - checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 +"buildcheck@npm:~0.0.6": + version: 0.0.7 + resolution: "buildcheck@npm:0.0.7" + checksum: 10/cca174bcc917ee9dc00b1be404b4f22656d9c243d439d3456e6bd52263f05ad5f5d3c77e62a1f6ccaf1d36cb65efc5ee3bb30ed10e1675f22a1abdfad99eb9b3 languageName: node linkType: hard -"brace-expansion@npm:^5.0.2": - version: 5.0.6 - resolution: "brace-expansion@npm:5.0.6" - dependencies: - balanced-match: "npm:^4.0.2" - checksum: 10/a7acf120fefa79e9d7c9c92898114f57c07596a3920197f3c5917e6a628b04220a5f7f9618c30bdd973a6576a32113b99f9c3f1c8245ccc399dd2a9a718d81d8 +"byline@npm:^5.0.0": + version: 5.0.0 + resolution: "byline@npm:5.0.0" + checksum: 10/737ca83e8eda2976728dae62e68bc733aea095fab08db4c6f12d3cee3cf45b6f97dce45d1f6b6ff9c2c947736d10074985b4425b31ce04afa1985a4ef3d334a7 languageName: node linkType: hard @@ -842,6 +2552,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 + languageName: node + linkType: hard + "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -856,6 +2576,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -863,6 +2590,19 @@ __metadata: languageName: node linkType: hard +"classic-level@npm:^3.0.0": + version: 3.0.0 + resolution: "classic-level@npm:3.0.0" + dependencies: + abstract-level: "npm:^3.1.0" + module-error: "npm:^1.0.1" + napi-macros: "npm:^2.2.2" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/96c07b0ca6f38dc5535c040804fdb845f728dcabd12838dafbcb379ca4b4cce906fb14c4ab8d871b3798f0e27a7815b9f584be535d1e00089f1104da97e44f95 + languageName: node + linkType: hard + "cli-cursor@npm:^5.0.0": version: 5.0.0 resolution: "cli-cursor@npm:5.0.0" @@ -879,6 +2619,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10/eaa5561aeb3135c2cddf7a3b3f562fc4238ff3b3fc666869ef2adf264be0f372136702f16add9299087fb1907c2e4ec5dbfe83bd24bce815c70a80c6c1a2e950 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -895,6 +2646,35 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^2.0.7": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10/2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4 + languageName: node + linkType: hard + +"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" @@ -902,6 +2682,52 @@ __metadata: 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" @@ -909,6 +2735,15 @@ __metadata: languageName: node linkType: hard +"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" @@ -920,7 +2755,21 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4": +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10/5c149c91bf9ce2142c89f84eee4c585f0cb1f6faf2536b1af89873f862666a28529d1ccafc44750aa01384da2197c4f76f4e149a3cc0c1cb2c46f5cc45f2bcb5 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -932,7 +2781,14 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.3": +"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 @@ -946,6 +2802,52 @@ __metadata: languageName: node linkType: hard +"docker-compose@npm:^1.4.2": + version: 1.4.2 + resolution: "docker-compose@npm:1.4.2" + dependencies: + yaml: "npm:^2.2.2" + checksum: 10/def0c2da93e29a65315525a17e3e7bf47a8fc6918b45cac4468f0b3aa33cc286d5b33c4dd31101ff76d901ac4f2644771fc8b0c2b0c08d4f60791cd83106efb5 + 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:^5.0.0": + version: 5.0.1 + resolution: "dockerode@npm:5.0.1" + 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" + checksum: 10/8809c7f6b2d5bcec06589cb7f3f677656f96ad1c9b46b2bb6f3487964730b3e9ce59d2809cb356a80c3ff0625c1d01ca25d02586be92104b8b8c3d793b03df13 + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10/5add88a3d68d42d6e6130a0cac450b7c2edbe73364bbd2fc334564418569bea97c6943a8fcd70e27130bf32afc236f30982fc4905039b703f23e9e0433c29934 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -953,6 +2855,16 @@ __metadata: languageName: node linkType: hard +"effect@npm:^3.19.19, effect@npm:^3.20.0": + version: 3.21.4 + resolution: "effect@npm:3.21.4" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + fast-check: "npm:^3.23.1" + checksum: 10/c95506043e070662af85963af779b3d4e9ec501e03069532888831cc268e0fd9f448dcd2a4c89809e05471e64dcd270313b196056f878d1c028037870694af11 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -976,6 +2888,15 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10/1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -990,6 +2911,20 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10/f8dc9e660d90919f11084db0a893128f3592b781ce967e4fccfb8f3106cb83e400a4032c559184ec52ee1dbd4b01e7776c7cd0b3327b1961b1a4a7008920fe78 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 + languageName: node + linkType: hard + "es-module-lexer@npm:^2.0.0": version: 2.0.0 resolution: "es-module-lexer@npm:2.0.0" @@ -997,6 +2932,34 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.2 + resolution: "es-object-atoms@npm:1.1.2" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/70041de72ab8996df74c17775cdedb8a0c36eb09a4111921d974f7d018af963023bb035a328b5772c2851daa40fb49f52313be0418763a975cb42cb6fe723255 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10/86814bf8afbcd8966653f731415888019d4bc4aca6b6c354132a7a75bb87566751e320369654a101d23a91c87a85c79b178bcf40332839bd347aff437c4fb65f + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 + languageName: node + linkType: hard + "estree-walker@npm:^3.0.3": version: 3.0.3 resolution: "estree-walker@npm:3.0.3" @@ -1006,6 +2969,36 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10/49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8 + languageName: node + linkType: hard + +"eventemitter3@npm:^5.0.1": + version: 5.0.4 + resolution: "eventemitter3@npm:5.0.4" + checksum: 10/54f5c8c543650d65f92d03dbef1bb73a682a920490c44699ad8f863a6b19bbca42fb7409aa09ca09cb98a44149d9a7bc1dffd55ca88a740bd928c7be0ad666a0 + languageName: node + linkType: hard + +"events-universal@npm:^1.0.0": + version: 1.0.1 + resolution: "events-universal@npm:1.0.1" + dependencies: + bare-events: "npm:^2.7.0" + checksum: 10/71b2e6079b4dc030c613ef73d99f1acb369dd3ddb6034f49fd98b3e2c6632cde9f61c15fb1351004339d7c79672252a4694ecc46a6124dc794b558be50a83867 + languageName: node + linkType: hard + +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10/a3d47e285e28d324d7180f1e493961a2bbb4cad6412090e4dec114f4db1f5b560c7696ee8e758f55e23913ede856e3689cd3aa9ae13c56b5d8314cd3b3ddd1be + languageName: node + linkType: hard + "expect-type@npm:^1.3.0": version: 1.3.0 resolution: "expect-type@npm:1.3.0" @@ -1020,6 +3013,15 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:^3.23.1": + version: 3.23.2 + resolution: "fast-check@npm:3.23.2" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10/dab344146b778e8bc2973366ea55528d1b58d3e3037270262b877c54241e800c4d744957722c24705c787020d702aece11e57c9e3dbd5ea19c3e10926bf1f3fe + languageName: node + linkType: hard + "fast-check@npm:^4.8.0": version: 4.8.0 resolution: "fast-check@npm:4.8.0" @@ -1029,6 +3031,27 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^4.0.0": + version: 4.0.3 + resolution: "fast-copy@npm:4.0.3" + checksum: 10/1e74e8b18a83f125b697b0dc7d802b4c73ec2aba7b181458e5e72d46a261faefcdee22ad9fa682c77f4606133451342f95de9835c2c804c481472585fa6ded26 + languageName: node + linkType: hard + +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 + languageName: node + linkType: hard + "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -1041,6 +3064,40 @@ __metadata: 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/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd + languageName: node + linkType: hard + "foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -1051,6 +3108,35 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.5": + version: 4.0.6 + resolution: "form-data@npm:4.0.6" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.4" + mime-types: "npm:^2.1.35" + checksum: 10/de6614c8537c92fa5fa3ee7e827758f98f5a9c033f348b7de81855ef36e5cb867e75d9f405d9483ab8d724a4a20d4e79926a299fa8dbba38f530eb659f0884e4 + languageName: node + linkType: hard + +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -1070,12 +3156,33 @@ __metadata: 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 +"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 + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10/185e20d20f10c8d661d59aac0f3b63b31132d492e1b11fcc2a93cb2c47257ebaee7407c38513efd2b35cafdf972d9beb2ea4593c1e0f3bf8f2744836928d7454 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 languageName: node linkType: hard @@ -1086,6 +3193,44 @@ __metadata: 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:^5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 10/0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 + 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:~10.5.0": version: 10.5.0 resolution: "glob@npm:10.5.0" @@ -1102,13 +3247,66 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10/94e296d69f92dc1c0768fcfeecfb3855582ab59a7c75e969d5f96ce50c3d201fd86d5a2857c22565764d5bb8a816c7b1e58f133ec318cd56274da36c5e3fb1a1 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard +"graphql-http@npm:^1.22.4": + version: 1.22.4 + resolution: "graphql-http@npm:1.22.4" + peerDependencies: + graphql: ">=0.11 <=16" + checksum: 10/ef81c3d86ac75743509d225aaf88a79262adee8801035712e5af655deedd5755afb0060e68306ca54aa54067c4ef0a382a03b2ecde016e0fb43454b73184a04d + languageName: node + linkType: hard + +"graphql-tag@npm:^2.12.6": + version: 2.12.7 + resolution: "graphql-tag@npm:2.12.7" + 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 || ^17.0.0 + checksum: 10/ae2e3cb416ac519da361cf329adca32817ecf627c9b579dc4828d649b3fdb1a3f12a464130c65dd35e8435de12778f026b2c5f98d9bd173dea6a996f953fed28 + languageName: node + linkType: hard + +"graphql-ws@npm:^6.0.7, graphql-ws@npm:^6.0.8": + version: 6.0.8 + resolution: "graphql-ws@npm:6.0.8" + peerDependencies: + "@fastify/websocket": ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + ws: ^8 + peerDependenciesMeta: + "@fastify/websocket": + optional: true + crossws: + optional: true + ws: + optional: true + checksum: 10/503d581c7dab4b9a884dad844fa9642a896803161aa1f1c8d3f12619e4e428f43cb39fe06a198c30bb685a521689d525b2870539c07bd68bb4bf704d039bdd9a + languageName: node + linkType: hard + +"graphql@npm:^16.13.0, graphql@npm:^16.14.0": + version: 16.14.2 + resolution: "graphql@npm:16.14.2" + checksum: 10/cd9a2581508f82621112a4dc625714e81ce725ab0a62502d97c14f61fee180e85276ac64bf9094f4fbf237737832ad4bb970aa26088f9dffece56f8919a49a29 + languageName: node + linkType: hard + "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -1116,6 +3314,38 @@ __metadata: 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, hasown@npm:^2.0.4": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10/13823863ae48161068b4c51606a3128451c66f14545a5169d667fe9fca168dcd38c27570c7a299e32ef844b8da3d55def7fe88602f8970d4311fb543ee88001a + languageName: node + linkType: hard + +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10/5f99bd91dae93d02867175c3856c561d7e3a24f16999b08f5fc79689044b938d7ed58457f4d8c8744c01403e6e0470b7896baa344d112b2355842fd935a75d69 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -1140,6 +3370,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/f0dce7bdcac5e8eaa0be3c7368bb8836ed010fb5b6349ffb412b172a203efe8f807d9a6681319105ea1b6901e1972c7b5ea899672a7b9aad58309f766dcbe0df + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -1159,6 +3399,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -1166,6 +3413,13 @@ __metadata: languageName: node linkType: hard +"inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 + languageName: node + linkType: hard + "ip-address@npm:10.1.1": version: 10.1.1 resolution: "ip-address@npm:10.1.1" @@ -1173,6 +3427,13 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:^2.0.5": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 10/3261a8b858edcc6c9566ba1694bf829e126faa88911d1c0a747ea658c5d81b14b6955e3a702d59dabadd58fdd440c01f321aa71d6547105fd21d03f94d0597e7 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -1187,6 +3448,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + "is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": version: 2.1.0 resolution: "is-unicode-supported@npm:2.1.0" @@ -1194,6 +3462,20 @@ __metadata: languageName: node linkType: hard +"is-what@npm:^5.2.0": + version: 5.5.0 + resolution: "is-what@npm:5.5.0" + checksum: 10/d53a6ea1aebf953f3bcf711a28e8463bfe79fc0e4e87575d77c692a30fd3d98f87b88d4c006c06753bf85f771c9d2c1d05b2c6b03c246883261fe190526195d9 + languageName: node + linkType: hard + +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1208,6 +3490,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: 10/e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -1249,6 +3540,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10/4b36e3479144ec196425f46b3618f8a96ce7e1b658f091a309cd4906215f5b7a402d7df331a3e0a09681381a658d0c5f039cb3cf6907e0a1e17ed847f5d37775 + languageName: node + linkType: hard + "js-tokens@npm:^10.0.0": version: 10.0.0 resolution: "js-tokens@npm:10.0.0" @@ -1256,6 +3554,50 @@ __metadata: languageName: node linkType: hard +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 10/59169a081e4eeb6f9559ae1f938f656191c000e0512aa6df9f3c8b2437a4ab1823819c6b9fd1818a4e39593ccfd72e9a051fdd3e2d1e340ed913679e888ded8c + languageName: node + linkType: hard + +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: 10/35f8cf8b5799c76570b211b079d4d706a20cbf13a4936d44cc7dbdacab1de6b346ab339ed3e3805f4693155ee5bbebbda4050fa2b666d61956e89a573089e3d4 + languageName: node + linkType: hard + +"level-supports@npm:^6.2.0": + version: 6.2.0 + resolution: "level-supports@npm:6.2.0" + checksum: 10/450c04839cf42ac7c73085b4928f1c1c51d9ab179aac9102cc8ef2389faf2d06cebaf57df2d025da89d78465004ccf29bfd972a04b0b35d5d423fa3f4516f906 + languageName: node + linkType: hard + +"level-transcoder@npm:^1.0.1": + version: 1.0.1 + resolution: "level-transcoder@npm:1.0.1" + dependencies: + buffer: "npm:^6.0.3" + module-error: "npm:^1.0.1" + checksum: 10/2fb41a1d8037fc279f851ead8cdc3852b738f1f935ac2895183cd606aae3e57008e085c7c2bd2b2d43cfd057333108cfaed604092e173ac2abdf5ab1b8333f9e + languageName: node + linkType: hard + +"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 + "lightningcss-android-arm64@npm:1.32.0": version: 1.32.0 resolution: "lightningcss-android-arm64@npm:1.32.0" @@ -1376,6 +3718,20 @@ __metadata: 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" @@ -1386,6 +3742,13 @@ __metadata: 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, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -1448,6 +3811,36 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd + languageName: node + linkType: hard + +"maybe-combine-errors@npm:^1.0.0": + version: 1.0.0 + resolution: "maybe-combine-errors@npm:1.0.0" + checksum: 10/16bb6d3dcf79fc61f5a04abe948c4c81cae0da6ee5da9a1d8196f1723b069d6ab60f752bc208e18481e2b82de146e068bc462558c65ecdf96fed0d021a1aa6ab + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 + languageName: node + linkType: hard + +"mime-types@npm:^2.1.35": + 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" @@ -1464,6 +3857,13 @@ __metadata: 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" @@ -1540,6 +3940,36 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba + 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" @@ -1547,6 +3977,65 @@ __metadata: languageName: node linkType: hard +"msgpackr-extract@npm:^3.0.2": + version: 3.0.4 + resolution: "msgpackr-extract@npm:3.0.4" + dependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.4" + "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.4" + "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.4" + "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.4" + "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.4" + "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.4" + node-gyp: "npm:latest" + node-gyp-build-optional-packages: "npm:5.2.2" + dependenciesMeta: + "@msgpackr-extract/msgpackr-extract-darwin-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-darwin-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-arm64": + optional: true + "@msgpackr-extract/msgpackr-extract-linux-x64": + optional: true + "@msgpackr-extract/msgpackr-extract-win32-x64": + optional: true + bin: + download-msgpackr-prebuilds: bin/download-prebuilds.js + checksum: 10/05a66482eca3c7932afef4300abc0cccfbb002185506d85d77fd2cff63870d58ef6903fef879ea09ff76ed0c18c9282a0dceb0d621a58e3c02adc9e0bfb8eb33 + languageName: node + linkType: hard + +"msgpackr@npm:^1.11.10, msgpackr@npm:^1.11.4": + version: 1.12.1 + resolution: "msgpackr@npm:1.12.1" + dependencies: + msgpackr-extract: "npm:^3.0.2" + dependenciesMeta: + msgpackr-extract: + optional: true + checksum: 10/90f5eabb2b059f441714ec0b09cfccea589caf36604598ae44a023e6c113f97889ca6c899beb318bc1a6bb7d6b4d6bef75a6fc13a01aa49c3111ae08da2c6db5 + languageName: node + linkType: hard + +"multipasta@npm:^0.2.7": + version: 0.2.7 + resolution: "multipasta@npm:0.2.7" + checksum: 10/244a7194ff508b3c5c1724f11c303f1c446cf6142cdbe82e57d5e59c44abb4942b1b983dd8c0d9c63080e684b2a8fa10f511df70d42dbef4d215ed7d41e76fcc + languageName: node + linkType: hard + +"nan@npm:^2.19.0, nan@npm:^2.23.0": + version: 2.28.0 + resolution: "nan@npm:2.28.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/299ffb18f43cc53b0728cb16ed15e868960168b84d4140b54c8e057c54e6714d0291eee63c02c3cae4bcf5d0d06b6a93fc8717d8d9ae72ad6d4e8eba5ddacc66 + languageName: node + linkType: hard + "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -1556,6 +4045,13 @@ __metadata: languageName: node linkType: hard +"napi-macros@npm:^2.2.2": + version: 2.2.2 + resolution: "napi-macros@npm:2.2.2" + checksum: 10/2cdb9c40ad4b424b14fbe5e13c5329559e2b511665acf41cdcda172fd2270202dc747a2d288b687c72bc70f654c797bc24a93adb67631128d62461588d7cc070 + languageName: node + linkType: hard + "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -1563,6 +4059,73 @@ __metadata: languageName: node linkType: hard +"nock@npm:^13.5.5": + version: 13.5.6 + resolution: "nock@npm:13.5.6" + dependencies: + debug: "npm:^4.1.0" + json-stringify-safe: "npm:^5.0.1" + propagate: "npm:^2.0.0" + checksum: 10/a57c265b75e5f7767e2f8baf058773cdbf357c31c5fea2761386ec03a008a657f9df921899fe2a9502773b47145b708863b32345aef529b3c45cba4019120f88 + languageName: node + linkType: hard + +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + +"node-gyp-build-optional-packages@npm:5.2.2": + version: 5.2.2 + resolution: "node-gyp-build-optional-packages@npm:5.2.2" + dependencies: + detect-libc: "npm:^2.0.1" + bin: + node-gyp-build-optional-packages: bin.js + node-gyp-build-optional-packages-optional: optional.js + node-gyp-build-optional-packages-test: build-test.js + checksum: 10/f448a328cf608071dc8cc4426ac5be0daec4788e4e1759e9f7ffcd286822cc799384edce17a8c79e610c4bbfc8e3aff788f3681f1d88290e0ca7aaa5342a090f + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/6a7d62289d1afc419fc8fc9bd00aa4e554369e50ca0acbc215cb91446148b75ff7e2a3b53c2c5b2c09a39d416d69f3d3237937860373104b5fe429bf30ad9ac5 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 11.4.2 resolution: "node-gyp@npm:11.4.2" @@ -1594,6 +4157,13 @@ __metadata: languageName: node linkType: hard +"normalize-path@npm:^3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10/88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 + languageName: node + linkType: hard + "object-inspect@npm:^1.12.3": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -1608,6 +4178,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" @@ -1633,6 +4219,18 @@ __metadata: languageName: unknown linkType: soft +"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" @@ -1694,10 +4292,70 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce +"picomatch@npm:4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce + 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.1.3": + 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:^10.3.1": + version: 10.3.1 + resolution: "pino@npm:10.3.1" + 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:^3.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:^4.0.0" + bin: + pino: bin.js + checksum: 10/46cad7bf1859c83a8a9c43af764e5165f44057ce76d44b1b2b4390f2abccb8a579f42abfe742d88b4d8e1d339213afb46ea50fc39c50095dd1f0f9fe26ea1342 languageName: node linkType: hard @@ -1719,6 +4377,27 @@ __metadata: 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" @@ -1729,6 +4408,77 @@ __metadata: 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:^3.0.1": + version: 3.0.1 + resolution: "properties-reader@npm:3.0.1" + dependencies: + "@kwsites/file-exists": "npm:^1.1.1" + mkdirp: "npm:^3.0.1" + checksum: 10/5a96c33dad9925c399bf3c3e343f3eb801fa1b4802330d1bfbf1e7d7639dafa935deb6c60be38b292544b57793b03ba1eaf5191cc3a7d018cda7bd6ead19a15c + languageName: node + linkType: hard + +"protobufjs@npm:^7.2.5, protobufjs@npm:^7.3.2, protobufjs@npm:^7.5.5": + version: 7.6.5 + resolution: "protobufjs@npm:7.6.5" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.5" + "@protobufjs/eventemitter": "npm:^1.1.1" + "@protobufjs/fetch": "npm:^1.1.1" + "@protobufjs/float": "npm:^1.0.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/58a5a635fbe0632a5c48a1ab659fabfc26d5b994915a20ad623a2cfb59a2ca7411d9f4f690ca79074967b632e2db58ecc184ab507927f5b7d0852fc16568d3da + 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: "pure-rand@npm:6.1.0" + checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4 + languageName: node + linkType: hard + "pure-rand@npm:^8.0.0": version: 8.4.0 resolution: "pure-rand@npm:8.4.0" @@ -1736,6 +4486,82 @@ __metadata: 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 + +"real-require@npm:^1.0.0": + version: 1.0.0 + resolution: "real-require@npm:1.0.0" + checksum: 10/ac2ae7681e20c92be45a5f49110414af1576c7b4512869c2260076a69fc7c336335ef354f466a3be92e779c55b8df0b0043d191797d82d7f18e6310958e5a890 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf + languageName: node + linkType: hard + "restore-cursor@npm:^5.0.0": version: 5.1.0 resolution: "restore-cursor@npm:5.1.0" @@ -1811,13 +4637,57 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.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.2 resolution: "semver@npm:7.7.2" @@ -1866,6 +4736,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -1880,6 +4757,15 @@ __metadata: 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" @@ -1901,6 +4787,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.1 + resolution: "sonic-boom@npm:4.2.1" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/161af46b3e6debc4ad3865b0db47f37289741a0b3005b8cf056f93a4e0e1a347e24ca1a2d8ccc864f7f19caa6185a766797f8382cdbfd2f3d046a0323d73a542 + languageName: node + linkType: hard + "source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -1908,6 +4803,47 @@ __metadata: languageName: node linkType: hard +"split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 10/1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f + languageName: node + linkType: hard + +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab + languageName: node + linkType: hard + +"ssh-remote-port-forward@npm:^1.0.4": + version: 1.0.4 + resolution: "ssh-remote-port-forward@npm:1.0.4" + dependencies: + "@types/ssh2": "npm:^0.5.48" + ssh2: "npm:^1.4.0" + checksum: 10/c6c04c5ddfde7cb06e9a8655a152bd28fe6771c6fe62ff0bc08be229491546c410f30b153c968b8d6817a57d38678a270c228f30143ec0fe1be546efc4f6b65a + languageName: node + linkType: hard + +"ssh2@npm:^1.15.0, ssh2@npm:^1.4.0": + version: 1.17.0 + resolution: "ssh2@npm:1.17.0" + dependencies: + asn1: "npm:^0.2.6" + bcrypt-pbkdf: "npm:^1.0.2" + cpu-features: "npm:~0.0.10" + nan: "npm:^2.23.0" + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: 10/5a7e911f234f73c4332f2b436cc6131c164962d2eac71f463ab401b54c4b8627875d9c9be1c55e0bfd1a0eae108cfa33217bc73939287e4a5e81f34f532b1036 + languageName: node + linkType: hard + "ssri@npm:^12.0.0": version: 12.0.0 resolution: "ssri@npm:12.0.0" @@ -1938,7 +4874,18 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.25.0": + version: 2.28.0 + resolution: "streamx@npm:2.28.0" + dependencies: + events-universal: "npm:^1.0.0" + fast-fifo: "npm:^1.3.2" + text-decoder: "npm:^1.1.0" + checksum: 10/ed9a289f09dca9a7bb03790f8b60f9e6bab1032e3fc426e939e7c8207b0511777d96905589a7c9c6d9c9b32e2f94dc884c2e5477ac38524e37f9ea0dfaf8227d + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, 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: @@ -1970,6 +4917,24 @@ __metadata: 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-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -1988,6 +4953,22 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:^5.0.2": + version: 5.0.3 + resolution: "strip-json-comments@npm:5.0.3" + checksum: 10/3ccbf26f278220f785e4b71f8a719a6a063d72558cc63cb450924254af258a4f4c008b8c9b055373a680dc7bd525be9e543ad742c177f8a7667e0b726258e0e4 + languageName: node + linkType: hard + +"superjson@npm:^2.2.6": + version: 2.2.6 + resolution: "superjson@npm:2.2.6" + dependencies: + copy-anything: "npm:^4" + checksum: 10/7bb6446b70e8a37ec9aa2f2d08295ae4e7e8268b86c89d83a306b3798cd0cc60d89016c0c5fa83b558db23e8de8863c585a4cf52d18c4834c48bad7d2b6ee25b + languageName: node + linkType: hard + "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -1997,6 +4978,60 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^2.1.4": + version: 2.1.5 + resolution: "tar-fs@npm:2.1.5" + 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/9dfbde66d223e3164a8018521c7c1eb205696917cbfb116d15cde286ccc7f2445964bb3d4772f369eb003aad67c2573e4f1390c74266fee32fa16cb846b6d144 + languageName: node + linkType: hard + +"tar-fs@npm:^3.1.2": + version: 3.1.3 + resolution: "tar-fs@npm:3.1.3" + 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/858176af2e7c41a302026c6f21940160d50045f567e4276950c921c501e8ddb42edbf5b5437298092fe60d42c7b999a599279d94c3f369b9adb1cfc5b57486fe + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + languageName: node + linkType: hard + +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": + version: 3.2.0 + resolution: "tar-stream@npm:3.2.0" + dependencies: + b4a: "npm:^1.6.4" + bare-fs: "npm:^4.5.5" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10/ce57a81521de73ae7a3b7d55a08da50d6771427c249bfa89a208518e48faf5254c8fa7201a8f5419ab8bde9601a74e6dd512b31a13ec89774aec96178f99a8d3 + languageName: node + linkType: hard + "tar@npm:~7.5.11": version: 7.5.16 resolution: "tar@npm:7.5.16" @@ -2010,6 +5045,56 @@ __metadata: languageName: node linkType: hard +"teex@npm:^1.0.1": + version: 1.0.1 + resolution: "teex@npm:1.0.1" + dependencies: + streamx: "npm:^2.12.5" + checksum: 10/36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 + languageName: node + linkType: hard + +"testcontainers@npm:^12.0.0": + version: 12.0.4 + resolution: "testcontainers@npm:12.0.4" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + "@types/dockerode": "npm:^4.0.1" + archiver: "npm:^7.0.1" + async-lock: "npm:^1.4.1" + byline: "npm:^5.0.0" + debug: "npm:^4.4.3" + docker-compose: "npm:^1.4.2" + dockerode: "npm:^5.0.0" + get-port: "npm:^5.1.1" + proper-lockfile: "npm:^4.1.2" + properties-reader: "npm:^3.0.1" + ssh-remote-port-forward: "npm:^1.0.4" + tar-fs: "npm:^3.1.2" + tmp: "npm:^0.2.7" + undici: "npm:^8.5.0" + checksum: 10/a63752b69014306311be2f0919c6acb4e3f7ded88bbf73962cc369c643f44023eeb0f0afc4093b98fe61f972bc85e97c46eda44b536b2b53c47ac879f2801022 + 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:^4.0.0": + version: 4.2.0 + resolution: "thread-stream@npm:4.2.0" + dependencies: + real-require: "npm:^1.0.0" + checksum: 10/040d1f5284806a28d12ac83fc0a1f0b32547c6773b5fab9494c664798fa7fb14197bede695ec61221c5f93f64a32b0f1850408d0ad5730af03ee1d1efe6c1b05 + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -2051,6 +5136,20 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.7": + version: 0.2.7 + resolution: "tmp@npm:0.2.7" + checksum: 10/0a3bc90beb0c6275273c3475fb57e466eaab1c9c4a101d029ff62b18146ce136e7f75d09de34863d9f2c2a492751402508f9e028bc98eb34a1416195d4b15619 + 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" @@ -2089,7 +5188,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 @@ -2125,6 +5224,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^0.14.3": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 10/04ee27901cde46c1c0a64b9584e04c96c5fe45b38c0d74930710751ea991408b405747d01dfae72f80fc158137018aea94f9c38c651cb9c318f0861a310c3679 + languageName: node + linkType: hard + "typescript@npm:^6.0.3": version: 6.0.3 resolution: "typescript@npm:6.0.3" @@ -2152,6 +5258,27 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: 10/6681d2837ac75a75ac1cc46090aa2b8ddc7c6b8ecc295a6cdb06838752a730da3d8afeecf05e5ab7903160eafad3a8b6ffa1927e5ded260590f4d4fef18646d5 + languageName: node + linkType: hard + +"undici@npm:^8.5.0": + version: 8.7.0 + resolution: "undici@npm:8.7.0" + checksum: 10/e9644903bda97f825228b4c7bee95b3586609f94df685b598c32bdaf3ac795928cbd25b0ed2690c3ea0b5abd324f5a9641b63b6c81c9b19f3ef4d5b3e402a48c + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -2170,6 +5297,13 @@ __metadata: languageName: node linkType: hard +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -2302,6 +5436,37 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + +"web-worker@npm:^1.5.0": + version: 1.5.0 + resolution: "web-worker@npm:1.5.0" + checksum: 10/1209461e2c731fe8e8297c95a8a324c6dd00fd9f3c489ed79d18a15592731324762b7b06c8b6bc404596259aa13cd413119e0153e12a80f47a7f374960461e0d + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -2336,7 +5501,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -2358,6 +5523,35 @@ __metadata: 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.18.0, ws@npm:^8.20.0, ws@npm:^8.8.1": + 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" @@ -2372,6 +5566,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.3 + resolution: "yargs@npm:17.7.3" + 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/a3826798c03b159e139d0580a3b2733953889a9a1bac8e4e1ca7a1a249b55315b213c323a6a1dbdb305f6e59496a9eaa810742c87e34abcf1a0584d8f59212a1 + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -2385,3 +5610,14 @@ __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