From f616ca987f325a45e47f9689a6d81174c827fbf1 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 23 Jun 2026 00:10:42 +0200 Subject: [PATCH 01/10] test: migrate suites to async simulator API Migrate every module's per-contract simulator and specs from the old synchronous createSimulator to the new async, backend-aware API: the factory now returns a class whose create() is awaited and whose circuits return promises, so one spec file runs on both backends selected by MIDNIGHT_BACKEND=dry|live. * simulators: constructor -> static async create(...); delegating methods return Promise; private-state injection helpers use the async getPrivateState/setPrivateState; artifactName added so the live harness can locate compiled assets + ZK keys. * specs: new X() -> await X.create(); every circuit/state call awaited; failure paths use await expect(...).rejects.toThrow(...); caller identity uses alias strings (as('OWNER')) in access + multisig. * live: a test:live script, vitest.live.config.ts, a registerLiveBackend harness (deploy-per-test against the local stack), and a label-gated live-tests CI workflow; make env-up / local-env.yml bring up node + indexer + proof server. * deps: consume @openzeppelin/compact-simulator via portal: against the sibling compact-tools checkout until the live backend is published. Validated dry: 28 files, 1183 tests green. Live: proven end-to-end on the security module against a local node. Note: the live _harness/ (network/deploy/wallet/ownWallet/providers) is shared infra that overlaps the in-flight native-shielded-token branch and should be de-duplicated at merge. --- .github/workflows/test-live.yml | 91 + Makefile | 31 + contracts/package.json | 3 +- .../src/access/test/AccessControl.test.ts | 695 ++++--- contracts/src/access/test/Ownable.test.ts | 364 ++-- .../access/test/ShieldedAccessControl.test.ts | 1203 ++++++------ contracts/src/access/test/ZOwnablePK.test.ts | 271 +-- .../test/simulators/AccessControlSimulator.ts | 66 +- .../test/simulators/OwnableSimulator.ts | 59 +- .../ShieldedAccessControlSimulator.ts | 101 +- .../test/simulators/ZOwnablePKSimulator.ts | 58 +- .../src/archive/test/ShieldedToken.test.ts | 115 +- .../test/simulators/ShieldedTokenSimulator.ts | 53 +- contracts/src/multisig/test/Forwarder.test.ts | 73 +- .../multisig/test/ForwarderPrivate.test.ts | 120 +- .../src/multisig/test/ProposalManager.test.ts | 264 +-- .../multisig/test/ShieldedMultiSig.test.ts | 492 ++--- .../multisig/test/ShieldedMultiSigV2.test.ts | 88 +- .../multisig/test/ShieldedMultiSigV3.test.ts | 393 ++-- .../multisig/test/ShieldedTreasury.test.ts | 140 +- contracts/src/multisig/test/Signer.test.ts | 408 +++-- .../src/multisig/test/SignerManager.test.ts | 204 ++- .../test/presets/ForwarderPrivate.test.ts | 34 +- .../test/presets/ForwarderShielded.test.ts | 22 +- .../test/presets/ForwarderUnshielded.test.ts | 26 +- .../MockForwarderPrivateSimulator.ts | 21 +- .../MockForwarderShieldedSimulator.ts | 19 +- .../MockForwarderUnshieldedSimulator.ts | 19 +- .../simulators/ProposalManagerSimulator.ts | 32 +- .../simulators/ShieldedMultiSigSimulator.ts | 55 +- .../simulators/ShieldedMultiSigV2Simulator.ts | 29 +- .../simulators/ShieldedMultiSigV3Simulator.ts | 37 +- .../simulators/ShieldedTreasurySimulator.ts | 24 +- .../test/simulators/SignerManagerSimulator.ts | 37 +- .../test/simulators/SignerSimulator.ts | 35 +- .../presets/ForwarderPrivateSimulator.ts | 21 +- .../presets/ForwarderShieldedSimulator.ts | 19 +- .../presets/ForwarderUnshieldedSimulator.ts | 19 +- .../src/security/test/Initializable.test.ts | 60 +- contracts/src/security/test/Pausable.test.ts | 84 +- .../test/simulators/InitializableSimulator.ts | 24 +- .../test/simulators/PausableSimulator.ts | 30 +- .../src/token/test/FungibleToken.test.ts | 1074 ++++++----- contracts/src/token/test/MultiToken.test.ts | 1266 +++++++------ .../src/token/test/nonFungibleToken.test.ts | 1632 +++++++++-------- .../test/simulators/FungibleTokenSimulator.ts | 87 +- .../test/simulators/MultiTokenSimulator.ts | 72 +- .../simulators/NonFungibleTokenSimulator.ts | 118 +- .../utils/test/simulators/UtilsSimulator.ts | 38 +- contracts/src/utils/test/utils.test.ts | 170 +- contracts/test/integration/_harness/deploy.ts | 84 + .../test/integration/_harness/live.setup.ts | 7 + .../test/integration/_harness/network.ts | 63 + .../test/integration/_harness/ownWallet.ts | 289 +++ .../test/integration/_harness/providers.ts | 56 + .../_harness/registerSimulatorLive.ts | 154 ++ contracts/test/integration/_harness/wallet.ts | 17 + contracts/vitest.live.config.ts | 24 + local-env.yml | 61 + yarn.lock | 19 +- 60 files changed, 6396 insertions(+), 4774 deletions(-) create mode 100644 .github/workflows/test-live.yml create mode 100644 Makefile create mode 100644 contracts/test/integration/_harness/deploy.ts create mode 100644 contracts/test/integration/_harness/live.setup.ts create mode 100644 contracts/test/integration/_harness/network.ts create mode 100644 contracts/test/integration/_harness/ownWallet.ts create mode 100644 contracts/test/integration/_harness/providers.ts create mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts create mode 100644 contracts/test/integration/_harness/wallet.ts create mode 100644 contracts/vitest.live.config.ts create mode 100644 local-env.yml diff --git a/.github/workflows/test-live.yml b/.github/workflows/test-live.yml new file mode 100644 index 00000000..493779d4 --- /dev/null +++ b/.github/workflows/test-live.yml @@ -0,0 +1,91 @@ +name: Compact Contracts Live Test Suite + +# Runs the unit specs against a real local Midnight stack (node + indexer + +# proof-server) via the simulator's live backend (`MIDNIGHT_BACKEND=live`). +# This is the full prove -> submit -> index -> read loop, so it is slow and +# heavy: gated to manual runs and PRs explicitly labeled `live-tests` rather +# than every push. +# +# DRAFT: this depends on `@openzeppelin/compact-simulator`'s live backend, which +# is consumed here via a `portal:` dependency on the sibling `compact-tools` +# checkout below. Once the simulator is published with the live backend, the +# "Check out simulator" + "Build simulator" steps can be dropped and the +# dependency pinned to the published version. + +on: + workflow_dispatch: + pull_request: + types: [labeled, synchronize, reopened] + +jobs: + run-live-suite: + name: Run Live Test Suite + # Only on manual dispatch or when the PR carries the `live-tests` label. + if: >- + github.event_name == 'workflow_dispatch' || + contains(github.event.pull_request.labels.*.name, 'live-tests') + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 60 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Check out compact-contracts + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + path: compact-contracts + + # Sibling checkout so the `portal:../../compact-tools/packages/simulator` + # dependency in contracts/package.json resolves. + - name: Check out compact-tools (simulator source) + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: OpenZeppelin/compact-tools + ref: main + path: compact-tools + + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24 + - name: Enable Corepack + run: corepack enable + + - name: Build simulator (live backend) + working-directory: compact-tools + run: | + yarn install --immutable + yarn workspace @openzeppelin/compact-simulator build + + - name: Install contracts dependencies + working-directory: compact-contracts + run: yarn install --no-immutable + + - name: Start local Midnight stack + working-directory: compact-contracts + run: make env-up + + - name: Wait for stack to be healthy + run: | + for i in $(seq 1 30); do + if curl -fsS http://127.0.0.1:6300/version >/dev/null \ + && curl -fsS http://127.0.0.1:9944/health >/dev/null; then + echo "stack healthy"; exit 0 + fi + echo "waiting for stack... ($i)"; sleep 5 + done + echo "stack did not become healthy in time"; exit 1 + + - name: Run live tests + working-directory: compact-contracts + run: yarn test:live + + - name: Stop local Midnight stack + if: always() + working-directory: compact-contracts + run: make env-down diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e5a601bf --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +COMPOSE_FILE := local-env.yml +LOGS_DIR := logs +SERVICES := proof-server indexer node + +.PHONY: env-up env-down env-logs env-logs-clean env-status + +## Start local environment and stream logs to logs/ +env-up: env-down + docker compose -f $(COMPOSE_FILE) up -d + @mkdir -p $(LOGS_DIR) + @for svc in $(SERVICES); do \ + docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ + done + @echo "Logs streaming to $(LOGS_DIR)/" + +## Stop local environment +env-down: + @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true + docker compose -f $(COMPOSE_FILE) down + +## Tail all logs +env-logs: + tail -f $(LOGS_DIR)/*.log + +## Clear log files +env-logs-clean: + rm -rf $(LOGS_DIR)/*.log + +## Show container status +env-status: + docker compose -f $(COMPOSE_FILE) ps diff --git a/contracts/package.json b/contracts/package.json index 91b68ba1..d1a80115 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,6 +34,7 @@ "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", "types": "tsc -p tsconfig.json --noEmit", @@ -46,7 +47,7 @@ "@openzeppelin/compact-cli": "^0.0.2" }, "devDependencies": { - "@openzeppelin/compact-simulator": "^0.1.0", + "@openzeppelin/compact-simulator": "portal:../../compact-tools/packages/simulator", "@tsconfig/node24": "^24.0.4", "@types/node": "25.9.3", "@vitest/coverage-v8": "^4.1.9", diff --git a/contracts/src/access/test/AccessControl.test.ts b/contracts/src/access/test/AccessControl.test.ts index a8d28ec6..7e6e9410 100644 --- a/contracts/src/access/test/AccessControl.test.ts +++ b/contracts/src/access/test/AccessControl.test.ts @@ -78,81 +78,93 @@ const operatorTypes = [ ] as const; describe('AccessControl', () => { - beforeEach(() => { - accessControl = new AccessControlSimulator(); + beforeEach(async () => { + accessControl = await AccessControlSimulator.create(); }); describe('hasRole', () => { - beforeEach(() => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + beforeEach(async () => { + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); }); - it('should return true when operator has a role', () => { - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + it('should return true when operator has a role', async () => { + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); - it('should return false when unauthorized', () => { - expect(accessControl.hasRole(OPERATOR_ROLE_1, UNAUTHORIZED.either)).toBe( - false, - ); + it('should return false when unauthorized', async () => { + expect( + await accessControl.hasRole(OPERATOR_ROLE_1, UNAUTHORIZED.either), + ).toBe(false); }); - it('should return false when role does not exist', () => { - expect(accessControl.hasRole(UNINITIALIZED_ROLE, OP1.either)).toBe(false); + it('should return false when role does not exist', async () => { + expect(await accessControl.hasRole(UNINITIALIZED_ROLE, OP1.either)).toBe( + false, + ); }); - it('should return true when queried with dirty Either (canonicalization)', () => { + it('should return true when queried with dirty Either (canonicalization)', async () => { const dirtyEither = { is_left: true, left: OP1.accountId, right: { bytes: new Uint8Array(32).fill(0xff) }, }; - expect(accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe( + true, + ); }); - it('should return false when dirty Either has wrong accountId', () => { + it('should return false when dirty Either has wrong accountId', async () => { const dirtyEither = { is_left: true, left: UNAUTHORIZED.accountId, right: { bytes: new Uint8Array(32).fill(0xff) }, }; - expect(accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe(false); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe( + false, + ); }); - it('should match hasRole with dirty left side on contract address', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); + it('should match hasRole with dirty left side on contract address', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); const dirtyContract = { is_left: false, left: new Uint8Array(32).fill(0xff), right: OP1_CONTRACT.right, }; - expect(accessControl.hasRole(OPERATOR_ROLE_1, dirtyContract)).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, dirtyContract)).toBe( + true, + ); }); }); describe('assertOnlyRole', () => { - beforeEach(() => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + beforeEach(async () => { + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); }); - it('should allow operator with role to call', () => { + it('should allow operator with role to call', async () => { // Set secret key for OP1 - accessControl.privateState.injectSecretKey(OP1.secretKey); + await accessControl.privateState.injectSecretKey(OP1.secretKey); - expect(() => accessControl.assertOnlyRole(OPERATOR_ROLE_1)).not.toThrow(); + await expect( + accessControl.assertOnlyRole(OPERATOR_ROLE_1), + ).resolves.not.toThrow(); }); - it('should fail if caller is unauthorized', () => { + it('should fail if caller is unauthorized', async () => { // Set bad secret key - accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => accessControl.assertOnlyRole(OPERATOR_ROLE_1)).toThrow( - 'AccessControl: unauthorized account', - ); + await expect( + accessControl.assertOnlyRole(OPERATOR_ROLE_1), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('should fail when contract address matches accountId bytes but is right variant', () => { + it('should fail when contract address matches accountId bytes but is right variant', async () => { // Grant role to a contract address whose bytes match ADMIN's accountId const contractWithSameBytes = { is_left: false, @@ -160,510 +172,591 @@ describe('AccessControl', () => { right: { bytes: ADMIN.accountId }, }; - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, contractWithSameBytes); + await accessControl._unsafeGrantRole( + OPERATOR_ROLE_1, + contractWithSameBytes, + ); expect( - accessControl.hasRole(OPERATOR_ROLE_1, contractWithSameBytes), + await accessControl.hasRole(OPERATOR_ROLE_1, contractWithSameBytes), ).toBe(true); // ADMIN's witness produces left(H(sk)) which has the same bytes // but is a different Either variant than the granted role - accessControl.privateState.injectSecretKey(ADMIN.secretKey); - expect(() => { - accessControl.assertOnlyRole(OPERATOR_ROLE_1); - }).toThrow('AccessControl: unauthorized account'); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await expect( + accessControl.assertOnlyRole(OPERATOR_ROLE_1), + ).rejects.toThrow('AccessControl: unauthorized account'); }); }); describe('_checkRole', () => { - beforeEach(() => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); + beforeEach(async () => { + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); }); - it('should not fail if user has role', () => { - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + it('should not fail if user has role', async () => { + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); - expect(() => + await expect( accessControl._checkRole(OPERATOR_ROLE_1, OP1.either), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('should not fail if contract has role', () => { - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe(true); + it('should not fail if contract has role', async () => { + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe( + true, + ); - expect(() => + await expect( accessControl._checkRole(OPERATOR_ROLE_1, OP1_CONTRACT), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('should fail if operator is unauthorized', () => { - expect(() => + it('should fail if operator is unauthorized', async () => { + await expect( accessControl._checkRole(OPERATOR_ROLE_1, UNAUTHORIZED.either), - ).toThrow('AccessControl: unauthorized account'); + ).rejects.toThrow('AccessControl: unauthorized account'); }); }); describe('DEFAULT_ADMIN_ROLE', () => { - it('should return zero bytes', () => { - expect(accessControl.DEFAULT_ADMIN_ROLE()).toEqual(DEFAULT_ADMIN_ROLE); + it('should return zero bytes', async () => { + expect(await accessControl.DEFAULT_ADMIN_ROLE()).toEqual( + DEFAULT_ADMIN_ROLE, + ); }); }); describe('getRoleAdmin', () => { - it('should return default admin role if admin role not set', () => { - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + it('should return default admin role if admin role not set', async () => { + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( DEFAULT_ADMIN_ROLE, ); }); - it('should return custom admin role if set', () => { - accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + it('should return custom admin role if set', async () => { + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( CUSTOM_ADMIN_ROLE, ); }); }); describe('grantRole', () => { - beforeEach(() => { - accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); + beforeEach(async () => { + await accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); }); - it('admin should grant role', () => { + it('admin should grant role', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + await accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); - it('admin should grant multiple roles', () => { + it('admin should grant multiple roles', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); for (let i = 0; i < operatorRolesList.length; i++) { for (let j = 0; j < commitmentOperators.length; j++) { - accessControl.grantRole(operatorRolesList[i], commitmentOperators[j]); + await accessControl.grantRole( + operatorRolesList[i], + commitmentOperators[j], + ); expect( - accessControl.hasRole(operatorRolesList[i], commitmentOperators[j]), + await accessControl.hasRole( + operatorRolesList[i], + commitmentOperators[j], + ), ).toBe(true); } } }); - it('should fail if unauthorized grants role', () => { + it('should fail if unauthorized grants role', async () => { // Set unauthorized SK - accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('should fail if operator grants role', () => { + it('should fail if operator grants role', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); // Set OP1 SK - accessControl.privateState.injectSecretKey(OP1.secretKey); + await accessControl.privateState.injectSecretKey(OP1.secretKey); - expect(() => { - accessControl.grantRole(OPERATOR_ROLE_1, OP2.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.grantRole(OPERATOR_ROLE_1, OP2.either), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('should fail if admin grants role to ContractAddress', () => { + it('should fail if admin grants role to ContractAddress', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - expect(() => { - accessControl.grantRole(OPERATOR_ROLE_1, OP1_CONTRACT); - }).toThrow('AccessControl: unsafe role approval'); + await expect( + accessControl.grantRole(OPERATOR_ROLE_1, OP1_CONTRACT), + ).rejects.toThrow('AccessControl: unsafe role approval'); }); - it('admin should not be able to grant after self-revocation', () => { + it('admin should not be able to grant after self-revocation', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.revokeRole(DEFAULT_ADMIN_ROLE, ADMIN.either); + await accessControl.revokeRole(DEFAULT_ADMIN_ROLE, ADMIN.either); - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), - ).toThrow('AccessControl: unauthorized account'); + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('admin should not be able to grant after renouncing role', () => { + it('admin should not be able to grant after renouncing role', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.renounceRole(DEFAULT_ADMIN_ROLE, ADMIN.either); + await accessControl.renounceRole(DEFAULT_ADMIN_ROLE, ADMIN.either); - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), - ).toThrow('AccessControl: unauthorized account'); + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('admin authority should not be transitive across role hierarchies', () => { - accessControl._setRoleAdmin(OPERATOR_ROLE_2, OPERATOR_ROLE_1); - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + it('admin authority should not be transitive across role hierarchies', async () => { + await accessControl._setRoleAdmin(OPERATOR_ROLE_2, OPERATOR_ROLE_1); + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); // ADMIN holds DEFAULT_ADMIN_ROLE but not OPERATOR_ROLE_1 - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_2, OP2.either), - ).toThrow('AccessControl: unauthorized account'); + ).rejects.toThrow('AccessControl: unauthorized account'); // OP1 holds OPERATOR_ROLE_1 which is admin of OPERATOR_ROLE_2 - accessControl.privateState.injectSecretKey(OP1.secretKey); - expect(() => + await accessControl.privateState.injectSecretKey(OP1.secretKey); + await expect( accessControl.grantRole(OPERATOR_ROLE_2, OP2.either), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('admin should re-grant a role after revoking it', () => { - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + it('admin should re-grant a role after revoking it', async () => { + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + await accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); - accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); + await accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + await accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); - it('should be idempotent when granting an already-held role', () => { - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + it('should be idempotent when granting an already-held role', async () => { + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + await accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); // Second grant should not throw or corrupt state - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), - ).not.toThrow(); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + ).resolves.not.toThrow(); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); // Revoke should still work normally after double-grant - accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); + await accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); }); }); describe('revokeRole', () => { - beforeEach(() => { - accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); + beforeEach(async () => { + await accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); }); describe.each( operatorTypes, )('when the operator is a %s', (_operatorType, _operator) => { - it('admin should revoke role', () => { + it('admin should revoke role', async () => { // Set admin SK - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - accessControl.revokeRole(OPERATOR_ROLE_1, _operator); - expect(accessControl.hasRole(OPERATOR_ROLE_1, _operator)).toBe(false); + await accessControl.revokeRole(OPERATOR_ROLE_1, _operator); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, _operator)).toBe( + false, + ); }); }); - it('should fail if unauthorized revokes role', () => { - accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + it('should fail if unauthorized revokes role', async () => { + await accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('should fail if operator revokes role', () => { - accessControl.privateState.injectSecretKey(OP1.secretKey); + it('should fail if operator revokes role', async () => { + await accessControl.privateState.injectSecretKey(OP1.secretKey); - expect(() => { - accessControl.revokeRole(OPERATOR_ROLE_1, OP2.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.revokeRole(OPERATOR_ROLE_1, OP2.either), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('admin should revoke multiple roles', () => { - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + it('admin should revoke multiple roles', async () => { + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); for (let i = 0; i < operatorRolesList.length; i++) { for (let j = 0; j < allOperators.length; j++) { - accessControl._unsafeGrantRole(operatorRolesList[i], allOperators[j]); - accessControl.revokeRole(operatorRolesList[i], allOperators[j]); + await accessControl._unsafeGrantRole( + operatorRolesList[i], + allOperators[j], + ); + await accessControl.revokeRole(operatorRolesList[i], allOperators[j]); expect( - accessControl.hasRole(operatorRolesList[i], allOperators[j]), + await accessControl.hasRole(operatorRolesList[i], allOperators[j]), ).toBe(false); } } }); - it('should not corrupt state when revoking a never-granted role', () => { - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + it('should not corrupt state when revoking a never-granted role', async () => { + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); // Revoke a role that was never granted to OP2 - accessControl.revokeRole(OPERATOR_ROLE_2, OP2.either); - expect(accessControl.hasRole(OPERATOR_ROLE_2, OP2.either)).toBe(false); + await accessControl.revokeRole(OPERATOR_ROLE_2, OP2.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_2, OP2.either)).toBe( + false, + ); // Subsequent grant should still work - accessControl.grantRole(OPERATOR_ROLE_2, OP2.either); - expect(accessControl.hasRole(OPERATOR_ROLE_2, OP2.either)).toBe(true); + await accessControl.grantRole(OPERATOR_ROLE_2, OP2.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_2, OP2.either)).toBe( + true, + ); }); }); describe('renounceRole', () => { - beforeEach(() => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + beforeEach(async () => { + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); }); - it('should allow operator to renounce own role', () => { - accessControl.privateState.injectSecretKey(OP1.secretKey); + it('should allow operator to renounce own role', async () => { + await accessControl.privateState.injectSecretKey(OP1.secretKey); - accessControl.renounceRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); + await accessControl.renounceRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); }); // Should be refactored with c2c - it('should fail when renouncing as a ContractAddress', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); + it('should fail when renouncing as a ContractAddress', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - expect(() => { - accessControl.renounceRole(OPERATOR_ROLE_1, OP1_CONTRACT); - }).toThrow('AccessControl: bad confirmation'); + await expect( + accessControl.renounceRole(OPERATOR_ROLE_1, OP1_CONTRACT), + ).rejects.toThrow('AccessControl: bad confirmation'); }); - it('should fail when unauthorized renounces role', () => { - accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + it('should fail when unauthorized renounces role', async () => { + await accessControl.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - accessControl.renounceRole(OPERATOR_ROLE_1, OP1.either); - }).toThrow('AccessControl: bad confirmation'); + await expect( + accessControl.renounceRole(OPERATOR_ROLE_1, OP1.either), + ).rejects.toThrow('AccessControl: bad confirmation'); }); - it('should not fail when renouncing a role not held', () => { - accessControl.privateState.injectSecretKey(OP1.secretKey); + it('should not fail when renouncing a role not held', async () => { + await accessControl.privateState.injectSecretKey(OP1.secretKey); // Confirm role not already held - expect(accessControl.hasRole(OPERATOR_ROLE_3, OP1.either)).toBe(false); + expect(await accessControl.hasRole(OPERATOR_ROLE_3, OP1.either)).toBe( + false, + ); - accessControl.renounceRole(OPERATOR_ROLE_3, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_3, OP1.either)).toBe(false); + await accessControl.renounceRole(OPERATOR_ROLE_3, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_3, OP1.either)).toBe( + false, + ); }); }); describe('_setRoleAdmin', () => { - beforeEach(() => { - accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + beforeEach(async () => { + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); }); - it('should set role admin', () => { - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + it('should set role admin', async () => { + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( CUSTOM_ADMIN_ROLE, ); }); - it('should set multiple role admins', () => { - accessControl._setRoleAdmin(OPERATOR_ROLE_2, CUSTOM_ADMIN_ROLE); - accessControl._setRoleAdmin(OPERATOR_ROLE_3, CUSTOM_ADMIN_ROLE); + it('should set multiple role admins', async () => { + await accessControl._setRoleAdmin(OPERATOR_ROLE_2, CUSTOM_ADMIN_ROLE); + await accessControl._setRoleAdmin(OPERATOR_ROLE_3, CUSTOM_ADMIN_ROLE); - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_1)).toEqual( CUSTOM_ADMIN_ROLE, ); - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_2)).toEqual( + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_2)).toEqual( CUSTOM_ADMIN_ROLE, ); - expect(accessControl.getRoleAdmin(OPERATOR_ROLE_3)).toEqual( + expect(await accessControl.getRoleAdmin(OPERATOR_ROLE_3)).toEqual( CUSTOM_ADMIN_ROLE, ); }); - it('should authorize new admin to grant / revoke roles', () => { - accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); - accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + it('should authorize new admin to grant / revoke roles', async () => { + await accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); // Set custom admin SK - accessControl.privateState.injectSecretKey(CUSTOM_ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(CUSTOM_ADMIN.secretKey); // Grant role and check it's been granted - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), - ).not.toThrow(); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + ).resolves.not.toThrow(); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); // Revoke role and check it's been revoked - expect(() => + await expect( accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either), - ).not.toThrow(); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); + ).resolves.not.toThrow(); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); }); - it('should disallow previous admin from granting / revoking roles', () => { - accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); - accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); - accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + it('should disallow previous admin from granting / revoking roles', async () => { + await accessControl._grantRole(DEFAULT_ADMIN_ROLE, ADMIN.either); + await accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); // Set init admin - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - expect(() => { - accessControl.grantRole(OPERATOR_ROLE_1, OP1.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), + ).rejects.toThrow('AccessControl: unauthorized account'); - expect(() => { - accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either); - }).toThrow('AccessControl: unauthorized account'); + await expect( + accessControl.revokeRole(OPERATOR_ROLE_1, OP1.either), + ).rejects.toThrow('AccessControl: unauthorized account'); }); - it('should allow overwriting admin role and transfer authority', () => { + it('should allow overwriting admin role and transfer authority', async () => { const NEW_ADMIN_ROLE = convertFieldToBytes(32, 99n, ''); const NEW_ADMIN = makeUser('NEW_ADMIN'); - accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); - accessControl._grantRole(NEW_ADMIN_ROLE, NEW_ADMIN.either); - accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); + await accessControl._grantRole(CUSTOM_ADMIN_ROLE, CUSTOM_ADMIN.either); + await accessControl._grantRole(NEW_ADMIN_ROLE, NEW_ADMIN.either); + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, CUSTOM_ADMIN_ROLE); // CUSTOM_ADMIN can grant - accessControl.privateState.injectSecretKey(CUSTOM_ADMIN.secretKey); - expect(() => + await accessControl.privateState.injectSecretKey(CUSTOM_ADMIN.secretKey); + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP1.either), - ).not.toThrow(); + ).resolves.not.toThrow(); // Overwrite admin role - accessControl._setRoleAdmin(OPERATOR_ROLE_1, NEW_ADMIN_ROLE); + await accessControl._setRoleAdmin(OPERATOR_ROLE_1, NEW_ADMIN_ROLE); // CUSTOM_ADMIN should lose authority - expect(() => + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP2.either), - ).toThrow('AccessControl: unauthorized account'); + ).rejects.toThrow('AccessControl: unauthorized account'); // NEW_ADMIN should gain authority - accessControl.privateState.injectSecretKey(NEW_ADMIN.secretKey); - expect(() => + await accessControl.privateState.injectSecretKey(NEW_ADMIN.secretKey); + await expect( accessControl.grantRole(OPERATOR_ROLE_1, OP2.either), - ).not.toThrow(); + ).resolves.not.toThrow(); }); }); describe('_grantRole', () => { - it('should grant role', () => { - expect(accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + it('should grant role', async () => { + expect(await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); - it('should return false if hasRole already', () => { - expect(accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + it('should return false if hasRole already', async () => { + expect(await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); - expect(accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + expect(await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); // Should be refactored with c2c - it('should fail to grant role to a ContractAddress', () => { - expect(() => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1_CONTRACT); - }).toThrow('AccessControl: unsafe role approval'); + it('should fail to grant role to a ContractAddress', async () => { + await expect( + accessControl._grantRole(OPERATOR_ROLE_1, OP1_CONTRACT), + ).rejects.toThrow('AccessControl: unsafe role approval'); }); - it('should grant multiple roles', () => { + it('should grant multiple roles', async () => { for (let i = 0; i < operatorRolesList.length; i++) { for (let j = 0; j < commitmentOperators.length; j++) { - accessControl._grantRole( + await accessControl._grantRole( operatorRolesList[i], commitmentOperators[j], ); expect( - accessControl.hasRole(operatorRolesList[i], commitmentOperators[j]), + await accessControl.hasRole( + operatorRolesList[i], + commitmentOperators[j], + ), ).toBe(true); } } }); - it('should allow regranting a revoked role', () => { - accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); - accessControl._revokeRole(OPERATOR_ROLE_1, OP1.either); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); + it('should allow regranting a revoked role', async () => { + await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either); + await accessControl._revokeRole(OPERATOR_ROLE_1, OP1.either); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, + ); - expect(accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); + expect(await accessControl._grantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, + ); }); }); describe('_unsafeGrantRole', () => { - it('should grant role', () => { - expect(accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + it('should grant role', async () => { + expect( + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either), + ).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( true, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); }); - it('should return false if hasRole already', () => { - expect(accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either)).toBe( + it('should return false if hasRole already', async () => { + expect( + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either), + ).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( true, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); - expect(accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either)).toBe( - false, + expect( + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either), + ).toBe(false); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + true, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(true); }); // Should be refactored with c2c - it('should grant role to a ContractAddress', () => { + it('should grant role to a ContractAddress', async () => { expect( - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT), + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT), ).toBe(true); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe( + true, + ); }); - it('should grant multiple roles', () => { + it('should grant multiple roles', async () => { for (let i = 0; i < operatorRolesList.length; i++) { for (let j = 0; j < allOperators.length; j++) { expect( - accessControl._unsafeGrantRole( + await accessControl._unsafeGrantRole( operatorRolesList[i], allOperators[j], ), ).toBe(true); expect( - accessControl.hasRole(operatorRolesList[i], allOperators[j]), + await accessControl.hasRole(operatorRolesList[i], allOperators[j]), ).toBe(true); } } }); - it('should match on subsequent hasRole with clean Either after dirty grant', () => { + it('should match on subsequent hasRole with clean Either after dirty grant', async () => { const dirtyEither = { is_left: true, left: OP2.accountId, right: { bytes: new Uint8Array(32).fill(0xff) }, }; - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, dirtyEither); + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, dirtyEither); // Clean Either should find the role - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP2.either)).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP2.either)).toBe( + true, + ); }); - it('should match on subsequent hasRole with dirty Either after clean grant', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP2.either); + it('should match on subsequent hasRole with dirty Either after clean grant', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP2.either); const dirtyEither = { is_left: true, left: OP2.accountId, right: { bytes: new Uint8Array(32).fill(0xff) }, }; - expect(accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, dirtyEither)).toBe( + true, + ); }); - it('should return false for duplicate grant with dirty Either', () => { + it('should return false for duplicate grant with dirty Either', async () => { // Init granted role - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either); + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either); const dirtyEither = { is_left: true, @@ -672,9 +765,9 @@ describe('AccessControl', () => { }; // Dirty Either should still detect the existing grant - expect(accessControl._unsafeGrantRole(OPERATOR_ROLE_1, dirtyEither)).toBe( - false, - ); + expect( + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, dirtyEither), + ).toBe(false); }); }); @@ -682,37 +775,45 @@ describe('AccessControl', () => { describe.each( operatorTypes, )('when the operator is a %s', (_, _operator) => { - it('should revoke role', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, _operator); - expect(accessControl._revokeRole(OPERATOR_ROLE_1, _operator)).toBe( - true, + it('should revoke role', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, _operator); + expect( + await accessControl._revokeRole(OPERATOR_ROLE_1, _operator), + ).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, _operator)).toBe( + false, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, _operator)).toBe(false); }); }); - it('should return false if account does not have role', () => { - expect(accessControl._revokeRole(OPERATOR_ROLE_1, OP1.either)).toBe( + it('should return false if account does not have role', async () => { + expect(await accessControl._revokeRole(OPERATOR_ROLE_1, OP1.either)).toBe( false, ); }); - it('should revoke multiple roles', () => { + it('should revoke multiple roles', async () => { for (let i = 0; i < operatorRolesList.length; i++) { for (let j = 0; j < allOperators.length; j++) { - accessControl._unsafeGrantRole(operatorRolesList[i], allOperators[j]); + await accessControl._unsafeGrantRole( + operatorRolesList[i], + allOperators[j], + ); expect( - accessControl._revokeRole(operatorRolesList[i], allOperators[j]), + await accessControl._revokeRole( + operatorRolesList[i], + allOperators[j], + ), ).toBe(true); expect( - accessControl.hasRole(operatorRolesList[i], allOperators[j]), + await accessControl.hasRole(operatorRolesList[i], allOperators[j]), ).toBe(false); } } }); - it('should revoke with dirty Either after clean grant', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either); + it('should revoke with dirty Either after clean grant', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1.either); const dirtyEither = { is_left: true, @@ -720,61 +821,65 @@ describe('AccessControl', () => { right: { bytes: new Uint8Array(32).fill(0xff) }, }; - expect(accessControl._revokeRole(OPERATOR_ROLE_1, dirtyEither)).toBe( - true, + expect( + await accessControl._revokeRole(OPERATOR_ROLE_1, dirtyEither), + ).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe( + false, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1.either)).toBe(false); }); - it('should return false when revoking ungranted role with dirty Either', () => { + it('should return false when revoking ungranted role with dirty Either', async () => { const dirtyEither = { is_left: true, left: OP2.accountId, right: { bytes: new Uint8Array(32).fill(0xff) }, }; - expect(accessControl._revokeRole(OPERATOR_ROLE_1, dirtyEither)).toBe( - false, - ); + expect( + await accessControl._revokeRole(OPERATOR_ROLE_1, dirtyEither), + ).toBe(false); }); - it('should revoke with dirty left side on contract address', () => { - accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); + it('should revoke with dirty left side on contract address', async () => { + await accessControl._unsafeGrantRole(OPERATOR_ROLE_1, OP1_CONTRACT); const dirtyContract = { is_left: false, left: new Uint8Array(32).fill(0xff), right: OP1_CONTRACT.right, }; - expect(accessControl._revokeRole(OPERATOR_ROLE_1, dirtyContract)).toBe( - true, + expect( + await accessControl._revokeRole(OPERATOR_ROLE_1, dirtyContract), + ).toBe(true); + expect(await accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe( + false, ); - expect(accessControl.hasRole(OPERATOR_ROLE_1, OP1_CONTRACT)).toBe(false); }); }); describe('privateState helpers', () => { describe('getCurrentSecretKey', () => { - it('should return the injected secret key', () => { - accessControl.privateState.injectSecretKey(ADMIN.secretKey); + it('should return the injected secret key', async () => { + await accessControl.privateState.injectSecretKey(ADMIN.secretKey); - expect(accessControl.privateState.getCurrentSecretKey()).toEqual( + expect(await accessControl.privateState.getCurrentSecretKey()).toEqual( ADMIN.secretKey, ); }); - it('should throw when the secret key is undefined', () => { - const sim = new AccessControlSimulator({ + it('should throw when the secret key is undefined', async () => { + const sim = await AccessControlSimulator.create({ privateState: { secretKey: undefined as unknown as Uint8Array }, }); - expect(() => sim.privateState.getCurrentSecretKey()).toThrow( + await expect(sim.privateState.getCurrentSecretKey()).rejects.toThrow( 'Missing secret key', ); }); }); - it('should expose an empty public ledger via getPublicState', () => { - expect(accessControl.getPublicState()).toStrictEqual({}); + it('should expose an empty public ledger via getPublicState', async () => { + expect(await accessControl.getPublicState()).toStrictEqual({}); }); }); }); diff --git a/contracts/src/access/test/Ownable.test.ts b/contracts/src/access/test/Ownable.test.ts index 48752163..59eb98b7 100644 --- a/contracts/src/access/test/Ownable.test.ts +++ b/contracts/src/access/test/Ownable.test.ts @@ -75,29 +75,29 @@ const zeroTypes = [ describe('Ownable', () => { describe('before initialized', () => { - it('should initialize', () => { - ownable = new OwnableSimulator(OWNER.either, isInit, { + it('should initialize', async () => { + ownable = await OwnableSimulator.create(OWNER.either, isInit, { privateState: { secretKey: OWNER.secretKey }, }); - expect(ownable.owner()).toEqual(OWNER.either); + expect(await ownable.owner()).toEqual(OWNER.either); }); - it('should fail to initialize when owner is a contract address', () => { - expect(() => { - new OwnableSimulator(OWNER_CONTRACT, isInit, { + it('should fail to initialize when owner is a contract address', async () => { + await expect( + OwnableSimulator.create(OWNER_CONTRACT, isInit, { privateState: { secretKey: OWNER.secretKey }, - }); - }).toThrow('Ownable: unsafe ownership transfer'); + }), + ).rejects.toThrow('Ownable: unsafe ownership transfer'); }); it.each( zeroTypes, - )('should fail to initialize when owner is zero (%s)', (_, _zero) => { - expect(() => { - ownable = new OwnableSimulator(_zero, isInit, { + )('should fail to initialize when owner is zero (%s)', async (_, _zero) => { + await expect( + OwnableSimulator.create(_zero, isInit, { privateState: { secretKey: OWNER.secretKey }, - }); - }).toThrow('Ownable: invalid initial owner'); + }), + ).rejects.toThrow('Ownable: invalid initial owner'); }); type FailingCircuits = [method: keyof OwnableSimulator, args: unknown[]]; @@ -112,27 +112,29 @@ describe('Ownable', () => { ]; it.each( circuitsToFail, - )('should fail when calling circuit "%s"', (circuitName, args) => { - ownable = new OwnableSimulator(OWNER.either, isBadInit, { + )('should fail when calling circuit "%s"', async (circuitName, args) => { + ownable = await OwnableSimulator.create(OWNER.either, isBadInit, { privateState: { secretKey: OWNER.secretKey }, }); - expect(() => { - (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('Ownable: contract not initialized'); + await expect( + (ownable[circuitName] as (...args: unknown[]) => Promise)( + ...args, + ), + ).rejects.toThrow('Ownable: contract not initialized'); }); - it('should canonicalize initial owner', () => { + it('should canonicalize initial owner', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - ownable = new OwnableSimulator(nonCanonical, isInit, { + ownable = await OwnableSimulator.create(nonCanonical, isInit, { privateState: { secretKey: OWNER.secretKey }, }); - const stored = ownable.owner(); + const stored = await ownable.owner(); expect(stored.is_left).toBe(true); expect(stored.left).toEqual(OWNER.accountId); expect(stored.right).toEqual({ bytes: zeroBytes }); @@ -140,48 +142,48 @@ describe('Ownable', () => { }); describe('when initialized', () => { - beforeEach(() => { - ownable = new OwnableSimulator(OWNER.either, isInit, { + beforeEach(async () => { + ownable = await OwnableSimulator.create(OWNER.either, isInit, { privateState: { secretKey: OWNER.secretKey }, }); }); describe('owner', () => { - it('should return owner', () => { - expect(ownable.owner()).toEqual(OWNER.either); + it('should return owner', async () => { + expect(await ownable.owner()).toEqual(OWNER.either); }); - it('should return zero when unowned', () => { - ownable._transferOwnership(ZERO_ACCOUNT); - expect(ownable.owner()).toEqual(ZERO_ACCOUNT); + it('should return zero when unowned', async () => { + await ownable._transferOwnership(ZERO_ACCOUNT); + expect(await ownable.owner()).toEqual(ZERO_ACCOUNT); }); }); describe('assertOnlyOwner', () => { - it('should allow owner to call', () => { - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + it('should allow owner to call', async () => { + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should fail when called by unauthorized', () => { - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + it('should fail when called by unauthorized', async () => { + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); }); - it('should reject all accountId callers when owner is a contract', () => { - ownable._unsafeTransferOwnership(OWNER_CONTRACT); + it('should reject all accountId callers when owner is a contract', async () => { + await ownable._unsafeTransferOwnership(OWNER_CONTRACT); // Original owner rejected - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: contract address owner authentication is not yet supported', ); // Sample other keys for (const label of ['SAMPLE_1', 'SAMPLE_2', 'SAMPLE_3']) { const sampleUser = makeUser(label); - ownable.privateState.injectSecretKey(sampleUser.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + await ownable.privateState.injectSecretKey(sampleUser.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: contract address owner authentication is not yet supported', ); } @@ -189,165 +191,165 @@ describe('Ownable', () => { }); describe('transferOwnership', () => { - it('should transfer ownership', () => { - ownable.transferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + it('should transfer ownership', async () => { + await ownable.transferOwnership(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); // Original owner can no longer call - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // Unauthorized still can't call - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should fail when unauthorized transfers ownership', () => { - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.transferOwnership(NEW_OWNER.either)).toThrow( - 'Ownable: caller is not the owner', - ); + it('should fail when unauthorized transfers ownership', async () => { + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect( + ownable.transferOwnership(NEW_OWNER.either), + ).rejects.toThrow('Ownable: caller is not the owner'); }); - it('should fail when transferring to a contract address', () => { - expect(() => ownable.transferOwnership(RECIPIENT_CONTRACT)).toThrow( - 'Ownable: unsafe ownership transfer', - ); + it('should fail when transferring to a contract address', async () => { + await expect( + ownable.transferOwnership(RECIPIENT_CONTRACT), + ).rejects.toThrow('Ownable: unsafe ownership transfer'); }); - it('should fail when transferring to zero (accountId)', () => { - expect(() => ownable.transferOwnership(ZERO_ACCOUNT)).toThrow( + it('should fail when transferring to zero (accountId)', async () => { + await expect(ownable.transferOwnership(ZERO_ACCOUNT)).rejects.toThrow( 'Ownable: invalid new owner', ); }); - it('should fail when transferring to zero (contract)', () => { - expect(() => ownable.transferOwnership(ZERO_CONTRACT)).toThrow( + it('should fail when transferring to zero (contract)', async () => { + await expect(ownable.transferOwnership(ZERO_CONTRACT)).rejects.toThrow( 'Ownable: unsafe ownership transfer', ); }); - it('should transfer multiple times', () => { - ownable.transferOwnership(NEW_OWNER.either); + it('should transfer multiple times', async () => { + await ownable.transferOwnership(NEW_OWNER.either); - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - ownable.transferOwnership(OWNER.either); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await ownable.transferOwnership(OWNER.either); - ownable.privateState.injectSecretKey(OWNER.secretKey); - ownable.transferOwnership(NEW_OWNER.either); + await ownable.privateState.injectSecretKey(OWNER.secretKey); + await ownable.transferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); }); }); describe('_unsafeTransferOwnership', () => { - it('should transfer ownership to accountId', () => { - ownable._unsafeTransferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + it('should transfer ownership to accountId', async () => { + await ownable._unsafeTransferOwnership(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); // Original owner rejected - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should transfer ownership to contract', () => { - ownable._unsafeTransferOwnership(OWNER_CONTRACT); - expect(ownable.owner()).toEqual(OWNER_CONTRACT); + it('should transfer ownership to contract', async () => { + await ownable._unsafeTransferOwnership(OWNER_CONTRACT); + expect(await ownable.owner()).toEqual(OWNER_CONTRACT); // No one can authenticate, c2c not supported - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: contract address owner authentication is not yet supported', ); }); - it('should fail when unauthorized transfers ownership', () => { - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => + it('should fail when unauthorized transfers ownership', async () => { + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect( ownable._unsafeTransferOwnership(NEW_OWNER.either), - ).toThrow('Ownable: caller is not the owner'); + ).rejects.toThrow('Ownable: caller is not the owner'); }); - it('should fail when transferring to zero (accountId)', () => { - expect(() => ownable._unsafeTransferOwnership(ZERO_ACCOUNT)).toThrow( - 'Ownable: invalid new owner', - ); + it('should fail when transferring to zero (accountId)', async () => { + await expect( + ownable._unsafeTransferOwnership(ZERO_ACCOUNT), + ).rejects.toThrow('Ownable: invalid new owner'); }); - it('should fail when transferring to zero (contract)', () => { - expect(() => ownable._unsafeTransferOwnership(ZERO_CONTRACT)).toThrow( - 'Ownable: invalid new owner', - ); + it('should fail when transferring to zero (contract)', async () => { + await expect( + ownable._unsafeTransferOwnership(ZERO_CONTRACT), + ).rejects.toThrow('Ownable: invalid new owner'); }); - it('should enforce permissions after transfer (accountId)', () => { - ownable._unsafeTransferOwnership(NEW_OWNER.either); + it('should enforce permissions after transfer (accountId)', async () => { + await ownable._unsafeTransferOwnership(NEW_OWNER.either); // Original owner can no longer call - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // Unauthorized still can't call - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should transfer multiple times', () => { - ownable._unsafeTransferOwnership(NEW_OWNER.either); + it('should transfer multiple times', async () => { + await ownable._unsafeTransferOwnership(NEW_OWNER.either); - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - ownable._unsafeTransferOwnership(OWNER.either); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await ownable._unsafeTransferOwnership(OWNER.either); - ownable.privateState.injectSecretKey(OWNER.secretKey); - ownable._unsafeTransferOwnership(OWNER_CONTRACT); + await ownable.privateState.injectSecretKey(OWNER.secretKey); + await ownable._unsafeTransferOwnership(OWNER_CONTRACT); - expect(ownable.owner()).toEqual(OWNER_CONTRACT); + expect(await ownable.owner()).toEqual(OWNER_CONTRACT); }); }); describe('renounceOwnership', () => { - it('should renounce ownership', () => { - expect(ownable.owner()).toEqual(OWNER.either); + it('should renounce ownership', async () => { + expect(await ownable.owner()).toEqual(OWNER.either); - ownable.renounceOwnership(); + await ownable.renounceOwnership(); - expect(ownable.owner()).toEqual(ZERO_ACCOUNT); + expect(await ownable.owner()).toEqual(ZERO_ACCOUNT); // Confirm revoked permissions - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); }); - it('should fail when renouncing from unauthorized', () => { - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.renounceOwnership()).toThrow( + it('should fail when renouncing from unauthorized', async () => { + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.renounceOwnership()).rejects.toThrow( 'Ownable: caller is not the owner', ); }); - it('should store canonical zero after renouncing', () => { - ownable.renounceOwnership(); + it('should store canonical zero after renouncing', async () => { + await ownable.renounceOwnership(); - const stored = ownable.owner(); + const stored = await ownable.owner(); expect(stored.is_left).toBe(true); expect(stored.left).toEqual(zeroBytes); expect(stored.right).toEqual({ bytes: zeroBytes }); @@ -355,118 +357,118 @@ describe('Ownable', () => { }); describe('_transferOwnership', () => { - it('should transfer ownership', () => { - ownable._transferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + it('should transfer ownership', async () => { + await ownable._transferOwnership(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); // Original owner can no longer call - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // Unauthorized still can't call - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should fail when transferring to contract address zero', () => { - expect(() => ownable._transferOwnership(ZERO_CONTRACT)).toThrow( + it('should fail when transferring to contract address zero', async () => { + await expect(ownable._transferOwnership(ZERO_CONTRACT)).rejects.toThrow( 'Ownable: unsafe ownership transfer', ); }); - it('should fail when transferring to non-zero contract address', () => { - expect(() => ownable._transferOwnership(OWNER_CONTRACT)).toThrow( - 'Ownable: unsafe ownership transfer', - ); + it('should fail when transferring to non-zero contract address', async () => { + await expect( + ownable._transferOwnership(OWNER_CONTRACT), + ).rejects.toThrow('Ownable: unsafe ownership transfer'); }); - it('should transfer multiple times', () => { - ownable._transferOwnership(NEW_OWNER.either); + it('should transfer multiple times', async () => { + await ownable._transferOwnership(NEW_OWNER.either); - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - ownable._transferOwnership(OWNER.either); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await ownable._transferOwnership(OWNER.either); - ownable.privateState.injectSecretKey(OWNER.secretKey); - ownable._transferOwnership(NEW_OWNER.either); + await ownable.privateState.injectSecretKey(OWNER.secretKey); + await ownable._transferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); }); - it('should allow transfers to zero', () => { - ownable._transferOwnership(ZERO_ACCOUNT); - expect(ownable.owner()).toEqual(ZERO_ACCOUNT); + it('should allow transfers to zero', async () => { + await ownable._transferOwnership(ZERO_ACCOUNT); + expect(await ownable.owner()).toEqual(ZERO_ACCOUNT); // No one can authenticate after zeroing - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); }); }); describe('_unsafeUncheckedTransferOwnership', () => { - it('should transfer ownership to accountId', () => { - ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); - expect(ownable.owner()).toEqual(NEW_OWNER.either); + it('should transfer ownership to accountId', async () => { + await ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); + expect(await ownable.owner()).toEqual(NEW_OWNER.either); // Original owner rejected - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should transfer ownership to contract', () => { - ownable._unsafeUncheckedTransferOwnership(OWNER_CONTRACT); - expect(ownable.owner()).toEqual(OWNER_CONTRACT); + it('should transfer ownership to contract', async () => { + await ownable._unsafeUncheckedTransferOwnership(OWNER_CONTRACT); + expect(await ownable.owner()).toEqual(OWNER_CONTRACT); // No one can authenticate, c2c not supported - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: contract address owner authentication is not yet supported', ); }); - it('should enforce permissions after transfer (accountId)', () => { - ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); + it('should enforce permissions after transfer (accountId)', async () => { + await ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); // Original owner can no longer call - expect(() => ownable.assertOnlyOwner()).toThrow( + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // Unauthorized still can't call - ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => ownable.assertOnlyOwner()).toThrow( + await ownable.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(ownable.assertOnlyOwner()).rejects.toThrow( 'Ownable: caller is not the owner', ); // New owner can call - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(() => ownable.assertOnlyOwner()).not.toThrow(); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await expect(ownable.assertOnlyOwner()).resolves.not.toThrow(); }); - it('should transfer multiple times', () => { - ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); + it('should transfer multiple times', async () => { + await ownable._unsafeUncheckedTransferOwnership(NEW_OWNER.either); - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - ownable._unsafeUncheckedTransferOwnership(OWNER.either); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await ownable._unsafeUncheckedTransferOwnership(OWNER.either); - ownable.privateState.injectSecretKey(OWNER.secretKey); - ownable._unsafeUncheckedTransferOwnership(OWNER_CONTRACT); + await ownable.privateState.injectSecretKey(OWNER.secretKey); + await ownable._unsafeUncheckedTransferOwnership(OWNER_CONTRACT); - expect(ownable.owner()).toEqual(OWNER_CONTRACT); + expect(await ownable.owner()).toEqual(OWNER_CONTRACT); }); - it('should canonicalize accountId (zero out inactive right side)', () => { + it('should canonicalize accountId (zero out inactive right side)', async () => { // Craft a non-canonical Either: is_left=true but right side has data const nonCanonical = { is_left: true, @@ -474,15 +476,15 @@ describe('Ownable', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - ownable._unsafeUncheckedTransferOwnership(nonCanonical); + await ownable._unsafeUncheckedTransferOwnership(nonCanonical); - const stored = ownable.owner(); + const stored = await ownable.owner(); expect(stored.is_left).toBe(true); expect(stored.left).toEqual(NEW_OWNER.accountId); expect(stored.right).toEqual({ bytes: zeroBytes }); }); - it('should canonicalize contract address (zero out inactive left side)', () => { + it('should canonicalize contract address (zero out inactive left side)', async () => { // Craft a non-canonical Either: is_left=false but left side has data const nonCanonical = { is_left: false, @@ -490,9 +492,9 @@ describe('Ownable', () => { right: utils.encodeToAddress('OWNER_CONTRACT'), }; - ownable._unsafeUncheckedTransferOwnership(nonCanonical); + await ownable._unsafeUncheckedTransferOwnership(nonCanonical); - const stored = ownable.owner(); + const stored = await ownable.owner(); expect(stored.is_left).toBe(false); expect(stored.left).toEqual(zeroBytes); expect(stored.right).toEqual(utils.encodeToAddress('OWNER_CONTRACT')); @@ -501,42 +503,42 @@ describe('Ownable', () => { }); describe('simulator wiring', () => { - it('should construct with a generated private state when none is supplied', () => { - const sim = new OwnableSimulator(OWNER.either, isInit); - const sk = sim.privateState.getCurrentSecretKey(); + it('should construct with a generated private state when none is supplied', async () => { + const sim = await OwnableSimulator.create(OWNER.either, isInit); + const sk = await sim.privateState.getCurrentSecretKey(); expect(sk).toBeInstanceOf(Uint8Array); expect(sk.length).toBe(32); }); - it('should expose an empty public ledger via getPublicState', () => { - const sim = new OwnableSimulator(OWNER.either, isInit, { + it('should expose an empty public ledger via getPublicState', async () => { + const sim = await OwnableSimulator.create(OWNER.either, isInit, { privateState: { secretKey: OWNER.secretKey }, }); - expect(sim.getPublicState()).toStrictEqual({}); + expect(await sim.getPublicState()).toStrictEqual({}); }); }); describe('privateState helpers', () => { describe('getCurrentSecretKey', () => { - it('should return the injected secret key', () => { - ownable = new OwnableSimulator(OWNER.either, isInit, { + it('should return the injected secret key', async () => { + ownable = await OwnableSimulator.create(OWNER.either, isInit, { privateState: { secretKey: OWNER.secretKey }, }); - ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); + await ownable.privateState.injectSecretKey(NEW_OWNER.secretKey); - expect(ownable.privateState.getCurrentSecretKey()).toEqual( + expect(await ownable.privateState.getCurrentSecretKey()).toEqual( NEW_OWNER.secretKey, ); }); - it('should throw when the secret key is undefined', () => { - const sim = new OwnableSimulator(OWNER.either, isInit, { + it('should throw when the secret key is undefined', async () => { + const sim = await OwnableSimulator.create(OWNER.either, isInit, { privateState: { secretKey: undefined as unknown as Uint8Array }, }); - expect(() => sim.privateState.getCurrentSecretKey()).toThrow( + await expect(sim.privateState.getCurrentSecretKey()).rejects.toThrow( 'Missing secret key', ); }); diff --git a/contracts/src/access/test/ShieldedAccessControl.test.ts b/contracts/src/access/test/ShieldedAccessControl.test.ts index d0530e9f..e9488ea3 100644 --- a/contracts/src/access/test/ShieldedAccessControl.test.ts +++ b/contracts/src/access/test/ShieldedAccessControl.test.ts @@ -8,8 +8,8 @@ import { } from '@midnight-ntwrk/compact-runtime'; import { beforeEach, describe, expect, it } from 'vitest'; import type { Ledger } from '../../../artifacts/MockShieldedAccessControl/contract/index.js'; -import { ShieldedAccessControlPrivateState } from './witnesses/ShieldedAccessControlWitnesses.js'; import { ShieldedAccessControlSimulator } from './simulators/ShieldedAccessControlSimulator.js'; +import { ShieldedAccessControlPrivateState } from './witnesses/ShieldedAccessControlWitnesses.js'; const INSTANCE_SALT = new Uint8Array(32).fill(48473095); const COMMITMENT_DOMAIN = 'ShieldedAccessControl:commitment'; @@ -104,8 +104,11 @@ let contract: ShieldedAccessControlSimulator; describe('ShieldedAccessControl', () => { describe('when not initialized', () => { - beforeEach(() => { - contract = new ShieldedAccessControlSimulator(INSTANCE_SALT, false); + beforeEach(async () => { + contract = await ShieldedAccessControlSimulator.create( + INSTANCE_SALT, + false, + ); }); const circuitsRequiringInit: [string, unknown[]][] = [ @@ -119,26 +122,28 @@ describe('ShieldedAccessControl', () => { ['_setRoleAdmin', [ROLE_ADMIN, ROLE_ADMIN]], ]; - it.each(circuitsRequiringInit)('%s should fail', (circuitName, args) => { - expect(() => { + it.each( + circuitsRequiringInit, + )('%s should fail', async (circuitName, args) => { + await expect( ( contract[circuitName as keyof ShieldedAccessControlSimulator] as ( ...a: unknown[] - ) => unknown - )(...args); - }).toThrow('ShieldedAccessControl: contract not initialized'); + ) => Promise + )(...args), + ).rejects.toThrow('ShieldedAccessControl: contract not initialized'); }); - it('_grantRole should independently check initialization', () => { - expect(() => contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: contract not initialized', - ); + it('_grantRole should independently check initialization', async () => { + await expect( + contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: contract not initialized'); }); - it('_revokeRole should independently check initialization', () => { - expect(() => contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: contract not initialized', - ); + it('_revokeRole should independently check initialization', async () => { + await expect( + contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: contract not initialized'); }); const circuitsNotRequiringInit: [string, unknown[]][] = [ @@ -151,85 +156,92 @@ describe('ShieldedAccessControl', () => { it.each( circuitsNotRequiringInit, - )('%s should succeed', (circuitName, args) => { - expect(() => { + )('%s should succeed', async (circuitName, args) => { + await expect( ( contract[circuitName as keyof ShieldedAccessControlSimulator] as ( ...a: unknown[] - ) => unknown - )(...args); - }).not.toThrow(); + ) => Promise + )(...args), + ).resolves.not.toThrow(); }); - it('should fail with zero instanceSalt', () => { - expect(() => { - new ShieldedAccessControlSimulator(new Uint8Array(32), true); - }).toThrow('ShieldedAccessControl: Instance salt must not be 0'); + it('should fail with zero instanceSalt', async () => { + await expect( + ShieldedAccessControlSimulator.create(new Uint8Array(32), true), + ).rejects.toThrow('ShieldedAccessControl: Instance salt must not be 0'); }); }); describe('after initialization', () => { - beforeEach(() => { - contract = new ShieldedAccessControlSimulator(INSTANCE_SALT, true, { - privateState: ShieldedAccessControlPrivateState.withSecretKey(ADMIN_SK), - }); + beforeEach(async () => { + contract = await ShieldedAccessControlSimulator.create( + INSTANCE_SALT, + true, + { + privateState: + ShieldedAccessControlPrivateState.withSecretKey(ADMIN_SK), + }, + ); }); describe('DEFAULT_ADMIN_ROLE', () => { - it('should return zero bytes', () => { - expect(contract.DEFAULT_ADMIN_ROLE()).toStrictEqual(new Uint8Array(32)); + it('should return zero bytes', async () => { + expect(await contract.DEFAULT_ADMIN_ROLE()).toStrictEqual( + new Uint8Array(32), + ); }); }); describe('computeAccountId', () => { - it('should match pre-computed accountId', () => { - expect(contract.computeAccountId(ADMIN_SK, INSTANCE_SALT)).toEqual( - ADMIN_ACCOUNT_ID, - ); + it('should match pre-computed accountId', async () => { + expect( + await contract.computeAccountId(ADMIN_SK, INSTANCE_SALT), + ).toEqual(ADMIN_ACCOUNT_ID); }); - it('should produce different accountId with different key', () => { - expect(contract.computeAccountId(BAD_SK, INSTANCE_SALT)).not.toEqual( - ADMIN_ACCOUNT_ID, - ); + it('should produce different accountId with different key', async () => { + expect( + await contract.computeAccountId(BAD_SK, INSTANCE_SALT), + ).not.toEqual(ADMIN_ACCOUNT_ID); }); - it('should produce different accountId with different salt', () => { + it('should produce different accountId with different salt', async () => { const differentSalt = new Uint8Array(32).fill(1); - expect(contract.computeAccountId(ADMIN_SK, differentSalt)).not.toEqual( - ADMIN_ACCOUNT_ID, - ); + expect( + await contract.computeAccountId(ADMIN_SK, differentSalt), + ).not.toEqual(ADMIN_ACCOUNT_ID); }); - it('should accept zero-byte secret key', () => { + it('should accept zero-byte secret key', async () => { const zeroKey = new Uint8Array(32); - expect(contract.computeAccountId(zeroKey, INSTANCE_SALT)).toEqual( + expect(await contract.computeAccountId(zeroKey, INSTANCE_SALT)).toEqual( buildAccountIdHash(zeroKey), ); }); }); describe('computeRoleCommitment', () => { - it('should match pre-computed commitment', () => { + it('should match pre-computed commitment', async () => { expect( - contract.computeRoleCommitment(ROLE_ADMIN, ADMIN_ACCOUNT_ID), + await contract.computeRoleCommitment(ROLE_ADMIN, ADMIN_ACCOUNT_ID), ).toEqual(ADMIN_ROLE_COMMITMENT); }); - it('should differ with wrong role', () => { + it('should differ with wrong role', async () => { expect( - contract.computeRoleCommitment(ROLE_OP1, ADMIN_ACCOUNT_ID), + await contract.computeRoleCommitment(ROLE_OP1, ADMIN_ACCOUNT_ID), ).not.toEqual(ADMIN_ROLE_COMMITMENT); }); - it('should differ with wrong accountId', () => { + it('should differ with wrong accountId', async () => { expect( - contract.computeRoleCommitment(ROLE_ADMIN, BAD_ACCOUNT_ID), + await contract.computeRoleCommitment(ROLE_ADMIN, BAD_ACCOUNT_ID), ).not.toEqual(ADMIN_ROLE_COMMITMENT); }); - it('should differ with different instanceSalt', () => { - const newContract = new ShieldedAccessControlSimulator( + it('should differ with different instanceSalt', async () => { + const newContract = await ShieldedAccessControlSimulator.create( new Uint8Array(32).fill(1), true, { @@ -238,956 +250,954 @@ describe('ShieldedAccessControl', () => { }, ); expect( - newContract.computeRoleCommitment(ROLE_ADMIN, ADMIN_ACCOUNT_ID), + await newContract.computeRoleCommitment(ROLE_ADMIN, ADMIN_ACCOUNT_ID), ).not.toEqual(ADMIN_ROLE_COMMITMENT); }); }); describe('computeNullifier', () => { - it('should match pre-computed nullifier', () => { - expect(contract.computeNullifier(ADMIN_ROLE_COMMITMENT)).toEqual( + it('should match pre-computed nullifier', async () => { + expect(await contract.computeNullifier(ADMIN_ROLE_COMMITMENT)).toEqual( ADMIN_ROLE_NULLIFIER, ); }); - it('should differ with wrong commitment', () => { - expect(contract.computeNullifier(OP1_ROLE_COMMITMENT)).not.toEqual( - ADMIN_ROLE_NULLIFIER, - ); + it('should differ with wrong commitment', async () => { + expect( + await contract.computeNullifier(OP1_ROLE_COMMITMENT), + ).not.toEqual(ADMIN_ROLE_NULLIFIER); }); }); describe('assertOnlyRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); }); describe('should fail', () => { - it('when witness returns path for a different commitment', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract.overrideWitness('wit_getRoleCommitmentPath', () => { - const ps = contract.getPrivateState(); - const path = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( + it('when witness returns path for a different commitment', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + contract.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + const path = + ctx.ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( OP1_ROLE_COMMITMENT, ); - if (path) return [ps, path]; + if (path) return [ctx.privateState, path]; throw new Error('Path should be defined'); }); - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).toThrow( + await expect(contract.assertOnlyRole(ROLE_ADMIN)).rejects.toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); - it('when caller has wrong secret key', () => { - contract.privateState.injectSecretKey(UNAUTHORIZED_SK); - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).toThrow( + it('when caller has wrong secret key', async () => { + await contract.privateState.injectSecretKey(UNAUTHORIZED_SK); + await expect(contract.assertOnlyRole(ROLE_ADMIN)).rejects.toThrow( 'ShieldedAccessControl: unauthorized account', ); }); - it('when witness provides invalid path', () => { + it('when witness provides invalid path', async () => { contract.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).toThrow( + await expect(contract.assertOnlyRole(ROLE_ADMIN)).rejects.toThrow( 'ShieldedAccessControl: unauthorized account', ); }); - it('when role is revoked', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).toThrow( + it('when role is revoked', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await expect(contract.assertOnlyRole(ROLE_ADMIN)).rejects.toThrow( 'ShieldedAccessControl: unauthorized account', ); }); - it('when role was never granted to anyone', () => { - expect(() => contract.assertOnlyRole(ROLE_NONEXISTENT)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + it('when role was never granted to anyone', async () => { + await expect( + contract.assertOnlyRole(ROLE_NONEXISTENT), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); }); describe('should succeed', () => { - it('when caller has correct key and valid path', () => { - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).not.toThrow(); + it('when caller has correct key and valid path', async () => { + await expect( + contract.assertOnlyRole(ROLE_ADMIN), + ).resolves.not.toThrow(); }); - it('when caller holds multiple roles with same key', () => { - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); - - expect(() => { - contract.assertOnlyRole(ROLE_ADMIN); - contract.assertOnlyRole(ROLE_OP1); - contract.assertOnlyRole(ROLE_OP2); - contract.assertOnlyRole(ROLE_OP3); - }).not.toThrow(); + it('when caller holds multiple roles with same key', async () => { + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); + + await contract.assertOnlyRole(ROLE_ADMIN); + await contract.assertOnlyRole(ROLE_OP1); + await contract.assertOnlyRole(ROLE_OP2); + await contract.assertOnlyRole(ROLE_OP3); }); - it('when role is revoked and re-issued with new accountId', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when role is revoked and re-issued with new accountId', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); const newKey = Buffer.alloc(32, 'NEW_ADMIN_KEY'); - contract.privateState.injectSecretKey(newKey); + await contract.privateState.injectSecretKey(newKey); const newAccountId = buildAccountIdHash(newKey); - contract._grantRole(ROLE_ADMIN, newAccountId); + await contract._grantRole(ROLE_ADMIN, newAccountId); - expect(() => contract.assertOnlyRole(ROLE_ADMIN)).not.toThrow(); + await expect( + contract.assertOnlyRole(ROLE_ADMIN), + ).resolves.not.toThrow(); }); }); }); describe('canProveRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); }); - it('should fail when witness returns path for a different commitment', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract.overrideWitness('wit_getRoleCommitmentPath', () => { - const ps = contract.getPrivateState(); - const path = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( + it('should fail when witness returns path for a different commitment', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + contract.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + const path = + ctx.ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( OP1_ROLE_COMMITMENT, ); - if (path) return [ps, path]; + if (path) return [ctx.privateState, path]; throw new Error('Path should be defined'); }); - expect(() => contract.canProveRole(ROLE_ADMIN)).toThrow( + await expect(contract.canProveRole(ROLE_ADMIN)).rejects.toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); describe('should return true', () => { - it('when caller has role', () => { - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); + it('when caller has role', async () => { + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); }); - it('when caller holds multiple roles with same key', () => { - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); + it('when caller holds multiple roles with same key', async () => { + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); - expect(contract.canProveRole(ROLE_OP2)).toBe(true); - expect(contract.canProveRole(ROLE_OP3)).toBe(true); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); + expect(await contract.canProveRole(ROLE_OP2)).toBe(true); + expect(await contract.canProveRole(ROLE_OP3)).toBe(true); }); - it('when role is revoked and re-issued with new accountId', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when role is revoked and re-issued with new accountId', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); const newKey = Buffer.alloc(32, 'NEW_ADMIN_KEY'); - contract.privateState.injectSecretKey(newKey); + await contract.privateState.injectSecretKey(newKey); const newAccountId = buildAccountIdHash(newKey); - contract._grantRole(ROLE_ADMIN, newAccountId); + await contract._grantRole(ROLE_ADMIN, newAccountId); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); }); - it('when multiple users hold the same role', () => { - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + it('when multiple users hold the same role', async () => { + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); // User 2 - contract._grantRole(ROLE_OP1, OP2_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP2_ACCOUNT_ID); // User 3 - contract._grantRole(ROLE_OP1, OP3_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP3_ACCOUNT_ID); // Prove as admin (who holds OP1) - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); // Prove as user 2 - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); // Prove as user 3 - contract.privateState.injectSecretKey(OPERATOR_3_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + await contract.privateState.injectSecretKey(OPERATOR_3_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); }); }); describe('should return false', () => { - it('when caller does not have role', () => { - expect(contract.canProveRole(ROLE_OP1)).toBe(false); + it('when caller does not have role', async () => { + expect(await contract.canProveRole(ROLE_OP1)).toBe(false); }); - it('when caller has wrong secret key', () => { - contract.privateState.injectSecretKey(BAD_SK); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); + it('when caller has wrong secret key', async () => { + await contract.privateState.injectSecretKey(BAD_SK); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); }); - it('when role is revoked', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); + it('when role is revoked', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); }); - it('when witness provides invalid path', () => { + it('when witness provides invalid path', async () => { contract.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); }); - it('when invalid witness path is provided for a revoked role', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when invalid witness path is provided for a revoked role', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); contract.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); }); }); }); describe('grantRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); }); describe('should fail', () => { - it('when caller does not have admin role', () => { - contract.privateState.injectSecretKey(UNAUTHORIZED_SK); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + it('when caller does not have admin role', async () => { + await contract.privateState.injectSecretKey(UNAUTHORIZED_SK); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when granting to an already-revoked accountId', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('when granting to an already-revoked accountId', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('when admin provides wrong secret key', () => { - contract.privateState.injectSecretKey(BAD_SK); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + it('when admin provides wrong secret key', async () => { + await contract.privateState.injectSecretKey(BAD_SK); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin provides invalid witness path', () => { + it('when admin provides invalid witness path', async () => { contract.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin role has been reassigned via _setRoleAdmin', () => { - contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); + it('when admin role has been reassigned via _setRoleAdmin', async () => { + await contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); // ADMIN holds DEFAULT_ADMIN_ROLE but not ROLE_OP1 - expect(() => contract.grantRole(ROLE_OP2, OP2_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP2, OP2_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when witness returns path for a different commitment', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract.overrideWitness('wit_getRoleCommitmentPath', () => { - const ps = contract.getPrivateState(); - const path = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( + it('when witness returns path for a different commitment', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + contract.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + const path = + ctx.ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( OP1_ROLE_COMMITMENT, ); - if (path) return [ps, path]; + if (path) return [ctx.privateState, path]; throw new Error('Path should be defined'); }); - expect(() => + await expect( contract.grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow( + ).rejects.toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); - it('when admin with duplicate grants is revoked', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); // duplicate - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when admin with duplicate grants is revoked', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); // duplicate + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); }); describe('should succeed', () => { - it('when caller has admin role', () => { - expect(() => + it('when caller has admin role', async () => { + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); }); - it('when granting the same role multiple times to the same accountId', () => { - expect(() => + it('when granting the same role multiple times to the same accountId', async () => { + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - expect(() => + ).resolves.not.toThrow(); + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - expect(() => + ).resolves.not.toThrow(); + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); }); - it('when caller has custom admin role', () => { - contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); - contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('when caller has custom admin role', async () => { + await contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); + await contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID); // Switch to operator 1 - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(() => + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + await expect( contract.grantRole(ROLE_OP2, OP2_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(contract.canProveRole(ROLE_OP2)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + expect(await contract.canProveRole(ROLE_OP2)).toBe(true); }); - it('when admin role is revoked and re-issued with new accountId', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when admin role is revoked and re-issued with new accountId', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); const newKey = Buffer.alloc(32, 'NEW_ADMIN_KEY'); - contract.privateState.injectSecretKey(newKey); + await contract.privateState.injectSecretKey(newKey); const newAccountId = buildAccountIdHash(newKey); - contract._grantRole(ROLE_ADMIN, newAccountId); + await contract._grantRole(ROLE_ADMIN, newAccountId); - expect(() => + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); }); - it('when multiple admins exist', () => { - contract._grantRole(ROLE_ADMIN, OP1_ACCOUNT_ID); - contract._grantRole(ROLE_ADMIN, OP2_ACCOUNT_ID); + it('when multiple admins exist', async () => { + await contract._grantRole(ROLE_ADMIN, OP1_ACCOUNT_ID); + await contract._grantRole(ROLE_ADMIN, OP2_ACCOUNT_ID); // Admin 1 can grant - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(() => + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); // Admin 2 can grant - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(() => + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + await expect( contract.grantRole(ROLE_OP2, OP2_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); }); - it('when admin holds multiple roles', () => { - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + it('when admin holds multiple roles', async () => { + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - expect(() => + await expect( contract.grantRole(ROLE_OP3, OP3_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_3_SK); - expect(contract.canProveRole(ROLE_OP3)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_3_SK); + expect(await contract.canProveRole(ROLE_OP3)).toBe(true); }); - it('when re-granting an active role (duplicate)', () => { - expect(() => + it('when re-granting an active role (duplicate)', async () => { + await expect( contract.grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).not.toThrow(); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); + ).resolves.not.toThrow(); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); }); }); }); describe('_grantRole', () => { - it('should insert commitment into Merkle tree', () => { - let root = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + it('should insert commitment into Merkle tree', async () => { + let root = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(root.field).toBe(0n); - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - root = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + root = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(root.field).not.toBe(0n); - const path = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( - ADMIN_ROLE_COMMITMENT, - ); + const path = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.findPathForLeaf( + ADMIN_ROLE_COMMITMENT, + ); expect(path).toBeDefined(); expect(path?.leaf).toStrictEqual(ADMIN_ROLE_COMMITMENT); }); - it('should insert multiple commitments into Merkle tree', () => { - const root = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + it('should insert multiple commitments into Merkle tree', async () => { + const root = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(root.field).toBe(0n); - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - const root1 = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + const root1 = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(root1.field).not.toBe(root.field); - contract._grantRole(ROLE_ADMIN, OP1_ACCOUNT_ID); - const root2 = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + await contract._grantRole(ROLE_ADMIN, OP1_ACCOUNT_ID); + const root2 = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(root2.field).not.toBe(root.field); expect(root2.field).not.toBe(root1.field); }); - it('should insert multiple leaves for the same (role, accountId)', () => { - const rootBefore = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + it('should insert multiple leaves for the same (role, accountId)', async () => { + const rootBefore = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - const rootAfterFirst = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + const rootAfterFirst = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - const rootAfterSecond = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + const rootAfterSecond = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); // Each grant should change the root (new leaf inserted) expect(rootAfterFirst).not.toEqual(rootBefore); expect(rootAfterSecond).not.toEqual(rootAfterFirst); }); - it('should invalidate all duplicates with a single revocation', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('should invalidate all duplicates with a single revocation', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); - contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_OP1)).toBe(false); + expect(await contract.canProveRole(ROLE_OP1)).toBe(false); }); - it('should throw when granting to a revoked accountId', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('should throw when granting to a revoked accountId', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); + await expect( + contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('should not update tree when granting to a revoked accountId', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('should not update tree when granting to a revoked accountId', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - const rootBefore = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); - expect(() => contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); - const rootAfter = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.root(); + const rootBefore = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); + await expect( + contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); + const rootAfter = ( + await contract.getPublicState() + ).ShieldedAccessControl__operatorRoles.root(); expect(rootBefore).toEqual(rootAfter); }); - it('should allow granting same role to new accountId after revoking different accountId', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('should allow granting same role to new accountId after revoking different accountId', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); // Different accountId for the same role - expect(() => + await expect( contract._grantRole(ROLE_OP1, OP2_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); }); }); describe('revokeRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); }); describe('should fail', () => { - it('when caller does not have admin role', () => { - contract.privateState.injectSecretKey(UNAUTHORIZED_SK); - expect(() => contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + it('when caller does not have admin role', async () => { + await contract.privateState.injectSecretKey(UNAUTHORIZED_SK); + await expect( + contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when re-revoking an already revoked role', () => { - contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); - expect(() => contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); + it('when re-revoking an already revoked role', async () => { + await contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID); + await expect( + contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('when admin provides wrong secret key', () => { - contract.privateState.injectSecretKey(BAD_SK); - expect(() => contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + it('when admin provides wrong secret key', async () => { + await contract.privateState.injectSecretKey(BAD_SK); + await expect( + contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when admin provides invalid witness path', () => { + it('when admin provides invalid witness path', async () => { contract.overrideWitness( 'wit_getRoleCommitmentPath', RETURN_BAD_PATH, ); - expect(() => contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when witness returns path for a different commitment', () => { - contract.overrideWitness('wit_getRoleCommitmentPath', () => { - const ps = contract.getPrivateState(); - const path = contract - .getPublicState() - .ShieldedAccessControl__operatorRoles.findPathForLeaf( + it('when witness returns path for a different commitment', async () => { + contract.overrideWitness('wit_getRoleCommitmentPath', (ctx) => { + const path = + ctx.ledger.ShieldedAccessControl__operatorRoles.findPathForLeaf( OP1_ROLE_COMMITMENT, ); - if (path) return [ps, path]; + if (path) return [ctx.privateState, path]; throw new Error('Path should be defined'); }); - expect(() => + await expect( contract.revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow( + ).rejects.toThrow( 'ShieldedAccessControl: Path must contain leaf matching computed role commitment for the provided role, accountId pairing', ); }); }); describe('should succeed', () => { - it('when caller has admin role', () => { - expect(() => + it('when caller has admin role', async () => { + await expect( contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(false); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(false); }); - it('when caller has custom admin role', () => { - contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); - contract._grantRole(ROLE_OP2, OP2_ACCOUNT_ID); + it('when caller has custom admin role', async () => { + await contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); + await contract._grantRole(ROLE_OP2, OP2_ACCOUNT_ID); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - expect(() => + await expect( contract.revokeRole(ROLE_OP2, OP2_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(contract.canProveRole(ROLE_OP2)).toBe(false); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + expect(await contract.canProveRole(ROLE_OP2)).toBe(false); }); - it('when admin self-revokes then cannot further grant or revoke', () => { - contract.revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when admin self-revokes then cannot further grant or revoke', async () => { + await contract.revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); - expect(() => contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); + await expect( + contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); }); - it('when revoking a role that was never granted', () => { - expect(() => + it('when revoking a role that was never granted', async () => { + await expect( contract.revokeRole(ROLE_NONEXISTENT, ADMIN_ACCOUNT_ID), - ).not.toThrow(); - expect(contract.canProveRole(ROLE_NONEXISTENT)).toBe(false); + ).resolves.not.toThrow(); + expect(await contract.canProveRole(ROLE_NONEXISTENT)).toBe(false); }); - it('when revoking a role from an unauthorized accountId that was never granted', () => { - expect(() => + it('when revoking a role from an unauthorized accountId that was never granted', async () => { + await expect( contract.revokeRole(ROLE_OP1, UNAUTHORIZED_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); - expect(() => + await expect( contract._grantRole(ROLE_OP1, UNAUTHORIZED_ACCOUNT_ID), - ).toThrow('ShieldedAccessControl: role is already revoked'); + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('when revoking a never-granted role should permanently block future grants', () => { - contract.revokeRole(ROLE_NONEXISTENT, OP2_ACCOUNT_ID); + it('when revoking a never-granted role should permanently block future grants', async () => { + await contract.revokeRole(ROLE_NONEXISTENT, OP2_ACCOUNT_ID); - expect(() => + await expect( contract._grantRole(ROLE_NONEXISTENT, OP2_ACCOUNT_ID), - ).toThrow('ShieldedAccessControl: role is already revoked'); + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('when admin role is revoked and re-issued then can revoke again', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('when admin role is revoked and re-issued then can revoke again', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); const newKey = Buffer.alloc(32, 'NEW_ADMIN_KEY'); - contract.privateState.injectSecretKey(newKey); + await contract.privateState.injectSecretKey(newKey); const newAccountId = buildAccountIdHash(newKey); - contract._grantRole(ROLE_ADMIN, newAccountId); + await contract._grantRole(ROLE_ADMIN, newAccountId); - expect(() => + await expect( contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(false); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(false); }); }); }); describe('_revokeRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); }); - it('should insert nullifier into set', () => { + it('should insert nullifier into set', async () => { expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(), ).toBe(0n); - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(), ).toBe(1n); expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN_ROLE_NULLIFIER, - ), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN_ROLE_NULLIFIER, + ), ).toBe(true); }); - it('should throw when re-revoking', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => + it('should throw when re-revoking', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await expect( contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow('ShieldedAccessControl: role is already revoked'); + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('should not update nullifier set when re-revoking', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - const sizeBefore = contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); + it('should not update nullifier set when re-revoking', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + const sizeBefore = ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(); - expect(() => + await expect( contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow(); - const sizeAfter = contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(); + ).rejects.toThrow(); + const sizeAfter = ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(); expect(sizeBefore).toEqual(sizeAfter); }); - it('should allow revoking a role that was never granted', () => { - expect(() => + it('should allow revoking a role that was never granted', async () => { + await expect( contract._revokeRole(ROLE_NONEXISTENT, ADMIN_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); }); }); describe('renounceRole', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); }); - it('should allow caller to renounce their own role', () => { - expect(() => + it('should allow caller to renounce their own role', async () => { + await expect( contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).not.toThrow(); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); + ).resolves.not.toThrow(); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); }); - it('should update nullifier set', () => { + it('should update nullifier set', async () => { expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(), ).toBe(0n); - contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.size(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.size(), ).toBe(1n); expect( - contract - .getPublicState() - .ShieldedAccessControl__roleCommitmentNullifiers.member( - ADMIN_ROLE_NULLIFIER, - ), + ( + await contract.getPublicState() + ).ShieldedAccessControl__roleCommitmentNullifiers.member( + ADMIN_ROLE_NULLIFIER, + ), ).toBe(true); }); - it('should fail when caller provides wrong accountId', () => { - expect(() => contract.renounceRole(ROLE_ADMIN, BAD_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: bad confirmation', - ); + it('should fail when caller provides wrong accountId', async () => { + await expect( + contract.renounceRole(ROLE_ADMIN, BAD_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: bad confirmation'); }); - it('should fail when caller has wrong secret key', () => { - contract.privateState.injectSecretKey(UNAUTHORIZED_SK); - expect(() => + it('should fail when caller has wrong secret key', async () => { + await contract.privateState.injectSecretKey(UNAUTHORIZED_SK); + await expect( contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow('ShieldedAccessControl: bad confirmation'); + ).rejects.toThrow('ShieldedAccessControl: bad confirmation'); }); - it('should throw when role is already revoked', () => { - contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => + it('should throw when role is already revoked', async () => { + await contract._revokeRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await expect( contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), - ).toThrow('ShieldedAccessControl: role is already revoked'); + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('should permanently block re-grant to same accountId', () => { - contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(() => contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); + it('should permanently block re-grant to same accountId', async () => { + await contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await expect( + contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); - it('should allow re-grant with new accountId after renounce', () => { - contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('should allow re-grant with new accountId after renounce', async () => { + await contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); const newKey = Buffer.alloc(32, 'NEW_ADMIN_KEY'); - contract.privateState.injectSecretKey(newKey); + await contract.privateState.injectSecretKey(newKey); const newAccountId = buildAccountIdHash(newKey); - contract._grantRole(ROLE_ADMIN, newAccountId); + await contract._grantRole(ROLE_ADMIN, newAccountId); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); }); - it('should not affect other roles held by same accountId', () => { - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + it('should not affect other roles held by same accountId', async () => { + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract.renounceRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(false); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); - expect(contract.canProveRole(ROLE_OP2)).toBe(true); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(false); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); + expect(await contract.canProveRole(ROLE_OP2)).toBe(true); }); // Pre-burn scenario: a user can burn a nullifier for a (role, accountId) pairing // that was never granted. This permanently blocks future grants to that accountId // for the specified role, but does not affect other accountIds holding the same role - it('should allow renouncing a role never granted to this accountId', () => { + it('should allow renouncing a role never granted to this accountId', async () => { // OP1 has ROLE_OP1, but ADMIN does not - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); // ADMIN renounces ROLE_OP1 despite never holding it - expect(() => + await expect( contract.renounceRole(ROLE_OP1, ADMIN_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); // OP1's grant is unaffected — different accountId, different nullifier - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); // ADMIN's accountId is now burned for ROLE_OP1 - expect(() => contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: role is already revoked', - ); + await expect( + contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: role is already revoked'); }); }); describe('getRoleAdmin', () => { - it('should return DEFAULT_ADMIN_ROLE when no admin set', () => { - expect(contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( + it('should return DEFAULT_ADMIN_ROLE when no admin set', async () => { + expect(await contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( new Uint8Array(32), ); - expect(contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( - contract.DEFAULT_ADMIN_ROLE(), + expect(await contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( + await contract.DEFAULT_ADMIN_ROLE(), ); }); - it('should restore DEFAULT_ADMIN_ROLE grant/revoke authority after reset to zero bytes', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + it('should restore DEFAULT_ADMIN_ROLE grant/revoke authority after reset to zero bytes', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); // Reassign OP1's admin to OP2 - contract._setRoleAdmin(ROLE_OP1, ROLE_OP2); + await contract._setRoleAdmin(ROLE_OP1, ROLE_OP2); // DEFAULT_ADMIN_ROLE holder cannot grant ROLE_OP1 anymore - expect(() => contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); // Reset OP1's admin back to DEFAULT_ADMIN_ROLE - contract._setRoleAdmin(ROLE_OP1, new Uint8Array(32)); + await contract._setRoleAdmin(ROLE_OP1, new Uint8Array(32)); // DEFAULT_ADMIN_ROLE holder can grant ROLE_OP1 again - expect(() => + await expect( contract.grantRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); // And can revoke - contract.privateState.injectSecretKey(ADMIN_SK); - expect(() => + await contract.privateState.injectSecretKey(ADMIN_SK); + await expect( contract.revokeRole(ROLE_OP1, OP1_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(contract.canProveRole(ROLE_OP1)).toBe(false); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + expect(await contract.canProveRole(ROLE_OP1)).toBe(false); }); - it('should return admin role after _setRoleAdmin', () => { - contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); - expect(contract.getRoleAdmin(ROLE_OP1)).toEqual( + it('should return admin role after _setRoleAdmin', async () => { + await contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); + expect(await contract.getRoleAdmin(ROLE_OP1)).toEqual( new Uint8Array(ROLE_ADMIN), ); }); }); describe('_setRoleAdmin', () => { - it('should set admin role', () => { - contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); - expect(contract.getRoleAdmin(ROLE_OP1)).toEqual( + it('should set admin role', async () => { + await contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); + expect(await contract.getRoleAdmin(ROLE_OP1)).toEqual( new Uint8Array(ROLE_ADMIN), ); }); - it('should update _adminRoles map', () => { + it('should update _adminRoles map', async () => { expect( - contract.getPublicState().ShieldedAccessControl__adminRoles.isEmpty(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__adminRoles.isEmpty(), ).toBe(true); - contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); - contract._setRoleAdmin(ROLE_OP2, ROLE_ADMIN); - contract._setRoleAdmin(ROLE_OP3, ROLE_ADMIN); + await contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); + await contract._setRoleAdmin(ROLE_OP2, ROLE_ADMIN); + await contract._setRoleAdmin(ROLE_OP3, ROLE_ADMIN); expect( - contract.getPublicState().ShieldedAccessControl__adminRoles.size(), + ( + await contract.getPublicState() + ).ShieldedAccessControl__adminRoles.size(), ).toBe(3n); }); - it('should override existing admin role', () => { - contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); - contract._setRoleAdmin(ROLE_OP1, ROLE_OP2); - expect(contract.getRoleAdmin(ROLE_OP1)).toEqual( + it('should override existing admin role', async () => { + await contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); + await contract._setRoleAdmin(ROLE_OP1, ROLE_OP2); + expect(await contract.getRoleAdmin(ROLE_OP1)).toEqual( new Uint8Array(ROLE_OP2), ); }); - it('should return DEFAULT_ADMIN_ROLE when reset to zero bytes', () => { - contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); - contract._setRoleAdmin(ROLE_OP1, new Uint8Array(32)); - expect(contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( - contract.DEFAULT_ADMIN_ROLE(), + it('should return DEFAULT_ADMIN_ROLE when reset to zero bytes', async () => { + await contract._setRoleAdmin(ROLE_OP1, ROLE_ADMIN); + await contract._setRoleAdmin(ROLE_OP1, new Uint8Array(32)); + expect(await contract.getRoleAdmin(ROLE_OP1)).toStrictEqual( + await contract.DEFAULT_ADMIN_ROLE(), ); }); - it('should allow a role to be its own admin', () => { - contract._setRoleAdmin(ROLE_OP1, ROLE_OP1); - expect(contract.getRoleAdmin(ROLE_OP1)).toEqual( + it('should allow a role to be its own admin', async () => { + await contract._setRoleAdmin(ROLE_OP1, ROLE_OP1); + expect(await contract.getRoleAdmin(ROLE_OP1)).toEqual( new Uint8Array(ROLE_OP1), ); }); - it('when new admin revokes after _setRoleAdmin reassignment', () => { - contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, OP2_ACCOUNT_ID); + it('when new admin revokes after _setRoleAdmin reassignment', async () => { + await contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, OP2_ACCOUNT_ID); // Switch to operator 1 who is now admin of ROLE_OP2 - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(() => + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + await expect( contract.revokeRole(ROLE_OP2, OP2_ACCOUNT_ID), - ).not.toThrow(); - contract.privateState.injectSecretKey(OPERATOR_2_SK); - expect(contract.canProveRole(ROLE_OP2)).toBe(false); + ).resolves.not.toThrow(); + await contract.privateState.injectSecretKey(OPERATOR_2_SK); + expect(await contract.canProveRole(ROLE_OP2)).toBe(false); }); - it('admin authority should not be transitive across role hierarchies', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('admin authority should not be transitive across role hierarchies', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._setRoleAdmin(ROLE_OP2, ROLE_OP1); + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); // ADMIN can grant ROLE_OP1 (admin is DEFAULT_ADMIN_ROLE) - expect(() => + await expect( contract.grantRole(ROLE_OP1, OP2_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); // But ADMIN cannot directly grant ROLE_OP2 (admin is ROLE_OP1, not DEFAULT_ADMIN_ROLE) - expect(() => contract.grantRole(ROLE_OP2, OP3_ACCOUNT_ID)).toThrow( - 'ShieldedAccessControl: unauthorized account', - ); + await expect( + contract.grantRole(ROLE_OP2, OP3_ACCOUNT_ID), + ).rejects.toThrow('ShieldedAccessControl: unauthorized account'); // OP1 holder can grant ROLE_OP2 - contract.privateState.injectSecretKey(OPERATOR_1_SK); - expect(() => + await contract.privateState.injectSecretKey(OPERATOR_1_SK); + await expect( contract.grantRole(ROLE_OP2, OP3_ACCOUNT_ID), - ).not.toThrow(); + ).resolves.not.toThrow(); }); }); describe('single key across multiple roles', () => { - beforeEach(() => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); + beforeEach(async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP1, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + await contract._grantRole(ROLE_OP3, ADMIN_ACCOUNT_ID); }); - it('should prove all roles with same key', () => { - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); - expect(contract.canProveRole(ROLE_OP2)).toBe(true); - expect(contract.canProveRole(ROLE_OP3)).toBe(true); + it('should prove all roles with same key', async () => { + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); + expect(await contract.canProveRole(ROLE_OP2)).toBe(true); + expect(await contract.canProveRole(ROLE_OP3)).toBe(true); }); - it('revoking one role should not affect others', () => { - contract._revokeRole(ROLE_OP2, ADMIN_ACCOUNT_ID); + it('revoking one role should not affect others', async () => { + await contract._revokeRole(ROLE_OP2, ADMIN_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); - expect(contract.canProveRole(ROLE_OP1)).toBe(true); - expect(contract.canProveRole(ROLE_OP2)).toBe(false); - expect(contract.canProveRole(ROLE_OP3)).toBe(true); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); + expect(await contract.canProveRole(ROLE_OP1)).toBe(true); + expect(await contract.canProveRole(ROLE_OP2)).toBe(false); + expect(await contract.canProveRole(ROLE_OP3)).toBe(true); }); }); describe('cross-contract isolation', () => { - it('should not validate a role granted on a different contract instance', () => { - contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); - expect(contract.canProveRole(ROLE_ADMIN)).toBe(true); + it('should not validate a role granted on a different contract instance', async () => { + await contract._grantRole(ROLE_ADMIN, ADMIN_ACCOUNT_ID); + expect(await contract.canProveRole(ROLE_ADMIN)).toBe(true); // Deploy a different contract with a different salt const differentSalt = new Uint8Array(32).fill(99); - const contractB = new ShieldedAccessControlSimulator( + const contractB = await ShieldedAccessControlSimulator.create( differentSalt, true, { @@ -1198,12 +1208,12 @@ describe('ShieldedAccessControl', () => { // Same key on contract B produces a different accountId (different salt) // so canProveRole should return false — role was never granted on B - expect(contractB.canProveRole(ROLE_ADMIN)).toBe(false); + expect(await contractB.canProveRole(ROLE_ADMIN)).toBe(false); }); - it('should produce different commitments for same role and key across instances', () => { + it('should produce different commitments for same role and key across instances', async () => { const differentSalt = new Uint8Array(32).fill(99); - const contractB = new ShieldedAccessControlSimulator( + const contractB = await ShieldedAccessControlSimulator.create( differentSalt, true, { @@ -1212,15 +1222,15 @@ describe('ShieldedAccessControl', () => { }, ); - const commitmentA = contract.computeRoleCommitment( + const commitmentA = await contract.computeRoleCommitment( ROLE_ADMIN, ADMIN_ACCOUNT_ID, ); - const accountIdOnB = contractB.computeAccountId( + const accountIdOnB = await contractB.computeAccountId( ADMIN_SK, differentSalt, ); - const commitmentB = contractB.computeRoleCommitment( + const commitmentB = await contractB.computeRoleCommitment( ROLE_ADMIN, accountIdOnB, ); @@ -1231,46 +1241,54 @@ describe('ShieldedAccessControl', () => { }); describe('privateState helpers', () => { - beforeEach(() => { - contract = new ShieldedAccessControlSimulator(INSTANCE_SALT, true, { - privateState: ShieldedAccessControlPrivateState.withSecretKey(ADMIN_SK), - }); + beforeEach(async () => { + contract = await ShieldedAccessControlSimulator.create( + INSTANCE_SALT, + true, + { + privateState: + ShieldedAccessControlPrivateState.withSecretKey(ADMIN_SK), + }, + ); }); describe('getCurrentSecretKey', () => { - it('should return the secret key from private state', () => { - expect(contract.privateState.getCurrentSecretKey()).toEqual(ADMIN_SK); + it('should return the secret key from private state', async () => { + expect(await contract.privateState.getCurrentSecretKey()).toEqual( + ADMIN_SK, + ); }); - it('should throw when the secret key is undefined', () => { - contract.privateState.injectSecretKey(undefined as never); + it('should throw when the secret key is undefined', async () => { + await contract.privateState.injectSecretKey(undefined as never); - expect(() => contract.privateState.getCurrentSecretKey()).toThrow( - 'Missing secret key', - ); + await expect( + contract.privateState.getCurrentSecretKey(), + ).rejects.toThrow('Missing secret key'); }); }); describe('getCommitmentPathWithFindForLeaf', () => { - it('should return undefined when the commitment is not in the tree', () => { + it('should return undefined when the commitment is not in the tree', async () => { const absentCommitment = buildRoleCommitmentHash( ROLE_NONEXISTENT, BAD_ACCOUNT_ID, ); expect( - contract.privateState.getCommitmentPathWithFindForLeaf( + await contract.privateState.getCommitmentPathWithFindForLeaf( absentCommitment, ), ).toBeUndefined(); }); - it('should return a path when the commitment is in the tree', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('should return a path when the commitment is in the tree', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); - const path = contract.privateState.getCommitmentPathWithFindForLeaf( - OP1_ROLE_COMMITMENT, - ); + const path = + await contract.privateState.getCommitmentPathWithFindForLeaf( + OP1_ROLE_COMMITMENT, + ); expect(path).toBeDefined(); expect(path?.leaf).toEqual(OP1_ROLE_COMMITMENT); @@ -1278,30 +1296,31 @@ describe('ShieldedAccessControl', () => { }); describe('getCommitmentPathWithWitnessImpl', () => { - it('should return a default path when the commitment is not in the tree', () => { + it('should return a default path when the commitment is not in the tree', async () => { const absentCommitment = buildRoleCommitmentHash( ROLE_NONEXISTENT, BAD_ACCOUNT_ID, ); const path = - contract.privateState.getCommitmentPathWithWitnessImpl( + await contract.privateState.getCommitmentPathWithWitnessImpl( absentCommitment, ); expect(path.leaf).toEqual(new Uint8Array(32)); }); - it('should return a path matching findPathForLeaf when the commitment is in the tree', () => { - contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); + it('should return a path matching findPathForLeaf when the commitment is in the tree', async () => { + await contract._grantRole(ROLE_OP1, OP1_ACCOUNT_ID); const witnessPath = - contract.privateState.getCommitmentPathWithWitnessImpl( + await contract.privateState.getCommitmentPathWithWitnessImpl( + OP1_ROLE_COMMITMENT, + ); + const findPath = + await contract.privateState.getCommitmentPathWithFindForLeaf( OP1_ROLE_COMMITMENT, ); - const findPath = contract.privateState.getCommitmentPathWithFindForLeaf( - OP1_ROLE_COMMITMENT, - ); expect(witnessPath.leaf).toEqual(findPath?.leaf); }); diff --git a/contracts/src/access/test/ZOwnablePK.test.ts b/contracts/src/access/test/ZOwnablePK.test.ts index d0fc41b6..6c6f971f 100644 --- a/contracts/src/access/test/ZOwnablePK.test.ts +++ b/contracts/src/access/test/ZOwnablePK.test.ts @@ -7,13 +7,12 @@ import { import { beforeEach, describe, expect, it } from 'vitest'; import * as utils from '#test-utils/address.js'; import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.js'; -import { ZOwnablePKPrivateState } from './witnesses/ZOwnablePKWitnesses.js'; import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js'; +import { ZOwnablePKPrivateState } from './witnesses/ZOwnablePKWitnesses.js'; // PKs -const [OWNER, Z_OWNER] = utils.generatePubKeyPair('OWNER'); -const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER'); -const [UNAUTHORIZED, _] = utils.generatePubKeyPair('UNAUTHORIZED'); +const [, Z_OWNER] = utils.generatePubKeyPair('OWNER'); +const [, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER'); const INSTANCE_SALT = new Uint8Array(32).fill(8675309); const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE')); @@ -77,33 +76,37 @@ const buildCommitment = ( describe('ZOwnablePK', () => { describe('before initialize', () => { - it('should fail when setting owner commitment as 0', () => { - expect(() => { - const badId = new Uint8Array(32).fill(0); - new ZOwnablePKSimulator(badId, INSTANCE_SALT, isInit); - }).toThrow('ZOwnablePK: invalid id'); + it('should fail when setting owner commitment as 0', async () => { + const badId = new Uint8Array(32).fill(0); + await expect( + ZOwnablePKSimulator.create(badId, INSTANCE_SALT, isInit), + ).rejects.toThrow('ZOwnablePK: invalid id'); }); - it('should initialize with non-zero commitment', () => { + it('should initialize with non-zero commitment', async () => { const notZeroPK = utils.encodeToPK('NOT_ZERO'); const notZeroNonce = new Uint8Array(32).fill(1); const nonZeroId = createIdHash(notZeroPK, notZeroNonce); - ownable = new ZOwnablePKSimulator(nonZeroId, INSTANCE_SALT, isInit); + ownable = await ZOwnablePKSimulator.create( + nonZeroId, + INSTANCE_SALT, + isInit, + ); const nonZeroCommitment = buildCommitmentFromId( nonZeroId, INSTANCE_SALT, INIT_COUNTER, ); - expect(ownable.owner()).toEqual(nonZeroCommitment); + expect(await ownable.owner()).toEqual(nonZeroCommitment); }); }); describe('when not initialized correctly', () => { const isNotInit = false; - beforeEach(() => { - ownable = new ZOwnablePKSimulator( + beforeEach(async () => { + ownable = await ZOwnablePKSimulator.create( randomByteArray, INSTANCE_SALT, isNotInit, @@ -121,23 +124,25 @@ describe('ZOwnablePK', () => { ['_computeOwnerCommitment', [randomByteArray, randomCounter]], ['_transferOwnership', [randomByteArray]], ]; - it.each(circuitsToFail)('%s should fail', (circuitName, args) => { - expect(() => { - (ownable[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('ZOwnablePK: contract not initialized'); + it.each(circuitsToFail)('%s should fail', async (circuitName, args) => { + await expect( + (ownable[circuitName] as (...args: unknown[]) => Promise)( + ...args, + ), + ).rejects.toThrow('ZOwnablePK: contract not initialized'); }); - it('should allow pure computeOwnerId', () => { + it('should allow pure computeOwnerId', async () => { const eitherOwner = utils.createEitherTestUser('OWNER'); - expect(() => { - ownable._computeOwnerId(eitherOwner, randomByteArray); - }).not.toThrow(); + await expect( + ownable._computeOwnerId(eitherOwner, randomByteArray), + ).resolves.not.toThrow(); }); }); describe('after initialization', () => { - beforeEach(() => { + beforeEach(async () => { // Create private state object and generate nonce const PS = ZOwnablePKPrivateState.generate(); // Bind nonce for convenience @@ -145,13 +150,18 @@ describe('ZOwnablePK', () => { // Prepare owner ID with gen nonce const ownerId = createIdHash(Z_OWNER, secretNonce); // Deploy contract with derived owner commitment and PS - ownable = new ZOwnablePKSimulator(ownerId, INSTANCE_SALT, isInit, { - privateState: PS, - }); + ownable = await ZOwnablePKSimulator.create( + ownerId, + INSTANCE_SALT, + isInit, + { + privateState: PS, + }, + ); }); describe('owner', () => { - it('should return the correct owner commitment', () => { + it('should return the correct owner commitment', async () => { const expCommitment = buildCommitment( Z_OWNER, secretNonce, @@ -159,7 +169,7 @@ describe('ZOwnablePK', () => { INIT_COUNTER, DOMAIN, ); - expect(ownable.owner()).toEqual(expCommitment); + expect(await ownable.owner()).toEqual(expCommitment); }); }); @@ -183,58 +193,62 @@ describe('ZOwnablePK', () => { ); }); - it('should transfer ownership', () => { - ownable.as(OWNER).transferOwnership(newIdHash); - expect(ownable.owner()).toEqual(newOwnerCommitment); + it('should transfer ownership', async () => { + await ownable.as('OWNER').transferOwnership(newIdHash); + expect(await ownable.owner()).toEqual(newOwnerCommitment); // Old owner - expect(() => { - ownable.as(OWNER).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow( + 'ZOwnablePK: caller is not the owner', + ); // Unauthorized - expect(() => { - ownable.as(UNAUTHORIZED).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect( + ownable.as('UNAUTHORIZED').assertOnlyOwner(), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); // New owner - ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce)); - expect(() => { - ownable.as(NEW_OWNER).assertOnlyOwner(); - }).not.toThrow(); + await ownable.privateState.injectSecretNonce( + Buffer.from(newOwnerNonce), + ); + await expect( + ownable.as('NEW_OWNER').assertOnlyOwner(), + ).resolves.not.toThrow(); }); - it('should fail when transferring to id zero', () => { + it('should fail when transferring to id zero', async () => { const badId = new Uint8Array(32).fill(0); - expect(() => { - ownable.as(OWNER).transferOwnership(badId); - }).toThrow('ZOwnablePK: invalid id'); + await expect( + ownable.as('OWNER').transferOwnership(badId), + ).rejects.toThrow('ZOwnablePK: invalid id'); }); - it('should fail when unauthorized transfers ownership', () => { - expect(() => { - ownable.as(UNAUTHORIZED).transferOwnership(newOwnerCommitment); - }).toThrow('ZOwnablePK: caller is not the owner'); + it('should fail when unauthorized transfers ownership', async () => { + await expect( + ownable.as('UNAUTHORIZED').transferOwnership(newOwnerCommitment), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); }); /** * @description More thoroughly tested in `_transferOwnership` * */ - it('should bump instance after transfer', () => { - const beforeInstance = ownable.getPublicState().ZOwnablePK__counter; + it('should bump instance after transfer', async () => { + const beforeInstance = (await ownable.getPublicState()) + .ZOwnablePK__counter; // Transfer - ownable.as(OWNER).transferOwnership(newOwnerCommitment); + await ownable.as('OWNER').transferOwnership(newOwnerCommitment); // Check counter - const afterInstance = ownable.getPublicState().ZOwnablePK__counter; + const afterInstance = (await ownable.getPublicState()) + .ZOwnablePK__counter; expect(afterInstance).toEqual(beforeInstance + 1n); }); - it('should change commitment when transferring ownership to self with same pk + nonce)', () => { + it('should change commitment when transferring ownership to self with same pk + nonce)', async () => { // Confirm current commitment const repeatedId = createIdHash(Z_OWNER, secretNonce); - const initCommitment = ownable.owner(); + const initCommitment = await ownable.owner(); const expInitCommitment = buildCommitmentFromId( repeatedId, INSTANCE_SALT, @@ -243,10 +257,10 @@ describe('ZOwnablePK', () => { expect(initCommitment).toEqual(expInitCommitment); // Transfer ownership to self with the same id -> `H(pk, nonce)` - ownable.as(OWNER).transferOwnership(repeatedId); + await ownable.as('OWNER').transferOwnership(repeatedId); // Check commitments don't match - const newCommitment = ownable.owner(); + const newCommitment = await ownable.owner(); expect(initCommitment).not.toEqual(newCommitment); // Build commitment locally and validate new commitment == expected @@ -259,97 +273,97 @@ describe('ZOwnablePK', () => { expect(newCommitment).toEqual(expNewCommitment); // Check same owner maintains permissions after transfer - expect(() => { - ownable.as(OWNER).assertOnlyOwner(); - }).not.toThrow(); + await expect( + ownable.as('OWNER').assertOnlyOwner(), + ).resolves.not.toThrow(); }); }); describe('renounceOwnership', () => { - it('should renounce ownership', () => { - ownable.as(OWNER).renounceOwnership(); + it('should renounce ownership', async () => { + await ownable.as('OWNER').renounceOwnership(); // Check owner is reset - expect(ownable.owner()).toEqual(new Uint8Array(32).fill(0)); + expect(await ownable.owner()).toEqual(new Uint8Array(32).fill(0)); // Check revoked permissions - expect(() => { - ownable.as(OWNER).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow( + 'ZOwnablePK: caller is not the owner', + ); }); - it('should fail when renouncing from unauthorized', () => { - expect(() => { - ownable.as(UNAUTHORIZED).renounceOwnership(); - }).toThrow('ZOwnablePK: caller is not the owner'); + it('should fail when renouncing from unauthorized', async () => { + await expect( + ownable.as('UNAUTHORIZED').renounceOwnership(), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); }); - it('should fail when renouncing from authorized with bad nonce', () => { - ownable.privateState.injectSecretNonce(BAD_NONCE); - expect(() => { - ownable.as(OWNER).renounceOwnership(); - }).toThrow('ZOwnablePK: caller is not the owner'); + it('should fail when renouncing from authorized with bad nonce', async () => { + await ownable.privateState.injectSecretNonce(BAD_NONCE); + await expect(ownable.as('OWNER').renounceOwnership()).rejects.toThrow( + 'ZOwnablePK: caller is not the owner', + ); }); - it('should fail when renouncing from unauthorized with bad nonce', () => { - ownable.privateState.injectSecretNonce(BAD_NONCE); - expect(() => { - ownable.as(UNAUTHORIZED).renounceOwnership(); - }).toThrow('ZOwnablePK: caller is not the owner'); + it('should fail when renouncing from unauthorized with bad nonce', async () => { + await ownable.privateState.injectSecretNonce(BAD_NONCE); + await expect( + ownable.as('UNAUTHORIZED').renounceOwnership(), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); }); }); describe('assertOnlyOwner', () => { - it('should allow authorized caller with correct nonce to call', () => { + it('should allow authorized caller with correct nonce to call', async () => { // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + expect(await ownable.privateState.getCurrentSecretNonce()).toEqual( secretNonce, ); - expect(() => { - ownable.as(OWNER).assertOnlyOwner(); - }).not.toThrow(); + await expect( + ownable.as('OWNER').assertOnlyOwner(), + ).resolves.not.toThrow(); }); - it('should fail when the authorized caller has the wrong nonce', () => { + it('should fail when the authorized caller has the wrong nonce', async () => { // Inject bad nonce - ownable.privateState.injectSecretNonce(BAD_NONCE); + await ownable.privateState.injectSecretNonce(BAD_NONCE); // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + expect(await ownable.privateState.getCurrentSecretNonce()).not.toEqual( secretNonce, ); // Set caller and call circuit - expect(() => { - ownable.as(OWNER).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow( + 'ZOwnablePK: caller is not the owner', + ); }); - it('should fail when unauthorized caller has the correct nonce', () => { + it('should fail when unauthorized caller has the correct nonce', async () => { // Check nonce is correct - expect(ownable.privateState.getCurrentSecretNonce()).toEqual( + expect(await ownable.privateState.getCurrentSecretNonce()).toEqual( secretNonce, ); - expect(() => { - ownable.as(UNAUTHORIZED).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect( + ownable.as('UNAUTHORIZED').assertOnlyOwner(), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); }); - it('should fail when unauthorized caller has the wrong nonce', () => { + it('should fail when unauthorized caller has the wrong nonce', async () => { // Inject bad nonce - ownable.privateState.injectSecretNonce(BAD_NONCE); + await ownable.privateState.injectSecretNonce(BAD_NONCE); // Check nonce does not match - expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual( + expect(await ownable.privateState.getCurrentSecretNonce()).not.toEqual( secretNonce, ); // Set unauthorized caller and call circuit - expect(() => { - ownable.as(UNAUTHORIZED).assertOnlyOwner(); - }).toThrow('ZOwnablePK: caller is not the owner'); + await expect( + ownable.as('UNAUTHORIZED').assertOnlyOwner(), + ).rejects.toThrow('ZOwnablePK: caller is not the owner'); }); }); @@ -374,14 +388,17 @@ describe('ZOwnablePK', () => { ]; it.each( testCases, - )('should match commitment for $label with counter $counter', ({ + )('should match commitment for $label with counter $counter', async ({ ownerPK, counter, }) => { const id = createIdHash(ownerPK, secretNonce); // Check buildCommitmentFromId - const hashFromContract = ownable._computeOwnerCommitment(id, counter); + const hashFromContract = await ownable._computeOwnerCommitment( + id, + counter, + ); const hashFromHelper1 = buildCommitmentFromId( id, INSTANCE_SALT, @@ -422,28 +439,30 @@ describe('ZOwnablePK', () => { it.each( testCases, - )('should match local and contract owner id for $label', ({ + )('should match local and contract owner id for $label', async ({ eitherOwner, nonce, }) => { - const ownerId = ownable._computeOwnerId(eitherOwner, nonce); + const ownerId = await ownable._computeOwnerId(eitherOwner, nonce); const expId = createIdHash(eitherOwner.left, nonce); expect(ownerId).toEqual(expId); }); - it('should fail to compute ContractAddress id', () => { + it('should fail to compute ContractAddress id', async () => { const eitherContract = utils.createEitherTestContractAddress('CONTRACT'); - expect(() => { - ownable._computeOwnerId(eitherContract, secretNonce); - }).toThrow('ZOwnablePK: contract address owners are not yet supported'); + await expect( + ownable._computeOwnerId(eitherContract, secretNonce), + ).rejects.toThrow( + 'ZOwnablePK: contract address owners are not yet supported', + ); }); }); describe('_transferOwnership', () => { - it('should transfer ownership', () => { + it('should transfer ownership', async () => { const id = createIdHash(Z_OWNER, secretNonce); - ownable._transferOwnership(id); + await ownable._transferOwnership(id); const nextCounter = INIT_COUNTER + 1n; const expCommitment = buildCommitmentFromId( @@ -451,27 +470,27 @@ describe('ZOwnablePK', () => { INSTANCE_SALT, nextCounter, ); - expect(ownable.owner()).toEqual(expCommitment); + expect(await ownable.owner()).toEqual(expCommitment); }); - it('should bump the counter with each transfer', () => { + it('should bump the counter with each transfer', async () => { const nTransfers = 10; const counterStart = 2; // count starts at 2 bc the constructor bumps the count to 1 for (let i = counterStart; i <= nTransfers; i++) { const pk = utils.encodeToPK(`Id${i}`); const nonce = new Uint8Array(32).fill(i); const id = createIdHash(pk, nonce); - ownable._transferOwnership(id); + await ownable._transferOwnership(id); - expect(ownable.getPublicState().ZOwnablePK__counter).toEqual( + expect((await ownable.getPublicState()).ZOwnablePK__counter).toEqual( BigInt(i), ); } }); - it('should allow transfer to all zeroes id', () => { + it('should allow transfer to all zeroes id', async () => { const zeroId = utils.zeroUint8Array(); - ownable._transferOwnership(zeroId); + await ownable._transferOwnership(zeroId); const nextCounter = INIT_COUNTER + 1n; const expCommitment = buildCommitmentFromId( @@ -479,18 +498,18 @@ describe('ZOwnablePK', () => { INSTANCE_SALT, nextCounter, ); - expect(ownable.owner()).toEqual(expCommitment); + expect(await ownable.owner()).toEqual(expCommitment); }); - it('should allow anyone to transfer', () => { + it('should allow anyone to transfer', async () => { const id = createIdHash(Z_OWNER, secretNonce); - expect(() => { - ownable.as(OWNER)._transferOwnership(id); - }).not.toThrow(); + await expect( + ownable.as('OWNER')._transferOwnership(id), + ).resolves.not.toThrow(); - expect(() => { - ownable.as(UNAUTHORIZED)._transferOwnership(id); - }).not.toThrow(); + await expect( + ownable.as('UNAUTHORIZED')._transferOwnership(id), + ).resolves.not.toThrow(); }); }); }); diff --git a/contracts/src/access/test/simulators/AccessControlSimulator.ts b/contracts/src/access/test/simulators/AccessControlSimulator.ts index 3314b2f5..e45ba80a 100644 --- a/contracts/src/access/test/simulators/AccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/AccessControlSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -31,26 +31,28 @@ const AccessControlSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => AccessControlWitnesses(), + artifactName: 'MockAccessControl', }); /** * AccessControl Simulator */ export class AccessControlSimulator extends AccessControlSimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< AccessControlPrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } /** * @description Returns the default admin role identifier. * @returns The default admin role identifier (zero bytes). */ - public DEFAULT_ADMIN_ROLE(): Uint8Array { + public DEFAULT_ADMIN_ROLE(): Promise { return this.circuits.pure.DEFAULT_ADMIN_ROLE(); } @@ -63,7 +65,7 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public hasRole( roleId: Uint8Array, account: Either, - ): boolean { + ): Promise { return this.circuits.impure.hasRole(roleId, account); } @@ -71,8 +73,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { * @description Retrieves an account's permission for `roleId`. * @param roleId - The role identifier. */ - public assertOnlyRole(roleId: Uint8Array) { - this.circuits.impure.assertOnlyRole(roleId); + public assertOnlyRole(roleId: Uint8Array): Promise<[]> { + return this.circuits.impure.assertOnlyRole(roleId); } /** @@ -83,8 +85,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public _checkRole( roleId: Uint8Array, account: Either, - ) { - this.circuits.impure._checkRole(roleId, account); + ): Promise<[]> { + return this.circuits.impure._checkRole(roleId, account); } /** @@ -92,7 +94,7 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { * @param roleId - The role identifier. * @returns The admin identifier for `roleId`. */ - public getRoleAdmin(roleId: Uint8Array): Uint8Array { + public getRoleAdmin(roleId: Uint8Array): Promise { return this.circuits.impure.getRoleAdmin(roleId); } @@ -104,8 +106,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public grantRole( roleId: Uint8Array, account: Either, - ) { - this.circuits.impure.grantRole(roleId, account); + ): Promise<[]> { + return this.circuits.impure.grantRole(roleId, account); } /** @@ -116,8 +118,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public revokeRole( roleId: Uint8Array, account: Either, - ) { - this.circuits.impure.revokeRole(roleId, account); + ): Promise<[]> { + return this.circuits.impure.revokeRole(roleId, account); } /** @@ -128,8 +130,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public renounceRole( roleId: Uint8Array, account: Either, - ) { - this.circuits.impure.renounceRole(roleId, account); + ): Promise<[]> { + return this.circuits.impure.renounceRole(roleId, account); } /** @@ -137,8 +139,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { * @param roleId - The role identifier. * @param adminId - The admin role identifier. */ - public _setRoleAdmin(roleId: Uint8Array, adminId: Uint8Array) { - this.circuits.impure._setRoleAdmin(roleId, adminId); + public _setRoleAdmin(roleId: Uint8Array, adminId: Uint8Array): Promise<[]> { + return this.circuits.impure._setRoleAdmin(roleId, adminId); } /** @@ -149,7 +151,7 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public _grantRole( roleId: Uint8Array, account: Either, - ): boolean { + ): Promise { return this.circuits.impure._grantRole(roleId, account); } @@ -162,7 +164,7 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public _unsafeGrantRole( roleId: Uint8Array, account: Either, - ): boolean { + ): Promise { return this.circuits.impure._unsafeGrantRole(roleId, account); } @@ -174,7 +176,7 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { public _revokeRole( roleId: Uint8Array, account: Either, - ): boolean { + ): Promise { return this.circuits.impure._revokeRole(roleId, account); } @@ -186,14 +188,16 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: (newSK: Uint8Array): AccessControlPrivateState => { - const currentState = this.getPrivateState(); - const updatedState = { - ...currentState, + injectSecretKey: async ( + newSK: Uint8Array, + ): Promise => { + const cur = await this.getPrivateState(); + const updated = { + ...cur, ...AccessControlPrivateState.withSecretKey(newSK), }; - this.circuitContextManager.updatePrivateState(updatedState); - return updatedState; + this.setPrivateState(updated); + return updated; }, /** @@ -201,8 +205,8 @@ export class AccessControlSimulator extends AccessControlSimulatorBase { * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } diff --git a/contracts/src/access/test/simulators/OwnableSimulator.ts b/contracts/src/access/test/simulators/OwnableSimulator.ts index 2e14f4e1..98a79f42 100644 --- a/contracts/src/access/test/simulators/OwnableSimulator.ts +++ b/contracts/src/access/test/simulators/OwnableSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -34,27 +34,32 @@ const OwnableSimulatorBase = createSimulator< contractArgs: (initialOwner, isInit) => [initialOwner, isInit], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => OwnableWitnesses(), + artifactName: 'MockOwnable', }); /** * Ownable Simulator */ export class OwnableSimulator extends OwnableSimulatorBase { - constructor( + static async create( initialOwner: Either, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< OwnablePrivateState, ReturnType > = {}, - ) { - super([initialOwner, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [initialOwner, isInit], + options, + ) as Promise; } /** * @description Returns the current contract owner. * @returns The contract owner. */ - public owner(): Either { + public owner(): Promise> { return this.circuits.impure.owner(); } @@ -62,8 +67,10 @@ export class OwnableSimulator extends OwnableSimulatorBase { * @description Transfers ownership of the contract to `newOwner`. * @param newOwner - The new owner. */ - public transferOwnership(newOwner: Either) { - this.circuits.impure.transferOwnership(newOwner); + public transferOwnership( + newOwner: Either, + ): Promise<[]> { + return this.circuits.impure.transferOwnership(newOwner); } /** @@ -72,8 +79,8 @@ export class OwnableSimulator extends OwnableSimulatorBase { */ public _unsafeTransferOwnership( newOwner: Either, - ) { - this.circuits.impure._unsafeTransferOwnership(newOwner); + ): Promise<[]> { + return this.circuits.impure._unsafeTransferOwnership(newOwner); } /** @@ -81,16 +88,16 @@ export class OwnableSimulator extends OwnableSimulatorBase { * It will not be possible to call `assertOnlyOnwer` circuits anymore. * Can only be called by the current owner. */ - public renounceOwnership() { - this.circuits.impure.renounceOwnership(); + public renounceOwnership(): Promise<[]> { + return this.circuits.impure.renounceOwnership(); } /** * @description Throws if called by any account other than the owner. * Use this to restrict access of specific circuits to the owner. */ - public assertOnlyOwner() { - this.circuits.impure.assertOnlyOwner(); + public assertOnlyOwner(): Promise<[]> { + return this.circuits.impure.assertOnlyOwner(); } /** @@ -98,8 +105,10 @@ export class OwnableSimulator extends OwnableSimulatorBase { * enforcing permission checks on the caller. * @param newOwner - The new owner. */ - public _transferOwnership(newOwner: Either) { - this.circuits.impure._transferOwnership(newOwner); + public _transferOwnership( + newOwner: Either, + ): Promise<[]> { + return this.circuits.impure._transferOwnership(newOwner); } /** @@ -108,8 +117,8 @@ export class OwnableSimulator extends OwnableSimulatorBase { */ public _unsafeUncheckedTransferOwnership( newOwner: Either, - ) { - this.circuits.impure._unsafeUncheckedTransferOwnership(newOwner); + ): Promise<[]> { + return this.circuits.impure._unsafeUncheckedTransferOwnership(newOwner); } public readonly privateState = { @@ -120,10 +129,12 @@ export class OwnableSimulator extends OwnableSimulatorBase { * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: (newSK: Uint8Array): OwnablePrivateState => { - const updatedState = OwnablePrivateState.withSecretKey(newSK); - this.circuitContextManager.updatePrivateState(updatedState); - return updatedState; + injectSecretKey: async ( + newSK: Uint8Array, + ): Promise => { + const updated = OwnablePrivateState.withSecretKey(newSK); + this.setPrivateState(updated); + return updated; }, /** @@ -131,8 +142,8 @@ export class OwnableSimulator extends OwnableSimulatorBase { * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } diff --git a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts index 05c3858a..0c84864d 100644 --- a/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts +++ b/contracts/src/access/test/simulators/ShieldedAccessControlSimulator.ts @@ -1,7 +1,10 @@ -import type { MerkleTreePath } from '@midnight-ntwrk/compact-runtime'; +import type { + MerkleTreePath, + WitnessContext, +} from '@midnight-ntwrk/compact-runtime'; import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -38,78 +41,86 @@ const ShieldedAccessControlSimulatorBase = createSimulator< ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedAccessControlWitnesses(), + artifactName: 'MockShieldedAccessControl', }); /** * ShieldedAccessControlSimulator */ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulatorBase { - constructor( + static async create( instanceSalt: Uint8Array, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< ShieldedAccessControlPrivateState, ReturnType > = {}, - ) { - super([instanceSalt, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [instanceSalt, isInit], + options, + ) as Promise; } - public DEFAULT_ADMIN_ROLE(): Uint8Array { + public DEFAULT_ADMIN_ROLE(): Promise { return this.circuits.pure.DEFAULT_ADMIN_ROLE(); } - public assertOnlyRole(role: Uint8Array) { - this.circuits.impure.assertOnlyRole(role); + public assertOnlyRole(role: Uint8Array): Promise<[]> { + return this.circuits.impure.assertOnlyRole(role); } - public canProveRole(role: Uint8Array): boolean { + public canProveRole(role: Uint8Array): Promise { return this.circuits.impure.canProveRole(role); } - public grantRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure.grantRole(role, accountId); + public grantRole(role: Uint8Array, accountId: Uint8Array): Promise<[]> { + return this.circuits.impure.grantRole(role, accountId); } - public _grantRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure._grantRole(role, accountId); + public _grantRole(role: Uint8Array, accountId: Uint8Array): Promise<[]> { + return this.circuits.impure._grantRole(role, accountId); } - public renounceRole(role: Uint8Array, callerConfirmation: Uint8Array) { - this.circuits.impure.renounceRole(role, callerConfirmation); + public renounceRole( + role: Uint8Array, + callerConfirmation: Uint8Array, + ): Promise<[]> { + return this.circuits.impure.renounceRole(role, callerConfirmation); } - public revokeRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure.revokeRole(role, accountId); + public revokeRole(role: Uint8Array, accountId: Uint8Array): Promise<[]> { + return this.circuits.impure.revokeRole(role, accountId); } - public _revokeRole(role: Uint8Array, accountId: Uint8Array) { - this.circuits.impure._revokeRole(role, accountId); + public _revokeRole(role: Uint8Array, accountId: Uint8Array): Promise<[]> { + return this.circuits.impure._revokeRole(role, accountId); } - public getRoleAdmin(role: Uint8Array): Uint8Array { + public getRoleAdmin(role: Uint8Array): Promise { return this.circuits.impure.getRoleAdmin(role); } - public _setRoleAdmin(role: Uint8Array, adminRole: Uint8Array) { - this.circuits.impure._setRoleAdmin(role, adminRole); + public _setRoleAdmin(role: Uint8Array, adminRole: Uint8Array): Promise<[]> { + return this.circuits.impure._setRoleAdmin(role, adminRole); } public computeRoleCommitment( role: Uint8Array, accountId: Uint8Array, - ): Uint8Array { + ): Promise { return this.circuits.impure.computeRoleCommitment(role, accountId); } - public computeNullifier(roleCommitment: Uint8Array): Uint8Array { + public computeNullifier(roleCommitment: Uint8Array): Promise { return this.circuits.pure.computeNullifier(roleCommitment); } public computeAccountId( secretKey: Uint8Array, instanceSalt: Uint8Array, - ): Uint8Array { + ): Promise { return this.circuits.pure.computeAccountId(secretKey, instanceSalt); } @@ -121,12 +132,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: ( + injectSecretKey: async ( newSK: Buffer, - ): ShieldedAccessControlPrivateState => { - const updatedState = { secretKey: newSK }; - this.circuitContextManager.updatePrivateState(updatedState); - return updatedState; + ): Promise => { + const updated = { secretKey: newSK }; + this.setPrivateState(updated); + return updated; }, /** @@ -134,8 +145,8 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } @@ -149,12 +160,12 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @param roleCommitment - The role commitment to search for. * @returns The Merkle tree path if the commitment exists, undefined otherwise. */ - getCommitmentPathWithFindForLeaf: ( + getCommitmentPathWithFindForLeaf: async ( roleCommitment: Uint8Array, - ): MerkleTreePath | undefined => { - return this.getPublicState().ShieldedAccessControl__operatorRoles.findPathForLeaf( - roleCommitment, - ); + ): Promise | undefined> => { + return ( + await this.getPublicState() + ).ShieldedAccessControl__operatorRoles.findPathForLeaf(roleCommitment); }, /** @@ -165,11 +176,19 @@ export class ShieldedAccessControlSimulator extends ShieldedAccessControlSimulat * @param roleCommitment - The role commitment to find a path for. * @returns The Merkle tree path as returned by the witness. */ - getCommitmentPathWithWitnessImpl: ( + getCommitmentPathWithWitnessImpl: async ( roleCommitment: Uint8Array, - ): MerkleTreePath => { + ): Promise> => { + const context: WitnessContext< + ShieldedAccessControlLedger, + ShieldedAccessControlPrivateState + > = { + ledger: await this.getPublicState(), + privateState: await this.getPrivateState(), + contractAddress: '', + }; return this.witnesses.wit_getRoleCommitmentPath( - this.getWitnessContext(), + context, roleCommitment, )[1]; }, diff --git a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts index 05ddf6ec..c0c780d3 100644 --- a/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts +++ b/contracts/src/access/test/simulators/ZOwnablePKSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -49,22 +49,27 @@ const ZOwnablePKSimulatorBase: any = createSimulator< }, ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ZOwnablePKWitnesses(), + artifactName: 'MockZOwnablePK', }); /** * ZOwnablePKSimulator */ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { - constructor( + static async create( ownerId: Uint8Array, instanceSalt: Uint8Array, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< ZOwnablePKPrivateState, ReturnType > = {}, - ) { - super([ownerId, instanceSalt, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [ownerId, instanceSalt, isInit], + options, + ) as Promise; } /** @@ -72,7 +77,7 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. * @returns The current owner's commitment. */ - public owner(): Uint8Array { + public owner(): Promise { return this.circuits.impure.owner(); } @@ -81,8 +86,8 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * `newOwnerId` must be precalculated and given to the current owner off chain. * @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`). */ - public transferOwnership(newOwnerId: Uint8Array) { - this.circuits.impure.transferOwnership(newOwnerId); + public transferOwnership(newOwnerId: Uint8Array): Promise<[]> { + return this.circuits.impure.transferOwnership(newOwnerId); } /** @@ -90,16 +95,16 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * It will not be possible to call `assertOnlyOnwer` circuits anymore. * Can only be called by the current owner. */ - public renounceOwnership() { - this.circuits.impure.renounceOwnership(); + public renounceOwnership(): Promise<[]> { + return this.circuits.impure.renounceOwnership(); } /** * @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match * the stored owner commitment. Use this to only allow the owner to call specific circuits. */ - public assertOnlyOwner() { - this.circuits.impure.assertOnlyOwner(); + public assertOnlyOwner(): Promise<[]> { + return this.circuits.impure.assertOnlyOwner(); } /** @@ -109,7 +114,10 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * after every transfer to prevent duplicate commitments given the same `id`. * @returns The commitment derived from `id` and `counter`. */ - public _computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array { + public _computeOwnerCommitment( + id: Uint8Array, + counter: bigint, + ): Promise { return this.circuits.impure._computeOwnerCommitment(id, counter); } @@ -123,7 +131,7 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { public _computeOwnerId( pk: Either, nonce: Uint8Array, - ): Uint8Array { + ): Promise { return this.circuits.pure._computeOwnerId(pk, nonce); } @@ -132,8 +140,8 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * enforcing permission checks on the caller. * @param newOwnerId - The unique identifier of the new owner calculated by `SHA256(pk, nonce)`. */ - public _transferOwnership(newOwnerId: Uint8Array) { - this.circuits.impure._transferOwnership(newOwnerId); + public _transferOwnership(newOwnerId: Uint8Array): Promise<[]> { + return this.circuits.impure._transferOwnership(newOwnerId); } public readonly privateState = { @@ -142,23 +150,21 @@ export class ZOwnablePKSimulator extends ZOwnablePKSimulatorBase { * @param newNonce The secret nonce. * @returns The ZOwnablePK private state after setting the new nonce. */ - injectSecretNonce: ( + injectSecretNonce: async ( newNonce: Buffer, - ): ZOwnablePKPrivateState => { - const currentState = - this.circuitContextManager.getContext().currentPrivateState; - const updatedState = { ...currentState, secretNonce: newNonce }; - this.circuitContextManager.updatePrivateState(updatedState); - return updatedState; + ): Promise => { + const cur = await this.getPrivateState(); + const updated = { ...cur, secretNonce: newNonce }; + this.setPrivateState(updated); + return updated; }, /** * @description Returns the secret nonce given the context. * @returns The secret nonce. */ - getCurrentSecretNonce: (): Uint8Array => { - return this.circuitContextManager.getContext().currentPrivateState - .secretNonce; + getCurrentSecretNonce: async (): Promise => { + return (await this.getPrivateState()).secretNonce; }, }; } diff --git a/contracts/src/archive/test/ShieldedToken.test.ts b/contracts/src/archive/test/ShieldedToken.test.ts index dac0b238..9fe8d4db 100644 --- a/contracts/src/archive/test/ShieldedToken.test.ts +++ b/contracts/src/archive/test/ShieldedToken.test.ts @@ -36,24 +36,39 @@ let thisTokenType: TokenType; describe('Shielded token', () => { describe('initializer and metadata', () => { - it('should initialize metadata', () => { - token = new ShieldedTokenSimulator(NONCE, NAME, SYMBOL, DECIMALS); + it('should initialize metadata', async () => { + token = await ShieldedTokenSimulator.create( + NONCE, + NAME, + SYMBOL, + DECIMALS, + ); - expect(token.name()).toEqual(NAME); - expect(token.symbol()).toEqual(SYMBOL); - expect(token.decimals()).toEqual(DECIMALS); + expect(await token.name()).toEqual(NAME); + expect(await token.symbol()).toEqual(SYMBOL); + expect(await token.decimals()).toEqual(DECIMALS); }); - it('should initialize empty metadata', () => { - token = new ShieldedTokenSimulator(NONCE, NO_STRING, NO_STRING, 0n); + it('should initialize empty metadata', async () => { + token = await ShieldedTokenSimulator.create( + NONCE, + NO_STRING, + NO_STRING, + 0n, + ); - expect(token.name()).toEqual(NO_STRING); - expect(token.symbol()).toEqual(NO_STRING); - expect(token.decimals()).toEqual(0n); + expect(await token.name()).toEqual(NO_STRING); + expect(await token.symbol()).toEqual(NO_STRING); + expect(await token.decimals()).toEqual(0n); }); - it('should set public state', () => { - token = new ShieldedTokenSimulator(NONCE, NAME, SYMBOL, DECIMALS); + it('should set public state', async () => { + token = await ShieldedTokenSimulator.create( + NONCE, + NAME, + SYMBOL, + DECIMALS, + ); expect(token.getCurrentPublicState().ShieldedToken__counter).toEqual(0n); expect(token.getCurrentPublicState().ShieldedToken__domain).toEqual( @@ -63,14 +78,14 @@ describe('Shielded token', () => { }); }); - beforeEach(() => { - token = new ShieldedTokenSimulator(NONCE, NAME, SYMBOL, DECIMALS); + beforeEach(async () => { + token = await ShieldedTokenSimulator.create(NONCE, NAME, SYMBOL, DECIMALS); thisTokenType = tokenType(DOMAIN, token.contractAddress); }); describe('mint', () => { - it('should mint', () => { - const res = token.mint(Z_OWNER, AMOUNT); + it('should mint', async () => { + const res = await token.mint(Z_OWNER, AMOUNT); const thisNonce = token.getCurrentPublicState().ShieldedToken__nonce; const thisCoinInfo = { color: encodeTokenType(thisTokenType), @@ -88,21 +103,21 @@ describe('Shielded token', () => { Z_OWNER, ); // Check supply - expect(token.totalSupply()).toEqual(AMOUNT); + expect(await token.totalSupply()).toEqual(AMOUNT); }); - it('should bump counter', () => { + it('should bump counter', async () => { expect(token.getCurrentPublicState().ShieldedToken__counter).toEqual(0n); - token.mint(Z_OWNER, AMOUNT); + await token.mint(Z_OWNER, AMOUNT); expect(token.getCurrentPublicState().ShieldedToken__counter).toEqual(1n); }); - it('should bump nonce', () => { + it('should bump nonce', async () => { const initNonce = token.getCurrentPublicState().ShieldedToken__nonce; expect(initNonce).toEqual(NONCE); - token.mint(Z_OWNER, AMOUNT); + await token.mint(Z_OWNER, AMOUNT); // TODO: create js equivalent of `evolve_nonce` circuit to derive correct value expect(initNonce).not.toEqual( @@ -110,27 +125,27 @@ describe('Shielded token', () => { ); }); - it('should fail when minting to the zero address', () => { - expect(() => { - token.mint(utils.ZERO_KEY, AMOUNT); - }).toThrow('ShieldedToken: invalid recipient'); + it('should fail when minting to the zero address', async () => { + await expect(token.mint(utils.ZERO_KEY, AMOUNT)).rejects.toThrow( + 'ShieldedToken: invalid recipient', + ); }); - it('should fail when minting overflow uint64', () => { - token.mint(Z_OWNER, MAX_UINT64); + it('should fail when minting overflow uint64', async () => { + await token.mint(Z_OWNER, MAX_UINT64); - expect(() => { - token.mint(Z_OWNER, 1n); - }).toThrow('arithmetic overflow'); + await expect(token.mint(Z_OWNER, 1n)).rejects.toThrow( + 'arithmetic overflow', + ); }); }); describe('burn', () => { - beforeEach(() => { - token.mint(Z_OWNER, AMOUNT); + beforeEach(async () => { + await token.mint(Z_OWNER, AMOUNT); }); - it('should burn (whole)', () => { + it('should burn (whole)', async () => { const nonceStr = NONCE.filter((x) => x !== 0) .join('') .padStart(64, '0'); //297481949006 @@ -142,7 +157,7 @@ describe('Shielded token', () => { const encoded_coin_info = encodeCoinInfo(coin_info); // Burn - const res = token.burn(encoded_coin_info, AMOUNT); + const res = await token.burn(encoded_coin_info, AMOUNT); // Check circuit result expect(res.result.change.is_some).toBe(false); @@ -162,10 +177,10 @@ describe('Shielded token', () => { expect(txInputs.nonce).toEqual(encoded_coin_info.nonce); // Check supply - expect(token.totalSupply()).toEqual(0n); + expect(await token.totalSupply()).toEqual(0n); }); - it('should burn (partial)', () => { + it('should burn (partial)', async () => { const nonceStr = NONCE.filter((x) => x !== 0) .join('') .padStart(64, '0'); @@ -178,7 +193,7 @@ describe('Shielded token', () => { const encoded_coin_info = encodeCoinInfo(coin_info); // Burn - const res = token.burn(encoded_coin_info, partialAmt); + const res = await token.burn(encoded_coin_info, partialAmt); // Check circuit result const change = res.result.change; @@ -204,10 +219,10 @@ describe('Shielded token', () => { expect(txInput2.nonce).not.toEqual(encoded_coin_info.nonce); // Check supply - expect(token.totalSupply()).toEqual(1n); + expect(await token.totalSupply()).toEqual(1n); }); - it('should fail with incorrect domain', () => { + it('should fail with incorrect domain', async () => { const nonceStr = NONCE.filter((x) => x !== 0) .join('') .padStart(64, '0'); @@ -220,12 +235,12 @@ describe('Shielded token', () => { }; const encoded_coin_info = encodeCoinInfo(coin_info); - expect(() => { - token.burn(encoded_coin_info, AMOUNT); - }).toThrow('ShieldedToken: token not created from this contract'); + await expect(token.burn(encoded_coin_info, AMOUNT)).rejects.toThrow( + 'ShieldedToken: token not created from this contract', + ); }); - it('should fail with incorrect address', () => { + it('should fail with incorrect address', async () => { const nonceStr = NONCE.filter((x) => x !== 0) .join('') .padStart(64, '0'); @@ -238,12 +253,12 @@ describe('Shielded token', () => { }; const encoded_coin_info = encodeCoinInfo(coin_info); - expect(() => { - token.burn(encoded_coin_info, AMOUNT); - }).toThrow('ShieldedToken: token not created from this contract'); + await expect(token.burn(encoded_coin_info, AMOUNT)).rejects.toThrow( + 'ShieldedToken: token not created from this contract', + ); }); - it('should fail when not enough balance', () => { + it('should fail when not enough balance', async () => { const nonceStr = NONCE.filter((x) => x !== 0) .join('') .padStart(64, '0'); @@ -255,9 +270,9 @@ describe('Shielded token', () => { }; const encoded_coin_info = encodeCoinInfo(coin_info); - expect(() => { - token.burn(encoded_coin_info, AMOUNT + 1n); - }).toThrow('ShieldedToken: insufficient token amount to burn'); + await expect(token.burn(encoded_coin_info, AMOUNT + 1n)).rejects.toThrow( + 'ShieldedToken: insufficient token amount to burn', + ); }); }); }); diff --git a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts index da476a9d..85a33d6b 100644 --- a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts +++ b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts @@ -19,14 +19,24 @@ import { type SendResult, type ZswapCoinPublicKey, } from '../../../../artifacts/MockShieldedToken/contract/index.js'; // Combined imports +import type { IContractSimulator } from '../types/test.js'; import { type ShieldedTokenPrivateState, ShieldedTokenWitnesses, } from '../witnesses/ShieldedTokenWitnesses.js'; -import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of a shielded token contract for testing purposes. + * + * @remarks + * The `archive` module predates the backend-aware `@openzeppelin/compact-simulator` + * factory (`createSimulator`). Its tests inspect the raw `CircuitResults` + * (`res.context.currentZswapLocalState` inputs/outputs) and override the Zswap + * sender per call, neither of which the async circuit proxy surfaces. The + * simulator therefore keeps its hand-rolled `IContractSimulator` shape, but its + * lifecycle is aligned with the async API: construction goes through + * `await ShieldedTokenSimulator.create(...)` and every circuit method is awaited. + * * @template P - The private state type, fixed to ShieldedTokenPrivateState. * @template L - The ledger type, fixed to Contract.Ledger. */ @@ -45,27 +55,40 @@ export class ShieldedTokenSimulator /** * @description Initializes the mock contract. */ - constructor( + private constructor( + contract: MockShielded, + circuitContext: CircuitContext, + ) { + this.contract = contract; + this.circuitContext = circuitContext; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Constructs a simulator, deploying the mock contract to fresh + * in-memory state. + */ + static async create( nonce: Uint8Array, name: Maybe, symbol: Maybe, decimals: bigint, - ) { - this.contract = new MockShielded( + ): Promise { + const contract = new MockShielded( ShieldedTokenWitnesses, ); const { currentPrivateState, currentContractState, currentZswapLocalState, - } = this.contract.initialState( + } = contract.initialState( createConstructorContext({}, '0'.repeat(64)), nonce, name, symbol, decimals, ); - this.circuitContext = { + const circuitContext: CircuitContext = { currentPrivateState, currentZswapLocalState, originalState: currentContractState, @@ -74,7 +97,7 @@ export class ShieldedTokenSimulator sampleContractAddress(), ), }; - this.contractAddress = this.circuitContext.transactionContext.address; + return new ShieldedTokenSimulator(contract, circuitContext); } /** @@ -105,7 +128,7 @@ export class ShieldedTokenSimulator * @description Returns the token name. * @returns The token name. */ - public name(): Maybe { + public async name(): Promise> { return this.contract.impureCircuits.name(this.circuitContext).result; } @@ -113,7 +136,7 @@ export class ShieldedTokenSimulator * @description Returns the symbol of the token. * @returns The token name. */ - public symbol(): Maybe { + public async symbol(): Promise> { return this.contract.impureCircuits.symbol(this.circuitContext).result; } @@ -121,7 +144,7 @@ export class ShieldedTokenSimulator * @description Returns the number of decimals used to get its user representation. * @returns The account's token balance. */ - public decimals(): bigint { + public async decimals(): Promise { return this.contract.impureCircuits.decimals(this.circuitContext).result; } @@ -129,15 +152,15 @@ export class ShieldedTokenSimulator * @description Returns the value of tokens in existence. * @returns The total supply of tokens. */ - public totalSupply(): bigint { + public async totalSupply(): Promise { return this.contract.impureCircuits.totalSupply(this.circuitContext).result; } - public mint( + public async mint( recipient: Either, amount: bigint, sender?: CoinPublicKey, - ): CircuitResults { + ): Promise> { const res = this.contract.impureCircuits.mint( { ...this.circuitContext, @@ -153,11 +176,11 @@ export class ShieldedTokenSimulator return res; } - public burn( + public async burn( coin: CoinInfo, amount: bigint, sender?: CoinPublicKey, - ): CircuitResults { + ): Promise> { const res = this.contract.impureCircuits.burn( { ...this.circuitContext, diff --git a/contracts/src/multisig/test/Forwarder.test.ts b/contracts/src/multisig/test/Forwarder.test.ts index 931c3432..e9fa63eb 100644 --- a/contracts/src/multisig/test/Forwarder.test.ts +++ b/contracts/src/multisig/test/Forwarder.test.ts @@ -28,87 +28,92 @@ function makeCoin(color: Uint8Array, value: bigint, nonce?: Uint8Array) { describe('ForwarderShielded module', () => { describe('initialization', () => { - it('should initialize on construction when isInit is true', () => { - expect( - () => new MockForwarderShieldedSimulator(SHIELDED_PARENT, true), - ).not.toThrow(); + it('should initialize on construction when isInit is true', async () => { + await MockForwarderShieldedSimulator.create(SHIELDED_PARENT, true); }); - it('should fail initialization with a zero parent', () => { - expect( - () => new MockForwarderShieldedSimulator(SHIELDED_ZERO, true), - ).toThrow('ForwarderShielded: zero parent'); + it('should fail initialization with a zero parent', async () => { + await expect( + MockForwarderShieldedSimulator.create(SHIELDED_ZERO, true), + ).rejects.toThrow('ForwarderShielded: zero parent'); }); - it('should store the coin-public-key parent in the left arm', () => { - const mock = new MockForwarderShieldedSimulator(SHIELDED_PARENT, true); - const parent = mock.getParent(); + it('should store the coin-public-key parent in the left arm', async () => { + const mock = await MockForwarderShieldedSimulator.create( + SHIELDED_PARENT, + true, + ); + const parent = await mock.getParent(); expect(parent.is_left).toBe(true); expect(parent.left).toEqual(SHIELDED_PARENT); }); }); describe('init guard', () => { - it('should fail deposit when not initialized', () => { - const mock = new MockForwarderShieldedSimulator(SHIELDED_PARENT, false); - expect(() => mock.deposit(makeCoin(COLOR, AMOUNT))).toThrow( + it('should fail deposit when not initialized', async () => { + const mock = await MockForwarderShieldedSimulator.create( + SHIELDED_PARENT, + false, + ); + await expect(mock.deposit(makeCoin(COLOR, AMOUNT))).rejects.toThrow( 'ForwarderShielded: contract not initialized', ); }); }); describe('deposit', () => { - it('should accept a shielded deposit and forward it', () => { - const mock = new MockForwarderShieldedSimulator(SHIELDED_PARENT, true); - expect(() => mock.deposit(makeCoin(COLOR, AMOUNT))).not.toThrow(); + it('should accept a shielded deposit and forward it', async () => { + const mock = await MockForwarderShieldedSimulator.create( + SHIELDED_PARENT, + true, + ); + await mock.deposit(makeCoin(COLOR, AMOUNT)); }); }); }); describe('ForwarderUnshielded module', () => { describe('initialization', () => { - it('should initialize on construction when isInit is true', () => { - expect( - () => new MockForwarderUnshieldedSimulator(UNSHIELDED_PARENT, true), - ).not.toThrow(); + it('should initialize on construction when isInit is true', async () => { + await MockForwarderUnshieldedSimulator.create(UNSHIELDED_PARENT, true); }); - it('should fail initialization with a zero parent', () => { - expect( - () => new MockForwarderUnshieldedSimulator(UNSHIELDED_ZERO, true), - ).toThrow('ForwarderUnshielded: zero parent'); + it('should fail initialization with a zero parent', async () => { + await expect( + MockForwarderUnshieldedSimulator.create(UNSHIELDED_ZERO, true), + ).rejects.toThrow('ForwarderUnshielded: zero parent'); }); - it('should store the user-address parent in the right arm', () => { - const mock = new MockForwarderUnshieldedSimulator( + it('should store the user-address parent in the right arm', async () => { + const mock = await MockForwarderUnshieldedSimulator.create( UNSHIELDED_PARENT, true, ); - const parent = mock.getParent(); + const parent = await mock.getParent(); expect(parent.is_left).toBe(false); expect(parent.right).toEqual(UNSHIELDED_PARENT); }); }); describe('init guard', () => { - it('should fail deposit when not initialized', () => { - const mock = new MockForwarderUnshieldedSimulator( + it('should fail deposit when not initialized', async () => { + const mock = await MockForwarderUnshieldedSimulator.create( UNSHIELDED_PARENT, false, ); - expect(() => mock.deposit(COLOR, AMOUNT)).toThrow( + await expect(mock.deposit(COLOR, AMOUNT)).rejects.toThrow( 'ForwarderUnshielded: contract not initialized', ); }); }); describe('deposit', () => { - it('should accept an unshielded deposit and forward it', () => { - const mock = new MockForwarderUnshieldedSimulator( + it('should accept an unshielded deposit and forward it', async () => { + const mock = await MockForwarderUnshieldedSimulator.create( UNSHIELDED_PARENT, true, ); - expect(() => mock.deposit(COLOR, AMOUNT)).not.toThrow(); + await mock.deposit(COLOR, AMOUNT); }); }); }); diff --git a/contracts/src/multisig/test/ForwarderPrivate.test.ts b/contracts/src/multisig/test/ForwarderPrivate.test.ts index 8b890195..92ffc2e6 100644 --- a/contracts/src/multisig/test/ForwarderPrivate.test.ts +++ b/contracts/src/multisig/test/ForwarderPrivate.test.ts @@ -52,66 +52,66 @@ function commitment(parent: Uint8Array, opSecret: Uint8Array): Uint8Array { } /** Initialized forwarder committed to `committedBytes`, with one coin deposited. */ -function freshMock( +async function freshMock( committedBytes: Uint8Array, opSecret: Uint8Array = OP_SECRET, -): MockForwarderPrivateSimulator { - const mock = new MockForwarderPrivateSimulator( +): Promise { + const mock = await MockForwarderPrivateSimulator.create( commitment(committedBytes, opSecret), true, ); - mock.deposit(makeCoin(COLOR, AMOUNT)); + await mock.deposit(makeCoin(COLOR, AMOUNT)); return mock; } describe('ForwarderPrivate module', () => { describe('initialization', () => { - it('should initialize on construction when isInit is true', () => { + it('should initialize on construction when isInit is true', async () => { const c = commitment(PARENT_BYTES, OP_SECRET); - const mock = new MockForwarderPrivateSimulator(c, true); - expect(() => mock.deposit(makeCoin(COLOR, AMOUNT))).not.toThrow(); + const mock = await MockForwarderPrivateSimulator.create(c, true); + await mock.deposit(makeCoin(COLOR, AMOUNT)); }); - it('should fail initialization with zero commitment', () => { - expect(() => new MockForwarderPrivateSimulator(ZERO, true)).toThrow( - 'ForwarderPrivate: zero commitment', - ); + it('should fail initialization with zero commitment', async () => { + await expect( + MockForwarderPrivateSimulator.create(ZERO, true), + ).rejects.toThrow('ForwarderPrivate: zero commitment'); }); - it('should store the parent commitment after initialization', () => { + it('should store the parent commitment after initialization', async () => { const c = commitment(PARENT_BYTES, OP_SECRET); - const mock = new MockForwarderPrivateSimulator(c, true); + const mock = await MockForwarderPrivateSimulator.create(c, true); // The module is imported with a prefix only, so `_parentCommitment` is // not in the public ledger reader; read it via the getter circuit. - expect(mock.getParentCommitment()).toStrictEqual(c); + expect(await mock.getParentCommitment()).toStrictEqual(c); }); }); describe('init guard', () => { let mock: MockForwarderPrivateSimulator; - beforeEach(() => { - mock = new MockForwarderPrivateSimulator( + beforeEach(async () => { + mock = await MockForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), false, ); }); - it('should fail deposit when not initialized', () => { - expect(() => mock.deposit(makeCoin(COLOR, AMOUNT))).toThrow( + it('should fail deposit when not initialized', async () => { + await expect(mock.deposit(makeCoin(COLOR, AMOUNT))).rejects.toThrow( 'ForwarderPrivate: contract not initialized', ); }); - it('should fail drain when not initialized', () => { - expect(() => + it('should fail drain when not initialized', async () => { + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, AMOUNT, ), - ).toThrow('ForwarderPrivate: contract not initialized'); + ).rejects.toThrow('ForwarderPrivate: contract not initialized'); }); }); @@ -145,12 +145,12 @@ describe('ForwarderPrivate module', () => { describe('drain', () => { let mock: MockForwarderPrivateSimulator; - beforeEach(() => { - mock = freshMock(PARENT_BYTES); + beforeEach(async () => { + mock = await freshMock(PARENT_BYTES); }); - it('should succeed drain with correct (parent, opSecret)', () => { - const result = mock.drain( + it('should succeed drain with correct (parent, opSecret)', async () => { + const result = await mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, @@ -159,52 +159,52 @@ describe('ForwarderPrivate module', () => { expect(result.sent.value).toEqual(AMOUNT); }); - it('should fail drain with wrong parent key', () => { - expect(() => + it('should fail drain with wrong parent key', async () => { + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(WRONG_BYTES), OP_SECRET, AMOUNT, ), - ).toThrow('ForwarderPrivate: invalid parent'); + ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); - it('should fail drain with wrong opSecret', () => { - expect(() => + it('should fail drain with wrong opSecret', async () => { + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), WRONG_OP_SECRET, AMOUNT, ), - ).toThrow('ForwarderPrivate: invalid parent'); + ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); - it('should fail drain with both wrong', () => { - expect(() => + it('should fail drain with both wrong', async () => { + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(WRONG_BYTES), WRONG_OP_SECRET, AMOUNT, ), - ).toThrow('ForwarderPrivate: invalid parent'); + ).rejects.toThrow('ForwarderPrivate: invalid parent'); }); - it('should fail drain with value > coin.value', () => { - expect(() => + it('should fail drain with value > coin.value', async () => { + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, AMOUNT + 1n, ), - ).toThrow(); + ).rejects.toThrow(); }); - it('should produce no change when drain value equals coin value', () => { - const result = mock.drain( + it('should produce no change when drain value equals coin value', async () => { + const result = await mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, @@ -213,8 +213,8 @@ describe('ForwarderPrivate module', () => { expect(result.change.is_some).toBe(false); }); - it('should produce a change coin when drain value is less than coin value', () => { - const result = mock.drain( + 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, @@ -225,8 +225,8 @@ describe('ForwarderPrivate module', () => { expect(result.change.value.color).toEqual(COLOR); }); - it('should produce a sent coin of exactly value on partial drain', () => { - const result = mock.drain( + 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, @@ -239,16 +239,16 @@ 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', () => { - const mock = freshMock(PARENT_BYTES); - expect(() => + it('should reject a zero parent key', async () => { + const mock = await freshMock(PARENT_BYTES); + await expect( mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(ZERO), OP_SECRET, AMOUNT, ), - ).toThrow('ForwarderPrivate: zero parent'); + ).rejects.toThrow('ForwarderPrivate: zero parent'); }); }); @@ -263,19 +263,19 @@ describe('ForwarderPrivate module', () => { // coin-public-key recipient occurs 0 times in the published tx); not // simulator-observable, so it is asserted by the residual-surface check here. describe('drain — residual public surface (INV-12 / INV-17 / INV-25)', () => { - it('should not mutate the parent commitment on a successful drain', () => { + it('should not mutate the parent commitment on a successful drain', async () => { const c = commitment(PARENT_BYTES, OP_SECRET); - const mock = new MockForwarderPrivateSimulator(c, true); - mock.deposit(makeCoin(COLOR, AMOUNT)); + const mock = await MockForwarderPrivateSimulator.create(c, true); + await mock.deposit(makeCoin(COLOR, AMOUNT)); - const before = mock.getParentCommitment(); - mock.drain( + const before = await mock.getParentCommitment(); + await mock.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, AMOUNT, ); - const after = mock.getParentCommitment(); + const after = await mock.getParentCommitment(); expect(after).toStrictEqual(before); expect(after).toStrictEqual(c); @@ -283,19 +283,19 @@ describe('ForwarderPrivate module', () => { }); describe('property: change arithmetic', () => { - it('should preserve change.value == coin.value - drain.value on partial drain', () => { - fc.assert( - fc.property( + 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 }), - (coinVal, drainVal) => { + async (coinVal, drainVal) => { fc.pre(drainVal < coinVal); - const mock = new MockForwarderPrivateSimulator( + const mock = await MockForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), true, ); - mock.deposit(makeCoin(COLOR, coinVal)); - const result = mock.drain( + await mock.deposit(makeCoin(COLOR, coinVal)); + const result = await mock.drain( makeQualifiedCoin(COLOR, coinVal, 0n), key(PARENT_BYTES), OP_SECRET, diff --git a/contracts/src/multisig/test/ProposalManager.test.ts b/contracts/src/multisig/test/ProposalManager.test.ts index 42b35da8..b93c0d48 100644 --- a/contracts/src/multisig/test/ProposalManager.test.ts +++ b/contracts/src/multisig/test/ProposalManager.test.ts @@ -17,8 +17,8 @@ const Z_CONTRACT_RECIPIENT = utils.encodeToAddress('CONTRACT_RECIPIENT'); let contract: ProposalManagerSimulator; describe('ProposalManager', () => { - beforeEach(() => { - contract = new ProposalManagerSimulator(); + beforeEach(async () => { + contract = await ProposalManagerSimulator.create(); }); describe('recipient helpers (pure)', () => { @@ -89,25 +89,25 @@ describe('ProposalManager', () => { }); describe('_createProposal', () => { - it('should create a proposal and return id', () => { + it('should create a proposal and return id', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); expect(id).toEqual(1n); }); - it('should create sequential proposal ids', () => { + it('should create sequential proposal ids', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id1 = contract._createProposal(recipient, COLOR, AMOUNT); - const id2 = contract._createProposal(recipient, COLOR2, AMOUNT2); + const id1 = await contract._createProposal(recipient, COLOR, AMOUNT); + const id2 = await contract._createProposal(recipient, COLOR2, AMOUNT2); expect(id1).toEqual(1n); expect(id2).toEqual(2n); }); - it('should store proposal data correctly', () => { + it('should store proposal data correctly', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); - const proposal = contract.getProposal(id); + const proposal = await contract.getProposal(id); expect(proposal.to.kind).toEqual(RecipientKind.ShieldedUser); expect(proposal.to.address).toEqual(Z_RECIPIENT.bytes); expect(proposal.color).toEqual(COLOR); @@ -115,241 +115,259 @@ describe('ProposalManager', () => { expect(proposal.status).toEqual(ProposalStatus.Active); }); - it('should store contract recipient correctly', () => { + it('should store contract recipient correctly', async () => { const recipient = contract.contractRecipient(Z_CONTRACT_RECIPIENT); - const id = contract._createProposal(recipient, COLOR2, AMOUNT2); + const id = await contract._createProposal(recipient, COLOR2, AMOUNT2); - const proposal = contract.getProposal(id); + const proposal = await contract.getProposal(id); expect(proposal.to.kind).toEqual(RecipientKind.Contract); expect(proposal.to.address).toEqual(Z_CONTRACT_RECIPIENT.bytes); expect(proposal.color).toEqual(COLOR2); expect(proposal.amount).toEqual(AMOUNT2); }); - it('should fail with zero amount', () => { + it('should fail with zero amount', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - expect(() => { - contract._createProposal(recipient, COLOR, 0n); - }).toThrow('ProposalManager: zero amount'); + await expect( + contract._createProposal(recipient, COLOR, 0n), + ).rejects.toThrow('ProposalManager: zero amount'); }); }); describe('assertProposalExists', () => { - it('should pass for existing proposal', () => { + it('should pass for existing proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - expect(() => contract.assertProposalExists(id)).not.toThrow(); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract.assertProposalExists(id); }); - it('should fail for non-existing proposal', () => { - expect(() => { - contract.assertProposalExists(999n); - }).toThrow('ProposalManager: proposal not found'); + it('should fail for non-existing proposal', async () => { + await expect(contract.assertProposalExists(999n)).rejects.toThrow( + 'ProposalManager: proposal not found', + ); }); }); describe('assertProposalActive', () => { - it('should pass for active proposal', () => { + it('should pass for active proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - expect(() => contract.assertProposalActive(id)).not.toThrow(); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract.assertProposalActive(id); }); - it('should fail for non-existing proposal', () => { - expect(() => { - contract.assertProposalActive(999n); - }).toThrow('ProposalManager: proposal not found'); + it('should fail for non-existing proposal', async () => { + await expect(contract.assertProposalActive(999n)).rejects.toThrow( + 'ProposalManager: proposal not found', + ); }); - it('should fail for cancelled proposal', () => { + it('should fail for cancelled proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._cancelProposal(id); - expect(() => { - contract.assertProposalActive(id); - }).toThrow('ProposalManager: proposal not active'); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._cancelProposal(id); + await expect(contract.assertProposalActive(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); - it('should fail for executed proposal', () => { + it('should fail for executed proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._markExecuted(id); - expect(() => { - contract.assertProposalActive(id); - }).toThrow('ProposalManager: proposal not active'); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._markExecuted(id); + await expect(contract.assertProposalActive(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); }); describe('_cancelProposal', () => { - it('should cancel an active proposal', () => { + it('should cancel an active proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); - contract._cancelProposal(id); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Cancelled); + await contract._cancelProposal(id); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Cancelled, + ); }); - it('should preserve proposal data after cancellation', () => { + it('should preserve proposal data after cancellation', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); - contract._cancelProposal(id); - const proposal = contract.getProposal(id); + await contract._cancelProposal(id); + const proposal = await contract.getProposal(id); expect(proposal.to.address).toEqual(Z_RECIPIENT.bytes); expect(proposal.color).toEqual(COLOR); expect(proposal.amount).toEqual(AMOUNT); }); - it('should fail for non-existing proposal', () => { - expect(() => { - contract._cancelProposal(999n); - }).toThrow('ProposalManager: proposal not found'); + it('should fail for non-existing proposal', async () => { + await expect(contract._cancelProposal(999n)).rejects.toThrow( + 'ProposalManager: proposal not found', + ); }); - it('should fail for already cancelled proposal', () => { + it('should fail for already cancelled proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._cancelProposal(id); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._cancelProposal(id); - expect(() => { - contract._cancelProposal(id); - }).toThrow('ProposalManager: proposal not active'); + await expect(contract._cancelProposal(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); - it('should fail for executed proposal', () => { + it('should fail for executed proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._markExecuted(id); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._markExecuted(id); - expect(() => { - contract._cancelProposal(id); - }).toThrow('ProposalManager: proposal not active'); + await expect(contract._cancelProposal(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); }); describe('_markExecuted', () => { - it('should mark an active proposal as executed', () => { + it('should mark an active proposal as executed', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); - contract._markExecuted(id); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Executed); + await contract._markExecuted(id); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Executed, + ); }); - it('should fail for non-existing proposal', () => { - expect(() => { - contract._markExecuted(999n); - }).toThrow('ProposalManager: proposal not found'); + it('should fail for non-existing proposal', async () => { + await expect(contract._markExecuted(999n)).rejects.toThrow( + 'ProposalManager: proposal not found', + ); }); - it('should fail for already executed proposal', () => { + it('should fail for already executed proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._markExecuted(id); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._markExecuted(id); - expect(() => { - contract._markExecuted(id); - }).toThrow('ProposalManager: proposal not active'); + await expect(contract._markExecuted(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); - it('should fail for cancelled proposal', () => { + it('should fail for cancelled proposal', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - contract._cancelProposal(id); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + await contract._cancelProposal(id); - expect(() => { - contract._markExecuted(id); - }).toThrow('ProposalManager: proposal not active'); + await expect(contract._markExecuted(id)).rejects.toThrow( + 'ProposalManager: proposal not active', + ); }); }); describe('view circuits', () => { let proposalId: bigint; - beforeEach(() => { + beforeEach(async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - proposalId = contract._createProposal(recipient, COLOR, AMOUNT); + proposalId = await contract._createProposal(recipient, COLOR, AMOUNT); }); - it('getProposal should return full proposal', () => { - const proposal = contract.getProposal(proposalId); + it('getProposal should return full proposal', async () => { + const proposal = await contract.getProposal(proposalId); expect(proposal.to.kind).toEqual(RecipientKind.ShieldedUser); expect(proposal.color).toEqual(COLOR); expect(proposal.amount).toEqual(AMOUNT); expect(proposal.status).toEqual(ProposalStatus.Active); }); - it('getProposalRecipient should return recipient', () => { - const recipient = contract.getProposalRecipient(proposalId); + it('getProposalRecipient should return recipient', async () => { + const recipient = await contract.getProposalRecipient(proposalId); expect(recipient.kind).toEqual(RecipientKind.ShieldedUser); expect(recipient.address).toEqual(Z_RECIPIENT.bytes); }); - it('getProposalAmount should return amount', () => { - expect(contract.getProposalAmount(proposalId)).toEqual(AMOUNT); + it('getProposalAmount should return amount', async () => { + expect(await contract.getProposalAmount(proposalId)).toEqual(AMOUNT); }); - it('getProposalColor should return color', () => { - expect(contract.getProposalColor(proposalId)).toEqual(COLOR); + it('getProposalColor should return color', async () => { + expect(await contract.getProposalColor(proposalId)).toEqual(COLOR); }); - it('getProposalStatus should return status', () => { - expect(contract.getProposalStatus(proposalId)).toEqual( + it('getProposalStatus should return status', async () => { + expect(await contract.getProposalStatus(proposalId)).toEqual( ProposalStatus.Active, ); }); - it('all view circuits should fail for non-existing proposal', () => { + it('all view circuits should fail for non-existing proposal', async () => { const badId = 999n; - expect(() => contract.getProposal(badId)).toThrow( + await expect(contract.getProposal(badId)).rejects.toThrow( 'ProposalManager: proposal not found', ); - expect(() => contract.getProposalRecipient(badId)).toThrow( + await expect(contract.getProposalRecipient(badId)).rejects.toThrow( 'ProposalManager: proposal not found', ); - expect(() => contract.getProposalAmount(badId)).toThrow( + await expect(contract.getProposalAmount(badId)).rejects.toThrow( 'ProposalManager: proposal not found', ); - expect(() => contract.getProposalColor(badId)).toThrow( + await expect(contract.getProposalColor(badId)).rejects.toThrow( 'ProposalManager: proposal not found', ); - expect(() => contract.getProposalStatus(badId)).toThrow( + await expect(contract.getProposalStatus(badId)).rejects.toThrow( 'ProposalManager: proposal not found', ); }); }); describe('lifecycle transitions', () => { - it('should handle create -> cancel flow', () => { + it('should handle create -> cancel flow', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Active); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Active, + ); - contract._cancelProposal(id); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Cancelled); + await contract._cancelProposal(id); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Cancelled, + ); }); - it('should handle create -> execute flow', () => { + it('should handle create -> execute flow', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id = contract._createProposal(recipient, COLOR, AMOUNT); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Active); + const id = await contract._createProposal(recipient, COLOR, AMOUNT); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Active, + ); - contract._markExecuted(id); - expect(contract.getProposalStatus(id)).toEqual(ProposalStatus.Executed); + await contract._markExecuted(id); + expect(await contract.getProposalStatus(id)).toEqual( + ProposalStatus.Executed, + ); }); - it('should handle multiple proposals independently', () => { + it('should handle multiple proposals independently', async () => { const recipient = contract.shieldedUserRecipient(Z_RECIPIENT); - const id1 = contract._createProposal(recipient, COLOR, AMOUNT); - const id2 = contract._createProposal(recipient, COLOR2, AMOUNT2); + const id1 = await contract._createProposal(recipient, COLOR, AMOUNT); + const id2 = await contract._createProposal(recipient, COLOR2, AMOUNT2); - contract._cancelProposal(id1); + await contract._cancelProposal(id1); - expect(contract.getProposalStatus(id1)).toEqual(ProposalStatus.Cancelled); - expect(contract.getProposalStatus(id2)).toEqual(ProposalStatus.Active); + expect(await contract.getProposalStatus(id1)).toEqual( + ProposalStatus.Cancelled, + ); + expect(await contract.getProposalStatus(id2)).toEqual( + ProposalStatus.Active, + ); - contract._markExecuted(id2); - expect(contract.getProposalStatus(id2)).toEqual(ProposalStatus.Executed); + await contract._markExecuted(id2); + expect(await contract.getProposalStatus(id2)).toEqual( + ProposalStatus.Executed, + ); }); }); }); diff --git a/contracts/src/multisig/test/ShieldedMultiSig.test.ts b/contracts/src/multisig/test/ShieldedMultiSig.test.ts index 6fb97363..d1ee95b8 100644 --- a/contracts/src/multisig/test/ShieldedMultiSig.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSig.test.ts @@ -10,12 +10,12 @@ const COLOR = new Uint8Array(32).fill(1); const AMOUNT = 1000n; const PROPOSAL_AMOUNT = 400n; -const [SIGNER1, Z_SIGNER1] = utils.generateEitherPubKeyPair('SIGNER1'); -const [SIGNER2, Z_SIGNER2] = utils.generateEitherPubKeyPair('SIGNER2'); -const [SIGNER3, Z_SIGNER3] = utils.generateEitherPubKeyPair('SIGNER3'); +const [, Z_SIGNER1] = utils.generateEitherPubKeyPair('SIGNER1'); +const [, Z_SIGNER2] = utils.generateEitherPubKeyPair('SIGNER2'); +const [, Z_SIGNER3] = utils.generateEitherPubKeyPair('SIGNER3'); const SIGNERS = [Z_SIGNER1, Z_SIGNER2, Z_SIGNER3]; -const [_NON_SIGNER, Z_NON_SIGNER] = utils.generateEitherPubKeyPair('OTHER'); +const [, Z_NON_SIGNER] = utils.generateEitherPubKeyPair('OTHER'); const [, Z_RECIPIENT_PK] = utils.generatePubKeyPair('RECIPIENT'); function makeRecipient(pk: { bytes: Uint8Array }): { @@ -41,121 +41,125 @@ let multisig: ShieldedMultiSigSimulator; describe('ShieldedMultiSig', () => { describe('constructor', () => { - it('should initialize with signers and threshold', () => { - multisig = new ShieldedMultiSigSimulator(SIGNERS, THRESHOLD); - expect(multisig.getSignerCount()).toEqual(BigInt(SIGNERS.length)); - expect(multisig.getThreshold()).toEqual(THRESHOLD); + it('should initialize with signers and threshold', async () => { + multisig = await ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); + expect(await multisig.getSignerCount()).toEqual(BigInt(SIGNERS.length)); + expect(await multisig.getThreshold()).toEqual(THRESHOLD); }); - it('should register all signers', () => { - multisig = new ShieldedMultiSigSimulator(SIGNERS, THRESHOLD); + it('should register all signers', async () => { + multisig = await ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); for (const signer of SIGNERS) { - expect(multisig.isSigner(signer)).toEqual(true); + expect(await multisig.isSigner(signer)).toEqual(true); } }); - it('should reject non-signers', () => { - multisig = new ShieldedMultiSigSimulator(SIGNERS, THRESHOLD); - expect(multisig.isSigner(Z_NON_SIGNER)).toEqual(false); + it('should reject non-signers', async () => { + multisig = await ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); + expect(await multisig.isSigner(Z_NON_SIGNER)).toEqual(false); }); - it('should fail with zero threshold', () => { - expect(() => { - new ShieldedMultiSigSimulator(SIGNERS, 0n); - }).toThrow('SignerManager: threshold must be > 0'); + it('should fail with zero threshold', async () => { + await expect( + ShieldedMultiSigSimulator.create(SIGNERS, 0n), + ).rejects.toThrow('SignerManager: threshold must be > 0'); }); - it('should fail with threshold exceeding signer count', () => { - expect(() => { - new ShieldedMultiSigSimulator(SIGNERS, 4n); - }).toThrow('SignerManager: threshold exceeds signer count'); + it('should fail with threshold exceeding signer count', async () => { + await expect( + ShieldedMultiSigSimulator.create(SIGNERS, 4n), + ).rejects.toThrow('SignerManager: threshold exceeds signer count'); }); }); describe('when initialized', () => { - beforeEach(() => { - multisig = new ShieldedMultiSigSimulator(SIGNERS, THRESHOLD); + beforeEach(async () => { + multisig = await ShieldedMultiSigSimulator.create(SIGNERS, THRESHOLD); }); describe('deposit', () => { - it('should accept deposits', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); - expect(multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); + it('should accept deposits', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); }); - it('should accumulate deposits', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(1))); - multisig.deposit(makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(2))); - expect(multisig.getTokenBalance(COLOR)).toEqual(AMOUNT * 2n); + it('should accumulate deposits', async () => { + await multisig.deposit( + makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(1)), + ); + await multisig.deposit( + makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(2)), + ); + expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT * 2n); }); - it('should track received total', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); - expect(multisig.getReceivedTotal(COLOR)).toEqual(AMOUNT); + it('should track received total', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + expect(await multisig.getReceivedTotal(COLOR)).toEqual(AMOUNT); }); }); describe('createShieldedProposal', () => { - it('should allow signer to create proposal', () => { + it('should allow signer to create proposal', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); expect(id).toEqual(1n); }); - it('should store proposal data correctly', () => { + it('should store proposal data correctly', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - const proposal = multisig.getProposal(id); + 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', () => { + it('should fail for non-signer', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - expect(() => { + await expect( multisig - .as(_NON_SIGNER) - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - }).toThrow('SignerManager: not a signer'); + .as('OTHER') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), + ).rejects.toThrow('SignerManager: not a signer'); }); - it('should fail with zero amount', () => { + it('should fail with zero amount', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - expect(() => { - multisig.as(SIGNER1).createShieldedProposal(to, COLOR, 0n); - }).toThrow('ProposalManager: zero amount'); + await expect( + multisig.as('SIGNER1').createShieldedProposal(to, COLOR, 0n), + ).rejects.toThrow('ProposalManager: zero amount'); }); - it('should reject UnshieldedUser recipient kind', () => { + it('should reject UnshieldedUser recipient kind', async () => { const to = { kind: RecipientKind.UnshieldedUser, address: Z_RECIPIENT_PK.bytes, }; - expect(() => { + await expect( multisig - .as(SIGNER1) - .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - }).toThrow( + .as('SIGNER1') + .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT), + ).rejects.toThrow( 'ShieldedMultiSig: recipient must be a shielded user or contract', ); }); - it('should accept Contract recipient kind', () => { + it('should accept Contract recipient kind', async () => { const to = { kind: RecipientKind.Contract, address: new Uint8Array(32).fill(7), }; - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); expect(id).toEqual(1n); - expect(multisig.getProposalRecipient(id).kind).toEqual( + expect((await multisig.getProposalRecipient(id)).kind).toEqual( RecipientKind.Contract, ); }); @@ -164,134 +168,134 @@ describe('ShieldedMultiSig', () => { describe('approveProposal', () => { let proposalId: bigint; - beforeEach(() => { + beforeEach(async () => { const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = multisig - .as(SIGNER1) + proposalId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); }); - it('should allow signer to approve', () => { - multisig.as(SIGNER1).approveProposal(proposalId); + it('should allow signer to approve', async () => { + await multisig.as('SIGNER1').approveProposal(proposalId); expect( - multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), ).toEqual(true); - expect(multisig.getApprovalCount(proposalId)).toEqual(1n); + expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); }); - it('should allow multiple signers to approve', () => { - multisig.as(SIGNER1).approveProposal(proposalId); - multisig.as(SIGNER2).approveProposal(proposalId); - expect(multisig.getApprovalCount(proposalId)).toEqual(2n); + 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', () => { - expect(() => { - multisig.as(_NON_SIGNER).approveProposal(proposalId); - }).toThrow('SignerManager: not a signer'); + 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', () => { - multisig.as(SIGNER1).approveProposal(proposalId); - expect(() => { - multisig.as(SIGNER1).approveProposal(proposalId); - }).toThrow('Multisig: already approved'); + 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', () => { - expect(() => { - multisig.as(SIGNER1).approveProposal(999n); - }).toThrow('ProposalManager: proposal not found'); + 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', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); - multisig.as(SIGNER1).approveProposal(proposalId); - multisig.as(SIGNER2).approveProposal(proposalId); - multisig.executeShieldedProposal(proposalId); + 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); - expect(() => { - multisig.as(SIGNER3).approveProposal(proposalId); - }).toThrow('ProposalManager: proposal not active'); + await expect( + multisig.as('SIGNER3').approveProposal(proposalId), + ).rejects.toThrow('ProposalManager: proposal not active'); }); }); describe('revokeApproval', () => { let proposalId: bigint; - beforeEach(() => { + beforeEach(async () => { const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = multisig - .as(SIGNER1) + proposalId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - multisig.as(SIGNER1).approveProposal(proposalId); + await multisig.as('SIGNER1').approveProposal(proposalId); }); - it('should allow signer to revoke their approval', () => { - multisig.as(SIGNER1).revokeApproval(proposalId); + it('should allow signer to revoke their approval', async () => { + await multisig.as('SIGNER1').revokeApproval(proposalId); expect( - multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), ).toEqual(false); - expect(multisig.getApprovalCount(proposalId)).toEqual(0n); + expect(await multisig.getApprovalCount(proposalId)).toEqual(0n); }); - it('should fail for non-signer', () => { - expect(() => { - multisig.as(_NON_SIGNER).revokeApproval(proposalId); - }).toThrow('SignerManager: not a signer'); + 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', () => { - expect(() => { - multisig.as(SIGNER2).revokeApproval(proposalId); - }).toThrow('Multisig: not approved'); + 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', () => { - multisig.as(SIGNER1).revokeApproval(proposalId); - multisig.as(SIGNER1).approveProposal(proposalId); + it('should allow re-approval after revoke', async () => { + await multisig.as('SIGNER1').revokeApproval(proposalId); + await multisig.as('SIGNER1').approveProposal(proposalId); expect( - multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), + await multisig.isProposalApprovedBySigner(proposalId, Z_SIGNER1), ).toEqual(true); - expect(multisig.getApprovalCount(proposalId)).toEqual(1n); + expect(await multisig.getApprovalCount(proposalId)).toEqual(1n); }); - it('should fail for executed proposal', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); - multisig.as(SIGNER2).approveProposal(proposalId); - multisig.executeShieldedProposal(proposalId); + it('should fail for executed proposal', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + await multisig.as('SIGNER2').approveProposal(proposalId); + await multisig.executeShieldedProposal(proposalId); - expect(() => { - multisig.as(SIGNER1).revokeApproval(proposalId); - }).toThrow('ProposalManager: proposal not active'); + await expect( + multisig.as('SIGNER1').revokeApproval(proposalId), + ).rejects.toThrow('ProposalManager: proposal not active'); }); }); describe('executeShieldedProposal', () => { let proposalId: bigint; - beforeEach(() => { + beforeEach(async () => { // Fund the treasury - multisig.deposit(makeCoin(COLOR, AMOUNT)); + await multisig.deposit(makeCoin(COLOR, AMOUNT)); // Create and approve proposal to threshold const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = multisig - .as(SIGNER1) + proposalId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - multisig.as(SIGNER1).approveProposal(proposalId); - multisig.as(SIGNER2).approveProposal(proposalId); + await multisig.as('SIGNER1').approveProposal(proposalId); + await multisig.as('SIGNER2').approveProposal(proposalId); }); - it('should execute when threshold is met', () => { - multisig.executeShieldedProposal(proposalId); - expect(multisig.getProposalStatus(proposalId)).toEqual( + 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', () => { - const result = multisig.executeShieldedProposal(proposalId); + 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); @@ -299,229 +303,237 @@ describe('ShieldedMultiSig', () => { expect(result.change.value.color).toEqual(COLOR); }); - it('should return no change when sending full balance', () => { + it('should return no change when sending full balance', async () => { // Create proposal for the full amount const to = makeRecipient(Z_RECIPIENT_PK); - const fullId = multisig - .as(SIGNER1) + const fullId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, AMOUNT); - multisig.as(SIGNER1).approveProposal(fullId); - multisig.as(SIGNER2).approveProposal(fullId); + await multisig.as('SIGNER1').approveProposal(fullId); + await multisig.as('SIGNER2').approveProposal(fullId); - const result = multisig.executeShieldedProposal(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', () => { - multisig.executeShieldedProposal(proposalId); - expect(multisig.getTokenBalance(COLOR)).toEqual( + 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', () => { - multisig.executeShieldedProposal(proposalId); - expect(multisig.getSentTotal(COLOR)).toEqual(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', () => { + 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 = multisig - .as(SIGNER1) + const id2 = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, 100n); - multisig.as(SIGNER1).approveProposal(id2); + await multisig.as('SIGNER1').approveProposal(id2); - expect(() => { - multisig.executeShieldedProposal(id2); - }).toThrow('SignerManager: threshold not met'); + await expect(multisig.executeShieldedProposal(id2)).rejects.toThrow( + 'SignerManager: threshold not met', + ); }); - it('should fail for non-existing proposal', () => { - expect(() => { - multisig.executeShieldedProposal(999n); - }).toThrow('ProposalManager: proposal not found'); + 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', () => { - multisig.executeShieldedProposal(proposalId); - expect(() => { - multisig.executeShieldedProposal(proposalId); - }).toThrow('ProposalManager: proposal not active'); + 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', () => { + it('should fail with insufficient treasury balance', async () => { // Create proposal for more than treasury holds const to = makeRecipient(Z_RECIPIENT_PK); - const bigId = multisig - .as(SIGNER1) + const bigId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, AMOUNT + 1n); - multisig.as(SIGNER1).approveProposal(bigId); - multisig.as(SIGNER2).approveProposal(bigId); + await multisig.as('SIGNER1').approveProposal(bigId); + await multisig.as('SIGNER2').approveProposal(bigId); - expect(() => { - multisig.executeShieldedProposal(bigId); - }).toThrow('ShieldedTreasury: coin value insufficient'); + await expect(multisig.executeShieldedProposal(bigId)).rejects.toThrow( + 'ShieldedTreasury: coin value insufficient', + ); }); }); describe('view - approvals', () => { - it('should return false for unapproved signer', () => { + it('should return false for unapproved signer', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect(multisig.isProposalApprovedBySigner(id, Z_SIGNER1)).toEqual( - false, - ); + expect( + await multisig.isProposalApprovedBySigner(id, Z_SIGNER1), + ).toEqual(false); }); - it('should return 0 approval count for new proposal', () => { + it('should return 0 approval count for new proposal', async () => { const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); - expect(multisig.getApprovalCount(id)).toEqual(0n); + expect(await multisig.getApprovalCount(id)).toEqual(0n); }); }); describe('view - proposal delegation', () => { let proposalId: bigint; - beforeEach(() => { + beforeEach(async () => { const to = makeRecipient(Z_RECIPIENT_PK); - proposalId = multisig - .as(SIGNER1) + proposalId = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); }); - it('getProposalRecipient should return recipient', () => { - const recipient = multisig.getProposalRecipient(proposalId); + 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', () => { - expect(multisig.getProposalAmount(proposalId)).toEqual(PROPOSAL_AMOUNT); + it('getProposalAmount should return amount', async () => { + expect(await multisig.getProposalAmount(proposalId)).toEqual( + PROPOSAL_AMOUNT, + ); }); - it('getProposalColor should return color', () => { - expect(multisig.getProposalColor(proposalId)).toEqual(COLOR); + it('getProposalColor should return color', async () => { + expect(await multisig.getProposalColor(proposalId)).toEqual(COLOR); }); }); describe('view - signer manager delegation', () => { - it('getSignerCount should match initial count', () => { - expect(multisig.getSignerCount()).toEqual(BigInt(SIGNERS.length)); + it('getSignerCount should match initial count', async () => { + expect(await multisig.getSignerCount()).toEqual(BigInt(SIGNERS.length)); }); - it('getThreshold should match initial threshold', () => { - expect(multisig.getThreshold()).toEqual(THRESHOLD); + it('getThreshold should match initial threshold', async () => { + expect(await multisig.getThreshold()).toEqual(THRESHOLD); }); - it('isSigner should return true for signer', () => { - expect(multisig.isSigner(Z_SIGNER1)).toEqual(true); + it('isSigner should return true for signer', async () => { + expect(await multisig.isSigner(Z_SIGNER1)).toEqual(true); }); - it('isSigner should return false for non-signer', () => { - expect(multisig.isSigner(Z_NON_SIGNER)).toEqual(false); + it('isSigner should return false for non-signer', async () => { + expect(await multisig.isSigner(Z_NON_SIGNER)).toEqual(false); }); }); describe('view - treasury delegation', () => { - beforeEach(() => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); + beforeEach(async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); }); - it('getTokenBalance should reflect deposits', () => { - expect(multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); + it('getTokenBalance should reflect deposits', async () => { + expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); }); - it('getReceivedTotal should reflect deposits', () => { - expect(multisig.getReceivedTotal(COLOR)).toEqual(AMOUNT); + it('getReceivedTotal should reflect deposits', async () => { + expect(await multisig.getReceivedTotal(COLOR)).toEqual(AMOUNT); }); - it('getSentTotal should be 0 before any sends', () => { - expect(multisig.getSentTotal(COLOR)).toEqual(0n); + it('getSentTotal should be 0 before any sends', async () => { + expect(await multisig.getSentTotal(COLOR)).toEqual(0n); }); - it('getReceivedMinusSent should equal balance', () => { - expect(multisig.getReceivedMinusSent(COLOR)).toEqual(AMOUNT); + it('getReceivedMinusSent should equal balance', async () => { + expect(await multisig.getReceivedMinusSent(COLOR)).toEqual(AMOUNT); }); }); describe('full lifecycle', () => { - it('should handle deposit -> propose -> approve -> execute', () => { + it('should handle deposit -> propose -> approve -> execute', async () => { // Deposit - multisig.deposit(makeCoin(COLOR, AMOUNT)); - expect(multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); + await multisig.deposit(makeCoin(COLOR, AMOUNT)); + expect(await multisig.getTokenBalance(COLOR)).toEqual(AMOUNT); // Propose const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); // Approve to threshold - multisig.as(SIGNER1).approveProposal(id); - multisig.as(SIGNER2).approveProposal(id); - expect(multisig.getApprovalCount(id)).toEqual(THRESHOLD); + await multisig.as('SIGNER1').approveProposal(id); + await multisig.as('SIGNER2').approveProposal(id); + expect(await multisig.getApprovalCount(id)).toEqual(THRESHOLD); // Execute - multisig.executeShieldedProposal(id); - expect(multisig.getProposalStatus(id)).toEqual(ProposalStatus.Executed); - expect(multisig.getTokenBalance(COLOR)).toEqual( + await multisig.executeShieldedProposal(id); + expect(await multisig.getProposalStatus(id)).toEqual( + ProposalStatus.Executed, + ); + expect(await multisig.getTokenBalance(COLOR)).toEqual( AMOUNT - PROPOSAL_AMOUNT, ); - expect(multisig.getReceivedMinusSent(COLOR)).toEqual( + expect(await multisig.getReceivedMinusSent(COLOR)).toEqual( AMOUNT - PROPOSAL_AMOUNT, ); }); - it('should handle multiple proposals concurrently', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); + it('should handle multiple proposals concurrently', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); const to = makeRecipient(Z_RECIPIENT_PK); - const id1 = multisig - .as(SIGNER1) + const id1 = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, 200n); - const id2 = multisig - .as(SIGNER2) + const id2 = await multisig + .as('SIGNER2') .createShieldedProposal(to, COLOR, 300n); // Approve and execute first - multisig.as(SIGNER1).approveProposal(id1); - multisig.as(SIGNER2).approveProposal(id1); - multisig.executeShieldedProposal(id1); + await multisig.as('SIGNER1').approveProposal(id1); + await multisig.as('SIGNER2').approveProposal(id1); + await multisig.executeShieldedProposal(id1); // Approve and execute second - multisig.as(SIGNER1).approveProposal(id2); - multisig.as(SIGNER3).approveProposal(id2); - multisig.executeShieldedProposal(id2); + await multisig.as('SIGNER1').approveProposal(id2); + await multisig.as('SIGNER3').approveProposal(id2); + await multisig.executeShieldedProposal(id2); - expect(multisig.getTokenBalance(COLOR)).toEqual(AMOUNT - 200n - 300n); + expect(await multisig.getTokenBalance(COLOR)).toEqual( + AMOUNT - 200n - 300n, + ); }); - it('should handle approve -> revoke -> re-approve -> execute', () => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); + it('should handle approve -> revoke -> re-approve -> execute', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); const to = makeRecipient(Z_RECIPIENT_PK); - const id = multisig - .as(SIGNER1) + const id = await multisig + .as('SIGNER1') .createShieldedProposal(to, COLOR, PROPOSAL_AMOUNT); // Approve then revoke - multisig.as(SIGNER1).approveProposal(id); - multisig.as(SIGNER1).revokeApproval(id); - expect(multisig.getApprovalCount(id)).toEqual(0n); + await multisig.as('SIGNER1').approveProposal(id); + await multisig.as('SIGNER1').revokeApproval(id); + expect(await multisig.getApprovalCount(id)).toEqual(0n); // Re-approve with enough signers - multisig.as(SIGNER2).approveProposal(id); - multisig.as(SIGNER3).approveProposal(id); - expect(multisig.getApprovalCount(id)).toEqual(2n); + await multisig.as('SIGNER2').approveProposal(id); + await multisig.as('SIGNER3').approveProposal(id); + expect(await multisig.getApprovalCount(id)).toEqual(2n); - multisig.executeShieldedProposal(id); - expect(multisig.getProposalStatus(id)).toEqual(ProposalStatus.Executed); + 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 ebe07316..3dea44c7 100644 --- a/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSigV2.test.ts @@ -70,52 +70,60 @@ let multisig: ShieldedMultiSigV2Simulator; describe('ShieldedMultiSigV2', () => { describe('constructor', () => { - it('should initialize with 2-of-3 threshold', () => { - multisig = new ShieldedMultiSigV2Simulator( + it('should initialize with 2-of-3 threshold', async () => { + multisig = await ShieldedMultiSigV2Simulator.create( INSTANCE_SALT, SIGNER_COMMITMENTS, 2n, ); - expect(multisig.getSignerCount()).toEqual(3n); - expect(multisig.getThreshold()).toEqual(2n); + expect(await multisig.getSignerCount()).toEqual(3n); + expect(await multisig.getThreshold()).toEqual(2n); }); - it('should initialize with 1-of-3 threshold', () => { - multisig = new ShieldedMultiSigV2Simulator( + it('should initialize with 1-of-3 threshold', async () => { + multisig = await ShieldedMultiSigV2Simulator.create( INSTANCE_SALT, SIGNER_COMMITMENTS, 1n, ); - expect(multisig.getThreshold()).toEqual(1n); + expect(await multisig.getThreshold()).toEqual(1n); }); - it('should fail with zero threshold', () => { - expect(() => { - new ShieldedMultiSigV2Simulator(INSTANCE_SALT, SIGNER_COMMITMENTS, 0n); - }).toThrow('SignerManager: threshold must be > 0'); + it('should fail with zero threshold', async () => { + await expect( + ShieldedMultiSigV2Simulator.create( + INSTANCE_SALT, + SIGNER_COMMITMENTS, + 0n, + ), + ).rejects.toThrow('SignerManager: threshold must be > 0'); }); - it('should fail with threshold greater than 2', () => { - expect(() => { - new ShieldedMultiSigV2Simulator(INSTANCE_SALT, SIGNER_COMMITMENTS, 3n); - }).toThrow( + it('should fail with threshold greater than 2', async () => { + await expect( + ShieldedMultiSigV2Simulator.create( + INSTANCE_SALT, + SIGNER_COMMITMENTS, + 3n, + ), + ).rejects.toThrow( 'ShieldedMultiSigV2: threshold cannot exceed 2 (execute verifies at most 2 signatures)', ); }); - it('should register all signer commitments', () => { - multisig = new ShieldedMultiSigV2Simulator( + it('should register all signer commitments', async () => { + multisig = await ShieldedMultiSigV2Simulator.create( INSTANCE_SALT, SIGNER_COMMITMENTS, 2n, ); for (const commitment of SIGNER_COMMITMENTS) { - expect(multisig.isSigner(commitment)).toEqual(true); + expect(await multisig.isSigner(commitment)).toEqual(true); } }); - it('should reject a non-signer commitment', () => { - multisig = new ShieldedMultiSigV2Simulator( + it('should reject a non-signer commitment', async () => { + multisig = await ShieldedMultiSigV2Simulator.create( INSTANCE_SALT, SIGNER_COMMITMENTS, 2n, @@ -124,13 +132,13 @@ describe('ShieldedMultiSigV2', () => { NON_SIGNER_PK, INSTANCE_SALT, ); - expect(multisig.isSigner(unknown)).toEqual(false); + expect(await multisig.isSigner(unknown)).toEqual(false); }); }); describe('when initialized', () => { - beforeEach(() => { - multisig = new ShieldedMultiSigV2Simulator( + beforeEach(async () => { + multisig = await ShieldedMultiSigV2Simulator.create( INSTANCE_SALT, SIGNER_COMMITMENTS, 2n, @@ -138,48 +146,46 @@ describe('ShieldedMultiSigV2', () => { }); describe('view', () => { - it('getNonce should start at 0', () => { - expect(multisig.getNonce()).toEqual(0n); + it('getNonce should start at 0', async () => { + expect(await multisig.getNonce()).toEqual(0n); }); - it('getSignerCount should return 3', () => { - expect(multisig.getSignerCount()).toEqual(3n); + it('getSignerCount should return 3', async () => { + expect(await multisig.getSignerCount()).toEqual(3n); }); - it('getThreshold should match constructor arg', () => { - expect(multisig.getThreshold()).toEqual(2n); + it('getThreshold should match constructor arg', async () => { + expect(await multisig.getThreshold()).toEqual(2n); }); }); describe('deposit', () => { - it('should accept deposits without reverting', () => { - expect(() => { - multisig.deposit(makeCoin(COLOR, AMOUNT)); - }).not.toThrow(); + it('should accept deposits without reverting', async () => { + await multisig.deposit(makeCoin(COLOR, AMOUNT)); }); }); describe('execute', () => { - it('should reject duplicate signer', () => { + it('should reject duplicate signer', async () => { const to = makeRecipient(new Uint8Array(32).fill(7)); const coin = makeQualifiedCoin(COLOR, AMOUNT, 0n); - expect(() => { - multisig.execute(to, 100n, coin, [PK1, PK1], [DUMMY_SIG, DUMMY_SIG]); - }).toThrow('Multisig: duplicate signer'); + await expect( + multisig.execute(to, 100n, coin, [PK1, PK1], [DUMMY_SIG, DUMMY_SIG]), + ).rejects.toThrow('Multisig: duplicate signer'); }); - it('should reject a non-signer pubkey', () => { + it('should reject a non-signer pubkey', async () => { const to = makeRecipient(new Uint8Array(32).fill(7)); const coin = makeQualifiedCoin(COLOR, AMOUNT, 0n); - expect(() => { + await expect( multisig.execute( to, 100n, coin, [PK1, NON_SIGNER_PK], [DUMMY_SIG, DUMMY_SIG], - ); - }).toThrow('SignerManager: not a signer'); + ), + ).rejects.toThrow('SignerManager: not a signer'); }); }); }); diff --git a/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts b/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts index 240c9baf..dcf3900b 100644 --- a/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts +++ b/contracts/src/multisig/test/ShieldedMultiSigV3.test.ts @@ -50,65 +50,68 @@ let multisig: ShieldedMultiSigV3Simulator; describe('ShieldedMultiSigV3', () => { describe('constructor', () => { - it('should initialize', () => { - multisig = new ShieldedMultiSigV3Simulator( + it('should initialize', async () => { + multisig = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, SIGNER_COMMITMENTS, ); - expect(multisig.getSignerCount()).toEqual(3n); - expect(multisig.getThreshold()).toEqual(2n); + expect(await multisig.getSignerCount()).toEqual(3n); + expect(await multisig.getThreshold()).toEqual(2n); }); - it('should register all signer commitments', () => { - multisig = new ShieldedMultiSigV3Simulator( + it('should register all signer commitments', async () => { + multisig = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, SIGNER_COMMITMENTS, ); for (const commitment of SIGNER_COMMITMENTS) { - expect(multisig.isSigner(commitment)).toEqual(true); + expect(await multisig.isSigner(commitment)).toEqual(true); } }); - it('should reject a non-signer commitment', () => { - multisig = new ShieldedMultiSigV3Simulator( + it('should reject a non-signer commitment', async () => { + multisig = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, SIGNER_COMMITMENTS, ); - const unknown = multisig._calculateSignerId(NON_SIGNER_PK, INSTANCE_SALT); - expect(multisig.isSigner(unknown)).toEqual(false); + const unknown = await multisig._calculateSignerId( + NON_SIGNER_PK, + INSTANCE_SALT, + ); + expect(await multisig.isSigner(unknown)).toEqual(false); }); - it('should fail with duplicate signer commitments', () => { - expect(() => { - new ShieldedMultiSigV3Simulator( + it('should fail with duplicate signer commitments', async () => { + await expect( + ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, [COMMITMENT1, COMMITMENT1, COMMITMENT2], - ); - }).toThrow('Signer: signer already active'); + ), + ).rejects.toThrow('Signer: signer already active'); }); - it('should store token domain', () => { - multisig = new ShieldedMultiSigV3Simulator( + it('should store token domain', async () => { + multisig = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, SIGNER_COMMITMENTS, ); - expect(multisig.getTokenDomain()).toEqual(TOKEN_DOMAIN); + expect(await multisig.getTokenDomain()).toEqual(TOKEN_DOMAIN); }); }); describe('when initialized', () => { - beforeEach(() => { - multisig = new ShieldedMultiSigV3Simulator( + beforeEach(async () => { + multisig = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, @@ -117,293 +120,310 @@ describe('ShieldedMultiSigV3', () => { }); describe('view', () => { - it('getNonce should start at 0', () => { - expect(multisig.getNonce()).toEqual(0n); + it('getNonce should start at 0', async () => { + expect(await multisig.getNonce()).toEqual(0n); }); - it('getSignerCount should return 3', () => { - expect(multisig.getSignerCount()).toEqual(3n); + it('getSignerCount should return 3', async () => { + expect(await multisig.getSignerCount()).toEqual(3n); }); - it('getThreshold should match constructor arg', () => { - expect(multisig.getThreshold()).toEqual(2n); + it('getThreshold should match constructor arg', async () => { + expect(await multisig.getThreshold()).toEqual(2n); }); - it('getTokenType should return non-zero', () => { - expect(multisig.getTokenType()).not.toEqual(new Uint8Array(32)); + it('getTokenType should return non-zero', async () => { + expect(await multisig.getTokenType()).not.toEqual(new Uint8Array(32)); }); - it('getTokenType should be deterministic', () => { - expect(multisig.getTokenType()).toEqual(multisig.getTokenType()); + it('getTokenType should be deterministic', async () => { + expect(await multisig.getTokenType()).toEqual( + await multisig.getTokenType(), + ); }); }); describe('_calculateSignerId', () => { - it('should produce deterministic commitments', () => { - const c1 = multisig._calculateSignerId(PK1, INSTANCE_SALT); - const c2 = multisig._calculateSignerId(PK1, INSTANCE_SALT); + it('should produce deterministic commitments', async () => { + const c1 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); + const c2 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); expect(c1).toEqual(c2); }); - it('should produce different commitments for different keys', () => { - const c1 = multisig._calculateSignerId(PK1, INSTANCE_SALT); - const c2 = multisig._calculateSignerId(PK2, INSTANCE_SALT); + it('should produce different commitments for different keys', async () => { + const c1 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); + const c2 = await multisig._calculateSignerId(PK2, INSTANCE_SALT); expect(c1).not.toEqual(c2); }); - it('should produce different commitments for different salts', () => { + it('should produce different commitments for different salts', async () => { const salt2 = new Uint8Array(32).fill(0xcc); - const c1 = multisig._calculateSignerId(PK1, INSTANCE_SALT); - const c2 = multisig._calculateSignerId(PK1, salt2); + const c1 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); + const c2 = await multisig._calculateSignerId(PK1, salt2); expect(c1).not.toEqual(c2); }); - it('should match registered commitments', () => { - expect(multisig._calculateSignerId(PK1, INSTANCE_SALT)).toEqual( + it('should match registered commitments', async () => { + expect(await multisig._calculateSignerId(PK1, INSTANCE_SALT)).toEqual( COMMITMENT1, ); - expect(multisig._calculateSignerId(PK2, INSTANCE_SALT)).toEqual( + expect(await multisig._calculateSignerId(PK2, INSTANCE_SALT)).toEqual( COMMITMENT2, ); - expect(multisig._calculateSignerId(PK3, INSTANCE_SALT)).toEqual( + expect(await multisig._calculateSignerId(PK3, INSTANCE_SALT)).toEqual( COMMITMENT3, ); }); }); describe('mint', () => { - it('should mint to a user recipient with signers 0 and 1', () => { - expect(() => { - multisig.mint( - 100n, - USER_RECIPIENT, - [PK1, PK2], - [DUMMY_SIG, DUMMY_SIG], - ); - }).not.toThrow(); + it('should mint to a user recipient with signers 0 and 1', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); }); - it('should mint to a user recipient with signers 0 and 2', () => { - expect(() => { - multisig.mint( - 100n, - USER_RECIPIENT, - [PK1, PK3], - [DUMMY_SIG, DUMMY_SIG], - ); - }).not.toThrow(); + it('should mint to a user recipient with signers 0 and 2', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK3], + [DUMMY_SIG, DUMMY_SIG], + ); }); - it('should mint to a user recipient with signers 1 and 2', () => { - expect(() => { - multisig.mint( - 100n, - USER_RECIPIENT, - [PK2, PK3], - [DUMMY_SIG, DUMMY_SIG], - ); - }).not.toThrow(); + it('should mint to a user recipient with signers 1 and 2', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK2, PK3], + [DUMMY_SIG, DUMMY_SIG], + ); }); - it('should mint to a contract recipient', () => { - expect(() => { - multisig.mint( - 100n, - CONTRACT_RECIPIENT, - [PK1, PK2], - [DUMMY_SIG, DUMMY_SIG], - ); - }).not.toThrow(); + it('should mint to a contract recipient', async () => { + await multisig.mint( + 100n, + CONTRACT_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); }); - it('should reject duplicate signer', () => { - expect(() => { + it('should reject duplicate signer', async () => { + await expect( multisig.mint( 100n, USER_RECIPIENT, [PK1, PK1], [DUMMY_SIG, DUMMY_SIG], - ); - }).toThrow('Multisig: duplicate signer'); + ), + ).rejects.toThrow('Multisig: duplicate signer'); }); - it('should reject a non-signer pubkey', () => { - expect(() => { + it('should reject a non-signer pubkey', async () => { + await expect( multisig.mint( 100n, USER_RECIPIENT, [PK1, NON_SIGNER_PK], [DUMMY_SIG, DUMMY_SIG], - ); - }).toThrow('Signer: not a signer'); + ), + ).rejects.toThrow('Signer: not a signer'); }); - it('should increment nonce after mint', () => { - expect(multisig.getNonce()).toEqual(0n); - multisig.mint(100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - expect(multisig.getNonce()).toEqual(1n); + it('should increment nonce after mint', async () => { + expect(await multisig.getNonce()).toEqual(0n); + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + expect(await multisig.getNonce()).toEqual(1n); }); - it('should increment nonce on each mint', () => { - multisig.mint(100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - multisig.mint(200n, USER_RECIPIENT, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); - multisig.mint( + it('should increment nonce on each mint', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + await multisig.mint( + 200n, + USER_RECIPIENT, + [PK1, PK3], + [DUMMY_SIG, DUMMY_SIG], + ); + await multisig.mint( 300n, CONTRACT_RECIPIENT, [PK2, PK3], [DUMMY_SIG, DUMMY_SIG], ); - expect(multisig.getNonce()).toEqual(3n); + expect(await multisig.getNonce()).toEqual(3n); }); - it('should accept zero amount', () => { - expect(() => { - multisig.mint(0n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + it('should accept zero amount', async () => { + await multisig.mint( + 0n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); }); - it('should prevent replay by incrementing nonce', () => { - multisig.mint(100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); + it('should prevent replay by incrementing nonce', async () => { + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); // Second mint with same params succeeds because nonce is different // (stub ver doesn't actually check signatures) - expect(() => { - multisig.mint( - 100n, - USER_RECIPIENT, - [PK1, PK2], - [DUMMY_SIG, DUMMY_SIG], - ); - }).not.toThrow(); - expect(multisig.getNonce()).toEqual(2n); + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + expect(await multisig.getNonce()).toEqual(2n); }); }); describe('burn', () => { - it('should burn with valid coin and signers 0 and 1', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + 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', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + 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', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 100n, [PK2, PK3], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + 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', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 50n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + 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', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 0n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).not.toThrow(); + 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 reject duplicate signer', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK1], [DUMMY_SIG, DUMMY_SIG]); - }).toThrow('Multisig: duplicate signer'); + it('should reject duplicate signer', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await expect( + multisig.burn(coin, 100n, [PK1, PK1], [DUMMY_SIG, DUMMY_SIG]), + ).rejects.toThrow('Multisig: duplicate signer'); }); - it('should reject a non-signer pubkey', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 100n); - expect(() => { + it('should reject a non-signer pubkey', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 100n); + await expect( multisig.burn( coin, 100n, [PK1, NON_SIGNER_PK], [DUMMY_SIG, DUMMY_SIG], - ); - }).toThrow('Signer: not a signer'); + ), + ).rejects.toThrow('Signer: not a signer'); }); - it('should reject wrong token color', () => { + it('should reject wrong token color', async () => { const wrongColor = new Uint8Array(32).fill(0xde); const coin = makeQualifiedCoin(wrongColor, 100n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).toThrow('Multisig: coin not from this contract'); + await expect( + multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]), + ).rejects.toThrow('Multisig: coin not from this contract'); }); - it('should reject insufficient coin value', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 10n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).toThrow('Multisig: insufficient coin value'); + it('should reject insufficient coin value', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 10n); + await expect( + multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]), + ).rejects.toThrow('Multisig: insufficient coin value'); }); - it('should reject when amount exceeds value by 1', () => { - const coin = makeQualifiedCoin(multisig.getTokenType(), 99n); - expect(() => { - multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - }).toThrow('Multisig: insufficient coin value'); + it('should reject when amount exceeds value by 1', async () => { + const coin = makeQualifiedCoin(await multisig.getTokenType(), 99n); + await expect( + multisig.burn(coin, 100n, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]), + ).rejects.toThrow('Multisig: insufficient coin value'); }); - it('should share nonce across mint and burn', () => { - multisig.mint(100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - expect(multisig.getNonce()).toEqual(1n); + 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(multisig.getTokenType(), 100n); - multisig.burn(coin, 50n, [PK1, PK3], [DUMMY_SIG, DUMMY_SIG]); - expect(multisig.getNonce()).toEqual(2n); + 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', () => { - it('should isolate signers across instances with different salts', () => { + it('should isolate signers across instances with different salts', async () => { const salt2 = new Uint8Array(32).fill(0xcc); - const c1 = multisig._calculateSignerId(PK1, INSTANCE_SALT); - const c2 = multisig._calculateSignerId(PK1, salt2); + const c1 = await multisig._calculateSignerId(PK1, INSTANCE_SALT); + const c2 = await multisig._calculateSignerId(PK1, salt2); expect(c1).not.toEqual(c2); }); - it('should derive different token types with different domains', () => { + it('should derive different token types with different domains', async () => { const altDomain = new Uint8Array(32); Buffer.from('alt:token:').copy(altDomain); - const alt = new ShieldedMultiSigV3Simulator( + const alt = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, altDomain, SIGNER_COMMITMENTS, ); - expect(multisig.getTokenType()).not.toEqual(alt.getTokenType()); + expect(await multisig.getTokenType()).not.toEqual( + await alt.getTokenType(), + ); }); }); describe('nonce', () => { - it('should start at 0', () => { - expect(multisig.getNonce()).toEqual(0n); + it('should start at 0', async () => { + expect(await multisig.getNonce()).toEqual(0n); }); - it('should increment monotonically', () => { + it('should increment monotonically', async () => { for (let i = 0; i < 5; i++) { - multisig.mint(1n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - expect(multisig.getNonce()).toEqual(BigInt(i + 1)); + await multisig.mint( + 1n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + expect(await multisig.getNonce()).toEqual(BigInt(i + 1)); } }); }); describe('cross-instance replay', () => { - it('should derive different message hashes for different instances', () => { - const instance2 = new ShieldedMultiSigV3Simulator( + it('should derive different message hashes for different instances', async () => { + const instance2 = await ShieldedMultiSigV3Simulator.create( INSTANCE_SALT, INIT_COIN_NONCE, TOKEN_DOMAIN, @@ -413,16 +433,21 @@ describe('ShieldedMultiSigV3', () => { // With stub verification, both succeed independently. // Once real ECDSA is available, a signature produced for one // instance's message hash must not validate against the other's. - multisig.mint(100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG]); - instance2.mint( + await multisig.mint( + 100n, + USER_RECIPIENT, + [PK1, PK2], + [DUMMY_SIG, DUMMY_SIG], + ); + await instance2.mint( 100n, USER_RECIPIENT, [PK1, PK2], [DUMMY_SIG, DUMMY_SIG], ); - expect(multisig.getNonce()).toEqual(1n); - expect(instance2.getNonce()).toEqual(1n); + expect(await multisig.getNonce()).toEqual(1n); + expect(await instance2.getNonce()).toEqual(1n); }); }); }); diff --git a/contracts/src/multisig/test/ShieldedTreasury.test.ts b/contracts/src/multisig/test/ShieldedTreasury.test.ts index 01fe3281..6295e7e2 100644 --- a/contracts/src/multisig/test/ShieldedTreasury.test.ts +++ b/contracts/src/multisig/test/ShieldedTreasury.test.ts @@ -23,120 +23,126 @@ function makeCoin( let treasury: ShieldedTreasurySimulator; describe('ShieldedTreasury', () => { - beforeEach(() => { - treasury = new ShieldedTreasurySimulator(); + beforeEach(async () => { + treasury = await ShieldedTreasurySimulator.create(); }); describe('initial state', () => { - it('should return 0 balance for unknown color', () => { - expect(treasury.getTokenBalance(COLOR)).toEqual(0n); + it('should return 0 balance for unknown color', async () => { + expect(await treasury.getTokenBalance(COLOR)).toEqual(0n); }); - it('should return 0 received total for unknown color', () => { - expect(treasury.getReceivedTotal(COLOR)).toEqual(0n); + it('should return 0 received total for unknown color', async () => { + expect(await treasury.getReceivedTotal(COLOR)).toEqual(0n); }); - it('should return 0 sent total for unknown color', () => { - expect(treasury.getSentTotal(COLOR)).toEqual(0n); + it('should return 0 sent total for unknown color', async () => { + expect(await treasury.getSentTotal(COLOR)).toEqual(0n); }); - it('should return 0 receivedMinusSent for unknown color', () => { - expect(treasury.getReceivedMinusSent(COLOR)).toEqual(0n); + it('should return 0 receivedMinusSent for unknown color', async () => { + expect(await treasury.getReceivedMinusSent(COLOR)).toEqual(0n); }); }); describe('_deposit', () => { - it('should deposit and update balance', () => { - treasury._deposit(makeCoin(COLOR, AMOUNT)); - expect(treasury.getTokenBalance(COLOR)).toEqual(AMOUNT); + it('should deposit and update balance', async () => { + await treasury._deposit(makeCoin(COLOR, AMOUNT)); + expect(await treasury.getTokenBalance(COLOR)).toEqual(AMOUNT); }); - it('should track received total', () => { - treasury._deposit(makeCoin(COLOR, AMOUNT)); - expect(treasury.getReceivedTotal(COLOR)).toEqual(AMOUNT); + it('should track received total', async () => { + await treasury._deposit(makeCoin(COLOR, AMOUNT)); + expect(await treasury.getReceivedTotal(COLOR)).toEqual(AMOUNT); }); - it('should accumulate multiple deposits', () => { - treasury._deposit(makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(1))); - treasury._deposit(makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(2))); - expect(treasury.getTokenBalance(COLOR)).toEqual(AMOUNT * 2n); - expect(treasury.getReceivedTotal(COLOR)).toEqual(AMOUNT * 2n); + it('should accumulate multiple deposits', async () => { + await treasury._deposit( + makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(1)), + ); + await treasury._deposit( + makeCoin(COLOR, AMOUNT, new Uint8Array(32).fill(2)), + ); + expect(await treasury.getTokenBalance(COLOR)).toEqual(AMOUNT * 2n); + expect(await treasury.getReceivedTotal(COLOR)).toEqual(AMOUNT * 2n); }); - it('should track balances per color independently', () => { - treasury._deposit(makeCoin(COLOR, AMOUNT)); - treasury._deposit(makeCoin(COLOR2, AMOUNT * 2n)); - expect(treasury.getTokenBalance(COLOR)).toEqual(AMOUNT); - expect(treasury.getTokenBalance(COLOR2)).toEqual(AMOUNT * 2n); + it('should track balances per color independently', async () => { + await treasury._deposit(makeCoin(COLOR, AMOUNT)); + await treasury._deposit(makeCoin(COLOR2, AMOUNT * 2n)); + expect(await treasury.getTokenBalance(COLOR)).toEqual(AMOUNT); + expect(await treasury.getTokenBalance(COLOR2)).toEqual(AMOUNT * 2n); }); - it('should allow zero value deposit', () => { - treasury._deposit(makeCoin(COLOR, 0n)); - expect(treasury.getTokenBalance(COLOR)).toEqual(0n); - expect(treasury.getReceivedTotal(COLOR)).toEqual(0n); + it('should allow zero value deposit', async () => { + await treasury._deposit(makeCoin(COLOR, 0n)); + expect(await treasury.getTokenBalance(COLOR)).toEqual(0n); + expect(await treasury.getReceivedTotal(COLOR)).toEqual(0n); }); - it('should maintain receivedMinusSent consistency', () => { - treasury._deposit(makeCoin(COLOR, AMOUNT)); - expect(treasury.getReceivedMinusSent(COLOR)).toEqual(AMOUNT); + it('should maintain receivedMinusSent consistency', async () => { + await treasury._deposit(makeCoin(COLOR, AMOUNT)); + expect(await treasury.getReceivedMinusSent(COLOR)).toEqual(AMOUNT); }); }); describe('_send', () => { - beforeEach(() => { - treasury._deposit(makeCoin(COLOR, AMOUNT)); + beforeEach(async () => { + await treasury._deposit(makeCoin(COLOR, AMOUNT)); }); - it('should send partial amount', () => { - treasury._send(Z_RECIPIENT, COLOR, 400n); - expect(treasury.getTokenBalance(COLOR)).toEqual(AMOUNT - 400n); + it('should send partial amount', async () => { + await treasury._send(Z_RECIPIENT, COLOR, 400n); + expect(await treasury.getTokenBalance(COLOR)).toEqual(AMOUNT - 400n); }); - it('should send full balance', () => { - treasury._send(Z_RECIPIENT, COLOR, AMOUNT); - expect(treasury.getTokenBalance(COLOR)).toEqual(0n); + it('should send full balance', async () => { + await treasury._send(Z_RECIPIENT, COLOR, AMOUNT); + expect(await treasury.getTokenBalance(COLOR)).toEqual(0n); }); - it('should track sent total', () => { - treasury._send(Z_RECIPIENT, COLOR, 400n); - expect(treasury.getSentTotal(COLOR)).toEqual(400n); + it('should track sent total', async () => { + await treasury._send(Z_RECIPIENT, COLOR, 400n); + expect(await treasury.getSentTotal(COLOR)).toEqual(400n); }); - it('should maintain receivedMinusSent after send', () => { - treasury._send(Z_RECIPIENT, COLOR, 400n); - expect(treasury.getReceivedMinusSent(COLOR)).toEqual(AMOUNT - 400n); + it('should maintain receivedMinusSent after send', async () => { + await treasury._send(Z_RECIPIENT, COLOR, 400n); + expect(await treasury.getReceivedMinusSent(COLOR)).toEqual(AMOUNT - 400n); }); - it('should fail with insufficient balance', () => { - expect(() => { - treasury._send(Z_RECIPIENT, COLOR, AMOUNT + 1n); - }).toThrow('ShieldedTreasury: coin value insufficient'); + it('should fail with insufficient balance', async () => { + await expect( + treasury._send(Z_RECIPIENT, COLOR, AMOUNT + 1n), + ).rejects.toThrow('ShieldedTreasury: coin value insufficient'); }); - it('should fail for unknown color', () => { - expect(() => { - treasury._send(Z_RECIPIENT, COLOR2, 1n); - }).toThrow('ShieldedTreasury: no balance'); + it('should fail for unknown color', async () => { + await expect(treasury._send(Z_RECIPIENT, COLOR2, 1n)).rejects.toThrow( + 'ShieldedTreasury: no balance', + ); }); }); describe('accounting consistency', () => { - it('should keep receivedMinusSent equal to balance', () => { - treasury._deposit(makeCoin(COLOR, 500n)); - treasury._send(Z_RECIPIENT, COLOR, 200n); - treasury._deposit(makeCoin(COLOR, 300n, new Uint8Array(32).fill(3))); - - const balance = treasury.getTokenBalance(COLOR); - const rms = treasury.getReceivedMinusSent(COLOR); + it('should keep receivedMinusSent equal to balance', async () => { + await treasury._deposit(makeCoin(COLOR, 500n)); + await treasury._send(Z_RECIPIENT, COLOR, 200n); + await treasury._deposit( + makeCoin(COLOR, 300n, new Uint8Array(32).fill(3)), + ); + + const balance = await treasury.getTokenBalance(COLOR); + const rms = await treasury.getReceivedMinusSent(COLOR); expect(balance).toEqual(600n); expect(rms).toEqual(600n); }); - it('should accumulate sent total across sends', () => { - treasury._deposit(makeCoin(COLOR, 1000n)); - treasury._send(Z_RECIPIENT, COLOR, 200n); - treasury._send(Z_RECIPIENT, COLOR, 300n); - expect(treasury.getSentTotal(COLOR)).toEqual(500n); + it('should accumulate sent total across sends', async () => { + await treasury._deposit(makeCoin(COLOR, 1000n)); + await treasury._send(Z_RECIPIENT, COLOR, 200n); + await treasury._send(Z_RECIPIENT, COLOR, 300n); + expect(await treasury.getSentTotal(COLOR)).toEqual(500n); }); }); }); diff --git a/contracts/src/multisig/test/Signer.test.ts b/contracts/src/multisig/test/Signer.test.ts index d915db18..da5af332 100644 --- a/contracts/src/multisig/test/Signer.test.ts +++ b/contracts/src/multisig/test/Signer.test.ts @@ -16,9 +16,9 @@ let contract: SignerSimulator; describe('Signer', () => { describe('when not initialized', () => { - beforeEach(() => { + beforeEach(async () => { const isNotInit = false; - contract = new SignerSimulator(SIGNERS, 0n, isNotInit); + contract = await SignerSimulator.create(SIGNERS, 0n, isNotInit); }); const circuitsRequiringInit: [string, unknown[]][] = [ @@ -28,349 +28,359 @@ describe('Signer', () => { ['getThreshold', []], ]; - it.each(circuitsRequiringInit)('%s should fail', (circuitName, args) => { - expect(() => { + it.each( + circuitsRequiringInit, + )('%s should fail', async (circuitName, args) => { + await expect( ( contract[circuitName as keyof SignerSimulator] as ( ...a: unknown[] - ) => unknown - )(...args); - }).toThrow('Signer: contract not initialized'); + ) => Promise + )(...args), + ).rejects.toThrow('Signer: contract not initialized'); }); - it('isSigner should succeed (no init guard)', () => { - expect(contract.isSigner(SIGNER)).toEqual(false); + it('isSigner should succeed (no init guard)', async () => { + expect(await contract.isSigner(SIGNER)).toEqual(false); }); }); describe('initialization', () => { - it('should fail with a threshold of zero', () => { - expect(() => { - new SignerSimulator(SIGNERS, 0n, IS_INIT); - }).toThrow('Signer: threshold must not be zero'); + it('should fail with a threshold of zero', async () => { + await expect( + SignerSimulator.create(SIGNERS, 0n, IS_INIT), + ).rejects.toThrow('Signer: threshold must not be zero'); }); - it('should fail when threshold exceeds signer count', () => { - expect(() => { - new SignerSimulator(SIGNERS, BigInt(SIGNERS.length) + 1n, IS_INIT); - }).toThrow('Signer: threshold exceeds signer count'); + it('should fail when threshold exceeds signer count', async () => { + await expect( + SignerSimulator.create(SIGNERS, BigInt(SIGNERS.length) + 1n, IS_INIT), + ).rejects.toThrow('Signer: threshold exceeds signer count'); }); - it('should fail with duplicate signers', () => { + it('should fail with duplicate signers', async () => { const duplicateSigners = [SIGNER, SIGNER, SIGNER2]; - expect(() => { - new SignerSimulator(duplicateSigners, THRESHOLD, IS_INIT); - }).toThrow('Signer: signer already active'); + await expect( + SignerSimulator.create(duplicateSigners, THRESHOLD, IS_INIT), + ).rejects.toThrow('Signer: signer already active'); }); - it('should initialize with threshold equal to signer count', () => { - const contract = new SignerSimulator( + it('should initialize with threshold equal to signer count', async () => { + const contract = await SignerSimulator.create( SIGNERS, BigInt(SIGNERS.length), IS_INIT, ); - expect(contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); + expect(await contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); }); - it('should initialize', () => { - expect(() => { - contract = new SignerSimulator(SIGNERS, THRESHOLD, IS_INIT); - }).not.toThrow(); + it('should initialize', async () => { + contract = await SignerSimulator.create(SIGNERS, THRESHOLD, IS_INIT); - expect(contract.getThreshold()).toEqual(THRESHOLD); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); - expect(() => { - for (let i = 0; i < SIGNERS.length; i++) { - contract.assertSigner(SIGNERS[i]); - } - }).not.toThrow(); + expect(await contract.getThreshold()).toEqual(THRESHOLD); + expect(await contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); + for (let i = 0; i < SIGNERS.length; i++) { + await contract.assertSigner(SIGNERS[i]); + } }); - it('should fail when initialized twice', () => { - contract = new SignerSimulator(SIGNERS, THRESHOLD, IS_INIT); - expect(() => { - contract.initialize(SIGNERS, THRESHOLD); - }).toThrow('Signer: contract already initialized'); + it('should fail when initialized twice', async () => { + contract = await SignerSimulator.create(SIGNERS, THRESHOLD, IS_INIT); + await expect(contract.initialize(SIGNERS, THRESHOLD)).rejects.toThrow( + 'Signer: contract already initialized', + ); }); }); - beforeEach(() => { - contract = new SignerSimulator(SIGNERS, THRESHOLD, IS_INIT); + beforeEach(async () => { + contract = await SignerSimulator.create(SIGNERS, THRESHOLD, IS_INIT); }); describe('assertSigner', () => { - it('should pass with good signer', () => { - expect(() => contract.assertSigner(SIGNER)).not.toThrow(); + it('should pass with good signer', async () => { + await contract.assertSigner(SIGNER); }); - it('should fail with bad signer', () => { - expect(() => { - contract.assertSigner(OTHER); - }).toThrow('Signer: not a signer'); + it('should fail with bad signer', async () => { + await expect(contract.assertSigner(OTHER)).rejects.toThrow( + 'Signer: not a signer', + ); }); }); describe('assertThresholdMet', () => { - it('should pass when approvals equal threshold', () => { - expect(() => contract.assertThresholdMet(THRESHOLD)).not.toThrow(); + it('should pass when approvals equal threshold', async () => { + await contract.assertThresholdMet(THRESHOLD); }); - it('should pass when approvals exceed threshold', () => { - expect(() => contract.assertThresholdMet(THRESHOLD + 1n)).not.toThrow(); + it('should pass when approvals exceed threshold', async () => { + await contract.assertThresholdMet(THRESHOLD + 1n); }); - it('should fail when approvals are below threshold', () => { - expect(() => { - contract.assertThresholdMet(THRESHOLD - 1n); - }).toThrow('Signer: threshold not met'); + it('should fail when approvals are below threshold', async () => { + await expect(contract.assertThresholdMet(THRESHOLD - 1n)).rejects.toThrow( + 'Signer: threshold not met', + ); }); - it('should fail with zero approvals', () => { - expect(() => { - contract.assertThresholdMet(0n); - }).toThrow('Signer: threshold not met'); + it('should fail with zero approvals', async () => { + await expect(contract.assertThresholdMet(0n)).rejects.toThrow( + 'Signer: threshold not met', + ); }); }); describe('getSignerCount', () => { - it('should return the initial signer count', () => { - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); + it('should return the initial signer count', async () => { + expect(await contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); }); - it('should reflect additions', () => { - contract._addSigner(OTHER); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) + 1n); + it('should reflect additions', async () => { + await contract._addSigner(OTHER); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) + 1n, + ); }); - it('should reflect removals', () => { - contract._removeSigner(SIGNER3); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) - 1n); + it('should reflect removals', async () => { + await contract._removeSigner(SIGNER3); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) - 1n, + ); }); }); describe('getThreshold', () => { - it('should return the initial threshold', () => { - expect(contract.getThreshold()).toEqual(THRESHOLD); + it('should return the initial threshold', async () => { + expect(await contract.getThreshold()).toEqual(THRESHOLD); }); - it('should reflect _changeThreshold', () => { - contract._changeThreshold(3n); - expect(contract.getThreshold()).toEqual(3n); + it('should reflect _changeThreshold', async () => { + await contract._changeThreshold(3n); + expect(await contract.getThreshold()).toEqual(3n); }); - it('should reflect _setThreshold', () => { - contract._setThreshold(1n); - expect(contract.getThreshold()).toEqual(1n); + it('should reflect _setThreshold', async () => { + await contract._setThreshold(1n); + expect(await contract.getThreshold()).toEqual(1n); }); }); describe('isSigner', () => { - it('should return true for an active signer', () => { - expect(contract.isSigner(SIGNER)).toEqual(true); + it('should return true for an active signer', async () => { + expect(await contract.isSigner(SIGNER)).toEqual(true); }); - it('should return false for a non-signer', () => { - expect(contract.isSigner(OTHER)).toEqual(false); + it('should return false for a non-signer', async () => { + expect(await contract.isSigner(OTHER)).toEqual(false); }); }); describe('_addSigner', () => { - it('should add a new signer', () => { - contract._addSigner(OTHER); + it('should add a new signer', async () => { + await contract._addSigner(OTHER); - expect(contract.isSigner(OTHER)).toEqual(true); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) + 1n); + expect(await contract.isSigner(OTHER)).toEqual(true); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) + 1n, + ); }); - it('should fail when adding an existing signer', () => { - contract._addSigner(OTHER); + it('should fail when adding an existing signer', async () => { + await contract._addSigner(OTHER); - expect(() => { - contract._addSigner(OTHER); - }).toThrow('Signer: signer already active'); + await expect(contract._addSigner(OTHER)).rejects.toThrow( + 'Signer: signer already active', + ); }); - it('should add multiple new signers', () => { - contract._addSigner(OTHER); - contract._addSigner(OTHER2); + it('should add multiple new signers', async () => { + await contract._addSigner(OTHER); + await contract._addSigner(OTHER2); - expect(contract.isSigner(OTHER)).toEqual(true); - expect(contract.isSigner(OTHER2)).toEqual(true); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) + 2n); + expect(await contract.isSigner(OTHER)).toEqual(true); + expect(await contract.isSigner(OTHER2)).toEqual(true); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) + 2n, + ); }); - it('should allow re-adding a previously removed signer', () => { - expect(contract.isSigner(SIGNER)).toEqual(true); + it('should allow re-adding a previously removed signer', async () => { + expect(await contract.isSigner(SIGNER)).toEqual(true); - contract._removeSigner(SIGNER); - expect(contract.isSigner(SIGNER)).toEqual(false); + await contract._removeSigner(SIGNER); + expect(await contract.isSigner(SIGNER)).toEqual(false); - contract._addSigner(SIGNER); - expect(contract.isSigner(SIGNER)).toEqual(true); + await contract._addSigner(SIGNER); + expect(await contract.isSigner(SIGNER)).toEqual(true); }); }); describe('_removeSigner', () => { - it('should remove an existing signer', () => { - contract._removeSigner(SIGNER3); + it('should remove an existing signer', async () => { + await contract._removeSigner(SIGNER3); - expect(contract.isSigner(SIGNER3)).toEqual(false); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) - 1n); + expect(await contract.isSigner(SIGNER3)).toEqual(false); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) - 1n, + ); }); - it('should fail when removing a non-signer', () => { - expect(() => { - contract._removeSigner(OTHER); - }).toThrow('Signer: not a signer'); + it('should fail when removing a non-signer', async () => { + await expect(contract._removeSigner(OTHER)).rejects.toThrow( + 'Signer: not a signer', + ); }); - it('should fail when removal would breach threshold', () => { - contract._removeSigner(SIGNER3); + it('should fail when removal would breach threshold', async () => { + await contract._removeSigner(SIGNER3); - expect(() => { - contract._removeSigner(SIGNER2); - }).toThrow('Signer: removal would breach threshold'); + await expect(contract._removeSigner(SIGNER2)).rejects.toThrow( + 'Signer: removal would breach threshold', + ); }); - it('should allow removal after threshold is lowered', () => { - contract._changeThreshold(1n); - contract._removeSigner(SIGNER3); - contract._removeSigner(SIGNER2); + it('should allow removal after threshold is lowered', async () => { + await contract._changeThreshold(1n); + await contract._removeSigner(SIGNER3); + await contract._removeSigner(SIGNER2); - expect(contract.getSignerCount()).toEqual(1n); - expect(contract.isSigner(SIGNER)).toEqual(true); - expect(contract.isSigner(SIGNER2)).toEqual(false); - expect(contract.isSigner(SIGNER3)).toEqual(false); + expect(await contract.getSignerCount()).toEqual(1n); + expect(await contract.isSigner(SIGNER)).toEqual(true); + expect(await contract.isSigner(SIGNER2)).toEqual(false); + expect(await contract.isSigner(SIGNER3)).toEqual(false); }); - it('should keep signer count in sync after multiple add/remove operations', () => { - contract._addSigner(OTHER); - contract._addSigner(OTHER2); - contract._removeSigner(SIGNER3); - contract._removeSigner(OTHER); + it('should keep signer count in sync after multiple add/remove operations', async () => { + await contract._addSigner(OTHER); + await contract._addSigner(OTHER2); + await contract._removeSigner(SIGNER3); + await contract._removeSigner(OTHER); - expect(contract.getSignerCount()).toEqual(3n); - expect(contract.isSigner(SIGNER)).toEqual(true); - expect(contract.isSigner(SIGNER2)).toEqual(true); - expect(contract.isSigner(SIGNER3)).toEqual(false); - expect(contract.isSigner(OTHER)).toEqual(false); - expect(contract.isSigner(OTHER2)).toEqual(true); + expect(await contract.getSignerCount()).toEqual(3n); + expect(await contract.isSigner(SIGNER)).toEqual(true); + expect(await contract.isSigner(SIGNER2)).toEqual(true); + expect(await contract.isSigner(SIGNER3)).toEqual(false); + expect(await contract.isSigner(OTHER)).toEqual(false); + expect(await contract.isSigner(OTHER2)).toEqual(true); }); }); describe('_changeThreshold', () => { - it('should update the threshold', () => { - contract._changeThreshold(3n); + it('should update the threshold', async () => { + await contract._changeThreshold(3n); - expect(contract.getThreshold()).toEqual(3n); + expect(await contract.getThreshold()).toEqual(3n); }); - it('should allow lowering the threshold', () => { - contract._changeThreshold(1n); + it('should allow lowering the threshold', async () => { + await contract._changeThreshold(1n); - expect(contract.getThreshold()).toEqual(1n); + expect(await contract.getThreshold()).toEqual(1n); }); - it('should fail with a threshold of zero', () => { - expect(() => { - contract._changeThreshold(0n); - }).toThrow('Signer: threshold must not be zero'); + it('should fail with a threshold of zero', async () => { + await expect(contract._changeThreshold(0n)).rejects.toThrow( + 'Signer: threshold must not be zero', + ); }); - it('should fail when threshold exceeds signer count', () => { - expect(() => { - contract._changeThreshold(BigInt(SIGNERS.length) + 1n); - }).toThrow('Signer: threshold exceeds signer count'); + it('should fail when threshold exceeds signer count', async () => { + await expect( + contract._changeThreshold(BigInt(SIGNERS.length) + 1n), + ).rejects.toThrow('Signer: threshold exceeds signer count'); }); - it('should allow threshold equal to signer count', () => { - contract._changeThreshold(BigInt(SIGNERS.length)); + it('should allow threshold equal to signer count', async () => { + await contract._changeThreshold(BigInt(SIGNERS.length)); - expect(contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); + expect(await contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); }); - it('should reflect new threshold in assertThresholdMet', () => { - contract._changeThreshold(3n); + it('should reflect new threshold in assertThresholdMet', async () => { + await contract._changeThreshold(3n); - expect(() => { - contract.assertThresholdMet(2n); - }).toThrow('Signer: threshold not met'); + await expect(contract.assertThresholdMet(2n)).rejects.toThrow( + 'Signer: threshold not met', + ); - expect(() => contract.assertThresholdMet(3n)).not.toThrow(); + await contract.assertThresholdMet(3n); }); }); describe('_setThreshold', () => { - beforeEach(() => { + beforeEach(async () => { const isNotInit = false; - contract = new SignerSimulator(SIGNERS, 0n, isNotInit); + contract = await SignerSimulator.create(SIGNERS, 0n, isNotInit); }); - it('should have an empty state', () => { - expect(contract.getPublicState()._threshold).toEqual(0n); - expect(contract.getPublicState()._signerCount).toEqual(0n); - expect(contract.getPublicState()._signers.isEmpty()).toEqual(true); + it('should have an empty state', async () => { + expect((await contract.getPublicState())._threshold).toEqual(0n); + expect((await contract.getPublicState())._signerCount).toEqual(0n); + expect((await contract.getPublicState())._signers.isEmpty()).toEqual( + true, + ); }); - it('should set threshold without signers', () => { - expect(contract.getPublicState()._threshold).toEqual(0n); + it('should set threshold without signers', async () => { + expect((await contract.getPublicState())._threshold).toEqual(0n); - contract._setThreshold(2n); - expect(contract.getPublicState()._threshold).toEqual(2n); + await contract._setThreshold(2n); + expect((await contract.getPublicState())._threshold).toEqual(2n); }); - it('should set threshold multiple times', () => { - contract._setThreshold(2n); - contract._setThreshold(3n); - expect(contract.getPublicState()._threshold).toEqual(3n); + it('should set threshold multiple times', async () => { + await contract._setThreshold(2n); + await contract._setThreshold(3n); + expect((await contract.getPublicState())._threshold).toEqual(3n); }); - it('should fail with zero threshold', () => { - expect(() => { - contract._setThreshold(0n); - }).toThrow('Signer: threshold must not be zero'); + it('should fail with zero threshold', async () => { + await expect(contract._setThreshold(0n)).rejects.toThrow( + 'Signer: threshold must not be zero', + ); }); }); describe('custom setup flow when not initialized', () => { - beforeEach(() => { + beforeEach(async () => { const isNotInit = false; - contract = new SignerSimulator(SIGNERS, 0n, isNotInit); + contract = await SignerSimulator.create(SIGNERS, 0n, isNotInit); }); - it('should have no signers by default', () => { - expect(contract.getPublicState()._signerCount).toEqual(0n); - expect(contract.isSigner(SIGNER)).toEqual(false); + it('should have no signers by default', async () => { + expect((await contract.getPublicState())._signerCount).toEqual(0n); + expect(await contract.isSigner(SIGNER)).toEqual(false); }); - it('should have zero threshold by default', () => { - expect(contract.getPublicState()._threshold).toEqual(0n); + it('should have zero threshold by default', async () => { + expect((await contract.getPublicState())._threshold).toEqual(0n); }); - it('should allow adding signers then setting threshold', () => { - contract._addSigner(SIGNER); - contract._addSigner(SIGNER2); - contract._addSigner(SIGNER3); - contract._changeThreshold(2n); + it('should allow adding signers then setting threshold', async () => { + await contract._addSigner(SIGNER); + await contract._addSigner(SIGNER2); + await contract._addSigner(SIGNER3); + await contract._changeThreshold(2n); - expect(contract.getPublicState()._signerCount).toEqual(3n); - expect(contract.getPublicState()._threshold).toEqual(2n); - expect(contract.isSigner(SIGNER)).toEqual(true); + expect((await contract.getPublicState())._signerCount).toEqual(3n); + expect((await contract.getPublicState())._threshold).toEqual(2n); + expect(await contract.isSigner(SIGNER)).toEqual(true); }); - it('should allow setting threshold then adding signers to meet it', () => { - contract._setThreshold(2n); - contract._addSigner(SIGNER); - contract._addSigner(SIGNER2); + it('should allow setting threshold then adding signers to meet it', async () => { + await contract._setThreshold(2n); + await contract._addSigner(SIGNER); + await contract._addSigner(SIGNER2); - expect(contract.getPublicState()._signerCount).toEqual(2n); - expect(contract.getPublicState()._threshold).toEqual(2n); + expect((await contract.getPublicState())._signerCount).toEqual(2n); + expect((await contract.getPublicState())._threshold).toEqual(2n); }); - it('should fail _changeThreshold before signers are added', () => { - expect(() => { - contract._changeThreshold(2n); - }).toThrow('Signer: threshold exceeds signer count'); + it('should fail _changeThreshold before signers are added', async () => { + await expect(contract._changeThreshold(2n)).rejects.toThrow( + 'Signer: threshold exceeds signer count', + ); }); }); }); diff --git a/contracts/src/multisig/test/SignerManager.test.ts b/contracts/src/multisig/test/SignerManager.test.ts index 1ead55f7..9ecd2468 100644 --- a/contracts/src/multisig/test/SignerManager.test.ts +++ b/contracts/src/multisig/test/SignerManager.test.ts @@ -18,184 +18,186 @@ let contract: SignerManagerSimulator; describe('SigningManager', () => { describe('initialization', () => { - it('should fail with a threshold of zero', () => { - expect(() => { - new SignerManagerSimulator(SIGNERS, 0n); - }).toThrow('SignerManager: threshold must be > 0'); + it('should fail with a threshold of zero', async () => { + await expect(SignerManagerSimulator.create(SIGNERS, 0n)).rejects.toThrow( + 'SignerManager: threshold must be > 0', + ); }); - it('should fail with duplicate signers', () => { + it('should fail with duplicate signers', async () => { const duplicateSigners: SignerSet = [Z_SIGNER, Z_SIGNER, Z_SIGNER2]; - expect(() => { - new SignerManagerSimulator(duplicateSigners, THRESHOLD); - }).toThrow('SignerManager: signer already active'); + await expect( + SignerManagerSimulator.create(duplicateSigners, THRESHOLD), + ).rejects.toThrow('SignerManager: signer already active'); }); - it('should initialize', () => { - expect(() => { - contract = new SignerManagerSimulator(SIGNERS, THRESHOLD); - }).to.be.ok; + it('should initialize', async () => { + contract = await SignerManagerSimulator.create(SIGNERS, THRESHOLD); // Check thresh - expect(contract.getThreshold()).toEqual(THRESHOLD); + expect(await contract.getThreshold()).toEqual(THRESHOLD); // Check signers - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); - expect(() => { - for (let i = 0; i < SIGNERS.length; i++) { - contract.assertSigner(SIGNERS[i]); - } - }).to.be.ok; + expect(await contract.getSignerCount()).toEqual(BigInt(SIGNERS.length)); + for (let i = 0; i < SIGNERS.length; i++) { + await contract.assertSigner(SIGNERS[i]); + } }); }); - beforeEach(() => { - contract = new SignerManagerSimulator(SIGNERS, THRESHOLD); + beforeEach(async () => { + contract = await SignerManagerSimulator.create(SIGNERS, THRESHOLD); }); describe('assertSigner', () => { - it('should pass with good signer', () => { - expect(() => contract.assertSigner(Z_SIGNER)).not.toThrow(); + it('should pass with good signer', async () => { + await contract.assertSigner(Z_SIGNER); }); - it('should fail with bad signer', () => { - expect(() => { - contract.assertSigner(Z_OTHER); - }).toThrow('SignerManager: not a signer'); + it('should fail with bad signer', async () => { + await expect(contract.assertSigner(Z_OTHER)).rejects.toThrow( + 'SignerManager: not a signer', + ); }); }); describe('assertThresholdMet', () => { - it('should pass when approvals equal threshold', () => { - expect(() => contract.assertThresholdMet(THRESHOLD)).not.toThrow(); + it('should pass when approvals equal threshold', async () => { + await contract.assertThresholdMet(THRESHOLD); }); - it('should pass when approvals exceed threshold', () => { - expect(() => contract.assertThresholdMet(THRESHOLD + 1n)).not.toThrow(); + it('should pass when approvals exceed threshold', async () => { + await contract.assertThresholdMet(THRESHOLD + 1n); }); - it('should fail when approvals are below threshold', () => { - expect(() => { - contract.assertThresholdMet(THRESHOLD - 1n); - }).toThrow('SignerManager: threshold not met'); + it('should fail when approvals are below threshold', async () => { + await expect(contract.assertThresholdMet(THRESHOLD - 1n)).rejects.toThrow( + 'SignerManager: threshold not met', + ); }); - it('should fail with zero approvals', () => { - expect(() => { - contract.assertThresholdMet(0n); - }).toThrow('SignerManager: threshold not met'); + it('should fail with zero approvals', async () => { + await expect(contract.assertThresholdMet(0n)).rejects.toThrow( + 'SignerManager: threshold not met', + ); }); }); describe('isSigner', () => { - it('should return true for an active signer', () => { - expect(contract.isSigner(Z_SIGNER)).toEqual(true); + it('should return true for an active signer', async () => { + expect(await contract.isSigner(Z_SIGNER)).toEqual(true); }); - it('should return false for a non-signer', () => { - expect(contract.isSigner(Z_OTHER)).toEqual(false); + it('should return false for a non-signer', async () => { + expect(await contract.isSigner(Z_OTHER)).toEqual(false); }); }); describe('_addSigner', () => { - it('should add a new signer', () => { - contract._addSigner(Z_OTHER); + it('should add a new signer', async () => { + await contract._addSigner(Z_OTHER); - expect(contract.isSigner(Z_OTHER)).toEqual(true); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) + 1n); + expect(await contract.isSigner(Z_OTHER)).toEqual(true); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) + 1n, + ); }); - it('should fail when adding an existing signer', () => { - expect(() => { - contract._addSigner(Z_SIGNER); - }).toThrow('SignerManager: signer already active'); + it('should fail when adding an existing signer', async () => { + await expect(contract._addSigner(Z_SIGNER)).rejects.toThrow( + 'SignerManager: signer already active', + ); }); - it('should add multiple new signers', () => { - contract._addSigner(Z_OTHER); - contract._addSigner(Z_OTHER2); + it('should add multiple new signers', async () => { + await contract._addSigner(Z_OTHER); + await contract._addSigner(Z_OTHER2); - expect(contract.isSigner(Z_OTHER)).toEqual(true); - expect(contract.isSigner(Z_OTHER2)).toEqual(true); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) + 2n); + expect(await contract.isSigner(Z_OTHER)).toEqual(true); + expect(await contract.isSigner(Z_OTHER2)).toEqual(true); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) + 2n, + ); }); }); describe('_removeSigner', () => { - it('should remove an existing signer', () => { - contract._removeSigner(Z_SIGNER3); + it('should remove an existing signer', async () => { + await contract._removeSigner(Z_SIGNER3); - expect(contract.isSigner(Z_SIGNER3)).toEqual(false); - expect(contract.getSignerCount()).toEqual(BigInt(SIGNERS.length) - 1n); + expect(await contract.isSigner(Z_SIGNER3)).toEqual(false); + expect(await contract.getSignerCount()).toEqual( + BigInt(SIGNERS.length) - 1n, + ); }); - it('should fail when removing a non-signer', () => { - expect(() => { - contract._removeSigner(Z_OTHER); - }).toThrow('SignerManager: not a signer'); + it('should fail when removing a non-signer', async () => { + await expect(contract._removeSigner(Z_OTHER)).rejects.toThrow( + 'SignerManager: not a signer', + ); }); - it('should fail when removal would breach threshold', () => { + it('should fail when removal would breach threshold', async () => { // Remove one signer: count goes from 3 to 2, threshold is 2 — ok - contract._removeSigner(Z_SIGNER3); + await contract._removeSigner(Z_SIGNER3); // Remove another: count would go from 2 to 1, threshold is 2 — breach - expect(() => { - contract._removeSigner(Z_SIGNER2); - }).toThrow('SignerManager: removal would breach threshold'); + await expect(contract._removeSigner(Z_SIGNER2)).rejects.toThrow( + 'SignerManager: removal would breach threshold', + ); }); - it('should allow removal after threshold is lowered', () => { - contract._changeThreshold(1n); - contract._removeSigner(Z_SIGNER3); - contract._removeSigner(Z_SIGNER2); + it('should allow removal after threshold is lowered', async () => { + await contract._changeThreshold(1n); + await contract._removeSigner(Z_SIGNER3); + await contract._removeSigner(Z_SIGNER2); - expect(contract.getSignerCount()).toEqual(1n); - expect(contract.isSigner(Z_SIGNER)).toEqual(true); - expect(contract.isSigner(Z_SIGNER2)).toEqual(false); - expect(contract.isSigner(Z_SIGNER3)).toEqual(false); + expect(await contract.getSignerCount()).toEqual(1n); + expect(await contract.isSigner(Z_SIGNER)).toEqual(true); + expect(await contract.isSigner(Z_SIGNER2)).toEqual(false); + expect(await contract.isSigner(Z_SIGNER3)).toEqual(false); }); }); describe('_changeThreshold', () => { - it('should update the threshold', () => { - contract._changeThreshold(3n); + it('should update the threshold', async () => { + await contract._changeThreshold(3n); - expect(contract.getThreshold()).toEqual(3n); + expect(await contract.getThreshold()).toEqual(3n); }); - it('should allow lowering the threshold', () => { - contract._changeThreshold(1n); + it('should allow lowering the threshold', async () => { + await contract._changeThreshold(1n); - expect(contract.getThreshold()).toEqual(1n); + expect(await contract.getThreshold()).toEqual(1n); }); - it('should fail with a threshold of zero', () => { - expect(() => { - contract._changeThreshold(0n); - }).toThrow('SignerManager: threshold must be > 0'); + it('should fail with a threshold of zero', async () => { + await expect(contract._changeThreshold(0n)).rejects.toThrow( + 'SignerManager: threshold must be > 0', + ); }); - it('should fail when threshold exceeds signer count', () => { - expect(() => { - contract._changeThreshold(BigInt(SIGNERS.length) + 1n); - }).toThrow('SignerManager: threshold exceeds signer count'); + it('should fail when threshold exceeds signer count', async () => { + await expect( + contract._changeThreshold(BigInt(SIGNERS.length) + 1n), + ).rejects.toThrow('SignerManager: threshold exceeds signer count'); }); - it('should allow threshold equal to signer count', () => { - contract._changeThreshold(BigInt(SIGNERS.length)); + it('should allow threshold equal to signer count', async () => { + await contract._changeThreshold(BigInt(SIGNERS.length)); - expect(contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); + expect(await contract.getThreshold()).toEqual(BigInt(SIGNERS.length)); }); - it('should reflect new threshold in assertThresholdMet', () => { - contract._changeThreshold(3n); + it('should reflect new threshold in assertThresholdMet', async () => { + await contract._changeThreshold(3n); - expect(() => { - contract.assertThresholdMet(2n); - }).toThrow('SignerManager: threshold not met'); + await expect(contract.assertThresholdMet(2n)).rejects.toThrow( + 'SignerManager: threshold not met', + ); - expect(() => contract.assertThresholdMet(3n)).not.toThrow(); + await contract.assertThresholdMet(3n); }); }); }); diff --git a/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts b/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts index 75a21259..ba87b3fd 100644 --- a/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderPrivate.test.ts @@ -26,25 +26,25 @@ function commitment(parent: Uint8Array, opSecret: Uint8Array): Uint8Array { } describe('ForwarderPrivate preset', () => { - it('should store the parentCommitment passed to the constructor', () => { + it('should store the parentCommitment passed to the constructor', async () => { const c = commitment(PARENT_BYTES, OP_SECRET); - const fwd = new ForwarderPrivateSimulator(c); - expect(fwd.getParentCommitment()).toEqual(c); + const fwd = await ForwarderPrivateSimulator.create(c); + expect(await fwd.getParentCommitment()).toEqual(c); }); - it('should expose deposit and forward to _deposit', () => { - const fwd = new ForwarderPrivateSimulator( + it('should expose deposit and forward to _deposit', async () => { + const fwd = await ForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), ); - expect(() => fwd.deposit(makeCoin(COLOR, AMOUNT))).not.toThrow(); + await fwd.deposit(makeCoin(COLOR, AMOUNT)); }); - it('should expose drain and forward to _drain', () => { - const fwd = new ForwarderPrivateSimulator( + it('should expose drain and forward to _drain', async () => { + const fwd = await ForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), ); - fwd.deposit(makeCoin(COLOR, AMOUNT)); - const result = fwd.drain( + await fwd.deposit(makeCoin(COLOR, AMOUNT)); + const result = await fwd.drain( makeQualifiedCoin(COLOR, AMOUNT, 0n), key(PARENT_BYTES), OP_SECRET, @@ -59,16 +59,16 @@ describe('ForwarderPrivate preset', () => { expect(c1).toEqual(c2); }); - it('should propagate the zero-commitment guard from the module', () => { - expect(() => new ForwarderPrivateSimulator(new Uint8Array(32))).toThrow( - 'ForwarderPrivate: zero commitment', - ); + it('should propagate the zero-commitment guard from the module', async () => { + await expect( + ForwarderPrivateSimulator.create(new Uint8Array(32)), + ).rejects.toThrow('ForwarderPrivate: zero commitment'); }); - it('should expose the public ledger state', () => { - const fwd = new ForwarderPrivateSimulator( + it('should expose the public ledger state', async () => { + const fwd = await ForwarderPrivateSimulator.create( commitment(PARENT_BYTES, OP_SECRET), ); - expect(fwd.getPublicState()).toBeDefined(); + expect(await fwd.getPublicState()).toBeDefined(); }); }); diff --git a/contracts/src/multisig/test/presets/ForwarderShielded.test.ts b/contracts/src/multisig/test/presets/ForwarderShielded.test.ts index 1f034b20..14445072 100644 --- a/contracts/src/multisig/test/presets/ForwarderShielded.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderShielded.test.ts @@ -16,26 +16,26 @@ function makeCoin(color: Uint8Array, value: bigint) { } describe('ForwarderShielded preset', () => { - it('should store the parent passed to the constructor in the left arm', () => { - const fwd = new ForwarderShieldedSimulator(PARENT); - const parent = fwd.getParent(); + it('should store the parent passed to the constructor in the left arm', async () => { + const fwd = await ForwarderShieldedSimulator.create(PARENT); + const parent = await fwd.getParent(); expect(parent.is_left).toBe(true); expect(parent.left).toEqual(PARENT); }); - it('should expose deposit and forward to _deposit', () => { - const fwd = new ForwarderShieldedSimulator(PARENT); - expect(() => fwd.deposit(makeCoin(COLOR, AMOUNT))).not.toThrow(); + it('should expose deposit and forward to _deposit', async () => { + const fwd = await ForwarderShieldedSimulator.create(PARENT); + await fwd.deposit(makeCoin(COLOR, AMOUNT)); }); - it('should propagate the zero-parent guard from the module', () => { - expect(() => new ForwarderShieldedSimulator(ZERO_KEY)).toThrow( + it('should propagate the zero-parent guard from the module', async () => { + await expect(ForwarderShieldedSimulator.create(ZERO_KEY)).rejects.toThrow( 'ForwarderShielded: zero parent', ); }); - it('should expose the public ledger state', () => { - const fwd = new ForwarderShieldedSimulator(PARENT); - expect(fwd.getPublicState()).toBeDefined(); + it('should expose the public ledger state', async () => { + const fwd = await ForwarderShieldedSimulator.create(PARENT); + expect(await fwd.getPublicState()).toBeDefined(); }); }); diff --git a/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts b/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts index 5d81cda3..0f3ade64 100644 --- a/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts +++ b/contracts/src/multisig/test/presets/ForwarderUnshielded.test.ts @@ -12,26 +12,26 @@ const COLOR = new Uint8Array(32).fill(1); const AMOUNT = 1000n; describe('ForwarderUnshielded preset', () => { - it('should store the parent passed to the constructor in the right arm', () => { - const fwd = new ForwarderUnshieldedSimulator(PARENT); - const parent = fwd.getParent(); + it('should store the parent passed to the constructor in the right arm', async () => { + const fwd = await ForwarderUnshieldedSimulator.create(PARENT); + const parent = await fwd.getParent(); expect(parent.is_left).toBe(false); expect(parent.right).toEqual(PARENT); }); - it('should expose deposit and forward to _deposit', () => { - const fwd = new ForwarderUnshieldedSimulator(PARENT); - expect(() => fwd.deposit(COLOR, AMOUNT)).not.toThrow(); + it('should expose deposit and forward to _deposit', async () => { + const fwd = await ForwarderUnshieldedSimulator.create(PARENT); + await fwd.deposit(COLOR, AMOUNT); }); - it('should propagate the zero-parent guard from the module', () => { - expect(() => new ForwarderUnshieldedSimulator(ZERO_ADDR)).toThrow( - 'ForwarderUnshielded: zero parent', - ); + it('should propagate the zero-parent guard from the module', async () => { + await expect( + ForwarderUnshieldedSimulator.create(ZERO_ADDR), + ).rejects.toThrow('ForwarderUnshielded: zero parent'); }); - it('should expose the public ledger state', () => { - const fwd = new ForwarderUnshieldedSimulator(PARENT); - expect(fwd.getPublicState()).toBeDefined(); + it('should expose the public ledger state', async () => { + const fwd = await ForwarderUnshieldedSimulator.create(PARENT); + expect(await fwd.getPublicState()).toBeDefined(); }); }); diff --git a/contracts/src/multisig/test/simulators/MockForwarderPrivateSimulator.ts b/contracts/src/multisig/test/simulators/MockForwarderPrivateSimulator.ts index 3c3ec47f..7f3e2db7 100644 --- a/contracts/src/multisig/test/simulators/MockForwarderPrivateSimulator.ts +++ b/contracts/src/multisig/test/simulators/MockForwarderPrivateSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -31,18 +31,23 @@ const MockForwarderPrivateSimulatorBase = createSimulator< contractArgs: (parentCommitment, isInit) => [parentCommitment, isInit], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'MockForwarderPrivate', }); export class MockForwarderPrivateSimulator extends MockForwarderPrivateSimulatorBase { - constructor( + static async create( parentCommitment: Uint8Array, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parentCommitment, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parentCommitment, isInit], + options, + ) as Promise; } public static calculateParentCommitment( @@ -52,7 +57,7 @@ export class MockForwarderPrivateSimulator extends MockForwarderPrivateSimulator return pureCircuits.calculateParentCommitment(parentAddr, opSecret); } - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } @@ -61,11 +66,11 @@ export class MockForwarderPrivateSimulator extends MockForwarderPrivateSimulator parent: ZswapCoinPublicKey, opSecret: Uint8Array, value: bigint, - ): ShieldedSendResult { + ): Promise { return this.circuits.impure.drain(coin, parent, opSecret, value); } - public getParentCommitment(): Uint8Array { + public getParentCommitment(): Promise { return this.circuits.impure.getParentCommitment(); } } diff --git a/contracts/src/multisig/test/simulators/MockForwarderShieldedSimulator.ts b/contracts/src/multisig/test/simulators/MockForwarderShieldedSimulator.ts index 81771494..dacc6336 100644 --- a/contracts/src/multisig/test/simulators/MockForwarderShieldedSimulator.ts +++ b/contracts/src/multisig/test/simulators/MockForwarderShieldedSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -30,25 +30,30 @@ const MockForwarderShieldedSimulatorBase = createSimulator< contractArgs: (parent, isInit) => [parent, isInit], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'MockForwarderShielded', }); export class MockForwarderShieldedSimulator extends MockForwarderShieldedSimulatorBase { - constructor( + static async create( parent: ZswapCoinPublicKey, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parent, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parent, isInit], + options, + ) as Promise; } - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } - public getParent(): Either { + public getParent(): Promise> { return this.circuits.impure.getParent(); } } diff --git a/contracts/src/multisig/test/simulators/MockForwarderUnshieldedSimulator.ts b/contracts/src/multisig/test/simulators/MockForwarderUnshieldedSimulator.ts index 82fa0eae..ffc3ba95 100644 --- a/contracts/src/multisig/test/simulators/MockForwarderUnshieldedSimulator.ts +++ b/contracts/src/multisig/test/simulators/MockForwarderUnshieldedSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -29,25 +29,30 @@ const MockForwarderUnshieldedSimulatorBase = createSimulator< contractArgs: (parent, isInit) => [parent, isInit], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'MockForwarderUnshielded', }); export class MockForwarderUnshieldedSimulator extends MockForwarderUnshieldedSimulatorBase { - constructor( + static async create( parent: UserAddress, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parent, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parent, isInit], + options, + ) as Promise; } - public deposit(color: Uint8Array, amount: bigint) { + public deposit(color: Uint8Array, amount: bigint): Promise<[]> { return this.circuits.impure.deposit(color, amount); } - public getParent(): Either { + public getParent(): Promise> { return this.circuits.impure.getParent(); } } diff --git a/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts b/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts index f43e676a..98c97436 100644 --- a/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts +++ b/contracts/src/multisig/test/simulators/ProposalManagerSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -35,16 +35,18 @@ const ProposalManagerSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ProposalManagerWitnesses(), + artifactName: 'MockProposalManager', }); export class ProposalManagerSimulator extends ProposalManagerSimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< ProposalManagerPrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } // Pure circuits (recipient helpers) @@ -77,11 +79,11 @@ export class ProposalManagerSimulator extends ProposalManagerSimulatorBase { } // Guards - public assertProposalExists(id: bigint) { + public assertProposalExists(id: bigint): Promise<[]> { return this.circuits.impure.assertProposalExists(id); } - public assertProposalActive(id: bigint) { + public assertProposalActive(id: bigint): Promise<[]> { return this.circuits.impure.assertProposalActive(id); } @@ -90,36 +92,36 @@ export class ProposalManagerSimulator extends ProposalManagerSimulatorBase { to: Recipient, color: Uint8Array, amount: bigint, - ): bigint { + ): Promise { return this.circuits.impure._createProposal(to, color, amount); } - public _cancelProposal(id: bigint) { + public _cancelProposal(id: bigint): Promise<[]> { return this.circuits.impure._cancelProposal(id); } - public _markExecuted(id: bigint) { + public _markExecuted(id: bigint): Promise<[]> { return this.circuits.impure._markExecuted(id); } // View - public getProposal(id: bigint): Proposal { + public getProposal(id: bigint): Promise { return this.circuits.impure.getProposal(id); } - public getProposalRecipient(id: bigint): Recipient { + public getProposalRecipient(id: bigint): Promise { return this.circuits.impure.getProposalRecipient(id); } - public getProposalAmount(id: bigint): bigint { + public getProposalAmount(id: bigint): Promise { return this.circuits.impure.getProposalAmount(id); } - public getProposalColor(id: bigint): Uint8Array { + public getProposalColor(id: bigint): Promise { return this.circuits.impure.getProposalColor(id); } - public getProposalStatus(id: bigint): number { + public getProposalStatus(id: bigint): Promise { return this.circuits.impure.getProposalStatus(id); } } diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts index f881384b..a58035f4 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type Ledger, @@ -48,22 +48,27 @@ const ShieldedMultiSigSimulatorBase = createSimulator< contractArgs: (signers, thresh) => [signers, thresh], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedMultiSigWitnesses(), + artifactName: 'ShieldedMultiSig', }); export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { - constructor( + static async create( signers: EitherPKAddress[], thresh: bigint, - options: BaseSimulatorOptions< + options: SimulatorOptions< ShieldedMultiSigPrivateState, ReturnType > = {}, - ) { - super([signers, thresh], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [signers, thresh], + options, + ) as Promise; } // Deposit - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } @@ -72,19 +77,19 @@ export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { to: Recipient, color: Uint8Array, amount: bigint, - ): bigint { + ): Promise { return this.circuits.impure.createShieldedProposal(to, color, amount); } - public approveProposal(id: bigint) { + public approveProposal(id: bigint): Promise<[]> { return this.circuits.impure.approveProposal(id); } - public revokeApproval(id: bigint) { + public revokeApproval(id: bigint): Promise<[]> { return this.circuits.impure.revokeApproval(id); } - public executeShieldedProposal(id: bigint): ShieldedSendResult { + public executeShieldedProposal(id: bigint): Promise { return this.circuits.impure.executeShieldedProposal(id); } @@ -92,67 +97,67 @@ export class ShieldedMultiSigSimulator extends ShieldedMultiSigSimulatorBase { public isProposalApprovedBySigner( id: bigint, signer: EitherPKAddress, - ): boolean { + ): Promise { return this.circuits.impure.isProposalApprovedBySigner(id, signer); } - public getApprovalCount(id: bigint): bigint { + public getApprovalCount(id: bigint): Promise { return this.circuits.impure.getApprovalCount(id); } // View - Proposals - public getProposal(id: bigint): Proposal { + public getProposal(id: bigint): Promise { return this.circuits.impure.getProposal(id); } - public getProposalRecipient(id: bigint): Recipient { + public getProposalRecipient(id: bigint): Promise { return this.circuits.impure.getProposalRecipient(id); } - public getProposalAmount(id: bigint): bigint { + public getProposalAmount(id: bigint): Promise { return this.circuits.impure.getProposalAmount(id); } - public getProposalColor(id: bigint): Uint8Array { + public getProposalColor(id: bigint): Promise { return this.circuits.impure.getProposalColor(id); } - public getProposalStatus(id: bigint): number { + public getProposalStatus(id: bigint): Promise { return this.circuits.impure.getProposalStatus(id); } // View - Treasury - public getTokenBalance(color: Uint8Array): bigint { + public getTokenBalance(color: Uint8Array): Promise { return this.circuits.impure.getTokenBalance(color); } - public getReceivedTotal(color: Uint8Array): bigint { + public getReceivedTotal(color: Uint8Array): Promise { return this.circuits.impure.getReceivedTotal(color); } - public getSentTotal(color: Uint8Array): bigint { + public getSentTotal(color: Uint8Array): Promise { return this.circuits.impure.getSentTotal(color); } - public getReceivedMinusSent(color: Uint8Array): bigint { + public getReceivedMinusSent(color: Uint8Array): Promise { return this.circuits.impure.getReceivedMinusSent(color); } // View - Signers - public getSignerCount(): bigint { + public getSignerCount(): Promise { return this.circuits.impure.getSignerCount(); } - public getThreshold(): bigint { + public getThreshold(): Promise { return this.circuits.impure.getThreshold(); } - public isSigner(account: EitherPKAddress): boolean { + public isSigner(account: EitherPKAddress): Promise { return this.circuits.impure.isSigner(account); } // Ledger access - public getLedger(): Ledger { + public getLedger(): Promise { return this.getPublicState(); } } diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts index 3e091fa5..c03078bd 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigV2Simulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type Ledger, @@ -49,19 +49,24 @@ const ShieldedMultiSigV2SimulatorBase = createSimulator< ], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedMultiSigV2Witnesses(), + artifactName: 'ShieldedMultiSigV2', }); export class ShieldedMultiSigV2Simulator extends ShieldedMultiSigV2SimulatorBase { - constructor( + static async create( instanceSalt: Uint8Array, signerCommitments: Uint8Array[], thresh: bigint, - options: BaseSimulatorOptions< + options: SimulatorOptions< ShieldedMultiSigV2PrivateState, ReturnType > = {}, - ) { - super([instanceSalt, signerCommitments, thresh], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [instanceSalt, signerCommitments, thresh], + options, + ) as Promise; } public static calculateSignerId( @@ -71,7 +76,7 @@ export class ShieldedMultiSigV2Simulator extends ShieldedMultiSigV2SimulatorBase return pureCircuits._calculateSignerId(pk, salt); } - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } @@ -81,27 +86,27 @@ export class ShieldedMultiSigV2Simulator extends ShieldedMultiSigV2SimulatorBase coin: QualifiedShieldedCoinInfo, pubkeys: Uint8Array[], signatures: Uint8Array[], - ): ShieldedSendResult { + ): Promise { return this.circuits.impure.execute(to, amount, coin, pubkeys, signatures); } - public getNonce(): bigint { + public getNonce(): Promise { return this.circuits.impure.getNonce(); } - public getSignerCount(): bigint { + public getSignerCount(): Promise { return this.circuits.impure.getSignerCount(); } - public getThreshold(): bigint { + public getThreshold(): Promise { return this.circuits.impure.getThreshold(); } - public isSigner(commitment: Uint8Array): boolean { + public isSigner(commitment: Uint8Array): Promise { return this.circuits.impure.isSigner(commitment); } - public getLedger(): Ledger { + public getLedger(): Promise { return this.getPublicState(); } } diff --git a/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts b/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts index bbd5bc28..afba649f 100644 --- a/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedMultiSigV3Simulator.ts @@ -1,8 +1,10 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { + type ContractAddress, + type Either, ledger, pureCircuits, Contract as ShieldedMultiSigV3Contract, @@ -38,26 +40,31 @@ const ShieldedMultiSigV3SimulatorBase = createSimulator< ) => [instanceSalt, initCoinNonce, tokenDomain, signerCommitments], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedMultiSigV3Witnesses(), + artifactName: 'ShieldedMultiSigV3', }); export class ShieldedMultiSigV3Simulator extends ShieldedMultiSigV3SimulatorBase { - constructor( + static async create( instanceSalt: Uint8Array, initCoinNonce: Uint8Array, tokenDomain: Uint8Array, signerCommitments: Uint8Array[], - options: BaseSimulatorOptions< + options: SimulatorOptions< ShieldedMultiSigV3PrivateState, ReturnType > = {}, - ) { - super( + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( [instanceSalt, initCoinNonce, tokenDomain, signerCommitments], options, - ); + ) as Promise; } - public _calculateSignerId(pk: Uint8Array, salt: Uint8Array): Uint8Array { + public _calculateSignerId( + pk: Uint8Array, + salt: Uint8Array, + ): Promise { return this.circuits.pure._calculateSignerId(pk, salt); } @@ -66,7 +73,7 @@ export class ShieldedMultiSigV3Simulator extends ShieldedMultiSigV3SimulatorBase recipient: Either, pubkeys: Uint8Array[], signatures: Uint8Array[], - ) { + ): Promise<[]> { return this.circuits.impure.mint(amount, recipient, pubkeys, signatures); } @@ -80,31 +87,31 @@ export class ShieldedMultiSigV3Simulator extends ShieldedMultiSigV3SimulatorBase amount: bigint, pubkeys: Uint8Array[], signatures: Uint8Array[], - ) { + ): Promise<[]> { return this.circuits.impure.burn(coin, amount, pubkeys, signatures); } - public getNonce(): bigint { + public getNonce(): Promise { return this.circuits.impure.getNonce(); } - public getTokenDomain(): Uint8Array { + public getTokenDomain(): Promise { return this.circuits.impure.getTokenDomain(); } - public getTokenType(): Uint8Array { + public getTokenType(): Promise { return this.circuits.impure.getTokenType(); } - public getSignerCount(): bigint { + public getSignerCount(): Promise { return this.circuits.impure.getSignerCount(); } - public getThreshold(): bigint { + public getThreshold(): Promise { return this.circuits.impure.getThreshold(); } - public isSigner(commitment: Uint8Array): boolean { + public isSigner(commitment: Uint8Array): Promise { return this.circuits.impure.isSigner(commitment); } } diff --git a/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts b/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts index 6cbfb61c..210972d2 100644 --- a/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts +++ b/contracts/src/multisig/test/simulators/ShieldedTreasurySimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -32,19 +32,21 @@ const ShieldedTreasurySimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ShieldedTreasuryWitnesses(), + artifactName: 'MockShieldedTreasury', }); export class ShieldedTreasurySimulator extends ShieldedTreasurySimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< ShieldedTreasuryPrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } - public _deposit(coin: ShieldedCoinInfo) { + public _deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure._deposit(coin); } @@ -56,23 +58,23 @@ export class ShieldedTreasurySimulator extends ShieldedTreasurySimulatorBase { }, color: Uint8Array, amount: bigint, - ): ShieldedSendResult { + ): Promise { return this.circuits.impure._send(recipient, color, amount); } - public getTokenBalance(color: Uint8Array): bigint { + public getTokenBalance(color: Uint8Array): Promise { return this.circuits.impure.getTokenBalance(color); } - public getReceivedTotal(color: Uint8Array): bigint { + public getReceivedTotal(color: Uint8Array): Promise { return this.circuits.impure.getReceivedTotal(color); } - public getSentTotal(color: Uint8Array): bigint { + public getSentTotal(color: Uint8Array): Promise { return this.circuits.impure.getSentTotal(color); } - public getReceivedMinusSent(color: Uint8Array): bigint { + public getReceivedMinusSent(color: Uint8Array): Promise { return this.circuits.impure.getReceivedMinusSent(color); } } diff --git a/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts b/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts index 151aee48..be5ee9aa 100644 --- a/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts +++ b/contracts/src/multisig/test/simulators/SignerManagerSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -43,54 +43,65 @@ const SignerManagerSimulatorBase = createSimulator< contractArgs: (signers, thresh) => [signers, thresh], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => SignerManagerWitnesses(), + artifactName: 'MockSignerManager', }); /** * SignerManager Simulator */ export class SignerManagerSimulator extends SignerManagerSimulatorBase { - constructor( + static async create( signers: SignerSet, thresh: bigint, - options: BaseSimulatorOptions< + options: SimulatorOptions< SignerManagerPrivateState, ReturnType > = {}, - ) { - super([signers, thresh], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [signers, thresh], + options, + ) as Promise; } - public assertSigner(caller: Either) { + public assertSigner( + caller: Either, + ): Promise<[]> { return this.circuits.impure.assertSigner(caller); } - public assertThresholdMet(approvalCount: bigint) { + public assertThresholdMet(approvalCount: bigint): Promise<[]> { return this.circuits.impure.assertThresholdMet(approvalCount); } - public getSignerCount(): bigint { + public getSignerCount(): Promise { return this.circuits.impure.getSignerCount(); } - public getThreshold(): bigint { + public getThreshold(): Promise { return this.circuits.impure.getThreshold(); } public isSigner( account: Either, - ): boolean { + ): Promise { return this.circuits.impure.isSigner(account); } - public _addSigner(signer: Either) { + public _addSigner( + signer: Either, + ): Promise<[]> { return this.circuits.impure._addSigner(signer); } - public _removeSigner(signer: Either) { + public _removeSigner( + signer: Either, + ): Promise<[]> { return this.circuits.impure._removeSigner(signer); } - public _changeThreshold(newThreshold: bigint) { + public _changeThreshold(newThreshold: bigint): Promise<[]> { return this.circuits.impure._changeThreshold(newThreshold); } } diff --git a/contracts/src/multisig/test/simulators/SignerSimulator.ts b/contracts/src/multisig/test/simulators/SignerSimulator.ts index 37d0fa82..ef3f3948 100644 --- a/contracts/src/multisig/test/simulators/SignerSimulator.ts +++ b/contracts/src/multisig/test/simulators/SignerSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -32,61 +32,66 @@ const SignerSimulatorBase = createSimulator< contractArgs: (signers, thresh, isInit) => [signers, thresh, isInit], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => SignerWitnesses(), + artifactName: 'MockSigner', }); /** * Signer Simulator */ export class SignerSimulator extends SignerSimulatorBase { - constructor( + static async create( signers: Uint8Array[], thresh: bigint, isInit: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< SignerPrivateState, ReturnType > = {}, - ) { - super([signers, thresh, isInit], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [signers, thresh, isInit], + options, + ) as Promise; } - public initialize(signers: Uint8Array[], thresh: bigint) { + public initialize(signers: Uint8Array[], thresh: bigint): Promise<[]> { return this.circuits.impure.initialize(signers, thresh); } - public assertSigner(caller: Uint8Array) { + public assertSigner(caller: Uint8Array): Promise<[]> { return this.circuits.impure.assertSigner(caller); } - public assertThresholdMet(approvalCount: bigint) { + public assertThresholdMet(approvalCount: bigint): Promise<[]> { return this.circuits.impure.assertThresholdMet(approvalCount); } - public getSignerCount(): bigint { + public getSignerCount(): Promise { return this.circuits.impure.getSignerCount(); } - public getThreshold(): bigint { + public getThreshold(): Promise { return this.circuits.impure.getThreshold(); } - public isSigner(account: Uint8Array): boolean { + public isSigner(account: Uint8Array): Promise { return this.circuits.impure.isSigner(account); } - public _addSigner(signer: Uint8Array) { + public _addSigner(signer: Uint8Array): Promise<[]> { return this.circuits.impure._addSigner(signer); } - public _removeSigner(signer: Uint8Array) { + public _removeSigner(signer: Uint8Array): Promise<[]> { return this.circuits.impure._removeSigner(signer); } - public _changeThreshold(newThreshold: bigint) { + public _changeThreshold(newThreshold: bigint): Promise<[]> { return this.circuits.impure._changeThreshold(newThreshold); } - public _setThreshold(newThreshold: bigint) { + public _setThreshold(newThreshold: bigint): Promise<[]> { return this.circuits.impure._setThreshold(newThreshold); } } diff --git a/contracts/src/multisig/test/simulators/presets/ForwarderPrivateSimulator.ts b/contracts/src/multisig/test/simulators/presets/ForwarderPrivateSimulator.ts index 2b90a5e0..8c06eb45 100644 --- a/contracts/src/multisig/test/simulators/presets/ForwarderPrivateSimulator.ts +++ b/contracts/src/multisig/test/simulators/presets/ForwarderPrivateSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { Contract as ForwarderPrivate, @@ -28,17 +28,22 @@ const ForwarderPrivateSimulatorBase = createSimulator< contractArgs: (parentCommitment) => [parentCommitment], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'ForwarderPrivate', }); export class ForwarderPrivateSimulator extends ForwarderPrivateSimulatorBase { - constructor( + static async create( parentCommitment: Uint8Array, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parentCommitment], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parentCommitment], + options, + ) as Promise; } public static calculateParentCommitment( @@ -48,7 +53,7 @@ export class ForwarderPrivateSimulator extends ForwarderPrivateSimulatorBase { return pureCircuits.calculateParentCommitment(parentAddr, opSecret); } - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } @@ -57,11 +62,11 @@ export class ForwarderPrivateSimulator extends ForwarderPrivateSimulatorBase { parent: ZswapCoinPublicKey, opSecret: Uint8Array, value: bigint, - ): ShieldedSendResult { + ): Promise { return this.circuits.impure.drain(coin, parent, opSecret, value); } - public getParentCommitment(): Uint8Array { + public getParentCommitment(): Promise { return this.circuits.impure.getParentCommitment(); } } diff --git a/contracts/src/multisig/test/simulators/presets/ForwarderShieldedSimulator.ts b/contracts/src/multisig/test/simulators/presets/ForwarderShieldedSimulator.ts index aec81b82..e34b9717 100644 --- a/contracts/src/multisig/test/simulators/presets/ForwarderShieldedSimulator.ts +++ b/contracts/src/multisig/test/simulators/presets/ForwarderShieldedSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -27,24 +27,29 @@ const ForwarderShieldedSimulatorBase = createSimulator< contractArgs: (parent) => [parent], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'ForwarderShielded', }); export class ForwarderShieldedSimulator extends ForwarderShieldedSimulatorBase { - constructor( + static async create( parent: ZswapCoinPublicKey, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parent], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parent], + options, + ) as Promise; } - public deposit(coin: ShieldedCoinInfo) { + public deposit(coin: ShieldedCoinInfo): Promise<[]> { return this.circuits.impure.deposit(coin); } - public getParent(): Either { + public getParent(): Promise> { return this.circuits.impure.getParent(); } } diff --git a/contracts/src/multisig/test/simulators/presets/ForwarderUnshieldedSimulator.ts b/contracts/src/multisig/test/simulators/presets/ForwarderUnshieldedSimulator.ts index 78dc9adb..0119fe24 100644 --- a/contracts/src/multisig/test/simulators/presets/ForwarderUnshieldedSimulator.ts +++ b/contracts/src/multisig/test/simulators/presets/ForwarderUnshieldedSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -26,24 +26,29 @@ const ForwarderUnshieldedSimulatorBase = createSimulator< contractArgs: (parent) => [parent], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => emptyWitnesses(), + artifactName: 'ForwarderUnshielded', }); export class ForwarderUnshieldedSimulator extends ForwarderUnshieldedSimulatorBase { - constructor( + static async create( parent: UserAddress, - options: BaseSimulatorOptions< + options: SimulatorOptions< EmptyPrivateState, ReturnType > = {}, - ) { - super([parent], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [parent], + options, + ) as Promise; } - public deposit(color: Uint8Array, amount: bigint) { + public deposit(color: Uint8Array, amount: bigint): Promise<[]> { return this.circuits.impure.deposit(color, amount); } - public getParent(): Either { + public getParent(): Promise> { return this.circuits.impure.getParent(); } } diff --git a/contracts/src/security/test/Initializable.test.ts b/contracts/src/security/test/Initializable.test.ts index 5f96eea9..6ac860e4 100644 --- a/contracts/src/security/test/Initializable.test.ts +++ b/contracts/src/security/test/Initializable.test.ts @@ -4,62 +4,62 @@ import { InitializableSimulator } from './simulators/InitializableSimulator.js'; let initializable: InitializableSimulator; describe('Initializable', () => { - beforeEach(() => { - initializable = new InitializableSimulator(); + beforeEach(async () => { + initializable = await InitializableSimulator.create(); }); - it('should generate the initial ledger state deterministically', () => { - const initializable2 = new InitializableSimulator(); - expect(initializable.getPublicState()).toEqual( - initializable2.getPublicState(), + it('should generate the initial ledger state deterministically', async () => { + const initializable2 = await InitializableSimulator.create(); + expect(await initializable.getPublicState()).toEqual( + await initializable2.getPublicState(), ); }); describe('initialize', () => { - it('should not be initialized', () => { + it('should not be initialized', async () => { expect( - initializable.getPublicState().Initializable__isInitialized, + (await initializable.getPublicState()).Initializable__isInitialized, ).toEqual(false); }); - it('should initialize', () => { - initializable.initialize(); + it('should initialize', async () => { + await initializable.initialize(); expect( - initializable.getPublicState().Initializable__isInitialized, + (await initializable.getPublicState()).Initializable__isInitialized, ).toEqual(true); }); }); - it('should fail when re-initialized', () => { - expect(() => { - initializable.initialize(); - initializable.initialize(); - }).toThrow('Initializable: contract already initialized'); + it('should fail when re-initialized', async () => { + await initializable.initialize(); + await expect(initializable.initialize()).rejects.toThrow( + 'Initializable: contract already initialized', + ); }); describe('assertInitialized', () => { - it('should fail when not initialized', () => { - expect(() => { - initializable.assertInitialized(); - }).toThrow('Initializable: contract not initialized'); + it('should fail when not initialized', async () => { + await expect(initializable.assertInitialized()).rejects.toThrow( + 'Initializable: contract not initialized', + ); }); - it('should not fail when initialized', () => { - initializable.initialize(); - initializable.assertInitialized(); + it('should not fail when initialized', async () => { + await initializable.initialize(); + await initializable.assertInitialized(); }); }); describe('assertNotInitialized', () => { - it('should fail when initialized', () => { - initializable.initialize(); - expect(() => { - initializable.assertNotInitialized(); - }).toThrow('Initializable: contract already initialized'); + it('should fail when initialized', async () => { + await initializable.initialize(); + await expect(initializable.assertNotInitialized()).rejects.toThrow( + 'Initializable: contract already initialized', + ); }); - it('should not fail when not initialied', () => { - initializable.assertNotInitialized(); + it('should not fail when not initialied', async () => { + await initializable.assertNotInitialized(); }); }); }); diff --git a/contracts/src/security/test/Pausable.test.ts b/contracts/src/security/test/Pausable.test.ts index 6148d804..82927336 100644 --- a/contracts/src/security/test/Pausable.test.ts +++ b/contracts/src/security/test/Pausable.test.ts @@ -4,86 +4,82 @@ import { PausableSimulator } from './simulators/PausableSimulator.js'; let pausable: PausableSimulator; describe('Pausable', () => { - beforeEach(() => { - pausable = new PausableSimulator(); + beforeEach(async () => { + pausable = await PausableSimulator.create(); }); describe('when not paused', () => { - it('should not be paused in initial state', () => { - expect(pausable.isPaused()).toBe(false); + it('should not be paused in initial state', async () => { + expect(await pausable.isPaused()).toBe(false); }); - it('should throw when calling assertPaused', () => { - expect(() => { - pausable.assertPaused(); - }).toThrow('Pausable: not paused'); + it('should throw when calling assertPaused', async () => { + await expect(pausable.assertPaused()).rejects.toThrow( + 'Pausable: not paused', + ); }); - it('should not throw when calling assertNotPaused', () => { - pausable.assertNotPaused(); + it('should not throw when calling assertNotPaused', async () => { + await pausable.assertNotPaused(); }); - it('should pause from unpaused state', () => { - pausable.pause(); - expect(pausable.isPaused()).toBe(true); + it('should pause from unpaused state', async () => { + await pausable.pause(); + expect(await pausable.isPaused()).toBe(true); }); - it('should throw when unpausing in an unpaused state', () => { - expect(() => { - pausable.unpause(); - }).toThrow('Pausable: not paused'); + it('should throw when unpausing in an unpaused state', async () => { + await expect(pausable.unpause()).rejects.toThrow('Pausable: not paused'); }); }); describe('when paused', () => { - beforeEach(() => { - pausable.pause(); + beforeEach(async () => { + await pausable.pause(); }); - it('should not throw when calling assertPaused', () => { - pausable.assertPaused(); + it('should not throw when calling assertPaused', async () => { + await pausable.assertPaused(); }); - it('should throw when calling assertNotPaused', () => { - expect(() => { - pausable.assertNotPaused(); - }).toThrow('Pausable: paused'); + it('should throw when calling assertNotPaused', async () => { + await expect(pausable.assertNotPaused()).rejects.toThrow( + 'Pausable: paused', + ); }); - it('should unpause from paused state', () => { - pausable.unpause(); - expect(pausable.isPaused()).toBe(false); + it('should unpause from paused state', async () => { + await pausable.unpause(); + expect(await pausable.isPaused()).toBe(false); }); - it('should throw when pausing in an paused state', () => { - expect(() => { - pausable.pause(); - }).toThrow('Pausable: paused'); + it('should throw when pausing in an paused state', async () => { + await expect(pausable.pause()).rejects.toThrow('Pausable: paused'); }); }); describe('Multiple Operations', () => { - it('should handle pause → unpause → pause sequence', () => { - pausable.pause(); - expect(pausable.isPaused()).toBe(true); + it('should handle pause → unpause → pause sequence', async () => { + await pausable.pause(); + expect(await pausable.isPaused()).toBe(true); - pausable.unpause(); - expect(pausable.isPaused()).toBe(false); + await pausable.unpause(); + expect(await pausable.isPaused()).toBe(false); - pausable.pause(); - expect(pausable.isPaused()).toBe(true); + await pausable.pause(); + expect(await pausable.isPaused()).toBe(true); }); }); describe('simulator wiring', () => { - it('should expose the public ledger via getPublicState', () => { - const sim = new PausableSimulator(); + it('should expose the public ledger via getPublicState', async () => { + const sim = await PausableSimulator.create(); - expect(sim.getPublicState().Pausable__isPaused).toBe(false); + expect((await sim.getPublicState()).Pausable__isPaused).toBe(false); - sim.pause(); + await sim.pause(); - expect(sim.getPublicState().Pausable__isPaused).toBe(true); + expect((await sim.getPublicState()).Pausable__isPaused).toBe(true); }); }); }); diff --git a/contracts/src/security/test/simulators/InitializableSimulator.ts b/contracts/src/security/test/simulators/InitializableSimulator.ts index 19a2c705..087e66e3 100644 --- a/contracts/src/security/test/simulators/InitializableSimulator.ts +++ b/contracts/src/security/test/simulators/InitializableSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -29,41 +29,43 @@ const InitializableSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => InitializableWitnesses(), + artifactName: 'MockInitializable', }); /** * Initializable Simulator */ export class InitializableSimulator extends InitializableSimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< InitializablePrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } /** * @description Initializes the state. */ - public initialize() { - this.circuits.impure.initialize(); + public initialize(): Promise<[]> { + return this.circuits.impure.initialize(); } /** * @description Asserts that the contract has been initialized, throwing an error if not. * @throws Will throw "Initializable: contract not initialized" if the contract is not initialized. */ - public assertInitialized() { - this.circuits.impure.assertInitialized(); + public assertInitialized(): Promise<[]> { + return this.circuits.impure.assertInitialized(); } /** * @description Asserts that the contract has not been initialized, throwing an error if it has. * @throws Will throw "Initializable: contract already initialized" if the contract is already initialized. */ - public assertNotInitialized() { - this.circuits.impure.assertNotInitialized(); + public assertNotInitialized(): Promise<[]> { + return this.circuits.impure.assertNotInitialized(); } } diff --git a/contracts/src/security/test/simulators/PausableSimulator.ts b/contracts/src/security/test/simulators/PausableSimulator.ts index 90338353..5110ca07 100644 --- a/contracts/src/security/test/simulators/PausableSimulator.ts +++ b/contracts/src/security/test/simulators/PausableSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { ledger, @@ -29,54 +29,56 @@ const PausableSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => PausableWitnesses(), + artifactName: 'MockPausable', }); /** * Pausable Simulator */ export class PausableSimulator extends PausableSimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< PausablePrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } /** * @description Returns true if the contract is paused, and false otherwise. * @returns True if paused. */ - public isPaused(): boolean { + public isPaused(): Promise { return this.circuits.impure.isPaused(); } /** * @description Makes a circuit only callable when the contract is paused. */ - public assertPaused() { - this.circuits.impure.assertPaused(); + public assertPaused(): Promise<[]> { + return this.circuits.impure.assertPaused(); } /** * @description Makes a circuit only callable when the contract is not paused. */ - public assertNotPaused() { - this.circuits.impure.assertNotPaused(); + public assertNotPaused(): Promise<[]> { + return this.circuits.impure.assertNotPaused(); } /** * @description Triggers a stopped state. */ - public pause() { - this.circuits.impure.pause(); + public pause(): Promise<[]> { + return this.circuits.impure.pause(); } /** * @description Lifts the pause on the contract. */ - public unpause() { - this.circuits.impure.unpause(); + public unpause(): Promise<[]> { + return this.circuits.impure.unpause(); } } diff --git a/contracts/src/token/test/FungibleToken.test.ts b/contracts/src/token/test/FungibleToken.test.ts index 47f0e18b..19a2c121 100644 --- a/contracts/src/token/test/FungibleToken.test.ts +++ b/contracts/src/token/test/FungibleToken.test.ts @@ -91,29 +91,29 @@ const recipientTypes = [ describe('FungibleToken', () => { describe('before initialization', () => { - it('should initialize metadata', () => { - token = new FungibleTokenSimulator(NAME, SYMBOL, DECIMALS, INIT); - expect(token.name()).toEqual(NAME); - expect(token.symbol()).toEqual(SYMBOL); - expect(token.decimals()).toEqual(DECIMALS); + it('should initialize metadata', async () => { + token = await FungibleTokenSimulator.create(NAME, SYMBOL, DECIMALS, INIT); + expect(await token.name()).toEqual(NAME); + expect(await token.symbol()).toEqual(SYMBOL); + expect(await token.decimals()).toEqual(DECIMALS); }); - it('should initialize empty metadata', () => { - token = new FungibleTokenSimulator( + it('should initialize empty metadata', async () => { + token = await FungibleTokenSimulator.create( EMPTY_STRING, EMPTY_STRING, NO_DECIMALS, INIT, ); - expect(token.name()).toEqual(EMPTY_STRING); - expect(token.symbol()).toEqual(EMPTY_STRING); - expect(token.decimals()).toEqual(NO_DECIMALS); + expect(await token.name()).toEqual(EMPTY_STRING); + expect(await token.symbol()).toEqual(EMPTY_STRING); + expect(await token.decimals()).toEqual(NO_DECIMALS); }); }); describe('when not initialized correctly', () => { - beforeEach(() => { - token = new FungibleTokenSimulator( + beforeEach(async () => { + token = await FungibleTokenSimulator.create( EMPTY_STRING, EMPTY_STRING, NO_DECIMALS, @@ -146,43 +146,45 @@ describe('FungibleToken', () => { ['_burn', [OWNER.either, AMOUNT]], ]; - it.each(circuitsToFail)('%s should fail', (circuitName, args) => { - expect(() => { - (token[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('FungibleToken: contract not initialized'); + it.each(circuitsToFail)('%s should fail', async (circuitName, args) => { + await expect( + (token[circuitName] as (...args: unknown[]) => Promise)( + ...args, + ), + ).rejects.toThrow('FungibleToken: contract not initialized'); }); }); describe('when initialized correctly', () => { - beforeEach(() => { - token = new FungibleTokenSimulator(NAME, SYMBOL, DECIMALS, INIT); + beforeEach(async () => { + token = await FungibleTokenSimulator.create(NAME, SYMBOL, DECIMALS, INIT); }); describe('totalSupply', () => { - it('returns 0 when there is no supply', () => { - expect(token.totalSupply()).toEqual(0n); + it('returns 0 when there is no supply', async () => { + expect(await token.totalSupply()).toEqual(0n); }); - it('returns the amount of existing tokens when there is a supply', () => { - token._mint(OWNER.either, AMOUNT); - expect(token.totalSupply()).toEqual(AMOUNT); + it('returns the amount of existing tokens when there is a supply', async () => { + await token._mint(OWNER.either, AMOUNT); + expect(await token.totalSupply()).toEqual(AMOUNT); }); }); describe('balanceOf', () => { describe.each(ownerTypes)('when the owner is a %s', (_, owner) => { - it('should return zero when requested account has no balance', () => { - expect(token.balanceOf(owner)).toEqual(0n); + it('should return zero when requested account has no balance', async () => { + expect(await token.balanceOf(owner)).toEqual(0n); }); - it('should return balance when requested account has tokens', () => { - token._unsafeMint(owner, AMOUNT); - expect(token.balanceOf(owner)).toEqual(AMOUNT); + it('should return balance when requested account has tokens', async () => { + await token._unsafeMint(owner, AMOUNT); + expect(await token.balanceOf(owner)).toEqual(AMOUNT); }); }); - it('should return correct balance with non-canonical lookup (left)', () => { - token._mint(OWNER.either, AMOUNT); + it('should return correct balance with non-canonical lookup (left)', async () => { + await token._mint(OWNER.either, AMOUNT); const nonCanonical = { is_left: true, @@ -190,11 +192,11 @@ describe('FungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.balanceOf(nonCanonical)).toEqual(AMOUNT); + expect(await token.balanceOf(nonCanonical)).toEqual(AMOUNT); }); - it('should return correct balance with non-canonical lookup (right)', () => { - token._unsafeMint(OWNER_CONTRACT, AMOUNT); + it('should return correct balance with non-canonical lookup (right)', async () => { + await token._unsafeMint(OWNER_CONTRACT, AMOUNT); const nonCanonical = { is_left: false, @@ -202,13 +204,13 @@ describe('FungibleToken', () => { right: OWNER_CONTRACT.right, }; - expect(token.balanceOf(nonCanonical)).toEqual(AMOUNT); + expect(await token.balanceOf(nonCanonical)).toEqual(AMOUNT); }); }); describe('allowance', () => { - it('should return correct allowance with non-canonical owner lookup (left)', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); + it('should return correct allowance with non-canonical owner lookup (left)', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); const nonCanonicalOwner = { is_left: true, @@ -216,13 +218,13 @@ describe('FungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.allowance(nonCanonicalOwner, SPENDER.either)).toEqual( - AMOUNT, - ); + expect( + await token.allowance(nonCanonicalOwner, SPENDER.either), + ).toEqual(AMOUNT); }); - it('should return correct allowance with non-canonical spender lookup (left)', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); + it('should return correct allowance with non-canonical spender lookup (left)', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); const nonCanonicalSpender = { is_left: true, @@ -230,13 +232,13 @@ describe('FungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.allowance(OWNER.either, nonCanonicalSpender)).toEqual( - AMOUNT, - ); + expect( + await token.allowance(OWNER.either, nonCanonicalSpender), + ).toEqual(AMOUNT); }); - it('should return correct allowance with non-canonical owner lookup (right)', () => { - token._approve(OWNER_CONTRACT, SPENDER.either, AMOUNT); + it('should return correct allowance with non-canonical owner lookup (right)', async () => { + await token._approve(OWNER_CONTRACT, SPENDER.either, AMOUNT); const nonCanonicalOwner = { is_left: false, @@ -244,13 +246,13 @@ describe('FungibleToken', () => { right: OWNER_CONTRACT.right, }; - expect(token.allowance(nonCanonicalOwner, SPENDER.either)).toEqual( - AMOUNT, - ); + expect( + await token.allowance(nonCanonicalOwner, SPENDER.either), + ).toEqual(AMOUNT); }); - it('should return correct allowance with non-canonical spender lookup (right)', () => { - token._approve(OWNER.either, RECIPIENT_CONTRACT, AMOUNT); + it('should return correct allowance with non-canonical spender lookup (right)', async () => { + await token._approve(OWNER.either, RECIPIENT_CONTRACT, AMOUNT); const nonCanonicalSpender = { is_left: false, @@ -258,96 +260,96 @@ describe('FungibleToken', () => { right: RECIPIENT_CONTRACT.right, }; - expect(token.allowance(OWNER.either, nonCanonicalSpender)).toEqual( - AMOUNT, - ); + expect( + await token.allowance(OWNER.either, nonCanonicalSpender), + ).toEqual(AMOUNT); }); }); describe('transfer', () => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either)).toEqual(0n); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(0n); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); - it('should transfer partial', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should transfer partial', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const partialAmt = AMOUNT - 1n; - const txSuccess = token.transfer(RECIPIENT.either, partialAmt); + const txSuccess = await token.transfer(RECIPIENT.either, partialAmt); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); }); - it('should transfer full', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should transfer full', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - const txSuccess = token.transfer(RECIPIENT.either, AMOUNT); + const txSuccess = await token.transfer(RECIPIENT.either, AMOUNT); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); }); - it('should fail with insufficient balance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail with insufficient balance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transfer(RECIPIENT.either, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient balance'); + await expect( + token.transfer(RECIPIENT.either, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient balance'); }); - it('should fail with transfer from zero identity', () => { + it('should fail with transfer from zero identity', async () => { // Inject a key that produces zero accountId — infeasible in practice, // but we can test the zero check by using _unsafeUncheckedTransfer directly - expect(() => { + await expect( token._unsafeUncheckedTransfer( ZERO_ACCOUNT, RECIPIENT.either, AMOUNT, - ); - }).toThrow('FungibleToken: invalid sender'); + ), + ).rejects.toThrow('FungibleToken: invalid sender'); }); - it('should fail with transfer to zero', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail with transfer to zero', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transfer(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect(token.transfer(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid receiver', + ); }); - it('should allow transfer of 0 tokens', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should allow transfer of 0 tokens', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - const txSuccess = token.transfer(RECIPIENT.either, 0n); + const txSuccess = await token.transfer(RECIPIENT.either, 0n); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either)).toEqual(0n); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(0n); }); - it('should handle transfer with empty _balances', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should handle transfer with empty _balances', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transfer(RECIPIENT.either, 1n); - }).toThrow('FungibleToken: insufficient balance'); + await expect(token.transfer(RECIPIENT.either, 1n)).rejects.toThrow( + 'FungibleToken: insufficient balance', + ); }); - it('should fail when transferring to a contract', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail when transferring to a contract', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transfer(OWNER_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: unsafe transfer'); + await expect(token.transfer(OWNER_CONTRACT, AMOUNT)).rejects.toThrow( + 'FungibleToken: unsafe transfer', + ); }); }); @@ -355,437 +357,453 @@ describe('FungibleToken', () => { describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); - expect(token.balanceOf(recipient)).toEqual(0n); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); + expect(await token.balanceOf(recipient)).toEqual(0n); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); - it('should transfer partial', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should transfer partial', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const partialAmt = AMOUNT - 1n; - const txSuccess = token._unsafeTransfer(recipient, partialAmt); + const txSuccess = await token._unsafeTransfer(recipient, partialAmt); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(recipient)).toEqual(partialAmt); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(recipient)).toEqual(partialAmt); }); - it('should transfer full', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should transfer full', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - const txSuccess = token._unsafeTransfer(recipient, AMOUNT); + const txSuccess = await token._unsafeTransfer(recipient, AMOUNT); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(recipient)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(recipient)).toEqual(AMOUNT); }); - it('should fail with insufficient balance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail with insufficient balance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransfer(recipient, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient balance'); + await expect( + token._unsafeTransfer(recipient, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient balance'); }); - it('should allow transfer of 0 tokens', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should allow transfer of 0 tokens', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - const txSuccess = token._unsafeTransfer(recipient, 0n); + const txSuccess = await token._unsafeTransfer(recipient, 0n); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); - expect(token.balanceOf(recipient)).toEqual(0n); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); + expect(await token.balanceOf(recipient)).toEqual(0n); }); - it('should handle transfer with empty _balances', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should handle transfer with empty _balances', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransfer(recipient, 1n); - }).toThrow('FungibleToken: insufficient balance'); + await expect(token._unsafeTransfer(recipient, 1n)).rejects.toThrow( + 'FungibleToken: insufficient balance', + ); }); }); - it('should fail with transfer to zero (accountId)', () => { - token._mint(OWNER.either, AMOUNT); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail with transfer to zero (accountId)', async () => { + await token._mint(OWNER.either, AMOUNT); + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransfer(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect( + token._unsafeTransfer(ZERO_ACCOUNT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); - it('should fail with transfer to zero (contract)', () => { - token._mint(OWNER.either, AMOUNT); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail with transfer to zero (contract)', async () => { + await token._mint(OWNER.either, AMOUNT); + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransfer(ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect( + token._unsafeTransfer(ZERO_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); }); describe('approve', () => { - beforeEach(() => { - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + beforeEach(async () => { + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); - it('should approve and update allowance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should approve and update allowance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + await token.approve(SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); }); - it('should approve and update allowance for multiple spenders', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should approve and update allowance for multiple spenders', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + await token.approve(SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); - token.approve(OTHER.either, AMOUNT); - expect(token.allowance(OWNER.either, OTHER.either)).toEqual(AMOUNT); + await token.approve(OTHER.either, AMOUNT); + expect(await token.allowance(OWNER.either, OTHER.either)).toEqual( + AMOUNT, + ); - expect(token.allowance(OWNER.either, RECIPIENT.either)).toEqual(0n); + expect(await token.allowance(OWNER.either, RECIPIENT.either)).toEqual( + 0n, + ); }); - it('should fail when approve to zero', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should fail when approve to zero', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.approve(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid spender'); + await expect(token.approve(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid spender', + ); }); - it('should transfer exact allowance and fail subsequent transfer', () => { - token._mint(OWNER.either, AMOUNT); + it('should transfer exact allowance and fail subsequent transfer', async () => { + await token._mint(OWNER.either, AMOUNT); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, AMOUNT); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); - expect(() => { - token.transferFrom(OWNER.either, RECIPIENT.either, 1n); - }).toThrow('FungibleToken: insufficient allowance'); + await expect( + token.transferFrom(OWNER.either, RECIPIENT.either, 1n), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should allow approve of 0 tokens', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should allow approve of 0 tokens', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, 0n); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + await token.approve(SPENDER.either, 0n); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); - it('should handle allowance with empty _allowances', () => { - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + it('should handle allowance with empty _allowances', async () => { + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); }); describe('transferFrom', () => { - beforeEach(() => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT); - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, AMOUNT); + await token._mint(OWNER.either, AMOUNT); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); - it('should transferFrom spender (partial)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should transferFrom spender (partial)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); const partialAmt = AMOUNT - 1n; - const txSuccess = token.transferFrom( + const txSuccess = await token.transferFrom( OWNER.either, RECIPIENT.either, partialAmt, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(1n); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(1n); }); - it('should transferFrom spender (full)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should transferFrom spender (full)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - const txSuccess = token.transferFrom( + const txSuccess = await token.transferFrom( OWNER.either, RECIPIENT.either, AMOUNT, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); - it('should transferFrom and not consume infinite allowance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, MAX_UINT128); + it('should transferFrom and not consume infinite allowance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, MAX_UINT128); - token.privateState.injectSecretKey(SPENDER.secretKey); - const txSuccess = token.transferFrom( + await token.privateState.injectSecretKey(SPENDER.secretKey); + const txSuccess = await token.transferFrom( OWNER.either, RECIPIENT.either, AMOUNT, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual( + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( MAX_UINT128, ); }); - it('should fail when transfer amount exceeds allowance', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail when transfer amount exceeds allowance', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient allowance'); + await expect( + token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should fail when transfer amount exceeds balance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT + 1n); + it('should fail when transfer amount exceeds balance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, AMOUNT + 1n); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient balance'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient balance'); }); - it('should fail when spender does not have allowance', () => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + it('should fail when spender does not have allowance', async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT); - }).toThrow('FungibleToken: insufficient allowance'); + await expect( + token.transferFrom(OWNER.either, RECIPIENT.either, AMOUNT), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should fail to transferFrom to the zero address', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail to transferFrom to the zero address', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect( + token.transferFrom(OWNER.either, ZERO_ACCOUNT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); - it('should fail when transferring to a contract', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail when transferring to a contract', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, OWNER_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: unsafe transfer'); + await expect( + token.transferFrom(OWNER.either, OWNER_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: unsafe transfer'); }); }); describe('_unsafeTransferFrom', () => { - beforeEach(() => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT); - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, AMOUNT); + await token._mint(OWNER.either, AMOUNT); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should transferFrom spender (partial)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should transferFrom spender (partial)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); const partialAmt = AMOUNT - 1n; - const txSuccess = token._unsafeTransferFrom( + const txSuccess = await token._unsafeTransferFrom( OWNER.either, recipient, partialAmt, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(recipient)).toEqual(partialAmt); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(1n); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(recipient)).toEqual(partialAmt); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + 1n, + ); }); - it('should transferFrom spender (full)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should transferFrom spender (full)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - const txSuccess = token._unsafeTransferFrom( + const txSuccess = await token._unsafeTransferFrom( OWNER.either, recipient, AMOUNT, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(recipient)).toEqual(AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(recipient)).toEqual(AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + 0n, + ); }); - it('should transferFrom and not consume infinite allowance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, MAX_UINT128); + it('should transferFrom and not consume infinite allowance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, MAX_UINT128); - token.privateState.injectSecretKey(SPENDER.secretKey); - const txSuccess = token._unsafeTransferFrom( + await token.privateState.injectSecretKey(SPENDER.secretKey); + const txSuccess = await token._unsafeTransferFrom( OWNER.either, recipient, AMOUNT, ); expect(txSuccess).toBe(true); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(recipient)).toEqual(AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual( + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(recipient)).toEqual(AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( MAX_UINT128, ); }); - it('should fail when transfer amount exceeds allowance', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail when transfer amount exceeds allowance', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient allowance'); + await expect( + token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should fail when transfer amount exceeds balance', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, AMOUNT + 1n); + it('should fail when transfer amount exceeds balance', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, AMOUNT + 1n); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient balance'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient balance'); }); - it('should fail when spender does not have allowance', () => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + it('should fail when spender does not have allowance', async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT); - }).toThrow('FungibleToken: insufficient allowance'); + await expect( + token._unsafeTransferFrom(OWNER.either, recipient, AMOUNT), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); }); - it('should fail to transfer to the zero address (accountId)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail to transfer to the zero address (accountId)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect( + token._unsafeTransferFrom(OWNER.either, ZERO_ACCOUNT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); - it('should fail to transfer to the zero address (contract)', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); + it('should fail to transfer to the zero address (contract)', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + await expect( + token._unsafeTransferFrom(OWNER.either, ZERO_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); }); describe('_transfer', () => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); - it('should update balances (partial)', () => { + it('should update balances (partial)', async () => { const partialAmt = AMOUNT - 1n; - token._transfer(OWNER.either, RECIPIENT.either, partialAmt); + await token._transfer(OWNER.either, RECIPIENT.either, partialAmt); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(partialAmt); }); - it('should fail when transferring to a contract', () => { - expect(() => { - token._transfer(OWNER.either, OWNER_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: unsafe transfer'); + it('should fail when transferring to a contract', async () => { + await expect( + token._transfer(OWNER.either, OWNER_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: unsafe transfer'); }); }); describe('_unsafeUncheckedTransfer', () => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); }); - afterEach(() => { - expect(token.totalSupply()).toEqual(AMOUNT); + afterEach(async () => { + expect(await token.totalSupply()).toEqual(AMOUNT); }); describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should update balances (partial)', () => { + it('should update balances (partial)', async () => { const partialAmt = AMOUNT - 1n; - token._unsafeUncheckedTransfer(OWNER.either, recipient, partialAmt); + await token._unsafeUncheckedTransfer( + OWNER.either, + recipient, + partialAmt, + ); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(recipient)).toEqual(partialAmt); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(recipient)).toEqual(partialAmt); }); - it('should update balances (full)', () => { - token._unsafeUncheckedTransfer(OWNER.either, recipient, AMOUNT); + it('should update balances (full)', async () => { + await token._unsafeUncheckedTransfer(OWNER.either, recipient, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(recipient)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(recipient)).toEqual(AMOUNT); }); - it('should fail when transfer amount exceeds balance', () => { - expect(() => { + it('should fail when transfer amount exceeds balance', async () => { + await expect( token._unsafeUncheckedTransfer( OWNER.either, recipient, AMOUNT + 1n, - ); - }).toThrow('FungibleToken: insufficient balance'); + ), + ).rejects.toThrow('FungibleToken: insufficient balance'); }); - it('should fail when transfer from zero', () => { - expect(() => { - token._unsafeUncheckedTransfer(ZERO_CONTRACT, recipient, AMOUNT); - }).toThrow('FungibleToken: invalid sender'); + it('should fail when transfer from zero', async () => { + await expect( + token._unsafeUncheckedTransfer(ZERO_CONTRACT, recipient, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid sender'); }); }); - it('should fail when transfer to zero (accountId)', () => { - expect(() => { - token._unsafeUncheckedTransfer(OWNER.either, ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + it('should fail when transfer to zero (accountId)', async () => { + await expect( + token._unsafeUncheckedTransfer(OWNER.either, ZERO_ACCOUNT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); - it('should fail when transfer to zero (contract)', () => { - expect(() => { - token._unsafeUncheckedTransfer(OWNER.either, ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + it('should fail when transfer to zero (contract)', async () => { + await expect( + token._unsafeUncheckedTransfer(OWNER.either, ZERO_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid receiver'); }); - it('should canonicalize recipient (zero out inactive right side)', () => { + it('should canonicalize recipient (zero out inactive right side)', async () => { // Check init amt for recipient is zero - expect(token.balanceOf(RECIPIENT.either)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(0n); const nonCanonical = { is_left: true, @@ -793,75 +811,87 @@ describe('FungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - token._unsafeUncheckedTransfer(OWNER.either, nonCanonical, AMOUNT); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + await token._unsafeUncheckedTransfer( + OWNER.either, + nonCanonical, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); }); - it('should canonicalize recipient contract address (zero out inactive left side)', () => { + it('should canonicalize recipient contract address (zero out inactive left side)', async () => { const nonCanonical = { is_left: false, left: new Uint8Array(32).fill(1), right: RECIPIENT_CONTRACT.right, }; - token._unsafeUncheckedTransfer(OWNER.either, nonCanonical, AMOUNT); - expect(token.balanceOf(RECIPIENT_CONTRACT)).toEqual(AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(0n); + await token._unsafeUncheckedTransfer( + OWNER.either, + nonCanonical, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT_CONTRACT)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); - it('should canonicalize fromAddress (zero out inactive right side)', () => { + it('should canonicalize fromAddress (zero out inactive right side)', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._unsafeUncheckedTransfer(nonCanonical, RECIPIENT.either, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + await token._unsafeUncheckedTransfer( + nonCanonical, + RECIPIENT.either, + AMOUNT, + ); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); }); }); describe('_mint', () => { - it('should mint and update supply', () => { - expect(token.totalSupply()).toEqual(0n); + it('should mint and update supply', async () => { + expect(await token.totalSupply()).toEqual(0n); - token._mint(RECIPIENT.either, AMOUNT); - expect(token.totalSupply()).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); + await token._mint(RECIPIENT.either, AMOUNT); + expect(await token.totalSupply()).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT); }); - it('should catch mint overflow', () => { - token._mint(RECIPIENT.either, MAX_UINT128); + it('should catch mint overflow', async () => { + await token._mint(RECIPIENT.either, MAX_UINT128); - expect(() => { - token._mint(RECIPIENT.either, 1n); - }).toThrow('FungibleToken: arithmetic overflow'); + await expect(token._mint(RECIPIENT.either, 1n)).rejects.toThrow( + 'FungibleToken: arithmetic overflow', + ); }); - it('should not mint to zero (accountId)', () => { - expect(() => { - token._mint(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + it('should not mint to zero (accountId)', async () => { + await expect(token._mint(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid receiver', + ); }); - it('should not mint to zero (contract)', () => { - expect(() => { - // caught by unsafe transfer guard first - token._mint(ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: unsafe transfer'); + it('should not mint to zero (contract)', async () => { + // caught by unsafe transfer guard first + await expect(token._mint(ZERO_CONTRACT, AMOUNT)).rejects.toThrow( + 'FungibleToken: unsafe transfer', + ); }); - it('should allow mint of 0 tokens', () => { - token._mint(OWNER.either, 0n); - expect(token.totalSupply()).toEqual(0n); - expect(token.balanceOf(OWNER.either)).toEqual(0n); + it('should allow mint of 0 tokens', async () => { + await token._mint(OWNER.either, 0n); + expect(await token.totalSupply()).toEqual(0n); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); - it('should fail when minting to a contract', () => { - expect(() => { - token._mint(OWNER_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: unsafe transfer'); + it('should fail when minting to a contract', async () => { + await expect(token._mint(OWNER_CONTRACT, AMOUNT)).rejects.toThrow( + 'FungibleToken: unsafe transfer', + ); }); }); @@ -869,229 +899,247 @@ describe('FungibleToken', () => { describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should mint and update supply', () => { - expect(token.totalSupply()).toEqual(0n); + it('should mint and update supply', async () => { + expect(await token.totalSupply()).toEqual(0n); - token._unsafeMint(recipient, AMOUNT); - expect(token.totalSupply()).toEqual(AMOUNT); - expect(token.balanceOf(recipient)).toEqual(AMOUNT); + await token._unsafeMint(recipient, AMOUNT); + expect(await token.totalSupply()).toEqual(AMOUNT); + expect(await token.balanceOf(recipient)).toEqual(AMOUNT); }); - it('should catch mint overflow', () => { - token._unsafeMint(recipient, MAX_UINT128); + it('should catch mint overflow', async () => { + await token._unsafeMint(recipient, MAX_UINT128); - expect(() => { - token._unsafeMint(recipient, 1n); - }).toThrow('FungibleToken: arithmetic overflow'); + await expect(token._unsafeMint(recipient, 1n)).rejects.toThrow( + 'FungibleToken: arithmetic overflow', + ); }); - it('should allow mint of 0 tokens', () => { - token._unsafeMint(recipient, 0n); - expect(token.totalSupply()).toEqual(0n); - expect(token.balanceOf(recipient)).toEqual(0n); + it('should allow mint of 0 tokens', async () => { + await token._unsafeMint(recipient, 0n); + expect(await token.totalSupply()).toEqual(0n); + expect(await token.balanceOf(recipient)).toEqual(0n); }); }); - it('should not mint to zero (accountId)', () => { - expect(() => { - token._unsafeMint(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + it('should not mint to zero (accountId)', async () => { + await expect(token._unsafeMint(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid receiver', + ); }); - it('should not mint to zero (contract)', () => { - expect(() => { - token._unsafeMint(ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid receiver'); + it('should not mint to zero (contract)', async () => { + await expect(token._unsafeMint(ZERO_CONTRACT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid receiver', + ); }); - it('should canonicalize sender (zero out inactive right side)', () => { + it('should canonicalize sender (zero out inactive right side)', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._unsafeMint(nonCanonical, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); + await token._unsafeMint(nonCanonical, AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); }); }); describe('_burn', () => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); }); - it('should burn tokens', () => { - token._burn(OWNER.either, 1n); + it('should burn tokens', async () => { + await token._burn(OWNER.either, 1n); const afterBurn = AMOUNT - 1n; - expect(token.balanceOf(OWNER.either)).toEqual(afterBurn); - expect(token.totalSupply()).toEqual(afterBurn); + expect(await token.balanceOf(OWNER.either)).toEqual(afterBurn); + expect(await token.totalSupply()).toEqual(afterBurn); }); - it('should throw when burning from zero (accountId)', () => { - expect(() => { - token._burn(ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid sender'); + it('should throw when burning from zero (accountId)', async () => { + await expect(token._burn(ZERO_ACCOUNT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid sender', + ); }); - it('should throw when burning from zero (contract)', () => { - expect(() => { - token._burn(ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid sender'); + it('should throw when burning from zero (contract)', async () => { + await expect(token._burn(ZERO_CONTRACT, AMOUNT)).rejects.toThrow( + 'FungibleToken: invalid sender', + ); }); - it('should throw when burn amount is greater than balance', () => { - expect(() => { - token._burn(OWNER.either, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient balance'); + it('should throw when burn amount is greater than balance', async () => { + await expect(token._burn(OWNER.either, AMOUNT + 1n)).rejects.toThrow( + 'FungibleToken: insufficient balance', + ); }); - it('should allow burn of 0 tokens', () => { - token._burn(OWNER.either, 0n); - expect(token.totalSupply()).toEqual(AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); + it('should allow burn of 0 tokens', async () => { + await token._burn(OWNER.either, 0n); + expect(await token.totalSupply()).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); }); - it('should burn with non-canonical account (left)', () => { + it('should burn with non-canonical account (left)', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._burn(nonCanonical, 1n); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT - 1n); - expect(token.totalSupply()).toEqual(AMOUNT - 1n); + await token._burn(nonCanonical, 1n); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT - 1n); + expect(await token.totalSupply()).toEqual(AMOUNT - 1n); }); }); describe('_approve', () => { - beforeEach(() => { - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + beforeEach(async () => { + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); - it('should approve and update allowance', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + it('should approve and update allowance', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); }); - it('should approve and update allowance for multiple spenders', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + it('should approve and update allowance for multiple spenders', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); - token._approve(OWNER.either, OTHER.either, AMOUNT); - expect(token.allowance(OWNER.either, OTHER.either)).toEqual(AMOUNT); + await token._approve(OWNER.either, OTHER.either, AMOUNT); + expect(await token.allowance(OWNER.either, OTHER.either)).toEqual( + AMOUNT, + ); - expect(token.allowance(OWNER.either, RECIPIENT.either)).toEqual(0n); + expect(await token.allowance(OWNER.either, RECIPIENT.either)).toEqual( + 0n, + ); }); - it('should fail when approve from zero (accountId)', () => { - expect(() => { - token._approve(ZERO_ACCOUNT, SPENDER.either, AMOUNT); - }).toThrow('FungibleToken: invalid owner'); + it('should fail when approve from zero (accountId)', async () => { + await expect( + token._approve(ZERO_ACCOUNT, SPENDER.either, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid owner'); }); - it('should fail when approve from zero (contract)', () => { - expect(() => { - token._approve(ZERO_CONTRACT, SPENDER.either, AMOUNT); - }).toThrow('FungibleToken: invalid owner'); + it('should fail when approve from zero (contract)', async () => { + await expect( + token._approve(ZERO_CONTRACT, SPENDER.either, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid owner'); }); - it('should fail when approve to zero (accountId)', () => { - expect(() => { - token._approve(OWNER.either, ZERO_ACCOUNT, AMOUNT); - }).toThrow('FungibleToken: invalid spender'); + it('should fail when approve to zero (accountId)', async () => { + await expect( + token._approve(OWNER.either, ZERO_ACCOUNT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid spender'); }); - it('should fail when approve to zero (contract)', () => { - expect(() => { - token._approve(OWNER.either, ZERO_CONTRACT, AMOUNT); - }).toThrow('FungibleToken: invalid spender'); + it('should fail when approve to zero (contract)', async () => { + await expect( + token._approve(OWNER.either, ZERO_CONTRACT, AMOUNT), + ).rejects.toThrow('FungibleToken: invalid spender'); }); - it('should allow approve of 0 tokens', () => { - token._approve(OWNER.either, SPENDER.either, 0n); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + it('should allow approve of 0 tokens', async () => { + await token._approve(OWNER.either, SPENDER.either, 0n); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); - it('should canonicalize owner in allowance (zero out inactive right side)', () => { + it('should canonicalize owner in allowance (zero out inactive right side)', async () => { const nonCanonicalOwner = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._approve(nonCanonicalOwner, SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + await token._approve(nonCanonicalOwner, SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); }); - it('should canonicalize spender in allowance (zero out inactive right side)', () => { + it('should canonicalize spender in allowance (zero out inactive right side)', async () => { const nonCanonicalSpender = { is_left: true, left: SPENDER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._approve(OWNER.either, nonCanonicalSpender, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(AMOUNT); + await token._approve(OWNER.either, nonCanonicalSpender, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( + AMOUNT, + ); }); - it('should canonicalize contract address owner (zero out inactive left side)', () => { + it('should canonicalize contract address owner (zero out inactive left side)', async () => { const nonCanonicalOwner = { is_left: false, left: new Uint8Array(32).fill(1), right: OWNER_CONTRACT.right, }; - token._approve(nonCanonicalOwner, SPENDER.either, AMOUNT); - expect(token.allowance(OWNER_CONTRACT, SPENDER.either)).toEqual(AMOUNT); + await token._approve(nonCanonicalOwner, SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER_CONTRACT, SPENDER.either)).toEqual( + AMOUNT, + ); }); }); describe('_spendAllowance', () => { - beforeEach(() => { - token._mint(OWNER.either, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, AMOUNT); }); - it('should update allowance when not unlimited', () => { - token._approve(OWNER.either, SPENDER.either, MAX_UINT128 - 1n); - token._spendAllowance(OWNER.either, SPENDER.either, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual( + it('should update allowance when not unlimited', async () => { + await token._approve(OWNER.either, SPENDER.either, MAX_UINT128 - 1n); + await token._spendAllowance(OWNER.either, SPENDER.either, AMOUNT); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( MAX_UINT128 - 1n - AMOUNT, ); }); - it('should not update allowance when unlimited', () => { - token._approve(OWNER.either, SPENDER.either, MAX_UINT128); - token._spendAllowance(OWNER.either, SPENDER.either, MAX_UINT128 - 1n); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual( + it('should not update allowance when unlimited', async () => { + await token._approve(OWNER.either, SPENDER.either, MAX_UINT128); + await token._spendAllowance( + OWNER.either, + SPENDER.either, + MAX_UINT128 - 1n, + ); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual( MAX_UINT128, ); }); - it('should fail when owner allowance is not initialized', () => { - expect(() => { - token._spendAllowance(OTHER.either, SPENDER.either, AMOUNT); - }).toThrow('FungibleToken: insufficient allowance'); + it('should fail when owner allowance is not initialized', async () => { + await expect( + token._spendAllowance(OTHER.either, SPENDER.either, AMOUNT), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should fail when spender is not initialized', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); - expect(() => { - token._spendAllowance(OWNER.either, OTHER.either, AMOUNT); - }).toThrow('FungibleToken: insufficient allowance'); + it('should fail when spender is not initialized', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); + await expect( + token._spendAllowance(OWNER.either, OTHER.either, AMOUNT), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should fail when spender has insufficient allowance', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); - expect(() => { - token._spendAllowance(OWNER.either, SPENDER.either, AMOUNT + 1n); - }).toThrow('FungibleToken: insufficient allowance'); + it('should fail when spender has insufficient allowance', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); + await expect( + token._spendAllowance(OWNER.either, SPENDER.either, AMOUNT + 1n), + ).rejects.toThrow('FungibleToken: insufficient allowance'); }); - it('should canonicalize when spending allowance', () => { - token._approve(OWNER.either, SPENDER.either, AMOUNT); + it('should canonicalize when spending allowance', async () => { + await token._approve(OWNER.either, SPENDER.either, AMOUNT); const nonCanonicalOwner = { is_left: true, @@ -1104,52 +1152,74 @@ describe('FungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - token._spendAllowance(nonCanonicalOwner, nonCanonicalSpender, AMOUNT); - expect(token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); + await token._spendAllowance( + nonCanonicalOwner, + nonCanonicalSpender, + AMOUNT, + ); + expect(await token.allowance(OWNER.either, SPENDER.either)).toEqual(0n); }); }); describe('Multiple Operations', () => { - it('should handle mint → transfer → burn sequence', () => { - token._mint(OWNER.either, AMOUNT); - expect(token.totalSupply()).toEqual(AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); + it('should handle mint → transfer → burn sequence', async () => { + await token._mint(OWNER.either, AMOUNT); + expect(await token.totalSupply()).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); - token.privateState.injectSecretKey(OWNER.secretKey); - token.transfer(RECIPIENT.either, AMOUNT - 1n); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT - 1n); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.transfer(RECIPIENT.either, AMOUNT - 1n); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(AMOUNT - 1n); - token._burn(OWNER.either, 1n); - expect(token.totalSupply()).toEqual(AMOUNT - 1n); - expect(token.balanceOf(OWNER.either)).toEqual(0n); + await token._burn(OWNER.either, 1n); + expect(await token.totalSupply()).toEqual(AMOUNT - 1n); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); }); }); describe('simulator wiring', () => { - it('should expose an empty public ledger via getPublicState', () => { - const sim = new FungibleTokenSimulator(NAME, SYMBOL, DECIMALS, INIT); + it('should expose an empty public ledger via getPublicState', async () => { + const sim = await FungibleTokenSimulator.create( + NAME, + SYMBOL, + DECIMALS, + INIT, + ); - expect(sim.getPublicState()).toStrictEqual({}); + expect(await sim.getPublicState()).toStrictEqual({}); }); }); describe('privateState helpers', () => { describe('getCurrentSecretKey', () => { - it('should return the injected secret key', () => { - const sim = new FungibleTokenSimulator(NAME, SYMBOL, DECIMALS, INIT); - sim.privateState.injectSecretKey(OWNER.secretKey); + it('should return the injected secret key', async () => { + const sim = await FungibleTokenSimulator.create( + NAME, + SYMBOL, + DECIMALS, + INIT, + ); + await sim.privateState.injectSecretKey(OWNER.secretKey); - expect(sim.privateState.getCurrentSecretKey()).toEqual(OWNER.secretKey); + expect(await sim.privateState.getCurrentSecretKey()).toEqual( + OWNER.secretKey, + ); }); - it('should throw when the secret key is undefined', () => { - const sim = new FungibleTokenSimulator(NAME, SYMBOL, DECIMALS, INIT, { - privateState: { secretKey: undefined as unknown as Uint8Array }, - }); + it('should throw when the secret key is undefined', async () => { + const sim = await FungibleTokenSimulator.create( + NAME, + SYMBOL, + DECIMALS, + INIT, + { + privateState: { secretKey: undefined as unknown as Uint8Array }, + }, + ); - expect(() => sim.privateState.getCurrentSecretKey()).toThrow( + await expect(sim.privateState.getCurrentSecretKey()).rejects.toThrow( 'Missing secret key', ); }); diff --git a/contracts/src/token/test/MultiToken.test.ts b/contracts/src/token/test/MultiToken.test.ts index 1807308c..88d955b6 100644 --- a/contracts/src/token/test/MultiToken.test.ts +++ b/contracts/src/token/test/MultiToken.test.ts @@ -125,30 +125,30 @@ let token: MultiTokenSimulator; describe('MultiToken', () => { describe('before initialization', () => { - it('should initialize metadata', () => { - token = new MultiTokenSimulator(initWithURI); + it('should initialize metadata', async () => { + token = await MultiTokenSimulator.create(initWithURI); - expect(token.uri(TOKEN_ID)).toEqual(URI); + expect(await token.uri(TOKEN_ID)).toEqual(URI); }); - it('should initialize empty metadata', () => { - token = new MultiTokenSimulator(initWithEmptyURI); + it('should initialize empty metadata', async () => { + token = await MultiTokenSimulator.create(initWithEmptyURI); - expect(token.uri(TOKEN_ID)).toEqual(NO_STRING); + expect(await token.uri(TOKEN_ID)).toEqual(NO_STRING); }); - it('should not be able to re-initialize', () => { - token = new MultiTokenSimulator(initWithEmptyURI); + it('should not be able to re-initialize', async () => { + token = await MultiTokenSimulator.create(initWithEmptyURI); - expect(() => { - token.initialize(URI); - }).toThrow('MultiToken: contract already initialized'); + await expect(token.initialize(URI)).rejects.toThrow( + 'MultiToken: contract already initialized', + ); }); }); describe('when not initialized correctly', () => { - beforeEach(() => { - token = new MultiTokenSimulator(badInit); + beforeEach(async () => { + token = await MultiTokenSimulator.create(badInit); }); type FailingCircuits = [method: keyof MultiTokenSimulator, args: unknown[]]; @@ -169,24 +169,26 @@ describe('MultiToken', () => { ['_setApprovalForAll', [OWNER.either, SPENDER.either, true]], ]; - it.each(circuitsToFail)('%s should fail', (circuitName, args) => { - expect(() => { - (token[circuitName] as (...args: unknown[]) => unknown)(...args); - }).toThrow('MultiToken: contract not initialized'); + it.each(circuitsToFail)('%s should fail', async (circuitName, args) => { + await expect( + (token[circuitName] as (...args: unknown[]) => Promise)( + ...args, + ), + ).rejects.toThrow('MultiToken: contract not initialized'); }); - it('should allow initialization post deployment', () => { - token.initialize(URI); + it('should allow initialization post deployment', async () => { + await token.initialize(URI); - expect(() => { - token.balanceOf(OWNER.either, TOKEN_ID); - }).not.toThrow(); + await expect( + token.balanceOf(OWNER.either, TOKEN_ID), + ).resolves.not.toThrow(); }); }); describe('when initialized correctly', () => { - beforeEach(() => { - token = new MultiTokenSimulator(initWithURI); + beforeEach(async () => { + token = await MultiTokenSimulator.create(initWithURI); }); describe('balanceOf', () => { @@ -196,980 +198,1128 @@ describe('MultiToken', () => { ] as const; describe.each(ownerTypes)('when the owner is a %s', (_, owner) => { - it('should return zero when requested account has no balance', () => { - expect(token.balanceOf(owner, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(owner, TOKEN_ID2)).toEqual(0n); + it('should return zero when requested account has no balance', async () => { + expect(await token.balanceOf(owner, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(owner, TOKEN_ID2)).toEqual(0n); }); - it('should return balance when requested account has tokens', () => { - token._unsafeMint(owner, TOKEN_ID, AMOUNT); - expect(token.balanceOf(owner, TOKEN_ID)).toEqual(AMOUNT); + it('should return balance when requested account has tokens', async () => { + await token._unsafeMint(owner, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(owner, TOKEN_ID)).toEqual(AMOUNT); - token._unsafeMint(owner, TOKEN_ID2, AMOUNT2); - expect(token.balanceOf(owner, TOKEN_ID2)).toEqual(AMOUNT2); + await token._unsafeMint(owner, TOKEN_ID2, AMOUNT2); + expect(await token.balanceOf(owner, TOKEN_ID2)).toEqual(AMOUNT2); }); - it('should handle token ID 0', () => { + it('should handle token ID 0', async () => { const ZERO_ID = 0n; - token._unsafeMint(owner, ZERO_ID, AMOUNT); - expect(token.balanceOf(owner, ZERO_ID)).toEqual(AMOUNT); + await token._unsafeMint(owner, ZERO_ID, AMOUNT); + expect(await token.balanceOf(owner, ZERO_ID)).toEqual(AMOUNT); }); - it('should handle MAX_UINT128 token ID', () => { + it('should handle MAX_UINT128 token ID', async () => { const MAX_ID = MAX_UINT128; - token._unsafeMint(owner, MAX_ID, AMOUNT); - expect(token.balanceOf(owner, MAX_ID)).toEqual(AMOUNT); + await token._unsafeMint(owner, MAX_ID, AMOUNT); + expect(await token.balanceOf(owner, MAX_ID)).toEqual(AMOUNT); }); }); - it('should return correct balance with non-canonical lookup (left)', () => { - token._unsafeMint(OWNER.either, TOKEN_ID, AMOUNT); + it('should return correct balance with non-canonical lookup (left)', async () => { + await token._unsafeMint(OWNER.either, TOKEN_ID, AMOUNT); const nonCanonical = nonCanonicalLeft(OWNER.accountId); - expect(token.balanceOf(nonCanonical, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(nonCanonical, TOKEN_ID)).toEqual(AMOUNT); }); - it('should return correct balance with non-canonical lookup (right)', () => { - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + it('should return correct balance with non-canonical lookup (right)', async () => { + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - expect(token.balanceOf(nonCanonical, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(nonCanonical, TOKEN_ID)).toEqual(AMOUNT); }); }); describe('isApprovedForAll', () => { - it('should return false when not set', () => { - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + it('should return false when not set', async () => { + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( false, ); }); - it('should handle approving owner as operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(OWNER.either, true); - expect(token.isApprovedForAll(OWNER.either, OWNER.either)).toBe(true); + it('should handle approving owner as operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(OWNER.either, true); + expect(await token.isApprovedForAll(OWNER.either, OWNER.either)).toBe( + true, + ); }); - it('should handle multiple approvals of same operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + it('should handle multiple approvals of same operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should handle revoking non-existent approval', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + it('should handle revoking non-existent approval', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( false, ); }); - it('should return correct result with non-canonical owner lookup', () => { - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + it('should return correct result with non-canonical owner lookup', async () => { + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); const nonCanonical = nonCanonicalLeft(OWNER.accountId); - expect(token.isApprovedForAll(nonCanonical, SPENDER.either)).toBe(true); + expect(await token.isApprovedForAll(nonCanonical, SPENDER.either)).toBe( + true, + ); }); - it('should return correct result with non-canonical operator lookup', () => { - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + it('should return correct result with non-canonical operator lookup', async () => { + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); const nonCanonical = nonCanonicalLeft(SPENDER.accountId); - expect(token.isApprovedForAll(OWNER.either, nonCanonical)).toBe(true); + expect(await token.isApprovedForAll(OWNER.either, nonCanonical)).toBe( + true, + ); }); }); describe('setApprovalForAll', () => { - it('should return false when set to false', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + it('should return false when set to false', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( false, ); }); - it('should fail when attempting to approve zero address as an operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.setApprovalForAll(ZERO_ACCOUNT, true); - }).toThrow('MultiToken: invalid operator'); + it('should fail when attempting to approve zero address as an operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.setApprovalForAll(ZERO_ACCOUNT, true), + ).rejects.toThrow('MultiToken: invalid operator'); }); describe('when spender is approved as an operator', () => { - beforeEach(() => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + beforeEach(async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); }); - it('should return true when set to true', () => { - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - true, - ); + it('should return true when set to true', async () => { + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(true); }); - it('should unset → set → unset operator', () => { - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - false, - ); - - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - true, - ); - - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - false, - ); + it('should unset → set → unset operator', async () => { + await token.setApprovalForAll(SPENDER.either, false); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(false); + + await token.setApprovalForAll(SPENDER.either, true); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(true); + + await token.setApprovalForAll(SPENDER.either, false); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(false); }); }); }); describe('transferFrom', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); }); describe.each(callerTypes)('when the caller is the %s', (_, caller) => { - beforeEach(() => { + beforeEach(async () => { if (caller === SPENDER) { - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); } - token.privateState.injectSecretKey(caller.secretKey); + await token.privateState.injectSecretKey(caller.secretKey); }); - it('should transfer whole', () => { - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); + it('should transfer whole', async () => { + await token.transferFrom( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should transfer partial', () => { + it('should transfer partial', async () => { const partialAmt = AMOUNT - 1n; - token.transferFrom( + await token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, partialAmt, ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( AMOUNT - partialAmt, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( partialAmt, ); }); - it('should allow transfer of 0 tokens', () => { - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, 0n); + it('should allow transfer of 0 tokens', async () => { + await token.transferFrom( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + 0n, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); }); - it('should handle self-transfer', () => { - token.transferFrom(OWNER.either, OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + it('should handle self-transfer', async () => { + await token.transferFrom( + OWNER.either, + OWNER.either, + TOKEN_ID, + AMOUNT, + ); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); }); - it('should handle MAX_UINT128 transfer amount', () => { - token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT); + it('should handle MAX_UINT128 transfer amount', async () => { + await token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT); - token.transferFrom( + await token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, MAX_UINT128, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( MAX_UINT128, ); }); - it('should handle rapid state changes', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should handle rapid state changes', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); - - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - false, + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, ); - - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - true, + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, ); + + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, false); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(false); + + await token.setApprovalForAll(SPENDER.either, true); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(true); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with transfer from zero', () => { - expect(() => { + it('should fail with transfer from zero', async () => { + await expect( token.transferFrom( ZERO_ACCOUNT, RECIPIENT.either, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with transfer to zero (id)', () => { - expect(() => { - token.transferFrom(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail with transfer to zero (id)', async () => { + await expect( + token.transferFrom(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail with transfer to zero (contract)', () => { - expect(() => { - token.transferFrom(OWNER.either, ZERO_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: unsafe transfer'); + it('should fail with transfer to zero (contract)', async () => { + await expect( + token.transferFrom(OWNER.either, ZERO_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: unsafe transfer'); }); - it('should fail when transferring to a contract address', () => { - expect(() => { + it('should fail when transferring to a contract address', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT_CONTRACT, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unsafe transfer'); + ), + ).rejects.toThrow('MultiToken: unsafe transfer'); }); }); - it('should handle concurrent operations on same token ID', () => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT * 2n); + it('should handle concurrent operations on same token ID', async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT * 2n); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(OTHER.either, true); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(OTHER.either, true); // First spender transfers half - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); // Second spender transfers remaining - token.privateState.injectSecretKey(OTHER.secretKey); - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + await token.privateState.injectSecretKey(OTHER.secretKey); + await token.transferFrom( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( AMOUNT * 2n, ); }); - it('should handle non-canonical fromAddress (id)', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should handle non-canonical fromAddress (id)', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = nonCanonicalLeft(OWNER.accountId); - token.transferFrom(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + await token.transferFrom( + nonCanonical, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical fromAddress (contract address)', () => { - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); - token._setApprovalForAll(OWNER_CONTRACT, OWNER.either, true); + it('should handle non-canonical fromAddress (contract address)', async () => { + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + await token._setApprovalForAll(OWNER_CONTRACT, OWNER.either, true); - token.privateState.injectSecretKey(OWNER.secretKey); + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - token.transferFrom(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + await token.transferFrom( + nonCanonical, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); describe('when the caller is unauthorized', () => { - beforeEach(() => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + beforeEach(async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); }); - it('should fail when transfer whole', () => { - expect(() => { + it('should fail when transfer whole', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail when transfer partial', () => { - expect(() => { - const partialAmt = AMOUNT - 1n; + it('should fail when transfer partial', async () => { + const partialAmt = AMOUNT - 1n; + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, partialAmt, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail when transfer zero', () => { - expect(() => { - token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, 0n); - }).toThrow('MultiToken: unauthorized operator'); + it('should fail when transfer zero', async () => { + await expect( + token.transferFrom(OWNER.either, RECIPIENT.either, TOKEN_ID, 0n), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token.transferFrom( OWNER.either, RECIPIENT.either, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with transfer from zero', () => { - expect(() => { + it('should fail with transfer from zero', async () => { + await expect( token.transferFrom( ZERO_ACCOUNT, RECIPIENT.either, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); }); }); describe('_unsafeTransferFrom', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT); }); describe.each(callerTypes)('when the caller is the %s', (_, caller) => { - beforeEach(() => { + beforeEach(async () => { if (caller === SPENDER) { - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); } - token.privateState.injectSecretKey(caller.secretKey); + await token.privateState.injectSecretKey(caller.secretKey); }); describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should transfer whole', () => { - token._unsafeTransferFrom( + it('should transfer whole', async () => { + await token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); }); - it('should transfer partial', () => { + it('should transfer partial', async () => { const partialAmt = AMOUNT - 1n; - token._unsafeTransferFrom( + await token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, partialAmt, ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( AMOUNT - partialAmt, ); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(partialAmt); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual( + partialAmt, + ); }); - it('should allow transfer of 0 tokens', () => { - token._unsafeTransferFrom(OWNER.either, recipient, TOKEN_ID, 0n); + it('should allow transfer of 0 tokens', async () => { + await token._unsafeTransferFrom( + OWNER.either, + recipient, + TOKEN_ID, + 0n, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + AMOUNT, + ); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n); }); - it('should handle self-transfer', () => { - token._unsafeTransferFrom( + it('should handle self-transfer', async () => { + await token._unsafeTransferFrom( OWNER.either, OWNER.either, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle MAX_UINT128 transfer amount', () => { - token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT); + it('should handle MAX_UINT128 transfer amount', async () => { + await token._mint(OWNER.either, TOKEN_ID, MAX_UINT128 - AMOUNT); - token._unsafeTransferFrom( + await token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, MAX_UINT128, ); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(MAX_UINT128); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual( + MAX_UINT128, + ); }); - it('should handle rapid state changes', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should handle rapid state changes', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token._unsafeTransferFrom( + await token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - false, - ); + await token.setApprovalForAll(SPENDER.either, false); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(false); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( - true, - ); + await token.setApprovalForAll(SPENDER.either, true); + expect( + await token.isApprovedForAll(OWNER.either, SPENDER.either), + ).toBe(true); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, recipient, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with transfer from zero', () => { - expect(() => { + it('should fail with transfer from zero', async () => { + await expect( token._unsafeTransferFrom( ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); }); - it('should fail with transfer to zero (id)', () => { - expect(() => { + it('should fail with transfer to zero (id)', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: invalid receiver'); + ), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail with transfer to zero (contract)', () => { - expect(() => { + it('should fail with transfer to zero (contract)', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, ZERO_CONTRACT, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: invalid receiver'); + ), + ).rejects.toThrow('MultiToken: invalid receiver'); }); }); - it('should handle concurrent operations on same token ID', () => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT * 2n); + it('should handle concurrent operations on same token ID', async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT * 2n); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(OTHER.either, true); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(OTHER.either, true); // First spender transfers half - token.privateState.injectSecretKey(SPENDER.secretKey); - token._unsafeTransferFrom( + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token._unsafeTransferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); // Second spender transfers remaining - token.privateState.injectSecretKey(OTHER.secretKey); - token._unsafeTransferFrom( + await token.privateState.injectSecretKey(OTHER.secretKey); + await token._unsafeTransferFrom( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( AMOUNT * 2n, ); }); - it('should handle non-canonical fromAddress (id)', () => { + it('should handle non-canonical fromAddress (id)', async () => { const nonCanonical = nonCanonicalLeft(OWNER.accountId); - token.privateState.injectSecretKey(OWNER.secretKey); - token._unsafeTransferFrom( + await token.privateState.injectSecretKey(OWNER.secretKey); + await token._unsafeTransferFrom( nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical fromAddress (contract address)', () => { + it('should handle non-canonical fromAddress (contract address)', async () => { // Mint to contract address to test the transfer of non-canonical `fromAddress` - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); // Approve owner (id) to move OWNER_CONTRACT's token - token._setApprovalForAll(OWNER_CONTRACT, OWNER.either, true); + await token._setApprovalForAll(OWNER_CONTRACT, OWNER.either, true); - token.privateState.injectSecretKey(OWNER.secretKey); + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - token._unsafeTransferFrom( + await token._unsafeTransferFrom( nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should canonicalize recipient (id)', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should canonicalize recipient (id)', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = nonCanonicalLeft(RECIPIENT.accountId); - token._unsafeTransferFrom(OWNER.either, nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + await token._unsafeTransferFrom( + OWNER.either, + nonCanonical, + TOKEN_ID, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should canonicalize recipient (contract address)', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should canonicalize recipient (contract address)', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = nonCanonicalRight(RECIPIENT_CONTRACT.right); - token._unsafeTransferFrom(OWNER.either, nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual(AMOUNT); + await token._unsafeTransferFrom( + OWNER.either, + nonCanonical, + TOKEN_ID, + AMOUNT, + ); + expect(await token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual( + AMOUNT, + ); }); describe('when the caller is unauthorized', () => { - beforeEach(() => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + beforeEach(async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); }); describe.each( recipientTypes, )('when recipient is %s', (_, recipient) => { - it('should fail when transfer whole', () => { - expect(() => { + it('should fail when transfer whole', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail when transfer partial', () => { - expect(() => { - const partialAmt = AMOUNT - 1n; + it('should fail when transfer partial', async () => { + const partialAmt = AMOUNT - 1n; + await expect( token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, partialAmt, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail when transfer zero', () => { - expect(() => { - token._unsafeTransferFrom(OWNER.either, recipient, TOKEN_ID, 0n); - }).toThrow('MultiToken: unauthorized operator'); + it('should fail when transfer zero', async () => { + await expect( + token._unsafeTransferFrom(OWNER.either, recipient, TOKEN_ID, 0n), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, recipient, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token._unsafeTransferFrom( OWNER.either, recipient, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); - it('should fail with transfer from zero', () => { + it('should fail with transfer from zero', async () => { // With witness-based identity, the caller is H(sk) which is // always non-zero. Transferring from ZERO_ACCOUNT means // canonFrom != caller → isApprovedForAll(zeroAccount, caller) → false // → "unauthorized operator" - expect(() => { + await expect( token._unsafeTransferFrom( ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT, - ); - }).toThrow('MultiToken: unauthorized operator'); + ), + ).rejects.toThrow('MultiToken: unauthorized operator'); }); }); }); }); describe('_transfer', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); }); - it('should transfer whole', () => { - token._transfer(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); + it('should transfer whole', async () => { + await token._transfer(OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should transfer partial', () => { + it('should transfer partial', async () => { const partialAmt = AMOUNT - 1n; - token._transfer(OWNER.either, RECIPIENT.either, TOKEN_ID, partialAmt); + await token._transfer( + OWNER.either, + RECIPIENT.either, + TOKEN_ID, + partialAmt, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( AMOUNT - partialAmt, ); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(partialAmt); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + partialAmt, + ); }); - it('should allow transfer of 0 tokens', () => { - token._transfer(OWNER.either, RECIPIENT.either, TOKEN_ID, 0n); + it('should allow transfer of 0 tokens', async () => { + await token._transfer(OWNER.either, RECIPIENT.either, TOKEN_ID, 0n); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token._transfer( OWNER.either, RECIPIENT.either, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token._transfer( OWNER.either, RECIPIENT.either, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail when transfer from 0', () => { - expect(() => { - token._transfer(ZERO_ACCOUNT, RECIPIENT.either, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid sender'); + it('should fail when transfer from 0', async () => { + await expect( + token._transfer(ZERO_ACCOUNT, RECIPIENT.either, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid sender'); }); - it('should fail when transfer to 0', () => { - expect(() => { - token._transfer(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when transfer to 0', async () => { + await expect( + token._transfer(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail when transfer to contract address', () => { - expect(() => { - token._transfer(OWNER.either, RECIPIENT_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: unsafe transfer'); + it('should fail when transfer to contract address', async () => { + await expect( + token._transfer(OWNER.either, RECIPIENT_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: unsafe transfer'); }); - it('should handle non-canonical fromAddress (id)', () => { + it('should handle non-canonical fromAddress (id)', async () => { const nonCanonical = nonCanonicalLeft(OWNER.accountId); - token._transfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + await token._transfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical fromAddress (contract address)', () => { - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + it('should handle non-canonical fromAddress (contract address)', async () => { + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - token._transfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + await token._transfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); }); describe('_unsafeTransfer', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(0n); }); describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should transfer whole', () => { - token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, AMOUNT); + it('should transfer whole', async () => { + await token._unsafeTransfer( + OWNER.either, + recipient, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); }); - it('should transfer partial', () => { + it('should transfer partial', async () => { const partialAmt = AMOUNT - 1n; - token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, partialAmt); + await token._unsafeTransfer( + OWNER.either, + recipient, + TOKEN_ID, + partialAmt, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( AMOUNT - partialAmt, ); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(partialAmt); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual( + partialAmt, + ); }); - it('should allow transfer of 0 tokens', () => { - token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, 0n); + it('should allow transfer of 0 tokens', async () => { + await token._unsafeTransfer(OWNER.either, recipient, TOKEN_ID, 0n); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(0n); }); - it('should fail with insufficient balance', () => { - expect(() => { + it('should fail with insufficient balance', async () => { + await expect( token._unsafeTransfer( OWNER.either, recipient, TOKEN_ID, AMOUNT + 1n, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail with nonexistent id', () => { - expect(() => { + it('should fail with nonexistent id', async () => { + await expect( token._unsafeTransfer( OWNER.either, recipient, NONEXISTENT_ID, AMOUNT, - ); - }).toThrow('MultiToken: insufficient balance'); + ), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail when transfer from 0 (id)', () => { - expect(() => { - token._unsafeTransfer(ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid sender'); + it('should fail when transfer from 0 (id)', async () => { + await expect( + token._unsafeTransfer(ZERO_ACCOUNT, recipient, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid sender'); }); - it('should fail when transfer from 0 (contract address)', () => { - expect(() => { - token._unsafeTransfer(ZERO_CONTRACT, recipient, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid sender'); + it('should fail when transfer from 0 (contract address)', async () => { + await expect( + token._unsafeTransfer(ZERO_CONTRACT, recipient, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid sender'); }); }); - it('should handle non-canonical fromAddress (id)', () => { + it('should handle non-canonical fromAddress (id)', async () => { const nonCanonical = nonCanonicalLeft(OWNER.accountId); - token._unsafeTransfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + await token._unsafeTransfer( + nonCanonical, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical fromAddress (contract address)', () => { + it('should handle non-canonical fromAddress (contract address)', async () => { // Mint to contract address to test the transfer of non-canonical `fromAddress` - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - token._unsafeTransfer(nonCanonical, RECIPIENT.either, TOKEN_ID, AMOUNT); + await token._unsafeTransfer( + nonCanonical, + RECIPIENT.either, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical to (id)', () => { + it('should handle non-canonical to (id)', async () => { const nonCanonical = nonCanonicalLeft(RECIPIENT.accountId); - token._unsafeTransfer(OWNER.either, nonCanonical, TOKEN_ID, AMOUNT); + await token._unsafeTransfer( + OWNER.either, + nonCanonical, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should handle non-canonical to (contract address)', () => { + it('should handle non-canonical to (contract address)', async () => { const nonCanonical = nonCanonicalRight(RECIPIENT_CONTRACT.right); - token._unsafeTransfer(OWNER.either, nonCanonical, TOKEN_ID, AMOUNT); + await token._unsafeTransfer( + OWNER.either, + nonCanonical, + TOKEN_ID, + AMOUNT, + ); - expect(token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should fail when transfer to 0 (id)', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when transfer to 0 (id)', async () => { + await expect( + token._unsafeTransfer(OWNER.either, ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail when transfer to 0 (contract address)', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, ZERO_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when transfer to 0 (contract address)', async () => { + await expect( + token._unsafeTransfer(OWNER.either, ZERO_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); }); describe('_setURI', () => { - it('sets a new URI', () => { - token._setURI(NEW_URI); + it('sets a new URI', async () => { + await token._setURI(NEW_URI); - expect(token.uri(TOKEN_ID)).toEqual(NEW_URI); - expect(token.uri(TOKEN_ID2)).toEqual(NEW_URI); + expect(await token.uri(TOKEN_ID)).toEqual(NEW_URI); + expect(await token.uri(TOKEN_ID2)).toEqual(NEW_URI); }); - it('sets an empty URI → newURI → empty URI → URI', () => { + it('sets an empty URI → newURI → empty URI → URI', async () => { const URIS = [NO_STRING, NEW_URI, NO_STRING, URI]; for (let i = 0; i < URIS.length; i++) { - token._setURI(URIS[i]); + await token._setURI(URIS[i]); - expect(token.uri(TOKEN_ID)).toEqual(URIS[i]); - expect(token.uri(TOKEN_ID2)).toEqual(URIS[i]); + expect(await token.uri(TOKEN_ID)).toEqual(URIS[i]); + expect(await token.uri(TOKEN_ID2)).toEqual(URIS[i]); } }); - it('should handle long URI', () => { + it('should handle long URI', async () => { const LONG_URI = `https://example.com/${'a'.repeat(1000)}`; - token._setURI(LONG_URI); - expect(token.uri(TOKEN_ID)).toEqual(LONG_URI); + await token._setURI(LONG_URI); + expect(await token.uri(TOKEN_ID)).toEqual(LONG_URI); }); - it('should handle URI with special characters', () => { + it('should handle URI with special characters', async () => { const SPECIAL_URI = 'https://example.com/path?param=value#fragment'; - token._setURI(SPECIAL_URI); - expect(token.uri(TOKEN_ID)).toEqual(SPECIAL_URI); + await token._setURI(SPECIAL_URI); + expect(await token.uri(TOKEN_ID)).toEqual(SPECIAL_URI); }); }); describe('_mint', () => { - it('should update balance when minting', () => { - token._mint(RECIPIENT.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + it('should update balance when minting', async () => { + await token._mint(RECIPIENT.either, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should update balance with multiple mints', () => { + it('should update balance with multiple mints', async () => { for (let i = 0; i < 3; i++) { - token._mint(RECIPIENT.either, TOKEN_ID, 1n); + await token._mint(RECIPIENT.either, TOKEN_ID, 1n); } - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(3n); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(3n); }); - it('should fail when overflowing uint128', () => { - token._mint(RECIPIENT.either, TOKEN_ID, MAX_UINT128); + it('should fail when overflowing uint128', async () => { + await token._mint(RECIPIENT.either, TOKEN_ID, MAX_UINT128); - expect(() => { - token._mint(RECIPIENT.either, TOKEN_ID, 1n); - }).toThrow('MultiToken: arithmetic overflow'); + await expect( + token._mint(RECIPIENT.either, TOKEN_ID, 1n), + ).rejects.toThrow('MultiToken: arithmetic overflow'); }); - it('should fail when minting to zero address (id)', () => { - expect(() => { - token._mint(ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when minting to zero address (id)', async () => { + await expect( + token._mint(ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail when minting to zero address (contract)', () => { - expect(() => { - token._mint(ZERO_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: unsafe transfer'); + it('should fail when minting to zero address (contract)', async () => { + await expect( + token._mint(ZERO_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: unsafe transfer'); }); - it('should fail when minting to a contract address', () => { - expect(() => { - token._mint(RECIPIENT_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: unsafe transfer'); + it('should fail when minting to a contract address', async () => { + await expect( + token._mint(RECIPIENT_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: unsafe transfer'); }); - it('should canonicalize recipient', () => { + it('should canonicalize recipient', async () => { const nonCanonical = nonCanonicalLeft(RECIPIENT.accountId); - token._mint(nonCanonical, TOKEN_ID, AMOUNT); + await token._mint(nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); }); @@ -1177,167 +1327,179 @@ describe('MultiToken', () => { describe.each( recipientTypes, )('when the recipient is a %s', (_, recipient) => { - it('should update balance when minting', () => { - token._unsafeMint(recipient, TOKEN_ID, AMOUNT); + it('should update balance when minting', async () => { + await token._unsafeMint(recipient, TOKEN_ID, AMOUNT); - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT); }); - it('should update balance with multiple mints', () => { + it('should update balance with multiple mints', async () => { for (let i = 0; i < 3; i++) { - token._unsafeMint(recipient, TOKEN_ID, 1n); + await token._unsafeMint(recipient, TOKEN_ID, 1n); } - expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(3n); + expect(await token.balanceOf(recipient, TOKEN_ID)).toEqual(3n); }); - it('should fail when overflowing uint128', () => { - token._unsafeMint(recipient, TOKEN_ID, MAX_UINT128); + it('should fail when overflowing uint128', async () => { + await token._unsafeMint(recipient, TOKEN_ID, MAX_UINT128); - expect(() => { - token._unsafeMint(recipient, TOKEN_ID, 1n); - }).toThrow('MultiToken: arithmetic overflow'); + await expect( + token._unsafeMint(recipient, TOKEN_ID, 1n), + ).rejects.toThrow('MultiToken: arithmetic overflow'); }); }); - it('should fail when minting to zero address (id)', () => { - expect(() => { - token._unsafeMint(ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when minting to zero address (id)', async () => { + await expect( + token._unsafeMint(ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should fail when minting to zero address (contract)', () => { - expect(() => { - token._unsafeMint(ZERO_CONTRACT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid receiver'); + it('should fail when minting to zero address (contract)', async () => { + await expect( + token._unsafeMint(ZERO_CONTRACT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid receiver'); }); - it('should canonicalize recipient', () => { + it('should canonicalize recipient', async () => { const nonCanonical = nonCanonicalLeft(RECIPIENT.accountId); - token._unsafeMint(nonCanonical, TOKEN_ID, AMOUNT); + await token._unsafeMint(nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT.either, TOKEN_ID)).toEqual( + AMOUNT, + ); }); - it('should canonicalize contract address recipient', () => { + it('should canonicalize contract address recipient', async () => { const nonCanonical = nonCanonicalRight(RECIPIENT_CONTRACT.right); - token._unsafeMint(nonCanonical, TOKEN_ID, AMOUNT); + await token._unsafeMint(nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual(AMOUNT); + expect(await token.balanceOf(RECIPIENT_CONTRACT, TOKEN_ID)).toEqual( + AMOUNT, + ); }); }); describe('_burn', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT); }); - it('should burn tokens', () => { - token._burn(OWNER.either, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + it('should burn tokens', async () => { + await token._burn(OWNER.either, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); }); - it('should burn partial', () => { + it('should burn partial', async () => { const partialAmt = 1n; - token._burn(OWNER.either, TOKEN_ID, partialAmt); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + await token._burn(OWNER.either, TOKEN_ID, partialAmt); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( AMOUNT - partialAmt, ); }); - it('should update balance with multiple burns', () => { + it('should update balance with multiple burns', async () => { for (let i = 0; i < 3; i++) { - token._burn(OWNER.either, TOKEN_ID, 1n); + await token._burn(OWNER.either, TOKEN_ID, 1n); } - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(AMOUNT - 3n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual( + AMOUNT - 3n, + ); }); - it('should fail when not enough balance to burn', () => { - expect(() => { - token._burn(OWNER.either, TOKEN_ID, AMOUNT + 1n); - }).toThrow('MultiToken: insufficient balance'); + it('should fail when not enough balance to burn', async () => { + await expect( + token._burn(OWNER.either, TOKEN_ID, AMOUNT + 1n), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should fail when burning the zero address tokens', () => { - expect(() => { - token._burn(ZERO_ACCOUNT, TOKEN_ID, AMOUNT); - }).toThrow('MultiToken: invalid sender'); + it('should fail when burning the zero address tokens', async () => { + await expect( + token._burn(ZERO_ACCOUNT, TOKEN_ID, AMOUNT), + ).rejects.toThrow('MultiToken: invalid sender'); }); - it('should fail when burning tokens from nonexistent id', () => { - expect(() => { - token._burn(OWNER.either, NONEXISTENT_ID, AMOUNT); - }).toThrow('MultiToken: insufficient balance'); + it('should fail when burning tokens from nonexistent id', async () => { + await expect( + token._burn(OWNER.either, NONEXISTENT_ID, AMOUNT), + ).rejects.toThrow('MultiToken: insufficient balance'); }); - it('should handle non-canonical fromAddress (id)', () => { + it('should handle non-canonical fromAddress (id)', async () => { const nonCanonical = nonCanonicalLeft(OWNER.accountId); - token._burn(nonCanonical, TOKEN_ID, AMOUNT); + await token._burn(nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER.either, TOKEN_ID)).toEqual(0n); }); - it('should handle non-canonical fromAddress (contract address)', () => { - token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(AMOUNT); + it('should handle non-canonical fromAddress (contract address)', async () => { + await token._unsafeMint(OWNER_CONTRACT, TOKEN_ID, AMOUNT); + expect(await token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(AMOUNT); const nonCanonical = nonCanonicalRight(OWNER_CONTRACT.right); - token._burn(nonCanonical, TOKEN_ID, AMOUNT); + await token._burn(nonCanonical, TOKEN_ID, AMOUNT); - expect(token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); + expect(await token.balanceOf(OWNER_CONTRACT, TOKEN_ID)).toEqual(0n); }); }); describe('_setApprovalForAll', () => { - it('should return false when set to false', () => { - token._setApprovalForAll(OWNER.either, SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + it('should return false when set to false', async () => { + await token._setApprovalForAll(OWNER.either, SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( false, ); }); - it('should fail when attempting to approve zero address as an operator', () => { - expect(() => { - token._setApprovalForAll(OWNER.either, ZERO_ACCOUNT, true); - }).toThrow('MultiToken: invalid operator'); + it('should fail when attempting to approve zero address as an operator', async () => { + await expect( + token._setApprovalForAll(OWNER.either, ZERO_ACCOUNT, true), + ).rejects.toThrow('MultiToken: invalid operator'); }); - it('should fail when owner is zero address', () => { - expect(() => { - token._setApprovalForAll(ZERO_ACCOUNT, SPENDER.either, true); - }).toThrow('MultiToken: invalid owner'); + it('should fail when owner is zero address', async () => { + await expect( + token._setApprovalForAll(ZERO_ACCOUNT, SPENDER.either, true), + ).rejects.toThrow('MultiToken: invalid owner'); }); - it('should set → unset → set operator', () => { - token._setApprovalForAll(OWNER.either, SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + it('should set → unset → set operator', async () => { + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); - token._setApprovalForAll(OWNER.either, SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + await token._setApprovalForAll(OWNER.either, SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( false, ); - token._setApprovalForAll(OWNER.either, SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should canonicalize owner and operator', () => { + it('should canonicalize owner and operator', async () => { const nonCanonicalOwner = nonCanonicalLeft(OWNER.accountId); const nonCanonicalOp = nonCanonicalLeft(SPENDER.accountId); - token._setApprovalForAll(nonCanonicalOwner, nonCanonicalOp, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token._setApprovalForAll(nonCanonicalOwner, nonCanonicalOp, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); }); }); describe('simulator wiring', () => { - it('should expose the balances map via getPublicState', () => { - const sim = new MultiTokenSimulator(initWithURI); + it('should expose the balances map via getPublicState', async () => { + const sim = await MultiTokenSimulator.create(initWithURI); - const ledgerState = sim.getPublicState(); + const ledgerState = await sim.getPublicState(); expect(ledgerState.MultiToken__balances.isEmpty()).toBe(true); expect(ledgerState.MultiToken__balances.size()).toBe(0n); @@ -1346,19 +1508,21 @@ describe('MultiToken', () => { describe('privateState helpers', () => { describe('getCurrentSecretKey', () => { - it('should return the injected secret key', () => { - const sim = new MultiTokenSimulator(initWithURI); - sim.privateState.injectSecretKey(OWNER.secretKey); + it('should return the injected secret key', async () => { + const sim = await MultiTokenSimulator.create(initWithURI); + await sim.privateState.injectSecretKey(OWNER.secretKey); - expect(sim.privateState.getCurrentSecretKey()).toEqual(OWNER.secretKey); + expect(await sim.privateState.getCurrentSecretKey()).toEqual( + OWNER.secretKey, + ); }); - it('should throw when the secret key is undefined', () => { - const sim = new MultiTokenSimulator(initWithURI, { + it('should throw when the secret key is undefined', async () => { + const sim = await MultiTokenSimulator.create(initWithURI, { privateState: { secretKey: undefined as unknown as Uint8Array }, }); - expect(() => sim.privateState.getCurrentSecretKey()).toThrow( + await expect(sim.privateState.getCurrentSecretKey()).rejects.toThrow( 'Missing secret key', ); }); diff --git a/contracts/src/token/test/nonFungibleToken.test.ts b/contracts/src/token/test/nonFungibleToken.test.ts index 55a1f37e..95f411c6 100644 --- a/contracts/src/token/test/nonFungibleToken.test.ts +++ b/contracts/src/token/test/nonFungibleToken.test.ts @@ -83,81 +83,97 @@ let token: NonFungibleTokenSimulator; describe('NonFungibleToken', () => { describe('initializer and metadata', () => { - it('should initialize metadata', () => { - token = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); - expect(token.name()).toEqual(NAME); - expect(token.symbol()).toEqual(SYMBOL); + it('should initialize metadata', async () => { + token = await NonFungibleTokenSimulator.create(NAME, SYMBOL, INIT); + expect(await token.name()).toEqual(NAME); + expect(await token.symbol()).toEqual(SYMBOL); }); - it('should initialize empty metadata', () => { - token = new NonFungibleTokenSimulator(EMPTY_STRING, EMPTY_STRING, INIT); - expect(token.name()).toEqual(EMPTY_STRING); - expect(token.symbol()).toEqual(EMPTY_STRING); + it('should initialize empty metadata', async () => { + token = await NonFungibleTokenSimulator.create( + EMPTY_STRING, + EMPTY_STRING, + INIT, + ); + expect(await token.name()).toEqual(EMPTY_STRING); + expect(await token.symbol()).toEqual(EMPTY_STRING); }); - it('should initialize metadata with whitespace', () => { - token = new NonFungibleTokenSimulator(' NAME ', ' SYMBOL ', INIT); - expect(token.name()).toEqual(' NAME '); - expect(token.symbol()).toEqual(' SYMBOL '); + it('should initialize metadata with whitespace', async () => { + token = await NonFungibleTokenSimulator.create( + ' NAME ', + ' SYMBOL ', + INIT, + ); + expect(await token.name()).toEqual(' NAME '); + expect(await token.symbol()).toEqual(' SYMBOL '); }); - it('should initialize metadata with special characters', () => { - token = new NonFungibleTokenSimulator('NAME!@#', 'SYMBOL$%^', INIT); - expect(token.name()).toEqual('NAME!@#'); - expect(token.symbol()).toEqual('SYMBOL$%^'); + it('should initialize metadata with special characters', async () => { + token = await NonFungibleTokenSimulator.create( + 'NAME!@#', + 'SYMBOL$%^', + INIT, + ); + expect(await token.name()).toEqual('NAME!@#'); + expect(await token.symbol()).toEqual('SYMBOL$%^'); }); - it('should initialize metadata with very long strings', () => { + it('should initialize metadata with very long strings', async () => { const longName = 'A'.repeat(1000); const longSymbol = 'B'.repeat(1000); - token = new NonFungibleTokenSimulator(longName, longSymbol, INIT); - expect(token.name()).toEqual(longName); - expect(token.symbol()).toEqual(longSymbol); + token = await NonFungibleTokenSimulator.create( + longName, + longSymbol, + INIT, + ); + expect(await token.name()).toEqual(longName); + expect(await token.symbol()).toEqual(longSymbol); }); }); - beforeEach(() => { - token = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); + beforeEach(async () => { + token = await NonFungibleTokenSimulator.create(NAME, SYMBOL, INIT); }); describe('balanceOf', () => { - it('should return zero when requested account has no balance', () => { - expect(token.balanceOf(OWNER.either)).toEqual(0n); + it('should return zero when requested account has no balance', async () => { + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); - it('should return balance when requested account has tokens', () => { - token._mint(OWNER.either, AMOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(AMOUNT); + it('should return balance when requested account has tokens', async () => { + await token._mint(OWNER.either, AMOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(AMOUNT); }); - it('should return correct balance for multiple tokens', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); - expect(token.balanceOf(OWNER.either)).toEqual(3n); + it('should return correct balance for multiple tokens', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); + expect(await token.balanceOf(OWNER.either)).toEqual(3n); }); - it('should return correct balance after burning multiple tokens', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); - token._burn(TOKENID_1); - token._burn(TOKENID_2); - expect(token.balanceOf(OWNER.either)).toEqual(1n); + it('should return correct balance after burning multiple tokens', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); + await token._burn(TOKENID_1); + await token._burn(TOKENID_2); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); }); - it('should return correct balance after transferring multiple tokens', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); - token._transfer(OWNER.either, RECIPIENT.either, TOKENID_1); - token._transfer(OWNER.either, RECIPIENT.either, TOKENID_2); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(RECIPIENT.either)).toEqual(2n); + it('should return correct balance after transferring multiple tokens', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); + await token._transfer(OWNER.either, RECIPIENT.either, TOKENID_1); + await token._transfer(OWNER.either, RECIPIENT.either, TOKENID_2); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(RECIPIENT.either)).toEqual(2n); }); - it('should return correct balance with non-canonical lookup (left)', () => { - token._mint(OWNER.either, TOKENID_1); + it('should return correct balance with non-canonical lookup (left)', async () => { + await token._mint(OWNER.either, TOKENID_1); const nonCanonical = { is_left: true, @@ -165,11 +181,11 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.balanceOf(nonCanonical)).toEqual(1n); + expect(await token.balanceOf(nonCanonical)).toEqual(1n); }); - it('should return correct balance with non-canonical lookup (right)', () => { - token._unsafeMint(SOME_CONTRACT, TOKENID_1); + it('should return correct balance with non-canonical lookup (right)', async () => { + await token._unsafeMint(SOME_CONTRACT, TOKENID_1); const nonCanonical = { is_left: false, @@ -177,322 +193,342 @@ describe('NonFungibleToken', () => { right: SOME_CONTRACT.right, }; - expect(token.balanceOf(nonCanonical)).toEqual(1n); + expect(await token.balanceOf(nonCanonical)).toEqual(1n); }); }); describe('ownerOf', () => { - it('should throw if token does not exist', () => { - expect(() => { - token.ownerOf(NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect(token.ownerOf(NON_EXISTENT_TOKEN)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should throw if token has been burned', () => { - token._mint(OWNER.either, TOKENID_1); - token._burn(TOKENID_1); - expect(() => { - token.ownerOf(TOKENID_1); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token has been burned', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._burn(TOKENID_1); + await expect(token.ownerOf(TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should return owner of token if it exists', () => { - token._mint(OWNER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + it('should return owner of token if it exists', async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); }); - it('should return correct owner for multiple tokens', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - expect(token.ownerOf(TOKENID_2)).toEqual(OWNER.either); - expect(token.ownerOf(TOKENID_3)).toEqual(OWNER.either); + it('should return correct owner for multiple tokens', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + expect(await token.ownerOf(TOKENID_2)).toEqual(OWNER.either); + expect(await token.ownerOf(TOKENID_3)).toEqual(OWNER.either); }); - it('should return correct owner after multiple transfers', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._transfer(OWNER.either, SPENDER.either, TOKENID_1); - token._transfer(OWNER.either, OTHER.either, TOKENID_2); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); - expect(token.ownerOf(TOKENID_2)).toEqual(OTHER.either); + it('should return correct owner after multiple transfers', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._transfer(OWNER.either, SPENDER.either, TOKENID_1); + await token._transfer(OWNER.either, OTHER.either, TOKENID_2); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + expect(await token.ownerOf(TOKENID_2)).toEqual(OTHER.either); }); - it('should return correct owner after multiple burns and mints', () => { - token._mint(OWNER.either, TOKENID_1); - token._burn(TOKENID_1); - token._mint(SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + it('should return correct owner after multiple burns and mints', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._burn(TOKENID_1); + await token._mint(SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); }); describe('tokenURI', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should throw if token does not exist', () => { - expect(() => { - token.tokenURI(NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect(token.tokenURI(NON_EXISTENT_TOKEN)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should return the empty string for an unset tokenURI', () => { - expect(token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); + it('should return the empty string for an unset tokenURI', async () => { + expect(await token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); }); - it('should return the empty string if tokenURI set as default value', () => { - token._setTokenURI(TOKENID_1, EMPTY_URI); - expect(token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); + it('should return the empty string if tokenURI set as default value', async () => { + await token._setTokenURI(TOKENID_1, EMPTY_URI); + expect(await token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); }); - it('should return some string if tokenURI is set', () => { - token._setTokenURI(TOKENID_1, SOME_URI); - expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + it('should return some string if tokenURI is set', async () => { + await token._setTokenURI(TOKENID_1, SOME_URI); + expect(await token.tokenURI(TOKENID_1)).toEqual(SOME_URI); }); - it('should return very long tokenURI', () => { + it('should return very long tokenURI', async () => { const longURI = 'A'.repeat(1000); - token._setTokenURI(TOKENID_1, longURI); - expect(token.tokenURI(TOKENID_1)).toEqual(longURI); + await token._setTokenURI(TOKENID_1, longURI); + expect(await token.tokenURI(TOKENID_1)).toEqual(longURI); }); - it('should return tokenURI with special characters', () => { + it('should return tokenURI with special characters', async () => { const specialURI = '!@#$%^&*()_+'; - token._setTokenURI(TOKENID_1, specialURI); - expect(token.tokenURI(TOKENID_1)).toEqual(specialURI); + await token._setTokenURI(TOKENID_1, specialURI); + expect(await token.tokenURI(TOKENID_1)).toEqual(specialURI); }); - it('should update tokenURI multiple times', () => { - token._setTokenURI(TOKENID_1, 'URI1'); - token._setTokenURI(TOKENID_1, 'URI2'); - token._setTokenURI(TOKENID_1, 'URI3'); - expect(token.tokenURI(TOKENID_1)).toEqual('URI3'); + it('should update tokenURI multiple times', async () => { + await token._setTokenURI(TOKENID_1, 'URI1'); + await token._setTokenURI(TOKENID_1, 'URI2'); + await token._setTokenURI(TOKENID_1, 'URI3'); + expect(await token.tokenURI(TOKENID_1)).toEqual('URI3'); }); - it('should maintain tokenURI after token transfer', () => { - token._setTokenURI(TOKENID_1, SOME_URI); - token._transfer(OWNER.either, RECIPIENT.either, TOKENID_1); - expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + it('should maintain tokenURI after token transfer', async () => { + await token._setTokenURI(TOKENID_1, SOME_URI); + await token._transfer(OWNER.either, RECIPIENT.either, TOKENID_1); + expect(await token.tokenURI(TOKENID_1)).toEqual(SOME_URI); }); }); describe('approve', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should throw if not owner', () => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - token.approve(SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid approver'); + it('should throw if not owner', async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect(token.approve(SPENDER.either, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid approver', + ); }); - it('should approve spender', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should approve spender', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should allow operator to approve', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should allow operator to approve', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.approve(OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(OTHER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.approve(OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(OTHER.either); }); - it('spender approved for only TOKENID_1 should not be able to approve', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('spender approved for only TOKENID_1 should not be able to approve', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.approve(OTHER.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid approver'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect(token.approve(OTHER.either, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid approver', + ); }); - it('should approve same address multiple times', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token.approve(SPENDER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should approve same address multiple times', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token.approve(SPENDER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should approve after token transfer', () => { - token._transfer(OWNER.either, SPENDER.either, TOKENID_1); + it('should approve after token transfer', async () => { + await token._transfer(OWNER.either, SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.approve(OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(OTHER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.approve(OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(OTHER.either); }); - it('should approve after token burn and remint', () => { - token._burn(TOKENID_1); - token._mint(OWNER.either, TOKENID_1); + it('should approve after token burn and remint', async () => { + await token._burn(TOKENID_1); + await token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should approve with very long token ID', () => { + it('should approve with very long token ID', async () => { const longTokenId = BigInt('18446744073709551615'); - token._mint(OWNER.either, longTokenId); + await token._mint(OWNER.either, longTokenId); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, longTokenId); - expect(token.getApproved(longTokenId)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, longTokenId); + expect(await token.getApproved(longTokenId)).toEqual(SPENDER.either); }); - it('should normalize right-variant zero approval', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(ZERO_CONTRACT, TOKENID_1); + it('should normalize right-variant zero approval', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(ZERO_CONTRACT, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('getApproved', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should throw if token does not exist', () => { - expect(() => { - token.getApproved(NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect(token.getApproved(NON_EXISTENT_TOKEN)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should throw if token has been burned', () => { - token._burn(TOKENID_1); - expect(() => { - token.getApproved(TOKENID_1); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token has been burned', async () => { + await token._burn(TOKENID_1); + await expect(token.getApproved(TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should get current approved spender', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(OWNER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(OWNER.either); + it('should get current approved spender', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(OWNER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(OWNER.either); }); - it('should return zero if approval not set', () => { - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should return zero if approval not set', async () => { + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('setApprovalForAll', () => { - it('should not approve zero address', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should not approve zero address', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.setApprovalForAll(ZERO_ACCOUNT, true); - }).toThrow('NonFungibleToken: invalid operator'); + await expect(token.setApprovalForAll(ZERO_ACCOUNT, true)).rejects.toThrow( + 'NonFungibleToken: invalid operator', + ); }); - it('should set operator', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should set operator', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should allow operator to manage owner tokens', () => { - token._mint(OWNER.either, TOKENID_1); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); + it('should allow operator to manage owner tokens', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); - token.approve(OTHER.either, TOKENID_2); - expect(token.getApproved(TOKENID_2)).toEqual(OTHER.either); + await token.approve(OTHER.either, TOKENID_2); + expect(await token.getApproved(TOKENID_2)).toEqual(OTHER.either); - token.approve(SPENDER.either, TOKENID_3); - expect(token.getApproved(TOKENID_3)).toEqual(SPENDER.either); + await token.approve(SPENDER.either, TOKENID_3); + expect(await token.getApproved(TOKENID_3)).toEqual(SPENDER.either); }); - it('should revoke approval for all', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should revoke approval for all', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); - token.setApprovalForAll(SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(false); + await token.setApprovalForAll(SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + false, + ); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.approve(SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid approver'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect(token.approve(SPENDER.either, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid approver', + ); }); - it('should set approval for all to same address multiple times', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should set approval for all to same address multiple times', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should set approval for all after token transfer', () => { - token._mint(OWNER.either, TOKENID_1); - token._transfer(OWNER.either, SPENDER.either, TOKENID_1); + it('should set approval for all after token transfer', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._transfer(OWNER.either, SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.setApprovalForAll(OTHER.either, true); - expect(token.isApprovedForAll(SPENDER.either, OTHER.either)).toBe(true); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.setApprovalForAll(OTHER.either, true); + expect(await token.isApprovedForAll(SPENDER.either, OTHER.either)).toBe( + true, + ); }); - it('should set approval for all with multiple operators', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); + it('should set approval for all with multiple operators', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(OTHER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); - expect(token.isApprovedForAll(OWNER.either, OTHER.either)).toBe(true); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(OTHER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); + expect(await token.isApprovedForAll(OWNER.either, OTHER.either)).toBe( + true, + ); }); - it('should set approval for all with very long token IDs', () => { + it('should set approval for all with very long token IDs', async () => { const longTokenId = BigInt('18446744073709551615'); - token._mint(OWNER.either, longTokenId); - token.privateState.injectSecretKey(OWNER.secretKey); + await token._mint(OWNER.either, longTokenId); + await token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); }); describe('isApprovedForAll', () => { - it('should return false if approval not set', () => { - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(false); + it('should return false if approval not set', async () => { + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + false, + ); }); - it('should return true if approval set', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + it('should return true if approval set', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should return correct result with non-canonical owner lookup', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should return correct result with non-canonical owner lookup', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); const nonCanonical = { is_left: true, @@ -500,13 +536,15 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.isApprovedForAll(nonCanonical, SPENDER.either)).toBe(true); + expect(await token.isApprovedForAll(nonCanonical, SPENDER.either)).toBe( + true, + ); }); - it('should return correct result with non-canonical operator lookup', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should return correct result with non-canonical operator lookup', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); const nonCanonical = { is_left: true, @@ -514,238 +552,240 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - expect(token.isApprovedForAll(OWNER.either, nonCanonical)).toBe(true); + expect(await token.isApprovedForAll(OWNER.either, nonCanonical)).toBe( + true, + ); }); }); describe('transferFrom', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should not transfer to ContractAddress', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: unsafe transfer'); + it('should not transfer to ContractAddress', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.transferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: unsafe transfer'); }); - it('should not transfer to zero address', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.transferFrom(OWNER.either, ZERO_ACCOUNT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should not transfer from zero address', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transferFrom(ZERO_ACCOUNT, SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: incorrect owner'); + it('should not transfer from zero address', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.transferFrom(ZERO_ACCOUNT, SPENDER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: incorrect owner'); }); - it('should not transfer from unauthorized', () => { - token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); - expect(() => { - token.transferFrom(OWNER.either, UNAUTHORIZED.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + it('should not transfer from unauthorized', async () => { + await token.privateState.injectSecretKey(UNAUTHORIZED.secretKey); + await expect( + token.transferFrom(OWNER.either, UNAUTHORIZED.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should not transfer token that has not been minted', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should not transfer token that has not been minted', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.transferFrom(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should transfer token without approvers or operators', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.transferFrom(OWNER.either, RECIPIENT.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(RECIPIENT.either); + it('should transfer token without approvers or operators', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.transferFrom(OWNER.either, RECIPIENT.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(RECIPIENT.either); }); - it('should transfer token via approved operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('should transfer token via approved operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should transfer token via approvedForAll operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should transfer token via approvedForAll operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should allow transfer to same address', () => { - token._approve(SPENDER.either, TOKENID_1, OWNER.either); - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + it('should allow transfer to same address', async () => { + await token._approve(SPENDER.either, TOKENID_1, OWNER.either); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, OWNER.either, TOKENID_1); - }).not.toThrow(); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token.transferFrom(OWNER.either, OWNER.either, TOKENID_1), + ).resolves.not.toThrow(); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); expect( - token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1), + await token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1), ).toEqual(true); }); - it('should not transfer after approval revocation', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token.approve(ZERO_ACCOUNT, TOKENID_1); + it('should not transfer after approval revocation', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token.approve(ZERO_ACCOUNT, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should not transfer after approval for all revocation', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token.setApprovalForAll(SPENDER.either, false); + it('should not transfer after approval for all revocation', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + await token.setApprovalForAll(SPENDER.either, false); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should transfer multiple tokens in sequence', () => { - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); + it('should transfer multiple tokens in sequence', async () => { + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token.approve(SPENDER.either, TOKENID_2); - token.approve(SPENDER.either, TOKENID_3); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token.approve(SPENDER.either, TOKENID_2); + await token.approve(SPENDER.either, TOKENID_3); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_2); - token.transferFrom(OWNER.either, SPENDER.either, TOKENID_3); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_1); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_2); + await token.transferFrom(OWNER.either, SPENDER.either, TOKENID_3); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); - expect(token.ownerOf(TOKENID_2)).toEqual(SPENDER.either); - expect(token.ownerOf(TOKENID_3)).toEqual(SPENDER.either); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + expect(await token.ownerOf(TOKENID_2)).toEqual(SPENDER.either); + expect(await token.ownerOf(TOKENID_3)).toEqual(SPENDER.either); }); - it('should transfer with very long token IDs', () => { + it('should transfer with very long token IDs', async () => { const longTokenId = BigInt('18446744073709551615'); - token._mint(OWNER.either, longTokenId); + await token._mint(OWNER.either, longTokenId); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, longTokenId); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, longTokenId); - token.privateState.injectSecretKey(SPENDER.secretKey); - token.transferFrom(OWNER.either, SPENDER.either, longTokenId); - expect(token.ownerOf(longTokenId)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token.transferFrom(OWNER.either, SPENDER.either, longTokenId); + expect(await token.ownerOf(longTokenId)).toEqual(SPENDER.either); }); - it('should revoke approval after transferFrom', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token._setApprovalForAll(OWNER.either, SPENDER.either, true); + it('should revoke approval after transferFrom', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); - token.transferFrom(OWNER.either, OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); - expect(token._isAuthorized(OTHER.either, SPENDER.either, TOKENID_1)).toBe( - false, - ); + await token.transferFrom(OWNER.either, OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect( + await token._isAuthorized(OTHER.either, SPENDER.either, TOKENID_1), + ).toBe(false); - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token.approve(UNAUTHORIZED.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid approver'); - expect(() => { - token.transferFrom(OTHER.either, UNAUTHORIZED.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token.approve(UNAUTHORIZED.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid approver'); + await expect( + token.transferFrom(OTHER.either, UNAUTHORIZED.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should store canonical zero after clearing approval via transfer', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('should store canonical zero after clearing approval via transfer', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token.transferFrom(OWNER.either, RECIPIENT.either, TOKENID_1); + await token.transferFrom(OWNER.either, RECIPIENT.either, TOKENID_1); // _update calls _approve(zeroAccount(), tokenId, zeroAccount()) internally, // which should store the left-variant zero - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('_requireOwned', () => { - it('should throw if token has not been minted', () => { - expect(() => { - token._requireOwned(TOKENID_1); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token has not been minted', async () => { + await expect(token._requireOwned(TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should throw if token has been burned', () => { - token._mint(OWNER.either, TOKENID_1); - token._burn(TOKENID_1); - expect(() => { - token._requireOwned(TOKENID_1); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token has been burned', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._burn(TOKENID_1); + await expect(token._requireOwned(TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: nonexistent token', + ); }); - it('should return correct owner', () => { - token._mint(OWNER.either, TOKENID_1); - expect(token._requireOwned(TOKENID_1)).toEqual(OWNER.either); + it('should return correct owner', async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token._requireOwned(TOKENID_1)).toEqual(OWNER.either); }); }); describe('_ownerOf', () => { - it('should return zero address if token does not exist', () => { - expect(token._ownerOf(NON_EXISTENT_TOKEN)).toEqual(ZERO_ACCOUNT); + it('should return zero address if token does not exist', async () => { + expect(await token._ownerOf(NON_EXISTENT_TOKEN)).toEqual(ZERO_ACCOUNT); }); - it('should return owner of token', () => { - token._mint(OWNER.either, TOKENID_1); - expect(token._ownerOf(TOKENID_1)).toEqual(OWNER.either); + it('should return owner of token', async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token._ownerOf(TOKENID_1)).toEqual(OWNER.either); }); }); describe('_approve', () => { - it('should approve if auth is owner', () => { - token._mint(OWNER.either, TOKENID_1); - token._approve(SPENDER.either, TOKENID_1, OWNER.either); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should approve if auth is owner', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._approve(SPENDER.either, TOKENID_1, OWNER.either); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should approve if auth is approved for all', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should approve if auth is approved for all', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token._approve(SPENDER.either, TOKENID_1, SPENDER.either); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + await token._approve(SPENDER.either, TOKENID_1, SPENDER.either); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should throw if auth is unauthorized', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._approve(SPENDER.either, TOKENID_1, UNAUTHORIZED.either); - }).toThrow('NonFungibleToken: invalid approver'); + it('should throw if auth is unauthorized', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect( + token._approve(SPENDER.either, TOKENID_1, UNAUTHORIZED.either), + ).rejects.toThrow('NonFungibleToken: invalid approver'); }); - it('should approve if auth is zero address', () => { - token._mint(OWNER.either, TOKENID_1); - token._approve(SPENDER.either, TOKENID_1, ZERO_ACCOUNT); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should approve if auth is zero address', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._approve(SPENDER.either, TOKENID_1, ZERO_ACCOUNT); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should canonicalize approved address', () => { - token._mint(OWNER.either, TOKENID_1); + it('should canonicalize approved address', async () => { + await token._mint(OWNER.either, TOKENID_1); const nonCanonical = { is_left: true, @@ -753,165 +793,173 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - token._approve(nonCanonical, TOKENID_1, OWNER.either); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + await token._approve(nonCanonical, TOKENID_1, OWNER.either); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should normalize right-variant zero to zeroAccount()', () => { - token._mint(OWNER.either, TOKENID_1); + it('should normalize right-variant zero to zeroAccount()', async () => { + await token._mint(OWNER.either, TOKENID_1); // Approve with a right-variant zero (contract address zero) - token._approve(ZERO_CONTRACT, TOKENID_1, OWNER.either); + await token._approve(ZERO_CONTRACT, TOKENID_1, OWNER.either); // getApproved should return the left-variant zeroAccount, not the right-variant - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should normalize left-variant zero to zeroAccount()', () => { - token._mint(OWNER.either, TOKENID_1); + it('should normalize left-variant zero to zeroAccount()', async () => { + await token._mint(OWNER.either, TOKENID_1); // First set a real approval - token._approve(SPENDER.either, TOKENID_1, OWNER.either); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + await token._approve(SPENDER.either, TOKENID_1, OWNER.either); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); // Clear it with left-variant zero - token._approve(ZERO_ACCOUNT, TOKENID_1, OWNER.either); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + await token._approve(ZERO_ACCOUNT, TOKENID_1, OWNER.either); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('_checkAuthorized', () => { - it('should throw if token not minted', () => { - expect(() => { - token._checkAuthorized(ZERO_ACCOUNT, OWNER.either, TOKENID_1); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token not minted', async () => { + await expect( + token._checkAuthorized(ZERO_ACCOUNT, OWNER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should throw if unauthorized', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._checkAuthorized(OWNER.either, UNAUTHORIZED.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + it('should throw if unauthorized', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect( + token._checkAuthorized(OWNER.either, UNAUTHORIZED.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should not throw if approved', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token._checkAuthorized(OWNER.either, SPENDER.either, TOKENID_1); + it('should not throw if approved', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token._checkAuthorized(OWNER.either, SPENDER.either, TOKENID_1); }); - it('should not throw if approvedForAll', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - token._checkAuthorized(OWNER.either, SPENDER.either, TOKENID_1); + it('should not throw if approvedForAll', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + await token._checkAuthorized(OWNER.either, SPENDER.either, TOKENID_1); }); }); describe('_isAuthorized', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should return true if spender is authorized', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - expect(token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1)).toBe( - true, - ); + it('should return true if spender is authorized', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + expect( + await token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1), + ).toBe(true); }); - it('should return true if spender is authorized for all', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1)).toBe( - true, - ); + it('should return true if spender is authorized for all', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + expect( + await token._isAuthorized(OWNER.either, SPENDER.either, TOKENID_1), + ).toBe(true); }); - it('should return true if spender is owner', () => { - expect(token._isAuthorized(OWNER.either, OWNER.either, TOKENID_1)).toBe( - true, - ); + it('should return true if spender is owner', async () => { + expect( + await token._isAuthorized(OWNER.either, OWNER.either, TOKENID_1), + ).toBe(true); }); - it('should return false if spender is zero address', () => { - expect(token._isAuthorized(OWNER.either, ZERO_ACCOUNT, TOKENID_1)).toBe( - false, - ); + it('should return false if spender is zero address', async () => { + expect( + await token._isAuthorized(OWNER.either, ZERO_ACCOUNT, TOKENID_1), + ).toBe(false); }); - it('should return false for unauthorized', () => { + it('should return false for unauthorized', async () => { expect( - token._isAuthorized(OWNER.either, UNAUTHORIZED.either, TOKENID_1), + await token._isAuthorized(OWNER.either, UNAUTHORIZED.either, TOKENID_1), ).toBe(false); }); }); describe('_getApproved', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should return zero address if token is not minted', () => { - expect(token._getApproved(NON_EXISTENT_TOKEN)).toEqual(ZERO_ACCOUNT); + it('should return zero address if token is not minted', async () => { + expect(await token._getApproved(NON_EXISTENT_TOKEN)).toEqual( + ZERO_ACCOUNT, + ); }); - it('should return approved address', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - expect(token._getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should return approved address', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + expect(await token._getApproved(TOKENID_1)).toEqual(SPENDER.either); }); - it('should return zero address if no approvals', () => { - expect(token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should return zero address if no approvals', async () => { + expect(await token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('_setApprovalForAll', () => { - it('should approve operator', () => { - token._mint(OWNER.either, TOKENID_1); - token._setApprovalForAll(OWNER.either, SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + it('should approve operator', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._setApprovalForAll(OWNER.either, SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); - it('should revoke operator approval', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + it('should revoke operator approval', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); - token._setApprovalForAll(OWNER.either, SPENDER.either, false); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(false); + await token._setApprovalForAll(OWNER.either, SPENDER.either, false); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + false, + ); }); - it('should throw if operator is zero address (left)', () => { - expect(() => { - token._setApprovalForAll(OWNER.either, ZERO_ACCOUNT, true); - }).toThrow('NonFungibleToken: invalid operator'); + it('should throw if operator is zero address (left)', async () => { + await expect( + token._setApprovalForAll(OWNER.either, ZERO_ACCOUNT, true), + ).rejects.toThrow('NonFungibleToken: invalid operator'); }); - it('should throw if operator is zero address (right)', () => { - expect(() => { - token._setApprovalForAll(OWNER.either, ZERO_CONTRACT, true); - }).toThrow('NonFungibleToken: invalid operator'); + it('should throw if operator is zero address (right)', async () => { + await expect( + token._setApprovalForAll(OWNER.either, ZERO_CONTRACT, true), + ).rejects.toThrow('NonFungibleToken: invalid operator'); }); - it('should fail if owner is zero address (left)', () => { - expect(() => { - token._setApprovalForAll(ZERO_ACCOUNT, RECIPIENT.either, true); - }).toThrow('NonFungibleToken: invalid owner'); + it('should fail if owner is zero address (left)', async () => { + await expect( + token._setApprovalForAll(ZERO_ACCOUNT, RECIPIENT.either, true), + ).rejects.toThrow('NonFungibleToken: invalid owner'); }); - it('should fail if owner is zero address (right)', () => { - expect(() => { - token._setApprovalForAll(ZERO_CONTRACT, RECIPIENT.either, true); - }).toThrow('NonFungibleToken: invalid owner'); + it('should fail if owner is zero address (right)', async () => { + await expect( + token._setApprovalForAll(ZERO_CONTRACT, RECIPIENT.either, true), + ).rejects.toThrow('NonFungibleToken: invalid owner'); }); - it('should canonicalize owner and operator', () => { - token._mint(OWNER.either, TOKENID_1); + it('should canonicalize owner and operator', async () => { + await token._mint(OWNER.either, TOKENID_1); const nonCanonicalOwner = { is_left: true, @@ -924,434 +972,436 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - token._setApprovalForAll(nonCanonicalOwner, nonCanonicalOp, true); - expect(token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe(true); + await token._setApprovalForAll(nonCanonicalOwner, nonCanonicalOp, true); + expect(await token.isApprovedForAll(OWNER.either, SPENDER.either)).toBe( + true, + ); }); }); describe('_mint', () => { - it('should not mint to ContractAddress', () => { - expect(() => { - token._mint(SOME_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: unsafe transfer'); + it('should not mint to ContractAddress', async () => { + await expect(token._mint(SOME_CONTRACT, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: unsafe transfer', + ); }); - it('should not mint to zero address', () => { - expect(() => { - token._mint(ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not mint to zero address', async () => { + await expect(token._mint(ZERO_ACCOUNT, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid receiver', + ); }); - it('should not mint a token that already exists', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._mint(OWNER.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid sender'); + it('should not mint a token that already exists', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect(token._mint(OWNER.either, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid sender', + ); }); - it('should mint token', () => { - token._mint(OWNER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - expect(token.balanceOf(OWNER.either)).toEqual(1n); + it('should mint token', async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); - expect(token.balanceOf(OWNER.either)).toEqual(3n); + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); + expect(await token.balanceOf(OWNER.either)).toEqual(3n); }); - it('should mint multiple tokens in sequence', () => { + it('should mint multiple tokens in sequence', async () => { for (let i = 0; i < 10; i++) { - token._mint(OWNER.either, TOKENID_1 + BigInt(i)); + await token._mint(OWNER.either, TOKENID_1 + BigInt(i)); } - expect(token.balanceOf(OWNER.either)).toEqual(10n); + expect(await token.balanceOf(OWNER.either)).toEqual(10n); }); - it('should mint with very long token IDs', () => { + it('should mint with very long token IDs', async () => { const longTokenId = BigInt('18446744073709551615'); - token._mint(OWNER.either, longTokenId); - expect(token.ownerOf(longTokenId)).toEqual(OWNER.either); + await token._mint(OWNER.either, longTokenId); + expect(await token.ownerOf(longTokenId)).toEqual(OWNER.either); }); - it('should mint after burning', () => { - token._mint(OWNER.either, TOKENID_1); - token._burn(TOKENID_1); - token._mint(OWNER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + it('should mint after burning', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._burn(TOKENID_1); + await token._mint(OWNER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); }); - it('should mint with special characters in metadata', () => { - token._mint(OWNER.either, TOKENID_1); - token._setTokenURI(TOKENID_1, '!@#$%^&*()_+'); - expect(token.tokenURI(TOKENID_1)).toEqual('!@#$%^&*()_+'); + it('should mint with special characters in metadata', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._setTokenURI(TOKENID_1, '!@#$%^&*()_+'); + expect(await token.tokenURI(TOKENID_1)).toEqual('!@#$%^&*()_+'); }); - it('should canonicalize recipient', () => { + it('should canonicalize recipient', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._mint(nonCanonical, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - expect(token.balanceOf(OWNER.either)).toEqual(1n); + await token._mint(nonCanonical, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); }); - it('should not mint to zero (contract)', () => { - expect(() => { - token._mint(ZERO_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: unsafe transfer'); + it('should not mint to zero (contract)', async () => { + await expect(token._mint(ZERO_CONTRACT, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: unsafe transfer', + ); }); }); describe('_burn', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should burn token', () => { - expect(token.balanceOf(OWNER.either)).toEqual(1n); + it('should burn token', async () => { + expect(await token.balanceOf(OWNER.either)).toEqual(1n); - token._burn(TOKENID_1); - expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); - expect(token.balanceOf(OWNER.either)).toEqual(0n); + await token._burn(TOKENID_1); + expect(await token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); - it('should not burn a token that does not exist', () => { - expect(() => { - token._burn(NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: invalid sender'); + it('should not burn a token that does not exist', async () => { + await expect(token._burn(NON_EXISTENT_TOKEN)).rejects.toThrow( + 'NonFungibleToken: invalid sender', + ); }); - it('should clear approval when token is burned', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(SPENDER.either); + it('should clear approval when token is burned', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(SPENDER.either); - token._burn(TOKENID_1); - expect(token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + await token._burn(TOKENID_1); + expect(await token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should burn multiple tokens in sequence', () => { - token._mint(OWNER.either, TOKENID_2); - token._mint(OWNER.either, TOKENID_3); + it('should burn multiple tokens in sequence', async () => { + await token._mint(OWNER.either, TOKENID_2); + await token._mint(OWNER.either, TOKENID_3); - token._burn(TOKENID_1); - token._burn(TOKENID_2); - token._burn(TOKENID_3); - expect(token.balanceOf(OWNER.either)).toEqual(0n); + await token._burn(TOKENID_1); + await token._burn(TOKENID_2); + await token._burn(TOKENID_3); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); }); - it('should burn with very long token IDs', () => { + it('should burn with very long token IDs', async () => { const longTokenId = BigInt('18446744073709551615'); - token._mint(OWNER.either, longTokenId); - token._burn(longTokenId); - expect(token._ownerOf(longTokenId)).toEqual(ZERO_ACCOUNT); + await token._mint(OWNER.either, longTokenId); + await token._burn(longTokenId); + expect(await token._ownerOf(longTokenId)).toEqual(ZERO_ACCOUNT); }); - it('should burn after transfer', () => { - token._transfer(OWNER.either, SPENDER.either, TOKENID_1); - token._burn(TOKENID_1); - expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should burn after transfer', async () => { + await token._transfer(OWNER.either, SPENDER.either, TOKENID_1); + await token._burn(TOKENID_1); + expect(await token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should burn after approval', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token._burn(TOKENID_1); - expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); - expect(token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should burn after approval', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token._burn(TOKENID_1); + expect(await token._ownerOf(TOKENID_1)).toEqual(ZERO_ACCOUNT); + expect(await token._getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should clear tokenURI on burn', () => { - token._setTokenURI(TOKENID_1, SOME_URI); - expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + it('should clear tokenURI on burn', async () => { + await token._setTokenURI(TOKENID_1, SOME_URI); + expect(await token.tokenURI(TOKENID_1)).toEqual(SOME_URI); - token._burn(TOKENID_1); + await token._burn(TOKENID_1); - token._mint(OWNER.either, TOKENID_1); - expect(token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); + await token._mint(OWNER.either, TOKENID_1); + expect(await token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); }); }); describe('_transfer', () => { - it('should not transfer to ContractAddress', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._transfer(OWNER.either, SOME_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: unsafe transfer'); + it('should not transfer to ContractAddress', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect( + token._transfer(OWNER.either, SOME_CONTRACT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: unsafe transfer'); }); - it('should transfer token', () => { - token._mint(OWNER.either, TOKENID_1); - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(SPENDER.either)).toEqual(0n); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + it('should transfer token', async () => { + await token._mint(OWNER.either, TOKENID_1); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(SPENDER.either)).toEqual(0n); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - token._transfer(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(SPENDER.either)).toEqual(1n); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token._transfer(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(SPENDER.either)).toEqual(1n); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should not transfer to zero address', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._transfer(OWNER.either, ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect( + token._transfer(OWNER.either, ZERO_ACCOUNT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should throw if from does not own token', () => { - token._mint(OWNER.either, TOKENID_1); - expect(() => { - token._transfer(UNAUTHORIZED.either, SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: incorrect owner'); + it('should throw if from does not own token', async () => { + await token._mint(OWNER.either, TOKENID_1); + await expect( + token._transfer(UNAUTHORIZED.either, SPENDER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: incorrect owner'); }); - it('should throw if token does not exist', () => { - expect(() => { - token._transfer(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect( + token._transfer(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should revoke approval after _transfer', () => { - token._mint(OWNER.either, TOKENID_1); - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token._transfer(OWNER.either, OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should revoke approval after _transfer', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token._transfer(OWNER.either, OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); }); describe('_setTokenURI', () => { - it('should throw if token does not exist', () => { - expect(() => { - token._setTokenURI(NON_EXISTENT_TOKEN, EMPTY_URI); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect( + token._setTokenURI(NON_EXISTENT_TOKEN, EMPTY_URI), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should set tokenURI', () => { - token._mint(OWNER.either, TOKENID_1); - token._setTokenURI(TOKENID_1, SOME_URI); - expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + it('should set tokenURI', async () => { + await token._mint(OWNER.either, TOKENID_1); + await token._setTokenURI(TOKENID_1, SOME_URI); + expect(await token.tokenURI(TOKENID_1)).toEqual(SOME_URI); }); }); describe('_unsafeMint', () => { - it('should mint to ContractAddress', () => { - expect(() => { - token._unsafeMint(SOME_CONTRACT, TOKENID_1); - }).not.toThrow(); + it('should mint to ContractAddress', async () => { + await expect( + token._unsafeMint(SOME_CONTRACT, TOKENID_1), + ).resolves.not.toThrow(); }); - it('should not mint to zero address (accountId)', () => { - expect(() => { - token._unsafeMint(ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not mint to zero address (accountId)', async () => { + await expect(token._unsafeMint(ZERO_ACCOUNT, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid receiver', + ); }); - it('should not mint to zero address (contract)', () => { - expect(() => { - token._unsafeMint(ZERO_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not mint to zero address (contract)', async () => { + await expect(token._unsafeMint(ZERO_CONTRACT, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid receiver', + ); }); - it('should not mint a token that already exists', () => { - token._unsafeMint(OWNER.either, TOKENID_1); - expect(() => { - token._unsafeMint(OWNER.either, TOKENID_1); - }).toThrow('NonFungibleToken: invalid sender'); + it('should not mint a token that already exists', async () => { + await token._unsafeMint(OWNER.either, TOKENID_1); + await expect(token._unsafeMint(OWNER.either, TOKENID_1)).rejects.toThrow( + 'NonFungibleToken: invalid sender', + ); }); - it('should mint token to account', () => { - token._unsafeMint(OWNER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - expect(token.balanceOf(OWNER.either)).toEqual(1n); + it('should mint token to account', async () => { + await token._unsafeMint(OWNER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + expect(await token.balanceOf(OWNER.either)).toEqual(1n); - token._unsafeMint(OWNER.either, TOKENID_2); - token._unsafeMint(OWNER.either, TOKENID_3); - expect(token.balanceOf(OWNER.either)).toEqual(3n); + await token._unsafeMint(OWNER.either, TOKENID_2); + await token._unsafeMint(OWNER.either, TOKENID_3); + expect(await token.balanceOf(OWNER.either)).toEqual(3n); }); }); describe('_unsafeTransfer', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should transfer to ContractAddress', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, SOME_CONTRACT, TOKENID_1); - }).not.toThrow(); + it('should transfer to ContractAddress', async () => { + await expect( + token._unsafeTransfer(OWNER.either, SOME_CONTRACT, TOKENID_1), + ).resolves.not.toThrow(); }); - it('should transfer token to account', () => { - expect(token.balanceOf(OWNER.either)).toEqual(1n); - expect(token.balanceOf(SPENDER.either)).toEqual(0n); - expect(token.ownerOf(TOKENID_1)).toEqual(OWNER.either); + it('should transfer token to account', async () => { + expect(await token.balanceOf(OWNER.either)).toEqual(1n); + expect(await token.balanceOf(SPENDER.either)).toEqual(0n); + expect(await token.ownerOf(TOKENID_1)).toEqual(OWNER.either); - token._unsafeTransfer(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.balanceOf(OWNER.either)).toEqual(0n); - expect(token.balanceOf(SPENDER.either)).toEqual(1n); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token._unsafeTransfer(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.balanceOf(OWNER.either)).toEqual(0n); + expect(await token.balanceOf(SPENDER.either)).toEqual(1n); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should not transfer to zero address (accountId)', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address (accountId)', async () => { + await expect( + token._unsafeTransfer(OWNER.either, ZERO_ACCOUNT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should not transfer to zero address (contract)', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, ZERO_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address (contract)', async () => { + await expect( + token._unsafeTransfer(OWNER.either, ZERO_CONTRACT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should throw if from does not own token', () => { - expect(() => { + it('should throw if from does not own token', async () => { + await expect( token._unsafeTransfer( UNAUTHORIZED.either, UNAUTHORIZED.either, TOKENID_1, - ); - }).toThrow('NonFungibleToken: incorrect owner'); + ), + ).rejects.toThrow('NonFungibleToken: incorrect owner'); }); - it('should throw if token does not exist', () => { - expect(() => { - token._unsafeTransfer(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN); - }).toThrow('NonFungibleToken: nonexistent token'); + it('should throw if token does not exist', async () => { + await expect( + token._unsafeTransfer(OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should revoke approval after _unsafeTransfer', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); - token._unsafeTransfer(OWNER.either, OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + it('should revoke approval after _unsafeTransfer', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); + await token._unsafeTransfer(OWNER.either, OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should canonicalize contract address recipient', () => { + it('should canonicalize contract address recipient', async () => { const nonCanonical = { is_left: false, left: new Uint8Array(32).fill(1), right: SOME_CONTRACT.right, }; - token._unsafeTransfer(OWNER.either, nonCanonical, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); - expect(token.balanceOf(SOME_CONTRACT)).toEqual(1n); + await token._unsafeTransfer(OWNER.either, nonCanonical, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + expect(await token.balanceOf(SOME_CONTRACT)).toEqual(1n); }); - it('should handle non-canonical fromAddress', () => { + it('should handle non-canonical fromAddress', async () => { const nonCanonical = { is_left: true, left: OWNER.accountId, right: utils.encodeToAddress('JUNK_DATA'), }; - token._unsafeTransfer(nonCanonical, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token._unsafeTransfer(nonCanonical, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); }); describe('_unsafeTransferFrom', () => { - beforeEach(() => { - token._mint(OWNER.either, TOKENID_1); + beforeEach(async () => { + await token._mint(OWNER.either, TOKENID_1); }); - it('should transfer to ContractAddress', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); - }).not.toThrow(); + it('should transfer to ContractAddress', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1), + ).resolves.not.toThrow(); }); - it('should not transfer to zero address (accountId)', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, ZERO_ACCOUNT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address (accountId)', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token._unsafeTransferFrom(OWNER.either, ZERO_ACCOUNT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should not transfer to zero address (contract)', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, ZERO_CONTRACT, TOKENID_1); - }).toThrow('NonFungibleToken: invalid receiver'); + it('should not transfer to zero address (contract)', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token._unsafeTransferFrom(OWNER.either, ZERO_CONTRACT, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: invalid receiver'); }); - it('should not transfer from zero address', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { - token._unsafeTransferFrom(ZERO_ACCOUNT, SPENDER.either, TOKENID_1); - }).toThrow('NonFungibleToken: incorrect owner'); + it('should not transfer from zero address', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( + token._unsafeTransferFrom(ZERO_ACCOUNT, SPENDER.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: incorrect owner'); }); - it('unapproved operator should not transfer', () => { - token.privateState.injectSecretKey(SPENDER.secretKey); - expect(() => { - token._unsafeTransferFrom(OWNER.either, UNAUTHORIZED.either, TOKENID_1); - }).toThrow('NonFungibleToken: insufficient approval'); + it('unapproved operator should not transfer', async () => { + await token.privateState.injectSecretKey(SPENDER.secretKey); + await expect( + token._unsafeTransferFrom(OWNER.either, UNAUTHORIZED.either, TOKENID_1), + ).rejects.toThrow('NonFungibleToken: insufficient approval'); }); - it('should not transfer token that has not been minted', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - expect(() => { + it('should not transfer token that has not been minted', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await expect( token._unsafeTransferFrom( OWNER.either, SPENDER.either, NON_EXISTENT_TOKEN, - ); - }).toThrow('NonFungibleToken: nonexistent token'); + ), + ).rejects.toThrow('NonFungibleToken: nonexistent token'); }); - it('should transfer token to spender via approved operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('should transfer token to spender via approved operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - token._unsafeTransferFrom(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token._unsafeTransferFrom(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should transfer token to ContractAddress via approved operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('should transfer token to ContractAddress via approved operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token.privateState.injectSecretKey(SPENDER.secretKey); - token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); }); - it('should transfer token to spender via approvedForAll operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should transfer token to spender via approvedForAll operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token._unsafeTransferFrom(OWNER.either, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token._unsafeTransferFrom(OWNER.either, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should transfer token to ContractAddress via approvedForAll operator', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.setApprovalForAll(SPENDER.either, true); + it('should transfer token to ContractAddress via approvedForAll operator', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.setApprovalForAll(SPENDER.either, true); - token.privateState.injectSecretKey(SPENDER.secretKey); - token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + await token.privateState.injectSecretKey(SPENDER.secretKey); + await token._unsafeTransferFrom(OWNER.either, SOME_CONTRACT, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); }); - it('should revoke approval after _unsafeTransferFrom', () => { - token.privateState.injectSecretKey(OWNER.secretKey); - token.approve(SPENDER.either, TOKENID_1); + it('should revoke approval after _unsafeTransferFrom', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); + await token.approve(SPENDER.either, TOKENID_1); - token._unsafeTransferFrom(OWNER.either, OTHER.either, TOKENID_1); - expect(token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); + await token._unsafeTransferFrom(OWNER.either, OTHER.either, TOKENID_1); + expect(await token.getApproved(TOKENID_1)).toEqual(ZERO_ACCOUNT); }); - it('should handle non-canonical fromAddress', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should handle non-canonical fromAddress', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = { is_left: true, @@ -1359,12 +1409,12 @@ describe('NonFungibleToken', () => { right: utils.encodeToAddress('JUNK_DATA'), }; - token._unsafeTransferFrom(nonCanonical, SPENDER.either, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); + await token._unsafeTransferFrom(nonCanonical, SPENDER.either, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SPENDER.either); }); - it('should canonicalize contract address recipient', () => { - token.privateState.injectSecretKey(OWNER.secretKey); + it('should canonicalize contract address recipient', async () => { + await token.privateState.injectSecretKey(OWNER.secretKey); const nonCanonical = { is_left: false, @@ -1372,9 +1422,9 @@ describe('NonFungibleToken', () => { right: SOME_CONTRACT.right, }; - token._unsafeTransferFrom(OWNER.either, nonCanonical, TOKENID_1); - expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); - expect(token.balanceOf(SOME_CONTRACT)).toEqual(1n); + await token._unsafeTransferFrom(OWNER.either, nonCanonical, TOKENID_1); + expect(await token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + expect(await token.balanceOf(SOME_CONTRACT)).toEqual(1n); }); }); }); @@ -1415,40 +1465,48 @@ const circuitsToFail: FailingCircuits[] = [ let uninitializedToken: NonFungibleTokenSimulator; describe('Uninitialized NonFungibleToken', () => { - beforeEach(() => { - uninitializedToken = new NonFungibleTokenSimulator(NAME, SYMBOL, BAD_INIT); + beforeEach(async () => { + uninitializedToken = await NonFungibleTokenSimulator.create( + NAME, + SYMBOL, + BAD_INIT, + ); }); - it.each(circuitsToFail)('%s should fail', (circuitName, args) => { - expect(() => { - (uninitializedToken[circuitName] as (...args: unknown[]) => unknown)( - ...args, - ); - }).toThrow('NonFungibleToken: contract not initialized'); + it.each(circuitsToFail)('%s should fail', async (circuitName, args) => { + await expect( + ( + uninitializedToken[circuitName] as ( + ...args: unknown[] + ) => Promise + )(...args), + ).rejects.toThrow('NonFungibleToken: contract not initialized'); }); }); describe('NonFungibleTokenSimulator wiring', () => { - it('should expose an empty public ledger via getPublicState', () => { - const sim = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); + it('should expose an empty public ledger via getPublicState', async () => { + const sim = await NonFungibleTokenSimulator.create(NAME, SYMBOL, INIT); - expect(sim.getPublicState()).toStrictEqual({}); + expect(await sim.getPublicState()).toStrictEqual({}); }); describe('privateState getCurrentSecretKey', () => { - it('should return the injected secret key', () => { - const sim = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); - sim.privateState.injectSecretKey(OWNER.secretKey); + it('should return the injected secret key', async () => { + const sim = await NonFungibleTokenSimulator.create(NAME, SYMBOL, INIT); + await sim.privateState.injectSecretKey(OWNER.secretKey); - expect(sim.privateState.getCurrentSecretKey()).toEqual(OWNER.secretKey); + expect(await sim.privateState.getCurrentSecretKey()).toEqual( + OWNER.secretKey, + ); }); - it('should throw when the secret key is undefined', () => { - const sim = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT, { + it('should throw when the secret key is undefined', async () => { + const sim = await NonFungibleTokenSimulator.create(NAME, SYMBOL, INIT, { privateState: { secretKey: undefined as unknown as Uint8Array }, }); - expect(() => sim.privateState.getCurrentSecretKey()).toThrow( + await expect(sim.privateState.getCurrentSecretKey()).rejects.toThrow( 'Missing secret key', ); }); diff --git a/contracts/src/token/test/simulators/FungibleTokenSimulator.ts b/contracts/src/token/test/simulators/FungibleTokenSimulator.ts index 3a9ef8da..a6ba9190 100644 --- a/contracts/src/token/test/simulators/FungibleTokenSimulator.ts +++ b/contracts/src/token/test/simulators/FungibleTokenSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -41,29 +41,34 @@ const FungibleTokenSimulatorBase = createSimulator< ], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => FungibleTokenWitnesses(), + artifactName: 'MockFungibleToken', }); /** * FungibleToken Simulator */ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { - constructor( + static async create( name: string, symbol: string, decimals: bigint, init: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< FungibleTokenPrivateState, ReturnType > = {}, - ) { - super([name, symbol, decimals, init], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [name, symbol, decimals, init], + options, + ) as Promise; } /** * @description Returns the token name. * @returns The token name. */ - public name(): string { + public name(): Promise { return this.circuits.impure.name(); } @@ -71,7 +76,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @description Returns the symbol of the token. * @returns The token name. */ - public symbol(): string { + public symbol(): Promise { return this.circuits.impure.symbol(); } @@ -79,7 +84,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @description Returns the number of decimals used to get its user representation. * @returns The account's token balance. */ - public decimals(): bigint { + public decimals(): Promise { return this.circuits.impure.decimals(); } @@ -87,7 +92,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @description Returns the value of tokens in existence. * @returns The total supply of tokens. */ - public totalSupply(): bigint { + public totalSupply(): Promise { return this.circuits.impure.totalSupply(); } @@ -96,7 +101,9 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @param account The public key or contract address to query. * @returns The account's token balance. */ - public balanceOf(account: Either): bigint { + public balanceOf( + account: Either, + ): Promise { return this.circuits.impure.balanceOf(account); } @@ -110,7 +117,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { public allowance( owner: Either, spender: Either, - ): bigint { + ): Promise { return this.circuits.impure.allowance(owner, spender); } @@ -123,7 +130,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { public transfer( to: Either, value: bigint, - ): boolean { + ): Promise { return this.circuits.impure.transfer(to, value); } @@ -136,7 +143,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { public _unsafeTransfer( to: Either, value: bigint, - ): boolean { + ): Promise { return this.circuits.impure._unsafeTransfer(to, value); } @@ -152,7 +159,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { fromAddress: Either, to: Either, value: bigint, - ): boolean { + ): Promise { return this.circuits.impure.transferFrom(fromAddress, to, value); } @@ -167,7 +174,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { fromAddress: Either, to: Either, value: bigint, - ): boolean { + ): Promise { return this.circuits.impure._unsafeTransferFrom(fromAddress, to, value); } @@ -180,7 +187,7 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { public approve( spender: Either, value: bigint, - ): boolean { + ): Promise { return this.circuits.impure.approve(spender, value); } @@ -200,8 +207,8 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { fromAddress: Either, to: Either, value: bigint, - ) { - this.circuits.impure._transfer(fromAddress, to, value); + ): Promise<[]> { + return this.circuits.impure._transfer(fromAddress, to, value); } /** @@ -214,8 +221,12 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { fromAddress: Either, to: Either, value: bigint, - ) { - this.circuits.impure._unsafeUncheckedTransfer(fromAddress, to, value); + ): Promise<[]> { + return this.circuits.impure._unsafeUncheckedTransfer( + fromAddress, + to, + value, + ); } /** @@ -224,8 +235,11 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @param account The recipient of tokens minted. * @param value The amount of tokens minted. */ - public _mint(account: Either, value: bigint) { - this.circuits.impure._mint(account, value); + public _mint( + account: Either, + value: bigint, + ): Promise<[]> { + return this.circuits.impure._mint(account, value); } /** @@ -236,8 +250,8 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { public _unsafeMint( account: Either, value: bigint, - ) { - this.circuits.impure._unsafeMint(account, value); + ): Promise<[]> { + return this.circuits.impure._unsafeMint(account, value); } /** @@ -246,8 +260,11 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @param account The target owner of tokens to burn. * @param value The amount of tokens to burn. */ - public _burn(account: Either, value: bigint) { - this.circuits.impure._burn(account, value); + public _burn( + account: Either, + value: bigint, + ): Promise<[]> { + return this.circuits.impure._burn(account, value); } /** @@ -262,8 +279,8 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { owner: Either, spender: Either, value: bigint, - ) { - this.circuits.impure._approve(owner, spender, value); + ): Promise<[]> { + return this.circuits.impure._approve(owner, spender, value); } /** @@ -277,8 +294,8 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { owner: Either, spender: Either, value: bigint, - ) { - this.circuits.impure._spendAllowance(owner, spender, value); + ): Promise<[]> { + return this.circuits.impure._spendAllowance(owner, spender, value); } public readonly privateState = { @@ -289,9 +306,11 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: (newSK: Uint8Array): FungibleTokenPrivateState => { + injectSecretKey: async ( + newSK: Uint8Array, + ): Promise => { const updatedState = FungibleTokenPrivateState.withSecretKey(newSK); - this.circuitContextManager.updatePrivateState(updatedState); + this.setPrivateState(updatedState); return updatedState; }, @@ -300,8 +319,8 @@ export class FungibleTokenSimulator extends FungibleTokenSimulatorBase { * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } diff --git a/contracts/src/token/test/simulators/MultiTokenSimulator.ts b/contracts/src/token/test/simulators/MultiTokenSimulator.ts index 388ddf88..2702a3bb 100644 --- a/contracts/src/token/test/simulators/MultiTokenSimulator.ts +++ b/contracts/src/token/test/simulators/MultiTokenSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -32,20 +32,22 @@ const MultiTokenSimulatorBase = createSimulator< contractArgs: (_uri) => [_uri], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => MultiTokenWitnesses(), + artifactName: 'MockMultiToken', }); /** * MultiToken Simulator */ export class MultiTokenSimulator extends MultiTokenSimulatorBase { - constructor( + static async create( _uri: Maybe, - options: BaseSimulatorOptions< + options: SimulatorOptions< MultiTokenPrivateState, ReturnType > = {}, - ) { - super([_uri], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([_uri], options) as Promise; } /** @@ -53,8 +55,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { * however, this method enables the tests to assert it cannot be called again. * @param uri The base URI for all token URIs. */ - public initialize(uri: string) { - this.circuits.impure.initialize(uri); + public initialize(uri: string): Promise<[]> { + return this.circuits.impure.initialize(uri); } /** @@ -62,7 +64,7 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { * @param id The token identifier to query. * @returns The token URI. */ - public uri(id: bigint): string { + public uri(id: bigint): Promise { return this.circuits.impure.uri(id); } @@ -75,7 +77,7 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { public balanceOf( account: Either, id: bigint, - ): bigint { + ): Promise { return this.circuits.impure.balanceOf(account, id); } @@ -88,8 +90,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { public setApprovalForAll( operator: Either, approved: boolean, - ) { - this.circuits.impure.setApprovalForAll(operator, approved); + ): Promise<[]> { + return this.circuits.impure.setApprovalForAll(operator, approved); } /** @@ -101,7 +103,7 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { public isApprovedForAll( account: Either, operator: Either, - ): boolean { + ): Promise { return this.circuits.impure.isApprovedForAll(account, operator); } @@ -118,8 +120,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure.transferFrom(fromAddress, to, id, value); + ): Promise<[]> { + return this.circuits.impure.transferFrom(fromAddress, to, id, value); } /** @@ -135,8 +137,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._unsafeTransferFrom(fromAddress, to, id, value); + ): Promise<[]> { + return this.circuits.impure._unsafeTransferFrom(fromAddress, to, id, value); } /** @@ -153,8 +155,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._transfer(fromAddress, to, id, value); + ): Promise<[]> { + return this.circuits.impure._transfer(fromAddress, to, id, value); } /** @@ -171,16 +173,16 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._unsafeTransfer(fromAddress, to, id, value); + ): Promise<[]> { + return this.circuits.impure._unsafeTransfer(fromAddress, to, id, value); } /** * @description Sets a new URI for all token types. * @param newURI The new base URI for all tokens. */ - public _setURI(newURI: string) { - this.circuits.impure._setURI(newURI); + public _setURI(newURI: string): Promise<[]> { + return this.circuits.impure._setURI(newURI); } /** @@ -193,8 +195,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._mint(to, id, value); + ): Promise<[]> { + return this.circuits.impure._mint(to, id, value); } /** @@ -207,8 +209,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { to: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._unsafeMint(to, id, value); + ): Promise<[]> { + return this.circuits.impure._unsafeMint(to, id, value); } /** @@ -221,8 +223,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { fromAddress: Either, id: bigint, value: bigint, - ) { - this.circuits.impure._burn(fromAddress, id, value); + ): Promise<[]> { + return this.circuits.impure._burn(fromAddress, id, value); } /** @@ -237,8 +239,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { owner: Either, operator: Either, approved: boolean, - ) { - this.circuits.impure._setApprovalForAll(owner, operator, approved); + ): Promise<[]> { + return this.circuits.impure._setApprovalForAll(owner, operator, approved); } public readonly privateState = { @@ -249,9 +251,11 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: (newSK: Uint8Array): MultiTokenPrivateState => { + injectSecretKey: async ( + newSK: Uint8Array, + ): Promise => { const updatedState = MultiTokenPrivateState.withSecretKey(newSK); - this.circuitContextManager.updatePrivateState(updatedState); + this.setPrivateState(updatedState); return updatedState; }, @@ -260,8 +264,8 @@ export class MultiTokenSimulator extends MultiTokenSimulatorBase { * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } diff --git a/contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts b/contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts index 02ce0d92..a2622111 100644 --- a/contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts +++ b/contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -35,29 +35,34 @@ const NonFungibleTokenSimulatorBase = createSimulator< contractArgs: (name, symbol, init) => [name, symbol, init], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => NonFungibleTokenWitnesses(), + artifactName: 'MockNonFungibleToken', }); /** * NonFungibleToken Simulator */ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { - constructor( + static async create( name: string, symbol: string, init: boolean, - options: BaseSimulatorOptions< + options: SimulatorOptions< NonFungibleTokenPrivateState, ReturnType > = {}, - ) { - super([name, symbol, init], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + [name, symbol, init], + options, + ) as Promise; } /** * @description Returns the token name. * @returns The token name. */ - public name(): string { + public name(): Promise { return this.circuits.impure.name(); } @@ -65,7 +70,7 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @description Returns the symbol of the token. * @returns The token symbol. */ - public symbol(): string { + public symbol(): Promise { return this.circuits.impure.symbol(); } @@ -74,7 +79,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param account The public key to query. * @return The number of tokens in `account`'s account. */ - public balanceOf(account: Either): bigint { + public balanceOf( + account: Either, + ): Promise { return this.circuits.impure.balanceOf(account); } @@ -83,7 +90,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The identifier for a token. * @return The public key that owns the token. */ - public ownerOf(tokenId: bigint): Either { + public ownerOf( + tokenId: bigint, + ): Promise> { return this.circuits.impure.ownerOf(tokenId); } @@ -97,7 +106,7 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The identifier for a token. * @returns The token id's URI. */ - public tokenURI(tokenId: bigint): string { + public tokenURI(tokenId: bigint): Promise { return this.circuits.impure.tokenURI(tokenId); } @@ -115,8 +124,11 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param to The account receiving the approval * @param tokenId The token `to` may be permitted to transfer */ - public approve(to: Either, tokenId: bigint) { - this.circuits.impure.approve(to, tokenId); + public approve( + to: Either, + tokenId: bigint, + ): Promise<[]> { + return this.circuits.impure.approve(to, tokenId); } /** @@ -124,7 +136,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The token an account may be approved to manage * @return The account approved to manage the token */ - public getApproved(tokenId: bigint): Either { + public getApproved( + tokenId: bigint, + ): Promise> { return this.circuits.impure.getApproved(tokenId); } @@ -142,8 +156,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { public setApprovalForAll( operator: Either, approved: boolean, - ) { - this.circuits.impure.setApprovalForAll(operator, approved); + ): Promise<[]> { + return this.circuits.impure.setApprovalForAll(operator, approved); } /** @@ -156,7 +170,7 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { public isApprovedForAll( owner: Either, operator: Either, - ): boolean { + ): Promise { return this.circuits.impure.isApprovedForAll(owner, operator); } @@ -178,8 +192,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { fromAddress: Either, to: Either, tokenId: bigint, - ) { - this.circuits.impure.transferFrom(fromAddress, to, tokenId); + ): Promise<[]> { + return this.circuits.impure.transferFrom(fromAddress, to, tokenId); } /** @@ -191,7 +205,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The token that should be owned * @return The owner of `tokenId` */ - public _requireOwned(tokenId: bigint): Either { + public _requireOwned( + tokenId: bigint, + ): Promise> { return this.circuits.impure._requireOwned(tokenId); } @@ -201,7 +217,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The target token of the owner query * @return The owner of the token */ - public _ownerOf(tokenId: bigint): Either { + public _ownerOf( + tokenId: bigint, + ): Promise> { return this.circuits.impure._ownerOf(tokenId); } @@ -219,8 +237,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { to: Either, tokenId: bigint, auth: Either, - ) { - this.circuits.impure._approve(to, tokenId, auth); + ): Promise<[]> { + return this.circuits.impure._approve(to, tokenId, auth); } /** @@ -240,7 +258,7 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { owner: Either, spender: Either, tokenId: bigint, - ) { + ): Promise<[]> { return this.circuits.impure._checkAuthorized(owner, spender, tokenId); } @@ -260,7 +278,7 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { owner: Either, spender: Either, tokenId: bigint, - ): boolean { + ): Promise { return this.circuits.impure._isAuthorized(owner, spender, tokenId); } @@ -270,7 +288,9 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The token to query * @return An account approved to spend `tokenId` */ - public _getApproved(tokenId: bigint): Either { + public _getApproved( + tokenId: bigint, + ): Promise> { return this.circuits.impure._getApproved(tokenId); } @@ -289,8 +309,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { owner: Either, operator: Either, approved: boolean, - ) { - this.circuits.impure._setApprovalForAll(owner, operator, approved); + ): Promise<[]> { + return this.circuits.impure._setApprovalForAll(owner, operator, approved); } /** @@ -304,8 +324,11 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param to The account receiving `tokenId` * @param tokenId The token to transfer */ - public _mint(to: Either, tokenId: bigint) { - this.circuits.impure._mint(to, tokenId); + public _mint( + to: Either, + tokenId: bigint, + ): Promise<[]> { + return this.circuits.impure._mint(to, tokenId); } /** @@ -319,8 +342,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * * @param tokenId The token to burn */ - public _burn(tokenId: bigint) { - this.circuits.impure._burn(tokenId); + public _burn(tokenId: bigint): Promise<[]> { + return this.circuits.impure._burn(tokenId); } /** @@ -340,8 +363,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { fromAddress: Either, to: Either, tokenId: bigint, - ) { - this.circuits.impure._transfer(fromAddress, to, tokenId); + ): Promise<[]> { + return this.circuits.impure._transfer(fromAddress, to, tokenId); } /** @@ -353,8 +376,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param tokenId The identifier of the token. * @param tokenURI The URI of `tokenId`. */ - public _setTokenURI(tokenId: bigint, tokenURI: string) { - this.circuits.impure._setTokenURI(tokenId, tokenURI); + public _setTokenURI(tokenId: bigint, tokenURI: string): Promise<[]> { + return this.circuits.impure._setTokenURI(tokenId, tokenURI); } /** @@ -379,8 +402,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { fromAddress: Either, to: Either, tokenId: bigint, - ) { - this.circuits.impure._unsafeTransferFrom(fromAddress, to, tokenId); + ): Promise<[]> { + return this.circuits.impure._unsafeTransferFrom(fromAddress, to, tokenId); } /** @@ -405,8 +428,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { fromAddress: Either, to: Either, tokenId: bigint, - ) { - this.circuits.impure._unsafeTransfer(fromAddress, to, tokenId); + ): Promise<[]> { + return this.circuits.impure._unsafeTransfer(fromAddress, to, tokenId); } /** @@ -424,8 +447,11 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param {Either} to - The account receiving `tokenId` * @param {TokenId} tokenId - The token to transfer */ - public _unsafeMint(to: Either, tokenId: bigint) { - this.circuits.impure._unsafeMint(to, tokenId); + public _unsafeMint( + to: Either, + tokenId: bigint, + ): Promise<[]> { + return this.circuits.impure._unsafeMint(to, tokenId); } public readonly privateState = { @@ -436,9 +462,11 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @param newSK - The new secret key to set. * @returns The updated private state. */ - injectSecretKey: (newSK: Uint8Array): NonFungibleTokenPrivateState => { + injectSecretKey: async ( + newSK: Uint8Array, + ): Promise => { const updatedState = NonFungibleTokenPrivateState.withSecretKey(newSK); - this.circuitContextManager.updatePrivateState(updatedState); + this.setPrivateState(updatedState); return updatedState; }, @@ -447,8 +475,8 @@ export class NonFungibleTokenSimulator extends NonFungibleTokenSimulatorBase { * @returns The secret key. * @throws If the secret key is undefined. */ - getCurrentSecretKey: (): Uint8Array => { - const sk = this.getPrivateState().secretKey; + getCurrentSecretKey: async (): Promise => { + const sk = (await this.getPrivateState()).secretKey; if (typeof sk === 'undefined') { throw new Error('Missing secret key'); } diff --git a/contracts/src/utils/test/simulators/UtilsSimulator.ts b/contracts/src/utils/test/simulators/UtilsSimulator.ts index 9832db76..67a0812b 100644 --- a/contracts/src/utils/test/simulators/UtilsSimulator.ts +++ b/contracts/src/utils/test/simulators/UtilsSimulator.ts @@ -1,6 +1,6 @@ import { - type BaseSimulatorOptions, createSimulator, + type SimulatorOptions, } from '@openzeppelin/compact-simulator'; import { type ContractAddress, @@ -31,19 +31,21 @@ const UtilsSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => UtilsWitnesses(), + artifactName: 'MockUtils', }); /** * Utils Simulator */ export class UtilsSimulator extends UtilsSimulatorBase { - constructor( - options: BaseSimulatorOptions< + static async create( + options: SimulatorOptions< UtilsPrivateState, ReturnType > = {}, - ) { - super([], options); + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } /** @@ -53,7 +55,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { */ public isKeyOrAddressZero( keyOrAddress: Either, - ): boolean { + ): Promise { return this.circuits.pure.isKeyOrAddressZero(keyOrAddress); } @@ -69,7 +71,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { public isKeyOrAddressEqual( keyOrAddress: Either, other: Either, - ): boolean { + ): Promise { return this.circuits.pure.isKeyOrAddressEqual(keyOrAddress, other); } @@ -78,7 +80,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { * @param key The target value to check. * @returns Returns true if `key` is zero. */ - public isKeyZero(key: ZswapCoinPublicKey): boolean { + public isKeyZero(key: ZswapCoinPublicKey): Promise { return this.circuits.pure.isKeyZero(key); } @@ -89,7 +91,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { */ public isContractAddress( keyOrAddress: Either, - ): boolean { + ): Promise { return this.circuits.pure.isContractAddress(keyOrAddress); } @@ -97,7 +99,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { * @description A helper function that returns the empty string: "" * @returns The empty string: "" */ - public emptyString(): string { + public emptyString(): Promise { return this.circuits.pure.emptyString(); } @@ -108,7 +110,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { */ public canonicalizeKeyOrAddress( keyOrAddress: Either, - ): Either { + ): Promise> { return this.circuits.pure.canonicalizeKeyOrAddress(keyOrAddress); } @@ -117,7 +119,9 @@ export class UtilsSimulator extends UtilsSimulatorBase { * right-variant `Either`. * @returns The contract's own address as a recipient. */ - public selfAsRecipient(): Either { + public selfAsRecipient(): Promise< + Either + > { return this.circuits.impure.selfAsRecipient(); } @@ -125,7 +129,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { * @description The maximum value representable by a `Uint<128>`. * @returns `2^128 - 1`. */ - public UINT128_MAX(): bigint { + public UINT128_MAX(): Promise { return this.circuits.pure.UINT128_MAX(); } @@ -134,7 +138,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { * (left variant with zero `Bytes<32>`). * @returns The canonical zero value. */ - public zeroAccount(): Either { + public zeroAccount(): Promise> { return this.circuits.pure.zeroAccount(); } @@ -143,7 +147,9 @@ export class UtilsSimulator extends UtilsSimulatorBase { * @param target The value to check. * @returns Returns true if the active branch is zero. */ - public isTargetZero(target: Either): boolean { + public isTargetZero( + target: Either, + ): Promise { return this.circuits.pure.isTargetZero(target); } @@ -153,7 +159,7 @@ export class UtilsSimulator extends UtilsSimulatorBase { * @param secretKey A 32-byte cryptographically secure random value. * @returns The computed account identifier. */ - public computeAccountId(secretKey: Uint8Array): Uint8Array { + public computeAccountId(secretKey: Uint8Array): Promise { return this.circuits.pure.computeAccountId(secretKey); } } diff --git a/contracts/src/utils/test/utils.test.ts b/contracts/src/utils/test/utils.test.ts index 12f6d211..ed2db809 100644 --- a/contracts/src/utils/test/utils.test.ts +++ b/contracts/src/utils/test/utils.test.ts @@ -3,7 +3,7 @@ import { CompactTypeVector, persistentHash, } from '@midnight-ntwrk/compact-runtime'; -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import * as contractUtils from '#test-utils/address.js'; import { UtilsSimulator } from './simulators/UtilsSimulator.js'; @@ -45,55 +45,63 @@ const eitherContract = (str: string) => ({ let contract: UtilsSimulator; describe('Utils', () => { - contract = new UtilsSimulator(); + beforeEach(async () => { + contract = await UtilsSimulator.create(); + }); describe('isKeyOrAddressZero', () => { - it('should return zero for the zero address', () => { - expect(contract.isKeyOrAddressZero(contractUtils.ZERO_KEY)).toBe(true); + it('should return zero for the zero address', async () => { + expect(await contract.isKeyOrAddressZero(contractUtils.ZERO_KEY)).toBe( + true, + ); }); - it('should not return zero for nonzero addresses', () => { - expect(contract.isKeyOrAddressZero(Z_SOME_KEY)).toBe(false); - expect(contract.isKeyOrAddressZero(SOME_CONTRACT)).toBe(false); + it('should not return zero for nonzero addresses', async () => { + expect(await contract.isKeyOrAddressZero(Z_SOME_KEY)).toBe(false); + expect(await contract.isKeyOrAddressZero(SOME_CONTRACT)).toBe(false); }); - it('should not return zero for a zero contract address', () => { - expect(contract.isKeyOrAddressZero(contractUtils.ZERO_ADDRESS)).toBe( - true, - ); + it('should not return zero for a zero contract address', async () => { + expect( + await contract.isKeyOrAddressZero(contractUtils.ZERO_ADDRESS), + ).toBe(true); }); }); describe('isKeyOrAddressEqual', () => { - it('should return true for two matching pubkeys', () => { - expect(contract.isKeyOrAddressEqual(Z_SOME_KEY, Z_SOME_KEY)).toBe(true); - }); - - it('should return true for two matching contract addresses', () => { - expect(contract.isKeyOrAddressEqual(SOME_CONTRACT, SOME_CONTRACT)).toBe( + it('should return true for two matching pubkeys', async () => { + expect(await contract.isKeyOrAddressEqual(Z_SOME_KEY, Z_SOME_KEY)).toBe( true, ); }); - it('should return false for two different pubkeys', () => { - expect(contract.isKeyOrAddressEqual(Z_SOME_KEY, Z_OTHER_KEY)).toBe(false); + it('should return true for two matching contract addresses', async () => { + expect( + await contract.isKeyOrAddressEqual(SOME_CONTRACT, SOME_CONTRACT), + ).toBe(true); }); - it('should return false for two different contract addresses', () => { - expect(contract.isKeyOrAddressEqual(SOME_CONTRACT, OTHER_CONTRACT)).toBe( + it('should return false for two different pubkeys', async () => { + expect(await contract.isKeyOrAddressEqual(Z_SOME_KEY, Z_OTHER_KEY)).toBe( false, ); }); - it('should return false for two different address types', () => { - expect(contract.isKeyOrAddressEqual(Z_SOME_KEY, SOME_CONTRACT)).toBe( - false, - ); + it('should return false for two different contract addresses', async () => { + expect( + await contract.isKeyOrAddressEqual(SOME_CONTRACT, OTHER_CONTRACT), + ).toBe(false); }); - it('should return false for two different address types of equal value', () => { + it('should return false for two different address types', async () => { expect( - contract.isKeyOrAddressEqual( + await contract.isKeyOrAddressEqual(Z_SOME_KEY, SOME_CONTRACT), + ).toBe(false); + }); + + it('should return false for two different address types of equal value', async () => { + expect( + await contract.isKeyOrAddressEqual( contractUtils.ZERO_KEY, contractUtils.ZERO_ADDRESS, ), @@ -102,75 +110,75 @@ describe('Utils', () => { }); describe('isKeyZero', () => { - it('should return zero for the zero address', () => { - expect(contract.isKeyZero(contractUtils.ZERO_KEY.left)).toBe(true); + it('should return zero for the zero address', async () => { + expect(await contract.isKeyZero(contractUtils.ZERO_KEY.left)).toBe(true); }); - it('should not return zero for nonzero addresses', () => { - expect(contract.isKeyZero(Z_SOME_KEY.left)).toBe(false); + it('should not return zero for nonzero addresses', async () => { + expect(await contract.isKeyZero(Z_SOME_KEY.left)).toBe(false); }); }); describe('isContractAddress', () => { - it('should return true if ContractAddress', () => { - expect(contract.isContractAddress(SOME_CONTRACT)).toBe(true); + it('should return true if ContractAddress', async () => { + expect(await contract.isContractAddress(SOME_CONTRACT)).toBe(true); }); - it('should return false ZswapCoinPublicKey', () => { - expect(contract.isContractAddress(Z_SOME_KEY)).toBe(false); + it('should return false ZswapCoinPublicKey', async () => { + expect(await contract.isContractAddress(Z_SOME_KEY)).toBe(false); }); }); describe('emptyString', () => { - it('should return the empty string', () => { - expect(contract.emptyString()).toBe(EMPTY_STRING); + it('should return the empty string', async () => { + expect(await contract.emptyString()).toBe(EMPTY_STRING); }); }); describe('canonicalizeKeyOrAddress', () => { - it('should zero the right side when is_left is true', () => { + it('should zero the right side when is_left is true', async () => { const crafted = { is_left: true, left: Z_SOME_KEY.left, right: SOME_CONTRACT.right, }; - const canonical = contract.canonicalizeKeyOrAddress(crafted); + const canonical = await contract.canonicalizeKeyOrAddress(crafted); expect(canonical.is_left).toBe(true); expect(canonical.left).toEqual(Z_SOME_KEY.left); expect(canonical.right).toEqual(contractUtils.ZERO_ADDRESS.right); }); - it('should zero the left side when is_left is false', () => { + it('should zero the left side when is_left is false', async () => { const crafted = { is_left: false, left: Z_SOME_KEY.left, right: SOME_CONTRACT.right, }; - const canonical = contract.canonicalizeKeyOrAddress(crafted); + const canonical = await contract.canonicalizeKeyOrAddress(crafted); expect(canonical.is_left).toBe(false); expect(canonical.left).toEqual(contractUtils.ZERO_KEY.left); expect(canonical.right).toEqual(SOME_CONTRACT.right); }); - it('should be idempotent for canonical pubkey', () => { - const canonical = contract.canonicalizeKeyOrAddress(Z_SOME_KEY); + it('should be idempotent for canonical pubkey', async () => { + const canonical = await contract.canonicalizeKeyOrAddress(Z_SOME_KEY); expect(canonical).toEqual(Z_SOME_KEY); }); - it('should be idempotent for canonical contract address', () => { - const canonical = contract.canonicalizeKeyOrAddress(SOME_CONTRACT); + it('should be idempotent for canonical contract address', async () => { + const canonical = await contract.canonicalizeKeyOrAddress(SOME_CONTRACT); expect(canonical).toEqual(SOME_CONTRACT); }); - it('should be idempotent for already-zero pubkey', () => { - const canonical = contract.canonicalizeKeyOrAddress( + it('should be idempotent for already-zero pubkey', async () => { + const canonical = await contract.canonicalizeKeyOrAddress( contractUtils.ZERO_KEY, ); expect(canonical).toEqual(contractUtils.ZERO_KEY); }); - it('should be idempotent for already-zero contract address', () => { - const canonical = contract.canonicalizeKeyOrAddress( + it('should be idempotent for already-zero contract address', async () => { + const canonical = await contract.canonicalizeKeyOrAddress( contractUtils.ZERO_ADDRESS, ); expect(canonical).toEqual(contractUtils.ZERO_ADDRESS); @@ -178,51 +186,53 @@ describe('Utils', () => { }); describe('selfAsRecipient', () => { - it('should return the contract address as a right-variant recipient', () => { - const result = contract.selfAsRecipient(); + it('should return the contract address as a right-variant recipient', async () => { + const result = await contract.selfAsRecipient(); expect(result.is_left).toBe(false); - expect(contract.isContractAddress(result)).toBe(true); + expect(await contract.isContractAddress(result)).toBe(true); }); - it('should return a 32-byte contract address', () => { - const result = contract.selfAsRecipient(); + it('should return a 32-byte contract address', async () => { + const result = await contract.selfAsRecipient(); expect(result.right.bytes).toBeInstanceOf(Uint8Array); expect(result.right.bytes.length).toBe(32); }); - it('should return the same address on repeated calls', () => { - const first = contract.selfAsRecipient(); - const second = contract.selfAsRecipient(); + it('should return the same address on repeated calls', async () => { + const first = await contract.selfAsRecipient(); + const second = await contract.selfAsRecipient(); expect(first.right.bytes).toEqual(second.right.bytes); }); }); describe('UINT128_MAX', () => { - it('should return 2^128 - 1', () => { - expect(contract.UINT128_MAX()).toBe((1n << 128n) - 1n); + it('should return 2^128 - 1', async () => { + expect(await contract.UINT128_MAX()).toBe((1n << 128n) - 1n); }); }); describe('zeroAccount', () => { - it('should return a left variant', () => { - expect(contract.zeroAccount().is_left).toBe(true); + it('should return a left variant', async () => { + expect((await contract.zeroAccount()).is_left).toBe(true); }); - it('should have zero left and right branches', () => { - const zero = contract.zeroAccount(); + it('should have zero left and right branches', async () => { + const zero = await contract.zeroAccount(); expect(zero.left).toEqual(zeroBytes); expect(zero.right).toEqual({ bytes: zeroBytes }); }); }); describe('isTargetZero', () => { - it('should return true for the canonical zero account', () => { - expect(contract.isTargetZero(contract.zeroAccount())).toBe(true); + it('should return true for the canonical zero account', async () => { + expect(await contract.isTargetZero(await contract.zeroAccount())).toBe( + true, + ); }); - it('should return true for a zero right-variant (contract)', () => { + it('should return true for a zero right-variant (contract)', async () => { expect( - contract.isTargetZero({ + await contract.isTargetZero({ is_left: false, left: zeroBytes, right: { bytes: zeroBytes }, @@ -230,27 +240,31 @@ describe('Utils', () => { ).toBe(true); }); - it('should return false for a nonzero account (left variant)', () => { + it('should return false for a nonzero account (left variant)', async () => { const account = eitherAccount(buildAccountIdHash(createTestSK('ACCT'))); - expect(contract.isTargetZero(account)).toBe(false); + expect(await contract.isTargetZero(account)).toBe(false); }); - it('should return false for a nonzero contract (right variant)', () => { - expect(contract.isTargetZero(eitherContract('SOME_CONTRACT'))).toBe( + it('should return false for a nonzero contract (right variant)', async () => { + expect(await contract.isTargetZero(eitherContract('SOME_CONTRACT'))).toBe( false, ); }); }); describe('computeAccountId', () => { - it('should match the persistentHash derivation', () => { + it('should match the persistentHash derivation', async () => { const sk = createTestSK('SOME_SK'); - expect(contract.computeAccountId(sk)).toEqual(buildAccountIdHash(sk)); + expect(await contract.computeAccountId(sk)).toEqual( + buildAccountIdHash(sk), + ); }); - it('should produce distinct identifiers for distinct keys', () => { - const ids = ['A', 'B', 'C'].map((label) => - contract.computeAccountId(createTestSK(label)), + it('should produce distinct identifiers for distinct keys', async () => { + const ids = await Promise.all( + ['A', 'B', 'C'].map((label) => + contract.computeAccountId(createTestSK(label)), + ), ); for (let i = 0; i < ids.length; i++) { for (let j = i + 1; j < ids.length; j++) { @@ -261,8 +275,8 @@ describe('Utils', () => { }); describe('simulator wiring', () => { - it('should expose an empty public ledger via getPublicState', () => { - expect(contract.getPublicState()).toStrictEqual({}); + it('should expose an empty public ledger via getPublicState', async () => { + expect(await contract.getPublicState()).toStrictEqual({}); }); }); }); diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts new file mode 100644 index 00000000..f9a1d283 --- /dev/null +++ b/contracts/test/integration/_harness/deploy.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + CompiledContract, + Contract as ContractNs, +} from '@midnight-ntwrk/compact-js'; +import { + type DeployContractOptionsWithPrivateState, + type DeployedContract, + deployContract, +} from '@midnight-ntwrk/midnight-js-contracts'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Absolute path to `contracts/artifacts//`. + * Used by `NodeZkConfigProvider`, which expects the directory containing + * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). + */ +export function moduleRootPath(moduleName: string): string { + // _harness/ is at contracts/test/integration/_harness/ + // module root at contracts/artifacts// + return path.resolve( + currentDir, + '..', + '..', + '..', + 'artifacts', + moduleName, + ); +} + +/** + * Absolute path to `contracts/artifacts//contract/` — where the + * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) + * live. Used by `CompiledContract.withCompiledFileAssets`. + */ +export function contractAssetsPath(moduleName: string): string { + return path.join(moduleRootPath(moduleName), 'contract'); +} + +/** + * Generic deploy wrapper. + * + * Each per-module fixture builds its own `CompiledContract` (because + * `witnesses` are module-specific) and passes it here along with providers, + * a private-state id, the initial private-state value, and the contract's + * constructor arguments — all properly typed via `Contract.*` helpers from + * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. + */ +export async function deployModule( + providers: MidnightProviders< + ContractNs.ProvableCircuitId, + string, + ContractNs.PrivateState + >, + // The third generic of `CompiledContract` (the witnesses map) defaults to + // `never` for empty-witness contracts; accept `any` so both shapes pass. + compiledContract: CompiledContract.CompiledContract< + C, + ContractNs.PrivateState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + >, + privateStateId: string, + initialPrivateState: ContractNs.PrivateState, + args: ContractNs.InitializeParameters, +): Promise> { + // The deployContract options shape is conditional on whether + // `Contract.InitializeParameters` is empty — TypeScript can't reduce + // that conditional under an unbounded `C extends Contract.Any`, so we + // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. + // Two-step cast (through `unknown`) because TS rejects the direct cast + // as "neither type sufficiently overlaps" — same conditional-resolution + // issue. Scoped to this single helper. + const options = { + compiledContract, + privateStateId, + initialPrivateState, + args, + } as unknown as DeployContractOptionsWithPrivateState; + return deployContract(providers, options); +} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts new file mode 100644 index 00000000..e5b05fec --- /dev/null +++ b/contracts/test/integration/_harness/live.setup.ts @@ -0,0 +1,7 @@ +import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; + +// Runs once per worker before the unit specs when `test:live` is used. Registers +// the live backend so `await Sim.create()` attaches to a freshly-deployed +// contract on the local stack (brought up by `make env-up`). On the dry path +// (default `test`) this file is not loaded, so nothing changes there. +registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts new file mode 100644 index 00000000..dacf5ebc --- /dev/null +++ b/contracts/test/integration/_harness/network.ts @@ -0,0 +1,63 @@ +import { + type NetworkId, + setNetworkId, +} from '@midnight-ntwrk/midnight-js-network-id'; + +/** + * Endpoint configuration for the local stack. Replaces testkit-js' + * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally + * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. + */ +export interface LocalNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly networkId: string; + readonly indexer: string; + readonly indexerWS: string; + readonly node: string; + readonly nodeWS: string; + readonly proofServer: string; + readonly faucet: string | undefined; +} + +/** + * Prefunded wallet mnemonic for the local `undeployed` network — the canonical + * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` + * recognises as the genesis-funded account. Inlined so the harness no longer + * depends on testkit-js' `TEST_MNEMONIC`. + */ +export const LOCAL_WALLET_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; + +/** + * Default endpoints for the local stack brought up by `make env-up`. + * Each is overridable via a MIDNIGHT_* env var so CI / other hosts + * can point the same harness at a relocated stack. + */ +export function networkConfig(): LocalNetworkConfig { + return { + walletNetworkId: 'undeployed' as NetworkId, + networkId: 'undeployed', + indexer: + process.env.MIDNIGHT_INDEXER_URL ?? + 'http://127.0.0.1:8088/api/v4/graphql', + indexerWS: + process.env.MIDNIGHT_INDEXER_WS_URL ?? + 'ws://127.0.0.1:8088/api/v4/graphql/ws', + node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', + nodeWS: 'ws://127.0.0.1:9944', + proofServer: + process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', + faucet: undefined, + }; +} + +/** + * Set the process-wide network id. Must be called once before any provider + * or wallet is constructed. Idempotent. + */ +let networkIdSet = false; +export function setupNetwork(): void { + if (networkIdSet) return; + setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); + networkIdSet = true; +} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts new file mode 100644 index 00000000..fd2a78df --- /dev/null +++ b/contracts/test/integration/_harness/ownWallet.ts @@ -0,0 +1,289 @@ +/** + * Own test wallet provider — a testkit-js-free reconstruction of the wallet + * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * WHY THIS EXISTS + * --------------- + * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s + * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy + * dependency (testcontainers, docker orchestration, a fixed env model) of which + * we use almost nothing — we run our own local stack via `make env-up`. All + * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter + * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. + * + * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, + * so the harness no longer imports testkit. It is a behavioural drop-in for the + * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. + * + * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: + * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed + * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk + * `balanceTx` / `submitTx` delegate to the facade identically to testkit. + * + * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, + * `shielded`) so a future coin-injecting shielded wallet can be slotted in via + * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks + * the spend-path (burn / round-trip) integration specs. See + * `NativeShieldedToken-tests.md` "Own wallet tool". + */ +import { + DustSecretKey, + LedgerParameters, + ZswapSecretKeys, +} from '@midnight-ntwrk/ledger-v8'; +import type { + FinalizedTransaction, + TransactionId, +} from '@midnight-ntwrk/midnight-js-protocol/ledger'; +import type { + MidnightProvider, + WalletProvider, +} from '@midnight-ntwrk/midnight-js-types'; +import { + createKeystore, + DustWallet, + HDWallet, + InMemoryTransactionHistoryStorage, + mergeWalletEntries, + PublicKey, + type Role, + Roles, + ShieldedWallet, + UnshieldedWallet, + WalletEntrySchema, + WalletFacade, +} from '@midnight-ntwrk/wallet-sdk'; +import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import { mnemonicToSeedSync } from '@scure/bip39'; +import pino, { type Logger } from 'pino'; + +/** + * Minimal endpoint config our wallet needs — a structural subset of the fields + * `networkConfig()` already returns, with no testkit type dependency. + */ +export interface OwnNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly indexer: string; + readonly indexerWS: string; + readonly nodeWS: string; + readonly proofServer: string; +} + +/** + * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at + * preset-dev needs headroom to cover fees on undeployed; mirrors the value the + * old testkit-based `buildWallet` passed via `DustWalletOptions`. + */ +const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; + +let sharedLogger: Logger | undefined; +function ownLogger(): Logger { + if (!sharedLogger) { + sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); + } + return sharedLogger; +} + +/** The three role sub-seeds derived from a master seed, plus the master. */ +interface DerivedSeeds { + readonly masterSeedHex: string; + readonly shielded: Uint8Array; + readonly unshielded: Uint8Array; + readonly dust: Uint8Array; +} + +/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ +function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { + if (!masterSeedHex || masterSeedHex.length === 0) { + throw new Error('Own wallet: master seed cannot be empty'); + } + const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); + if (result.type !== 'seedOk') { + throw new Error('Own wallet: invalid seed, failed to create HD wallet'); + } + const derived = result.hdWallet + .selectAccount(0) + .selectRole(role) + .deriveKeyAt(0); + if (derived.type !== 'keyDerived') { + throw new Error(`Own wallet: key derivation failed for role ${role}`); + } + return derived.key; +} + +function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { + return { + masterSeedHex, + shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), + unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), + dust: deriveKeyForRole(masterSeedHex, Roles.Dust), + }; +} + +function seedsFromMnemonic(mnemonic: string): DerivedSeeds { + if (!mnemonic || mnemonic.trim().length === 0) { + throw new Error('Own wallet: mnemonic cannot be empty'); + } + return seedsFromMasterHex( + Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), + ); +} + +/** + * Map our endpoint config to the wallet-sdk facade configuration object. + * Shape lifted from testkit's `mapEnvironmentToConfiguration`. + */ +function facadeConfiguration(env: OwnNetworkConfig) { + return { + indexerClientConnection: { + indexerHttpUrl: env.indexer, + indexerWsUrl: env.indexerWS, + }, + provingServerUrl: new URL(env.proofServer), + networkId: env.walletNetworkId, + relayURL: new URL(env.nodeWS), + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + costParameters: { feeBlocksMargin: 5 }, + }; +} + +/** + * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, + * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the + * same delegations testkit's `MidnightWalletProvider` performed. + */ +export class OwnWalletProvider implements WalletProvider, MidnightProvider { + private constructor( + readonly env: OwnNetworkConfig, + readonly facade: WalletFacade, + readonly zswapSecretKeys: ZswapSecretKeys, + readonly dustSecretKey: DustSecretKey, + private readonly unshieldedKeystore: ReturnType, + private readonly logger: Logger, + ) {} + + getCoinPublicKey() { + return this.zswapSecretKeys.coinPublicKey; + } + + getEncryptionPublicKey() { + return this.zswapSecretKeys.encryptionPublicKey; + } + + async balanceTx( + tx: Parameters[0], + ttl: Date = ttlOneHour(), + ): Promise { + const recipe = await this.facade.balanceUnboundTransaction( + tx, + { + shieldedSecretKeys: this.zswapSecretKeys, + dustSecretKey: this.dustSecretKey, + }, + { ttl }, + ); + const signed = await this.facade.signRecipe(recipe, (payload) => + this.unshieldedKeystore.signData(payload), + ); + return this.facade.finalizeRecipe(signed); + } + + submitTx(tx: FinalizedTransaction): Promise { + return this.facade.submitTransaction(tx); + } + + async stop(): Promise { + await this.facade.stop(); + } + + /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ + static async build( + env: OwnNetworkConfig, + keyMaterial: { mnemonic: string } | { seedHex: string }, + options: { waitForFunds?: boolean } = {}, + ): Promise { + const logger = ownLogger(); + const seeds = + 'mnemonic' in keyMaterial + ? seedsFromMnemonic(keyMaterial.mnemonic) + : seedsFromMasterHex(keyMaterial.seedHex); + + const config = facadeConfiguration(env); + const unshieldedKeystore = createKeystore( + seeds.unshielded, + env.walletNetworkId, + ); + + const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); + const unshielded = UnshieldedWallet({ + ...config, + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); + + const dustConfig = { + ...config, + costParameters: { + ledgerParams: LedgerParameters.initialParameters(), + additionalFeeOverhead: + env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, + feeBlocksMargin: 5, + }, + }; + const dust = DustWallet(dustConfig).startWithSeed( + seeds.dust, + LedgerParameters.initialParameters().dust, + ); + + const facade = await WalletFacade.init({ + configuration: config, + shielded: () => shielded, + unshielded: () => unshielded, + dust: () => dust, + }); + + const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); + const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); + + logger.info('Own wallet: starting facade...'); + await facade.start(zswapSecretKeys, dustSecretKey); + if (options.waitForFunds ?? true) { + await waitForShieldedSync(facade, logger); + } + + return new OwnWalletProvider( + env, + facade, + zswapSecretKeys, + dustSecretKey, + unshieldedKeystore, + logger, + ); + } +} + +function ttlOneHour(): Date { + return new Date(Date.now() + 60 * 60 * 1000); +} + +/** + * Block until the shielded wallet reports a synced state. The facade's shielded + * API exposes a `waitForSyncedState`; fall back to a short settle if absent. + */ +async function waitForShieldedSync( + facade: WalletFacade, + logger: Logger, +): Promise { + const shielded = facade.shielded as { + waitForSyncedState?: (gap?: bigint) => Promise; + }; + if (typeof shielded.waitForSyncedState === 'function') { + await shielded.waitForSyncedState(); + logger.info('Own wallet: shielded state synced'); + } +} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts new file mode 100644 index 00000000..7a2cca7a --- /dev/null +++ b/contracts/test/integration/_harness/providers.ts @@ -0,0 +1,56 @@ +import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; +import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; +import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; +import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; +import type { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's + * artifact directory. Each module test passes its own `` so the + * ZK config provider reads that module's keys. + * + * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. + * + * @param wallet A started `TestWalletProvider` + * @param artifactPath Absolute path to `contracts/artifacts//contract` + * (the directory containing `contract-info.json` etc.) + * @param privateStateStoreName LevelDB namespace, unique per test contract + * @param circuitKeys Type parameter carrying the module's circuit union + */ +export function buildProviders< + CircuitKey extends string, + PrivateStateId extends string, + PrivateState, +>( + wallet: OwnWalletProvider, + artifactPath: string, + privateStateStoreName: string, +): MidnightProviders { + const zkConfigProvider = new NodeZkConfigProvider(artifactPath); + + const privateStateConfig = { + privateStateStoreName, + accountId: wallet.getCoinPublicKey(), + // Fixed test password: local/undeployed wallets don't need real entropy. + // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, + // min-length, mixed classes) deterministically across runs. + privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', + } as Parameters>[0]; + + return { + privateStateProvider: + levelPrivateStateProvider(privateStateConfig), + publicDataProvider: indexerPublicDataProvider( + wallet.env.indexer, + wallet.env.indexerWS, + ), + zkConfigProvider, + proofProvider: httpClientProofProvider( + wallet.env.proofServer, + zkConfigProvider, + ), + walletProvider: wallet, + midnightProvider: wallet, + }; +} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts new file mode 100644 index 00000000..bacbd094 --- /dev/null +++ b/contracts/test/integration/_harness/registerSimulatorLive.ts @@ -0,0 +1,154 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { CompiledContract } from '@midnight-ntwrk/compact-js'; +import { + type LiveBackendRequest, + type LiveContext, + registerLiveBackend, +} from '@openzeppelin/compact-simulator'; +import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; +import { + type LocalNetworkConfig, + networkConfig, + setupNetwork, +} from './network.js'; +import type { OwnWalletProvider } from './ownWallet.js'; +import { buildProviders } from './providers.js'; +import { buildWallet } from './wallet.js'; + +/** + * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local + * stack (`make env-up`). Registered once (from the `test:live` setup file); + * afterwards a migrated spec's `await Sim.create()` deploys the contract named by + * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the + * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. + * + * Deploy-per-`create()` gives each test a fresh contract (true isolation), which + * the unit specs assume (they rely on `beforeEach`-fresh state). + */ + +let env: LocalNetworkConfig | undefined; +let deployerPromise: Promise | undefined; +let deployCounter = 0; + +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +function getDeployer(): Promise { + if (!env) { + setupNetwork(); + env = networkConfig(); + } + if (!deployerPromise) { + deployerPromise = (async () => { + const wallet = await buildWallet(env as LocalNetworkConfig); + // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, + // so also wait for the dust wallet to sync — otherwise the first tx builds + // a stale dust spend proof (node rejects with InvalidDustSpendProof). + const dust = ( + wallet.facade as { + dust?: { waitForSyncedState?: () => Promise }; + } + ).dust; + if (typeof dust?.waitForSyncedState === 'function') { + await dust.waitForSyncedState(); + } + return wallet; + })(); + } + return deployerPromise; +} + +async function buildLiveContext( + req: LiveBackendRequest, +): Promise> { + const name = req.config.artifactName; + if (!name) { + throw new Error( + 'live backend: SimulatorConfig.artifactName is required to deploy on live', + ); + } + + const contractEntry = pathToFileURL( + path.join(moduleRootPath(name), 'contract', 'index.js'), + ).href; + const mod = await import(contractEntry); + const ContractClass = mod.Contract; + + const witnesses = req.config.witnessesFactory(); + const compiled = CompiledContract.make(name, ContractClass).pipe( + CompiledContract.withWitnesses((witnesses ?? {}) as never), + CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), + ); + + const deployer = await getDeployer(); + const psId = `${name}-ps`; + const storeName = `${name}-${++deployCounter}`; + // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics + const providers = buildProviders( + deployer, + moduleRootPath(name), + storeName, + ) as any; + + const initialPS = + req.options.privateState ?? req.config.defaultPrivateState(); + const args = req.config.contractArgs(...req.contractArgs); + + // Retry: a freshly-started dev node may still be ramping dust generation, and + // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. + let deployed: Awaited> | undefined; + let lastErr: unknown; + for (let attempt = 1; attempt <= 8; attempt++) { + try { + // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific + deployed = await deployModule( + providers, + compiled, + psId, + initialPS, + args as any, + ); + break; + } catch (err) { + lastErr = err; + await sleep(5000); + } + } + if (!deployed) { + throw new Error( + `deploy of ${name} failed after retries: ${String(lastErr)}`, + ); + } + const address = deployed.deployTxData.public.contractAddress; + + return { + contractAddress: address, + handleFor: async () => ({ + // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle + callTx: deployed.callTx as any, + }), + async queryLedger() { + for (let attempt = 0; attempt < 15; attempt++) { + const cs = + await providers.publicDataProvider.queryContractState(address); + if (cs != null) return cs.data; + await sleep(400); + } + throw new Error(`no contract state at ${address} after retries`); + }, + async queryPrivateState() { + const ps = await providers.privateStateProvider.get(psId); + return ps ?? initialPS; + }, + }; +} + +let registered = false; + +/** Registers the live backend. Idempotent per worker. */ +export function registerSimulatorLiveBackend(): void { + if (registered) return; + registered = true; + registerLiveBackend((req) => buildLiveContext(req)); +} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts new file mode 100644 index 00000000..70e0f88b --- /dev/null +++ b/contracts/test/integration/_harness/wallet.ts @@ -0,0 +1,17 @@ +import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; +import { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js + * dependency. `OwnWalletProvider` implements both `MidnightProvider` and + * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. + * Tests that need per-signer isolation pass their own BIP39 phrase. + */ +export async function buildWallet( + env: LocalNetworkConfig, + mnemonic: string = LOCAL_WALLET_MNEMONIC, +): Promise { + return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); +} diff --git a/contracts/vitest.live.config.ts b/contracts/vitest.live.config.ts new file mode 100644 index 00000000..52377aad --- /dev/null +++ b/contracts/vitest.live.config.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..fedb1fef --- /dev/null +++ b/local-env.yml @@ -0,0 +1,61 @@ +# WARNING: Insecure default credentials below. For local development only — do not use in production. +services: + proof-server: + image: 'midnightntwrk/proof-server:latest' + command: ['midnight-proof-server -v'] + ports: + - '6300:6300' + environment: + RUST_BACKTRACE: 'full' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + + indexer: + image: 'midnightntwrk/indexer-standalone:latest' + ports: + - '8088:8088' + environment: + RUST_LOG: 'indexer=debug,chain_indexer=debug,indexer_api=debug,wallet_indexer=debug,indexer_common=debug,fastrace_opentelemetry=off,info' + APP__INFRA__NODE__URL: 'ws://node:9944' + APP__APPLICATION__NETWORK_ID: 'undeployed' + APP__INFRA__STORAGE__PASSWORD: 'indexer' + APP__INFRA__PUB_SUB__PASSWORD: 'indexer' + APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' + APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' + healthcheck: + test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + depends_on: + node: + condition: service_healthy + logging: + driver: local + options: + max-size: '10m' + max-file: '3' + + node: + image: 'midnightntwrk/midnight-node:0.22.2' + ports: + - '9944:9944' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] + interval: 2s + timeout: 5s + retries: 20 + start_period: 5s + environment: + CFG_PRESET: 'dev' + SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' + logging: + driver: local + options: + max-size: '10m' + max-file: '3' diff --git a/yarn.lock b/yarn.lock index 873c24f2..e4bfd1a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -301,7 +301,7 @@ __metadata: resolution: "@openzeppelin/compact-contracts@workspace:contracts" dependencies: "@openzeppelin/compact-cli": "npm:^0.0.2" - "@openzeppelin/compact-simulator": "npm:^0.1.0" + "@openzeppelin/compact-simulator": "portal:../../compact-tools/packages/simulator" "@tsconfig/node24": "npm:^24.0.4" "@types/node": "npm:25.9.3" "@vitest/coverage-v8": "npm:^4.1.9" @@ -312,15 +312,22 @@ __metadata: languageName: unknown linkType: soft -"@openzeppelin/compact-simulator@npm:^0.1.0": - version: 0.1.0 - resolution: "@openzeppelin/compact-simulator@npm:0.1.0" +"@openzeppelin/compact-simulator@portal:../../compact-tools/packages/simulator::locator=%40openzeppelin%2Fcompact-contracts%40workspace%3Acontracts": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-simulator@portal:../../compact-tools/packages/simulator::locator=%40openzeppelin%2Fcompact-contracts%40workspace%3Acontracts" dependencies: "@midnight-ntwrk/compact-runtime": "npm:0.16.0" "@midnight-ntwrk/ledger-v8": "npm:8.1.0" - checksum: 10/432bb2b4c8440e30046198471bb34cf08efec697d10a8f3cc0ce4ffe78e339eb7ea2c405e3114dbdae28c63e07936cba039105277f1ac17d521536b55aa0c7d0 + peerDependencies: + "@midnight-ntwrk/midnight-js-contracts": ^4.1.0 + "@midnight-ntwrk/midnight-js-types": ^4.1.0 + peerDependenciesMeta: + "@midnight-ntwrk/midnight-js-contracts": + optional: true + "@midnight-ntwrk/midnight-js-types": + optional: true languageName: node - linkType: hard + linkType: soft "@oxc-project/types@npm:=0.122.0": version: 0.122.0 From 625ad7a118293af1f3264a51a669c9f8c1d59dc5 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 24 Jun 2026 19:06:28 +0200 Subject: [PATCH 02/10] build(deps-dev): pin compact-simulator to 0.2.0 The async backend-aware simulator (OpenZeppelin/compact-tools#121) is now published as @openzeppelin/compact-simulator@0.2.0. Replace the placeholder `portal:` dependency on a sibling compact-tools checkout with the published package, so the default `test` CI can install the dependency and run the dry suite (the `portal:` link could not resolve in CI). Validated dry on the utils, access, token, and security modules (298 tests green) against the published API; the imported surface (createSimulator, registerLiveBackend, LiveBackendRequest, LiveContext, SimulatorOptions) is unchanged from the portal build. --- contracts/package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index d1a80115..7796032d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -47,7 +47,7 @@ "@openzeppelin/compact-cli": "^0.0.2" }, "devDependencies": { - "@openzeppelin/compact-simulator": "portal:../../compact-tools/packages/simulator", + "@openzeppelin/compact-simulator": "^0.2.0", "@tsconfig/node24": "^24.0.4", "@types/node": "25.9.3", "@vitest/coverage-v8": "^4.1.9", diff --git a/yarn.lock b/yarn.lock index e4bfd1a9..a5435c2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -301,7 +301,7 @@ __metadata: resolution: "@openzeppelin/compact-contracts@workspace:contracts" dependencies: "@openzeppelin/compact-cli": "npm:^0.0.2" - "@openzeppelin/compact-simulator": "portal:../../compact-tools/packages/simulator" + "@openzeppelin/compact-simulator": "npm:^0.2.0" "@tsconfig/node24": "npm:^24.0.4" "@types/node": "npm:25.9.3" "@vitest/coverage-v8": "npm:^4.1.9" @@ -312,9 +312,9 @@ __metadata: languageName: unknown linkType: soft -"@openzeppelin/compact-simulator@portal:../../compact-tools/packages/simulator::locator=%40openzeppelin%2Fcompact-contracts%40workspace%3Acontracts": - version: 0.0.0-use.local - resolution: "@openzeppelin/compact-simulator@portal:../../compact-tools/packages/simulator::locator=%40openzeppelin%2Fcompact-contracts%40workspace%3Acontracts" +"@openzeppelin/compact-simulator@npm:^0.2.0": + version: 0.2.0 + resolution: "@openzeppelin/compact-simulator@npm:0.2.0" dependencies: "@midnight-ntwrk/compact-runtime": "npm:0.16.0" "@midnight-ntwrk/ledger-v8": "npm:8.1.0" @@ -326,8 +326,9 @@ __metadata: optional: true "@midnight-ntwrk/midnight-js-types": optional: true + checksum: 10/294e53a3eaade37ae679be8aaff764239d8cb645eae0e69ceb56587b36614ed67b9a38ad641633d91a6ee65d96920475ccedec40c1f1dd734d26a86b0382ce90 languageName: node - linkType: soft + linkType: hard "@oxc-project/types@npm:=0.122.0": version: 0.122.0 From b35b06e859b22ff251d7680594704472c1b58bc3 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 24 Jun 2026 19:48:45 +0200 Subject: [PATCH 03/10] ci(live-tests): drop redundant compact-tools build step The live backend is now consumed from the published @openzeppelin/compact-simulator, so the sibling compact-tools checkout and `yarn workspace ... build` step are no longer needed to resolve the dependency. Remove both and install against the committed lockfile with `--immutable` (was `--no-immutable`, required only while the `portal:` link had to be re-resolved). Behaviour-only cleanup of the label-gated workflow; the dry `test` CI is unaffected. --- .github/workflows/test-live.yml | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-live.yml b/.github/workflows/test-live.yml index 493779d4..8ea5bce8 100644 --- a/.github/workflows/test-live.yml +++ b/.github/workflows/test-live.yml @@ -6,11 +6,9 @@ name: Compact Contracts Live Test Suite # heavy: gated to manual runs and PRs explicitly labeled `live-tests` rather # than every push. # -# DRAFT: this depends on `@openzeppelin/compact-simulator`'s live backend, which -# is consumed here via a `portal:` dependency on the sibling `compact-tools` -# checkout below. Once the simulator is published with the live backend, the -# "Check out simulator" + "Build simulator" steps can be dropped and the -# dependency pinned to the published version. +# Consumes the published `@openzeppelin/compact-simulator` (live backend), so the +# install is a plain `yarn install --immutable` against the committed lockfile — +# no sibling `compact-tools` checkout/build. on: workflow_dispatch: @@ -40,15 +38,6 @@ jobs: with: path: compact-contracts - # Sibling checkout so the `portal:../../compact-tools/packages/simulator` - # dependency in contracts/package.json resolves. - - name: Check out compact-tools (simulator source) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - repository: OpenZeppelin/compact-tools - ref: main - path: compact-tools - - name: Set up Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: @@ -56,15 +45,9 @@ jobs: - name: Enable Corepack run: corepack enable - - name: Build simulator (live backend) - working-directory: compact-tools - run: | - yarn install --immutable - yarn workspace @openzeppelin/compact-simulator build - - name: Install contracts dependencies working-directory: compact-contracts - run: yarn install --no-immutable + run: yarn install --immutable - name: Start local Midnight stack working-directory: compact-contracts From 885e67945811ebabe3a05f3479ad3cbeddacbe82 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 11:00:49 +0200 Subject: [PATCH 04/10] test: remove live and integration test suites Rescope this PR to the dry async unit-test migration only. Remove the live-backend wiring (test/integration/_harness, the vitest.live.config.ts, the test:live script, Makefile + local-env.yml) and the label-gated live-tests workflow. The live workflow returns in a follow-up PR (as a sharded GitHub-hosted matrix, since a single job caps at 6h). Also drop the pre-existing integration suite (test/integration specs, fixtures, _mocks, vitest.integration.config.ts, the compact:integration and test:integration scripts): it uses the old synchronous simulator API and does not run against the async @openzeppelin/compact-simulator 0.2.0, and it is not wired into CI. It will be re-added, migrated, alongside the live workflow. What remains: the per-module simulator + spec async migration and the @openzeppelin/compact-simulator ^0.2.0 pin. --- .github/workflows/test-live.yml | 74 ----- Makefile | 31 -- contracts/package.json | 3 - contracts/test/integration/README.md | 8 - contracts/test/integration/_harness/deploy.ts | 84 ----- .../test/integration/_harness/live.setup.ts | 7 - .../test/integration/_harness/network.ts | 63 ---- .../test/integration/_harness/ownWallet.ts | 289 ------------------ .../test/integration/_harness/providers.ts | 56 ---- .../_harness/registerSimulatorLive.ts | 154 ---------- contracts/test/integration/_harness/wallet.ts | 17 -- .../integration/_mocks/ComposedTokens.compact | 48 --- .../_mocks/SharedInitCollision.compact | 21 -- .../_mocks/sharedInit/ModuleA.compact | 17 -- .../_mocks/sharedInit/ModuleB.compact | 17 -- .../integration/fixtures/composedTokens.ts | 61 ---- .../fixtures/sharedInitCollision.ts | 50 --- .../specs/initStateIsolation.spec.ts | 73 ----- contracts/vitest.integration.config.ts | 13 - contracts/vitest.live.config.ts | 24 -- local-env.yml | 61 ---- 21 files changed, 1171 deletions(-) delete mode 100644 .github/workflows/test-live.yml delete mode 100644 Makefile delete mode 100644 contracts/test/integration/README.md delete mode 100644 contracts/test/integration/_harness/deploy.ts delete mode 100644 contracts/test/integration/_harness/live.setup.ts delete mode 100644 contracts/test/integration/_harness/network.ts delete mode 100644 contracts/test/integration/_harness/ownWallet.ts delete mode 100644 contracts/test/integration/_harness/providers.ts delete mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts delete mode 100644 contracts/test/integration/_harness/wallet.ts delete mode 100644 contracts/test/integration/_mocks/ComposedTokens.compact delete mode 100644 contracts/test/integration/_mocks/SharedInitCollision.compact delete mode 100644 contracts/test/integration/_mocks/sharedInit/ModuleA.compact delete mode 100644 contracts/test/integration/_mocks/sharedInit/ModuleB.compact delete mode 100644 contracts/test/integration/fixtures/composedTokens.ts delete mode 100644 contracts/test/integration/fixtures/sharedInitCollision.ts delete mode 100644 contracts/test/integration/specs/initStateIsolation.spec.ts delete mode 100644 contracts/vitest.integration.config.ts delete mode 100644 contracts/vitest.live.config.ts delete mode 100644 local-env.yml diff --git a/.github/workflows/test-live.yml b/.github/workflows/test-live.yml deleted file mode 100644 index 8ea5bce8..00000000 --- a/.github/workflows/test-live.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Compact Contracts Live Test Suite - -# Runs the unit specs against a real local Midnight stack (node + indexer + -# proof-server) via the simulator's live backend (`MIDNIGHT_BACKEND=live`). -# This is the full prove -> submit -> index -> read loop, so it is slow and -# heavy: gated to manual runs and PRs explicitly labeled `live-tests` rather -# than every push. -# -# Consumes the published `@openzeppelin/compact-simulator` (live backend), so the -# install is a plain `yarn install --immutable` against the committed lockfile — -# no sibling `compact-tools` checkout/build. - -on: - workflow_dispatch: - pull_request: - types: [labeled, synchronize, reopened] - -jobs: - run-live-suite: - name: Run Live Test Suite - # Only on manual dispatch or when the PR carries the `live-tests` label. - if: >- - github.event_name == 'workflow_dispatch' || - contains(github.event.pull_request.labels.*.name, 'live-tests') - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 60 - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Check out compact-contracts - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - path: compact-contracts - - - name: Set up Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - - name: Enable Corepack - run: corepack enable - - - name: Install contracts dependencies - working-directory: compact-contracts - run: yarn install --immutable - - - name: Start local Midnight stack - working-directory: compact-contracts - run: make env-up - - - name: Wait for stack to be healthy - run: | - for i in $(seq 1 30); do - if curl -fsS http://127.0.0.1:6300/version >/dev/null \ - && curl -fsS http://127.0.0.1:9944/health >/dev/null; then - echo "stack healthy"; exit 0 - fi - echo "waiting for stack... ($i)"; sleep 5 - done - echo "stack did not become healthy in time"; exit 1 - - - name: Run live tests - working-directory: compact-contracts - run: yarn test:live - - - name: Stop local Midnight stack - if: always() - working-directory: compact-contracts - run: make env-down diff --git a/Makefile b/Makefile deleted file mode 100644 index e5a601bf..00000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -COMPOSE_FILE := local-env.yml -LOGS_DIR := logs -SERVICES := proof-server indexer node - -.PHONY: env-up env-down env-logs env-logs-clean env-status - -## Start local environment and stream logs to logs/ -env-up: env-down - docker compose -f $(COMPOSE_FILE) up -d - @mkdir -p $(LOGS_DIR) - @for svc in $(SERVICES); do \ - docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ - done - @echo "Logs streaming to $(LOGS_DIR)/" - -## Stop local environment -env-down: - @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true - docker compose -f $(COMPOSE_FILE) down - -## Tail all logs -env-logs: - tail -f $(LOGS_DIR)/*.log - -## Clear log files -env-logs-clean: - rm -rf $(LOGS_DIR)/*.log - -## Show container status -env-status: - docker compose -f $(COMPOSE_FILE) ps diff --git a/contracts/package.json b/contracts/package.json index 7796032d..50ded4f7 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,9 +34,6 @@ "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", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/contracts/test/integration/README.md b/contracts/test/integration/README.md deleted file mode 100644 index 0979f99f..00000000 --- a/contracts/test/integration/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Integration tests - -Compose multiple production modules into one top-level contract and drive it -through the simulator, covering interactions the per-module unit tests can't. - -```sh -yarn test:integration -``` diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts deleted file mode 100644 index f9a1d283..00000000 --- a/contracts/test/integration/_harness/deploy.ts +++ /dev/null @@ -1,84 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - CompiledContract, - Contract as ContractNs, -} from '@midnight-ntwrk/compact-js'; -import { - type DeployContractOptionsWithPrivateState, - type DeployedContract, - deployContract, -} from '@midnight-ntwrk/midnight-js-contracts'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; - -const currentDir = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Absolute path to `contracts/artifacts//`. - * Used by `NodeZkConfigProvider`, which expects the directory containing - * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). - */ -export function moduleRootPath(moduleName: string): string { - // _harness/ is at contracts/test/integration/_harness/ - // module root at contracts/artifacts// - return path.resolve( - currentDir, - '..', - '..', - '..', - 'artifacts', - moduleName, - ); -} - -/** - * Absolute path to `contracts/artifacts//contract/` — where the - * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) - * live. Used by `CompiledContract.withCompiledFileAssets`. - */ -export function contractAssetsPath(moduleName: string): string { - return path.join(moduleRootPath(moduleName), 'contract'); -} - -/** - * Generic deploy wrapper. - * - * Each per-module fixture builds its own `CompiledContract` (because - * `witnesses` are module-specific) and passes it here along with providers, - * a private-state id, the initial private-state value, and the contract's - * constructor arguments — all properly typed via `Contract.*` helpers from - * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. - */ -export async function deployModule( - providers: MidnightProviders< - ContractNs.ProvableCircuitId, - string, - ContractNs.PrivateState - >, - // The third generic of `CompiledContract` (the witnesses map) defaults to - // `never` for empty-witness contracts; accept `any` so both shapes pass. - compiledContract: CompiledContract.CompiledContract< - C, - ContractNs.PrivateState, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any - >, - privateStateId: string, - initialPrivateState: ContractNs.PrivateState, - args: ContractNs.InitializeParameters, -): Promise> { - // The deployContract options shape is conditional on whether - // `Contract.InitializeParameters` is empty — TypeScript can't reduce - // that conditional under an unbounded `C extends Contract.Any`, so we - // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. - // Two-step cast (through `unknown`) because TS rejects the direct cast - // as "neither type sufficiently overlaps" — same conditional-resolution - // issue. Scoped to this single helper. - const options = { - compiledContract, - privateStateId, - initialPrivateState, - args, - } as unknown as DeployContractOptionsWithPrivateState; - return deployContract(providers, options); -} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts deleted file mode 100644 index e5b05fec..00000000 --- a/contracts/test/integration/_harness/live.setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; - -// Runs once per worker before the unit specs when `test:live` is used. Registers -// the live backend so `await Sim.create()` attaches to a freshly-deployed -// contract on the local stack (brought up by `make env-up`). On the dry path -// (default `test`) this file is not loaded, so nothing changes there. -registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts deleted file mode 100644 index dacf5ebc..00000000 --- a/contracts/test/integration/_harness/network.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - type NetworkId, - setNetworkId, -} from '@midnight-ntwrk/midnight-js-network-id'; - -/** - * Endpoint configuration for the local stack. Replaces testkit-js' - * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally - * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. - */ -export interface LocalNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly networkId: string; - readonly indexer: string; - readonly indexerWS: string; - readonly node: string; - readonly nodeWS: string; - readonly proofServer: string; - readonly faucet: string | undefined; -} - -/** - * Prefunded wallet mnemonic for the local `undeployed` network — the canonical - * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` - * recognises as the genesis-funded account. Inlined so the harness no longer - * depends on testkit-js' `TEST_MNEMONIC`. - */ -export const LOCAL_WALLET_MNEMONIC = - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; - -/** - * Default endpoints for the local stack brought up by `make env-up`. - * Each is overridable via a MIDNIGHT_* env var so CI / other hosts - * can point the same harness at a relocated stack. - */ -export function networkConfig(): LocalNetworkConfig { - return { - walletNetworkId: 'undeployed' as NetworkId, - networkId: 'undeployed', - indexer: - process.env.MIDNIGHT_INDEXER_URL ?? - 'http://127.0.0.1:8088/api/v4/graphql', - indexerWS: - process.env.MIDNIGHT_INDEXER_WS_URL ?? - 'ws://127.0.0.1:8088/api/v4/graphql/ws', - node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', - nodeWS: 'ws://127.0.0.1:9944', - proofServer: - process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', - faucet: undefined, - }; -} - -/** - * Set the process-wide network id. Must be called once before any provider - * or wallet is constructed. Idempotent. - */ -let networkIdSet = false; -export function setupNetwork(): void { - if (networkIdSet) return; - setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); - networkIdSet = true; -} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts deleted file mode 100644 index fd2a78df..00000000 --- a/contracts/test/integration/_harness/ownWallet.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Own test wallet provider — a testkit-js-free reconstruction of the wallet - * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * WHY THIS EXISTS - * --------------- - * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s - * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy - * dependency (testcontainers, docker orchestration, a fixed env model) of which - * we use almost nothing — we run our own local stack via `make env-up`. All - * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter - * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. - * - * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, - * so the harness no longer imports testkit. It is a behavioural drop-in for the - * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. - * - * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: - * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed - * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk - * `balanceTx` / `submitTx` delegate to the facade identically to testkit. - * - * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, - * `shielded`) so a future coin-injecting shielded wallet can be slotted in via - * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks - * the spend-path (burn / round-trip) integration specs. See - * `NativeShieldedToken-tests.md` "Own wallet tool". - */ -import { - DustSecretKey, - LedgerParameters, - ZswapSecretKeys, -} from '@midnight-ntwrk/ledger-v8'; -import type { - FinalizedTransaction, - TransactionId, -} from '@midnight-ntwrk/midnight-js-protocol/ledger'; -import type { - MidnightProvider, - WalletProvider, -} from '@midnight-ntwrk/midnight-js-types'; -import { - createKeystore, - DustWallet, - HDWallet, - InMemoryTransactionHistoryStorage, - mergeWalletEntries, - PublicKey, - type Role, - Roles, - ShieldedWallet, - UnshieldedWallet, - WalletEntrySchema, - WalletFacade, -} from '@midnight-ntwrk/wallet-sdk'; -import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; -import { mnemonicToSeedSync } from '@scure/bip39'; -import pino, { type Logger } from 'pino'; - -/** - * Minimal endpoint config our wallet needs — a structural subset of the fields - * `networkConfig()` already returns, with no testkit type dependency. - */ -export interface OwnNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly indexer: string; - readonly indexerWS: string; - readonly nodeWS: string; - readonly proofServer: string; -} - -/** - * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at - * preset-dev needs headroom to cover fees on undeployed; mirrors the value the - * old testkit-based `buildWallet` passed via `DustWalletOptions`. - */ -const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; - -let sharedLogger: Logger | undefined; -function ownLogger(): Logger { - if (!sharedLogger) { - sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); - } - return sharedLogger; -} - -/** The three role sub-seeds derived from a master seed, plus the master. */ -interface DerivedSeeds { - readonly masterSeedHex: string; - readonly shielded: Uint8Array; - readonly unshielded: Uint8Array; - readonly dust: Uint8Array; -} - -/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ -function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { - if (!masterSeedHex || masterSeedHex.length === 0) { - throw new Error('Own wallet: master seed cannot be empty'); - } - const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); - if (result.type !== 'seedOk') { - throw new Error('Own wallet: invalid seed, failed to create HD wallet'); - } - const derived = result.hdWallet - .selectAccount(0) - .selectRole(role) - .deriveKeyAt(0); - if (derived.type !== 'keyDerived') { - throw new Error(`Own wallet: key derivation failed for role ${role}`); - } - return derived.key; -} - -function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { - return { - masterSeedHex, - shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), - unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), - dust: deriveKeyForRole(masterSeedHex, Roles.Dust), - }; -} - -function seedsFromMnemonic(mnemonic: string): DerivedSeeds { - if (!mnemonic || mnemonic.trim().length === 0) { - throw new Error('Own wallet: mnemonic cannot be empty'); - } - return seedsFromMasterHex( - Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), - ); -} - -/** - * Map our endpoint config to the wallet-sdk facade configuration object. - * Shape lifted from testkit's `mapEnvironmentToConfiguration`. - */ -function facadeConfiguration(env: OwnNetworkConfig) { - return { - indexerClientConnection: { - indexerHttpUrl: env.indexer, - indexerWsUrl: env.indexerWS, - }, - provingServerUrl: new URL(env.proofServer), - networkId: env.walletNetworkId, - relayURL: new URL(env.nodeWS), - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - costParameters: { feeBlocksMargin: 5 }, - }; -} - -/** - * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, - * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the - * same delegations testkit's `MidnightWalletProvider` performed. - */ -export class OwnWalletProvider implements WalletProvider, MidnightProvider { - private constructor( - readonly env: OwnNetworkConfig, - readonly facade: WalletFacade, - readonly zswapSecretKeys: ZswapSecretKeys, - readonly dustSecretKey: DustSecretKey, - private readonly unshieldedKeystore: ReturnType, - private readonly logger: Logger, - ) {} - - getCoinPublicKey() { - return this.zswapSecretKeys.coinPublicKey; - } - - getEncryptionPublicKey() { - return this.zswapSecretKeys.encryptionPublicKey; - } - - async balanceTx( - tx: Parameters[0], - ttl: Date = ttlOneHour(), - ): Promise { - const recipe = await this.facade.balanceUnboundTransaction( - tx, - { - shieldedSecretKeys: this.zswapSecretKeys, - dustSecretKey: this.dustSecretKey, - }, - { ttl }, - ); - const signed = await this.facade.signRecipe(recipe, (payload) => - this.unshieldedKeystore.signData(payload), - ); - return this.facade.finalizeRecipe(signed); - } - - submitTx(tx: FinalizedTransaction): Promise { - return this.facade.submitTransaction(tx); - } - - async stop(): Promise { - await this.facade.stop(); - } - - /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ - static async build( - env: OwnNetworkConfig, - keyMaterial: { mnemonic: string } | { seedHex: string }, - options: { waitForFunds?: boolean } = {}, - ): Promise { - const logger = ownLogger(); - const seeds = - 'mnemonic' in keyMaterial - ? seedsFromMnemonic(keyMaterial.mnemonic) - : seedsFromMasterHex(keyMaterial.seedHex); - - const config = facadeConfiguration(env); - const unshieldedKeystore = createKeystore( - seeds.unshielded, - env.walletNetworkId, - ); - - const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); - const unshielded = UnshieldedWallet({ - ...config, - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); - - const dustConfig = { - ...config, - costParameters: { - ledgerParams: LedgerParameters.initialParameters(), - additionalFeeOverhead: - env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, - feeBlocksMargin: 5, - }, - }; - const dust = DustWallet(dustConfig).startWithSeed( - seeds.dust, - LedgerParameters.initialParameters().dust, - ); - - const facade = await WalletFacade.init({ - configuration: config, - shielded: () => shielded, - unshielded: () => unshielded, - dust: () => dust, - }); - - const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); - const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); - - logger.info('Own wallet: starting facade...'); - await facade.start(zswapSecretKeys, dustSecretKey); - if (options.waitForFunds ?? true) { - await waitForShieldedSync(facade, logger); - } - - return new OwnWalletProvider( - env, - facade, - zswapSecretKeys, - dustSecretKey, - unshieldedKeystore, - logger, - ); - } -} - -function ttlOneHour(): Date { - return new Date(Date.now() + 60 * 60 * 1000); -} - -/** - * Block until the shielded wallet reports a synced state. The facade's shielded - * API exposes a `waitForSyncedState`; fall back to a short settle if absent. - */ -async function waitForShieldedSync( - facade: WalletFacade, - logger: Logger, -): Promise { - const shielded = facade.shielded as { - waitForSyncedState?: (gap?: bigint) => Promise; - }; - if (typeof shielded.waitForSyncedState === 'function') { - await shielded.waitForSyncedState(); - logger.info('Own wallet: shielded state synced'); - } -} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts deleted file mode 100644 index 7a2cca7a..00000000 --- a/contracts/test/integration/_harness/providers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; -import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; -import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; -import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; -import type { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's - * artifact directory. Each module test passes its own `` so the - * ZK config provider reads that module's keys. - * - * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. - * - * @param wallet A started `TestWalletProvider` - * @param artifactPath Absolute path to `contracts/artifacts//contract` - * (the directory containing `contract-info.json` etc.) - * @param privateStateStoreName LevelDB namespace, unique per test contract - * @param circuitKeys Type parameter carrying the module's circuit union - */ -export function buildProviders< - CircuitKey extends string, - PrivateStateId extends string, - PrivateState, ->( - wallet: OwnWalletProvider, - artifactPath: string, - privateStateStoreName: string, -): MidnightProviders { - const zkConfigProvider = new NodeZkConfigProvider(artifactPath); - - const privateStateConfig = { - privateStateStoreName, - accountId: wallet.getCoinPublicKey(), - // Fixed test password: local/undeployed wallets don't need real entropy. - // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, - // min-length, mixed classes) deterministically across runs. - privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', - } as Parameters>[0]; - - return { - privateStateProvider: - levelPrivateStateProvider(privateStateConfig), - publicDataProvider: indexerPublicDataProvider( - wallet.env.indexer, - wallet.env.indexerWS, - ), - zkConfigProvider, - proofProvider: httpClientProofProvider( - wallet.env.proofServer, - zkConfigProvider, - ), - walletProvider: wallet, - midnightProvider: wallet, - }; -} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts deleted file mode 100644 index bacbd094..00000000 --- a/contracts/test/integration/_harness/registerSimulatorLive.ts +++ /dev/null @@ -1,154 +0,0 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { CompiledContract } from '@midnight-ntwrk/compact-js'; -import { - type LiveBackendRequest, - type LiveContext, - registerLiveBackend, -} from '@openzeppelin/compact-simulator'; -import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; -import { - type LocalNetworkConfig, - networkConfig, - setupNetwork, -} from './network.js'; -import type { OwnWalletProvider } from './ownWallet.js'; -import { buildProviders } from './providers.js'; -import { buildWallet } from './wallet.js'; - -/** - * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local - * stack (`make env-up`). Registered once (from the `test:live` setup file); - * afterwards a migrated spec's `await Sim.create()` deploys the contract named by - * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the - * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. - * - * Deploy-per-`create()` gives each test a fresh contract (true isolation), which - * the unit specs assume (they rely on `beforeEach`-fresh state). - */ - -let env: LocalNetworkConfig | undefined; -let deployerPromise: Promise | undefined; -let deployCounter = 0; - -const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -function getDeployer(): Promise { - if (!env) { - setupNetwork(); - env = networkConfig(); - } - if (!deployerPromise) { - deployerPromise = (async () => { - const wallet = await buildWallet(env as LocalNetworkConfig); - // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, - // so also wait for the dust wallet to sync — otherwise the first tx builds - // a stale dust spend proof (node rejects with InvalidDustSpendProof). - const dust = ( - wallet.facade as { - dust?: { waitForSyncedState?: () => Promise }; - } - ).dust; - if (typeof dust?.waitForSyncedState === 'function') { - await dust.waitForSyncedState(); - } - return wallet; - })(); - } - return deployerPromise; -} - -async function buildLiveContext( - req: LiveBackendRequest, -): Promise> { - const name = req.config.artifactName; - if (!name) { - throw new Error( - 'live backend: SimulatorConfig.artifactName is required to deploy on live', - ); - } - - const contractEntry = pathToFileURL( - path.join(moduleRootPath(name), 'contract', 'index.js'), - ).href; - const mod = await import(contractEntry); - const ContractClass = mod.Contract; - - const witnesses = req.config.witnessesFactory(); - const compiled = CompiledContract.make(name, ContractClass).pipe( - CompiledContract.withWitnesses((witnesses ?? {}) as never), - CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), - ); - - const deployer = await getDeployer(); - const psId = `${name}-ps`; - const storeName = `${name}-${++deployCounter}`; - // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics - const providers = buildProviders( - deployer, - moduleRootPath(name), - storeName, - ) as any; - - const initialPS = - req.options.privateState ?? req.config.defaultPrivateState(); - const args = req.config.contractArgs(...req.contractArgs); - - // Retry: a freshly-started dev node may still be ramping dust generation, and - // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. - let deployed: Awaited> | undefined; - let lastErr: unknown; - for (let attempt = 1; attempt <= 8; attempt++) { - try { - // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific - deployed = await deployModule( - providers, - compiled, - psId, - initialPS, - args as any, - ); - break; - } catch (err) { - lastErr = err; - await sleep(5000); - } - } - if (!deployed) { - throw new Error( - `deploy of ${name} failed after retries: ${String(lastErr)}`, - ); - } - const address = deployed.deployTxData.public.contractAddress; - - return { - contractAddress: address, - handleFor: async () => ({ - // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle - callTx: deployed.callTx as any, - }), - async queryLedger() { - for (let attempt = 0; attempt < 15; attempt++) { - const cs = - await providers.publicDataProvider.queryContractState(address); - if (cs != null) return cs.data; - await sleep(400); - } - throw new Error(`no contract state at ${address} after retries`); - }, - async queryPrivateState() { - const ps = await providers.privateStateProvider.get(psId); - return ps ?? initialPS; - }, - }; -} - -let registered = false; - -/** Registers the live backend. Idempotent per worker. */ -export function registerSimulatorLiveBackend(): void { - if (registered) return; - registered = true; - registerLiveBackend((req) => buildLiveContext(req)); -} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts deleted file mode 100644 index 70e0f88b..00000000 --- a/contracts/test/integration/_harness/wallet.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; -import { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js - * dependency. `OwnWalletProvider` implements both `MidnightProvider` and - * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. - * Tests that need per-signer isolation pass their own BIP39 phrase. - */ -export async function buildWallet( - env: LocalNetworkConfig, - mnemonic: string = LOCAL_WALLET_MNEMONIC, -): Promise { - return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); -} diff --git a/contracts/test/integration/_mocks/ComposedTokens.compact b/contracts/test/integration/_mocks/ComposedTokens.compact deleted file mode 100644 index 3b4a7870..00000000 --- a/contracts/test/integration/_mocks/ComposedTokens.compact +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT - -// WARNING: FOR TESTING PURPOSES ONLY. -// This contract exposes internal circuits and bypasses safety checks that the -// corresponding production contract relies on. DO NOT deploy or use this -// contract in any production application. - -// Top-level contract composing two PRODUCTION modules that live in the SAME -// directory (FungibleToken + NonFungibleToken). Each now owns its `_isInitialized` -// flag, so initializing one must NOT initialize the other. The -// `initStateIsolation` spec asserts that isolation (the fix for #556). -// -// `initFT` / `initNFT` select which module is initialized at construction, so a -// test can initialize one and assert the other is still uninitialized. (Both -// initializers write `sealed` ledger fields, so they must run in the constructor.) -pragma language_version >= 0.23.0; - -import CompactStandardLibrary; -import "../../../src/token/FungibleToken" prefix FungibleToken_; -import "../../../src/token/NonFungibleToken" prefix NonFungibleToken_; - -export { ContractAddress, Either, Maybe }; - -constructor( - ftName_: Opaque<"string">, - ftSymbol_: Opaque<"string">, - ftDecimals_: Uint<8>, - nftName_: Opaque<"string">, - nftSymbol_: Opaque<"string">, - initFT: Boolean, - initNFT: Boolean -) { - if (disclose(initFT)) { - FungibleToken_initialize(ftName_, ftSymbol_, ftDecimals_); - } - if (disclose(initNFT)) { - NonFungibleToken_initialize(nftName_, nftSymbol_); - } -} - -// Init-guarded getters: each calls its module's `assertInitialized()`. -export circuit ftName(): Opaque<"string"> { - return FungibleToken_name(); -} - -export circuit nftName(): Opaque<"string"> { - return NonFungibleToken_name(); -} diff --git a/contracts/test/integration/_mocks/SharedInitCollision.compact b/contracts/test/integration/_mocks/SharedInitCollision.compact deleted file mode 100644 index cd5ca6c2..00000000 --- a/contracts/test/integration/_mocks/SharedInitCollision.compact +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -// WARNING: FOR TESTING PURPOSES ONLY. Intentionally exhibits the compiler#270 -// bug. DO NOT use this pattern in production. - -// Top-level contract composing TWO modules (ModuleA, ModuleB) that both import -// the shared, stateful `Initializable` from the SAME directory. The compiler -// deduplicates the transitive `_isInitialized` ledger state into a single slot, -// so initializing one module also marks the other initialized. The -// `initStateIsolation` spec asserts this collision (the bug), justifying why the -// production modules now track their init flag per-module instead. -pragma language_version >= 0.23.0; - -import CompactStandardLibrary; -import "./sharedInit/ModuleA" prefix A_; -import "./sharedInit/ModuleB" prefix B_; - -export circuit initA(): [] { A_initA(); } -export circuit initB(): [] { B_initB(); } -export circuit checkA(): [] { A_checkA(); } -export circuit checkB(): [] { B_checkB(); } diff --git a/contracts/test/integration/_mocks/sharedInit/ModuleA.compact b/contracts/test/integration/_mocks/sharedInit/ModuleA.compact deleted file mode 100644 index c1035ad9..00000000 --- a/contracts/test/integration/_mocks/sharedInit/ModuleA.compact +++ /dev/null @@ -1,17 +0,0 @@ -// TEST-ONLY. Demonstrates the compiler#270 collision: a module that depends on -// the shared, stateful `Initializable` module. Paired with ModuleB (same -// directory) so a composing contract triggers the shared-ledger-slot bug. -pragma language_version >= 0.23.0; - -module ModuleA { - import CompactStandardLibrary; - import "../../../../src/security/Initializable" prefix Initializable_; - - export circuit initA(): [] { - Initializable_initialize(); - } - - export circuit checkA(): [] { - Initializable_assertInitialized(); - } -} diff --git a/contracts/test/integration/_mocks/sharedInit/ModuleB.compact b/contracts/test/integration/_mocks/sharedInit/ModuleB.compact deleted file mode 100644 index c96cbd3b..00000000 --- a/contracts/test/integration/_mocks/sharedInit/ModuleB.compact +++ /dev/null @@ -1,17 +0,0 @@ -// TEST-ONLY. Demonstrates the compiler#270 collision: a module that depends on -// the shared, stateful `Initializable` module. Paired with ModuleA (same -// directory) so a composing contract triggers the shared-ledger-slot bug. -pragma language_version >= 0.23.0; - -module ModuleB { - import CompactStandardLibrary; - import "../../../../src/security/Initializable" prefix Initializable_; - - export circuit initB(): [] { - Initializable_initialize(); - } - - export circuit checkB(): [] { - Initializable_assertInitialized(); - } -} diff --git a/contracts/test/integration/fixtures/composedTokens.ts b/contracts/test/integration/fixtures/composedTokens.ts deleted file mode 100644 index 5d67cb2f..00000000 --- a/contracts/test/integration/fixtures/composedTokens.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createSimulator } from '@openzeppelin/compact-simulator'; -import { - ledger, - Contract as ComposedTokens, -} from '../../../artifacts/ComposedTokens/contract/index.js'; - -type EmptyPrivateState = Record; - -type ComposedTokensArgs = readonly [ - ftName: string, - ftSymbol: string, - ftDecimals: bigint, - nftName: string, - nftSymbol: string, - initFT: boolean, - initNFT: boolean, -]; - -const ComposedTokensSimulatorBase = createSimulator< - EmptyPrivateState, - ReturnType, - // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses - {}, - ComposedTokens, - ComposedTokensArgs ->({ - contractFactory: (witnesses) => - new ComposedTokens(witnesses), - defaultPrivateState: () => ({}), - contractArgs: (ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT) => [ - ftName, - ftSymbol, - ftDecimals, - nftName, - nftSymbol, - initFT, - initNFT, - ], - ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ({}), -}); - -/** - * Drives the ComposedTokens contract: production FungibleToken + NonFungibleToken - * (same directory) composed in one contract. `initFT` / `initNFT` choose which - * module is initialized at construction, so a test can prove the two init flags - * are independent (the #556 fix). - */ -export class ComposedTokensSimulator extends ComposedTokensSimulatorBase { - constructor(initFT: boolean, initNFT: boolean) { - super(['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], {}); - } - - public ftName(): string { - return this.circuits.impure.ftName(); - } - - public nftName(): string { - return this.circuits.impure.nftName(); - } -} diff --git a/contracts/test/integration/fixtures/sharedInitCollision.ts b/contracts/test/integration/fixtures/sharedInitCollision.ts deleted file mode 100644 index 1b566c01..00000000 --- a/contracts/test/integration/fixtures/sharedInitCollision.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createSimulator } from '@openzeppelin/compact-simulator'; -import { - ledger, - Contract as SharedInitCollision, -} from '../../../artifacts/SharedInitCollision/contract/index.js'; - -type EmptyPrivateState = Record; - -const SharedInitCollisionSimulatorBase = createSimulator< - EmptyPrivateState, - ReturnType, - // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses - {}, - SharedInitCollision, - readonly [] ->({ - contractFactory: (witnesses) => - new SharedInitCollision(witnesses), - defaultPrivateState: () => ({}), - contractArgs: () => [], - ledgerExtractor: (state) => ledger(state), - witnessesFactory: () => ({}), -}); - -/** - * Drives the SharedInitCollision contract: two same-directory modules that both - * import the shared, stateful `Initializable`. Used to assert the compiler#270 - * collision. - */ -export class SharedInitCollisionSimulator extends SharedInitCollisionSimulatorBase { - constructor() { - super([], {}); - } - - public initA(): void { - this.circuits.impure.initA(); - } - - public initB(): void { - this.circuits.impure.initB(); - } - - public checkA(): void { - this.circuits.impure.checkA(); - } - - public checkB(): void { - this.circuits.impure.checkB(); - } -} diff --git a/contracts/test/integration/specs/initStateIsolation.spec.ts b/contracts/test/integration/specs/initStateIsolation.spec.ts deleted file mode 100644 index ce230277..00000000 --- a/contracts/test/integration/specs/initStateIsolation.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ComposedTokensSimulator } from '../fixtures/composedTokens.js'; -import { SharedInitCollisionSimulator } from '../fixtures/sharedInitCollision.js'; - -/** - * Integration spec for issue #556 (compiler#270). - * - * Two composed contracts, both built from two modules living in the SAME - * directory: - * - * - `SharedInitCollision` — both modules import the shared, stateful - * `Initializable`. This reproduces the compiler bug: the transitive - * `_isInitialized` ledger state is deduplicated into ONE slot, so the two - * modules' initialization flags are entangled. - * - * - `ComposedTokens` — the production FungibleToken + NonFungibleToken, each of - * which now owns its `_isInitialized` flag. This proves the fix: the two - * initializations are independent. - * - * The first block documents the bug (and would have to be deleted/inverted if - * the compiler ever isolates transitive ledger state); the second block guards - * the fix against regression. - */ - -describe('Initializable state isolation (#556)', () => { - describe('the bug — shared Initializable across same-directory modules', () => { - it('should treat module B as initialized after only module A is initialized', () => { - const c = new SharedInitCollisionSimulator(); - - // Only module A is initialized. - c.initA(); - - // BUG: module B was never initialized, yet its init-guard passes, - // because both modules share a single `_isInitialized` ledger slot. - expect(() => c.checkB()).not.toThrow(); - }); - - it('should not allow module B to initialize once module A has set the shared slot', () => { - const c = new SharedInitCollisionSimulator(); - c.initA(); - - // BUG: B can never be initialized — the shared slot is already set. - expect(() => c.initB()).toThrow('Initializable: contract already initialized'); - }); - }); - - describe('the fix — per-module flags keep production modules isolated', () => { - it('should not initialize NonFungibleToken when only FungibleToken is initialized', () => { - const c = new ComposedTokensSimulator(true, false); - - // FT is usable. - expect(() => c.ftName()).not.toThrow(); - // NFT is independently still uninitialized. - expect(() => c.nftName()).toThrow( - 'NonFungibleToken: contract not initialized', - ); - }); - - it('should not initialize FungibleToken when only NonFungibleToken is initialized', () => { - const c = new ComposedTokensSimulator(false, true); - - expect(() => c.nftName()).not.toThrow(); - expect(() => c.ftName()).toThrow('FungibleToken: contract not initialized'); - }); - - it('should initialize each module independently', () => { - const c = new ComposedTokensSimulator(true, true); - - expect(() => c.ftName()).not.toThrow(); - expect(() => c.nftName()).not.toThrow(); - }); - }); -}); diff --git a/contracts/vitest.integration.config.ts b/contracts/vitest.integration.config.ts deleted file mode 100644 index 24a7d3d8..00000000 --- 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 52377aad..00000000 --- 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 deleted file mode 100644 index fedb1fef..00000000 --- a/local-env.yml +++ /dev/null @@ -1,61 +0,0 @@ -# WARNING: Insecure default credentials below. For local development only — do not use in production. -services: - proof-server: - image: 'midnightntwrk/proof-server:latest' - command: ['midnight-proof-server -v'] - ports: - - '6300:6300' - environment: - RUST_BACKTRACE: 'full' - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] - interval: 10s - timeout: 5s - retries: 20 - start_period: 10s - - indexer: - image: 'midnightntwrk/indexer-standalone:latest' - ports: - - '8088:8088' - environment: - RUST_LOG: 'indexer=debug,chain_indexer=debug,indexer_api=debug,wallet_indexer=debug,indexer_common=debug,fastrace_opentelemetry=off,info' - APP__INFRA__NODE__URL: 'ws://node:9944' - APP__APPLICATION__NETWORK_ID: 'undeployed' - APP__INFRA__STORAGE__PASSWORD: 'indexer' - APP__INFRA__PUB_SUB__PASSWORD: 'indexer' - APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' - APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' - healthcheck: - test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] - interval: 10s - timeout: 5s - retries: 20 - start_period: 10s - depends_on: - node: - condition: service_healthy - logging: - driver: local - options: - max-size: '10m' - max-file: '3' - - node: - image: 'midnightntwrk/midnight-node:0.22.2' - ports: - - '9944:9944' - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] - interval: 2s - timeout: 5s - retries: 20 - start_period: 5s - environment: - CFG_PRESET: 'dev' - SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' - logging: - driver: local - options: - max-size: '10m' - max-file: '3' From e5627a7d53ddd1eb7f0b028ccced01a7a996ff7e Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 11:08:37 +0200 Subject: [PATCH 05/10] test: keep pre-existing integration suite Restore the test/integration specs, fixtures, _mocks, vitest.integration.config.ts, and the compact:integration / test:integration scripts that the previous commit removed. They predate this PR (they live on main) and are not part of this migration, so the PR leaves them untouched. Only the live-backend wiring stays removed and deferred to a follow-up PR: test/integration/_harness, vitest.live.config.ts, the test:live script, Makefile, local-env.yml, and the live-tests workflow. --- contracts/package.json | 2 + contracts/test/integration/README.md | 8 ++ .../integration/_mocks/ComposedTokens.compact | 48 ++++++++++++ .../_mocks/SharedInitCollision.compact | 21 ++++++ .../_mocks/sharedInit/ModuleA.compact | 17 +++++ .../_mocks/sharedInit/ModuleB.compact | 17 +++++ .../integration/fixtures/composedTokens.ts | 61 ++++++++++++++++ .../fixtures/sharedInitCollision.ts | 50 +++++++++++++ .../specs/initStateIsolation.spec.ts | 73 +++++++++++++++++++ contracts/vitest.integration.config.ts | 13 ++++ 10 files changed, 310 insertions(+) create mode 100644 contracts/test/integration/README.md create mode 100644 contracts/test/integration/_mocks/ComposedTokens.compact create mode 100644 contracts/test/integration/_mocks/SharedInitCollision.compact create mode 100644 contracts/test/integration/_mocks/sharedInit/ModuleA.compact create mode 100644 contracts/test/integration/_mocks/sharedInit/ModuleB.compact create mode 100644 contracts/test/integration/fixtures/composedTokens.ts create mode 100644 contracts/test/integration/fixtures/sharedInitCollision.ts create mode 100644 contracts/test/integration/specs/initStateIsolation.spec.ts create mode 100644 contracts/vitest.integration.config.ts diff --git a/contracts/package.json b/contracts/package.json index 50ded4f7..193aa3ab 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,6 +34,8 @@ "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", + "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", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/contracts/test/integration/README.md b/contracts/test/integration/README.md new file mode 100644 index 00000000..0979f99f --- /dev/null +++ b/contracts/test/integration/README.md @@ -0,0 +1,8 @@ +# Integration tests + +Compose multiple production modules into one top-level contract and drive it +through the simulator, covering interactions the per-module unit tests can't. + +```sh +yarn test:integration +``` diff --git a/contracts/test/integration/_mocks/ComposedTokens.compact b/contracts/test/integration/_mocks/ComposedTokens.compact new file mode 100644 index 00000000..3b4a7870 --- /dev/null +++ b/contracts/test/integration/_mocks/ComposedTokens.compact @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. +// This contract exposes internal circuits and bypasses safety checks that the +// corresponding production contract relies on. DO NOT deploy or use this +// contract in any production application. + +// Top-level contract composing two PRODUCTION modules that live in the SAME +// directory (FungibleToken + NonFungibleToken). Each now owns its `_isInitialized` +// flag, so initializing one must NOT initialize the other. The +// `initStateIsolation` spec asserts that isolation (the fix for #556). +// +// `initFT` / `initNFT` select which module is initialized at construction, so a +// test can initialize one and assert the other is still uninitialized. (Both +// initializers write `sealed` ledger fields, so they must run in the constructor.) +pragma language_version >= 0.23.0; + +import CompactStandardLibrary; +import "../../../src/token/FungibleToken" prefix FungibleToken_; +import "../../../src/token/NonFungibleToken" prefix NonFungibleToken_; + +export { ContractAddress, Either, Maybe }; + +constructor( + ftName_: Opaque<"string">, + ftSymbol_: Opaque<"string">, + ftDecimals_: Uint<8>, + nftName_: Opaque<"string">, + nftSymbol_: Opaque<"string">, + initFT: Boolean, + initNFT: Boolean +) { + if (disclose(initFT)) { + FungibleToken_initialize(ftName_, ftSymbol_, ftDecimals_); + } + if (disclose(initNFT)) { + NonFungibleToken_initialize(nftName_, nftSymbol_); + } +} + +// Init-guarded getters: each calls its module's `assertInitialized()`. +export circuit ftName(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit nftName(): Opaque<"string"> { + return NonFungibleToken_name(); +} diff --git a/contracts/test/integration/_mocks/SharedInitCollision.compact b/contracts/test/integration/_mocks/SharedInitCollision.compact new file mode 100644 index 00000000..cd5ca6c2 --- /dev/null +++ b/contracts/test/integration/_mocks/SharedInitCollision.compact @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +// WARNING: FOR TESTING PURPOSES ONLY. Intentionally exhibits the compiler#270 +// bug. DO NOT use this pattern in production. + +// Top-level contract composing TWO modules (ModuleA, ModuleB) that both import +// the shared, stateful `Initializable` from the SAME directory. The compiler +// deduplicates the transitive `_isInitialized` ledger state into a single slot, +// so initializing one module also marks the other initialized. The +// `initStateIsolation` spec asserts this collision (the bug), justifying why the +// production modules now track their init flag per-module instead. +pragma language_version >= 0.23.0; + +import CompactStandardLibrary; +import "./sharedInit/ModuleA" prefix A_; +import "./sharedInit/ModuleB" prefix B_; + +export circuit initA(): [] { A_initA(); } +export circuit initB(): [] { B_initB(); } +export circuit checkA(): [] { A_checkA(); } +export circuit checkB(): [] { B_checkB(); } diff --git a/contracts/test/integration/_mocks/sharedInit/ModuleA.compact b/contracts/test/integration/_mocks/sharedInit/ModuleA.compact new file mode 100644 index 00000000..c1035ad9 --- /dev/null +++ b/contracts/test/integration/_mocks/sharedInit/ModuleA.compact @@ -0,0 +1,17 @@ +// TEST-ONLY. Demonstrates the compiler#270 collision: a module that depends on +// the shared, stateful `Initializable` module. Paired with ModuleB (same +// directory) so a composing contract triggers the shared-ledger-slot bug. +pragma language_version >= 0.23.0; + +module ModuleA { + import CompactStandardLibrary; + import "../../../../src/security/Initializable" prefix Initializable_; + + export circuit initA(): [] { + Initializable_initialize(); + } + + export circuit checkA(): [] { + Initializable_assertInitialized(); + } +} diff --git a/contracts/test/integration/_mocks/sharedInit/ModuleB.compact b/contracts/test/integration/_mocks/sharedInit/ModuleB.compact new file mode 100644 index 00000000..c96cbd3b --- /dev/null +++ b/contracts/test/integration/_mocks/sharedInit/ModuleB.compact @@ -0,0 +1,17 @@ +// TEST-ONLY. Demonstrates the compiler#270 collision: a module that depends on +// the shared, stateful `Initializable` module. Paired with ModuleA (same +// directory) so a composing contract triggers the shared-ledger-slot bug. +pragma language_version >= 0.23.0; + +module ModuleB { + import CompactStandardLibrary; + import "../../../../src/security/Initializable" prefix Initializable_; + + export circuit initB(): [] { + Initializable_initialize(); + } + + export circuit checkB(): [] { + Initializable_assertInitialized(); + } +} diff --git a/contracts/test/integration/fixtures/composedTokens.ts b/contracts/test/integration/fixtures/composedTokens.ts new file mode 100644 index 00000000..5d67cb2f --- /dev/null +++ b/contracts/test/integration/fixtures/composedTokens.ts @@ -0,0 +1,61 @@ +import { createSimulator } from '@openzeppelin/compact-simulator'; +import { + ledger, + Contract as ComposedTokens, +} from '../../../artifacts/ComposedTokens/contract/index.js'; + +type EmptyPrivateState = Record; + +type ComposedTokensArgs = readonly [ + ftName: string, + ftSymbol: string, + ftDecimals: bigint, + nftName: string, + nftSymbol: string, + initFT: boolean, + initNFT: boolean, +]; + +const ComposedTokensSimulatorBase = createSimulator< + EmptyPrivateState, + ReturnType, + // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses + {}, + ComposedTokens, + ComposedTokensArgs +>({ + contractFactory: (witnesses) => + new ComposedTokens(witnesses), + defaultPrivateState: () => ({}), + contractArgs: (ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT) => [ + ftName, + ftSymbol, + ftDecimals, + nftName, + nftSymbol, + initFT, + initNFT, + ], + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => ({}), +}); + +/** + * Drives the ComposedTokens contract: production FungibleToken + NonFungibleToken + * (same directory) composed in one contract. `initFT` / `initNFT` choose which + * module is initialized at construction, so a test can prove the two init flags + * are independent (the #556 fix). + */ +export class ComposedTokensSimulator extends ComposedTokensSimulatorBase { + constructor(initFT: boolean, initNFT: boolean) { + super(['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], {}); + } + + public ftName(): string { + return this.circuits.impure.ftName(); + } + + public nftName(): string { + return this.circuits.impure.nftName(); + } +} diff --git a/contracts/test/integration/fixtures/sharedInitCollision.ts b/contracts/test/integration/fixtures/sharedInitCollision.ts new file mode 100644 index 00000000..1b566c01 --- /dev/null +++ b/contracts/test/integration/fixtures/sharedInitCollision.ts @@ -0,0 +1,50 @@ +import { createSimulator } from '@openzeppelin/compact-simulator'; +import { + ledger, + Contract as SharedInitCollision, +} from '../../../artifacts/SharedInitCollision/contract/index.js'; + +type EmptyPrivateState = Record; + +const SharedInitCollisionSimulatorBase = createSimulator< + EmptyPrivateState, + ReturnType, + // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses + {}, + SharedInitCollision, + readonly [] +>({ + contractFactory: (witnesses) => + new SharedInitCollision(witnesses), + defaultPrivateState: () => ({}), + contractArgs: () => [], + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => ({}), +}); + +/** + * Drives the SharedInitCollision contract: two same-directory modules that both + * import the shared, stateful `Initializable`. Used to assert the compiler#270 + * collision. + */ +export class SharedInitCollisionSimulator extends SharedInitCollisionSimulatorBase { + constructor() { + super([], {}); + } + + public initA(): void { + this.circuits.impure.initA(); + } + + public initB(): void { + this.circuits.impure.initB(); + } + + public checkA(): void { + this.circuits.impure.checkA(); + } + + public checkB(): void { + this.circuits.impure.checkB(); + } +} diff --git a/contracts/test/integration/specs/initStateIsolation.spec.ts b/contracts/test/integration/specs/initStateIsolation.spec.ts new file mode 100644 index 00000000..ce230277 --- /dev/null +++ b/contracts/test/integration/specs/initStateIsolation.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { ComposedTokensSimulator } from '../fixtures/composedTokens.js'; +import { SharedInitCollisionSimulator } from '../fixtures/sharedInitCollision.js'; + +/** + * Integration spec for issue #556 (compiler#270). + * + * Two composed contracts, both built from two modules living in the SAME + * directory: + * + * - `SharedInitCollision` — both modules import the shared, stateful + * `Initializable`. This reproduces the compiler bug: the transitive + * `_isInitialized` ledger state is deduplicated into ONE slot, so the two + * modules' initialization flags are entangled. + * + * - `ComposedTokens` — the production FungibleToken + NonFungibleToken, each of + * which now owns its `_isInitialized` flag. This proves the fix: the two + * initializations are independent. + * + * The first block documents the bug (and would have to be deleted/inverted if + * the compiler ever isolates transitive ledger state); the second block guards + * the fix against regression. + */ + +describe('Initializable state isolation (#556)', () => { + describe('the bug — shared Initializable across same-directory modules', () => { + it('should treat module B as initialized after only module A is initialized', () => { + const c = new SharedInitCollisionSimulator(); + + // Only module A is initialized. + c.initA(); + + // BUG: module B was never initialized, yet its init-guard passes, + // because both modules share a single `_isInitialized` ledger slot. + expect(() => c.checkB()).not.toThrow(); + }); + + it('should not allow module B to initialize once module A has set the shared slot', () => { + const c = new SharedInitCollisionSimulator(); + c.initA(); + + // BUG: B can never be initialized — the shared slot is already set. + expect(() => c.initB()).toThrow('Initializable: contract already initialized'); + }); + }); + + describe('the fix — per-module flags keep production modules isolated', () => { + it('should not initialize NonFungibleToken when only FungibleToken is initialized', () => { + const c = new ComposedTokensSimulator(true, false); + + // FT is usable. + expect(() => c.ftName()).not.toThrow(); + // NFT is independently still uninitialized. + expect(() => c.nftName()).toThrow( + 'NonFungibleToken: contract not initialized', + ); + }); + + it('should not initialize FungibleToken when only NonFungibleToken is initialized', () => { + const c = new ComposedTokensSimulator(false, true); + + expect(() => c.nftName()).not.toThrow(); + expect(() => c.ftName()).toThrow('FungibleToken: contract not initialized'); + }); + + it('should initialize each module independently', () => { + const c = new ComposedTokensSimulator(true, true); + + expect(() => c.ftName()).not.toThrow(); + expect(() => c.nftName()).not.toThrow(); + }); + }); +}); diff --git a/contracts/vitest.integration.config.ts b/contracts/vitest.integration.config.ts new file mode 100644 index 00000000..24a7d3d8 --- /dev/null +++ b/contracts/vitest.integration.config.ts @@ -0,0 +1,13 @@ +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', + }, +}); From 0aefb2806b32d25cdec9b1e20699228af6d77127 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 11:20:11 +0200 Subject: [PATCH 06/10] test: restore live test infra + integration async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring back the local live-backend testing setup removed earlier and extend it to the integration suite. The live CI workflow stays out (it returns in a follow-up PR as a sharded matrix). * Restore the local stack (Makefile + local-env.yml), vitest.live.config.ts, the test/integration/_harness live wiring, and the test:live script — so `make env-up && yarn test:live` runs the unit specs on a real node. * Migrate the integration suite (composedTokens + sharedInitCollision fixtures, initStateIsolation spec) to the async backend-aware simulator, matching the unit migration: static create, awaited circuits, rejects/resolves assertions. Add artifactName so the live harness can deploy each composed contract. * Add integration live wiring: vitest.integration.live.config.ts, plus compact:integration:live (real ZK keys, no SKIP_ZK) and test:integration:live. The integration suite shares the unit live harness unchanged — the harness deploys any contract by artifactName, so no live-backend code is added. Validated dry: integration spec 5/5 green against the async API. --- Makefile | 31 ++ contracts/package.json | 3 + contracts/test/integration/_harness/deploy.ts | 84 +++++ .../test/integration/_harness/live.setup.ts | 7 + .../test/integration/_harness/network.ts | 63 ++++ .../test/integration/_harness/ownWallet.ts | 289 ++++++++++++++++++ .../test/integration/_harness/providers.ts | 56 ++++ .../_harness/registerSimulatorLive.ts | 154 ++++++++++ contracts/test/integration/_harness/wallet.ts | 17 ++ .../integration/fixtures/composedTokens.ts | 27 +- .../fixtures/sharedInitCollision.ts | 30 +- .../specs/initStateIsolation.spec.ts | 44 +-- contracts/vitest.integration.live.config.ts | 25 ++ contracts/vitest.live.config.ts | 24 ++ local-env.yml | 61 ++++ 15 files changed, 877 insertions(+), 38 deletions(-) create mode 100644 Makefile create mode 100644 contracts/test/integration/_harness/deploy.ts create mode 100644 contracts/test/integration/_harness/live.setup.ts create mode 100644 contracts/test/integration/_harness/network.ts create mode 100644 contracts/test/integration/_harness/ownWallet.ts create mode 100644 contracts/test/integration/_harness/providers.ts create mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts create mode 100644 contracts/test/integration/_harness/wallet.ts create mode 100644 contracts/vitest.integration.live.config.ts create mode 100644 contracts/vitest.live.config.ts create mode 100644 local-env.yml diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e5a601bf --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +COMPOSE_FILE := local-env.yml +LOGS_DIR := logs +SERVICES := proof-server indexer node + +.PHONY: env-up env-down env-logs env-logs-clean env-status + +## Start local environment and stream logs to logs/ +env-up: env-down + docker compose -f $(COMPOSE_FILE) up -d + @mkdir -p $(LOGS_DIR) + @for svc in $(SERVICES); do \ + docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ + done + @echo "Logs streaming to $(LOGS_DIR)/" + +## Stop local environment +env-down: + @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true + docker compose -f $(COMPOSE_FILE) down + +## Tail all logs +env-logs: + tail -f $(LOGS_DIR)/*.log + +## Clear log files +env-logs-clean: + rm -rf $(LOGS_DIR)/*.log + +## Show container status +env-status: + docker compose -f $(COMPOSE_FILE) ps diff --git a/contracts/package.json b/contracts/package.json index 193aa3ab..b9fa6c29 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,8 +34,11 @@ "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", + "compact:integration:live": "compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens", + "test:integration:live": "yarn run compact:integration:live && MIDNIGHT_BACKEND=live vitest run --config vitest.integration.live.config.ts", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts new file mode 100644 index 00000000..f9a1d283 --- /dev/null +++ b/contracts/test/integration/_harness/deploy.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + CompiledContract, + Contract as ContractNs, +} from '@midnight-ntwrk/compact-js'; +import { + type DeployContractOptionsWithPrivateState, + type DeployedContract, + deployContract, +} from '@midnight-ntwrk/midnight-js-contracts'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Absolute path to `contracts/artifacts//`. + * Used by `NodeZkConfigProvider`, which expects the directory containing + * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). + */ +export function moduleRootPath(moduleName: string): string { + // _harness/ is at contracts/test/integration/_harness/ + // module root at contracts/artifacts// + return path.resolve( + currentDir, + '..', + '..', + '..', + 'artifacts', + moduleName, + ); +} + +/** + * Absolute path to `contracts/artifacts//contract/` — where the + * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) + * live. Used by `CompiledContract.withCompiledFileAssets`. + */ +export function contractAssetsPath(moduleName: string): string { + return path.join(moduleRootPath(moduleName), 'contract'); +} + +/** + * Generic deploy wrapper. + * + * Each per-module fixture builds its own `CompiledContract` (because + * `witnesses` are module-specific) and passes it here along with providers, + * a private-state id, the initial private-state value, and the contract's + * constructor arguments — all properly typed via `Contract.*` helpers from + * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. + */ +export async function deployModule( + providers: MidnightProviders< + ContractNs.ProvableCircuitId, + string, + ContractNs.PrivateState + >, + // The third generic of `CompiledContract` (the witnesses map) defaults to + // `never` for empty-witness contracts; accept `any` so both shapes pass. + compiledContract: CompiledContract.CompiledContract< + C, + ContractNs.PrivateState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + >, + privateStateId: string, + initialPrivateState: ContractNs.PrivateState, + args: ContractNs.InitializeParameters, +): Promise> { + // The deployContract options shape is conditional on whether + // `Contract.InitializeParameters` is empty — TypeScript can't reduce + // that conditional under an unbounded `C extends Contract.Any`, so we + // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. + // Two-step cast (through `unknown`) because TS rejects the direct cast + // as "neither type sufficiently overlaps" — same conditional-resolution + // issue. Scoped to this single helper. + const options = { + compiledContract, + privateStateId, + initialPrivateState, + args, + } as unknown as DeployContractOptionsWithPrivateState; + return deployContract(providers, options); +} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts new file mode 100644 index 00000000..e5b05fec --- /dev/null +++ b/contracts/test/integration/_harness/live.setup.ts @@ -0,0 +1,7 @@ +import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; + +// Runs once per worker before the unit specs when `test:live` is used. Registers +// the live backend so `await Sim.create()` attaches to a freshly-deployed +// contract on the local stack (brought up by `make env-up`). On the dry path +// (default `test`) this file is not loaded, so nothing changes there. +registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts new file mode 100644 index 00000000..dacf5ebc --- /dev/null +++ b/contracts/test/integration/_harness/network.ts @@ -0,0 +1,63 @@ +import { + type NetworkId, + setNetworkId, +} from '@midnight-ntwrk/midnight-js-network-id'; + +/** + * Endpoint configuration for the local stack. Replaces testkit-js' + * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally + * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. + */ +export interface LocalNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly networkId: string; + readonly indexer: string; + readonly indexerWS: string; + readonly node: string; + readonly nodeWS: string; + readonly proofServer: string; + readonly faucet: string | undefined; +} + +/** + * Prefunded wallet mnemonic for the local `undeployed` network — the canonical + * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` + * recognises as the genesis-funded account. Inlined so the harness no longer + * depends on testkit-js' `TEST_MNEMONIC`. + */ +export const LOCAL_WALLET_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; + +/** + * Default endpoints for the local stack brought up by `make env-up`. + * Each is overridable via a MIDNIGHT_* env var so CI / other hosts + * can point the same harness at a relocated stack. + */ +export function networkConfig(): LocalNetworkConfig { + return { + walletNetworkId: 'undeployed' as NetworkId, + networkId: 'undeployed', + indexer: + process.env.MIDNIGHT_INDEXER_URL ?? + 'http://127.0.0.1:8088/api/v4/graphql', + indexerWS: + process.env.MIDNIGHT_INDEXER_WS_URL ?? + 'ws://127.0.0.1:8088/api/v4/graphql/ws', + node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', + nodeWS: 'ws://127.0.0.1:9944', + proofServer: + process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', + faucet: undefined, + }; +} + +/** + * Set the process-wide network id. Must be called once before any provider + * or wallet is constructed. Idempotent. + */ +let networkIdSet = false; +export function setupNetwork(): void { + if (networkIdSet) return; + setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); + networkIdSet = true; +} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts new file mode 100644 index 00000000..fd2a78df --- /dev/null +++ b/contracts/test/integration/_harness/ownWallet.ts @@ -0,0 +1,289 @@ +/** + * Own test wallet provider — a testkit-js-free reconstruction of the wallet + * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * WHY THIS EXISTS + * --------------- + * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s + * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy + * dependency (testcontainers, docker orchestration, a fixed env model) of which + * we use almost nothing — we run our own local stack via `make env-up`. All + * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter + * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. + * + * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, + * so the harness no longer imports testkit. It is a behavioural drop-in for the + * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. + * + * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: + * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed + * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk + * `balanceTx` / `submitTx` delegate to the facade identically to testkit. + * + * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, + * `shielded`) so a future coin-injecting shielded wallet can be slotted in via + * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks + * the spend-path (burn / round-trip) integration specs. See + * `NativeShieldedToken-tests.md` "Own wallet tool". + */ +import { + DustSecretKey, + LedgerParameters, + ZswapSecretKeys, +} from '@midnight-ntwrk/ledger-v8'; +import type { + FinalizedTransaction, + TransactionId, +} from '@midnight-ntwrk/midnight-js-protocol/ledger'; +import type { + MidnightProvider, + WalletProvider, +} from '@midnight-ntwrk/midnight-js-types'; +import { + createKeystore, + DustWallet, + HDWallet, + InMemoryTransactionHistoryStorage, + mergeWalletEntries, + PublicKey, + type Role, + Roles, + ShieldedWallet, + UnshieldedWallet, + WalletEntrySchema, + WalletFacade, +} from '@midnight-ntwrk/wallet-sdk'; +import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import { mnemonicToSeedSync } from '@scure/bip39'; +import pino, { type Logger } from 'pino'; + +/** + * Minimal endpoint config our wallet needs — a structural subset of the fields + * `networkConfig()` already returns, with no testkit type dependency. + */ +export interface OwnNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly indexer: string; + readonly indexerWS: string; + readonly nodeWS: string; + readonly proofServer: string; +} + +/** + * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at + * preset-dev needs headroom to cover fees on undeployed; mirrors the value the + * old testkit-based `buildWallet` passed via `DustWalletOptions`. + */ +const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; + +let sharedLogger: Logger | undefined; +function ownLogger(): Logger { + if (!sharedLogger) { + sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); + } + return sharedLogger; +} + +/** The three role sub-seeds derived from a master seed, plus the master. */ +interface DerivedSeeds { + readonly masterSeedHex: string; + readonly shielded: Uint8Array; + readonly unshielded: Uint8Array; + readonly dust: Uint8Array; +} + +/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ +function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { + if (!masterSeedHex || masterSeedHex.length === 0) { + throw new Error('Own wallet: master seed cannot be empty'); + } + const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); + if (result.type !== 'seedOk') { + throw new Error('Own wallet: invalid seed, failed to create HD wallet'); + } + const derived = result.hdWallet + .selectAccount(0) + .selectRole(role) + .deriveKeyAt(0); + if (derived.type !== 'keyDerived') { + throw new Error(`Own wallet: key derivation failed for role ${role}`); + } + return derived.key; +} + +function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { + return { + masterSeedHex, + shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), + unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), + dust: deriveKeyForRole(masterSeedHex, Roles.Dust), + }; +} + +function seedsFromMnemonic(mnemonic: string): DerivedSeeds { + if (!mnemonic || mnemonic.trim().length === 0) { + throw new Error('Own wallet: mnemonic cannot be empty'); + } + return seedsFromMasterHex( + Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), + ); +} + +/** + * Map our endpoint config to the wallet-sdk facade configuration object. + * Shape lifted from testkit's `mapEnvironmentToConfiguration`. + */ +function facadeConfiguration(env: OwnNetworkConfig) { + return { + indexerClientConnection: { + indexerHttpUrl: env.indexer, + indexerWsUrl: env.indexerWS, + }, + provingServerUrl: new URL(env.proofServer), + networkId: env.walletNetworkId, + relayURL: new URL(env.nodeWS), + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + costParameters: { feeBlocksMargin: 5 }, + }; +} + +/** + * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, + * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the + * same delegations testkit's `MidnightWalletProvider` performed. + */ +export class OwnWalletProvider implements WalletProvider, MidnightProvider { + private constructor( + readonly env: OwnNetworkConfig, + readonly facade: WalletFacade, + readonly zswapSecretKeys: ZswapSecretKeys, + readonly dustSecretKey: DustSecretKey, + private readonly unshieldedKeystore: ReturnType, + private readonly logger: Logger, + ) {} + + getCoinPublicKey() { + return this.zswapSecretKeys.coinPublicKey; + } + + getEncryptionPublicKey() { + return this.zswapSecretKeys.encryptionPublicKey; + } + + async balanceTx( + tx: Parameters[0], + ttl: Date = ttlOneHour(), + ): Promise { + const recipe = await this.facade.balanceUnboundTransaction( + tx, + { + shieldedSecretKeys: this.zswapSecretKeys, + dustSecretKey: this.dustSecretKey, + }, + { ttl }, + ); + const signed = await this.facade.signRecipe(recipe, (payload) => + this.unshieldedKeystore.signData(payload), + ); + return this.facade.finalizeRecipe(signed); + } + + submitTx(tx: FinalizedTransaction): Promise { + return this.facade.submitTransaction(tx); + } + + async stop(): Promise { + await this.facade.stop(); + } + + /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ + static async build( + env: OwnNetworkConfig, + keyMaterial: { mnemonic: string } | { seedHex: string }, + options: { waitForFunds?: boolean } = {}, + ): Promise { + const logger = ownLogger(); + const seeds = + 'mnemonic' in keyMaterial + ? seedsFromMnemonic(keyMaterial.mnemonic) + : seedsFromMasterHex(keyMaterial.seedHex); + + const config = facadeConfiguration(env); + const unshieldedKeystore = createKeystore( + seeds.unshielded, + env.walletNetworkId, + ); + + const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); + const unshielded = UnshieldedWallet({ + ...config, + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); + + const dustConfig = { + ...config, + costParameters: { + ledgerParams: LedgerParameters.initialParameters(), + additionalFeeOverhead: + env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, + feeBlocksMargin: 5, + }, + }; + const dust = DustWallet(dustConfig).startWithSeed( + seeds.dust, + LedgerParameters.initialParameters().dust, + ); + + const facade = await WalletFacade.init({ + configuration: config, + shielded: () => shielded, + unshielded: () => unshielded, + dust: () => dust, + }); + + const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); + const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); + + logger.info('Own wallet: starting facade...'); + await facade.start(zswapSecretKeys, dustSecretKey); + if (options.waitForFunds ?? true) { + await waitForShieldedSync(facade, logger); + } + + return new OwnWalletProvider( + env, + facade, + zswapSecretKeys, + dustSecretKey, + unshieldedKeystore, + logger, + ); + } +} + +function ttlOneHour(): Date { + return new Date(Date.now() + 60 * 60 * 1000); +} + +/** + * Block until the shielded wallet reports a synced state. The facade's shielded + * API exposes a `waitForSyncedState`; fall back to a short settle if absent. + */ +async function waitForShieldedSync( + facade: WalletFacade, + logger: Logger, +): Promise { + const shielded = facade.shielded as { + waitForSyncedState?: (gap?: bigint) => Promise; + }; + if (typeof shielded.waitForSyncedState === 'function') { + await shielded.waitForSyncedState(); + logger.info('Own wallet: shielded state synced'); + } +} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts new file mode 100644 index 00000000..7a2cca7a --- /dev/null +++ b/contracts/test/integration/_harness/providers.ts @@ -0,0 +1,56 @@ +import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; +import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; +import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; +import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; +import type { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's + * artifact directory. Each module test passes its own `` so the + * ZK config provider reads that module's keys. + * + * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. + * + * @param wallet A started `TestWalletProvider` + * @param artifactPath Absolute path to `contracts/artifacts//contract` + * (the directory containing `contract-info.json` etc.) + * @param privateStateStoreName LevelDB namespace, unique per test contract + * @param circuitKeys Type parameter carrying the module's circuit union + */ +export function buildProviders< + CircuitKey extends string, + PrivateStateId extends string, + PrivateState, +>( + wallet: OwnWalletProvider, + artifactPath: string, + privateStateStoreName: string, +): MidnightProviders { + const zkConfigProvider = new NodeZkConfigProvider(artifactPath); + + const privateStateConfig = { + privateStateStoreName, + accountId: wallet.getCoinPublicKey(), + // Fixed test password: local/undeployed wallets don't need real entropy. + // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, + // min-length, mixed classes) deterministically across runs. + privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', + } as Parameters>[0]; + + return { + privateStateProvider: + levelPrivateStateProvider(privateStateConfig), + publicDataProvider: indexerPublicDataProvider( + wallet.env.indexer, + wallet.env.indexerWS, + ), + zkConfigProvider, + proofProvider: httpClientProofProvider( + wallet.env.proofServer, + zkConfigProvider, + ), + walletProvider: wallet, + midnightProvider: wallet, + }; +} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts new file mode 100644 index 00000000..bacbd094 --- /dev/null +++ b/contracts/test/integration/_harness/registerSimulatorLive.ts @@ -0,0 +1,154 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { CompiledContract } from '@midnight-ntwrk/compact-js'; +import { + type LiveBackendRequest, + type LiveContext, + registerLiveBackend, +} from '@openzeppelin/compact-simulator'; +import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; +import { + type LocalNetworkConfig, + networkConfig, + setupNetwork, +} from './network.js'; +import type { OwnWalletProvider } from './ownWallet.js'; +import { buildProviders } from './providers.js'; +import { buildWallet } from './wallet.js'; + +/** + * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local + * stack (`make env-up`). Registered once (from the `test:live` setup file); + * afterwards a migrated spec's `await Sim.create()` deploys the contract named by + * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the + * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. + * + * Deploy-per-`create()` gives each test a fresh contract (true isolation), which + * the unit specs assume (they rely on `beforeEach`-fresh state). + */ + +let env: LocalNetworkConfig | undefined; +let deployerPromise: Promise | undefined; +let deployCounter = 0; + +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +function getDeployer(): Promise { + if (!env) { + setupNetwork(); + env = networkConfig(); + } + if (!deployerPromise) { + deployerPromise = (async () => { + const wallet = await buildWallet(env as LocalNetworkConfig); + // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, + // so also wait for the dust wallet to sync — otherwise the first tx builds + // a stale dust spend proof (node rejects with InvalidDustSpendProof). + const dust = ( + wallet.facade as { + dust?: { waitForSyncedState?: () => Promise }; + } + ).dust; + if (typeof dust?.waitForSyncedState === 'function') { + await dust.waitForSyncedState(); + } + return wallet; + })(); + } + return deployerPromise; +} + +async function buildLiveContext( + req: LiveBackendRequest, +): Promise> { + const name = req.config.artifactName; + if (!name) { + throw new Error( + 'live backend: SimulatorConfig.artifactName is required to deploy on live', + ); + } + + const contractEntry = pathToFileURL( + path.join(moduleRootPath(name), 'contract', 'index.js'), + ).href; + const mod = await import(contractEntry); + const ContractClass = mod.Contract; + + const witnesses = req.config.witnessesFactory(); + const compiled = CompiledContract.make(name, ContractClass).pipe( + CompiledContract.withWitnesses((witnesses ?? {}) as never), + CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), + ); + + const deployer = await getDeployer(); + const psId = `${name}-ps`; + const storeName = `${name}-${++deployCounter}`; + // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics + const providers = buildProviders( + deployer, + moduleRootPath(name), + storeName, + ) as any; + + const initialPS = + req.options.privateState ?? req.config.defaultPrivateState(); + const args = req.config.contractArgs(...req.contractArgs); + + // Retry: a freshly-started dev node may still be ramping dust generation, and + // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. + let deployed: Awaited> | undefined; + let lastErr: unknown; + for (let attempt = 1; attempt <= 8; attempt++) { + try { + // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific + deployed = await deployModule( + providers, + compiled, + psId, + initialPS, + args as any, + ); + break; + } catch (err) { + lastErr = err; + await sleep(5000); + } + } + if (!deployed) { + throw new Error( + `deploy of ${name} failed after retries: ${String(lastErr)}`, + ); + } + const address = deployed.deployTxData.public.contractAddress; + + return { + contractAddress: address, + handleFor: async () => ({ + // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle + callTx: deployed.callTx as any, + }), + async queryLedger() { + for (let attempt = 0; attempt < 15; attempt++) { + const cs = + await providers.publicDataProvider.queryContractState(address); + if (cs != null) return cs.data; + await sleep(400); + } + throw new Error(`no contract state at ${address} after retries`); + }, + async queryPrivateState() { + const ps = await providers.privateStateProvider.get(psId); + return ps ?? initialPS; + }, + }; +} + +let registered = false; + +/** Registers the live backend. Idempotent per worker. */ +export function registerSimulatorLiveBackend(): void { + if (registered) return; + registered = true; + registerLiveBackend((req) => buildLiveContext(req)); +} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts new file mode 100644 index 00000000..70e0f88b --- /dev/null +++ b/contracts/test/integration/_harness/wallet.ts @@ -0,0 +1,17 @@ +import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; +import { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js + * dependency. `OwnWalletProvider` implements both `MidnightProvider` and + * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. + * Tests that need per-signer isolation pass their own BIP39 phrase. + */ +export async function buildWallet( + env: LocalNetworkConfig, + mnemonic: string = LOCAL_WALLET_MNEMONIC, +): Promise { + return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); +} diff --git a/contracts/test/integration/fixtures/composedTokens.ts b/contracts/test/integration/fixtures/composedTokens.ts index 5d67cb2f..9ab8c600 100644 --- a/contracts/test/integration/fixtures/composedTokens.ts +++ b/contracts/test/integration/fixtures/composedTokens.ts @@ -1,4 +1,7 @@ -import { createSimulator } from '@openzeppelin/compact-simulator'; +import { + createSimulator, + type SimulatorOptions, +} from '@openzeppelin/compact-simulator'; import { ledger, Contract as ComposedTokens, @@ -27,7 +30,7 @@ const ComposedTokensSimulatorBase = createSimulator< contractFactory: (witnesses) => new ComposedTokens(witnesses), defaultPrivateState: () => ({}), - contractArgs: (ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT) => [ + contractArgs: ( ftName, ftSymbol, ftDecimals, @@ -35,9 +38,10 @@ const ComposedTokensSimulatorBase = createSimulator< nftSymbol, initFT, initNFT, - ], + ) => [ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ({}), + artifactName: 'ComposedTokens', }); /** @@ -47,15 +51,24 @@ const ComposedTokensSimulatorBase = createSimulator< * are independent (the #556 fix). */ export class ComposedTokensSimulator extends ComposedTokensSimulatorBase { - constructor(initFT: boolean, initNFT: boolean) { - super(['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], {}); + static async create( + initFT: boolean, + initNFT: boolean, + // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses + options: SimulatorOptions = {}, + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create( + ['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], + options, + ) as Promise; } - public ftName(): string { + public ftName(): Promise { return this.circuits.impure.ftName(); } - public nftName(): string { + public nftName(): Promise { return this.circuits.impure.nftName(); } } diff --git a/contracts/test/integration/fixtures/sharedInitCollision.ts b/contracts/test/integration/fixtures/sharedInitCollision.ts index 1b566c01..d6a88c38 100644 --- a/contracts/test/integration/fixtures/sharedInitCollision.ts +++ b/contracts/test/integration/fixtures/sharedInitCollision.ts @@ -1,4 +1,7 @@ -import { createSimulator } from '@openzeppelin/compact-simulator'; +import { + createSimulator, + type SimulatorOptions, +} from '@openzeppelin/compact-simulator'; import { ledger, Contract as SharedInitCollision, @@ -20,6 +23,7 @@ const SharedInitCollisionSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ({}), + artifactName: 'SharedInitCollision', }); /** @@ -28,23 +32,27 @@ const SharedInitCollisionSimulatorBase = createSimulator< * collision. */ export class SharedInitCollisionSimulator extends SharedInitCollisionSimulatorBase { - constructor() { - super([], {}); + static async create( + // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses + options: SimulatorOptions = {}, + ): Promise { + // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` + return super.create([], options) as Promise; } - public initA(): void { - this.circuits.impure.initA(); + public initA(): Promise<[]> { + return this.circuits.impure.initA(); } - public initB(): void { - this.circuits.impure.initB(); + public initB(): Promise<[]> { + return this.circuits.impure.initB(); } - public checkA(): void { - this.circuits.impure.checkA(); + public checkA(): Promise<[]> { + return this.circuits.impure.checkA(); } - public checkB(): void { - this.circuits.impure.checkB(); + public checkB(): Promise<[]> { + return this.circuits.impure.checkB(); } } diff --git a/contracts/test/integration/specs/initStateIsolation.spec.ts b/contracts/test/integration/specs/initStateIsolation.spec.ts index ce230277..764b2705 100644 --- a/contracts/test/integration/specs/initStateIsolation.spec.ts +++ b/contracts/test/integration/specs/initStateIsolation.spec.ts @@ -24,50 +24,54 @@ import { SharedInitCollisionSimulator } from '../fixtures/sharedInitCollision.js describe('Initializable state isolation (#556)', () => { describe('the bug — shared Initializable across same-directory modules', () => { - it('should treat module B as initialized after only module A is initialized', () => { - const c = new SharedInitCollisionSimulator(); + it('should treat module B as initialized after only module A is initialized', async () => { + const c = await SharedInitCollisionSimulator.create(); // Only module A is initialized. - c.initA(); + await c.initA(); // BUG: module B was never initialized, yet its init-guard passes, // because both modules share a single `_isInitialized` ledger slot. - expect(() => c.checkB()).not.toThrow(); + await expect(c.checkB()).resolves.not.toThrow(); }); - it('should not allow module B to initialize once module A has set the shared slot', () => { - const c = new SharedInitCollisionSimulator(); - c.initA(); + it('should not allow module B to initialize once module A has set the shared slot', async () => { + const c = await SharedInitCollisionSimulator.create(); + await c.initA(); // BUG: B can never be initialized — the shared slot is already set. - expect(() => c.initB()).toThrow('Initializable: contract already initialized'); + await expect(c.initB()).rejects.toThrow( + 'Initializable: contract already initialized', + ); }); }); describe('the fix — per-module flags keep production modules isolated', () => { - it('should not initialize NonFungibleToken when only FungibleToken is initialized', () => { - const c = new ComposedTokensSimulator(true, false); + it('should not initialize NonFungibleToken when only FungibleToken is initialized', async () => { + const c = await ComposedTokensSimulator.create(true, false); // FT is usable. - expect(() => c.ftName()).not.toThrow(); + await expect(c.ftName()).resolves.not.toThrow(); // NFT is independently still uninitialized. - expect(() => c.nftName()).toThrow( + await expect(c.nftName()).rejects.toThrow( 'NonFungibleToken: contract not initialized', ); }); - it('should not initialize FungibleToken when only NonFungibleToken is initialized', () => { - const c = new ComposedTokensSimulator(false, true); + it('should not initialize FungibleToken when only NonFungibleToken is initialized', async () => { + const c = await ComposedTokensSimulator.create(false, true); - expect(() => c.nftName()).not.toThrow(); - expect(() => c.ftName()).toThrow('FungibleToken: contract not initialized'); + await expect(c.nftName()).resolves.not.toThrow(); + await expect(c.ftName()).rejects.toThrow( + 'FungibleToken: contract not initialized', + ); }); - it('should initialize each module independently', () => { - const c = new ComposedTokensSimulator(true, true); + it('should initialize each module independently', async () => { + const c = await ComposedTokensSimulator.create(true, true); - expect(() => c.ftName()).not.toThrow(); - expect(() => c.nftName()).not.toThrow(); + await expect(c.ftName()).resolves.not.toThrow(); + await expect(c.nftName()).resolves.not.toThrow(); }); }); }); diff --git a/contracts/vitest.integration.live.config.ts b/contracts/vitest.integration.live.config.ts new file mode 100644 index 00000000..e26dfad8 --- /dev/null +++ b/contracts/vitest.integration.live.config.ts @@ -0,0 +1,25 @@ +import { configDefaults, defineConfig } from 'vitest/config'; + +// Live-backend run of the integration specs against the local stack +// (`make env-up`). Same spec files as the dry `test:integration`; only the +// backend (via `MIDNIGHT_BACKEND=live`) and this config differ — each +// `await Sim.create()` deploys + attaches a real composed 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: ['test/integration/specs/**/*.spec.ts'], + exclude: [...configDefaults.exclude], + setupFiles: ['./test/integration/_harness/live.setup.ts'], + reporters: 'verbose', + testTimeout: 180_000, + hookTimeout: 300_000, + fileParallelism: false, + sequence: { concurrent: false }, + }, +}); diff --git a/contracts/vitest.live.config.ts b/contracts/vitest.live.config.ts new file mode 100644 index 00000000..52377aad --- /dev/null +++ b/contracts/vitest.live.config.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..fedb1fef --- /dev/null +++ b/local-env.yml @@ -0,0 +1,61 @@ +# WARNING: Insecure default credentials below. For local development only — do not use in production. +services: + proof-server: + image: 'midnightntwrk/proof-server:latest' + command: ['midnight-proof-server -v'] + ports: + - '6300:6300' + environment: + RUST_BACKTRACE: 'full' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + + indexer: + image: 'midnightntwrk/indexer-standalone:latest' + ports: + - '8088:8088' + environment: + RUST_LOG: 'indexer=debug,chain_indexer=debug,indexer_api=debug,wallet_indexer=debug,indexer_common=debug,fastrace_opentelemetry=off,info' + APP__INFRA__NODE__URL: 'ws://node:9944' + APP__APPLICATION__NETWORK_ID: 'undeployed' + APP__INFRA__STORAGE__PASSWORD: 'indexer' + APP__INFRA__PUB_SUB__PASSWORD: 'indexer' + APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' + APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' + healthcheck: + test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + depends_on: + node: + condition: service_healthy + logging: + driver: local + options: + max-size: '10m' + max-file: '3' + + node: + image: 'midnightntwrk/midnight-node:0.22.2' + ports: + - '9944:9944' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] + interval: 2s + timeout: 5s + retries: 20 + start_period: 5s + environment: + CFG_PRESET: 'dev' + SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' + logging: + driver: local + options: + max-size: '10m' + max-file: '3' From bed2dd8bf2f90552a665ffab2c514a8968fabb41 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 11:35:31 +0200 Subject: [PATCH 07/10] test: drop integration-test live additions The integration suite is out of scope for this PR. Revert the integration fixtures (composedTokens, sharedInitCollision) and the initStateIsolation spec to their main versions, and remove the integration live wiring: vitest.integration.live.config.ts and the test:integration:live / compact:integration:live scripts. The integration tests are now untouched relative to main. Migrating them to the async simulator (and wiring them for live) is deferred to the follow-up PR alongside the live CI workflow. Kept: the unit async migration and the local unit live setup (test:live, vitest.live.config.ts, the _harness backend, Makefile, local-env.yml). --- contracts/package.json | 2 - .../integration/fixtures/composedTokens.ts | 27 +++--------- .../fixtures/sharedInitCollision.ts | 30 +++++-------- .../specs/initStateIsolation.spec.ts | 44 +++++++++---------- contracts/vitest.integration.live.config.ts | 25 ----------- 5 files changed, 38 insertions(+), 90 deletions(-) delete mode 100644 contracts/vitest.integration.live.config.ts diff --git a/contracts/package.json b/contracts/package.json index b9fa6c29..7796032d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -37,8 +37,6 @@ "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", - "compact:integration:live": "compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens", - "test:integration:live": "yarn run compact:integration:live && MIDNIGHT_BACKEND=live vitest run --config vitest.integration.live.config.ts", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, diff --git a/contracts/test/integration/fixtures/composedTokens.ts b/contracts/test/integration/fixtures/composedTokens.ts index 9ab8c600..5d67cb2f 100644 --- a/contracts/test/integration/fixtures/composedTokens.ts +++ b/contracts/test/integration/fixtures/composedTokens.ts @@ -1,7 +1,4 @@ -import { - createSimulator, - type SimulatorOptions, -} from '@openzeppelin/compact-simulator'; +import { createSimulator } from '@openzeppelin/compact-simulator'; import { ledger, Contract as ComposedTokens, @@ -30,7 +27,7 @@ const ComposedTokensSimulatorBase = createSimulator< contractFactory: (witnesses) => new ComposedTokens(witnesses), defaultPrivateState: () => ({}), - contractArgs: ( + contractArgs: (ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT) => [ ftName, ftSymbol, ftDecimals, @@ -38,10 +35,9 @@ const ComposedTokensSimulatorBase = createSimulator< nftSymbol, initFT, initNFT, - ) => [ftName, ftSymbol, ftDecimals, nftName, nftSymbol, initFT, initNFT], + ], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ({}), - artifactName: 'ComposedTokens', }); /** @@ -51,24 +47,15 @@ const ComposedTokensSimulatorBase = createSimulator< * are independent (the #556 fix). */ export class ComposedTokensSimulator extends ComposedTokensSimulatorBase { - static async create( - initFT: boolean, - initNFT: boolean, - // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses - options: SimulatorOptions = {}, - ): Promise { - // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` - return super.create( - ['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], - options, - ) as Promise; + constructor(initFT: boolean, initNFT: boolean) { + super(['FT', 'FTK', 18n, 'NFT', 'NFTK', initFT, initNFT], {}); } - public ftName(): Promise { + public ftName(): string { return this.circuits.impure.ftName(); } - public nftName(): Promise { + public nftName(): string { return this.circuits.impure.nftName(); } } diff --git a/contracts/test/integration/fixtures/sharedInitCollision.ts b/contracts/test/integration/fixtures/sharedInitCollision.ts index d6a88c38..1b566c01 100644 --- a/contracts/test/integration/fixtures/sharedInitCollision.ts +++ b/contracts/test/integration/fixtures/sharedInitCollision.ts @@ -1,7 +1,4 @@ -import { - createSimulator, - type SimulatorOptions, -} from '@openzeppelin/compact-simulator'; +import { createSimulator } from '@openzeppelin/compact-simulator'; import { ledger, Contract as SharedInitCollision, @@ -23,7 +20,6 @@ const SharedInitCollisionSimulatorBase = createSimulator< contractArgs: () => [], ledgerExtractor: (state) => ledger(state), witnessesFactory: () => ({}), - artifactName: 'SharedInitCollision', }); /** @@ -32,27 +28,23 @@ const SharedInitCollisionSimulatorBase = createSimulator< * collision. */ export class SharedInitCollisionSimulator extends SharedInitCollisionSimulatorBase { - static async create( - // biome-ignore lint/complexity/noBannedTypes: the contract declares no witnesses - options: SimulatorOptions = {}, - ): Promise { - // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this` - return super.create([], options) as Promise; + constructor() { + super([], {}); } - public initA(): Promise<[]> { - return this.circuits.impure.initA(); + public initA(): void { + this.circuits.impure.initA(); } - public initB(): Promise<[]> { - return this.circuits.impure.initB(); + public initB(): void { + this.circuits.impure.initB(); } - public checkA(): Promise<[]> { - return this.circuits.impure.checkA(); + public checkA(): void { + this.circuits.impure.checkA(); } - public checkB(): Promise<[]> { - return this.circuits.impure.checkB(); + public checkB(): void { + this.circuits.impure.checkB(); } } diff --git a/contracts/test/integration/specs/initStateIsolation.spec.ts b/contracts/test/integration/specs/initStateIsolation.spec.ts index 764b2705..ce230277 100644 --- a/contracts/test/integration/specs/initStateIsolation.spec.ts +++ b/contracts/test/integration/specs/initStateIsolation.spec.ts @@ -24,54 +24,50 @@ import { SharedInitCollisionSimulator } from '../fixtures/sharedInitCollision.js describe('Initializable state isolation (#556)', () => { describe('the bug — shared Initializable across same-directory modules', () => { - it('should treat module B as initialized after only module A is initialized', async () => { - const c = await SharedInitCollisionSimulator.create(); + it('should treat module B as initialized after only module A is initialized', () => { + const c = new SharedInitCollisionSimulator(); // Only module A is initialized. - await c.initA(); + c.initA(); // BUG: module B was never initialized, yet its init-guard passes, // because both modules share a single `_isInitialized` ledger slot. - await expect(c.checkB()).resolves.not.toThrow(); + expect(() => c.checkB()).not.toThrow(); }); - it('should not allow module B to initialize once module A has set the shared slot', async () => { - const c = await SharedInitCollisionSimulator.create(); - await c.initA(); + it('should not allow module B to initialize once module A has set the shared slot', () => { + const c = new SharedInitCollisionSimulator(); + c.initA(); // BUG: B can never be initialized — the shared slot is already set. - await expect(c.initB()).rejects.toThrow( - 'Initializable: contract already initialized', - ); + expect(() => c.initB()).toThrow('Initializable: contract already initialized'); }); }); describe('the fix — per-module flags keep production modules isolated', () => { - it('should not initialize NonFungibleToken when only FungibleToken is initialized', async () => { - const c = await ComposedTokensSimulator.create(true, false); + it('should not initialize NonFungibleToken when only FungibleToken is initialized', () => { + const c = new ComposedTokensSimulator(true, false); // FT is usable. - await expect(c.ftName()).resolves.not.toThrow(); + expect(() => c.ftName()).not.toThrow(); // NFT is independently still uninitialized. - await expect(c.nftName()).rejects.toThrow( + expect(() => c.nftName()).toThrow( 'NonFungibleToken: contract not initialized', ); }); - it('should not initialize FungibleToken when only NonFungibleToken is initialized', async () => { - const c = await ComposedTokensSimulator.create(false, true); + it('should not initialize FungibleToken when only NonFungibleToken is initialized', () => { + const c = new ComposedTokensSimulator(false, true); - await expect(c.nftName()).resolves.not.toThrow(); - await expect(c.ftName()).rejects.toThrow( - 'FungibleToken: contract not initialized', - ); + expect(() => c.nftName()).not.toThrow(); + expect(() => c.ftName()).toThrow('FungibleToken: contract not initialized'); }); - it('should initialize each module independently', async () => { - const c = await ComposedTokensSimulator.create(true, true); + it('should initialize each module independently', () => { + const c = new ComposedTokensSimulator(true, true); - await expect(c.ftName()).resolves.not.toThrow(); - await expect(c.nftName()).resolves.not.toThrow(); + expect(() => c.ftName()).not.toThrow(); + expect(() => c.nftName()).not.toThrow(); }); }); }); diff --git a/contracts/vitest.integration.live.config.ts b/contracts/vitest.integration.live.config.ts deleted file mode 100644 index e26dfad8..00000000 --- a/contracts/vitest.integration.live.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { configDefaults, defineConfig } from 'vitest/config'; - -// Live-backend run of the integration specs against the local stack -// (`make env-up`). Same spec files as the dry `test:integration`; only the -// backend (via `MIDNIGHT_BACKEND=live`) and this config differ — each -// `await Sim.create()` deploys + attaches a real composed 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: ['test/integration/specs/**/*.spec.ts'], - exclude: [...configDefaults.exclude], - setupFiles: ['./test/integration/_harness/live.setup.ts'], - reporters: 'verbose', - testTimeout: 180_000, - hookTimeout: 300_000, - fileParallelism: false, - sequence: { concurrent: false }, - }, -}); From 881d0b0d3da573d83bcf581aaa90c8a973029ce7 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 11:52:10 +0200 Subject: [PATCH 08/10] test: drop all live test infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce this PR to the migration itself: only the existing simulator-using files (per-module simulators + specs) move to the async backend-aware @openzeppelin/compact-simulator 0.2.0, plus the dependency bump. No new files. Remove the live scaffolding added earlier — the test/integration/_harness backend, vitest.live.config.ts, the test:live script, Makefile, and local-env.yml. The local live setup and the live CI workflow belong in a follow-up PR. The integration suite is untouched relative to main. --- Makefile | 31 -- contracts/package.json | 1 - contracts/test/integration/_harness/deploy.ts | 84 ----- .../test/integration/_harness/live.setup.ts | 7 - .../test/integration/_harness/network.ts | 63 ---- .../test/integration/_harness/ownWallet.ts | 289 ------------------ .../test/integration/_harness/providers.ts | 56 ---- .../_harness/registerSimulatorLive.ts | 154 ---------- contracts/test/integration/_harness/wallet.ts | 17 -- contracts/vitest.live.config.ts | 24 -- local-env.yml | 61 ---- 11 files changed, 787 deletions(-) delete mode 100644 Makefile delete mode 100644 contracts/test/integration/_harness/deploy.ts delete mode 100644 contracts/test/integration/_harness/live.setup.ts delete mode 100644 contracts/test/integration/_harness/network.ts delete mode 100644 contracts/test/integration/_harness/ownWallet.ts delete mode 100644 contracts/test/integration/_harness/providers.ts delete mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts delete mode 100644 contracts/test/integration/_harness/wallet.ts delete mode 100644 contracts/vitest.live.config.ts delete mode 100644 local-env.yml diff --git a/Makefile b/Makefile deleted file mode 100644 index e5a601bf..00000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -COMPOSE_FILE := local-env.yml -LOGS_DIR := logs -SERVICES := proof-server indexer node - -.PHONY: env-up env-down env-logs env-logs-clean env-status - -## Start local environment and stream logs to logs/ -env-up: env-down - docker compose -f $(COMPOSE_FILE) up -d - @mkdir -p $(LOGS_DIR) - @for svc in $(SERVICES); do \ - docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ - done - @echo "Logs streaming to $(LOGS_DIR)/" - -## Stop local environment -env-down: - @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true - docker compose -f $(COMPOSE_FILE) down - -## Tail all logs -env-logs: - tail -f $(LOGS_DIR)/*.log - -## Clear log files -env-logs-clean: - rm -rf $(LOGS_DIR)/*.log - -## Show container status -env-status: - docker compose -f $(COMPOSE_FILE) ps diff --git a/contracts/package.json b/contracts/package.json index 7796032d..193aa3ab 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,7 +34,6 @@ "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", "types": "tsc -p tsconfig.json --noEmit", diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts deleted file mode 100644 index f9a1d283..00000000 --- a/contracts/test/integration/_harness/deploy.ts +++ /dev/null @@ -1,84 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - CompiledContract, - Contract as ContractNs, -} from '@midnight-ntwrk/compact-js'; -import { - type DeployContractOptionsWithPrivateState, - type DeployedContract, - deployContract, -} from '@midnight-ntwrk/midnight-js-contracts'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; - -const currentDir = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Absolute path to `contracts/artifacts//`. - * Used by `NodeZkConfigProvider`, which expects the directory containing - * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). - */ -export function moduleRootPath(moduleName: string): string { - // _harness/ is at contracts/test/integration/_harness/ - // module root at contracts/artifacts// - return path.resolve( - currentDir, - '..', - '..', - '..', - 'artifacts', - moduleName, - ); -} - -/** - * Absolute path to `contracts/artifacts//contract/` — where the - * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) - * live. Used by `CompiledContract.withCompiledFileAssets`. - */ -export function contractAssetsPath(moduleName: string): string { - return path.join(moduleRootPath(moduleName), 'contract'); -} - -/** - * Generic deploy wrapper. - * - * Each per-module fixture builds its own `CompiledContract` (because - * `witnesses` are module-specific) and passes it here along with providers, - * a private-state id, the initial private-state value, and the contract's - * constructor arguments — all properly typed via `Contract.*` helpers from - * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. - */ -export async function deployModule( - providers: MidnightProviders< - ContractNs.ProvableCircuitId, - string, - ContractNs.PrivateState - >, - // The third generic of `CompiledContract` (the witnesses map) defaults to - // `never` for empty-witness contracts; accept `any` so both shapes pass. - compiledContract: CompiledContract.CompiledContract< - C, - ContractNs.PrivateState, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any - >, - privateStateId: string, - initialPrivateState: ContractNs.PrivateState, - args: ContractNs.InitializeParameters, -): Promise> { - // The deployContract options shape is conditional on whether - // `Contract.InitializeParameters` is empty — TypeScript can't reduce - // that conditional under an unbounded `C extends Contract.Any`, so we - // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. - // Two-step cast (through `unknown`) because TS rejects the direct cast - // as "neither type sufficiently overlaps" — same conditional-resolution - // issue. Scoped to this single helper. - const options = { - compiledContract, - privateStateId, - initialPrivateState, - args, - } as unknown as DeployContractOptionsWithPrivateState; - return deployContract(providers, options); -} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts deleted file mode 100644 index e5b05fec..00000000 --- a/contracts/test/integration/_harness/live.setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; - -// Runs once per worker before the unit specs when `test:live` is used. Registers -// the live backend so `await Sim.create()` attaches to a freshly-deployed -// contract on the local stack (brought up by `make env-up`). On the dry path -// (default `test`) this file is not loaded, so nothing changes there. -registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts deleted file mode 100644 index dacf5ebc..00000000 --- a/contracts/test/integration/_harness/network.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - type NetworkId, - setNetworkId, -} from '@midnight-ntwrk/midnight-js-network-id'; - -/** - * Endpoint configuration for the local stack. Replaces testkit-js' - * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally - * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. - */ -export interface LocalNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly networkId: string; - readonly indexer: string; - readonly indexerWS: string; - readonly node: string; - readonly nodeWS: string; - readonly proofServer: string; - readonly faucet: string | undefined; -} - -/** - * Prefunded wallet mnemonic for the local `undeployed` network — the canonical - * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` - * recognises as the genesis-funded account. Inlined so the harness no longer - * depends on testkit-js' `TEST_MNEMONIC`. - */ -export const LOCAL_WALLET_MNEMONIC = - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; - -/** - * Default endpoints for the local stack brought up by `make env-up`. - * Each is overridable via a MIDNIGHT_* env var so CI / other hosts - * can point the same harness at a relocated stack. - */ -export function networkConfig(): LocalNetworkConfig { - return { - walletNetworkId: 'undeployed' as NetworkId, - networkId: 'undeployed', - indexer: - process.env.MIDNIGHT_INDEXER_URL ?? - 'http://127.0.0.1:8088/api/v4/graphql', - indexerWS: - process.env.MIDNIGHT_INDEXER_WS_URL ?? - 'ws://127.0.0.1:8088/api/v4/graphql/ws', - node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', - nodeWS: 'ws://127.0.0.1:9944', - proofServer: - process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', - faucet: undefined, - }; -} - -/** - * Set the process-wide network id. Must be called once before any provider - * or wallet is constructed. Idempotent. - */ -let networkIdSet = false; -export function setupNetwork(): void { - if (networkIdSet) return; - setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); - networkIdSet = true; -} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts deleted file mode 100644 index fd2a78df..00000000 --- a/contracts/test/integration/_harness/ownWallet.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Own test wallet provider — a testkit-js-free reconstruction of the wallet - * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * WHY THIS EXISTS - * --------------- - * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s - * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy - * dependency (testcontainers, docker orchestration, a fixed env model) of which - * we use almost nothing — we run our own local stack via `make env-up`. All - * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter - * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. - * - * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, - * so the harness no longer imports testkit. It is a behavioural drop-in for the - * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. - * - * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: - * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed - * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk - * `balanceTx` / `submitTx` delegate to the facade identically to testkit. - * - * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, - * `shielded`) so a future coin-injecting shielded wallet can be slotted in via - * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks - * the spend-path (burn / round-trip) integration specs. See - * `NativeShieldedToken-tests.md` "Own wallet tool". - */ -import { - DustSecretKey, - LedgerParameters, - ZswapSecretKeys, -} from '@midnight-ntwrk/ledger-v8'; -import type { - FinalizedTransaction, - TransactionId, -} from '@midnight-ntwrk/midnight-js-protocol/ledger'; -import type { - MidnightProvider, - WalletProvider, -} from '@midnight-ntwrk/midnight-js-types'; -import { - createKeystore, - DustWallet, - HDWallet, - InMemoryTransactionHistoryStorage, - mergeWalletEntries, - PublicKey, - type Role, - Roles, - ShieldedWallet, - UnshieldedWallet, - WalletEntrySchema, - WalletFacade, -} from '@midnight-ntwrk/wallet-sdk'; -import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; -import { mnemonicToSeedSync } from '@scure/bip39'; -import pino, { type Logger } from 'pino'; - -/** - * Minimal endpoint config our wallet needs — a structural subset of the fields - * `networkConfig()` already returns, with no testkit type dependency. - */ -export interface OwnNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly indexer: string; - readonly indexerWS: string; - readonly nodeWS: string; - readonly proofServer: string; -} - -/** - * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at - * preset-dev needs headroom to cover fees on undeployed; mirrors the value the - * old testkit-based `buildWallet` passed via `DustWalletOptions`. - */ -const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; - -let sharedLogger: Logger | undefined; -function ownLogger(): Logger { - if (!sharedLogger) { - sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); - } - return sharedLogger; -} - -/** The three role sub-seeds derived from a master seed, plus the master. */ -interface DerivedSeeds { - readonly masterSeedHex: string; - readonly shielded: Uint8Array; - readonly unshielded: Uint8Array; - readonly dust: Uint8Array; -} - -/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ -function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { - if (!masterSeedHex || masterSeedHex.length === 0) { - throw new Error('Own wallet: master seed cannot be empty'); - } - const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); - if (result.type !== 'seedOk') { - throw new Error('Own wallet: invalid seed, failed to create HD wallet'); - } - const derived = result.hdWallet - .selectAccount(0) - .selectRole(role) - .deriveKeyAt(0); - if (derived.type !== 'keyDerived') { - throw new Error(`Own wallet: key derivation failed for role ${role}`); - } - return derived.key; -} - -function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { - return { - masterSeedHex, - shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), - unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), - dust: deriveKeyForRole(masterSeedHex, Roles.Dust), - }; -} - -function seedsFromMnemonic(mnemonic: string): DerivedSeeds { - if (!mnemonic || mnemonic.trim().length === 0) { - throw new Error('Own wallet: mnemonic cannot be empty'); - } - return seedsFromMasterHex( - Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), - ); -} - -/** - * Map our endpoint config to the wallet-sdk facade configuration object. - * Shape lifted from testkit's `mapEnvironmentToConfiguration`. - */ -function facadeConfiguration(env: OwnNetworkConfig) { - return { - indexerClientConnection: { - indexerHttpUrl: env.indexer, - indexerWsUrl: env.indexerWS, - }, - provingServerUrl: new URL(env.proofServer), - networkId: env.walletNetworkId, - relayURL: new URL(env.nodeWS), - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - costParameters: { feeBlocksMargin: 5 }, - }; -} - -/** - * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, - * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the - * same delegations testkit's `MidnightWalletProvider` performed. - */ -export class OwnWalletProvider implements WalletProvider, MidnightProvider { - private constructor( - readonly env: OwnNetworkConfig, - readonly facade: WalletFacade, - readonly zswapSecretKeys: ZswapSecretKeys, - readonly dustSecretKey: DustSecretKey, - private readonly unshieldedKeystore: ReturnType, - private readonly logger: Logger, - ) {} - - getCoinPublicKey() { - return this.zswapSecretKeys.coinPublicKey; - } - - getEncryptionPublicKey() { - return this.zswapSecretKeys.encryptionPublicKey; - } - - async balanceTx( - tx: Parameters[0], - ttl: Date = ttlOneHour(), - ): Promise { - const recipe = await this.facade.balanceUnboundTransaction( - tx, - { - shieldedSecretKeys: this.zswapSecretKeys, - dustSecretKey: this.dustSecretKey, - }, - { ttl }, - ); - const signed = await this.facade.signRecipe(recipe, (payload) => - this.unshieldedKeystore.signData(payload), - ); - return this.facade.finalizeRecipe(signed); - } - - submitTx(tx: FinalizedTransaction): Promise { - return this.facade.submitTransaction(tx); - } - - async stop(): Promise { - await this.facade.stop(); - } - - /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ - static async build( - env: OwnNetworkConfig, - keyMaterial: { mnemonic: string } | { seedHex: string }, - options: { waitForFunds?: boolean } = {}, - ): Promise { - const logger = ownLogger(); - const seeds = - 'mnemonic' in keyMaterial - ? seedsFromMnemonic(keyMaterial.mnemonic) - : seedsFromMasterHex(keyMaterial.seedHex); - - const config = facadeConfiguration(env); - const unshieldedKeystore = createKeystore( - seeds.unshielded, - env.walletNetworkId, - ); - - const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); - const unshielded = UnshieldedWallet({ - ...config, - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); - - const dustConfig = { - ...config, - costParameters: { - ledgerParams: LedgerParameters.initialParameters(), - additionalFeeOverhead: - env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, - feeBlocksMargin: 5, - }, - }; - const dust = DustWallet(dustConfig).startWithSeed( - seeds.dust, - LedgerParameters.initialParameters().dust, - ); - - const facade = await WalletFacade.init({ - configuration: config, - shielded: () => shielded, - unshielded: () => unshielded, - dust: () => dust, - }); - - const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); - const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); - - logger.info('Own wallet: starting facade...'); - await facade.start(zswapSecretKeys, dustSecretKey); - if (options.waitForFunds ?? true) { - await waitForShieldedSync(facade, logger); - } - - return new OwnWalletProvider( - env, - facade, - zswapSecretKeys, - dustSecretKey, - unshieldedKeystore, - logger, - ); - } -} - -function ttlOneHour(): Date { - return new Date(Date.now() + 60 * 60 * 1000); -} - -/** - * Block until the shielded wallet reports a synced state. The facade's shielded - * API exposes a `waitForSyncedState`; fall back to a short settle if absent. - */ -async function waitForShieldedSync( - facade: WalletFacade, - logger: Logger, -): Promise { - const shielded = facade.shielded as { - waitForSyncedState?: (gap?: bigint) => Promise; - }; - if (typeof shielded.waitForSyncedState === 'function') { - await shielded.waitForSyncedState(); - logger.info('Own wallet: shielded state synced'); - } -} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts deleted file mode 100644 index 7a2cca7a..00000000 --- a/contracts/test/integration/_harness/providers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; -import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; -import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; -import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; -import type { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's - * artifact directory. Each module test passes its own `` so the - * ZK config provider reads that module's keys. - * - * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. - * - * @param wallet A started `TestWalletProvider` - * @param artifactPath Absolute path to `contracts/artifacts//contract` - * (the directory containing `contract-info.json` etc.) - * @param privateStateStoreName LevelDB namespace, unique per test contract - * @param circuitKeys Type parameter carrying the module's circuit union - */ -export function buildProviders< - CircuitKey extends string, - PrivateStateId extends string, - PrivateState, ->( - wallet: OwnWalletProvider, - artifactPath: string, - privateStateStoreName: string, -): MidnightProviders { - const zkConfigProvider = new NodeZkConfigProvider(artifactPath); - - const privateStateConfig = { - privateStateStoreName, - accountId: wallet.getCoinPublicKey(), - // Fixed test password: local/undeployed wallets don't need real entropy. - // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, - // min-length, mixed classes) deterministically across runs. - privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', - } as Parameters>[0]; - - return { - privateStateProvider: - levelPrivateStateProvider(privateStateConfig), - publicDataProvider: indexerPublicDataProvider( - wallet.env.indexer, - wallet.env.indexerWS, - ), - zkConfigProvider, - proofProvider: httpClientProofProvider( - wallet.env.proofServer, - zkConfigProvider, - ), - walletProvider: wallet, - midnightProvider: wallet, - }; -} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts deleted file mode 100644 index bacbd094..00000000 --- a/contracts/test/integration/_harness/registerSimulatorLive.ts +++ /dev/null @@ -1,154 +0,0 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { CompiledContract } from '@midnight-ntwrk/compact-js'; -import { - type LiveBackendRequest, - type LiveContext, - registerLiveBackend, -} from '@openzeppelin/compact-simulator'; -import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; -import { - type LocalNetworkConfig, - networkConfig, - setupNetwork, -} from './network.js'; -import type { OwnWalletProvider } from './ownWallet.js'; -import { buildProviders } from './providers.js'; -import { buildWallet } from './wallet.js'; - -/** - * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local - * stack (`make env-up`). Registered once (from the `test:live` setup file); - * afterwards a migrated spec's `await Sim.create()` deploys the contract named by - * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the - * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. - * - * Deploy-per-`create()` gives each test a fresh contract (true isolation), which - * the unit specs assume (they rely on `beforeEach`-fresh state). - */ - -let env: LocalNetworkConfig | undefined; -let deployerPromise: Promise | undefined; -let deployCounter = 0; - -const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -function getDeployer(): Promise { - if (!env) { - setupNetwork(); - env = networkConfig(); - } - if (!deployerPromise) { - deployerPromise = (async () => { - const wallet = await buildWallet(env as LocalNetworkConfig); - // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, - // so also wait for the dust wallet to sync — otherwise the first tx builds - // a stale dust spend proof (node rejects with InvalidDustSpendProof). - const dust = ( - wallet.facade as { - dust?: { waitForSyncedState?: () => Promise }; - } - ).dust; - if (typeof dust?.waitForSyncedState === 'function') { - await dust.waitForSyncedState(); - } - return wallet; - })(); - } - return deployerPromise; -} - -async function buildLiveContext( - req: LiveBackendRequest, -): Promise> { - const name = req.config.artifactName; - if (!name) { - throw new Error( - 'live backend: SimulatorConfig.artifactName is required to deploy on live', - ); - } - - const contractEntry = pathToFileURL( - path.join(moduleRootPath(name), 'contract', 'index.js'), - ).href; - const mod = await import(contractEntry); - const ContractClass = mod.Contract; - - const witnesses = req.config.witnessesFactory(); - const compiled = CompiledContract.make(name, ContractClass).pipe( - CompiledContract.withWitnesses((witnesses ?? {}) as never), - CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), - ); - - const deployer = await getDeployer(); - const psId = `${name}-ps`; - const storeName = `${name}-${++deployCounter}`; - // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics - const providers = buildProviders( - deployer, - moduleRootPath(name), - storeName, - ) as any; - - const initialPS = - req.options.privateState ?? req.config.defaultPrivateState(); - const args = req.config.contractArgs(...req.contractArgs); - - // Retry: a freshly-started dev node may still be ramping dust generation, and - // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. - let deployed: Awaited> | undefined; - let lastErr: unknown; - for (let attempt = 1; attempt <= 8; attempt++) { - try { - // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific - deployed = await deployModule( - providers, - compiled, - psId, - initialPS, - args as any, - ); - break; - } catch (err) { - lastErr = err; - await sleep(5000); - } - } - if (!deployed) { - throw new Error( - `deploy of ${name} failed after retries: ${String(lastErr)}`, - ); - } - const address = deployed.deployTxData.public.contractAddress; - - return { - contractAddress: address, - handleFor: async () => ({ - // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle - callTx: deployed.callTx as any, - }), - async queryLedger() { - for (let attempt = 0; attempt < 15; attempt++) { - const cs = - await providers.publicDataProvider.queryContractState(address); - if (cs != null) return cs.data; - await sleep(400); - } - throw new Error(`no contract state at ${address} after retries`); - }, - async queryPrivateState() { - const ps = await providers.privateStateProvider.get(psId); - return ps ?? initialPS; - }, - }; -} - -let registered = false; - -/** Registers the live backend. Idempotent per worker. */ -export function registerSimulatorLiveBackend(): void { - if (registered) return; - registered = true; - registerLiveBackend((req) => buildLiveContext(req)); -} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts deleted file mode 100644 index 70e0f88b..00000000 --- a/contracts/test/integration/_harness/wallet.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; -import { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js - * dependency. `OwnWalletProvider` implements both `MidnightProvider` and - * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. - * Tests that need per-signer isolation pass their own BIP39 phrase. - */ -export async function buildWallet( - env: LocalNetworkConfig, - mnemonic: string = LOCAL_WALLET_MNEMONIC, -): Promise { - return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); -} diff --git a/contracts/vitest.live.config.ts b/contracts/vitest.live.config.ts deleted file mode 100644 index 52377aad..00000000 --- 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 deleted file mode 100644 index fedb1fef..00000000 --- a/local-env.yml +++ /dev/null @@ -1,61 +0,0 @@ -# WARNING: Insecure default credentials below. For local development only — do not use in production. -services: - proof-server: - image: 'midnightntwrk/proof-server:latest' - command: ['midnight-proof-server -v'] - ports: - - '6300:6300' - environment: - RUST_BACKTRACE: 'full' - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] - interval: 10s - timeout: 5s - retries: 20 - start_period: 10s - - indexer: - image: 'midnightntwrk/indexer-standalone:latest' - ports: - - '8088:8088' - environment: - RUST_LOG: 'indexer=debug,chain_indexer=debug,indexer_api=debug,wallet_indexer=debug,indexer_common=debug,fastrace_opentelemetry=off,info' - APP__INFRA__NODE__URL: 'ws://node:9944' - APP__APPLICATION__NETWORK_ID: 'undeployed' - APP__INFRA__STORAGE__PASSWORD: 'indexer' - APP__INFRA__PUB_SUB__PASSWORD: 'indexer' - APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' - APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' - healthcheck: - test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] - interval: 10s - timeout: 5s - retries: 20 - start_period: 10s - depends_on: - node: - condition: service_healthy - logging: - driver: local - options: - max-size: '10m' - max-file: '3' - - node: - image: 'midnightntwrk/midnight-node:0.22.2' - ports: - - '9944:9944' - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] - interval: 2s - timeout: 5s - retries: 20 - start_period: 5s - environment: - CFG_PRESET: 'dev' - SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' - logging: - driver: local - options: - max-size: '10m' - max-file: '3' From 055ea413de7a524685f81b8d1b170c056e40ddfe Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 12:01:00 +0200 Subject: [PATCH 09/10] test: restore unit live test setup Bring back the local unit live-testing setup: the `test:live` script, vitest.live.config.ts, the test/integration/_harness live backend, and make env-up / local-env.yml. `make env-up && yarn test:live` runs the migrated unit specs against a real node (MIDNIGHT_BACKEND=live). The integration suite stays untouched relative to main, and the live CI workflow is still deferred to a follow-up PR. --- Makefile | 31 ++ contracts/package.json | 1 + contracts/test/integration/_harness/deploy.ts | 84 +++++ .../test/integration/_harness/live.setup.ts | 7 + .../test/integration/_harness/network.ts | 63 ++++ .../test/integration/_harness/ownWallet.ts | 289 ++++++++++++++++++ .../test/integration/_harness/providers.ts | 56 ++++ .../_harness/registerSimulatorLive.ts | 154 ++++++++++ contracts/test/integration/_harness/wallet.ts | 17 ++ contracts/vitest.live.config.ts | 24 ++ local-env.yml | 61 ++++ 11 files changed, 787 insertions(+) create mode 100644 Makefile create mode 100644 contracts/test/integration/_harness/deploy.ts create mode 100644 contracts/test/integration/_harness/live.setup.ts create mode 100644 contracts/test/integration/_harness/network.ts create mode 100644 contracts/test/integration/_harness/ownWallet.ts create mode 100644 contracts/test/integration/_harness/providers.ts create mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts create mode 100644 contracts/test/integration/_harness/wallet.ts create mode 100644 contracts/vitest.live.config.ts create mode 100644 local-env.yml diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e5a601bf --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +COMPOSE_FILE := local-env.yml +LOGS_DIR := logs +SERVICES := proof-server indexer node + +.PHONY: env-up env-down env-logs env-logs-clean env-status + +## Start local environment and stream logs to logs/ +env-up: env-down + docker compose -f $(COMPOSE_FILE) up -d + @mkdir -p $(LOGS_DIR) + @for svc in $(SERVICES); do \ + docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \ + done + @echo "Logs streaming to $(LOGS_DIR)/" + +## Stop local environment +env-down: + @-pkill -f "docker compose -f $(COMPOSE_FILE) logs" 2>/dev/null || true + docker compose -f $(COMPOSE_FILE) down + +## Tail all logs +env-logs: + tail -f $(LOGS_DIR)/*.log + +## Clear log files +env-logs-clean: + rm -rf $(LOGS_DIR)/*.log + +## Show container status +env-status: + docker compose -f $(COMPOSE_FILE) ps diff --git a/contracts/package.json b/contracts/package.json index 193aa3ab..7796032d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,6 +34,7 @@ "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", "types": "tsc -p tsconfig.json --noEmit", diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts new file mode 100644 index 00000000..f9a1d283 --- /dev/null +++ b/contracts/test/integration/_harness/deploy.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + CompiledContract, + Contract as ContractNs, +} from '@midnight-ntwrk/compact-js'; +import { + type DeployContractOptionsWithPrivateState, + type DeployedContract, + deployContract, +} from '@midnight-ntwrk/midnight-js-contracts'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Absolute path to `contracts/artifacts//`. + * Used by `NodeZkConfigProvider`, which expects the directory containing + * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). + */ +export function moduleRootPath(moduleName: string): string { + // _harness/ is at contracts/test/integration/_harness/ + // module root at contracts/artifacts// + return path.resolve( + currentDir, + '..', + '..', + '..', + 'artifacts', + moduleName, + ); +} + +/** + * Absolute path to `contracts/artifacts//contract/` — where the + * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) + * live. Used by `CompiledContract.withCompiledFileAssets`. + */ +export function contractAssetsPath(moduleName: string): string { + return path.join(moduleRootPath(moduleName), 'contract'); +} + +/** + * Generic deploy wrapper. + * + * Each per-module fixture builds its own `CompiledContract` (because + * `witnesses` are module-specific) and passes it here along with providers, + * a private-state id, the initial private-state value, and the contract's + * constructor arguments — all properly typed via `Contract.*` helpers from + * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. + */ +export async function deployModule( + providers: MidnightProviders< + ContractNs.ProvableCircuitId, + string, + ContractNs.PrivateState + >, + // The third generic of `CompiledContract` (the witnesses map) defaults to + // `never` for empty-witness contracts; accept `any` so both shapes pass. + compiledContract: CompiledContract.CompiledContract< + C, + ContractNs.PrivateState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + >, + privateStateId: string, + initialPrivateState: ContractNs.PrivateState, + args: ContractNs.InitializeParameters, +): Promise> { + // The deployContract options shape is conditional on whether + // `Contract.InitializeParameters` is empty — TypeScript can't reduce + // that conditional under an unbounded `C extends Contract.Any`, so we + // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. + // Two-step cast (through `unknown`) because TS rejects the direct cast + // as "neither type sufficiently overlaps" — same conditional-resolution + // issue. Scoped to this single helper. + const options = { + compiledContract, + privateStateId, + initialPrivateState, + args, + } as unknown as DeployContractOptionsWithPrivateState; + return deployContract(providers, options); +} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts new file mode 100644 index 00000000..e5b05fec --- /dev/null +++ b/contracts/test/integration/_harness/live.setup.ts @@ -0,0 +1,7 @@ +import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; + +// Runs once per worker before the unit specs when `test:live` is used. Registers +// the live backend so `await Sim.create()` attaches to a freshly-deployed +// contract on the local stack (brought up by `make env-up`). On the dry path +// (default `test`) this file is not loaded, so nothing changes there. +registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts new file mode 100644 index 00000000..dacf5ebc --- /dev/null +++ b/contracts/test/integration/_harness/network.ts @@ -0,0 +1,63 @@ +import { + type NetworkId, + setNetworkId, +} from '@midnight-ntwrk/midnight-js-network-id'; + +/** + * Endpoint configuration for the local stack. Replaces testkit-js' + * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally + * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. + */ +export interface LocalNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly networkId: string; + readonly indexer: string; + readonly indexerWS: string; + readonly node: string; + readonly nodeWS: string; + readonly proofServer: string; + readonly faucet: string | undefined; +} + +/** + * Prefunded wallet mnemonic for the local `undeployed` network — the canonical + * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` + * recognises as the genesis-funded account. Inlined so the harness no longer + * depends on testkit-js' `TEST_MNEMONIC`. + */ +export const LOCAL_WALLET_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; + +/** + * Default endpoints for the local stack brought up by `make env-up`. + * Each is overridable via a MIDNIGHT_* env var so CI / other hosts + * can point the same harness at a relocated stack. + */ +export function networkConfig(): LocalNetworkConfig { + return { + walletNetworkId: 'undeployed' as NetworkId, + networkId: 'undeployed', + indexer: + process.env.MIDNIGHT_INDEXER_URL ?? + 'http://127.0.0.1:8088/api/v4/graphql', + indexerWS: + process.env.MIDNIGHT_INDEXER_WS_URL ?? + 'ws://127.0.0.1:8088/api/v4/graphql/ws', + node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', + nodeWS: 'ws://127.0.0.1:9944', + proofServer: + process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', + faucet: undefined, + }; +} + +/** + * Set the process-wide network id. Must be called once before any provider + * or wallet is constructed. Idempotent. + */ +let networkIdSet = false; +export function setupNetwork(): void { + if (networkIdSet) return; + setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); + networkIdSet = true; +} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts new file mode 100644 index 00000000..fd2a78df --- /dev/null +++ b/contracts/test/integration/_harness/ownWallet.ts @@ -0,0 +1,289 @@ +/** + * Own test wallet provider — a testkit-js-free reconstruction of the wallet + * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * WHY THIS EXISTS + * --------------- + * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s + * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy + * dependency (testcontainers, docker orchestration, a fixed env model) of which + * we use almost nothing — we run our own local stack via `make env-up`. All + * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter + * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. + * + * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, + * so the harness no longer imports testkit. It is a behavioural drop-in for the + * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. + * + * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: + * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed + * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk + * `balanceTx` / `submitTx` delegate to the facade identically to testkit. + * + * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, + * `shielded`) so a future coin-injecting shielded wallet can be slotted in via + * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks + * the spend-path (burn / round-trip) integration specs. See + * `NativeShieldedToken-tests.md` "Own wallet tool". + */ +import { + DustSecretKey, + LedgerParameters, + ZswapSecretKeys, +} from '@midnight-ntwrk/ledger-v8'; +import type { + FinalizedTransaction, + TransactionId, +} from '@midnight-ntwrk/midnight-js-protocol/ledger'; +import type { + MidnightProvider, + WalletProvider, +} from '@midnight-ntwrk/midnight-js-types'; +import { + createKeystore, + DustWallet, + HDWallet, + InMemoryTransactionHistoryStorage, + mergeWalletEntries, + PublicKey, + type Role, + Roles, + ShieldedWallet, + UnshieldedWallet, + WalletEntrySchema, + WalletFacade, +} from '@midnight-ntwrk/wallet-sdk'; +import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; +import { mnemonicToSeedSync } from '@scure/bip39'; +import pino, { type Logger } from 'pino'; + +/** + * Minimal endpoint config our wallet needs — a structural subset of the fields + * `networkConfig()` already returns, with no testkit type dependency. + */ +export interface OwnNetworkConfig { + readonly walletNetworkId: NetworkId; + readonly indexer: string; + readonly indexerWS: string; + readonly nodeWS: string; + readonly proofServer: string; +} + +/** + * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at + * preset-dev needs headroom to cover fees on undeployed; mirrors the value the + * old testkit-based `buildWallet` passed via `DustWalletOptions`. + */ +const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; + +let sharedLogger: Logger | undefined; +function ownLogger(): Logger { + if (!sharedLogger) { + sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); + } + return sharedLogger; +} + +/** The three role sub-seeds derived from a master seed, plus the master. */ +interface DerivedSeeds { + readonly masterSeedHex: string; + readonly shielded: Uint8Array; + readonly unshielded: Uint8Array; + readonly dust: Uint8Array; +} + +/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ +function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { + if (!masterSeedHex || masterSeedHex.length === 0) { + throw new Error('Own wallet: master seed cannot be empty'); + } + const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); + if (result.type !== 'seedOk') { + throw new Error('Own wallet: invalid seed, failed to create HD wallet'); + } + const derived = result.hdWallet + .selectAccount(0) + .selectRole(role) + .deriveKeyAt(0); + if (derived.type !== 'keyDerived') { + throw new Error(`Own wallet: key derivation failed for role ${role}`); + } + return derived.key; +} + +function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { + return { + masterSeedHex, + shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), + unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), + dust: deriveKeyForRole(masterSeedHex, Roles.Dust), + }; +} + +function seedsFromMnemonic(mnemonic: string): DerivedSeeds { + if (!mnemonic || mnemonic.trim().length === 0) { + throw new Error('Own wallet: mnemonic cannot be empty'); + } + return seedsFromMasterHex( + Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), + ); +} + +/** + * Map our endpoint config to the wallet-sdk facade configuration object. + * Shape lifted from testkit's `mapEnvironmentToConfiguration`. + */ +function facadeConfiguration(env: OwnNetworkConfig) { + return { + indexerClientConnection: { + indexerHttpUrl: env.indexer, + indexerWsUrl: env.indexerWS, + }, + provingServerUrl: new URL(env.proofServer), + networkId: env.walletNetworkId, + relayURL: new URL(env.nodeWS), + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + costParameters: { feeBlocksMargin: 5 }, + }; +} + +/** + * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, + * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the + * same delegations testkit's `MidnightWalletProvider` performed. + */ +export class OwnWalletProvider implements WalletProvider, MidnightProvider { + private constructor( + readonly env: OwnNetworkConfig, + readonly facade: WalletFacade, + readonly zswapSecretKeys: ZswapSecretKeys, + readonly dustSecretKey: DustSecretKey, + private readonly unshieldedKeystore: ReturnType, + private readonly logger: Logger, + ) {} + + getCoinPublicKey() { + return this.zswapSecretKeys.coinPublicKey; + } + + getEncryptionPublicKey() { + return this.zswapSecretKeys.encryptionPublicKey; + } + + async balanceTx( + tx: Parameters[0], + ttl: Date = ttlOneHour(), + ): Promise { + const recipe = await this.facade.balanceUnboundTransaction( + tx, + { + shieldedSecretKeys: this.zswapSecretKeys, + dustSecretKey: this.dustSecretKey, + }, + { ttl }, + ); + const signed = await this.facade.signRecipe(recipe, (payload) => + this.unshieldedKeystore.signData(payload), + ); + return this.facade.finalizeRecipe(signed); + } + + submitTx(tx: FinalizedTransaction): Promise { + return this.facade.submitTransaction(tx); + } + + async stop(): Promise { + await this.facade.stop(); + } + + /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ + static async build( + env: OwnNetworkConfig, + keyMaterial: { mnemonic: string } | { seedHex: string }, + options: { waitForFunds?: boolean } = {}, + ): Promise { + const logger = ownLogger(); + const seeds = + 'mnemonic' in keyMaterial + ? seedsFromMnemonic(keyMaterial.mnemonic) + : seedsFromMasterHex(keyMaterial.seedHex); + + const config = facadeConfiguration(env); + const unshieldedKeystore = createKeystore( + seeds.unshielded, + env.walletNetworkId, + ); + + const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); + const unshielded = UnshieldedWallet({ + ...config, + txHistoryStorage: new InMemoryTransactionHistoryStorage( + WalletEntrySchema, + mergeWalletEntries, + ), + }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); + + const dustConfig = { + ...config, + costParameters: { + ledgerParams: LedgerParameters.initialParameters(), + additionalFeeOverhead: + env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, + feeBlocksMargin: 5, + }, + }; + const dust = DustWallet(dustConfig).startWithSeed( + seeds.dust, + LedgerParameters.initialParameters().dust, + ); + + const facade = await WalletFacade.init({ + configuration: config, + shielded: () => shielded, + unshielded: () => unshielded, + dust: () => dust, + }); + + const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); + const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); + + logger.info('Own wallet: starting facade...'); + await facade.start(zswapSecretKeys, dustSecretKey); + if (options.waitForFunds ?? true) { + await waitForShieldedSync(facade, logger); + } + + return new OwnWalletProvider( + env, + facade, + zswapSecretKeys, + dustSecretKey, + unshieldedKeystore, + logger, + ); + } +} + +function ttlOneHour(): Date { + return new Date(Date.now() + 60 * 60 * 1000); +} + +/** + * Block until the shielded wallet reports a synced state. The facade's shielded + * API exposes a `waitForSyncedState`; fall back to a short settle if absent. + */ +async function waitForShieldedSync( + facade: WalletFacade, + logger: Logger, +): Promise { + const shielded = facade.shielded as { + waitForSyncedState?: (gap?: bigint) => Promise; + }; + if (typeof shielded.waitForSyncedState === 'function') { + await shielded.waitForSyncedState(); + logger.info('Own wallet: shielded state synced'); + } +} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts new file mode 100644 index 00000000..7a2cca7a --- /dev/null +++ b/contracts/test/integration/_harness/providers.ts @@ -0,0 +1,56 @@ +import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; +import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; +import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; +import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; +import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; +import type { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's + * artifact directory. Each module test passes its own `` so the + * ZK config provider reads that module's keys. + * + * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. + * + * @param wallet A started `TestWalletProvider` + * @param artifactPath Absolute path to `contracts/artifacts//contract` + * (the directory containing `contract-info.json` etc.) + * @param privateStateStoreName LevelDB namespace, unique per test contract + * @param circuitKeys Type parameter carrying the module's circuit union + */ +export function buildProviders< + CircuitKey extends string, + PrivateStateId extends string, + PrivateState, +>( + wallet: OwnWalletProvider, + artifactPath: string, + privateStateStoreName: string, +): MidnightProviders { + const zkConfigProvider = new NodeZkConfigProvider(artifactPath); + + const privateStateConfig = { + privateStateStoreName, + accountId: wallet.getCoinPublicKey(), + // Fixed test password: local/undeployed wallets don't need real entropy. + // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, + // min-length, mixed classes) deterministically across runs. + privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', + } as Parameters>[0]; + + return { + privateStateProvider: + levelPrivateStateProvider(privateStateConfig), + publicDataProvider: indexerPublicDataProvider( + wallet.env.indexer, + wallet.env.indexerWS, + ), + zkConfigProvider, + proofProvider: httpClientProofProvider( + wallet.env.proofServer, + zkConfigProvider, + ), + walletProvider: wallet, + midnightProvider: wallet, + }; +} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts new file mode 100644 index 00000000..bacbd094 --- /dev/null +++ b/contracts/test/integration/_harness/registerSimulatorLive.ts @@ -0,0 +1,154 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { CompiledContract } from '@midnight-ntwrk/compact-js'; +import { + type LiveBackendRequest, + type LiveContext, + registerLiveBackend, +} from '@openzeppelin/compact-simulator'; +import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; +import { + type LocalNetworkConfig, + networkConfig, + setupNetwork, +} from './network.js'; +import type { OwnWalletProvider } from './ownWallet.js'; +import { buildProviders } from './providers.js'; +import { buildWallet } from './wallet.js'; + +/** + * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local + * stack (`make env-up`). Registered once (from the `test:live` setup file); + * afterwards a migrated spec's `await Sim.create()` deploys the contract named by + * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the + * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. + * + * Deploy-per-`create()` gives each test a fresh contract (true isolation), which + * the unit specs assume (they rely on `beforeEach`-fresh state). + */ + +let env: LocalNetworkConfig | undefined; +let deployerPromise: Promise | undefined; +let deployCounter = 0; + +const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +function getDeployer(): Promise { + if (!env) { + setupNetwork(); + env = networkConfig(); + } + if (!deployerPromise) { + deployerPromise = (async () => { + const wallet = await buildWallet(env as LocalNetworkConfig); + // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, + // so also wait for the dust wallet to sync — otherwise the first tx builds + // a stale dust spend proof (node rejects with InvalidDustSpendProof). + const dust = ( + wallet.facade as { + dust?: { waitForSyncedState?: () => Promise }; + } + ).dust; + if (typeof dust?.waitForSyncedState === 'function') { + await dust.waitForSyncedState(); + } + return wallet; + })(); + } + return deployerPromise; +} + +async function buildLiveContext( + req: LiveBackendRequest, +): Promise> { + const name = req.config.artifactName; + if (!name) { + throw new Error( + 'live backend: SimulatorConfig.artifactName is required to deploy on live', + ); + } + + const contractEntry = pathToFileURL( + path.join(moduleRootPath(name), 'contract', 'index.js'), + ).href; + const mod = await import(contractEntry); + const ContractClass = mod.Contract; + + const witnesses = req.config.witnessesFactory(); + const compiled = CompiledContract.make(name, ContractClass).pipe( + CompiledContract.withWitnesses((witnesses ?? {}) as never), + CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), + ); + + const deployer = await getDeployer(); + const psId = `${name}-ps`; + const storeName = `${name}-${++deployCounter}`; + // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics + const providers = buildProviders( + deployer, + moduleRootPath(name), + storeName, + ) as any; + + const initialPS = + req.options.privateState ?? req.config.defaultPrivateState(); + const args = req.config.contractArgs(...req.contractArgs); + + // Retry: a freshly-started dev node may still be ramping dust generation, and + // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. + let deployed: Awaited> | undefined; + let lastErr: unknown; + for (let attempt = 1; attempt <= 8; attempt++) { + try { + // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific + deployed = await deployModule( + providers, + compiled, + psId, + initialPS, + args as any, + ); + break; + } catch (err) { + lastErr = err; + await sleep(5000); + } + } + if (!deployed) { + throw new Error( + `deploy of ${name} failed after retries: ${String(lastErr)}`, + ); + } + const address = deployed.deployTxData.public.contractAddress; + + return { + contractAddress: address, + handleFor: async () => ({ + // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle + callTx: deployed.callTx as any, + }), + async queryLedger() { + for (let attempt = 0; attempt < 15; attempt++) { + const cs = + await providers.publicDataProvider.queryContractState(address); + if (cs != null) return cs.data; + await sleep(400); + } + throw new Error(`no contract state at ${address} after retries`); + }, + async queryPrivateState() { + const ps = await providers.privateStateProvider.get(psId); + return ps ?? initialPS; + }, + }; +} + +let registered = false; + +/** Registers the live backend. Idempotent per worker. */ +export function registerSimulatorLiveBackend(): void { + if (registered) return; + registered = true; + registerLiveBackend((req) => buildLiveContext(req)); +} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts new file mode 100644 index 00000000..70e0f88b --- /dev/null +++ b/contracts/test/integration/_harness/wallet.ts @@ -0,0 +1,17 @@ +import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; +import { OwnWalletProvider } from './ownWallet.js'; + +/** + * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js + * dependency. `OwnWalletProvider` implements both `MidnightProvider` and + * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. + * + * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. + * Tests that need per-signer isolation pass their own BIP39 phrase. + */ +export async function buildWallet( + env: LocalNetworkConfig, + mnemonic: string = LOCAL_WALLET_MNEMONIC, +): Promise { + return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); +} diff --git a/contracts/vitest.live.config.ts b/contracts/vitest.live.config.ts new file mode 100644 index 00000000..52377aad --- /dev/null +++ b/contracts/vitest.live.config.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..fedb1fef --- /dev/null +++ b/local-env.yml @@ -0,0 +1,61 @@ +# WARNING: Insecure default credentials below. For local development only — do not use in production. +services: + proof-server: + image: 'midnightntwrk/proof-server:latest' + command: ['midnight-proof-server -v'] + ports: + - '6300:6300' + environment: + RUST_BACKTRACE: 'full' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:6300/version'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + + indexer: + image: 'midnightntwrk/indexer-standalone:latest' + ports: + - '8088:8088' + environment: + RUST_LOG: 'indexer=debug,chain_indexer=debug,indexer_api=debug,wallet_indexer=debug,indexer_common=debug,fastrace_opentelemetry=off,info' + APP__INFRA__NODE__URL: 'ws://node:9944' + APP__APPLICATION__NETWORK_ID: 'undeployed' + APP__INFRA__STORAGE__PASSWORD: 'indexer' + APP__INFRA__PUB_SUB__PASSWORD: 'indexer' + APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer' + APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132' + healthcheck: + test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running'] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + depends_on: + node: + condition: service_healthy + logging: + driver: local + options: + max-size: '10m' + max-file: '3' + + node: + image: 'midnightntwrk/midnight-node:0.22.2' + ports: + - '9944:9944' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9944/health'] + interval: 2s + timeout: 5s + retries: 20 + start_period: 5s + environment: + CFG_PRESET: 'dev' + SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e' + logging: + driver: local + options: + max-size: '10m' + max-file: '3' From a2b22b39c15ad45af6c43ac289b9afdd2d8d1e3c Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Thu, 25 Jun 2026 12:03:52 +0200 Subject: [PATCH 10/10] test: remove test/integration/_harness from the branch Drop the live-backend harness directory. Default dry CI is unaffected (it uses vitest.config.ts, not the live setup). --- contracts/test/integration/_harness/deploy.ts | 84 ----- .../test/integration/_harness/live.setup.ts | 7 - .../test/integration/_harness/network.ts | 63 ---- .../test/integration/_harness/ownWallet.ts | 289 ------------------ .../test/integration/_harness/providers.ts | 56 ---- .../_harness/registerSimulatorLive.ts | 154 ---------- contracts/test/integration/_harness/wallet.ts | 17 -- 7 files changed, 670 deletions(-) delete mode 100644 contracts/test/integration/_harness/deploy.ts delete mode 100644 contracts/test/integration/_harness/live.setup.ts delete mode 100644 contracts/test/integration/_harness/network.ts delete mode 100644 contracts/test/integration/_harness/ownWallet.ts delete mode 100644 contracts/test/integration/_harness/providers.ts delete mode 100644 contracts/test/integration/_harness/registerSimulatorLive.ts delete mode 100644 contracts/test/integration/_harness/wallet.ts diff --git a/contracts/test/integration/_harness/deploy.ts b/contracts/test/integration/_harness/deploy.ts deleted file mode 100644 index f9a1d283..00000000 --- a/contracts/test/integration/_harness/deploy.ts +++ /dev/null @@ -1,84 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - CompiledContract, - Contract as ContractNs, -} from '@midnight-ntwrk/compact-js'; -import { - type DeployContractOptionsWithPrivateState, - type DeployedContract, - deployContract, -} from '@midnight-ntwrk/midnight-js-contracts'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; - -const currentDir = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Absolute path to `contracts/artifacts//`. - * Used by `NodeZkConfigProvider`, which expects the directory containing - * `keys/` and `zkir/` (i.e. the module root, not the `contract/` subfolder). - */ -export function moduleRootPath(moduleName: string): string { - // _harness/ is at contracts/test/integration/_harness/ - // module root at contracts/artifacts// - return path.resolve( - currentDir, - '..', - '..', - '..', - 'artifacts', - moduleName, - ); -} - -/** - * Absolute path to `contracts/artifacts//contract/` — where the - * compiled `index.js`, `index.d.ts`, and `contract-info.json` (in compiler/) - * live. Used by `CompiledContract.withCompiledFileAssets`. - */ -export function contractAssetsPath(moduleName: string): string { - return path.join(moduleRootPath(moduleName), 'contract'); -} - -/** - * Generic deploy wrapper. - * - * Each per-module fixture builds its own `CompiledContract` (because - * `witnesses` are module-specific) and passes it here along with providers, - * a private-state id, the initial private-state value, and the contract's - * constructor arguments — all properly typed via `Contract.*` helpers from - * `@midnight-ntwrk/compact-js`, so callers don't need any escape casts. - */ -export async function deployModule( - providers: MidnightProviders< - ContractNs.ProvableCircuitId, - string, - ContractNs.PrivateState - >, - // The third generic of `CompiledContract` (the witnesses map) defaults to - // `never` for empty-witness contracts; accept `any` so both shapes pass. - compiledContract: CompiledContract.CompiledContract< - C, - ContractNs.PrivateState, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any - >, - privateStateId: string, - initialPrivateState: ContractNs.PrivateState, - args: ContractNs.InitializeParameters, -): Promise> { - // The deployContract options shape is conditional on whether - // `Contract.InitializeParameters` is empty — TypeScript can't reduce - // that conditional under an unbounded `C extends Contract.Any`, so we - // shape the literal once and assert it matches `DeployContractOptionsWithPrivateState`. - // Two-step cast (through `unknown`) because TS rejects the direct cast - // as "neither type sufficiently overlaps" — same conditional-resolution - // issue. Scoped to this single helper. - const options = { - compiledContract, - privateStateId, - initialPrivateState, - args, - } as unknown as DeployContractOptionsWithPrivateState; - return deployContract(providers, options); -} diff --git a/contracts/test/integration/_harness/live.setup.ts b/contracts/test/integration/_harness/live.setup.ts deleted file mode 100644 index e5b05fec..00000000 --- a/contracts/test/integration/_harness/live.setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerSimulatorLiveBackend } from './registerSimulatorLive.js'; - -// Runs once per worker before the unit specs when `test:live` is used. Registers -// the live backend so `await Sim.create()` attaches to a freshly-deployed -// contract on the local stack (brought up by `make env-up`). On the dry path -// (default `test`) this file is not loaded, so nothing changes there. -registerSimulatorLiveBackend(); diff --git a/contracts/test/integration/_harness/network.ts b/contracts/test/integration/_harness/network.ts deleted file mode 100644 index dacf5ebc..00000000 --- a/contracts/test/integration/_harness/network.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - type NetworkId, - setNetworkId, -} from '@midnight-ntwrk/midnight-js-network-id'; - -/** - * Endpoint configuration for the local stack. Replaces testkit-js' - * `EnvironmentConfiguration` — a plain struct of URLs we own, structurally - * compatible with `OwnWalletProvider`'s `OwnNetworkConfig`. - */ -export interface LocalNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly networkId: string; - readonly indexer: string; - readonly indexerWS: string; - readonly node: string; - readonly nodeWS: string; - readonly proofServer: string; - readonly faucet: string | undefined; -} - -/** - * Prefunded wallet mnemonic for the local `undeployed` network — the canonical - * BIP39 test seed ("abandon" × 23 + "diesel") that `midnight-node --preset=dev` - * recognises as the genesis-funded account. Inlined so the harness no longer - * depends on testkit-js' `TEST_MNEMONIC`. - */ -export const LOCAL_WALLET_MNEMONIC = - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon diesel'; - -/** - * Default endpoints for the local stack brought up by `make env-up`. - * Each is overridable via a MIDNIGHT_* env var so CI / other hosts - * can point the same harness at a relocated stack. - */ -export function networkConfig(): LocalNetworkConfig { - return { - walletNetworkId: 'undeployed' as NetworkId, - networkId: 'undeployed', - indexer: - process.env.MIDNIGHT_INDEXER_URL ?? - 'http://127.0.0.1:8088/api/v4/graphql', - indexerWS: - process.env.MIDNIGHT_INDEXER_WS_URL ?? - 'ws://127.0.0.1:8088/api/v4/graphql/ws', - node: process.env.MIDNIGHT_NODE_URL ?? 'http://127.0.0.1:9944', - nodeWS: 'ws://127.0.0.1:9944', - proofServer: - process.env.MIDNIGHT_PROOF_SERVER_URL ?? 'http://127.0.0.1:6300', - faucet: undefined, - }; -} - -/** - * Set the process-wide network id. Must be called once before any provider - * or wallet is constructed. Idempotent. - */ -let networkIdSet = false; -export function setupNetwork(): void { - if (networkIdSet) return; - setNetworkId((process.env.MIDNIGHT_NETWORK_ID ?? 'undeployed') as NetworkId); - networkIdSet = true; -} diff --git a/contracts/test/integration/_harness/ownWallet.ts b/contracts/test/integration/_harness/ownWallet.ts deleted file mode 100644 index fd2a78df..00000000 --- a/contracts/test/integration/_harness/ownWallet.ts +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Own test wallet provider — a testkit-js-free reconstruction of the wallet - * stack used by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * WHY THIS EXISTS - * --------------- - * The integration harness previously leaned on `@midnight-ntwrk/testkit-js`'s - * `MidnightWalletProvider` / `FluentWalletBuilder`. testkit is a heavy - * dependency (testcontainers, docker orchestration, a fixed env model) of which - * we use almost nothing — we run our own local stack via `make env-up`. All - * testkit gave us here was a thin `WalletProvider`/`MidnightProvider` adapter - * over `@midnight-ntwrk/wallet-sdk` plus seed-derivation glue. - * - * This module reproduces exactly that glue directly on `@midnight-ntwrk/wallet-sdk`, - * so the harness no longer imports testkit. It is a behavioural drop-in for the - * old `buildWallet()` (see wallet.ts) and `WalletPool` seed path. - * - * The construction mirrors testkit's `WalletFactory` / `FluentWalletBuilder`: - * seeds = role-derived sub-seeds from a BIP39 mnemonic or a raw 32-byte seed - * facade = WalletFacade.init({ shielded, unshielded, dust }) over wallet-sdk - * `balanceTx` / `submitTx` delegate to the facade identically to testkit. - * - * The provider deliberately exposes its internals (`facade`, `zswapSecretKeys`, - * `shielded`) so a future coin-injecting shielded wallet can be slotted in via - * `WalletFacade.init`'s custom `shielded` initialiser — the seam that unblocks - * the spend-path (burn / round-trip) integration specs. See - * `NativeShieldedToken-tests.md` "Own wallet tool". - */ -import { - DustSecretKey, - LedgerParameters, - ZswapSecretKeys, -} from '@midnight-ntwrk/ledger-v8'; -import type { - FinalizedTransaction, - TransactionId, -} from '@midnight-ntwrk/midnight-js-protocol/ledger'; -import type { - MidnightProvider, - WalletProvider, -} from '@midnight-ntwrk/midnight-js-types'; -import { - createKeystore, - DustWallet, - HDWallet, - InMemoryTransactionHistoryStorage, - mergeWalletEntries, - PublicKey, - type Role, - Roles, - ShieldedWallet, - UnshieldedWallet, - WalletEntrySchema, - WalletFacade, -} from '@midnight-ntwrk/wallet-sdk'; -import type { NetworkId } from '@midnight-ntwrk/midnight-js-network-id'; -import { mnemonicToSeedSync } from '@scure/bip39'; -import pino, { type Logger } from 'pino'; - -/** - * Minimal endpoint config our wallet needs — a structural subset of the fields - * `networkConfig()` already returns, with no testkit type dependency. - */ -export interface OwnNetworkConfig { - readonly walletNetworkId: NetworkId; - readonly indexer: string; - readonly indexerWS: string; - readonly nodeWS: string; - readonly proofServer: string; -} - -/** - * Wide fee overhead for the local `undeployed` network. Genesis-funded dust at - * preset-dev needs headroom to cover fees on undeployed; mirrors the value the - * old testkit-based `buildWallet` passed via `DustWalletOptions`. - */ -const UNDEPLOYED_FEE_OVERHEAD = 500_000_000_000_000_000n; - -let sharedLogger: Logger | undefined; -function ownLogger(): Logger { - if (!sharedLogger) { - sharedLogger = pino({ level: process.env.LOG_LEVEL ?? 'warn' }); - } - return sharedLogger; -} - -/** The three role sub-seeds derived from a master seed, plus the master. */ -interface DerivedSeeds { - readonly masterSeedHex: string; - readonly shielded: Uint8Array; - readonly unshielded: Uint8Array; - readonly dust: Uint8Array; -} - -/** Derive a role key the way testkit's `deriveKeyForRole` does (account 0, key 0). */ -function deriveKeyForRole(masterSeedHex: string, role: Role): Uint8Array { - if (!masterSeedHex || masterSeedHex.length === 0) { - throw new Error('Own wallet: master seed cannot be empty'); - } - const result = HDWallet.fromSeed(Buffer.from(masterSeedHex, 'hex')); - if (result.type !== 'seedOk') { - throw new Error('Own wallet: invalid seed, failed to create HD wallet'); - } - const derived = result.hdWallet - .selectAccount(0) - .selectRole(role) - .deriveKeyAt(0); - if (derived.type !== 'keyDerived') { - throw new Error(`Own wallet: key derivation failed for role ${role}`); - } - return derived.key; -} - -function seedsFromMasterHex(masterSeedHex: string): DerivedSeeds { - return { - masterSeedHex, - shielded: deriveKeyForRole(masterSeedHex, Roles.Zswap), - unshielded: deriveKeyForRole(masterSeedHex, Roles.NightExternal), - dust: deriveKeyForRole(masterSeedHex, Roles.Dust), - }; -} - -function seedsFromMnemonic(mnemonic: string): DerivedSeeds { - if (!mnemonic || mnemonic.trim().length === 0) { - throw new Error('Own wallet: mnemonic cannot be empty'); - } - return seedsFromMasterHex( - Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex'), - ); -} - -/** - * Map our endpoint config to the wallet-sdk facade configuration object. - * Shape lifted from testkit's `mapEnvironmentToConfiguration`. - */ -function facadeConfiguration(env: OwnNetworkConfig) { - return { - indexerClientConnection: { - indexerHttpUrl: env.indexer, - indexerWsUrl: env.indexerWS, - }, - provingServerUrl: new URL(env.proofServer), - networkId: env.walletNetworkId, - relayURL: new URL(env.nodeWS), - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - costParameters: { feeBlocksMargin: 5 }, - }; -} - -/** - * `WalletProvider` + `MidnightProvider` over a wallet-sdk `WalletFacade`, - * with no testkit dependency. `balanceTx`/`submitTx` are byte-for-byte the - * same delegations testkit's `MidnightWalletProvider` performed. - */ -export class OwnWalletProvider implements WalletProvider, MidnightProvider { - private constructor( - readonly env: OwnNetworkConfig, - readonly facade: WalletFacade, - readonly zswapSecretKeys: ZswapSecretKeys, - readonly dustSecretKey: DustSecretKey, - private readonly unshieldedKeystore: ReturnType, - private readonly logger: Logger, - ) {} - - getCoinPublicKey() { - return this.zswapSecretKeys.coinPublicKey; - } - - getEncryptionPublicKey() { - return this.zswapSecretKeys.encryptionPublicKey; - } - - async balanceTx( - tx: Parameters[0], - ttl: Date = ttlOneHour(), - ): Promise { - const recipe = await this.facade.balanceUnboundTransaction( - tx, - { - shieldedSecretKeys: this.zswapSecretKeys, - dustSecretKey: this.dustSecretKey, - }, - { ttl }, - ); - const signed = await this.facade.signRecipe(recipe, (payload) => - this.unshieldedKeystore.signData(payload), - ); - return this.facade.finalizeRecipe(signed); - } - - submitTx(tx: FinalizedTransaction): Promise { - return this.facade.submitTransaction(tx); - } - - async stop(): Promise { - await this.facade.stop(); - } - - /** Build a provider from a master seed (hex) or BIP39 mnemonic. */ - static async build( - env: OwnNetworkConfig, - keyMaterial: { mnemonic: string } | { seedHex: string }, - options: { waitForFunds?: boolean } = {}, - ): Promise { - const logger = ownLogger(); - const seeds = - 'mnemonic' in keyMaterial - ? seedsFromMnemonic(keyMaterial.mnemonic) - : seedsFromMasterHex(keyMaterial.seedHex); - - const config = facadeConfiguration(env); - const unshieldedKeystore = createKeystore( - seeds.unshielded, - env.walletNetworkId, - ); - - const shielded = ShieldedWallet(config).startWithSeed(seeds.shielded); - const unshielded = UnshieldedWallet({ - ...config, - txHistoryStorage: new InMemoryTransactionHistoryStorage( - WalletEntrySchema, - mergeWalletEntries, - ), - }).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)); - - const dustConfig = { - ...config, - costParameters: { - ledgerParams: LedgerParameters.initialParameters(), - additionalFeeOverhead: - env.walletNetworkId === 'undeployed' ? UNDEPLOYED_FEE_OVERHEAD : 0n, - feeBlocksMargin: 5, - }, - }; - const dust = DustWallet(dustConfig).startWithSeed( - seeds.dust, - LedgerParameters.initialParameters().dust, - ); - - const facade = await WalletFacade.init({ - configuration: config, - shielded: () => shielded, - unshielded: () => unshielded, - dust: () => dust, - }); - - const zswapSecretKeys = ZswapSecretKeys.fromSeed(seeds.shielded); - const dustSecretKey = DustSecretKey.fromSeed(seeds.dust); - - logger.info('Own wallet: starting facade...'); - await facade.start(zswapSecretKeys, dustSecretKey); - if (options.waitForFunds ?? true) { - await waitForShieldedSync(facade, logger); - } - - return new OwnWalletProvider( - env, - facade, - zswapSecretKeys, - dustSecretKey, - unshieldedKeystore, - logger, - ); - } -} - -function ttlOneHour(): Date { - return new Date(Date.now() + 60 * 60 * 1000); -} - -/** - * Block until the shielded wallet reports a synced state. The facade's shielded - * API exposes a `waitForSyncedState`; fall back to a short settle if absent. - */ -async function waitForShieldedSync( - facade: WalletFacade, - logger: Logger, -): Promise { - const shielded = facade.shielded as { - waitForSyncedState?: (gap?: bigint) => Promise; - }; - if (typeof shielded.waitForSyncedState === 'function') { - await shielded.waitForSyncedState(); - logger.info('Own wallet: shielded state synced'); - } -} diff --git a/contracts/test/integration/_harness/providers.ts b/contracts/test/integration/_harness/providers.ts deleted file mode 100644 index 7a2cca7a..00000000 --- a/contracts/test/integration/_harness/providers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider'; -import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider'; -import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider'; -import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider'; -import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types'; -import type { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build a fully-wired `MidnightProviders` bundle for a given compiled contract's - * artifact directory. Each module test passes its own `` so the - * ZK config provider reads that module's keys. - * - * Shape ported from midnight-apps/packages/lunarswap-cli/src/api/providers.ts. - * - * @param wallet A started `TestWalletProvider` - * @param artifactPath Absolute path to `contracts/artifacts//contract` - * (the directory containing `contract-info.json` etc.) - * @param privateStateStoreName LevelDB namespace, unique per test contract - * @param circuitKeys Type parameter carrying the module's circuit union - */ -export function buildProviders< - CircuitKey extends string, - PrivateStateId extends string, - PrivateState, ->( - wallet: OwnWalletProvider, - artifactPath: string, - privateStateStoreName: string, -): MidnightProviders { - const zkConfigProvider = new NodeZkConfigProvider(artifactPath); - - const privateStateConfig = { - privateStateStoreName, - accountId: wallet.getCoinPublicKey(), - // Fixed test password: local/undeployed wallets don't need real entropy. - // Chosen to satisfy `validatePassword` (no 3+ consecutive identical chars, - // min-length, mixed classes) deterministically across runs. - privateStoragePasswordProvider: () => 'Compact-Integration-Test-Pw!9', - } as Parameters>[0]; - - return { - privateStateProvider: - levelPrivateStateProvider(privateStateConfig), - publicDataProvider: indexerPublicDataProvider( - wallet.env.indexer, - wallet.env.indexerWS, - ), - zkConfigProvider, - proofProvider: httpClientProofProvider( - wallet.env.proofServer, - zkConfigProvider, - ), - walletProvider: wallet, - midnightProvider: wallet, - }; -} diff --git a/contracts/test/integration/_harness/registerSimulatorLive.ts b/contracts/test/integration/_harness/registerSimulatorLive.ts deleted file mode 100644 index bacbd094..00000000 --- a/contracts/test/integration/_harness/registerSimulatorLive.ts +++ /dev/null @@ -1,154 +0,0 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { CompiledContract } from '@midnight-ntwrk/compact-js'; -import { - type LiveBackendRequest, - type LiveContext, - registerLiveBackend, -} from '@openzeppelin/compact-simulator'; -import { contractAssetsPath, deployModule, moduleRootPath } from './deploy.js'; -import { - type LocalNetworkConfig, - networkConfig, - setupNetwork, -} from './network.js'; -import type { OwnWalletProvider } from './ownWallet.js'; -import { buildProviders } from './providers.js'; -import { buildWallet } from './wallet.js'; - -/** - * Wires the `@openzeppelin/compact-simulator` live backend to this repo's local - * stack (`make env-up`). Registered once (from the `test:live` setup file); - * afterwards a migrated spec's `await Sim.create()` deploys the contract named by - * `SimulatorConfig.artifactName`, attaches, and returns a `LiveContext` — so the - * same spec file runs unchanged on both `MIDNIGHT_BACKEND=dry` and `=live`. - * - * Deploy-per-`create()` gives each test a fresh contract (true isolation), which - * the unit specs assume (they rely on `beforeEach`-fresh state). - */ - -let env: LocalNetworkConfig | undefined; -let deployerPromise: Promise | undefined; -let deployCounter = 0; - -const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -function getDeployer(): Promise { - if (!env) { - setupNetwork(); - env = networkConfig(); - } - if (!deployerPromise) { - deployerPromise = (async () => { - const wallet = await buildWallet(env as LocalNetworkConfig); - // `buildWallet` waits for *shielded* sync; the deploy tx is paid in DUST, - // so also wait for the dust wallet to sync — otherwise the first tx builds - // a stale dust spend proof (node rejects with InvalidDustSpendProof). - const dust = ( - wallet.facade as { - dust?: { waitForSyncedState?: () => Promise }; - } - ).dust; - if (typeof dust?.waitForSyncedState === 'function') { - await dust.waitForSyncedState(); - } - return wallet; - })(); - } - return deployerPromise; -} - -async function buildLiveContext( - req: LiveBackendRequest, -): Promise> { - const name = req.config.artifactName; - if (!name) { - throw new Error( - 'live backend: SimulatorConfig.artifactName is required to deploy on live', - ); - } - - const contractEntry = pathToFileURL( - path.join(moduleRootPath(name), 'contract', 'index.js'), - ).href; - const mod = await import(contractEntry); - const ContractClass = mod.Contract; - - const witnesses = req.config.witnessesFactory(); - const compiled = CompiledContract.make(name, ContractClass).pipe( - CompiledContract.withWitnesses((witnesses ?? {}) as never), - CompiledContract.withCompiledFileAssets(contractAssetsPath(name)), - ); - - const deployer = await getDeployer(); - const psId = `${name}-ps`; - const storeName = `${name}-${++deployCounter}`; - // biome-ignore lint/suspicious/noExplicitAny: harness threads opaque midnight-js generics - const providers = buildProviders( - deployer, - moduleRootPath(name), - storeName, - ) as any; - - const initialPS = - req.options.privateState ?? req.config.defaultPrivateState(); - const args = req.config.contractArgs(...req.contractArgs); - - // Retry: a freshly-started dev node may still be ramping dust generation, and - // the dust wallet's view can lag, yielding a transient InvalidDustSpendProof. - let deployed: Awaited> | undefined; - let lastErr: unknown; - for (let attempt = 1; attempt <= 8; attempt++) { - try { - // biome-ignore lint/suspicious/noExplicitAny: args shape is contract-specific - deployed = await deployModule( - providers, - compiled, - psId, - initialPS, - args as any, - ); - break; - } catch (err) { - lastErr = err; - await sleep(5000); - } - } - if (!deployed) { - throw new Error( - `deploy of ${name} failed after retries: ${String(lastErr)}`, - ); - } - const address = deployed.deployTxData.public.contractAddress; - - return { - contractAddress: address, - handleFor: async () => ({ - // biome-ignore lint/suspicious/noExplicitAny: callTx is the midnight-js handle - callTx: deployed.callTx as any, - }), - async queryLedger() { - for (let attempt = 0; attempt < 15; attempt++) { - const cs = - await providers.publicDataProvider.queryContractState(address); - if (cs != null) return cs.data; - await sleep(400); - } - throw new Error(`no contract state at ${address} after retries`); - }, - async queryPrivateState() { - const ps = await providers.privateStateProvider.get(psId); - return ps ?? initialPS; - }, - }; -} - -let registered = false; - -/** Registers the live backend. Idempotent per worker. */ -export function registerSimulatorLiveBackend(): void { - if (registered) return; - registered = true; - registerLiveBackend((req) => buildLiveContext(req)); -} diff --git a/contracts/test/integration/_harness/wallet.ts b/contracts/test/integration/_harness/wallet.ts deleted file mode 100644 index 70e0f88b..00000000 --- a/contracts/test/integration/_harness/wallet.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LOCAL_WALLET_MNEMONIC, type LocalNetworkConfig } from './network.js'; -import { OwnWalletProvider } from './ownWallet.js'; - -/** - * Build (and start) a wallet provider from a BIP39 mnemonic, with no testkit-js - * dependency. `OwnWalletProvider` implements both `MidnightProvider` and - * `WalletProvider` expected by `@midnight-ntwrk/midnight-js-contracts#deployContract`. - * - * Default mnemonic is the prefunded genesis account on `midnight-node --preset=dev`. - * Tests that need per-signer isolation pass their own BIP39 phrase. - */ -export async function buildWallet( - env: LocalNetworkConfig, - mnemonic: string = LOCAL_WALLET_MNEMONIC, -): Promise { - return OwnWalletProvider.build(env, { mnemonic }, { waitForFunds: true }); -}