Skip to content

feat: Add bundled transaction support#874

Open
JosephDenman wants to merge 1 commit into
mainfrom
JosephDenman/multi-transaction
Open

feat: Add bundled transaction support#874
JosephDenman wants to merge 1 commit into
mainfrom
JosephDenman/multi-transaction

Conversation

@JosephDenman

Copy link
Copy Markdown
Contributor

Adds withMultiContractScopedTransaction for batching circuit calls to different contracts into a single ledger intent. Same shape as withContractScopedTransaction, but the scope is contract-polymorphic — ctx.for(contract) widens the type at each call site.

Cross-contract unshielded flows (A.send / B.receive) only validate when both calls share an intent segment and guaranteed/fallible half. The ledger's effects check (verify.rs:1599-1651) matches claimed_unshielded_spends against unshielded_inputs keyed on (intent_seg, is_guaranteed_half, token_type, recipient, value). merge() splices independently-partitioned intents, so alignment is on us; Transaction.addCalls jointly partitions, so it's on the ledger. We want the latter.

Signed-off-by: JosephDenman <joseph.denman@iohk.io>
@JosephDenman JosephDenman self-assigned this Apr 30, 2026
@JosephDenman JosephDenman requested a review from a team as a code owner April 30, 2026 17:24
@github-actions

Copy link
Copy Markdown
Contributor
Title Lines Statements Branches Functions
contracts Coverage: 91%
90.38% (536/593) 83.65% (220/263) 89.6% (112/125)
dapp-connector-proof-provider Coverage: 91%
90.38% (536/593) 83.65% (220/263) 89.6% (112/125)
fetch-zk-config-provider Coverage: 100%
100% (23/23) 84.61% (11/13) 100% (6/6)
http-client-proof-provider Coverage: 81%
81.39% (35/43) 93.33% (14/15) 66.66% (6/9)
indexer-public-data-provider Coverage: 44%
44.76% (107/239) 39.28% (44/112) 27.52% (30/109)
level-private-state-provider Coverage: 92%
92.77% (642/692) 82.15% (267/325) 100% (89/89)
logger-provider Coverage: 100%
100% (15/15) 100% (0/0) 100% (8/8)
midnight-js Coverage: 100%
100% (0/0) 100% (0/0) 100% (0/0)
node-zk-config-provider Coverage: 100%
100% (11/11) 100% (0/0) 100% (5/5)
utils Coverage: 89%
86.79% (46/53) 80.48% (33/41) 63.63% (7/11)

@github-actions

Copy link
Copy Markdown
Contributor

Unit Test Results - MidnightJS

   28 files     88 suites   4m 35s ⏱️
  627 tests   625 ✅ 2 💤 0 ❌
1 268 runs  1 264 ✅ 4 💤 0 ❌

Results for commit e10fb37.

@MidnightCI

Copy link
Copy Markdown
Contributor

E2E Tests Results

e2e_tests_reports: Run #3634

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
124 120 0 4 0 0 0 8m 26s

🎉 All tests passed!

Suites

120 passed, 0 failed, and 4 other

Suite Passed Failed Other Duration
✅ testkit-js/testkit-js-e2e/test/contracts.blocktime.it.test.ts
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should succeed when both device time and node time are less than future time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should fail immediately on device when device time is already past the check time
        ⏭️ Block Time Contract Tests 1 > blockTimeLt tests > should succeed on device but fail on node when submission is delayed
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should succeed when both device time and node time are greater than past time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should fail immediately on device when device time is less than check time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should succeed even with submission delay when checking past time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should succeed when both device time and node time are greater than past time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should fail when device time is not greater than check time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should succeed when both device time and node time are less than or equal to future time
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should fail immediately on device when device time exceeds check time
        ⏭️ Block Time Contract Tests 1 > blockTimeLt tests > should succeed on device but fail on node when submission delay causes time to exceed threshold
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should demonstrate different failure points for Lt check > Immediate past time - fails on device
        ⏭️ Block Time Contract Tests 1 > blockTimeLt tests > should demonstrate different failure points for Lt check > Near future time with delay - succeeds on device, fails on node
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should demonstrate different failure points for Lt check > Far future time - succeeds on both device and node
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should handle maximum time values
        ✅ Block Time Contract Tests 1 > blockTimeLt tests > should handle zero time value
