@black-market/sdk 0.5.2 → 0.6.0
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/README.md +8 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +45 -10
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +37 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +84 -26
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +102 -30
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +27 -4
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +126 -15
- package/examples/.env.example +7 -10
- package/examples/README.md +24 -18
- package/examples/launch-example-support.mjs +8 -12
- package/examples/launch-example.mjs +15 -18
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +37 -9
- package/src/lifecycle/rpc.ts +72 -25
- package/src/lifecycle/simulation.ts +90 -28
- package/src/lifecycle/types.ts +21 -5
package/docs/launch-lifecycle.md
CHANGED
|
@@ -7,6 +7,16 @@ The canonical authorities are the registry/certification/adapter/config contract
|
|
|
7
7
|
in the protocol repository. Plan identity is `LaunchPlanV1`. Pool-bound V4 uses **config 5**,
|
|
8
8
|
canonical Abyss uses **config 1**, and generic current shared-V4 support uses **config 4**.
|
|
9
9
|
|
|
10
|
+
## 0.6.0 admission changes
|
|
11
|
+
|
|
12
|
+
Optional caller policy no longer substitutes for, or gates, actual sequential execution proof.
|
|
13
|
+
Results distinguish `executionProof`, `protocolFit`, and `transportPreflight`. Robinhood
|
|
14
|
+
admission reads pinned Nitro compute limits and poster budgets, then requires native exact
|
|
15
|
+
validated replay. `buildNextTransaction` optionally preflights the immediate next envelope
|
|
16
|
+
through `submissionClient`. Deployment addresses, lifecycle plan/schema domains, profile
|
|
17
|
+
capabilities and explicit atomic/staged behavior are unchanged.
|
|
18
|
+
|
|
19
|
+
|
|
10
20
|
## Deployed infrastructure
|
|
11
21
|
|
|
12
22
|
Robinhood mainnet (`4663`) selects the mined `pool-launch-v1` deployment captured at block
|
|
@@ -72,7 +82,7 @@ Validation uses `validateV4LifecycleMarket`; the collector factory getter is
|
|
|
72
82
|
| `planLaunch({client, account, plan, mode, limits?, fork?, receipts?, confirmations?})` | Validates domain/funding/admission and canonical progress, records the exact atomic attempt, and reviews only the explicitly selected mode. |
|
|
73
83
|
| `simulateLaunchPlan({client, planned, limits?, fork?, receipts?, confirmations?})` | Rereads canonical state and proves remaining work. Preserves receipt evidence, economics and selected mode. |
|
|
74
84
|
| `readLaunchProgress({client, planned, receipts?, confirmations?})` | Confirmation-bound canonical state, latest head, receipt/replacement/reorg status, account-nonce safety and actual prepared market/live state. |
|
|
75
|
-
| `buildNextTransaction({client, planned, limits?, fork?, receipts?, confirmations?, action?})` | Rereads progress, constraints and stateful proof before returning one admitted transaction. `action: "cancel"` uses the stored commitment without requiring fresh adapter/profile admission. Terminal launches return `undefined`. |
|
|
85
|
+
| `buildNextTransaction({client, planned, submissionClient?, limits?, fork?, receipts?, confirmations?, action?})` | Rereads progress, constraints and stateful proof before returning one admitted transaction. Optional `submissionClient` adds read-only exact immediate envelope preflight. `action: "cancel"` uses the stored commitment without requiring fresh adapter/profile admission. Terminal launches return `undefined`. |
|
|
76
86
|
|
|
77
87
|
Clients may be viem public clients or raw `request({method,params})` RPC clients. Transport, wallet, signing and durable storage are application-owned. `PlannedLaunch` contains bigint values and live limit callbacks: persist `serializeLaunchPlan(plan)`, explicit execution mode, confirmation policy and receipt references, not the entire object.
|
|
78
88
|
|
|
@@ -186,15 +196,80 @@ No automatic atomic-to-staged fallback exists. Obtain separate consent to review
|
|
|
186
196
|
|
|
187
197
|
## Execution constraints and stateful simulation
|
|
188
198
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
Optional `chainGasLimit`, `rpcGasLimit`, `accountGasLimit`, `maxCalldataBytes` and
|
|
200
|
+
`maxSimulationGas` tighten admission when supplied. Missing policy remains explicit in
|
|
201
|
+
`limits.unknownExecutionConstraints`/`unknownConstraints`; it does not reject an otherwise
|
|
202
|
+
proved exact sequence or invent a provider/account cap. No policy service is required.
|
|
203
|
+
A `LifecycleLimitSource` callback receives the canonical chain/account/core/block context.
|
|
204
|
+
Any supplied provenance must match it; stale/mismatched/malformed facts or a failed supplied
|
|
205
|
+
resolver reject rather than disappearing into optional uncertainty. Gas caps are positive
|
|
206
|
+
exact uint64 `bigint`; calldata is a positive safe integer byte count.
|
|
207
|
+
Reviewed gas envelopes are positive uint64 too; zero-metering observations cannot admit
|
|
208
|
+
a zero-gas transaction. Headroom calculations retain wide exact bigint intermediates.
|
|
209
|
+
|
|
210
|
+
Headroom defaults to 1,500 bps, bounded 0–10,000. Generic EVM admission uses the pinned
|
|
211
|
+
bounded header and supplied total-envelope restrictions plus actual validated RPC proof.
|
|
212
|
+
It reports an absent independent per-transaction cutoff honestly, without importing a
|
|
213
|
+
universal Ethereum cutoff. `executionGasCeiling` and `transactionGasCeiling` are equal on
|
|
214
|
+
generic EVM. The permissive measurement pass never admits a launch: replay must execute
|
|
215
|
+
the exact returned gas/fee envelope, every postcondition and all ordered buy bounds.
|
|
216
|
+
Fork discovery uses gross sequential estimates, not refund-reduced receipt gas as an
|
|
217
|
+
executable minimum. Explicit ceiling recovery preserves gasleft/EIP-150 validation and
|
|
218
|
+
carries that successfully replayed ceiling, never a smaller unproved envelope.
|
|
192
219
|
|
|
193
220
|
Sequential backends:
|
|
194
221
|
|
|
195
|
-
1. `eth_simulateV1`:
|
|
196
|
-
2.
|
|
197
|
-
3. Neither: unavailable/provisional/unadmitted;
|
|
222
|
+
1. `eth_simulateV1`: ordered transactions with state carried between synthetic blocks, followed by exact validated replay.
|
|
223
|
+
2. Generic EVM only: explicitly separate disposable loopback fork, with snapshot-isolated real transactions, pinned chain/header identity, canonical poststate/allowances and mandatory cleanup. Source RPC stays read-only.
|
|
224
|
+
3. Neither: unavailable/provisional/unadmitted; disconnected dependent `eth_call` estimates cannot create an executable next transaction.
|
|
225
|
+
|
|
226
|
+
Every proof completion rechecks the planned source chain ID and pinned block hash. A
|
|
227
|
+
changed source transport cannot retain admission merely by returning the same fork hash.
|
|
228
|
+
|
|
229
|
+
### Nitro compute and poster accounting
|
|
230
|
+
|
|
231
|
+
For Robinhood `4663`, each pinned context reads ArbSys (`0x64`).`arbOSVersion` and ArbGasInfo
|
|
232
|
+
(`0x6c`).`getMaxTxGasLimit`/`getMaxBlockGasLimit`. ArbSys returns **55 + logical ArbOS version**;
|
|
233
|
+
the raw value must be at least 105, and `limits.arbOSVersion` reports logical version 50+.
|
|
234
|
+
The positive validated getter values establish `maxTxComputeGas`, `maxBlockComputeGas`
|
|
235
|
+
and their minimum `executionGasCeiling`. Failed reads never fall back to the huge Nitro
|
|
236
|
+
header or a hard-coded 32M. `limits.protocol` identifies `"nitro"` or `"evm"`.
|
|
237
|
+
|
|
238
|
+
Nitro `transactionGasCeiling`, when present, is the minimum of supplied **complete**
|
|
239
|
+
chain/RPC/account gas-envelope restrictions. It is independent of the compute ceiling:
|
|
240
|
+
an admitted transaction's `gas` may exceed `executionGasCeiling` because poster gas is
|
|
241
|
+
not compute. NodeInterface (`0xc8`).`gasEstimateL1Component` is read at the same pinned
|
|
242
|
+
block with the exact sender, destination, calldata and value, including future dependent
|
|
243
|
+
steps whose execution cannot yet be independently estimated. It budgets poster gas only.
|
|
244
|
+
The buffered compute-discovery envelope plus buffered poster budget is carried into
|
|
245
|
+
exact validated replay; an approximate poster quote is never subtracted from total
|
|
246
|
+
gas use to certify exact compute fit.
|
|
247
|
+
|
|
248
|
+
A separate pinned validated simulation probes all three ArbOS/version/getter values
|
|
249
|
+
against the canonical observations. Only that isolated capability probe temporarily
|
|
250
|
+
overrides the probe account's balance, so probe-only fees cannot reject an exactly-funded
|
|
251
|
+
launch. Probe funding/nonce changes, steps and fees never enter real plan measurement,
|
|
252
|
+
replay, postconditions or affordability, which use actual payer state. The native Nitro
|
|
253
|
+
`eth_simulateV1` commit-mode ArbOS charging hook removes the **actual** poster charge and
|
|
254
|
+
holds excess compute gas before execution. Successful exact replay and postconditions,
|
|
255
|
+
not the quote or permissive pass, prove fit. Missing native metering or a mismatched
|
|
256
|
+
probe fails closed. Generic Anvil/Hardhat forks cannot certify Nitro execution.
|
|
257
|
+
|
|
258
|
+
Transactions expose `estimate.executionFee`, optional external `dataFee`/`totalFee` and
|
|
259
|
+
`feeConfidence: "execution-and-data" | "execution-only"`. On Nitro, `posterGas`/`posterFee`
|
|
260
|
+
are the estimated buffered allocation **inside** `gasLimit`, `dataFeeIncludedInGas` is
|
|
261
|
+
true, `dataFee` is zero and `totalFee` equals `gasLimit * gasPrice`. Affordability counts
|
|
262
|
+
this complete envelope once; adding `posterFee` again would double-charge. A caller
|
|
263
|
+
`estimateDataFee` is therefore rejected on Nitro. On generic EVM it retains its original
|
|
264
|
+
semantics as a validated external fee added outside execution gas. Missing external fee
|
|
265
|
+
does not manufacture an all-in estimate. All observations and limits refresh for build-next.
|
|
266
|
+
|
|
267
|
+
Source references: [ArbSys version encoding](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/main/ArbSys.sol),
|
|
268
|
+
[ArbGasInfo compute getters](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/main/ArbGasInfo.sol),
|
|
269
|
+
[native simulate commit mode](https://github.com/OffchainLabs/go-ethereum/blob/51d716ccf8293072077feb9764d63d73f5e4ff20/internal/ethapi/simulate.go),
|
|
270
|
+
[ArbOS actual poster/compute charging](https://github.com/OffchainLabs/nitro/blob/ccc5c828d9ea22f161efc784bac4d4a6559b7e59/arbos/tx_processor.go),
|
|
271
|
+
and [NodeInterface poster estimate](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/node-interface/NodeInterface.sol).
|
|
272
|
+
These describe backend semantics, not evidence of a live launch executed by this SDK release.
|
|
198
273
|
|
|
199
274
|
Atomic/activation simulation `steps[].returnData` encodes the actual `LaunchReceiptV1` tuple. Controlled-fork output is reconstructed from matching real activation/buy receipt logs and canonical state, never geometric estimates or echoed request minima. `tokenOut` and `quoteSpent` are parallel arrays indexed by ordered buys.
|
|
200
275
|
|
|
@@ -206,12 +281,48 @@ const fork = createControlledLifecycleFork({
|
|
|
206
281
|
const planned = await planLaunch({client, account: plan.creator, plan, mode: "staged", limits, fork});
|
|
207
282
|
```
|
|
208
283
|
|
|
209
|
-
The fork endpoint must be distinct/loopback, including localhost/IP aliases and effective port. Anvil reset/impersonation is default; disposable Hardhat may explicitly use its reset method/impersonation. Never pass the source client as a fork or use a live unlocked wallet as a disposable backend. Operators own exclusive fork access during proof. Controlled-local
|
|
284
|
+
The fork endpoint must be distinct/loopback, including localhost/IP aliases and effective port. Anvil reset/impersonation is default; disposable Hardhat may explicitly use its reset method/impersonation. Never pass the source client as a fork or use a live unlocked wallet as a disposable backend. Operators own exclusive fork access during generic EVM proof. Controlled-local execution does not prove wallet submission acceptance and cannot certify Nitro metering or imply bound topology is a gas optimization.
|
|
210
285
|
|
|
211
286
|
## Wallet submission and canonical recovery
|
|
212
287
|
|
|
213
288
|
The SDK never signs. Before **every** approval/reset/core/predeploy/cancel signature, compare the wallet's configured chain (if any) and fresh connected `getChainId()` with the returned `next.chainId`. Failure/invalid identity is a refusal. Supply a matching explicit viem Chain descriptor; do not pass `chain: null` or disable wallet chain checks. Send exact `from/to/data/value/gas/gasPrice`, not a cached estimate or wallet batch.
|
|
214
289
|
|
|
290
|
+
Simulation and `next.admission` carry flat, independent proof outcomes:
|
|
291
|
+
|
|
292
|
+
| Field | Values | Scope |
|
|
293
|
+
| --- | --- | --- |
|
|
294
|
+
| `executionProof` | `"proved"`, `"failed"`, `"unavailable"` | Complete stateful exact-envelope execution and postconditions, never permissive measurement alone. |
|
|
295
|
+
| `protocolFit` | `"proved"`, `"failed"`, `"unknown"` | Applicable proven protocol and known tightening restrictions. Missing optional policy remains separately disclosed. |
|
|
296
|
+
| `transportPreflight` | `"not-requested"`, `"passed"`, `"failed"` | Immediate exact next-transaction acceptance estimate on the supplied submission RPC. |
|
|
297
|
+
|
|
298
|
+
`admitted` means the current exact sequence is proved to fit and affordable; it is
|
|
299
|
+
not guaranteed wallet submission or future execution. Admission reuses simulation
|
|
300
|
+
`blockNumber`, `blockHash`, `account`, `chainId` and `limits`. Plans never preflight
|
|
301
|
+
future dependent transactions against state that does not exist.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
const planned = await planLaunch({ client, account: plan.creator, plan, mode: "atomic" });
|
|
305
|
+
const submissionClient = {
|
|
306
|
+
request: ({ method, params }) => activeProvider.request({ method, params }),
|
|
307
|
+
};
|
|
308
|
+
const next = await buildNextTransaction({ client, planned, receipts, submissionClient });
|
|
309
|
+
// Freshly verify the active wallet account/connector/chain again, then review/sign next.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When supplied, `submissionClient` receives only read-only `eth_chainId` and
|
|
313
|
+
`eth_estimateGas` for the immediate next `from/to/data/value/gas/gasPrice` at `latest`.
|
|
314
|
+
Its positive estimate must fit the reviewed gas exactly; gas is never silently enlarged.
|
|
315
|
+
Chain identity is checked before and after estimation, and the source pinned hash/chain
|
|
316
|
+
is rechecked before returning. Success sets `next.admission.transportPreflight` to
|
|
317
|
+
`"passed"`. Refusal, unsupported RPC, malformed estimate, over-envelope estimate or
|
|
318
|
+
chain drift raises `SUBMISSION_PREFLIGHT_FAILED` without returning a transaction.
|
|
319
|
+
The error's `simulation` retains execution admission and its canonical context, changing
|
|
320
|
+
only transport outcome/reason to `"failed"`. Cancellation uses the same preflight.
|
|
321
|
+
Without a submission client, `"not-requested"` remains honest and no wallet RPC is implied.
|
|
322
|
+
Applications still own fresh active account/provider guards around asynchronous work;
|
|
323
|
+
a read-only preflight does not authenticate an active wallet or authorize a signature.
|
|
324
|
+
|
|
325
|
+
|
|
215
326
|
Persist transaction hashes immediately. After receipt, retain `observedBlockNumber`, `observedBlockHash`, confirmation policy and, for replacements, original `transactionHash` plus effective `replacementHash`. A same-nonce self-transfer is replacement-cancelled, not launch completion. Removed observed blocks are reorged; status zero is reverted; a missing receipt at the unchanged observed block remains pending. Unknown hashes do not invent reorgs.
|
|
216
327
|
|
|
217
328
|
`confirmations` is positive and defaults to 1. Progress anchors confirmed canonical and latest head hashes, compares confirmed/head/pending account nonces, and prevents ref-free reloads from advancing unconfirmed work. Pending receipts/state return no executable envelope (`RECEIPT_PENDING`/`UNCONFIRMED_STATE`). Reload the exact plan/mode/policy/receipt references; never trust a cached step counter. Market read failures are surfaced without removing adapter-independent cancellation.
|
|
@@ -310,13 +421,13 @@ never changes mode, drops positions/buys or overrides failed admission.
|
|
|
310
421
|
Optional `NFT_BASE_URI` provides an ERC404 base URI you actually host; its default is
|
|
311
422
|
empty, with real NFT units/mirror behavior but no claim of hosted NFT metadata.
|
|
312
423
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
424
|
+
The chain-4663 examples require native sequential Nitro `eth_simulateV1`; generic
|
|
425
|
+
Anvil/Hardhat simulation is not an ArbOS compute/poster proof. Source RPC requests
|
|
426
|
+
have a 120-second bound and are not retried. There are no invented default gas or
|
|
427
|
+
calldata caps. Explicit environment restrictions only tighten the complete envelope;
|
|
428
|
+
headroom defaults to 1,500 bps. SDK native pinned compute/poster reads and exact replay
|
|
429
|
+
remain mandatory, and build-next uses the same source RPC as the read-only submission
|
|
430
|
+
preflight before each local signature. Environment options are documented in the example guide.
|
|
320
431
|
|
|
321
432
|
Execution rechecks chain/creator/destination and exact finalized calldata before each
|
|
322
433
|
send. Exact admitted envelopes are signed locally; computed hashes and unsigned
|
package/examples/.env.example
CHANGED
|
@@ -5,16 +5,13 @@ LAUNCH_API_URL=
|
|
|
5
5
|
|
|
6
6
|
# Optional: blank uses the SDK official Robinhood chain-4663 mainnet RPC.
|
|
7
7
|
RPC_URL=
|
|
8
|
-
# Optional: separate owned loopback Anvil used only for SDK controlled simulation.
|
|
9
|
-
# Blank selects the actual RPC native eth_simulateV1 backend.
|
|
10
|
-
SIMULATION_RPC_URL=
|
|
11
8
|
# Optional ERC404 NFT metadata base URI that you actually host; blank means empty URI.
|
|
12
9
|
NFT_BASE_URI=
|
|
13
10
|
|
|
14
|
-
# Optional
|
|
15
|
-
#
|
|
16
|
-
# EXAMPLE_CHAIN_GAS_CAP=
|
|
17
|
-
# EXAMPLE_RPC_GAS_CAP=
|
|
18
|
-
# EXAMPLE_ACCOUNT_GAS_CAP=
|
|
19
|
-
# EXAMPLE_MAX_CALLDATA_BYTES=
|
|
20
|
-
# EXAMPLE_HEADROOM_BPS=
|
|
11
|
+
# Optional local tightening restrictions. Blank/unset means unknown, not refused.
|
|
12
|
+
# Nitro protocol compute caps and poster budgeting are read natively at the pinned block.
|
|
13
|
+
# EXAMPLE_CHAIN_GAS_CAP=
|
|
14
|
+
# EXAMPLE_RPC_GAS_CAP=
|
|
15
|
+
# EXAMPLE_ACCOUNT_GAS_CAP=
|
|
16
|
+
# EXAMPLE_MAX_CALLDATA_BYTES=
|
|
17
|
+
# EXAMPLE_HEADROOM_BPS=1500
|
package/examples/README.md
CHANGED
|
@@ -42,12 +42,15 @@ commitment before begin. `--receipts receipts.json` and `--confirmations N` rest
|
|
|
42
42
|
canonical recovery context. The CLI never signs, submits source-chain transactions,
|
|
43
43
|
uploads metadata, or publishes a launch. Modes never fall back silently.
|
|
44
44
|
|
|
45
|
-
Executable admission
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
Executable admission requires actual stateful measurement and exact validated replay.
|
|
46
|
+
`--limits current-limits.json` is optional tightening policy, not a required backend:
|
|
47
|
+
missing caps stay explicit uncertainty, while supplied malformed/stale/mismatched
|
|
48
|
+
provenance rejects. Encode bigint values as decimal strings. Robinhood `4663` reads
|
|
49
|
+
pinned native Nitro compute/poster evidence and requires native `eth_simulateV1`.
|
|
50
|
+
A **separate disposable loopback fork** (`--fork-rpc URL`) remains available for
|
|
51
|
+
generic EVM, not as a Nitro metering substitute. Unsigned review reports
|
|
52
|
+
`transportPreflight: "not-requested"` unless a consumer supplies its active submission
|
|
53
|
+
RPC to build-next. See the
|
|
51
54
|
[lifecycle guide](../docs/launch-lifecycle.md#execution-constraints-and-stateful-simulation).
|
|
52
55
|
|
|
53
56
|
## Real HTTP metadata sessions
|
|
@@ -91,7 +94,6 @@ Edit `.env` once:
|
|
|
91
94
|
PRIVATE_KEY=
|
|
92
95
|
LAUNCH_API_URL=
|
|
93
96
|
RPC_URL=
|
|
94
|
-
SIMULATION_RPC_URL=
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
Set `PRIVATE_KEY` to **your funded EOA's** 32-byte hex key and `LAUNCH_API_URL` to
|
|
@@ -111,11 +113,13 @@ Optional `NFT_BASE_URI` supplies your actually hosted ERC404 NFT metadata base U
|
|
|
111
113
|
Blank uses an empty base URI: NFT units/mirror behavior remain real, but the example
|
|
112
114
|
does not claim hosted NFT metadata exists.
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
The fixed Robinhood examples require native sequential Nitro `eth_simulateV1`, including
|
|
117
|
+
an isolated pinned ArbOS capability probe and exact fee-bearing replay. Generic
|
|
118
|
+
Anvil/Hardhat forks cannot certify Nitro compute/poster metering and are not a fallback.
|
|
117
119
|
Unsupported native simulation, insufficient funds or failed admission stop before API
|
|
118
|
-
staging/broadcast.
|
|
120
|
+
staging/broadcast. Each immediate next envelope is additionally preflighted read-only
|
|
121
|
+
through the same execution RPC before signing. There is no mode fallback, admission
|
|
122
|
+
override, automatic submission replay or reduced case.
|
|
119
123
|
|
|
120
124
|
### Commands and fixed cases
|
|
121
125
|
|
|
@@ -148,14 +152,16 @@ selected; token-only burn is 3000 bps in the burn case. V4 uses frozen registere
|
|
|
148
152
|
treasury/author terms with developer rate `0`; executor fee is 275 bps.
|
|
149
153
|
Hook salts/random identities are finalized and saved before execution.
|
|
150
154
|
|
|
151
|
-
###
|
|
155
|
+
### Optional execution restrictions
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
`
|
|
157
|
+
There are no default chain/RPC/account gas or calldata caps. The SDK reads current
|
|
158
|
+
Nitro protocol compute ceilings and exact-calldata poster budgets itself. Default
|
|
159
|
+
headroom is **1500 bps (15%)**. Set explicit decimal restrictions only when you intend
|
|
160
|
+
to tighten the complete transaction envelope: `EXAMPLE_CHAIN_GAS_CAP`,
|
|
161
|
+
`EXAMPLE_RPC_GAS_CAP`, `EXAMPLE_ACCOUNT_GAS_CAP`, `EXAMPLE_MAX_CALLDATA_BYTES`.
|
|
162
|
+
`EXAMPLE_HEADROOM_BPS` preserves your explicit 0–10000 policy. These are local
|
|
163
|
+
restrictions, not invented provider/account assurances or admission bypasses.
|
|
164
|
+
Poster allocation is already inside Nitro gas/affordability and is not charged twice.
|
|
159
165
|
|
|
160
166
|
### Evidence and failure recovery
|
|
161
167
|
|
|
@@ -70,7 +70,7 @@ export function loadExampleEnvironment() {
|
|
|
70
70
|
export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
71
71
|
redactor.addSecret(env.PRIVATE_KEY);
|
|
72
72
|
if (typeof env.PRIVATE_KEY === "string") redactor.addSecret(env.PRIVATE_KEY.slice(2));
|
|
73
|
-
for (const value of [env.RPC_URL, env.LAUNCH_API_URL, env.
|
|
73
|
+
for (const value of [env.RPC_URL, env.LAUNCH_API_URL, env.NFT_BASE_URI]) registerEndpointSecrets(redactor, value);
|
|
74
74
|
if (!env.PRIVATE_KEY?.trim()) throw exampleError("MISSING_CONFIGURATION", "PRIVATE_KEY is required in the environment or working-directory .env");
|
|
75
75
|
if (!env.LAUNCH_API_URL?.trim()) throw exampleError("MISSING_CONFIGURATION", "LAUNCH_API_URL is required in the environment or working-directory .env");
|
|
76
76
|
if (!/^0x[\da-f]{64}$/i.test(env.PRIVATE_KEY)) throw exampleError("INVALID_PRIVATE_KEY", "PRIVATE_KEY must be a 32-byte hex private key");
|
|
@@ -79,24 +79,20 @@ export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
|
79
79
|
catch { throw exampleError("INVALID_PRIVATE_KEY", "PRIVATE_KEY must be a valid 32-byte secp256k1 hex private key"); }
|
|
80
80
|
const addresses = sdk.getAddresses(4663);
|
|
81
81
|
const rpcUrl = executionRpcUrl(env.RPC_URL || sdk.robinhoodMainnet.rpcUrls.default.http[0]);
|
|
82
|
-
const forkRpcUrl = env.SIMULATION_RPC_URL ? loopbackUrl(env.SIMULATION_RPC_URL, "Simulation RPC") : undefined;
|
|
83
|
-
if (forkRpcUrl) {
|
|
84
|
-
const source = new URL(rpcUrl); const fork = new URL(forkRpcUrl);
|
|
85
|
-
const port = (url) => url.port || (url.protocol === "https:" ? "443" : "80");
|
|
86
|
-
if (["127.0.0.1", "localhost", "[::1]"].includes(source.hostname) && port(source) === port(fork)) throw exampleError("UNSAFE_ENDPOINT", "Execution and disposable simulation RPCs must use distinct loopback ports");
|
|
87
|
-
}
|
|
88
82
|
const gasCap = (key) => {
|
|
89
|
-
const value = env[key]
|
|
83
|
+
const value = env[key];
|
|
84
|
+
if (value === undefined || value === "") return undefined;
|
|
90
85
|
if (!/^[1-9]\d*$/.test(value)) throw exampleError("INVALID_CONFIGURATION", `${key} must be a positive decimal integer`);
|
|
91
86
|
return value;
|
|
92
87
|
};
|
|
93
88
|
const integer = (key, fallback, minimum, maximum) => {
|
|
89
|
+
if (fallback === undefined && (env[key] === undefined || env[key] === "")) return undefined;
|
|
94
90
|
const value = env[key] || String(fallback);
|
|
95
91
|
if (!/^(?:0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < minimum || Number(value) > maximum) throw exampleError("INVALID_CONFIGURATION", `${key} is outside its allowed integer range`);
|
|
96
92
|
return Number(value);
|
|
97
93
|
};
|
|
98
94
|
return {
|
|
99
|
-
chainId: "4663", rpcUrl,
|
|
95
|
+
chainId: "4663", rpcUrl, apiUrl: launchApiUrl(env.LAUNCH_API_URL),
|
|
100
96
|
orchestrator: addresses.launchOrchestrator, creator: account.address,
|
|
101
97
|
quoteAsset: addresses.weth, quoteDecimals: 18, oracleFactory: addresses.abyssFactory,
|
|
102
98
|
nftBaseUri: env.NFT_BASE_URI || "",
|
|
@@ -104,9 +100,9 @@ export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
|
104
100
|
chainTransactionGasLimit: gasCap("EXAMPLE_CHAIN_GAS_CAP"),
|
|
105
101
|
rpcTransactionGasLimit: gasCap("EXAMPLE_RPC_GAS_CAP"),
|
|
106
102
|
accountTransactionGasLimit: gasCap("EXAMPLE_ACCOUNT_GAS_CAP"),
|
|
107
|
-
maxCalldataBytes: integer("EXAMPLE_MAX_CALLDATA_BYTES",
|
|
108
|
-
headroomBps: integer("EXAMPLE_HEADROOM_BPS",
|
|
109
|
-
provenance: { scope: "
|
|
103
|
+
maxCalldataBytes: integer("EXAMPLE_MAX_CALLDATA_BYTES", undefined, 1, Number.MAX_SAFE_INTEGER),
|
|
104
|
+
headroomBps: integer("EXAMPLE_HEADROOM_BPS", 1500, 0, 10000),
|
|
105
|
+
provenance: { scope: "user-ceilings", note: "Optional local tightening restrictions, not verified provider or account limits" },
|
|
110
106
|
},
|
|
111
107
|
};
|
|
112
108
|
}
|
|
@@ -60,7 +60,7 @@ function rpcClient(url, artifacts, signal, label) {
|
|
|
60
60
|
const body = await response.json();
|
|
61
61
|
if (body.error) throw exampleError(body.error.code ?? "RPC_ERROR", `${label} ${method}: ${body.error.message}`, { data: body.error.data, cause: body.error });
|
|
62
62
|
if (!("result" in body)) throw exampleError("INVALID_RPC_RESPONSE", `${label} ${method}: result was omitted`, { data: body });
|
|
63
|
-
if (method === "eth_simulateV1"
|
|
63
|
+
if (method === "eth_simulateV1") artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
|
|
64
64
|
return body.result;
|
|
65
65
|
} catch (error) {
|
|
66
66
|
artifacts.event("rpc-error", { backend: label, method, params, elapsedMs: Date.now() - started, error: errorEvidence(error) });
|
|
@@ -256,20 +256,22 @@ function limitSource(fixture, artifacts) {
|
|
|
256
256
|
return async ({ block, chainId, account, orchestrator }) => {
|
|
257
257
|
const limits = {
|
|
258
258
|
chainId, account, orchestrator, observedBlockNumber: block.number, observedBlockHash: block.hash,
|
|
259
|
-
chainGasLimit:
|
|
260
|
-
|
|
259
|
+
chainGasLimit: policy.chainTransactionGasLimit === undefined ? undefined : BigInt(policy.chainTransactionGasLimit),
|
|
260
|
+
rpcGasLimit: policy.rpcTransactionGasLimit === undefined ? undefined : BigInt(policy.rpcTransactionGasLimit),
|
|
261
|
+
accountGasLimit: policy.accountTransactionGasLimit === undefined ? undefined : BigInt(policy.accountTransactionGasLimit),
|
|
262
|
+
maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
|
|
261
263
|
};
|
|
262
264
|
artifacts.event("execution-limits", { limits, provenance: policy.provenance });
|
|
263
265
|
return limits;
|
|
264
266
|
};
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits,
|
|
269
|
+
async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, wallet }) {
|
|
268
270
|
const references = [];
|
|
269
271
|
let activation;
|
|
270
272
|
for (let step = 0; step < 64; step += 1) {
|
|
271
273
|
artifacts.stage("build-next", { step });
|
|
272
|
-
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits,
|
|
274
|
+
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client });
|
|
273
275
|
if (!next) return { references, activation };
|
|
274
276
|
artifacts.event("next-admitted", { transaction: next });
|
|
275
277
|
assert.equal(next.chainId, Number(planned.chainId));
|
|
@@ -282,10 +284,12 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
282
284
|
const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
|
|
283
285
|
assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
|
|
284
286
|
assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.admission?.admitted === true, "Actual next transaction needs stateful gas/envelope admission");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
assert.ok(
|
|
287
|
+
assert.equal(next.admission.executionProof, "proved"); assert.equal(next.admission.protocolFit, "proved");
|
|
288
|
+
assert.equal(next.admission.transportPreflight, "passed", "Exact immediate-next submission RPC preflight is required before signing");
|
|
289
|
+
const ceiling = next.admission.limits.transactionGasCeiling;
|
|
290
|
+
assert.ok(next.gas > 0n && (ceiling === undefined || next.gas <= ceiling), "Complete gas envelope must fit supplied restrictions; Nitro compute is separately proved");
|
|
291
|
+
const calldataCap = next.admission.limits.maxCalldataBytes;
|
|
292
|
+
assert.ok(calldataCap === undefined || (next.data.length - 2) / 2 <= calldataCap, "Exact calldata must fit any supplied cap");
|
|
289
293
|
const envelope = transactionEnvelope(next);
|
|
290
294
|
const wire = { ...envelope };
|
|
291
295
|
for (const key of ["chainId", "value", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce"]) if (wire[key] !== undefined) wire[key] = toHex(BigInt(wire[key]));
|
|
@@ -529,21 +533,14 @@ async function executeExample({ fixture, scenario, sdk, artifacts, signal }) {
|
|
|
529
533
|
const { plan, predictedToken } = await constructPlan({ client, fixture, scenario, sdk, artifacts, signal });
|
|
530
534
|
artifacts.stage("execution-planning");
|
|
531
535
|
const limits = limitSource(fixture, artifacts);
|
|
532
|
-
|
|
533
|
-
if (fixture.forkRpcUrl !== undefined) {
|
|
534
|
-
const baseFork = sdk.createControlledLifecycleFork({ sourceRpcUrl: fixture.rpcUrl, forkRpcUrl: fixture.forkRpcUrl, allowTransactions: true, impersonation: "anvil", receiptTimeoutMs: 30_000 });
|
|
535
|
-
// Only the SDK's separate owned simulation fork is reset/impersonated/reverted.
|
|
536
|
-
fork = { ...baseFork, client: rpcClient(fixture.forkRpcUrl, artifacts, signal, "simulation") };
|
|
537
|
-
assert.match(await fork.client.request({ method: "web3_clientVersion" }), /anvil/i);
|
|
538
|
-
}
|
|
539
|
-
const planned = await sdk.planLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, fork, confirmations: 1 });
|
|
536
|
+
const planned = await sdk.planLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, confirmations: 1 });
|
|
540
537
|
artifacts.save("planning.json", planned);
|
|
541
538
|
artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, atomicAttempt: planned.atomicAttempt, prerequisites: planned.prerequisites, transactions: planned.transactions });
|
|
542
539
|
assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
|
|
543
540
|
if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
|
|
544
541
|
const transport = apiTransport({ artifacts, signal, apiUrl: fixture.apiUrl });
|
|
545
542
|
const session = await stageMetadata({ fixture, plan, sdk, artifacts, transport, wallet });
|
|
546
|
-
const executed = await executePlan({ client, fixture, planned, sdk, artifacts, signal, limits,
|
|
543
|
+
const executed = await executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, wallet });
|
|
547
544
|
await verifyChain({ client, planned, ...executed, fixture, scenario, sdk, artifacts });
|
|
548
545
|
await publishAndVerify({ fixture, planned, activation: executed.activation, session, sdk, artifacts, transport, signal, timeoutSeconds: 90 });
|
|
549
546
|
artifacts.stage("completed");
|
package/package.json
CHANGED
package/src/lifecycle/planner.ts
CHANGED
|
@@ -209,7 +209,7 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
|
|
|
209
209
|
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
210
210
|
return transactions.map((transaction) => {
|
|
211
211
|
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
|
|
212
|
-
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, reason: simulation.reason, limits: simulation.limits } };
|
|
212
|
+
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -301,7 +301,7 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
301
301
|
const predictedToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
302
302
|
const { tokenFactory, tokenFactoryCodeHash } = await readTokenFactoryBinding(client, plan.orchestrator, block);
|
|
303
303
|
const { limits } = await resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits);
|
|
304
|
-
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
304
|
+
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
305
305
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
306
306
|
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
307
307
|
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
|
|
@@ -318,7 +318,7 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
318
318
|
}
|
|
319
319
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
320
320
|
await assertLifecycleBlock(client, block);
|
|
321
|
-
return { ...planned, simulation: { ...placeholder, confidence: "stateful", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
321
|
+
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
322
322
|
}
|
|
323
323
|
const prerequisites = progress.canonical.phase === LifecyclePhase.None ? await readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] };
|
|
324
324
|
const validation = await validatePendingInputs(client, planned, progress, block);
|
|
@@ -351,6 +351,33 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
351
351
|
return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
async function preflightNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, simulation: LifecycleSimulation): Promise<LifecycleTransaction> {
|
|
355
|
+
if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
|
|
356
|
+
simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
|
|
357
|
+
next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
|
|
358
|
+
simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
|
|
359
|
+
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
|
|
360
|
+
}
|
|
361
|
+
const submissionClient = options.submissionClient;
|
|
362
|
+
if (submissionClient === undefined) return { ...next, dependencies: [] };
|
|
363
|
+
try {
|
|
364
|
+
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
365
|
+
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
366
|
+
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
367
|
+
}, "latest"]), "submission gas estimate");
|
|
368
|
+
if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
369
|
+
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
|
|
370
|
+
} catch (failure) {
|
|
371
|
+
const reason = `Exact next-transaction submission preflight failed: ${failure instanceof Error ? failure.message : String(failure)}`;
|
|
372
|
+
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
373
|
+
}
|
|
374
|
+
const canonical = await readLifecycleBlock(options.client, toHex(simulation.blockNumber));
|
|
375
|
+
if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
|
|
376
|
+
if (rpcQuantity(await lifecycleRpc(options.client, "eth_chainId"), "chain ID") !== simulation.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
|
|
377
|
+
if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
378
|
+
return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
|
|
379
|
+
}
|
|
380
|
+
|
|
354
381
|
/** Re-reads canonical state, revalidates all prerequisites, and proves the remaining sequence afresh. */
|
|
355
382
|
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
356
383
|
const block = await readLifecycleBlock(options.client);
|
|
@@ -368,17 +395,18 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
368
395
|
if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
369
396
|
const { planned } = options;
|
|
370
397
|
const transaction: LifecycleTransaction = { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
|
|
371
|
-
const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits
|
|
398
|
+
const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork }, [transaction], block);
|
|
372
399
|
if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
373
|
-
|
|
400
|
+
const next = attachEstimates([transaction], simulation)[0];
|
|
401
|
+
if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
402
|
+
return preflightNextTransaction(options, next, simulation);
|
|
374
403
|
}
|
|
375
|
-
const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits
|
|
404
|
+
const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
|
|
376
405
|
if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
|
|
377
406
|
assertPlannedDeploymentBindings(options.planned, refreshed);
|
|
378
407
|
const next = refreshed.transactions[0];
|
|
379
408
|
if (next === undefined) return undefined;
|
|
380
|
-
|
|
381
|
-
return { ...next, dependencies: [] };
|
|
409
|
+
return preflightNextTransaction(options, next, refreshed.simulation);
|
|
382
410
|
}
|
|
383
411
|
|
|
384
412
|
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
@@ -388,7 +416,7 @@ export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Pr
|
|
|
388
416
|
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
|
|
389
417
|
const current = await planLaunch({
|
|
390
418
|
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
391
|
-
limits: options.limits
|
|
419
|
+
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
392
420
|
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
|
|
393
421
|
});
|
|
394
422
|
assertPlannedDeploymentBindings(planned, current);
|