Skip to content

Upgrade Simulator, Contracts to 0.29.0#366

Open
emnul wants to merge 71 commits intomainfrom
upgrade-contracts-versions-0.28.0
Open

Upgrade Simulator, Contracts to 0.29.0#366
emnul wants to merge 71 commits intomainfrom
upgrade-contracts-versions-0.28.0

Conversation

@emnul
Copy link
Contributor

@emnul emnul commented Feb 3, 2026

This PR is part of #364 .

  • renames from variables to fromAddress to avoid new keyword collisions.
  • Updates language version across all contracts
  • Formats contracts using compact tool ( side effect of compact fixup )
  • Hard codes a temporary workaround in the compact compiler tool to avoid compilation of the archive contracts. It should be removed in Add new compiler flag to skip directories #367
  • Updates Simulators to use new createSimulator factory function
  • Removes deprecated simulator interface files
  • Updates address.ts files
  • Updates tests to use .as(ROLE) syntax
  • Refactors encodeToAddress to catch changes to ContractAddress requirements and meet new type expectations
  • Excludes archive contracts from tests

Summary by CodeRabbit

  • New Features

    • Added support for hierarchical artifact output and custom source/output directories in the Compact compiler.
    • Enhanced test setup automation with pre-test compilation.
  • Bug Fixes

    • Updated parameter naming for consistency in token transfer operations.
  • Chores

    • Bumped language version requirements to 0.21.0 across contracts.
    • Updated compiler version to 0.29.0.
    • Modernized simulator architecture and refactored test utilities.
    • Updated dependencies: compact-runtime to 0.14.0, ledger-v7 to 7.0.0.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Comprehensive upgrade and refactoring across the Compact smart contracts framework: bumps language version to 0.21.0, restructures simulator architecture using base factory pattern, renames token transfer parameters from from to fromAddress, updates runtime/ledger dependencies, and converts witness/private state objects to factory functions with generators.

Changes