✅ 13 ❌ 0 ⏭️ 3 2m 47s
✅ testkit-js/testkit-js-e2e/test/contracts.blocktime2.it.test.ts
        ✅ Block Time Contract Tests 2 > blockTimeLt tests > should demonstrate different failure points for Lt check > Immediate past time - fails on device
        ⏭️ Block Time Contract Tests 2 > blockTimeLt tests > should demonstrate different failure points for Lt check > Near future time with delay - succeeds on device, fails on node
        ✅ Block Time Contract Tests 2 > blockTimeLt tests > should demonstrate different failure points for Lt check > Far future time - succeeds on both device and node
        ✅ Block Time Contract Tests 2 > blockTimeLt tests > should handle maximum time values
        ✅ Block Time Contract Tests 2 > blockTimeLt tests > should handle zero time value
✅ 4 ❌ 0 ⏭️ 1 1m 11s
✅ testkit-js/testkit-js-e2e/test/contracts.it.test.ts
        ✅ Contracts API > should create unproven call and deploy transactions for contract with private state
        ✅ Contracts API > should deploy contract on the chain [@slow]
        ✅ Contracts API > should return deployed contract if it exists on specific address
        ✅ Contracts API > should return deployed contract if it exists on specific address without initialPrivateState
        ✅ Contracts API > should throw error if contract address has wrong format - length
        ✅ Contracts API > should return deployed contract if it exists on specific address with initialPrivateState and empty local private state store
        ✅ Contracts API > should return deployed contract if it exists on specific address with different initialPrivateState
        ✅ Contracts API > should wait indefinitely until contract exists on specific address [@slow]
        ✅ Contracts API > should throw for incompatible contract types that differ by circuit ids
        ✅ Contracts API > should throw for incompatible contract types with same shape but different verifier keys
        ✅ Contracts API > should return contract interface and execute circuit operations [@slow]
        ✅ Contracts API > should throw error on undefined public state at wrong address
        ✅ Contracts API > should submit a deploy transaction [@slow]
        ✅ Contracts API > should submit transaction that calls circuit in contract [@slow]
        ✅ Contracts API > should throw error if private state is undefined
        ✅ Contracts API > should throw error if public state is undefined
        ✅ Contracts API > should throw error if contract address has wrong format - not hex
        ✅ Contracts API > should return the latest observed state of a deployed contract and is independent of the chain state
        ✅ Contracts API > should wait indefinitely until state change, if stopped returns last contract state [@slow]
✅ 19 ❌ 0 ⏭️ 0 3m 23s
✅ testkit-js/testkit-js-e2e/test/contracts.scopedtx.it.test.ts
        ✅ Scoped Transaction Contract Tests > should submit scoped transaction that calls circuit in contract [@slow]
        ✅ Scoped Transaction Contract Tests > should submit scoped transaction that calls 2 different circuits in contract [@slow]
        ✅ Scoped Transaction Contract Tests > should submit scoped transaction that calls 2 circuits in contract and DOES NOT preserve execution order [@slow]
        ✅ Scoped Transaction Contract Tests > should not submit scoped transaction when one circuit call fails [@slow]
✅ 4 ❌ 0 ⏭️ 0 54.5s
✅ testkit-js/testkit-js-e2e/test/contracts.singlecontract.nostate.it.test.ts
        ✅ Contracts API > should deploy and find contracts with no private state [@slow]
        ✅ Contracts API > should create unproven call and deploy transactions for contract with no private state
        ✅ Contracts API > should submit deploy and call transactions for contracts with no private state [@slow]
✅ 3 ❌ 0 ⏭️ 0 1m 34s
✅ testkit-js/testkit-js-e2e/test/contracts.snarkupgrade.it.test.ts
        ✅ Contracts API Snark Upgrade [dedicated contract] [@slow] > should successfully remove verifier key using submitRemoveVerifierKeyTx
        ✅ Contracts API Snark Upgrade [dedicated contract] [@slow] > should successfully remove verifier key using createContractMaintenanceTxInterface
        ✅ Contracts API Snark Upgrade [dedicated contract] [@slow] > should successfully remove verifier key and disable circuit operation
        ✅ Contracts API Snark Upgrade [dedicated contract] [@slow] > should succeed on verifier key insertion retry after removal
        ✅ Contracts API Snark Upgrade [dedicated contract] [@slow] > should fail when inserting verifier key for wrong circuit after removal
