@aztec/aztec 0.0.1-commit.3fd054f6 → 0.0.1-commit.431c48d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dest/bin/index.js +3 -1
  2. package/dest/cli/aztec_start_action.d.ts +1 -1
  3. package/dest/cli/aztec_start_action.d.ts.map +1 -1
  4. package/dest/cli/aztec_start_action.js +33 -33
  5. package/dest/cli/aztec_start_options.d.ts +2 -2
  6. package/dest/cli/aztec_start_options.d.ts.map +1 -1
  7. package/dest/cli/aztec_start_options.js +21 -16
  8. package/dest/cli/cmds/compile.d.ts +1 -1
  9. package/dest/cli/cmds/compile.d.ts.map +1 -1
  10. package/dest/cli/cmds/compile.js +32 -24
  11. package/dest/cli/cmds/profile.d.ts +1 -1
  12. package/dest/cli/cmds/profile.d.ts.map +1 -1
  13. package/dest/cli/cmds/profile.js +1 -1
  14. package/dest/cli/cmds/profile_gates.d.ts +2 -2
  15. package/dest/cli/cmds/profile_gates.d.ts.map +1 -1
  16. package/dest/cli/cmds/profile_gates.js +21 -3
  17. package/dest/cli/cmds/prover.d.ts +4 -0
  18. package/dest/cli/cmds/prover.d.ts.map +1 -0
  19. package/dest/cli/cmds/prover.js +24 -0
  20. package/dest/cli/cmds/standby.d.ts +30 -6
  21. package/dest/cli/cmds/standby.d.ts.map +1 -1
  22. package/dest/cli/cmds/standby.js +48 -5
  23. package/dest/cli/cmds/start_bot.d.ts +1 -1
  24. package/dest/cli/cmds/start_bot.d.ts.map +1 -1
  25. package/dest/cli/cmds/start_bot.js +8 -4
  26. package/dest/cli/cmds/start_node.d.ts +1 -1
  27. package/dest/cli/cmds/start_node.d.ts.map +1 -1
  28. package/dest/cli/cmds/start_node.js +23 -47
  29. package/dest/cli/cmds/start_prover_agent.d.ts +1 -1
  30. package/dest/cli/cmds/start_prover_agent.d.ts.map +1 -1
  31. package/dest/cli/cmds/start_prover_agent.js +5 -17
  32. package/dest/cli/cmds/start_prover_broker.d.ts +1 -1
  33. package/dest/cli/cmds/start_prover_broker.d.ts.map +1 -1
  34. package/dest/cli/cmds/start_prover_broker.js +11 -8
  35. package/dest/cli/cmds/start_txe.d.ts +2 -2
  36. package/dest/cli/cmds/start_txe.d.ts.map +1 -1
  37. package/dest/cli/cmds/start_txe.js +6 -5
  38. package/dest/cli/cmds/utils/artifacts.d.ts +6 -1
  39. package/dest/cli/cmds/utils/artifacts.d.ts.map +1 -1
  40. package/dest/cli/cmds/utils/collect_crate_dirs.d.ts +21 -0
  41. package/dest/cli/cmds/utils/collect_crate_dirs.d.ts.map +1 -0
  42. package/dest/cli/cmds/utils/collect_crate_dirs.js +114 -0
  43. package/dest/cli/cmds/utils/needs_recompile.d.ts +1 -1
  44. package/dest/cli/cmds/utils/needs_recompile.d.ts.map +1 -1
  45. package/dest/cli/cmds/utils/needs_recompile.js +9 -53
  46. package/dest/cli/cmds/utils/warn_if_aztec_version_mismatch.d.ts +4 -0
  47. package/dest/cli/cmds/utils/warn_if_aztec_version_mismatch.d.ts.map +1 -0
  48. package/dest/cli/cmds/utils/warn_if_aztec_version_mismatch.js +61 -0
  49. package/dest/cli/util.d.ts +10 -5
  50. package/dest/cli/util.d.ts.map +1 -1
  51. package/dest/cli/util.js +26 -53
  52. package/dest/deploy/bridging.d.ts +84 -0
  53. package/dest/deploy/bridging.d.ts.map +1 -0
  54. package/dest/deploy/bridging.js +121 -0
  55. package/dest/deploy/fees.d.ts +120 -0
  56. package/dest/deploy/fees.d.ts.map +1 -0
  57. package/dest/deploy/fees.js +278 -0
  58. package/dest/deploy/graph.d.ts +40 -0
  59. package/dest/deploy/graph.d.ts.map +1 -0
  60. package/dest/deploy/graph.js +120 -0
  61. package/dest/deploy/index.d.ts +19 -0
  62. package/dest/deploy/index.d.ts.map +1 -0
  63. package/dest/deploy/index.js +15 -0
  64. package/dest/deploy/reporter.d.ts +106 -0
  65. package/dest/deploy/reporter.d.ts.map +1 -0
  66. package/dest/deploy/reporter.js +76 -0
  67. package/dest/deploy/runner.d.ts +8 -0
  68. package/dest/deploy/runner.d.ts.map +1 -0
  69. package/dest/deploy/runner.js +886 -0
  70. package/dest/deploy/state.d.ts +19 -0
  71. package/dest/deploy/state.d.ts.map +1 -0
  72. package/dest/deploy/state.js +59 -0
  73. package/dest/deploy/types.d.ts +233 -0
  74. package/dest/deploy/types.d.ts.map +1 -0
  75. package/dest/deploy/types.js +9 -0
  76. package/dest/examples/token.js +3 -3
  77. package/dest/local-network/auth_registry.d.ts +5 -0
  78. package/dest/local-network/auth_registry.d.ts.map +1 -0
  79. package/dest/local-network/auth_registry.js +17 -0
  80. package/dest/local-network/banana_fpc.d.ts +3 -2
  81. package/dest/local-network/banana_fpc.d.ts.map +1 -1
  82. package/dest/local-network/banana_fpc.js +11 -7
  83. package/dest/local-network/local-network.d.ts +10 -32
  84. package/dest/local-network/local-network.d.ts.map +1 -1
  85. package/dest/local-network/local-network.js +52 -73
  86. package/dest/testing/cheat_codes.d.ts +17 -23
  87. package/dest/testing/cheat_codes.d.ts.map +1 -1
  88. package/dest/testing/cheat_codes.js +17 -43
  89. package/dest/testing/epoch_test_settler.d.ts +2 -2
  90. package/dest/testing/epoch_test_settler.d.ts.map +1 -1
  91. package/dest/testing/epoch_test_settler.js +6 -25
  92. package/dest/testing/index.d.ts +2 -2
  93. package/dest/testing/index.d.ts.map +1 -1
  94. package/dest/testing/index.js +1 -1
  95. package/dest/testing/local-network.d.ts +51 -0
  96. package/dest/testing/local-network.d.ts.map +1 -0
  97. package/dest/testing/local-network.js +60 -0
  98. package/dest/testing/token_allowed_setup.d.ts +9 -4
  99. package/dest/testing/token_allowed_setup.d.ts.map +1 -1
  100. package/dest/testing/token_allowed_setup.js +12 -8
  101. package/package.json +37 -34
  102. package/scripts/add_crate.sh +11 -60
  103. package/scripts/aztec.sh +6 -2
  104. package/scripts/init.sh +5 -5
  105. package/scripts/new.sh +2 -2
  106. package/scripts/setup_workspace.sh +3 -2
  107. package/scripts/templates/blank/contract/Nargo.toml +6 -0
  108. package/scripts/templates/blank/contract/src/main.nr +10 -0
  109. package/scripts/templates/blank/test/Nargo.toml +7 -0
  110. package/scripts/templates/blank/test/src/lib.nr +11 -0
  111. package/scripts/templates/counter/contract/Nargo.toml +7 -0
  112. package/scripts/templates/counter/contract/src/main.nr +48 -0
  113. package/scripts/templates/counter/test/Nargo.toml +7 -0
  114. package/scripts/templates/counter/test/src/lib.nr +32 -0
  115. package/src/bin/index.ts +3 -1
  116. package/src/cli/aztec_start_action.ts +38 -25
  117. package/src/cli/aztec_start_options.ts +27 -23
  118. package/src/cli/cmds/compile.ts +37 -23
  119. package/src/cli/cmds/profile.ts +2 -1
  120. package/src/cli/cmds/profile_gates.ts +20 -4
  121. package/src/cli/cmds/prover.ts +42 -0
  122. package/src/cli/cmds/standby.ts +59 -7
  123. package/src/cli/cmds/start_bot.ts +9 -6
  124. package/src/cli/cmds/start_node.ts +32 -39
  125. package/src/cli/cmds/start_prover_agent.ts +5 -8
  126. package/src/cli/cmds/start_prover_broker.ts +10 -11
  127. package/src/cli/cmds/start_txe.ts +7 -5
  128. package/src/cli/cmds/utils/artifacts.ts +5 -0
  129. package/src/cli/cmds/utils/collect_crate_dirs.ts +118 -0
  130. package/src/cli/cmds/utils/needs_recompile.ts +8 -61
  131. package/src/cli/cmds/utils/warn_if_aztec_version_mismatch.ts +76 -0
  132. package/src/cli/util.ts +27 -68
  133. package/src/deploy/bridging.ts +191 -0
  134. package/src/deploy/fees.ts +322 -0
  135. package/src/deploy/graph.ts +137 -0
  136. package/src/deploy/index.ts +41 -0
  137. package/src/deploy/reporter.ts +184 -0
  138. package/src/deploy/runner.ts +936 -0
  139. package/src/deploy/state.ts +66 -0
  140. package/src/deploy/types.ts +244 -0
  141. package/src/examples/token.ts +11 -3
  142. package/src/local-network/auth_registry.ts +19 -0
  143. package/src/local-network/banana_fpc.ts +12 -8
  144. package/src/local-network/local-network.ts +61 -83
  145. package/src/testing/cheat_codes.ts +18 -49
  146. package/src/testing/epoch_test_settler.ts +8 -31
  147. package/src/testing/index.ts +1 -1
  148. package/src/testing/local-network.ts +97 -0
  149. package/src/testing/token_allowed_setup.ts +15 -8
  150. package/dest/cli/cmds/start_archiver.d.ts +0 -9
  151. package/dest/cli/cmds/start_archiver.d.ts.map +0 -1
  152. package/dest/cli/cmds/start_archiver.js +0 -48
  153. package/dest/testing/anvil_test_watcher.d.ts +0 -42
  154. package/dest/testing/anvil_test_watcher.d.ts.map +0 -1
  155. package/dest/testing/anvil_test_watcher.js +0 -181
  156. package/src/cli/cmds/start_archiver.ts +0 -50
  157. package/src/testing/anvil_test_watcher.ts +0 -210