Cohort / File(s) Summary
Configuration & Versioning
.github/ISSUE_TEMPLATE/01_bug_report.yml, .github/actions/setup/action.yml, package.json, packages/compact/package.json
Updated default Compact version from 0.26.0 to 0.29.0 in templates and builds; bumped compact-runtime to 0.14.0; replaced ledger with ledger-v7 7.0.0; updated package filter names from @openzeppelin-compact/contracts to @openzeppelin/compact-contracts.
Compact Language Pragma Updates
contracts/src/access/AccessControl.compact, contracts/src/access/Ownable.compact, contracts/src/access/ZOwnablePK.compact, contracts/src/access/test/mocks/Mock*.compact, contracts/src/archive/ShieldedToken.compact, contracts/src/archive/test/mocks/MockShieldedToken.compact, contracts/src/security/...compact, contracts/src/token/.../...compact, contracts/src/utils/...compact, packages/simulator/test/fixtures/sample-contracts/...compact
Bumped language_version pragma from 0.18.0 to 0.21.0 across all Compact contract files; minor formatting adjustments in multi-line signatures.
Token Parameter Renaming: fromfromAddress
contracts/src/token/FungibleToken.compact, contracts/src/token/MultiToken.compact, contracts/src/token/NonFungibleToken.compact, contracts/src/token/test/mocks/Mock*.compact
Systematically renamed first transfer parameter across all token contracts from from to fromAddress to improve semantic clarity; updated all internal call sites and balance updates.
Test Access Pattern Refactoring
contracts/src/access/test/AccessControl.test.ts, contracts/src/access/test/Ownable.test.ts, contracts/src/token/test/FungibleToken.test.ts, contracts/src/token/test/MultiToken.test.ts, contracts/src/token/test/nonFungibleToken.test.ts
Replaced direct method calls with context-wrapped pattern: method(args, caller)as(caller).method(args), eliminating explicit caller parameters in favor of fluent context binding.
Address Utilities Updates
contracts/src/access/test/utils/address.ts, contracts/src/archive/test/utils/address.ts, contracts/src/security/test/utils/address.ts, contracts/src/token/test/utils/address.ts, contracts/src/utils/test/utils/address.ts, packages/simulator/test/fixtures/utils/address.ts
Updated encodeContractAddress imports from ledger to ledger-v7; added runtime validation via isContractAddress; introduced generatePubKeyPair, generateEitherPubKeyPair, and zeroUint8Array helpers; updated return types to EncodedContractAddress.
Witness Factory Refactoring
contracts/src/access/witnesses/AccessControlWitnesses.ts, contracts/src/access/witnesses/OwnableWitnesses.ts, contracts/src/access/witnesses/ZOwnablePKWitnesses.ts, contracts/src/security/witnesses/InitializableWitnesses.ts, contracts/src/security/witnesses/PausableWitnesses.ts, contracts/src/token/witnesses/...Witnesses.ts, contracts/src/utils/witnesses/UtilsWitnesses.ts
Converted witness exports from static empty objects to factory functions returning witness interfaces; added PrivateState.generate() utility methods; introduced IWitnesses generic type interfaces.
Simulator Base Architecture Refactoring
contracts/src/access/test/simulators/AccessControlSimulator.ts, contracts/src/access/test/simulators/OwnableSimulator.ts, contracts/src/security/test/simulators/InitializableSimulator.ts, contracts/src/security/test/simulators/PausableSimulator.ts, contracts/src/token/test/simulators/FungibleTokenSimulator.ts, contracts/src/token/test/simulators/MultiTokenSimulator.ts, contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts, contracts/src/utils/test/simulators/UtilsSimulator.ts
Replaced manual contract-context wiring with createSimulator base factory pattern; removed explicit circuit context management in favor of circuits.pure/impure delegation; added privateState field; updated constructors to accept BaseSimulatorOptions.
Removed Test Utilities & Interfaces
contracts/src/access/test/types/test.ts, contracts/src/access/test/utils/AbstractContractSimulator.ts, contracts/src/access/test/utils/SimualatorStateManager.ts, contracts/src/access/test/utils/createCircuitProxies.ts, contracts/src/access/test/utils/test.ts, contracts/src/security/test/types/test.ts, contracts/src/security/test/utils/test.ts, contracts/src/token/test/types/test.ts, contracts/src/token/test/utils/test.ts, contracts/src/utils/test/types/test.ts, contracts/src/utils/test/utils/test.ts
Removed legacy IContractSimulator interface, AbstractContractSimulator base class, and CircuitContext construction utilities (useCircuitContext, useCircuitContextSender); these are replaced by the new base simulator factory pattern.
Import Path Normalization
contracts/src/access/test/ZOwnablePK.test.ts, contracts/src/archive/test/ShieldedToken.test.ts, contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts, contracts/src/archive/test/utils/address.ts, packages/simulator/test/fixtures/sample-contracts/witnesses/...Witnesses.ts, packages/simulator/test/integration/..., packages/simulator/test/fixtures/utils/address.ts
Systematically updated artifact imports from index.cjs to index.js; aligns with ESM module format across all test and simulator files.
Compiler & Builder Refactoring
packages/compact/src/Compiler.ts, packages/compact/src/Builder.ts, packages/compact/src/runCompiler.ts, packages/compact/src/runBuilder.ts, packages/compact/test/Compiler.test.ts, packages/compact/test/runCompiler.test.ts
Introduced CompilerOptions interface replacing discrete constructor parameters; added hierarchical artifact output support; updated CLI flags (--src, --out, --hierarchical); replaced version constants with options-based configuration; refactored fromArgs to parseArgs factory.
Simulator Core Architecture
packages/simulator/src/core/AbstractSimulator.ts, packages/simulator/src/core/CircuitContextManager.ts, packages/simulator/src/core/ContractSimulator.ts, packages/simulator/src/factory/createSimulator.ts, packages/simulator/src/factory/SimulatorConfig.ts, packages/simulator/src/types/Simulator.ts, packages/simulator/src/utils/CircuitContextUtils.ts
Updated CircuitContext to use currentQueryContext instead of originalState/transactionContext; replaced ContractState with StateValue; introduced ChargedState parameter; added cost model tracking; enhanced type parameters with TContract generic for type-safe contracts.
Test Fixtures Cleanup
packages/simulator/test/fixtures/test-artifacts/SampleZOwnable/..., packages/simulator/test/fixtures/test-artifacts/Simple/..., packages/simulator/test/fixtures/test-artifacts/Witness/...
Removed pre-compiled test artifact files (contract/index.cjs, index.d.cts, compiler/contract-info.json, zkir/*.zkir) in favor of runtime compilation via setup.ts.
Test Setup & Integration
packages/simulator/test/setup.ts, packages/simulator/vitest.config.ts, packages/simulator/test/integration/SampleZOwnableSimulator.ts, packages/simulator/test/integration/SimpleSimulator.ts, packages/simulator/test/integration/WitnessSimulator.ts, packages/simulator/test/unit/core/StateManager.test.ts
Added global Vitest setup hook to compile sample contracts before tests; updated integration tests to use new artifact paths and explicit generic type parameters; replaced legacy state/coin types with shielded equivalents; added currentQueryContext references.
Sample Contract Updates
packages/simulator/test/fixtures/sample-contracts/SampleZOwnable.compact, packages/simulator/test/fixtures/sample-contracts/Simple.compact, packages/simulator/test/fixtures/sample-contracts/Witness.compact
Updated language pragmas and replaced broad CompactStandardLibrary imports with targeted named imports; added test-context header comments.
Documentation & Build Config
packages/compact/README.md, packages/simulator/README.md, contracts/vitest.config.ts, turbo.json
Added comprehensive CLI documentation for compact tools; updated example imports to use .js paths; excluded archive tests from test suite; removed compact:archive dependency from consolidated compact task.
Version & Utility Cleanup
packages/compact/src/versions.ts
Removed exported COMPACT_VERSION and LANGUAGE_VERSION constants (now supplied via options).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • 0xisk
  • andrew-fleming

🐰 A grand refactoring hops through the code,
From circuits old to bases new encoded,
With fromAddress clarity, witnesses grow wise,
The simulators spring forward with factory-based cries!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Upgrade Simulator, Contracts to 0.29.0' directly and clearly summarizes the primary change—upgrading core simulator and contract components to version 0.29.0, which aligns with the file summaries showing language version bumps from 0.18.0/0.26.0 to 0.21.0/0.29.0.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch upgrade-contracts-versions-0.28.0

Comment @coderabbitai help to get the list of available commands and usage tips.

@emnul emnul changed the title Update language version across contracts Upgrade Contracts to 0.28.0 Feb 4, 2026
@emnul emnul marked this pull request as ready for review February 4, 2026 03:42
@emnul emnul requested review from a team as code owners February 4, 2026 03:42
@emnul emnul marked this pull request as draft February 4, 2026 04:32
@emnul emnul changed the title Upgrade Simulator, Contracts to 0.28.0 Upgrade Simulator, Contracts to 0.29.0 Feb 11, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/compact/src/Builder.ts (1)

13-107: ⚠️ Potential issue | 🟠 Major

Builder still accepts flags despite BuilderOptions omitting them.
BuilderOptions excludes flags, but the constructor/fromArgs still accept CompilerOptions and pass flags through to CompactCompiler, allowing --skip-zk to slip in. If builds must always include ZK proofs, strip or reject flags and align the examples.

🔧 Suggested fix (type + flag stripping)
-  constructor(options: CompilerOptions = {}) {
+  constructor(options: BuilderOptions = {}) {
     this.options = options;
   }

   static fromArgs(
     args: string[],
     env: NodeJS.ProcessEnv = process.env,
   ): CompactBuilder {
-    const options = CompactCompiler.parseArgs(args, env);
-    return new CompactBuilder(options);
+    const { flags: _flags, ...builderOptions } =
+      CompactCompiler.parseArgs(args, env);
+    return new CompactBuilder(builderOptions);
   }
packages/compact/src/runCompiler.ts (1)

133-140: ⚠️ Potential issue | 🟡 Minor

Handle --src/--out parse errors the same way as --dir.
parseArgs now throws for missing --src/--out values, but those fall into the unexpected-error path and skip usage help.

🔧 Suggested fix
-  if (errorMessage.includes('--dir flag requires a directory name')) {
-    spinner.fail(
-      chalk.red('[COMPILE] Error: --dir flag requires a directory name'),
-    );
+  if (
+    errorMessage.includes('--dir flag requires a directory name') ||
+    errorMessage.includes('--src flag requires a directory path') ||
+    errorMessage.includes('--out flag requires a directory path')
+  ) {
+    spinner.fail(chalk.red(`[COMPILE] Error: ${errorMessage}`));
     showUsageHelp();
     return;
   }
🤖 Fix all issues with AI agents
In `@contracts/src/token/FungibleToken.compact`:
- Line 519: Implement the missing shieldedBurnAddress helper inside the Utils
module and export it with the project's existing naming convention, then update
all call sites to use that exported name; specifically, add a function in
Utils.compact (e.g., export as Utils_shieldedBurnAddress or if your Utils module
exposes an object, Utils_.shieldedBurnAddress) that returns the burn address
(matching expected return type used by FungibleToken.compact,
NonFungibleToken.compact, MultiToken.compact, Ownable.compact) and replace the
bare shieldedBurnAddress() calls in those files with the new Utils-prefixed
identifier.

In `@contracts/src/token/test/MultiToken.test.ts`:
- Around line 511-529: The test sets approvals for two different operators
(Z_SPENDER and Z_OTHER) but both transfers call ._unsafeTransferFrom via
token.as(caller) so Z_OTHER is never used; update the transfers to perform the
first transfer using token.as(Z_SPENDER)._unsafeTransferFrom(...) and the second
transfer using token.as(Z_OTHER)._unsafeTransferFrom(...) (keeping the existing
_mint, setApprovalForAll, and balanceOf assertions) so the test exercises two
distinct approved operators.
- Around line 306-324: The test mistakenly uses the same operator for both
transfers so it never verifies a second distinct spender; update the second
transfer to use the other approved spender (replace the second
token.as(SPENDER).transferFrom(...) call with token.as(Z_OTHER or an OTHER
constant).transferFrom(...)) and ensure an OTHER constant is defined if needed;
keep the approvals via token.as(caller).setApprovalForAll(Z_SPENDER, true) and
token.as(caller).setApprovalForAll(Z_OTHER, true) and the initial mint via
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n) unchanged.

In `@contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts`:
- Around line 65-71: Update the JSDoc for the symbol() method in
NonFungibleTokenSimulator: change the `@returns` description from "The token
name." to "The token symbol." so it matches the `@description`. Locate the public
symbol(): string { return this.circuits.impure.symbol(); } method and adjust its
docblock accordingly.

In `@contracts/src/token/test/utils/address.ts`:
- Around line 36-46: The function encodeToAddress currently passes a hex string
to isContractAddress but the API expects an Either<ZswapCoinPublicKey,
ContractAddress>; change the check to construct or obtain that Either (e.g. use
createEitherTestContractAddress(generatedAddress) or the appropriate Either
factory) and pass that Either into isContractAddress, then if valid call
encodeContractAddress on the ContractAddress contained in the Either (extracting
the right value) before returning bytes; keep the existing error throw for
invalid inputs and preserve use of toHexPadded and encodeContractAddress.

In `@packages/compact/README.md`:
- Line 26: Update the example version strings that currently read "Compactc
version: 0.29.0" to "Compactc version: 0.28.0" so the README matches the PR
target (upgrade-contracts-versions-0.28.0); search for occurrences of the
literal "Compactc version: 0.29.0" in the README (including the instances shown
in the diff and around lines indicated) and replace each with "Compactc version:
0.28.0".

In `@packages/compact/src/Builder.ts`:
- Around line 66-83: The build steps in Builder (the steps array) hardcode paths
like "src/artifacts" and "dist", causing mismatch with configured src/out
directories; update Builder to derive those copy/move commands from the instance
options (e.g., use options.srcDir and options.outDir or the compiler-resolved
directories) and replace the hardcoded paths in the steps entries for "Copying
artifacts" and "Copying and cleaning .compact files" so the shell commands
reference the resolved src/out locations; ensure the mkdir/cp/find commands use
the derived paths and preserve the existing shell flags and fallbacks.

In `@packages/simulator/test/fixtures/utils/address.ts`:
- Around line 32-43: encodeToAddress may produce oversized hex when
toHexPadded(str) returns >64 chars; ensure you clamp the payload explicitly
before prefixing: compute hex = toHexPadded(str), then truncate/clamp hex to at
most 60 hex characters (30 bytes) for the part that you will prepend
PREFIX_ADDRESS to, so that PREFIX_ADDRESS + truncated is always 64 hex chars;
keep using encodeContractAddress on the resulting fixed-length string and
reference encodeToAddress, toHexPadded, PREFIX_ADDRESS, and
encodeContractAddress when making the change.

In `@packages/simulator/test/setup.ts`:
- Around line 26-45: The current isSpawnSyncRet type guard requires
SpawnSyncReturns-specific fields (error, status) which prometified exec errors
don't have; update isSpawnSyncRet to return true for either the SpawnSyncReturns
shape or the exec-style error shape by checking stdout/stderr (string|Buffer)
plus one of: (typedErr.error instanceof Error) OR exec-style properties like
typeof typedErr.code === 'number' || typeof typedErr.code === 'string' ||
typedErr.code === null, typeof typedErr.killed === 'boolean', and (typeof
typedErr.signal === 'string' || typedErr.signal === null) and optionally typeof
typedErr.cmd === 'string'; use these symbol names (isSpawnSyncRet,
SpawnSyncReturns, promisify(exec)) to locate and replace the guard logic so exec
rejection objects are recognized and handled instead of re-thrown.

In `@packages/simulator/test/unit/core/StateManager.test.ts`:
- Around line 122-125: Replace the spread+cast pattern that loses WASM backing
by constructing a proper QueryContext instance: instead of creating
modifiedTxCtx with {...ctx.currentQueryContext, address:
encodeToAddress('otherAddress')} as QueryContext, call the QueryContext
constructor with the original state and the new address (use
ctx.currentQueryContext.state and encodeToAddress('otherAddress')) so the
resulting object retains the WASM __wbg_ptr backing.
🧹 Nitpick comments (9)
contracts/src/archive/test/ShieldedToken.test.ts (1)

55-63: Nitpick: Duplicate assertion for ShieldedToken__counter.

Lines 58 and 62 both check the same value (ShieldedToken__counter === 0n). Consider removing the duplicate.

🔧 Suggested fix
     it('should set public state', () => {
       token = new ShieldedTokenSimulator(NONCE, NAME, SYMBOL, DECIMALS);

       expect(token.getCurrentPublicState().ShieldedToken__counter).toEqual(0n);
       expect(token.getCurrentPublicState().ShieldedToken__domain).toEqual(
         DOMAIN,
       );
-      expect(token.getCurrentPublicState().ShieldedToken__counter).toEqual(0n);
     });
contracts/src/token/test/MultiToken.test.ts (1)

219-228: Inconsistent use of .as(caller) pattern in setApprovalForAll tests.

Lines 220, 223, and 226 call token.setApprovalForAll(...) directly without the .as(caller) wrapper, while the beforeEach at line 212 and other tests in this file use .as(caller). This inconsistency may cause the test to rely on implicit simulator state rather than explicitly setting the caller context.

Suggested fix for consistency
       it('should unset → set → unset operator', () => {
-          token.setApprovalForAll(Z_SPENDER, false);
+          token.as(caller).setApprovalForAll(Z_SPENDER, false);
           expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false);

-          token.setApprovalForAll(Z_SPENDER, true);
+          token.as(caller).setApprovalForAll(Z_SPENDER, true);
           expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true);

-          token.setApprovalForAll(Z_SPENDER, false);
+          token.as(caller).setApprovalForAll(Z_SPENDER, false);
           expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false);
         });
packages/compact/README.md (1)

67-77: Consider adding language specification to code fences.

The directory structure code blocks lack language specification, which some Markdown linters flag. Consider adding text or plaintext markers for better tooling compatibility:

📝 Suggested formatting improvement

For the first block:

-```
+```text
 src/
   access/
     AccessControl.compact

Apply the same pattern to the second directory tree block at lines 81-87.

Also applies to: 81-87

packages/simulator/test/unit/core/StateManager.test.ts (1)

67-78: Remove assertions on __wbg_ptr implementation detail.
__wbg_ptr is a WebAssembly binding internal pointer. Testing for its presence and type makes tests fragile across library updates without providing functional validation. The existing assertions on instance types and public properties (address, state) are sufficient.

Proposed simplification
     it('should set original state', () => {
       expect(ctx.currentQueryContext).toBeInstanceOf(QueryContext);
-      expect(ctx.currentQueryContext).toHaveProperty('__wbg_ptr');
-      expect((ctx.currentQueryContext as any).__wbg_ptr).toBeTypeOf('number');
     });

     it('should set tx ctx', () => {
       // Need to go deeper
       expect(ctx.currentQueryContext).toBeInstanceOf(QueryContext);
       expect(ctx.currentQueryContext.address).toEqual(dummyContractAddress());
       expect(ctx.currentQueryContext.state).toBeInstanceOf(ChargedState);
-      expect(ctx.currentQueryContext.state).toHaveProperty('__wbg_ptr');
     });
.github/ISSUE_TEMPLATE/01_bug_report.yml (1)

95-97: Consider adding previous versions to the dropdown.

The dropdown only contains the new default version. Users encountering bugs on older versions won't be able to accurately report which version they're using. Consider keeping a few recent versions as options.

💡 Suggested improvement
       options:
         - 0.29.0 (Default)
+        - 0.28.0
+        - 0.26.0
       default: 0
packages/simulator/test/setup.ts (1)

71-86: Consider adding a timeout to the exec call.

The execAsync(command) call has no timeout, which could cause the test setup to hang indefinitely if the compact tool stalls. This can block CI pipelines.

⏱️ Proposed fix to add timeout
-  const command = `compact compile --skip-zk "${inputPath}" "${outputDir}"`;
+  const command = `compact compile --skip-zk "${inputPath}" "${outputDir}"`;
+  const COMPILE_TIMEOUT_MS = 60_000; // 60 seconds
   try {
-    await execAsync(command);
+    await execAsync(command, { timeout: COMPILE_TIMEOUT_MS });
   } catch (err: unknown) {
contracts/src/archive/test/utils/address.ts (1)

37-41: Add a comment explaining why the archive version uses a different encoding approach.

The archive implementation at lines 37-41 is indeed different from the four other encodeToAddress implementations in the codebase (token, utils, access, and security modules). Those all use toHexPadded() with isContractAddress validation and throw on invalid input. The archive version uniquely uses Buffer.from(str, 'ascii').toString('hex') with PREFIX_ADDRESS and skips validation entirely. Consider adding an inline comment documenting whether this difference is intentional for backward compatibility with legacy contract data.

contracts/src/security/test/utils/address.ts (1)

1-109: Consider consolidating duplicate address utility files.

This file is nearly identical to contracts/src/token/test/utils/address.ts. Consider extracting common utilities to a shared location to reduce maintenance burden. However, this may be intentional for test module isolation.

contracts/src/token/test/simulators/NonFungibleTokenSimulator.ts (1)

450-452: Consider adding explicit type annotation for privateState.

The property lacks an explicit type annotation, so TypeScript infers it as {}. For consistency with NonFungibleTokenPrivateState (which is Record<string, never>), consider adding the type explicitly.

📝 Proposed fix
-  public readonly privateState = {};
+  public readonly privateState: NonFungibleTokenPrivateState = {};

Comment on lines 306 to 324
it('should handle concurrent operations on same token ID', () => {
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n);

// Set up two spenders
token.setApprovalForAll(Z_SPENDER, true, caller);
token.setApprovalForAll(Z_OTHER, true, caller);
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);

// First spender transfers half
token.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT, SPENDER);
token
.as(SPENDER)
.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);
expect(token.balanceOf(Z_RECIPIENT, TOKEN_ID)).toEqual(AMOUNT);

// Second spender transfers remaining
token.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT, SPENDER);
token
.as(SPENDER)
.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);
expect(token.balanceOf(Z_RECIPIENT, TOKEN_ID)).toEqual(AMOUNT * 2n);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test does not use the second spender as intended.

The test sets up two spenders (Z_SPENDER and Z_OTHER) at lines 310-311, but both transfers at lines 314-316 and 320-322 use SPENDER. The comment at line 319 says "Second spender transfers remaining" but the code doesn't actually use the second operator. This means the test doesn't verify that multiple distinct operators can perform transfers.

Suggested fix
           // Second spender transfers remaining
           token
-            .as(SPENDER)
+            .as(utils.toHexPadded('OTHER'))
             .transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);

Alternatively, define an OTHER constant similar to SPENDER:

const OTHER = utils.toHexPadded('OTHER');
🤖 Prompt for AI Agents
In `@contracts/src/token/test/MultiToken.test.ts` around lines 306 - 324, The test
mistakenly uses the same operator for both transfers so it never verifies a
second distinct spender; update the second transfer to use the other approved
spender (replace the second token.as(SPENDER).transferFrom(...) call with
token.as(Z_OTHER or an OTHER constant).transferFrom(...)) and ensure an OTHER
constant is defined if needed; keep the approvals via
token.as(caller).setApprovalForAll(Z_SPENDER, true) and
token.as(caller).setApprovalForAll(Z_OTHER, true) and the initial mint via
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n) unchanged.

Comment on lines 511 to 529
it('should handle concurrent operations on same token ID', () => {
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n);

// Set up two spenders
token.setApprovalForAll(Z_SPENDER, true, caller);
token.setApprovalForAll(Z_OTHER, true, caller);
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);

// First spender transfers half
token._unsafeTransferFrom(
Z_OWNER,
recipient,
TOKEN_ID,
AMOUNT,
SPENDER,
);
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);

// Second spender transfers remaining
token._unsafeTransferFrom(
Z_OWNER,
recipient,
TOKEN_ID,
AMOUNT,
SPENDER,
);
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT * 2n);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test does not use the second spender as intended.

Similar to the transferFrom test, this test sets up two spenders (Z_SPENDER and Z_OTHER) but both transfers use caller (either OWNER or SPENDER depending on the parameterized test). The second approved operator Z_OTHER is never actually used to perform a transfer.

Suggested fix

To properly test concurrent operations with multiple operators, the second transfer should use a different spender:

           // Second spender transfers remaining
           token
-            .as(caller)
+            .as(utils.toHexPadded('OTHER'))
             ._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle concurrent operations on same token ID', () => {
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n);
// Set up two spenders
token.setApprovalForAll(Z_SPENDER, true, caller);
token.setApprovalForAll(Z_OTHER, true, caller);
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);
// First spender transfers half
token._unsafeTransferFrom(
Z_OWNER,
recipient,
TOKEN_ID,
AMOUNT,
SPENDER,
);
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
// Second spender transfers remaining
token._unsafeTransferFrom(
Z_OWNER,
recipient,
TOKEN_ID,
AMOUNT,
SPENDER,
);
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT * 2n);
});
it('should handle concurrent operations on same token ID', () => {
token._mint(Z_OWNER, TOKEN_ID, AMOUNT * 2n);
// Set up two spenders
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);
// First spender transfers half
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT);
// Second spender transfers remaining
token
.as(Z_OTHER)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
expect(token.balanceOf(recipient, TOKEN_ID)).toEqual(AMOUNT * 2n);
});
🤖 Prompt for AI Agents
In `@contracts/src/token/test/MultiToken.test.ts` around lines 511 - 529, The test
sets approvals for two different operators (Z_SPENDER and Z_OTHER) but both
transfers call ._unsafeTransferFrom via token.as(caller) so Z_OTHER is never
used; update the transfers to perform the first transfer using
token.as(Z_SPENDER)._unsafeTransferFrom(...) and the second transfer using
token.as(Z_OTHER)._unsafeTransferFrom(...) (keeping the existing _mint,
setApprovalForAll, and balanceOf assertions) so the test exercises two distinct
approved operators.

Comment on lines +66 to 83
private readonly options: BuilderOptions;
private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> =
[
// Step 1: Clean dist directory
{
cmd: 'rm -rf dist && mkdir -p dist',
msg: 'Cleaning dist directory',
shell: '/bin/bash',
},

// Step 2: TypeScript compilation (witnesses/ -> dist/witnesses/)
{
cmd: 'tsc --project tsconfig.build.json',
msg: 'Compiling TypeScript',
},

// Step 3: Copy .compact files preserving structure (excludes Mock* files and archive/)
{
// biome-ignore-start lint/suspicious/noUselessEscapeInString: Needed inside JS template literal
cmd: `
find src -type f -name "*.compact" ! -name "Mock*" ! -path "*/archive/*" | while read file; do
# Remove src/ prefix from path
rel_path="\${file#src/}"
mkdir -p "dist/\$(dirname "\$rel_path")"
cp "\$file" "dist/\$rel_path"
done
`,
// biome-ignore-end lint/suspicious/noUselessEscapeInString: Needed inside JS template literal
msg: 'Copying .compact files (excluding mocks and archive)',
cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true',
msg: 'Copying artifacts',
shell: '/bin/bash',
},

// Step 4: Copy essential files for distribution
{
cmd: `
# Copy package.json and README
cp package.json dist/ 2>/dev/null || true
cp ../README.md dist/ # Go up one level to monorepo root
`,
msg: 'Copying package metadata',
cmd: 'mkdir -p dist && find src -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true',
msg: 'Copying and cleaning .compact files',
shell: '/bin/bash',
},
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Copy steps ignore srcDir/outDir and can miss compiled artifacts.
The compiler outputs to outDir (default artifacts at repo root), but the build step copies from src/artifacts. This breaks default behavior and any --src/--out usage. Derive the copy paths from options (or reuse the compiler’s resolved directories).

🔧 Suggested fix (derive paths from options)
-  private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> =
-    [
-      {
-        cmd: 'tsc --project tsconfig.build.json',
-        msg: 'Compiling TypeScript',
-      },
-      {
-        cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true',
-        msg: 'Copying artifacts',
-        shell: '/bin/bash',
-      },
-      {
-        cmd: 'mkdir -p dist && find src -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true',
-        msg: 'Copying and cleaning .compact files',
-        shell: '/bin/bash',
-      },
-    ];
+  private readonly steps: Array<{ cmd: string; msg: string; shell?: string }>;
+
+  constructor(options: BuilderOptions = {}) {
+    this.options = options;
+    const srcDir = options.srcDir ?? 'src';
+    const outDir = options.outDir ?? 'artifacts';
+    this.steps = [
+      {
+        cmd: 'tsc --project tsconfig.build.json',
+        msg: 'Compiling TypeScript',
+      },
+      {
+        cmd: `mkdir -p dist/artifacts && cp -Rf "${outDir}"/* dist/artifacts/ 2>/dev/null || true`,
+        msg: 'Copying artifacts',
+        shell: '/bin/bash',
+      },
+      {
+        cmd: `mkdir -p dist && find "${srcDir}" -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true`,
+        msg: 'Copying and cleaning .compact files',
+        shell: '/bin/bash',
+      },
+    ];
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly options: BuilderOptions;
private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> =
[
// Step 1: Clean dist directory
{
cmd: 'rm -rf dist && mkdir -p dist',
msg: 'Cleaning dist directory',
shell: '/bin/bash',
},
// Step 2: TypeScript compilation (witnesses/ -> dist/witnesses/)
{
cmd: 'tsc --project tsconfig.build.json',
msg: 'Compiling TypeScript',
},
// Step 3: Copy .compact files preserving structure (excludes Mock* files and archive/)
{
// biome-ignore-start lint/suspicious/noUselessEscapeInString: Needed inside JS template literal
cmd: `
find src -type f -name "*.compact" ! -name "Mock*" ! -path "*/archive/*" | while read file; do
# Remove src/ prefix from path
rel_path="\${file#src/}"
mkdir -p "dist/\$(dirname "\$rel_path")"
cp "\$file" "dist/\$rel_path"
done
`,
// biome-ignore-end lint/suspicious/noUselessEscapeInString: Needed inside JS template literal
msg: 'Copying .compact files (excluding mocks and archive)',
cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true',
msg: 'Copying artifacts',
shell: '/bin/bash',
},
// Step 4: Copy essential files for distribution
{
cmd: `
# Copy package.json and README
cp package.json dist/ 2>/dev/null || true
cp ../README.md dist/ # Go up one level to monorepo root
`,
msg: 'Copying package metadata',
cmd: 'mkdir -p dist && find src -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true',
msg: 'Copying and cleaning .compact files',
shell: '/bin/bash',
},
];
private readonly options: BuilderOptions;
private readonly steps: Array<{ cmd: string; msg: string; shell?: string }>;
constructor(options: BuilderOptions = {}) {
this.options = options;
const srcDir = options.srcDir ?? 'src';
const outDir = options.outDir ?? 'artifacts';
this.steps = [
{
cmd: 'tsc --project tsconfig.build.json',
msg: 'Compiling TypeScript',
},
{
cmd: `mkdir -p dist/artifacts && cp -Rf "${outDir}"/* dist/artifacts/ 2>/dev/null || true`,
msg: 'Copying artifacts',
shell: '/bin/bash',
},
{
cmd: `mkdir -p dist && find "${srcDir}" -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true`,
msg: 'Copying and cleaning .compact files',
shell: '/bin/bash',
},
];
}
🤖 Prompt for AI Agents
In `@packages/compact/src/Builder.ts` around lines 66 - 83, The build steps in
Builder (the steps array) hardcode paths like "src/artifacts" and "dist",
causing mismatch with configured src/out directories; update Builder to derive
those copy/move commands from the instance options (e.g., use options.srcDir and
options.outDir or the compiler-resolved directories) and replace the hardcoded
paths in the steps entries for "Copying artifacts" and "Copying and cleaning
.compact files" so the shell commands reference the resolved src/out locations;
ensure the mkdir/cp/find commands use the derived paths and preserve the
existing shell flags and fallbacks.

Comment on lines +32 to +43
* Truncates to 30 bytes before prepending PREFIX_ADDRESS to comply
* with field value constraints (max unsigned integer is 2^248-1).
* The truncation compensates for the bytes added by the prefix.
* @param str String to hexify and encode.
* @returns Encoded `ZswapCoinPublicKey`.
* @returns Encoded `Compact.ContractAddress`.
*/
export const encodeToAddress = (str: string): Compact.ContractAddress => ({
bytes: encodeContractAddress(PREFIX_ADDRESS + toHexPadded(str)),
});
export const encodeToAddress = (str: string): Compact.ContractAddress => {
const hex = toHexPadded(str);
const truncated = hex.slice(0, -4);
return {
bytes: encodeContractAddress(PREFIX_ADDRESS + truncated),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the address.ts file to understand the context
cat -n packages/simulator/test/fixtures/utils/address.ts

Repository: OpenZeppelin/compact-contracts

Length of output: 4290


🏁 Script executed:

# Search for toHexPadded function definition
rg "toHexPadded" --type ts -A 5

Repository: OpenZeppelin/compact-contracts

Length of output: 15519


🏁 Script executed:

# Search for PREFIX_ADDRESS definition
rg "PREFIX_ADDRESS" --type ts -B 2 -A 2

Repository: OpenZeppelin/compact-contracts

Length of output: 2737


🏁 Script executed:

# Search for encodeContractAddress function
rg "encodeContractAddress" --type ts -B 2 -A 5

Repository: OpenZeppelin/compact-contracts

Length of output: 7915


🌐 Web query:

@midnight-ntwrk/ledger-v7 encodeContractAddress contract address payload length specification

💡 Result:

In @midnight-ntwrk/ledger-v7, encodeContractAddress(...) expects the raw contract address payload to be 32 bytes (256 bits) — i.e. a 64-hex-character value (often written as 0x + 64 hex chars). This matches how Midnight JSON-RPC APIs represent contract addresses (32-byte hex). [1]

If you’re encoding from bytes, the input must therefore be exactly 32 bytes; if you’re encoding from hex, it must be exactly 64 hex chars (optionally with a 0x prefix). The returned string is the Bech32m-encoded form used across the Midnight wallet ecosystem. [2]

Sources:
[1] https://www.ankr.com/docs/rpc-service/chains/chains-api/midnight/
[2] https://www.npmjs.com/package/%40midnight-ntwrk%2Fwallet-sdk-address-format


🏁 Script executed:

# Check how encodeToAddress is called in the codebase
rg "encodeToAddress" --type ts -B 2 -A 2

Repository: OpenZeppelin/compact-contracts

Length of output: 9142


🏁 Script executed:

# Check what strings are passed to encodeToAddress
rg "encodeToAddress\(" --type ts -A 1 | head -50

Repository: OpenZeppelin/compact-contracts

Length of output: 2283


The current logic correctly produces 64 hex characters for typical test inputs, but lacks explicit input clamping for edge cases.

For normal short strings (current usage), toHexPadded(str) → 64 hex chars (32 bytes) → slice(0, -4) → 60 hex chars (30 bytes) → PREFIX_ADDRESS + truncated → 64 hex chars (32 bytes), which matches the encodeContractAddress requirement. However, if a long ASCII string (>32 bytes) is passed, toHexPadded produces >64 hex characters, and the slice only removes 4 chars, risking oversized payloads. Consider adding explicit clamping:

Suggested improvement (defensive clamping)
-  const hex = toHexPadded(str);
-  const truncated = hex.slice(0, -4);
+  const hex = toHexPadded(str).slice(0, 64);
+  const truncated = hex.slice(0, -4);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Truncates to 30 bytes before prepending PREFIX_ADDRESS to comply
* with field value constraints (max unsigned integer is 2^248-1).
* The truncation compensates for the bytes added by the prefix.
* @param str String to hexify and encode.
* @returns Encoded `ZswapCoinPublicKey`.
* @returns Encoded `Compact.ContractAddress`.
*/
export const encodeToAddress = (str: string): Compact.ContractAddress => ({
bytes: encodeContractAddress(PREFIX_ADDRESS + toHexPadded(str)),
});
export const encodeToAddress = (str: string): Compact.ContractAddress => {
const hex = toHexPadded(str);
const truncated = hex.slice(0, -4);
return {
bytes: encodeContractAddress(PREFIX_ADDRESS + truncated),
};
* Truncates to 30 bytes before prepending PREFIX_ADDRESS to comply
* with field value constraints (max unsigned integer is 2^248-1).
* The truncation compensates for the bytes added by the prefix.
* `@param` str String to hexify and encode.
* `@returns` Encoded `Compact.ContractAddress`.
*/
export const encodeToAddress = (str: string): Compact.ContractAddress => {
const hex = toHexPadded(str).slice(0, 64);
const truncated = hex.slice(0, -4);
return {
bytes: encodeContractAddress(PREFIX_ADDRESS + truncated),
};
🤖 Prompt for AI Agents
In `@packages/simulator/test/fixtures/utils/address.ts` around lines 32 - 43,
encodeToAddress may produce oversized hex when toHexPadded(str) returns >64
chars; ensure you clamp the payload explicitly before prefixing: compute hex =
toHexPadded(str), then truncate/clamp hex to at most 60 hex characters (30
bytes) for the part that you will prepend PREFIX_ADDRESS to, so that
PREFIX_ADDRESS + truncated is always 64 hex chars; keep using
encodeContractAddress on the resulting fixed-length string and reference
encodeToAddress, toHexPadded, PREFIX_ADDRESS, and encodeContractAddress when
making the change.

Comment on lines +26 to +45
function isSpawnSyncRet(
err: unknown,
): err is SpawnSyncReturns<string | Buffer> {
if (typeof err !== 'object' || err === null) {
return false;
}

const typedErr = err as Partial<SpawnSyncReturns<string | Buffer>> &
Record<string, unknown>;

const okErr = typedErr.error instanceof Error;
const okStdout =
typeof typedErr.stdout === 'string' || Buffer.isBuffer(typedErr.stdout);
const okStderr =
typeof typedErr.stderr === 'string' || Buffer.isBuffer(typedErr.stderr);
const okStatus =
typeof typedErr.status === 'number' || typedErr.status === null;

return okErr && okStdout && okStderr && okStatus;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, verify the error shape from promisified exec
cat << 'EOF' | node --input-type=module
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
try {
  await execAsync('nonexistent-command-12345');
} catch (e) {
  console.log('Error keys:', Object.keys(e));
  console.log('Has status:', 'status' in e);
  console.log('Has code:', 'code' in e);
  console.log('code value:', e.code);
  console.log('status value:', e.status);
  console.log('Full error type:', e.constructor.name);
}
EOF

Repository: OpenZeppelin/compact-contracts

Length of output: 239


🏁 Script executed:

# Find the setup.ts file and examine it
fd -t f "setup.ts" packages/simulator/test/

Repository: OpenZeppelin/compact-contracts

Length of output: 106


🏁 Script executed:

# Search for how isSpawnSyncRet is used in the codebase
rg "isSpawnSyncRet" -A 3 -B 3

Repository: OpenZeppelin/compact-contracts

Length of output: 827


🏁 Script executed:

# Check if promisify(exec) is used in the setup.ts file
rg "promisify|exec" packages/simulator/test/setup.ts -A 2 -B 2

Repository: OpenZeppelin/compact-contracts

Length of output: 592


Type guard for SpawnSyncReturns does not match promisified exec errors.

The isSpawnSyncRet function checks for properties specific to SpawnSyncReturns: error, status, stdout, and stderr. However, promisify(exec) rejects with an Error that has only code, killed, signal, cmd, stdout, and stderr properties—no error or status property.

Since the guard checks typedErr.error instanceof Error and typedErr.status (as number or null), both will fail for exec errors, causing the function to return false and the error to be re-thrown on line 51 rather than being handled.

🤖 Prompt for AI Agents
In `@packages/simulator/test/setup.ts` around lines 26 - 45, The current
isSpawnSyncRet type guard requires SpawnSyncReturns-specific fields (error,
status) which prometified exec errors don't have; update isSpawnSyncRet to
return true for either the SpawnSyncReturns shape or the exec-style error shape
by checking stdout/stderr (string|Buffer) plus one of: (typedErr.error
instanceof Error) OR exec-style properties like typeof typedErr.code ===
'number' || typeof typedErr.code === 'string' || typedErr.code === null, typeof
typedErr.killed === 'boolean', and (typeof typedErr.signal === 'string' ||
typedErr.signal === null) and optionally typeof typedErr.cmd === 'string'; use
these symbol names (isSpawnSyncRet, SpawnSyncReturns, promisify(exec)) to locate
and replace the guard logic so exec rejection objects are recognized and handled
instead of re-thrown.

Comment on lines 122 to 125
const modifiedTxCtx: QueryContext = {
...ctx.transactionContext,
...ctx.currentQueryContext,
address: encodeToAddress('otherAddress'),
} as unknown as QueryContext;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and examine the StateManager test file around lines 122-125
cat -n packages/simulator/test/unit/core/StateManager.test.ts | sed -n '110,140p'

Repository: OpenZeppelin/compact-contracts

Length of output: 1230


🏁 Script executed:

# Search for QueryContext definition
rg "interface QueryContext|type QueryContext|class QueryContext" -A 5

Repository: OpenZeppelin/compact-contracts

Length of output: 56


🏁 Script executed:

# Search for QueryContext-related methods
rg "QueryContext" --type ts -A 2 | grep -E "(clone|copy|create|factory|make)" -i

Repository: OpenZeppelin/compact-contracts

Length of output: 764


🏁 Script executed:

# Search for QueryContext usage patterns
rg "currentQueryContext\|QueryContext.*\{" --type ts -B 2 -A 2 | head -100

Repository: OpenZeppelin/compact-contracts

Length of output: 56


🏁 Script executed:

# Search for QueryContext in all files, including imports
rg "QueryContext" --type ts | head -50

Repository: OpenZeppelin/compact-contracts

Length of output: 3093


🏁 Script executed:

# Get imports and definitions from the test file
head -50 packages/simulator/test/unit/core/StateManager.test.ts

Repository: OpenZeppelin/compact-contracts

Length of output: 1602


🏁 Script executed:

# Find where QueryContext might be defined
fd QueryContext --type ts

Repository: OpenZeppelin/compact-contracts

Length of output: 243


🏁 Script executed:

# Look for methods on QueryContext or similar patterns
rg "withAddress|clone|copy" --type ts -B 2 -A 3 | head -100

Repository: OpenZeppelin/compact-contracts

Length of output: 601


🏁 Script executed:

# Search for QueryContext constructor definition
rg "new QueryContext\(" --type ts -B 2 -A 2

Repository: OpenZeppelin/compact-contracts

Length of output: 2733


🏁 Script executed:

# Look for QueryContext in node_modules or package dependencies
rg "class QueryContext|export.*QueryContext" --type ts -A 5

Repository: OpenZeppelin/compact-contracts

Length of output: 56


🏁 Script executed:

# Get more context around the problematic code and similar patterns
cat -n packages/simulator/test/unit/core/StateManager.test.ts | sed -n '100,160p'

Repository: OpenZeppelin/compact-contracts

Length of output: 1644


🏁 Script executed:

# Look at how the state property is accessed and used
rg "currentQueryContext\.state" --type ts -B 1 -A 1 | head -50

Repository: OpenZeppelin/compact-contracts

Length of output: 1396


🏁 Script executed:

# Search for other test patterns with QueryContext
rg "QueryContext.*\{" --type ts -B 2 -A 5

Repository: OpenZeppelin/compact-contracts

Length of output: 723


🏁 Script executed:

# Look for comments or patterns around QueryContext creation
rg "QueryContext" packages/simulator/src/core/CircuitContextManager.ts -B 3 -A 3

Repository: OpenZeppelin/compact-contracts

Length of output: 423


Use QueryContext constructor instead of spreading. The spread+cast pattern loses the WASM backing (__wbg_ptr). Use new QueryContext(ctx.currentQueryContext.state, encodeToAddress('otherAddress')) to properly clone with the updated address.

🤖 Prompt for AI Agents
In `@packages/simulator/test/unit/core/StateManager.test.ts` around lines 122 -
125, Replace the spread+cast pattern that loses WASM backing by constructing a
proper QueryContext instance: instead of creating modifiedTxCtx with
{...ctx.currentQueryContext, address: encodeToAddress('otherAddress')} as
QueryContext, call the QueryContext constructor with the original state and the
new address (use ctx.currentQueryContext.state and
encodeToAddress('otherAddress')) so the resulting object retains the WASM
__wbg_ptr backing.

Copy link
Contributor

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, @emnul! I left some comments

This is 98% reviewed, I'll finish the nft and multitoken modules + tests tomorrow

Comment on lines +9 to +13
type ZswapCoinPublicKey = { bytes: Uint8Array };

type ContractAddress = { bytes: Uint8Array };

type Either<A, B> = { is_left: boolean; left: A; right: B };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also works :) I'd lean toward importing instead of duplicating the types down the road though once we have a nice solution

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree, should we open an issue in the ledger repo and propose to expose these Ledger ADT types the on-chain runtime or ledger package?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea. It's a small dx improvement but everyone benefits. Worst case, it's something we can offer

Copy link
Contributor

@andrew-fleming andrew-fleming Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original build setup was carefully crafted for release packaging (compact is not the easiest thing to package #212 #284). The proposed changes look like they're going to break it. Are these changes absolutely required to bump the version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question from the build changes: Is this necessary to bump the version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @emnul! I just have a concern about the changes on the packages/compact, we want at the end to switch to use compact-tools. I see we don't want to block this PR until the compact-tools gets released, but I'd say let's open an issue on compact-tools for the changes happened here.

emnul and others added 5 commits February 13, 2026 08:47
Co-authored-by: Andrew Fleming <fleming-andrew@protonmail.com>
Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
Co-authored-by: Andrew Fleming <fleming-andrew@protonmail.com>
Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
@emnul
Copy link
Contributor Author

emnul commented Feb 13, 2026

@andrew-fleming RE: Compact tools changes

Really the only change that's 100% necessary is the archive directory filtering in Compiler.ts to pass CI. All the other changes were made in an attempt to sync this repo's compact-tools packages with the unpublished version in a separate repo. I did not realize that pulling in those changes would break our contracts packaging behavior.

Should those sync changes be reverted for now?

@andrew-fleming
Copy link
Contributor

Really the only change that's 100% necessary is the archive directory filtering in Compiler.ts to pass CI. All the other changes were made in an attempt to sync this repo's compact-tools packages with the unpublished version in a separate repo. I did not realize that pulling in those changes would break our contracts packaging behavior.

@emnul aint no thing. To be fair, we haven't been able to publish 😢 now, I'm wondering if we should be running build for contracts from the tools package bc it's pretty specific for how we use it

Should those sync changes be reverted for now?

I'd say yeah, if you don't mind. Let's revert those changes to keep this PR clean so we're not yoloing anything. Measure twice, cut once


expect(() => {
token.transferFrom(Z_OWNER, Z_RECIPIENT, 1n, caller);
token.as(OWNER).transferFrom(Z_OWNER, Z_RECIPIENT, 1n);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token.as(OWNER).transferFrom(Z_OWNER, Z_RECIPIENT, 1n);
token.as(SPENDER).transferFrom(Z_OWNER, Z_RECIPIENT, 1n);

Comment on lines +9 to +13
type ZswapCoinPublicKey = { bytes: Uint8Array };

type ContractAddress = { bytes: Uint8Array };

type Either<A, B> = { is_left: boolean; left: A; right: B };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea. It's a small dx improvement but everyone benefits. Worst case, it's something we can offer

Comment on lines +299 to +300
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move this out of the 'transferFrom', When the caller is - block

Suggested change
token.as(caller).setApprovalForAll(Z_SPENDER, true);
token.as(caller).setApprovalForAll(Z_OTHER, true);
token.as(OWNER).setApprovalForAll(Z_SPENDER, true);
token.as(OWNER).setApprovalForAll(Z_OTHER, true);

Comment on lines +309 to +311
token
.as(SPENDER)
.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token
.as(SPENDER)
.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);
token
.as(OTHER)
.transferFrom(Z_OWNER, Z_RECIPIENT, TOKEN_ID, AMOUNT);

If the above suggestion is accepted, this should be OTHER to follow this test's goal (also need to create OTHER in the pk gen)

Comment on lines +508 to +510
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
token
.as(OTHER)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);

Similar thing here

Comment on lines +503 to +504
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.as(caller)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);
.as(SPENDER)
._unsafeTransferFrom(Z_OWNER, recipient, TOKEN_ID, AMOUNT);

Comment on lines +480 to +482
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1);
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2);
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1);
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2);
token.as(SPENDER).transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3);
token.setPersistentCaller(SPENDER);
token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1);
token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2);
token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3);

Not opposed to keeping the caller ctx explicit for each call. This is just another way to do it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants