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