@@ -0,0 +1,936 @@
1
+ /**
2
+ * The deployment engine: turns a declarative {@link DeploymentSpec} into one dependency graph of
3
+ * steps (contracts + actions), runs only what's missing, and is safe to re-run.
4
+ *
5
+ * resolve accounts → resolve deterministic addresses → inventory → plan → fund → execute → output
6
+ *
7
+ * Each phase is a method on {@link DeploymentRun}, which holds the state the phases share;
8
+ * {@link runDeployment} strings them together in the order above.
9
+ *
10
+ * - Deterministic contracts (addresses are a pure function of class/deployer/salt/args) resolve
11
+ * UPFRONT, so the plan knows their addresses before anything is sent.
12
+ * - Deferred contracts (args read runtime state) resolve at INVENTORY TIME when the state their
13
+ * args read already exists (the re-run case, which is what makes re-runs no-ops), and otherwise
14
+ * AT EXECUTION TIME, once their `dependsOn` has run.
15
+ * - Steps execute in topological layers over the single graph, so an action can precede a contract
16
+ * it sets up. Within a layer, contract publishes are individual txs and same-account actions batch
17
+ * into ≤{@link APP_MAX_CALLS}-call BatchCalls. The one-time fee-juice claim per account is
18
+ * consumed + mined by that account's first tx before the rest fan out.
19
+ * - Fund steps provision arbitrary addresses (contracts or accounts that never send) with bridged
20
+ * Fee Juice: bridge at execution time, then an L2 claim tx from the step's `from` account. Their
21
+ * claims persist between bridge and claim, so a crashed run resumes instead of re-bridging.
22
+ */
23
+ import type { ContractArtifact } from '@aztec/aztec.js/abi';
24
+ import { AztecAddress } from '@aztec/aztec.js/addresses';
25
+ import { BatchCall, type ContractBase, DeployMethod } from '@aztec/aztec.js/contracts';
26
+ import { Fr } from '@aztec/aztec.js/fields';
27
+ import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node';
28
+ import { FeeJuiceContract } from '@aztec/aztec.js/protocol';
29
+ import { APP_MAX_CALLS } from '@aztec/entrypoints/encoding';
30
+ import { chunk, compactArray } from '@aztec/foundation/collection';
31
+ import { getPXEConfig } from '@aztec/pxe/server';
32
+ import { getContractClassFromArtifact, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
33
+ import { deriveKeys, deriveMasterMessageSigningSecretKey } from '@aztec/stdlib/keys';
34
+ import type { TxReceipt } from '@aztec/stdlib/tx';
35
+ import { EmbeddedWallet } from '@aztec/wallets/embedded';
36
+
37
+ import { join } from 'node:path';
38
+
39
+ import {
40
+ type FeeSession,
41
+ type SendFee,
42
+ accountFunding,
43
+ defaultFeePolicy,
44
+ obtainFeeJuiceClaim,
45
+ prepareFeeSession,
46
+ publicFeeJuiceBalance,
47
+ } from './fees.js';
48
+ import { scheduleLayers, topologicalLayers } from './graph.js';
49
+ import {
50
+ type DeployPlan,
51
+ type DeployReporter,
52
+ type DeploySummary,
53
+ type DeployUnitKind,
54
+ consoleReporter,
55
+ } from './reporter.js';
56
+ import { type DeployState, loadState, saveState } from './state.js';
57
+ import type {
58
+ ActionStep,
59
+ ContractStep,
60
+ Ctx,
61
+ DeploymentSpec,
62
+ FeePolicy,
63
+ FundStep,
64
+ Resolver,
65
+ StepSpec,
66
+ Steps,
67
+ } from './types.js';
68
+
69
+ /** A step's idempotency gate (transitively) depends on itself. */
70
+ class GateCycleError extends Error {
71
+ constructor(message: string) {
72
+ super(message);
73
+ this.name = 'GateCycleError';
74
+ }
75
+ }
76
+
77
+ function getOrThrow<Value>(map: Map<string, Value>, alias: string, kind: string): Value {
78
+ const value = map.get(alias);
79
+ if (value === undefined) {
80
+ throw new Error(`Unknown ${kind} "${alias}".`);
81
+ }
82
+ return value;
83
+ }
84
+
85
+ function isDeferred<C>(step: ContractStep<C>): boolean {
86
+ return step.deferredInitializerArgs != null;
87
+ }
88
+
89
+ /**
90
+ * The contract aliases a pure resolver callback looks up, extracted by dry-running it against a
91
+ * resolver that records each `contract(alias)` lookup instead of resolving it. The callback's return
92
+ * value is discarded and the ZERO addresses never leave this function — which is what requires such
93
+ * callbacks to be pure: they run here with fake addresses and again later with real ones.
94
+ */
95
+ function referencedContracts(run: (resolve: Resolver) => unknown): string[] {
96
+ const references = new Set<string>();
97
+ run({
98
+ account: () => AztecAddress.ZERO,
99
+ contract: alias => {
100
+ references.add(alias);
101
+ return AztecAddress.ZERO;
102
+ },
103
+ });
104
+ return [...references];
105
+ }
106
+
107
+ /** One tx to send: a single contract publish, or a batch of same-account actions. */
108
+ interface ExecutionUnit {
109
+ label: string;
110
+ kind: DeployUnitKind;
111
+ account: AztecAddress;
112
+ send: (fee: SendFee) => Promise<unknown>;
113
+ }
114
+
115
+ /**
116
+ * Runs a {@link DeploymentSpec}: resolves accounts and deterministic addresses, inventories what's
117
+ * already on-chain, reports the plan, funds the working accounts, and executes the missing steps in
118
+ * dependency layers. Idempotent — a re-run sends nothing when everything is already in place.
119
+ */
120
+ export async function runDeployment<C extends Steps>(spec: DeploymentSpec<C>): Promise<void> {
121
+ const run = await DeploymentRun.create(spec);
122
+ await run.resolveDeterministicContracts();
123
+ await run.takeInventory();
124
+ const plan = await run.reportPlan();
125
+ if (run.hasNothingToDo()) {
126
+ run.reportNothingToDo();
127
+ await run.writeOutputs();
128
+ return;
129
+ }
130
+ const feeSession = await run.prepareFees(plan);
131
+ await run.executeLayers(feeSession);
132
+ await run.writeOutputs();
133
+ run.reportSummary();
134
+ }
135
+
136
+ /**
137
+ * One deployment run: the state shared across phases, with one method per phase. Build with
138
+ * {@link DeploymentRun.create}, then call the phase methods in the order {@link runDeployment}
139
+ * does — each phase reads state the earlier ones populated.
140
+ */
141
+ class DeploymentRun<C extends Steps> {
142
+ private readonly local: boolean;
143
+ private readonly label: string;
144
+ private readonly reporter: DeployReporter;
145
+ private readonly stateDirectory: string;
146
+ private readonly state: DeployState;
147
+ private readonly defaultSalt: Fr;
148
+ private readonly globalPolicy: FeePolicy;
149
+
150
+ // The steps map partitioned by kind once, so lookups are typed without casts.
151
+ private readonly steps: Map<string, StepSpec<C>>;
152
+ private readonly contractSteps = new Map<string, ContractStep<C>>();
153
+ private readonly actionSteps = new Map<string, ActionStep<C>>();
154
+ private readonly fundSteps = new Map<string, FundStep>();
155
+ /** Contract→contract address deps, auto-derived from deterministic `initializerArgs`. */
156
+ private readonly contractRefs: Map<string, string[]>;
157
+
158
+ // Populated as accounts resolve, contracts resolve (upfront, at inventory, or deferred at
159
+ // execution), and txs land.
160
+ private readonly accountAddresses = new Map<string, AztecAddress>();
161
+ private readonly policyByAddress = new Map<string, FeePolicy>();
162
+ private readonly contractAddresses = new Map<string, AztecAddress>();
163
+ private readonly contractInstances = new Map<string, ContractBase>();
164
+ private readonly deployMethods = new Map<string, DeployMethod<ContractBase>>();
165
+ private readonly classIds = new Map<string, Fr>();
166
+ private readonly publishedThisRun = new Set<string>();
167
+ private readonly classesPublishedThisRun = new Set<string>();
168
+ private readonly publishedCache = new Map<string, Promise<boolean>>();
169
+ private readonly gateCache = new Map<string, Promise<boolean>>();
170
+ private readonly gateInProgress = new Set<string>();
171
+ private readonly fundGateCache = new Map<string, Promise<boolean>>();
172
+
173
+ // Populated by the resolve / inventory / plan phases.
174
+ private resolveOrder: string[] = [];
175
+ private readonly actionsToRun = new Set<string>();
176
+ private readonly fundsToRun = new Set<string>();
177
+ private execAliases: string[] = [];
178
+ private layers: string[][] = [];
179
+
180
+ private readonly resolver: Resolver;
181
+ private readonly ctx: Ctx<C>;
182
+ /** Set only when the spec gave a URL — a connected node serves the debug API for warping itself. */
183
+ private readonly nodeUrl: string | undefined;
184
+
185
+ private constructor(
186
+ private readonly spec: DeploymentSpec<C>,
187
+ private readonly node: AztecNode,
188
+ private readonly wallet: EmbeddedWallet,
189
+ ) {
190
+ this.nodeUrl = typeof spec.node === 'string' ? spec.node : undefined;
191
+ this.local = spec.local ?? false;
192
+ this.label = spec.label ?? (this.local ? 'local' : 'network');
193
+ this.reporter = spec.reporter ?? consoleReporter();
194
+ this.stateDirectory = spec.stateDir ?? join(process.cwd(), '.deploy-state');
195
+ this.state = loadState(this.stateDirectory);
196
+ this.defaultSalt = spec.salt ?? new Fr(0);
197
+ this.globalPolicy = spec.fees ?? defaultFeePolicy(this.local);
198
+
199
+ this.steps = new Map(Object.entries(spec.steps) as [string, StepSpec<C>][]);
200
+ for (const [alias, step] of this.steps) {
201
+ if (step.kind === 'contract') {
202
+ this.contractSteps.set(alias, step);
203
+ } else if (step.kind === 'action') {
204
+ this.actionSteps.set(alias, step);
205
+ } else {
206
+ this.fundSteps.set(alias, step);
207
+ }
208
+ }
209
+ this.contractRefs = this.recordContractRefs();
210
+
211
+ this.resolver = {
212
+ account: alias => getOrThrow(this.accountAddresses, alias, 'account'),
213
+ contract: alias => getOrThrow(this.contractAddresses, alias, 'contract'),
214
+ };
215
+ this.ctx = {
216
+ ...this.resolver,
217
+ instance: ((alias: string) =>
218
+ getOrThrow(this.contractInstances, alias, 'contract instance')) as Ctx<C>['instance'],
219
+ done: id => this.done(id),
220
+ ran: id => this.ran(id),
221
+ wallet,
222
+ node,
223
+ };
224
+ }
225
+
226
+ /** Validates the spec, connects the node + ephemeral wallet, then resolves accounts and class ids. */
227
+ public static async create<C extends Steps>(spec: DeploymentSpec<C>): Promise<DeploymentRun<C>> {
228
+ DeploymentRun.validateSpec(spec);
229
+ const node = typeof spec.node === 'string' ? createAztecNodeClient(spec.node) : spec.node;
230
+ const wallet = await EmbeddedWallet.create(node, {
231
+ ephemeral: true,
232
+ pxeConfig: { ...getPXEConfig(), proverEnabled: !(spec.local ?? false) },
233
+ });
234
+ const run = new DeploymentRun(spec, node, wallet);
235
+ await run.resolveAccounts();
236
+ await run.computeClassIds();
237
+ return run;
238
+ }
239
+
240
+ /** Static spec validation — everything that can be rejected before touching the network. */
241
+ private static validateSpec<C extends Steps>(spec: DeploymentSpec<C>): void {
242
+ if (!spec.node) {
243
+ throw new Error('runDeployment requires a `node` on the spec — a JSON-RPC URL or a connected node.');
244
+ }
245
+ const steps = Object.entries(spec.steps) as [string, StepSpec<C>][];
246
+ const aliases = new Set(steps.map(([alias]) => alias));
247
+ for (const [alias, step] of steps) {
248
+ for (const dependency of step.dependsOn ?? []) {
249
+ if (!aliases.has(dependency)) {
250
+ throw new Error(`Unknown step "${dependency}" in dependsOn of "${alias}".`);
251
+ }
252
+ }
253
+ if (step.kind === 'fund' && step.amount <= 0n) {
254
+ throw new Error(`Fund step "${alias}" must bridge a positive amount.`);
255
+ }
256
+ if (step.kind !== 'contract') {
257
+ continue;
258
+ }
259
+ if (step.initializerArgs && step.deferredInitializerArgs) {
260
+ throw new Error(`Contract "${alias}" declares both initializerArgs and deferredInitializerArgs — pick one.`);
261
+ }
262
+ if (!isDeferred(step)) {
263
+ continue;
264
+ }
265
+ if (step.mode === 'register') {
266
+ throw new Error(`Contract "${alias}" is register-mode with deferred args — registration has no tx to defer.`);
267
+ }
268
+ if (step.dependsOn == null) {
269
+ throw new Error(
270
+ `Contract "${alias}" has deferred args but no dependsOn. Declare the steps whose effects the args read,` +
271
+ ` or an explicit empty array if they only read pre-existing state.`,
272
+ );
273
+ }
274
+ }
275
+ }
276
+
277
+ /** Resolves every deterministic contract upfront, layer by layer in constructor-arg dependency order. */
278
+ public async resolveDeterministicContracts(): Promise<void> {
279
+ const deterministicAliases = [...this.contractSteps]
280
+ .filter(([, step]) => !isDeferred(step))
281
+ .map(([alias]) => alias);
282
+ const layers = topologicalLayers(deterministicAliases, this.contractRefs);
283
+ this.resolveOrder = layers.flat();
284
+ for (const layer of layers) {
285
+ await Promise.all(
286
+ layer.map(alias => {
287
+ const step = getOrThrow(this.contractSteps, alias, 'contract');
288
+ return this.resolveContract(alias, step, step.initializerArgs?.(this.resolver) ?? []);
289
+ }),
290
+ );
291
+ }
292
+ }
293
+
294
+ /** Inventory: which steps still need doing? */
295
+ public async takeInventory(): Promise<void> {
296
+ // Deterministic publishes missing on-chain. The checks are independent node reads, so they run
297
+ // concurrently.
298
+ const missing = await Promise.all(
299
+ this.resolveOrder.map(async alias => {
300
+ const step = getOrThrow(this.contractSteps, alias, 'contract');
301
+ return step.mode === 'publish' && !(await this.isPublished(alias)) ? alias : undefined;
302
+ }),
303
+ );
304
+ for (const alias of compactArray(missing)) {
305
+ this.publishedThisRun.add(alias);
306
+ }
307
+
308
+ // Deferred contracts: try to resolve now — on a re-run the state their args read is already
309
+ // on-chain, so the address derives and publication is checked like any other contract (which is
310
+ // what makes re-runs no-ops). When the args can't resolve yet, the contract publishes this run
311
+ // and re-resolves at execution time, after its `dependsOn` has run. Runs before the action
312
+ // gates, so `ran`/`done` on a deferred contract answer correctly inside them.
313
+ for (const [alias, step] of this.contractSteps) {
314
+ if (isDeferred(step) && !(await this.tryResolveDeferred(alias, step))) {
315
+ this.publishedThisRun.add(alias);
316
+ }
317
+ }
318
+
319
+ // Fund gates: recipients whose public Fee Juice balance is below the step's threshold. Runs
320
+ // before the action gates so `ctx.done`/`ctx.ran` on a fund step answer correctly inside them.
321
+ for (const alias of this.fundSteps.keys()) {
322
+ if (!(await this.fundGate(alias))) {
323
+ this.fundsToRun.add(alias);
324
+ }
325
+ }
326
+
327
+ // Action gates. Sequential on purpose: evaluating them concurrently would turn the cyclic-gate
328
+ // detection (gateInProgress) into a silent deadlock of promises awaiting each other.
329
+ for (const alias of this.actionSteps.keys()) {
330
+ if (!(await this.actionGate(alias))) {
331
+ this.actionsToRun.add(alias);
332
+ }
333
+ }
334
+
335
+ this.execAliases = [
336
+ ...[...this.contractSteps.keys()].filter(alias => this.publishedThisRun.has(alias)),
337
+ ...[...this.fundSteps.keys()].filter(alias => this.fundsToRun.has(alias)),
338
+ ...[...this.actionSteps.keys()].filter(alias => this.actionsToRun.has(alias)),
339
+ ];
340
+ }
341
+
342
+ /**
343
+ * Builds the plan — account funding postures, per-step statuses, and the execution layers — and
344
+ * reports it (the default reporter renders it to stderr). The fund phase consumes its accounts.
345
+ */
346
+ public async reportPlan(): Promise<DeployPlan> {
347
+ const accountUsedBy = new Set<string>();
348
+ for (const alias of this.execAliases) {
349
+ const step = getOrThrow(this.steps, alias, 'step');
350
+ const address = step.kind === 'contract' ? step.deployer(this.resolver) : step.from(this.resolver);
351
+ accountUsedBy.add(address.toString());
352
+ }
353
+ const stepStatus = (alias: string, step: StepSpec<C>): DeployPlan['steps'][number]['status'] => {
354
+ if (step.kind === 'action') {
355
+ return this.actionsToRun.has(alias) ? 'to run' : 'done';
356
+ }
357
+ if (step.kind === 'fund') {
358
+ return this.fundsToRun.has(alias) ? 'to fund' : 'funded';
359
+ }
360
+ if (step.mode === 'register') {
361
+ return 'registered';
362
+ }
363
+ return this.publishedThisRun.has(alias) ? 'to publish' : 'published';
364
+ };
365
+ this.layers = this.buildLayers();
366
+ const plan: DeployPlan = {
367
+ label: this.label,
368
+ accounts: await Promise.all(
369
+ Object.keys(this.spec.accounts).map(async alias => {
370
+ const address = getOrThrow(this.accountAddresses, alias, 'account');
371
+ const policy = getOrThrow(this.policyByAddress, address.toString(), 'policy');
372
+ return {
373
+ alias,
374
+ address,
375
+ funding: await accountFunding(policy, this.wallet, address, accountUsedBy.has(address.toString())),
376
+ };
377
+ }),
378
+ ),
379
+ steps: [...this.steps].map(([id, step]) => ({
380
+ id,
381
+ kind: step.kind,
382
+ status: stepStatus(id, step),
383
+ dependsOn: [...(this.contractRefs.get(id) ?? []), ...(step.dependsOn ?? [])],
384
+ })),
385
+ layers: this.layers,
386
+ };
387
+ this.reporter.onPlan?.(plan);
388
+ return plan;
389
+ }
390
+
391
+ public hasNothingToDo(): boolean {
392
+ return this.execAliases.length === 0;
393
+ }
394
+
395
+ public reportNothingToDo(): void {
396
+ this.reporter.onNothingToDo?.(this.label);
397
+ }
398
+
399
+ /**
400
+ * Fund phase: prepares each working account's fee session per its resolved policy + funding
401
+ * (bridging Fee Juice when needed). The session owns all claim state — see
402
+ * {@link FeeSession.hasPendingClaim}.
403
+ */
404
+ public prepareFees(plan: DeployPlan): Promise<FeeSession> {
405
+ return prepareFeeSession({
406
+ local: this.local,
407
+ node: this.node,
408
+ nodeUrl: this.nodeUrl,
409
+ wallet: this.wallet,
410
+ state: this.state,
411
+ persist: () => this.persist(),
412
+ reporter: this.reporter,
413
+ accounts: plan.accounts
414
+ .filter(a => a.funding.kind !== 'idle')
415
+ .map(a => ({
416
+ address: a.address,
417
+ policy: getOrThrow(this.policyByAddress, a.address.toString(), 'policy'),
418
+ funding: a.funding,
419
+ })),
420
+ });
421
+ }
422
+
423
+ /** Executes the layers in order: publishes as individual txs, same-account actions batched. */
424
+ public async executeLayers(feeSession: FeeSession): Promise<void> {
425
+ for (const layer of this.layers) {
426
+ await this.runLayer(
427
+ [...this.publishUnits(layer), ...this.fundUnits(layer), ...this.actionUnits(layer)],
428
+ feeSession,
429
+ );
430
+ }
431
+ }
432
+
433
+ /** Persists resolved addresses and runs the spec's `output` hook against the final ctx. */
434
+ public async writeOutputs(): Promise<void> {
435
+ for (const [alias, address] of this.contractAddresses) {
436
+ this.state.addresses[alias] = address.toString();
437
+ }
438
+ this.persist();
439
+ await this.spec.output?.(this.ctx);
440
+ }
441
+
442
+ public reportSummary(): void {
443
+ const summary: DeploySummary = {
444
+ label: this.label,
445
+ contracts: [...this.contractAddresses].map(([alias, address]) => ({
446
+ alias,
447
+ address,
448
+ status: getOrThrow(this.contractSteps, alias, 'contract').mode === 'register' ? 'registered' : 'published',
449
+ })),
450
+ accounts: [...this.accountAddresses].map(([alias, address]) => ({ alias, address })),
451
+ };
452
+ this.reporter.onComplete?.(summary);
453
+ }
454
+
455
+ /** Accounts are initializerless (no deploy tx). Per-account salt + fee policy override the spec. */
456
+ private async resolveAccounts(): Promise<void> {
457
+ await Promise.all(
458
+ Object.entries(this.spec.accounts).map(async ([alias, account]) => {
459
+ const derived = await this.wallet.createSchnorrInitializerlessAccount(
460
+ account.secret,
461
+ account.salt ?? this.defaultSalt,
462
+ deriveMasterMessageSigningSecretKey(account.secret),
463
+ );
464
+ this.accountAddresses.set(alias, derived.address);
465
+ this.policyByAddress.set(derived.address.toString(), account.fees ?? this.globalPolicy);
466
+ }),
467
+ );
468
+ }
469
+
470
+ /**
471
+ * Class ids come from the ARTIFACT (not the instance/args), so they're known upfront for every
472
+ * contract — deferred ones included. This lets class-publish ordering cover all same-class
473
+ * contracts, so exactly one publishes the class and the rest are ordered after it (no race).
474
+ * Steps sharing a generated contract class share the artifact object, so the id is computed once
475
+ * per artifact identity — hashing it is CPU-heavy.
476
+ */
477
+ private async computeClassIds(): Promise<void> {
478
+ const idByArtifact = new Map<ContractArtifact, Promise<Fr>>();
479
+ await Promise.all(
480
+ [...this.contractSteps].map(async ([alias, step]) => {
481
+ if (step.mode !== 'publish') {
482
+ return;
483
+ }
484
+ let id = idByArtifact.get(step.contract.artifact);
485
+ if (!id) {
486
+ id = getContractClassFromArtifact(step.contract.artifact).then(contractClass => contractClass.id);
487
+ idByArtifact.set(step.contract.artifact, id);
488
+ }
489
+ this.classIds.set(alias, await id);
490
+ }),
491
+ );
492
+ }
493
+
494
+ /** Contract→contract address deps, auto-derived from each deterministic `initializerArgs`. */
495
+ private recordContractRefs(): Map<string, string[]> {
496
+ const contractRefs = new Map<string, string[]>();
497
+ for (const [alias, step] of this.contractSteps) {
498
+ contractRefs.set(alias, step.initializerArgs ? referencedContracts(step.initializerArgs) : []);
499
+ }
500
+ return contractRefs;
501
+ }
502
+
503
+ /**
504
+ * Publishes/registers a contract from already-computed initializer args (used upfront for
505
+ * deterministic contracts, and at inventory or execution time for deferred ones). Both modes
506
+ * derive the address from the full instantiation params — deployer and secret-derived public
507
+ * keys included — so a registered contract lands on the same address publishing it would.
508
+ */
509
+ private async resolveContract(alias: string, step: ContractStep<C>, args: unknown[]): Promise<void> {
510
+ this.publishedCache.delete(alias); // re-resolution may change the address
511
+ const salt = step.salt ?? this.defaultSalt;
512
+ const deployer = step.deployer(this.resolver);
513
+ const publicKeys = step.secret ? (await deriveKeys(step.secret)).publicKeys : undefined;
514
+ if (step.mode === 'publish') {
515
+ const deployMethod = DeployMethod.create<ContractBase>(
516
+ this.wallet,
517
+ {
518
+ artifact: step.contract.artifact,
519
+ postDeployCtor: (instance, boundWallet) => step.contract.at(instance.address, boundWallet),
520
+ args,
521
+ ...(step.initializer ? { constructorNameOrArtifact: step.initializer } : {}),
522
+ },
523
+ { deployer, salt, ...(publicKeys ? { publicKeys } : {}) },
524
+ );
525
+ const instance = await deployMethod.getInstance();
526
+ this.contractAddresses.set(alias, instance.address);
527
+ this.contractInstances.set(alias, step.contract.at(instance.address, this.wallet));
528
+ this.deployMethods.set(alias, deployMethod);
529
+ await this.wallet.registerContract(instance, step.contract.artifact, step.secret);
530
+ } else {
531
+ const instance = await getContractInstanceFromInstantiationParams(step.contract.artifact, {
532
+ salt,
533
+ deployer,
534
+ ...(publicKeys ? { publicKeys } : {}),
535
+ ...(args.length ? { constructorArgs: args } : {}),
536
+ ...(step.initializer ? { constructorArtifact: step.initializer } : {}),
537
+ });
538
+ this.contractAddresses.set(alias, instance.address);
539
+ this.contractInstances.set(alias, step.contract.at(instance.address, this.wallet));
540
+ await this.wallet.registerContract(instance, step.contract.artifact, step.secret);
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Attempts to resolve a deferred contract from current on-chain state, and reports whether there
546
+ * is no work left for it this run — the args resolved AND the instance is already published.
547
+ * Attempted only once every step it declares is in place, actions included: its args read their
548
+ * effects, so resolving earlier would derive the address from state that does not exist yet.
549
+ */
550
+ private async tryResolveDeferred(alias: string, step: ContractStep<C>): Promise<boolean> {
551
+ if (!(await this.dependenciesReady(alias, { contractsOnly: false }))) {
552
+ return false;
553
+ }
554
+ await this.runRecorded(alias, `Deferred initializer args for contract "${alias}"`, async ctx =>
555
+ this.resolveContract(alias, step, await step.deferredInitializerArgs!(ctx)),
556
+ );
557
+ return this.isPublished(alias);
558
+ }
559
+
560
+ /**
561
+ * Whether the steps `alias` declares as dependencies are all in place: a contract published or
562
+ * registered per its mode, a fund step above its threshold, an action already done. With
563
+ * `contractsOnly`, contracts alone — a gate exists to observe whether the actions and funds it
564
+ * follows landed, so it must still run once the contracts it reads are there.
565
+ */
566
+ private async dependenciesReady(alias: string, { contractsOnly }: { contractsOnly: boolean }): Promise<boolean> {
567
+ const step = getOrThrow(this.steps, alias, 'step');
568
+ for (const dependency of [...(this.contractRefs.get(alias) ?? []), ...(step.dependsOn ?? [])]) {
569
+ if (contractsOnly && getOrThrow(this.steps, dependency, 'step').kind !== 'contract') {
570
+ continue;
571
+ }
572
+ if (!(await this.done(dependency))) {
573
+ return false;
574
+ }
575
+ }
576
+ return true;
577
+ }
578
+
579
+ /**
580
+ * Runs a user callback against a ctx that records every contract alias it looks up, then requires
581
+ * those to be declared in the step's `dependsOn` — an undeclared read is state nothing checked was
582
+ * in place. The check runs even when the callback throws, so a callback that catches its own
583
+ * errors (the usual shape for a getter that reverts when unset) can't hide one.
584
+ */
585
+ private async runRecorded<T>(alias: string, description: string, run: (ctx: Ctx<C>) => Promise<T>): Promise<T> {
586
+ const references = new Set<string>();
587
+ const ctx: Ctx<C> = {
588
+ ...this.ctx,
589
+ contract: reference => {
590
+ references.add(reference);
591
+ return this.resolver.contract(reference);
592
+ },
593
+ instance: ((reference: string) => {
594
+ references.add(reference);
595
+ return getOrThrow(this.contractInstances, reference, 'contract instance');
596
+ }) as Ctx<C>['instance'],
597
+ };
598
+ let result: T;
599
+ try {
600
+ result = await run(ctx);
601
+ } catch (error) {
602
+ this.assertReferencesDeclared(alias, description, references, error);
603
+ throw error;
604
+ }
605
+ this.assertReferencesDeclared(alias, description, references);
606
+ return result;
607
+ }
608
+
609
+ /**
610
+ * Rejects contract aliases the callback read but the step doesn't declare. `cause` carries the
611
+ * error the callback threw, since an undeclared read is the likeliest reason it did.
612
+ */
613
+ private assertReferencesDeclared(alias: string, description: string, references: Set<string>, cause?: unknown): void {
614
+ const step = getOrThrow(this.steps, alias, 'step');
615
+ const declared = new Set([...(this.contractRefs.get(alias) ?? []), ...(step.dependsOn ?? [])]);
616
+ const undeclared = [...references].filter(reference => !declared.has(reference));
617
+ if (undeclared.length > 0) {
618
+ throw new Error(
619
+ `${description} reads ${undeclared.map(reference => `"${reference}"`).join(', ')} but does not declare it in` +
620
+ ` dependsOn. Declare every step it reads, so it only runs once they are in place.`,
621
+ { cause },
622
+ );
623
+ }
624
+ }
625
+
626
+ /** Whether the alias's resolved instance is published on-chain. Memoized — one node read per address. */
627
+ private isPublished(alias: string): Promise<boolean> {
628
+ const address = this.contractAddresses.get(alias);
629
+ if (!address) {
630
+ return Promise.resolve(false); // deferred & not yet resolved
631
+ }
632
+ let cached = this.publishedCache.get(alias);
633
+ if (!cached) {
634
+ cached = this.wallet.getContractMetadata(address).then(metadata => metadata.isContractPublished);
635
+ this.publishedCache.set(alias, cached);
636
+ }
637
+ return cached;
638
+ }
639
+
640
+ /**
641
+ * A fund step's idempotency gate: whether the recipient's public Fee Juice balance already
642
+ * clears the step's threshold. A recipient this run has yet to put in place holds nothing, so the
643
+ * balance is read only once the contracts its selector references are there. Memoized per run.
644
+ */
645
+ private fundGate(alias: string): Promise<boolean> {
646
+ const step = getOrThrow(this.fundSteps, alias, 'fund step');
647
+ let cached = this.fundGateCache.get(alias);
648
+ if (!cached) {
649
+ cached = (async () => {
650
+ for (const reference of referencedContracts(step.recipient)) {
651
+ if (!(await this.done(reference))) {
652
+ return false;
653
+ }
654
+ }
655
+ const recipient = step.recipient(this.resolver);
656
+ return (await publicFeeJuiceBalance(this.wallet, recipient, step.from(this.resolver))) >= step.threshold;
657
+ })();
658
+ this.fundGateCache.set(alias, cached);
659
+ }
660
+ return cached;
661
+ }
662
+
663
+ /**
664
+ * An action's `done` gate, memoized per run. Invoked only once the contracts the action declares
665
+ * are in place: until then the action cannot have run, and the gate would read state that does not
666
+ * exist. Gates may consult other steps via `ctx.done`, so a gate that (transitively) depends on
667
+ * itself throws {@link GateCycleError}.
668
+ */
669
+ private actionGate(alias: string): Promise<boolean> {
670
+ const step = this.actionSteps.get(alias);
671
+ if (!step) {
672
+ throw new Error(`Unknown action "${alias}".`);
673
+ }
674
+ if (this.gateInProgress.has(alias)) {
675
+ return Promise.reject(new GateCycleError(`Cyclic idempotency gate at "${alias}".`));
676
+ }
677
+ const cached = this.gateCache.get(alias);
678
+ if (cached) {
679
+ return cached;
680
+ }
681
+ this.gateInProgress.add(alias);
682
+ const pending = (async () => {
683
+ if (!(await this.dependenciesReady(alias, { contractsOnly: true }))) {
684
+ return false;
685
+ }
686
+ return this.runRecorded(alias, `Idempotency gate for action "${alias}"`, ctx => step.done(ctx));
687
+ })().finally(() => this.gateInProgress.delete(alias));
688
+ this.gateCache.set(alias, pending);
689
+ return pending;
690
+ }
691
+
692
+ /** `ctx.done`: whether step `id` is already satisfied this run — mode-aware. */
693
+ private done(id: string): Promise<boolean> {
694
+ const step = this.steps.get(id);
695
+ if (!step) {
696
+ throw new Error(`Unknown step "${id}".`);
697
+ }
698
+ if (step.kind === 'action') {
699
+ return this.actionGate(id);
700
+ }
701
+ if (step.kind === 'fund') {
702
+ return this.fundGate(id);
703
+ }
704
+ if (step.mode === 'register') {
705
+ return Promise.resolve(this.contractAddresses.has(id)); // registered in the PXE
706
+ }
707
+ return this.isPublished(id);
708
+ }
709
+
710
+ /** `ctx.ran`: whether step `id` did (or will do) work this run — mode-aware. */
711
+ private async ran(id: string): Promise<boolean> {
712
+ const step = this.steps.get(id);
713
+ if (!step) {
714
+ throw new Error(`Unknown step "${id}".`);
715
+ }
716
+ if (step.kind === 'action') {
717
+ return !(await this.actionGate(id));
718
+ }
719
+ if (step.kind === 'fund') {
720
+ return this.fundsToRun.has(id);
721
+ }
722
+ if (step.mode === 'register') {
723
+ return this.contractAddresses.has(id); // (re)registered this run
724
+ }
725
+ return this.publishedThisRun.has(id);
726
+ }
727
+
728
+ /**
729
+ * Builds the execution graph over the steps that run and groups it into layers. Exactly one
730
+ * contract per class publishes it (the first in declaration order); every other same-class
731
+ * contract — deterministic or deferred — is ordered after it.
732
+ */
733
+ private buildLayers(): string[][] {
734
+ const publisherByClass = new Map<string, string>();
735
+ for (const [alias, step] of this.contractSteps) {
736
+ if (step.mode !== 'publish') {
737
+ continue;
738
+ }
739
+ const classId = getOrThrow(this.classIds, alias, 'class id').toString();
740
+ if (!publisherByClass.has(classId)) {
741
+ publisherByClass.set(classId, alias);
742
+ }
743
+ }
744
+ const execDeps = new Map<string, string[]>();
745
+ for (const alias of this.execAliases) {
746
+ const step = getOrThrow(this.steps, alias, 'step');
747
+ // Constructor address refs (contractRefs) do NOT order publishes — addresses are deterministic,
748
+ // so a contract can publish in parallel with the ones it references. Only `dependsOn` (an action
749
+ // it follows / runtime state a deferred contract reads) and shared-class publication order here.
750
+ const deps = new Set<string>(step.dependsOn ?? []);
751
+ if (step.kind === 'contract' && step.mode === 'publish') {
752
+ const classId = this.classIds.get(alias)?.toString();
753
+ const publisher = classId ? publisherByClass.get(classId) : undefined;
754
+ if (publisher && publisher !== alias) {
755
+ deps.add(publisher);
756
+ }
757
+ }
758
+ execDeps.set(alias, [...deps]);
759
+ }
760
+ // Actions float as late as their dependents allow, so same-account actions coalesce into one
761
+ // batched tx; contract publishes stay early (they unblock dependents and aren't batched).
762
+ return scheduleLayers(this.execAliases, execDeps, id => getOrThrow(this.steps, id, 'step').kind === 'action');
763
+ }
764
+
765
+ /** A layer's contract publishes — one tx each (deferred contracts resolve their address first). */
766
+ private publishUnits(layer: string[]): ExecutionUnit[] {
767
+ const units: ExecutionUnit[] = [];
768
+ for (const alias of layer.filter(a => this.contractSteps.has(a))) {
769
+ const step = getOrThrow(this.contractSteps, alias, 'contract');
770
+ const account = step.deployer(this.resolver);
771
+ units.push({
772
+ label: `publish ${alias}`,
773
+ kind: 'publish',
774
+ account,
775
+ send: async fee => {
776
+ if (isDeferred(step)) {
777
+ // Re-resolve with post-`dependsOn` state: the inventory attempt either failed or ran
778
+ // before this run's earlier layers landed.
779
+ await this.resolveContract(alias, step, await step.deferredInitializerArgs!(this.ctx));
780
+ }
781
+ const classId = getOrThrow(this.classIds, alias, 'class id');
782
+ const deployMethod = getOrThrow(this.deployMethods, alias, 'deploy method');
783
+ const classKey = classId.toString();
784
+ const alreadyRegistered =
785
+ this.classesPublishedThisRun.has(classKey) ||
786
+ (await this.wallet.getContractClassMetadata(classId)).isContractClassPubliclyRegistered;
787
+ this.classesPublishedThisRun.add(classKey);
788
+ const sent = await deployMethod.send({
789
+ from: account,
790
+ fee,
791
+ wait: { timeout: 120 },
792
+ skipClassPublication: alreadyRegistered,
793
+ });
794
+ this.publishedCache.set(alias, Promise.resolve(true));
795
+ return sent;
796
+ },
797
+ });
798
+ }
799
+ return units;
800
+ }
801
+
802
+ /**
803
+ * A layer's fund steps — one unit each: obtain the recipient's claim (resume the persisted one,
804
+ * or bridge from L1 and wait for the message), then send the L2 `FeeJuice.claim` tx from the
805
+ * step's `from` account.
806
+ */
807
+ private fundUnits(layer: string[]): ExecutionUnit[] {
808
+ const units: ExecutionUnit[] = [];
809
+ for (const alias of layer.filter(a => this.fundSteps.has(a))) {
810
+ const step = getOrThrow(this.fundSteps, alias, 'fund step');
811
+ const account = step.from(this.resolver);
812
+ units.push({
813
+ label: `fund ${alias}`,
814
+ kind: 'fund',
815
+ account,
816
+ send: async fee => {
817
+ const recipient = step.recipient(this.resolver);
818
+ const claim = await obtainFeeJuiceClaim({
819
+ local: this.local,
820
+ node: this.node,
821
+ nodeUrl: this.nodeUrl,
822
+ recipient,
823
+ amount: step.amount,
824
+ l1: { l1FunderKey: step.l1FunderKey, l1RpcUrl: step.l1RpcUrl, l1ChainId: step.l1ChainId },
825
+ state: this.state,
826
+ persist: () => this.persist(),
827
+ reporter: this.reporter,
828
+ });
829
+ const sent = await FeeJuiceContract.at(this.wallet)
830
+ .methods.claim(recipient, claim.claimAmount, claim.claimSecret, claim.messageLeafIndex)
831
+ .send({ from: account, fee, wait: { timeout: 120 } });
832
+ claim.onConsumed();
833
+ return sent;
834
+ },
835
+ });
836
+ }
837
+ return units;
838
+ }
839
+
840
+ /** A layer's actions, batching independent same-account actions into ≤{@link APP_MAX_CALLS}-call BatchCalls. */
841
+ private actionUnits(layer: string[]): ExecutionUnit[] {
842
+ const actionsByAccount = new Map<string, { account: AztecAddress; aliases: string[] }>();
843
+ for (const alias of layer.filter(a => this.actionSteps.has(a))) {
844
+ const account = getOrThrow(this.actionSteps, alias, 'action').from(this.resolver);
845
+ const group = actionsByAccount.get(account.toString());
846
+ if (group) {
847
+ group.aliases.push(alias);
848
+ } else {
849
+ actionsByAccount.set(account.toString(), { account, aliases: [alias] });
850
+ }
851
+ }
852
+ const units: ExecutionUnit[] = [];
853
+ for (const { account, aliases } of actionsByAccount.values()) {
854
+ for (const batch of chunk(aliases, APP_MAX_CALLS)) {
855
+ units.push({
856
+ label: batch.length === 1 ? `action ${batch[0]}` : `batch [${batch.join(', ')}]`,
857
+ kind: 'action',
858
+ account,
859
+ send: async fee => {
860
+ const interactions = await Promise.all(
861
+ batch.map(alias => getOrThrow(this.actionSteps, alias, 'action').call(this.ctx)),
862
+ );
863
+ const sendOptions = { from: account, fee, wait: { timeout: 120 } };
864
+ return interactions.length === 1
865
+ ? interactions[0].send(sendOptions)
866
+ : new BatchCall(this.wallet, interactions).send(sendOptions);
867
+ },
868
+ });
869
+ }
870
+ }
871
+ return units;
872
+ }
873
+
874
+ private async runLayer(units: ExecutionUnit[], feeSession: FeeSession): Promise<void> {
875
+ if (units.length === 0) {
876
+ return;
877
+ }
878
+ // Per-account claim serialization: the first paying tx of each claim-holding account must mine
879
+ // (consuming + spending the claim) before that account's balance-payers fan out. The fee
880
+ // session owns the claim state; consuming the claim flips `hasPendingClaim` for later layers.
881
+ const claimFirst: ExecutionUnit[] = [];
882
+ const rest: ExecutionUnit[] = [];
883
+ const seen = new Set<string>();
884
+ for (const unit of units) {
885
+ const key = unit.account.toString();
886
+ if (feeSession.hasPendingClaim(unit.account) && !seen.has(key)) {
887
+ seen.add(key);
888
+ claimFirst.push(unit);
889
+ } else {
890
+ rest.push(unit);
891
+ }
892
+ }
893
+ for (const unit of claimFirst) {
894
+ await this.runUnit(unit, feeSession);
895
+ }
896
+ // Fund units run sequentially relative to each other: their send closures submit L1 bridge
897
+ // txs, and two bridges signed by the same funder key would race on nonces.
898
+ const fundRest = rest.filter(unit => unit.kind === 'fund');
899
+ const parallelRest = rest.filter(unit => unit.kind !== 'fund');
900
+ await Promise.all([
901
+ Promise.all(parallelRest.map(unit => this.runUnit(unit, feeSession))),
902
+ (async () => {
903
+ for (const unit of fundRest) {
904
+ await this.runUnit(unit, feeSession);
905
+ }
906
+ })(),
907
+ ]);
908
+ }
909
+
910
+ private async runUnit(unit: ExecutionUnit, feeSession: FeeSession): Promise<void> {
911
+ const info = { label: unit.label, kind: unit.kind, account: unit.account };
912
+ this.reporter.onUnitStart?.(info);
913
+ const { fee, onConsumed } = feeSession.next(unit.account);
914
+ const startedAt = Date.now();
915
+ let result: unknown;
916
+ try {
917
+ result = await unit.send(fee);
918
+ } catch (error) {
919
+ this.reporter.onUnitError?.(info, error);
920
+ throw error;
921
+ }
922
+ const receipt = (result as { receipt?: TxReceipt } | undefined)?.receipt;
923
+ this.reporter.onUnitSettled?.(info, {
924
+ txHash: receipt?.txHash,
925
+ blockNumber: receipt?.blockNumber != null ? Number(receipt.blockNumber) : undefined,
926
+ feePaid: receipt?.transactionFee,
927
+ status: receipt?.status != null ? String(receipt.status) : undefined,
928
+ durationMs: Date.now() - startedAt,
929
+ });
930
+ onConsumed();
931
+ }
932
+
933
+ private persist(): void {
934
+ saveState(this.stateDirectory, this.state);
935
+ }
936
+ }