✅ 5 ❌ 0 ⏭️ 0 3m 18s
✅ testkit-js/testkit-js-e2e/test/contracts.snarkupgrade.singlecontract.it.test.ts
        ✅ Contracts API Snark Upgrade [single contract] > submitReplaceAuthorityTx - successful replace authority with new key[@slow]
        ✅ Contracts API Snark Upgrade [single contract] > submitReplaceAuthorityTx - successful replace authority with same key [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > submitReplaceAuthorityTx - should fail on replace contract that is not deployed to contract address
        ✅ Contracts API Snark Upgrade [single contract] > submitReplaceAuthorityTx - should fail when signing key for contract address does not exist
        ✅ Contracts API Snark Upgrade [single contract] > submitInsertVerifierKeyTx - should fail on invalid verifier key
        ✅ Contracts API Snark Upgrade [single contract] > submitInsertVerifierKeyTx - successful insert on not present circuitId [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > submitInsertVerifierKeyTx - should fail on contract not present on contract address
        ✅ Contracts API Snark Upgrade [single contract] > submitInsertVerifierKeyTx - should fail on providers for different contract with different API
        ✅ Contracts API Snark Upgrade [single contract] > submitRemoveVerifierKeyTx - should fail on not present circuitId
        ✅ Contracts API Snark Upgrade [single contract] > submitRemoveVerifierKeyTx - should fail on contract not present on contract address
        ✅ Contracts API Snark Upgrade [single contract] > submitRemoveVerifierKeyTx - should fail on providers for different contract with different API
        ✅ Contracts API Snark Upgrade [single contract] > createContractMaintenanceTxInterface - replaceAuthority - successful replace authority with the new one [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > createContractMaintenanceTxInterface - replaceAuthority - successful replace authority with the same one [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > createContractMaintenanceTxInterface - replaceAuthority - should fail on contract not present on contract address
        ✅ Contracts API Snark Upgrade [single contract] > createContractMaintenanceTxInterface - insertVerifierKey - fail when key is still present
        ✅ Contracts API Snark Upgrade [single contract] > createContractMaintenanceTxInterface - insertVerifierKey - success when no key present [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > createCircuitMaintenanceTxInterfaces - insertVerifierKey - fail when key is already present
        ✅ Contracts API Snark Upgrade [single contract] > createCircuitMaintenanceTxInterfaces - insertVerifierKey - success when no key present [@slow]
        ✅ Contracts API Snark Upgrade [single contract] > createCircuitMaintenanceTxInterfaces - removeVerifierKey - should fail on contract not present on contract address
✅ 19 ❌ 0 ⏭️ 0 3m 35s
✅ testkit-js/testkit-js-e2e/test/contracts.snarkupgrade.smoke.it.test.ts
        ✅ Contracts API Snark Upgrade [@slow][@smoke] > should update verifier keys from one contract to another [@smoke]
        ✅ Contracts API Snark Upgrade [@slow][@smoke] > should fail on operate with previous authority after replacement
✅ 2 ❌ 0 ⏭️ 0 3m 38s
✅ testkit-js/testkit-js-e2e/test/dapp-connector-proving.it.test.ts
        ✅ DApp Connector Proving > should deploy and call contract using dapp-connector-proof-provider with wallet-delegated proving [@slow]
✅ 1 ❌ 0 ⏭️ 0 39.5s
✅ testkit-js/testkit-js-e2e/test/indexer-public-data-provider.observable1.it.test.ts
        ✅ Indexer API > should return the history of states starting from defined blockHash (inclusive:true, expected:1,2) [@slow]
        ✅ Indexer API > should return the history of states starting from defined blockHash (inclusive:false, expected:2) [@slow]
        ✅ Indexer API > should return the history of states starting from defined txId (inclusive:true, expected states:1,2) [@slow]
        ✅ Indexer API > should return the history of states starting from defined txId (inclusive:false, expected states:2) [@slow]
✅ 4 ❌ 0 ⏭️ 0 3m 35s
✅ testkit-js/testkit-js-e2e/test/indexer-public-data-provider.observable2.it.test.ts
        ✅ Indexer API > should return the history of states starting from defined blockHeight (inclusive:true, expected states:1,2) [@slow]
        ✅ Indexer API > should return the history of states starting from defined blockHeight (inclusive:false, expected states:2) [@slow]
        ✅ Indexer API > should return the entire history of states of the contract with the given address (config:{ type: 'all' }, expected states:0,1,2) [@slow]
        ✅ Indexer API > should return the history of states of the contract with the given address, starting with the most recent state (config:{ type: 'latest' }, expected states:1,2) [@slow]
✅ 4 ❌ 0 ⏭️ 0 3m 36s
✅ testkit-js/testkit-js-e2e/test/indexer-public-data-provider.singlecontract.it.test.ts
        ✅ Indexer API > queryDeployContractState - should return a contract state equivalent to the initial contract state produced during deployment construction
        ✅ Indexer API > queryContractState - should return the current contract state of a deployed contract
        ✅ Indexer API > queryContractState - should return the current contract state of a deployed contract at defined block height
        ✅ Indexer API > queryContractState - should return the current contract state of a deployed contract at defined block hash
        ✅ Indexer API > queryContractState - should return null on no contract at contract address
        ✅ Indexer API > queryZSwapAndContractState - should return the current ZSwap chain state and contract state of a deployed contract
        ✅ Indexer API > queryZSwapAndContractState - should return null on no contract at contract address
        ✅ Indexer API > watchForDeployTxData - should return the data of the transaction containing the deployment of the contract with the given address
        ✅ Indexer API > watchForTxData - should return the data of the transaction containing the contract call with the given transaction id
        ✅ Indexer API > watchForContractState - should immediately return the current state of a deployed contract
✅ 10 ❌ 0 ⏭️ 0 108ms
✅ testkit-js/testkit-js-e2e/test/level-private-state-provider.it.test.ts
        ✅ Level Private State Provider - Export/Import Integration > should preserve private state after database recreation [@slow]
✅ 1 ❌ 0 ⏭️ 0 58.2s
✅ testkit-js/testkit-js-e2e/test/nodejs.it.test.ts
        ✅ Ledger API - NodeJS Integration Tests > should run ESM module successfully
        ✅ Ledger API - NodeJS Integration Tests > should run CJS module with expected exit code
✅ 2 ❌ 0 ⏭️ 0 1m 40s
✅ testkit-js/testkit-js-e2e/test/proof-server.it.test.ts
        ✅ Proof server integration > should create proofs successfully for deploy and call transactions
        ✅ Proof server integration > should create proofs with transactions that has succesfull well-formedness
        ✅ Proof server integration > should execute 5 proveTx calls in parallel without errors
✅ 3 ❌ 0 ⏭️ 0 612ms
✅ testkit-js/testkit-js-e2e/test/shielded.advanced.it.test.ts
        ✅ Shielded tokens - advanced operations > should mint and send immediate shielded tokens
        ✅ Shielded tokens - advanced operations > should mint and burn shielded tokens
✅ 2 ❌ 0 ⏭️ 0 1m 6s
✅ testkit-js/testkit-js-e2e/test/shielded.transfer.it.test.ts
        ✅ Shielded tokens > should mint tokens
        ✅ Shielded tokens > should deposit shielded coin via receiveShielded (issue #686)
✅ 2 ❌ 0 ⏭️ 0 1m 18s
✅ testkit-js/testkit-js-e2e/test/unshielded.balance.it.test.ts
        ✅ Unshielded tokens - balance > should get balance of tokens - 0 value
        ✅ Unshielded tokens - balance > should get balance of tokens - minted amount
        ✅ Unshielded tokens - balance > should get balance of tokens - greater than - false
        ✅ Unshielded tokens - balance > should get balance of tokens - greater than - true
        ✅ Unshielded tokens - balance > should get balance of tokens - less than - false
        ✅ Unshielded tokens - balance > should get balance of tokens - less than - true
        ✅ Unshielded tokens - balance > should get balance of tokens - greater than or equal - true (equal)
        ✅ Unshielded tokens - balance > should get balance of tokens - greater than or equal - false
        ✅ Unshielded tokens - balance > should get balance of tokens - less than or equal - true (equal)
        ✅ Unshielded tokens - balance > should get balance of tokens - less than or equal - false
✅ 10 ❌ 0 ⏭️ 0 3m
✅ testkit-js/testkit-js-e2e/test/unshielded.cross-wallet-transfer.it.test.ts
        ✅ Unshielded cross-wallet transfer (issue #720) > should send night tokens to different wallet via right<>(disclose(addr))
        ✅ Unshielded cross-wallet transfer (issue #720) > should send night tokens to different wallet via disclose(recipient) (issue #720)
✅ 2 ❌ 0 ⏭️ 0 36.0s
✅ testkit-js/testkit-js-e2e/test/unshielded.mint-and-send.it.test.ts
        ✅ Unshielded tokens - mint and send variants > should mint tokens to contract address (self)
        ✅ Unshielded tokens - mint and send variants > should mint tokens to user address
        ✅ Unshielded tokens - mint and send variants > should send tokens to self
        ✅ Unshielded tokens - mint and send variants > should send tokens to contract address (self)
✅ 4 ❌ 0 ⏭️ 0 1m 47s
✅ testkit-js/testkit-js-e2e/test/unshielded.transfer.it.test.ts
        ✅ Unshielded tokens > Custom color > should mint different tokens
        ✅ Unshielded tokens > Custom color > should receive tokens - invalid
        ✅ Unshielded tokens > Custom color > should send tokens to wallet
        ✅ Unshielded tokens > Custom color > should receive tokens from wallet
        ✅ Unshielded tokens > Native color > should transfer night from wallet to contract - receiveNightTokens
        ✅ Unshielded tokens > Native color > should transfer night to wallet - sendNightTokensToUser
✅ 6 ❌ 0 ⏭️ 0 2m 6s

Github Test Reporter by CTRF 💚

@sp-io sp-io changed the title Add bundled transaction support feat: Add bundled transaction support May 5, 2026
@sp-io

sp-io commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review — PR #874: Add bundled transaction support

Thanks for the work — the architecture is solid, mirrors the existing transaction.ts / internal/transaction.ts split correctly, the symbol-keyed protocol surface is preserved, and the motivation (joint partitioning via addCalls for cross-contract unshielded flows) is clearly explained. Local verification: lint ✅, build:core ✅, unit tests ✅. Coverage: bundle.ts 100%, internal/bundle.ts 74,11% lines (uncovered: bundleIntoSingleIntent core, [Submit], getInFlightCalls).

A few items I'd like to see addressed before approval.

Blockers

1. Terminology — "SDK" vs "framework"
Before this PR, the term "SDK" did not appear in any .ts file in the monorepo. This PR introduces 12 occurrences:

  • bundle.ts:45, 54
  • internal/bundle.ts:60, 74, 182, 260, 281, 303, 305, 380
  • test/bundle.test.ts:98, 624

Several are user-visible — line 260 is an error message thrown at runtime, and line 45 leaks via BundledFinalizedTxData.calls JSDoc into the public API surface. Please s/SDK/framework/.

2. Structural cast on ledgerParameters is unnecessary — internal/bundle.ts:268-270

const ledgerParametersBytes: Uint8Array = (
  cached.states.ledgerParameters as { serialize(): Uint8Array }
).serialize();

LedgerParameters (re-exported from @midnight-ntwrk/midnight-js-protocol/ledger, originally from @midnight-ntwrk/ledger-v8) declares serialize(): Uint8Array natively. cached.states.ledgerParameters is already typed as LedgerParameters (see get-states.ts:36). The cast is bypassing TypeScript without need; please remove so this becomes cached.states.ledgerParameters.serialize().

3. Silent partial-persistence after on-chain success — internal/bundle.ts:365-367

for (const [psId, call] of latestByPsId) {
  await psp.set(psId, call.callData.private.nextPrivateState);
}

This loop runs after the chain has already advanced. If the second set() throws (LevelDB lock, ENOSPC, AES-GCM error), contract A's local private state is persisted, B's is not, and the user gets:

  • no log entry,
  • no error wrapping,
  • no list of which privateStateIds remained unpersisted.

Subsequent calls against B will then read stale local state and produce transcripts that disagree with the chain. Please either (a) Promise.allSettled and aggregate failures with the unpersisted IDs, or (b) try/catch and rethrow with { cause } plus a "the following private state IDs were not persisted: …" message.

4. Missing integration test for bundleIntoSingleIntent
bundleIntoSingleIntent (internal/bundle.ts:122-175) is the function that justifies the entire feature — joint partitioning, cross-package wasm bridging via LedgerContractState.deserialize, PrePartitionContractCall construction, Transaction.fromParts(...).addCalls(...). The unit suite necessarily stubs it out via BundleScopedInternalOptions.bundler because round-trippable wasm bytes can't be produced in unit tests, which means 0% of the joint-partitioning logic is exercised by CI.

testkit-js/testkit-js-e2e/test/contracts.scopedtx.it.test.ts is the analogue for single-contract scoped transactions; please add a parallel test for the multi-contract case that deploys two contracts with a real cross-contract unshielded flow (sender's claimed_unshielded_spends against recipient's unshielded_inputs) and asserts SucceedEntirely. Without this, the central correctness claim of the PR is unverified.

Important

5. InFlightCall is @internal but exposed via public type — bundle.ts:47
BundledFinalizedTxData.calls: readonly Internal.InFlightCall[] leaks contractStateBytes and ledgerParametersBytes (serialized wasm snapshots) to public callers. Suggest a public BundledCallRecord shape exposing only { identity, circuitId, callData }, keeping InFlightCall internal.

6. identityForCall ambiguity — internal/bundle.ts:388-403
The doc admits "the most recently cached identity for that privateStateId wins". Two contracts with privateStateId: undefined, or two distinct contracts sharing one PSID, silently alias to whichever was cached last. Please throw on duplicate matches and lock it with a unit test.

7. for(_contract) impl signature drift — internal/bundle.ts:322-326
The public interface (bundle.ts:76-79) requires FoundContract<C>, but the impl is typed _contract: unknown. Pure widening is intentional, but the laxer impl signature obscures that. Either narrow the impl parameter to match the interface or document explicitly that the argument is for type widening only and is not validated. The test at bundle.test.ts:144-146 passes {} as never — please tighten it with a throw-on-access Proxy to lock the no-runtime-work invariant.

8. [Submit]() exposed as a no-op delegate — internal/bundle.ts:308-311
Comment says "the framework never invokes this. We expose it so a BundleContextImpl is structurally a TransactionContext." Delegating to submit() (which has different return shape and throws CallTxFailedError with a circuit-ID array) is a footgun if a future code path ever invokes it. Consider throwing Error('BundleContextImpl[Submit] should not be invoked directly; use bundleScoped').

Test coverage gaps

  • Identity ambiguity: duplicate privateStateId, two undefined PSIDs.
  • Same-identity threading completeness — bundle.test.ts:380-382 asserts only privateState.generation; please also assert contractState.data was replaced and zswapChainState/ledgerParameters were preserved.
  • Submission-failure path other than CallTxFailedError (e.g., proveTx rejecting) — verifies the error wrap with scope name and cause.
  • Logger side-effects on the two error-wrap paths (internal/bundle.ts:450, 466).
  • Deprecated getCurrentStates() last-touched semantics (internal/bundle.ts:224-228).
  • getAdditionalMappings and getLastUnsubmittedCallTxDataToTransact — currently uncovered.

Test quality nits

  • bundle.test.ts:142 — bare rejects.toThrow() without a pattern; should be /No calls were submitted/.
  • The test file uses 30+ structural casts (16× as unknown as Record<symbol, ...>, 13× (s: unknown, i: unknown) => void, 14× merge-call casts, 4× (i: unknown) => unknown). Other tests in packages/contracts/src/test/ use 0–4 such casts. Please factor a single typed helper that exposes the symbol protocol on the context. The repo guideline forbids avoidable unknown casts.
  • bundle.test.ts:455-460 uses one-directional find(); combined with toHaveBeenCalledTimes(2) it's safe today, but per AGENTS.md prefer strict equality on the full sorted call list.
  • getInFlightCalls() (internal/bundle.ts:406) is added as a test seam but never used in the test suite — either use it or delete (YAGNI).

Strengths

  • File split mirrors transaction.ts / internal/transaction.ts correctly; protocol surface preserved and asserted in bundle.test.ts:117-125.
  • Pre-call snapshot capture invariant test with the serialize-counter trick (bundle.test.ts:277-329) is excellent — exactly the regression the PR description flags as load-bearing.
  • CallTxFailedError correctly bypassed at internal/bundle.ts:457-459 so domain errors aren't double-wrapped.
  • Submission-order preservation, latest-write-wins per PSID, missing-privateStateProvider guard, and FailEntirely + no-psp.set() invariant are all properly covered.
  • License headers present on all three new .ts files; naming consistency (withMultiContractScopedTransactionwithContractScopedTransaction) is good.
  • submitTx accepting circuitId: PCK | PCK[] is correct (submit-tx.ts:48); the array call at internal/bundle.ts:341-344 is supported.

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