@black-market/sdk 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +9 -7
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/auto-compound.d.ts +21 -0
  5. package/dist/lifecycle/auto-compound.d.ts.map +1 -0
  6. package/dist/lifecycle/auto-compound.js +34 -0
  7. package/dist/lifecycle/auto-compound.js.map +1 -0
  8. package/dist/lifecycle/calibration.d.ts +2 -4
  9. package/dist/lifecycle/calibration.d.ts.map +1 -1
  10. package/dist/lifecycle/calibration.js +1 -26
  11. package/dist/lifecycle/calibration.js.map +1 -1
  12. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
  13. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
  14. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
  15. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
  16. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
  17. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
  18. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
  19. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
  20. package/dist/lifecycle/gas-profile.d.ts +39 -0
  21. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  22. package/dist/lifecycle/gas-profile.js +193 -0
  23. package/dist/lifecycle/gas-profile.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +6 -4
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +6 -4
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +2 -0
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +30 -3
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/opening-buys.d.ts +13 -0
  33. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  34. package/dist/lifecycle/opening-buys.js +171 -0
  35. package/dist/lifecycle/opening-buys.js.map +1 -0
  36. package/dist/lifecycle/planner.d.ts +6 -9
  37. package/dist/lifecycle/planner.d.ts.map +1 -1
  38. package/dist/lifecycle/planner.js +175 -265
  39. package/dist/lifecycle/planner.js.map +1 -1
  40. package/dist/lifecycle/presets.d.ts +10 -3
  41. package/dist/lifecycle/presets.d.ts.map +1 -1
  42. package/dist/lifecycle/presets.js +13 -1
  43. package/dist/lifecycle/presets.js.map +1 -1
  44. package/dist/lifecycle/progress.d.ts +1 -1
  45. package/dist/lifecycle/progress.d.ts.map +1 -1
  46. package/dist/lifecycle/progress.js +21 -20
  47. package/dist/lifecycle/progress.js.map +1 -1
  48. package/dist/lifecycle/rpc.d.ts +25 -30
  49. package/dist/lifecycle/rpc.d.ts.map +1 -1
  50. package/dist/lifecycle/rpc.js +63 -81
  51. package/dist/lifecycle/rpc.js.map +1 -1
  52. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  53. package/dist/lifecycle/snapshot.js +2 -4
  54. package/dist/lifecycle/snapshot.js.map +1 -1
  55. package/dist/lifecycle/transactions.d.ts +29 -6
  56. package/dist/lifecycle/transactions.d.ts.map +1 -1
  57. package/dist/lifecycle/transactions.js +62 -27
  58. package/dist/lifecycle/transactions.js.map +1 -1
  59. package/dist/lifecycle/types.d.ts +55 -122
  60. package/dist/lifecycle/types.d.ts.map +1 -1
  61. package/dist/lifecycle/types.js +3 -3
  62. package/dist/lifecycle/types.js.map +1 -1
  63. package/dist/lifecycle/ufg.d.ts +1 -1
  64. package/dist/lifecycle/ufg.d.ts.map +1 -1
  65. package/dist/lifecycle/ufg.js +4 -8
  66. package/dist/lifecycle/ufg.js.map +1 -1
  67. package/docs/launch-lifecycle.md +110 -78
  68. package/docs/lifecycle-gas-profiles.md +90 -0
  69. package/docs/reference.md +99 -47
  70. package/examples/.env.example +0 -3
  71. package/examples/README.md +14 -11
  72. package/examples/launch-example-support.ts +4 -9
  73. package/examples/launch-example.ts +30 -36
  74. package/examples/launch-lifecycle-smoke.ts +11 -21
  75. package/examples/launch.ts +8 -11
  76. package/package.json +1 -1
  77. package/src/lifecycle/authors.ts +2 -2
  78. package/src/lifecycle/auto-compound.ts +38 -0
  79. package/src/lifecycle/calibration.ts +3 -23
  80. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  81. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  82. package/src/lifecycle/gas-profile.ts +186 -0
  83. package/src/lifecycle/index.ts +6 -4
  84. package/src/lifecycle/markets.ts +25 -3
  85. package/src/lifecycle/opening-buys.ts +157 -0
  86. package/src/lifecycle/planner.ts +168 -247
  87. package/src/lifecycle/presets.ts +15 -1
  88. package/src/lifecycle/progress.ts +19 -20
  89. package/src/lifecycle/rpc.ts +58 -68
  90. package/src/lifecycle/snapshot.ts +2 -4
  91. package/src/lifecycle/transactions.ts +69 -27
  92. package/src/lifecycle/types.ts +60 -99
  93. package/src/lifecycle/ufg.ts +4 -8
  94. package/dist/lifecycle/fork.d.ts +0 -15
  95. package/dist/lifecycle/fork.d.ts.map +0 -1
  96. package/dist/lifecycle/fork.js +0 -36
  97. package/dist/lifecycle/fork.js.map +0 -1
  98. package/dist/lifecycle/simulation.d.ts +0 -18
  99. package/dist/lifecycle/simulation.d.ts.map +0 -1
  100. package/dist/lifecycle/simulation.js +0 -754
  101. package/dist/lifecycle/simulation.js.map +0 -1
  102. package/src/lifecycle/fork.ts +0 -36
  103. package/src/lifecycle/simulation.ts +0 -689
package/docs/reference.md CHANGED
@@ -56,7 +56,7 @@ These are structural maxima, not a guarantee that activation fits gas.
56
56
  | Initial executor bounty | 0–1,000 bps; UFG requires 0 |
57
57
  | Gas-envelope caps | Positive uint64 bigint |
58
58
  | Gas headroom | `headroomBps`: 0–10,000; default 1,500 |
59
- | Robinhood simulation-used gas | `maxSimulationGas`: default 50,000,000 aggregate |
59
+ | Robinhood simulation-used gas | `maxSimulationGas`: optional aggregate policy ceiling; no SDK-invented default |
60
60
 
61
61
  Use `LIFECYCLE_MAX_ERC404_SUPPLY` and `LIFECYCLE_MAX_REWARD_ERC20_SUPPLY` for the
62
62
  exported supply bounds. Market token budgets equal total minted supply; position
@@ -133,8 +133,42 @@ UFG adds a canonical dynamic tuple via `encodeUfgLaunchConfig`:
133
133
  `abi.encode((uint16,uint16,address[]))`, including the leading `0x20` offset.
134
134
  `decodeUfgLaunchConfig` rejects alternate encodings and extra bytes;
135
135
  `hashUfgLaunchConfig` hashes the tuple. Config6 appends typed `ufgLaunchConfig`
136
- for UFG profiles only; Abyss carries no UFG tail. Full plan hashes bind auxiliary
137
- tails; hook-constructor economics hashes bind the zero-salt prefix only.
136
+ for UFG profiles only; Abyss carries no UFG tail. Full plan hashes bind every
137
+ auxiliary tail. HMM/UFG constructor economics hash the zero-salt prefix; the
138
+ auto-compounding profiles below hash the zero-salt prefix **and** their tail.
139
+
140
+ ## Auto-compounding profiles
141
+
142
+ | `KnownLifecycleProfile` | Profile ID |
143
+ | --- | --- |
144
+ | `V4AutoCompoundStaticPool` | `0xdd53c2c686f822b2d17536ccf4f9db954b4976d1be80404bc74571253179fae0` |
145
+ | `V4AutoCompoundDynamicPool` | `0x46b81bfe9316fbaa90e930c6f1d5ad5e6ecfced2c886fd5520ca2bae11b92744` |
146
+
147
+ `AUTO_COMPOUND_STATIC_V1_PROFILE_ID` / `AUTO_COMPOUND_DYNAMIC_V1_PROFILE_ID`
148
+ export these identities. The presets freeze the admitted envelope, adapter,
149
+ registration, topology and exact hook creation code. The generator checks preserved
150
+ admission bytes and successful receipts against registry `profile`,
151
+ `profileTopology`, `profileEnvelope`, adapter metadata, module identity and runtime
152
+ hashes at Robinhood block **100,486,085**
153
+ (`0xa41849e3e83fb977ed2fbfc7ff71c639bdf1fc34489dc46878128c021fdb6c44`).
154
+ These are construction snapshots, not a claim of current admission.
155
+ The disabled `0x1e41…1d68` / `0x53dd…e0d7` profiles are not supported.
156
+
157
+ `AutoCompoundConfigV1` contains `compoundShareBps` (integer 0–10,000).
158
+ `encodeAutoCompoundTail` / `decodeAutoCompoundTail` encode exactly
159
+ `abi.encode(bytes32 version, uint16 compoundShareBps)` (64 bytes), where
160
+ `AUTO_COMPOUND_CONFIG_VERSION = keccak256("black-market.auto-compound.config.v1")`.
161
+ The decoder requires the exact version, length and canonical narrow-integer
162
+ padding. Config6's typed `autoCompoundConfig` selects this tail only for these
163
+ two profiles; HMM and UFG tails cannot be combined with it.
164
+ `validateAutoCompoundConfig`, the market codec and `validateV4LifecycleMarket`
165
+ reject invalid shares; the market boundaries also require zero LP/developer fees.
166
+ Both InputToken and QuoteOnly fee modes are supported.
167
+
168
+ `hashPoolBoundV4MarketCommitment` includes the entire canonical zero-salt config
169
+ for these profiles. Never mine against a prefix-only commitment or append a share
170
+ after mining. Use [the launch guide](launch-lifecycle.md#configure-auto-compounding)
171
+ for selection and gas behavior.
138
172
 
139
173
  ## Choose a fee tier
140
174
 
@@ -158,49 +192,44 @@ splits are separate values. The example's 3000/60 is not the venue's sole fee ti
158
192
 
159
193
  ## Account for Nitro compute and poster gas
160
194
 
161
- On chain 4663, the SDK reads ArbSys (`0x64`) `arbOSVersion` and ArbGasInfo (`0x6c`)
162
- `getMaxTxGasLimit` / `getMaxBlockGasLimit` at the pinned block. ArbSys returns
163
- 55 + logical version; raw must be at least 105. `limits.arbOSVersion` reports the
164
- logical version. The minimum validated compute getter is `executionGasCeiling`.
165
- Failed getters cannot fall back to the large header gas limit or a hard-coded 32M.
166
-
167
- `transactionGasCeiling`, when set, is the minimum complete-envelope chain/RPC/account
168
- restriction. It is separate from compute. NodeInterface (`0xc8`)
169
- `gasEstimateL1Component` estimates poster allocation once for the next final
170
- transaction, using its exact sender/target/calldata/value at the same block.
171
- Compute-only calibration and fresh poster allocation size the immediate envelope
172
- before native validation. `step.computeGasRequired` retains a separable compute
173
- observation, never the old poster charge or provisional budget. A fresh poster
174
- quote is combined once when that command becomes next. Both unvalidated sizing
175
- and the final exact pass use affordable pinned envelopes; validation:false does
176
- not disable Nitro's upfront funds check. The final pass uses exactly returned gas.
177
- Sizing starts with per-call ceilings because unused gas is refunded between calls.
178
- Only a genuine upfront-funds refusal permits one second unvalidated sizing attempt
179
- with the budget shared across the sequence. Other failures do not retry; the final
180
- exact validated pass is never replayed.
181
-
182
- A tight payer balance can cover smaller exact staged envelopes yet not the
183
- discovery margin. This may return `failureCategory: "affordability"` even though
184
- the underlying launch could fit. The refused RPC exposes no successful-prefix
185
- results; the SDK does not invent them or add more metering attempts.
186
-
187
- Suffix steps are marked `provisional: true` and have no transaction estimate:
188
- their conservative complete budgets check sequence feasibility, bounded across
189
- the pinned affordable remaining balance. `buildNextTransaction` never reuses
190
- those budgets as estimates; it freshly quotes and proves only the next command.
195
+ On chain4663, `ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT` and the deployment preset's
196
+ `nativeTransactionGasLimit` specify32M **total** gas for begin, prepare, activate,
197
+ atomic and cancel. This includes poster gas. Guard-only maximum-gas getters are
198
+ not read. Pinned ArbSys version selects capacity coverage/native capability, not
199
+ a gas size. Other chains retain their resolved generic transaction ceiling.
200
+
201
+ `transactionGasCeiling`, when set, is a caller complete-envelope restriction.
202
+ Below the native ceiling it refuses; it never shrinks a launch step. Approvals
203
+ omit gas/fees and leave preparation to the wallet. One `ArbGasInfo.getPricesInWei`
204
+ leg in the initial aggregate supplies L1 per-byte and L2 prices, with no per-tx
205
+ NodeInterface oracle. See [capacity and poster evidence](lifecycle-gas-profiles.md).
206
+
207
+ `computeLifecycleLaunchGasBounds({ plan, predictedToken, ufg? })` returns conservative
208
+ atomic/activation compute upper bounds, excluding poster, or undefined for unknown
209
+ coverage. These select modes, not gas limits. Atomic is a preference that may select
210
+ staged; persist returned `planned.mode`. A large upper bound is uncertain, not
211
+ proof of overflow. Unknown activation gets full-sequence execution before begin.
212
+ Conversion-carrying commands/consuming activation always retain exact execution.
213
+
214
+ Only an immediate launch command receives a final gas/price/estimate/admission.
215
+ Suffix execution metrics stay provisional; build-next constructs only the canonical
216
+ next command at the same native ceiling with fresh fees and required admission.
217
+ There are no sizing passes or funds-only retries. Balance must cover value plus
218
+ the entire ceiling fee even when actual execution uses less gas. Incomplete proof
219
+ cannot authorize begin; explicit aggregate policies still refuse excessive work.
191
220
 
192
221
  On Nitro, `estimate.posterGas` / `posterFee` are buffered allocations **inside**
193
222
  `gasLimit`; `dataFeeIncludedInGas` is true, `dataFee` is zero, and `totalFee` equals
194
223
  `gasLimit × gasPrice`. Do not add poster fees a second time. Custom `estimateDataFee`
195
224
  is rejected on Nitro; generic EVM clients can use it for an external fee.
196
- `feeConfidence` distinguishes `execution-and-data` from `execution-only` estimates.
197
- Initial legacy gas price is the greater of fresh RPC recommendation and twice pinned
198
- base fee. Reviewed immediate envelopes retain their exact held gas/price.
225
+ `feeConfidence` distinguishes actual execution evidence from the conditional
226
+ `native-ceiling-and-data` budget. Fresh gas price is max(pinned price, twice base fee).
227
+ Held Nitro price must meet max(ArbGasInfo L2 price, header base fee), without an
228
+ automatic bump. Reviewed gas differing from the ceiling is stale and refuses.
199
229
 
200
- HMM atomic/activation sizing adds one forwarding target of
201
- `4,250,000 + 500,000 × (HMM markets - 1)` gas. Staged packing estimates 14.5M per HMM
202
- market and 16M per other market before measurement. These are sizing inputs, not
203
- fixed transaction fees or proof that every permitted topology fits.
230
+ Capacity-only profiles account for route, position and HMM forwarding overhead.
231
+ Staged packing retains conservative per-market grouping, but never changes the
232
+ gas assigned to a launch transaction or splits its final activation.
204
233
 
205
234
  ## Cache static reads and inspect timing
206
235
 
@@ -210,19 +239,22 @@ fixed transaction fees or proof that every permitted topology fits.
210
239
  | `sessionCache: createLifecycleSessionCache()` | Caller-owned reuse of SDK-selected static reads at one pin; successful deployment/backend observations also survive across blocks. Without it, those observations belong to the supplied client object. |
211
240
  | `onDiagnostic(event)` | Local opt-in stage/RPC timing listener; no automatic console output or telemetry. |
212
241
 
213
- Call `sessionCache.invalidate()` on wallet/chain switches and after each confirmed
214
- transaction to discard pinned static values. Session deployment/backend facts are
215
- keyed by chain and account where applicable and live until the session owner is
216
- discarded. `sessionCache.block` is readonly `{ number, hash }` or undefined.
242
+ Call `sessionCache.invalidate()` after confirmed transactions to discard pinned
243
+ static values. Deployment/backend facts are retained for the session owner and
244
+ keyed by chain/account where applicable. Chain identity is cached by bound client
245
+ object; create a new client on a chain change rather than mutating/reusing it.
246
+ `sessionCache.block` is readonly `{ number, hash }` or undefined.
217
247
  Planning pins retain the actual RPC L2 block number and header hash. EVM
218
248
  `BLOCKNUMBER`/`BLOCKHASH`, including Multicall3's metadata getters, expose L1 values
219
249
  on Nitro and are not L2 pin sources.
220
- Balances, allowances, current progress, receipts, nonces, policy callbacks, and
221
- stateful execution proof stay fresh. Session caching does not extend admission.
250
+ Balances, allowances, current progress, receipts, policy callbacks and required
251
+ execution proof stay fresh. Planning/build-next do not read account queue nonces;
252
+ the wallet assigns them and retains the observed submitted nonce for recovery.
253
+ Session caching does not extend admission.
222
254
 
223
255
  One RPC L2 header supplies number, hash, timestamp, gas limit and base fee.
224
256
  The initial Multicall3 call uses that explicit L2 block and includes native balance,
225
- funding/progress and Nitro getters. Independent getters are aggregated with no
257
+ funding/progress, Nitro getters and `getPricesInWei`. Independent getters are aggregated with no
226
258
  minimum-count threshold. Multicall3 runtime code is attested once per session.
227
259
  The 4663 preset enables `launchLensAbi.snapshot` without caller configuration:
228
260
  one call replaces dependent token/hook/market/funding reads inside that aggregate.
@@ -315,6 +347,26 @@ From the checkout root, `pnpm test` runs offline tests and `pnpm examples` perfo
315
347
  read-only deployed discovery. The following checks require separate infrastructure;
316
348
  run write-capable checks only with explicit permission on disposable services.
317
349
 
350
+ ### Auto-compounding read-only checks
351
+
352
+ After `pnpm build`, compare both profiles and fee modes with the live collector
353
+ factory's metadata/decoder and the reference Python encoder (no signing or sends):
354
+
355
+ ```sh
356
+ BLACK_MARKET_REFERENCE_ROOT=/path/to/black-market \
357
+ node --test test/integration/auto-compound.live.test.mjs
358
+ ```
359
+
360
+ The reference checkout must contain `scripts/auto_compound_config.py` and its
361
+ Python dependencies. Regenerate the frozen deployment modules offline with:
362
+
363
+ ```sh
364
+ node --experimental-transform-types scripts/generate-auto-compound-presets.mjs
365
+ # Explicitly refresh readback from a new pinned L2 header plus preserved admission evidence:
366
+ node --experimental-transform-types scripts/generate-auto-compound-presets.mjs \
367
+ --readback /path/to/black-market
368
+ ```
369
+
318
370
  ### UFG owned-fork check
319
371
 
320
372
  Run Anvil and the test in separate terminals; stop Anvil afterward:
@@ -37,6 +37,3 @@ NFT_BASE_URI=
37
37
  # EXAMPLE_MAX_CALLDATA_BYTES=
38
38
  # EXAMPLE_HEADROOM_BPS=1500
39
39
  #
40
- # Robinhood aggregate simulation gas-used budget defaults to 50M. Override only for
41
- # a provider supporting that budget; buy-output calibration needs activation coverage.
42
- # EXAMPLE_SIMULATION_GAS_CAP=100000000
@@ -1,8 +1,8 @@
1
1
  # Run the examples
2
2
 
3
- These commands use the checkout's built `@black-market/sdk` **0.14.1** and viem.
3
+ These commands use the checkout's built `@black-market/sdk` **0.15.1** and viem.
4
4
  Use **Node 24+** for direct TypeScript execution. Start with the one-market
5
- `launch-erc20-abyss.ts`; larger cases can exceed the RPC's simulation budget.
5
+ `launch-erc20-abyss.ts`; larger cases require staged preparation and a bounded activation.
6
6
  For valuation, token ordering, ranges, and buy protection, read the
7
7
  [launch guide](../docs/launch-lifecycle.md#price-the-launch).
8
8
 
@@ -44,7 +44,6 @@ Copy `launch-example.ts`, `launch-example-support.ts`, and `launch-example-kyber
44
44
  | `EXAMPLE_ACCOUNT_GAS_CAP` | Optional wallet/account transaction gas-envelope ceiling. |
45
45
  | `EXAMPLE_MAX_CALLDATA_BYTES` | Optional whole-transaction byte ceiling. |
46
46
  | `EXAMPLE_HEADROOM_BPS` | Gas headroom, default `1500`; accepted range 0–10,000. |
47
- | `EXAMPLE_SIMULATION_GAS_CAP` | Aggregate simulation-used gas budget; default 50M on chain 4663. |
48
47
 
49
48
  Configure **exactly one** of `PRIVATE_KEY` and `EXAMPLE_KEYSTORE_PATH`.
50
49
  A keystore must be a regular file owned by your user with owner-only permissions.
@@ -117,18 +116,20 @@ node examples/launch-erc20-abyss.ts --resume "launch-results/<original-run-dir>"
117
116
 
118
117
  | Mode | Reads / writes | Identity |
119
118
  | --- | --- | --- |
120
- | No flags | Live discovery, funding quotes, preparation, simulation, economic output; unsigned, no API writes | Fresh plan/run directory |
119
+ | No flags | Live discovery, funding quotes, preparation, local economic review and gas-budget checks; unsigned, no API writes | Fresh plan/run directory |
121
120
  | `--broadcast` | Signs, spends, stages/uploads metadata, deploys, verifies, and publishes; no further prompt | **Fresh plan**, not the preceding dry run |
122
121
  | `--resume <original-run-dir>` | May sign remaining steps or finish publication | Retained plan, receipts, mode, confirmation policy, and API session |
123
122
 
124
- Dry-run needs the signer source, RPC/API, and sufficient funds for execution admission.
123
+ Dry-run needs the signer source, RPC/API, and sufficient funds for current input/gas-budget checks.
125
124
  Review its economics, then review the fresh broadcast output too. Mode flags cannot
126
125
  be combined or duplicated. Resume is an execution authorization; omit `--broadcast`.
127
126
 
128
- The default proof window is **50M aggregate used gas**, not a per-transaction limit.
129
- Opening-buy calibration needs activation inside the proven prefix. A deferred activation
130
- cannot produce protected buy minima. Raise `EXAMPLE_SIMULATION_GAS_CAP` only when your
131
- provider supports it; start with one market when checking an unfamiliar RPC.
127
+ Every launch transaction uses the native ceiling,32M total on4663. Capacity profiles
128
+ only choose mode/chunks and reject oversized activation; exact local ordered math
129
+ protects opening buys. Multiple markets or conversion funding always stage.
130
+ There is no launch simulation, including Kyber. Routers enforce conversion minima
131
+ and deadlines on-chain; `assessment.ready` is not a receipt or guaranteed execution.
132
+ See [coverage and migration](../docs/lifecycle-gas-profiles.md).
132
133
 
133
134
  ### Fund opening buys
134
135
 
@@ -166,8 +167,10 @@ It does not automatically retry sends, adopt replacements, cancel, or create ano
166
167
  | Unstarted / Cancelled / unresolved signed hash | Stops for reconciliation; do not use a new invocation as a retry. |
167
168
 
168
169
  Resume retains salts and buy minima; it does not rerun preparation or recalibrate economics.
169
- For replacements, retain original `transactionHash` and effective `replacementHash`,
170
- plus observed block number/hash. A missing receipt alone does not establish a reorg.
170
+ For replacements, retain original `transactionHash`, effective `replacementHash`,
171
+ observed block number/hash and the actual submitted nonce as an exact bigint
172
+ (decimal string in journals). SDK planning has no account-queue nonce gate; the
173
+ wallet assigns and tracks nonces. A missing receipt alone does not establish a reorg.
171
174
 
172
175
  Metadata is staged before deployment. Publication uses the **original activation hash**.
173
176
  A `LaunchPublishPending` response means indexing is pending; the runner polls for up
@@ -252,14 +252,10 @@ export function exampleError(code, message, details = {}) {
252
252
  return Object.assign(new Error(message), { name: "LaunchExampleError", code, ...details });
253
253
  }
254
254
 
255
- /** An interrupted simulation still must restore its snapshot; never revert source state. */
256
- export function rpcRequestSignal(signal, backend, method) {
257
- const cleanup = backend === "simulation" && ["evm_revert", "anvil_stopImpersonatingAccount", "hardhat_stopImpersonatingAccount"].includes(method);
258
- if (!cleanup) signal.throwIfAborted();
259
- // Cold nested-fork state fetches can exceed 30 seconds. Bound each request,
260
- // including instrumented controlled simulation; never retry a source broadcast.
261
- const timeout = AbortSignal.timeout(120_000);
262
- return cleanup ? timeout : AbortSignal.any([signal, timeout]);
255
+ /** Bound each request and preserve interruption; launch planning owns no fork cleanup. */
256
+ export function rpcRequestSignal(signal) {
257
+ signal.throwIfAborted();
258
+ return AbortSignal.any([signal, AbortSignal.timeout(120_000)]);
263
259
  }
264
260
 
265
261
 
@@ -540,7 +536,6 @@ export async function readExampleConfiguration(sdk, redactor, env = process.env)
540
536
  chainTransactionGasLimit: gasCap("EXAMPLE_CHAIN_GAS_CAP"),
541
537
  rpcTransactionGasLimit: gasCap("EXAMPLE_RPC_GAS_CAP"),
542
538
  accountTransactionGasLimit: gasCap("EXAMPLE_ACCOUNT_GAS_CAP"),
543
- rpcTotalSimulationGasLimit: gasCap("EXAMPLE_SIMULATION_GAS_CAP"),
544
539
  maxCalldataBytes: integer("EXAMPLE_MAX_CALLDATA_BYTES", undefined, 1, Number.MAX_SAFE_INTEGER),
545
540
  headroomBps: integer("EXAMPLE_HEADROOM_BPS", 1500, 0, 10000),
546
541
  provenance: { scope: "user-ceilings", note: "Optional local tightening restrictions, not verified provider or account limits" },
@@ -78,7 +78,7 @@ function rpcClient(url, artifacts, signal, label) {
78
78
  // the example RPC client is admitted identically to the UI it validates.
79
79
  if (endpoint.hostname === "rpc.mainnet.chain.robinhood.com") headers.Origin = "https://abyss.trading";
80
80
  return { async request({ method, params = [] }) {
81
- const requestSignal = rpcRequestSignal(signal, label, method);
81
+ const requestSignal = rpcRequestSignal(signal);
82
82
  const started = Date.now();
83
83
  try {
84
84
  const response = await fetch(endpoint, {
@@ -90,7 +90,6 @@ function rpcClient(url, artifacts, signal, label) {
90
90
  const body = await response.json();
91
91
  if (body.error) throw exampleError(body.error.code ?? "RPC_ERROR", `${label} ${method}: ${body.error.message}`, { data: body.error.data, cause: body.error });
92
92
  if (!("result" in body)) throw exampleError("INVALID_RPC_RESPONSE", `${label} ${method}: result was omitted`, { data: body });
93
- if (method === "eth_simulateV1") artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
94
93
  return body.result;
95
94
  } catch (error) {
96
95
  artifacts.event("rpc-error", { backend: label, method, params, elapsedMs: Date.now() - started, error: errorEvidence(error) });
@@ -307,17 +306,13 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
307
306
  market.retainedSupplyOpeningValuationUsdX18 = ratio.numerator * retainedSupply * market.quoteUsdX18 /
308
307
  (ratio.denominator * 10n ** BigInt(market.quoteDecimals));
309
308
  }
310
- const activationTransaction = planned.transactions.find((transaction) => ["atomic", "activate"].includes(transaction.kind));
311
- const activationStep = planned.simulation.steps.find((step) => step.transactionId === activationTransaction?.id);
312
- if (!activationStep?.success || !activationStep.returnData) throw exampleError("MISSING_ECONOMIC_OUTPUTS", "Economic review requires the admitted activation receipt and every ordered buy output");
313
- const simulatedReceipt = decodeFunctionResult({ abi: sdk.launchLifecycleAbi,
314
- functionName: activationTransaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", data: activationStep.returnData });
315
- assert.equal(simulatedReceipt.planHash.toLowerCase(), planned.planHash.toLowerCase(), "Economic receipt exact plan");
316
- assert.equal(simulatedReceipt.tokenOut.length, planned.plan.buys.length, "Economic receipt ordered outputs");
317
- const simulatedBuys = planned.plan.buys.map((buy, index) => {
318
- const tokenOut = simulatedReceipt.tokenOut[index];
319
- const quoteSpent = simulatedReceipt.quoteSpent[index];
320
- assert.ok(tokenOut >= buy.minTokenOut && tokenOut > 0n && quoteSpent <= buy.quoteAmountIn, "Exact protected simulated buy");
309
+ const orderedOutputs = planned.assessment.gasProfile?.initialBuys;
310
+ if (!orderedOutputs) throw exampleError("MISSING_ECONOMIC_OUTPUTS", "Economic review requires the supported exact local opening curve");
311
+ const buyEvidence = "local-exact-curve";
312
+ assert.equal(orderedOutputs.length, planned.plan.buys.length, "Economic review ordered outputs");
313
+ const reviewedBuys = planned.plan.buys.map((buy, index) => {
314
+ const { tokenOut, quoteSpent } = orderedOutputs[index];
315
+ assert.ok(tokenOut >= buy.minTokenOut && tokenOut > 0n && quoteSpent <= buy.quoteAmountIn, "Exact protected opening buy");
321
316
  const market = marketEconomics[buy.marketIndex];
322
317
  const ratio = market.rawQuotePerRawToken;
323
318
  return { buyIndex: index, marketIndex: buy.marketIndex, quoteAsset: market.quoteAsset, quoteSymbol: market.quoteSymbol, quoteSpent, tokenOut,
@@ -325,10 +320,10 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
325
320
  feeAndPriceImpactPremiumBps: (quoteSpent * ratio.denominator - tokenOut * ratio.numerator) * 10000n /
326
321
  (tokenOut * ratio.numerator),
327
322
  // Effective execution price includes fees and path impact, not post-buy marginal spot.
328
- priceBasis: "admitted-simulation-all-in-average-including-fees-and-price-impact" };
323
+ priceBasis: `${buyEvidence}-all-in-average-including-fees-and-price-impact` };
329
324
  });
330
325
  const economicReview = {
331
- schema: "black-market.launch-economic-review.v1", caseId: scenario.id, chainId: fixture.chainId,
326
+ schema: "black-market.launch-economic-review.v2", caseId: scenario.id, chainId: fixture.chainId,
332
327
  targetMarketCapUsdX18: fixture.targetMarketCapUsdX18, quotes, metadata, image: image?.descriptor ?? null,
333
328
  actualMintedSupply: token.supply, nftUnit: token.nftUnit, tokenDecimals: 18,
334
329
  predictedToken, markets: marketEconomics,
@@ -337,8 +332,8 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
337
332
  executorFeeBps: plan.executorFeeBps, feeAssets, v4Markets: scenario.markets.flatMap((spec, marketIndex) => spec.venue === "v4" ?
338
333
  [{ marketIndex, feeMode: spec.feeMode ?? 0, protocolFeeDenominator: selected.v4.envelope.protocolFeeDenominator,
339
334
  treasury: selected.v4.envelope.protocolTreasury, developerBeneficiary: selected.v4.envelope.beneficiary, developerFeeBps: 0 }] : []) },
340
- initialBuy: { slippageBps: BUY_SLIPPAGE_BPS, funding: planned.plan.funding, orderedBuys: planned.plan.buys, simulatedBuys,
341
- observedBlockNumber: planned.simulation.blockNumber, observedBlockHash: planned.simulation.blockHash },
335
+ initialBuy: { slippageBps: BUY_SLIPPAGE_BPS, funding: planned.plan.funding, orderedBuys: planned.plan.buys, reviewedBuys, evidence: buyEvidence,
336
+ observedBlockNumber: planned.assessment.blockNumber, observedBlockHash: planned.assessment.blockHash },
342
337
  planHash: planned.planHash, finalizedPlan: finalizedPlan,
343
338
  };
344
339
  const reviewHash = economicReviewHash(economicReview);
@@ -346,7 +341,7 @@ async function constructPlan({ client, fixture, scenario, sdk, artifacts, signal
346
341
  // Complete exact intent is owner-only and durable before attribution or chain signing.
347
342
  artifacts.privateRecovery({
348
343
  schema: "black-market.launch-example-recovery.v1", sdk: "node", caseId: artifacts.result.caseId,
349
- plan: finalizedPlan, mode: scenario.mode, confirmations, executionLimits: fixture.executionLimits, economicReview: { ...economicReview, reviewHash },
344
+ plan: finalizedPlan, mode: planned.mode, confirmations, executionLimits: fixture.executionLimits, economicReview: { ...economicReview, reviewHash },
350
345
  });
351
346
  artifacts.save("plan.json", finalizedPlan);
352
347
  artifacts.save("hook-deployments.json", planned.hookDeployments);
@@ -477,7 +472,6 @@ function limitSource(fixture, artifacts) {
477
472
  chainGasLimit: policy.chainTransactionGasLimit === undefined ? undefined : BigInt(policy.chainTransactionGasLimit),
478
473
  rpcGasLimit: policy.rpcTransactionGasLimit === undefined ? undefined : BigInt(policy.rpcTransactionGasLimit),
479
474
  accountGasLimit: policy.accountTransactionGasLimit === undefined ? undefined : BigInt(policy.accountTransactionGasLimit),
480
- maxSimulationGas: policy.rpcTotalSimulationGasLimit === undefined ? undefined : BigInt(policy.rpcTotalSimulationGasLimit),
481
475
  maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
482
476
  };
483
477
  artifacts.event("execution-limits", { limits, provenance: policy.provenance });
@@ -490,11 +484,11 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
490
484
  let activation;
491
485
  for (let step = 0; step < 64; step += 1) {
492
486
  artifacts.stage("build-next", { step });
493
- const reviewedTransaction = planned.transactions[step];
494
- const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client, reviewedTransaction, signal });
487
+ const stored = planned.transactions[step];
488
+ const reviewedTransaction = stored?.gas === undefined || stored.gasPrice === undefined ? undefined : stored;
489
+ const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, reviewedTransaction, signal });
495
490
  if (!next) return { references, activation };
496
- assert.ok(reviewedTransaction, "No transaction outside the saved review may be submitted");
497
- for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "nonce", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
491
+ if (reviewedTransaction) for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved exact envelope`);
498
492
  if (retainedReferences.length) {
499
493
  const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
500
494
  assertRetainedExampleReceipts({ progress, references, artifacts });
@@ -509,12 +503,10 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
509
503
  assert.ok(functionName, "Native-input conversion cases do not permit unrelated approval/cancellation writes");
510
504
  const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
511
505
  assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
512
- 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");
513
- assert.equal(next.admission.executionProof, "proved"); assert.equal(next.admission.protocolFit, "proved");
514
- assert.equal(next.admission.transportPreflight, "passed", "Exact immediate-next submission RPC preflight is required before signing");
515
- const ceiling = next.admission.limits.transactionGasCeiling;
516
- assert.ok(next.gas > 0n && (ceiling === undefined || next.gas <= ceiling), "Complete gas envelope must fit supplied restrictions; Nitro compute is separately proved");
517
- const calldataCap = next.admission.limits.maxCalldataBytes;
506
+ assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.assessment?.ready === true, "Actual next transaction needs a current gas budget");
507
+ const ceiling = next.assessment.limits.transactionGasCeiling;
508
+ assert.ok(next.gas === next.assessment.limits.executionGasCeiling && (ceiling === undefined || next.gas <= ceiling), "Launch gas must equal the native ceiling and fit supplied restrictions");
509
+ const calldataCap = next.assessment.limits.maxCalldataBytes;
518
510
  assert.ok(calldataCap === undefined || (next.data.length - 2) / 2 <= calldataCap, "Exact calldata must fit any supplied cap");
519
511
  const envelope = transactionEnvelope(next);
520
512
  const wire = { ...envelope };
@@ -549,7 +541,7 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
549
541
  addressEqual(includedBlock?.hash, receipt.blockHash, "Canonical included block");
550
542
  const latest = BigInt(await client.request({ method: "eth_blockNumber" }));
551
543
  assert.ok(latest >= BigInt(receipt.blockNumber) + BigInt(planned.confirmations) - 1n, "Required confirmations must be included canonically");
552
- const reference = { transactionHash: row.transactionHash, observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: planned.confirmations };
544
+ const reference = { transactionHash: row.transactionHash, nonce: BigInt(row.transaction.nonce), observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: planned.confirmations };
553
545
  row.status = "confirmed"; row.reference = reference; artifacts.saveReceipts();
554
546
  references.push(reference);
555
547
  if (["atomic", "activate"].includes(next.kind)) {
@@ -882,9 +874,10 @@ async function executeExample({ fixture, privateKey, scenario, sdk, artifacts, s
882
874
  const { planned, predictedToken, economicReview, reviewHash } = await constructPlan({ client, fixture, scenario, sdk, artifacts, signal, limits, metadata, image });
883
875
  const { plan } = planned;
884
876
  artifacts.save("planning.json", planned);
885
- artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, prerequisites: planned.prerequisites, transactions: planned.transactions });
886
- assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
887
- if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
877
+ artifacts.event("execution-plan", { mode: planned.mode, assessment: planned.assessment, prerequisites: planned.prerequisites, transactions: planned.transactions });
878
+ if (scenario.mode === "staged") assert.equal(planned.mode, "staged", "Explicit staged mode is retained");
879
+ addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
880
+ if (!planned.assessment.ready) throw exampleError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "Refresh the launch review before continuing.", { assessment: planned.assessment });
888
881
  const usd = (value) => `${value / UNIT}.${(value % UNIT).toString().padStart(18, "0")}`;
889
882
  const fundingSummary = economicReview.quotes.every((quote) => quote.conversion.nativeWrap === true)
890
883
  ? "payable native wrap"
@@ -894,7 +887,7 @@ async function executeExample({ fixture, privateKey, scenario, sdk, artifacts, s
894
887
  console.log(`Economic review ${reviewHash}\n${metadata.name} (${metadata.symbol}); target USD ${usd(economicReview.targetMarketCapUsdX18)}; minted supply ${usd(economicReview.actualMintedSupply)}\n` +
895
888
  economicReview.markets.map((market) => `Market ${market.marketIndex} ${market.venue}/${market.quoteSymbol}: retained-supply opening valuation USD ${usd(market.retainedSupplyOpeningValuationUsdX18)} (minted-supply USD ${usd(market.openingValuationUsdX18)}), token USD ${usd(market.unitPriceUsdX18)}, token ${market.quoteSymbol} ${usd(market.unitPriceQuoteX18)}, ${market.positions.length} positions`).join("\n") +
896
889
  `\nExpected rounding burn ${economicReview.expectedUnusedBurn} raw token units; ${planned.plan.buys.length} ordered buys, native input ${fixture.buyAmountWei} wei each (${fundingSummary}); 50 bps conversion and ordered-buy protection.\nLP fee 3000 pips; V4 hook max 10000/min 1000 pips (fixed maximum schedule); executor 275 bps.\nImage ${image ? `${image.descriptor.contentType}, ${image.descriptor.contentLength} bytes, ${image.descriptor.sha256}` : "none (metadata only)"}.\nReview allocations, fees and exact protected buys: ${artifacts.directory}/economic-review.json`);
897
- console.log(economicReview.initialBuy.simulatedBuys.map((buy) => `Buy ${buy.buyIndex} market ${buy.marketIndex}/${buy.quoteSymbol}: quote spent ${buy.quoteSpent} raw units; simulated all-in USD/token ${usd(buy.allInUnitPriceUsdX18)}; fees + price-impact premium ${buy.feeAndPriceImpactPremiumBps} bps vs opening`).join("\n"));
890
+ console.log(economicReview.initialBuy.reviewedBuys.map((buy) => `Buy ${buy.buyIndex} market ${buy.marketIndex}/${buy.quoteSymbol}: quote spent ${buy.quoteSpent} raw units; ${economicReview.initialBuy.evidence} all-in USD/token ${usd(buy.allInUnitPriceUsdX18)}; fees + price-impact premium ${buy.feeAndPriceImpactPremiumBps} bps vs opening`).join("\n"));
898
891
  for (const quote of economicReview.quotes) {
899
892
  const seed = quote.usdSeed;
900
893
  console.log(`${quote.symbol}/USD ${usd(seed.priceUsdX18)}; source ${seed.source}; chain ${seed.chainId}; quote ${seed.tokenAddress} (${seed.tokenDecimals} decimals); retrieved ${seed.retrievedAt}.\n` +
@@ -1054,7 +1047,8 @@ export async function resumeLaunchExample({ fixture, privateKey, sdk, artifacts,
1054
1047
  assert.match(reference.transactionHash, /^0x[\da-fA-F]{64}$/, "Original receipt hash");
1055
1048
  assert.match(reference.observedBlockHash, /^0x[\da-fA-F]{64}$/, "Original receipt block hash");
1056
1049
  assert.ok(reference.observedBlockNumber !== undefined && reference.observedBlockNumber !== null, "Original receipt block number");
1057
- return { ...reference, observedBlockNumber: BigInt(reference.observedBlockNumber), confirmations: reference.confirmations ?? confirmations };
1050
+ if (reference.nonce !== undefined) assert.ok(typeof reference.nonce === "bigint" || typeof reference.nonce === "string" && /^(0|[1-9]\d*)$/.test(reference.nonce), "Retained submitted nonce must be an exact unsigned integer");
1051
+ return { ...reference, ...(reference.nonce === undefined ? {} : { nonce: BigInt(reference.nonce) }), observedBlockNumber: BigInt(reference.observedBlockNumber), confirmations: reference.confirmations ?? confirmations };
1058
1052
  });
1059
1053
  const retainedHashes = new Set(references.map((reference) => reference.transactionHash.toLowerCase()));
1060
1054
  if (references.some((reference) => reference.replacementHash && !same(reference.replacementHash, reference.transactionHash)) ||
@@ -1079,7 +1073,7 @@ export async function resumeLaunchExample({ fixture, privateKey, sdk, artifacts,
1079
1073
  if (![sdk.LifecyclePhase.Preparing, sdk.LifecyclePhase.Ready, sdk.LifecyclePhase.Active].includes(progress.canonical.phase)) {
1080
1074
  throw exampleError("RESUME_PHASE_UNSUPPORTED", "Resume requires canonical Preparing, Ready or Active; an unstarted or cancelled launch must never be relaunched");
1081
1075
  }
1082
- if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "Remaining committed launch work was not admitted", { simulation: planned.simulation });
1076
+ if (!planned.assessment.ready) throw exampleError(planned.assessment.code ?? "PLAN_NOT_READY", planned.assessment.reason ?? "Refresh the unfinished launch review before continuing.", { assessment: planned.assessment });
1083
1077
  Object.assign(artifacts.result.chain, { planHash: planned.planHash, launchId: planned.launchId, predictedToken: planned.predictedToken });
1084
1078
  // Canonical receipts restore the begin/deployment logs needed by verifyChain.
1085
1079
  for (const reference of references) {
@@ -1,23 +1,22 @@
1
- // Deliberately executing proof on an owned disposable loopback fixture only.
1
+ // Deliberately executing actual transactions on an owned disposable loopback fixture only.
2
2
  import assert from "node:assert/strict";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { decodeEventLog, decodeFunctionResult, encodeFunctionData, toHex, zeroHash } from "viem";
5
5
  import {
6
6
  buildClaimDeveloperFeesPageTransaction, buildNextTransaction, buildPoolBoundHookDeploymentTransaction,
7
- createControlledLifecycleFork, decodeDeveloperClaimReceipt, decodePoolBoundV4LifecycleMarketConfig,
7
+ decodeDeveloperClaimReceipt, decodePoolBoundV4LifecycleMarketConfig,
8
8
  encodeLaunchPlan, encodePoolBoundV4LifecycleMarketConfig, hashLaunchIdentity, hashLaunchPlan,
9
9
  launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleFeeHubAbi,
10
10
  LifecyclePhase, parseLaunchPlan, planLaunch, predictLifecycleToken, prepareAndPlanLifecycleLaunch,
11
11
  readAuthorHubs, readDeveloperFees, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment,
12
- simulateLaunchPlan, isPoolBoundV4ConfigVersion,
12
+ isPoolBoundV4ConfigVersion,
13
13
  } from "@black-market/sdk/lifecycle";
14
14
  const [manifestFile, plansFile] = process.argv.slice(2);
15
15
  if (!manifestFile || !plansFile) throw new Error("Usage: node examples/launch-lifecycle-smoke.ts manifest.json sdk-plans.json");
16
16
  const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
17
17
  const exported = JSON.parse(await readFile(plansFile, "utf8"));
18
18
  const rpcUrl = process.env.LAUNCH_LIFECYCLE_RPC_URL;
19
- const forkRpcUrl = process.env.LAUNCH_LIFECYCLE_FORK_RPC_URL;
20
- if (!rpcUrl || !forkRpcUrl || !["127.0.0.1", "localhost", "[::1]"].includes(new URL(rpcUrl).hostname) || process.env.LAUNCH_LIFECYCLE_ALLOW_LOCAL_EXECUTION !== "1" || manifest.fixtureOnly !== true) throw new Error("Smoke requires explicitly authorized owned loopback fixture execution and a separate disposable fork");
19
+ if (!rpcUrl || !["127.0.0.1", "localhost", "[::1]"].includes(new URL(rpcUrl).hostname) || process.env.LAUNCH_LIFECYCLE_ALLOW_LOCAL_EXECUTION !== "1" || manifest.fixtureOnly !== true) throw new Error("Smoke requires explicitly authorized owned loopback fixture execution");
21
20
  if (!Array.isArray(exported.plans) || exported.plans.length === 0) throw new Error("Current Solidity-exported reviewed plans are required");
22
21
  let requestId = 0;
23
22
  const client = { async request({ method, params = [] }) {
@@ -36,7 +35,6 @@ const limits = async ({ block, chainId, account, orchestrator }) => ({
36
35
  chainGasLimit: BigInt(policy.chainTransactionGasLimit), rpcGasLimit: BigInt(policy.rpcTransactionGasLimit),
37
36
  accountGasLimit: BigInt(policy.accountTransactionGasLimit), maxCalldataBytes: Number(policy.maxCalldataBytes), headroomBps: Number(policy.headroomBps),
38
37
  });
39
- const fork = createControlledLifecycleFork({ sourceRpcUrl: rpcUrl, forkRpcUrl, allowTransactions: true, impersonation: "anvil" });
40
38
  async function readContract(to, abi, functionName, args = []) {
41
39
  return decodeFunctionResult({ abi, functionName, data: await client.request({ method: "eth_call", params: [{ to, data: encodeFunctionData({ abi, functionName, args }) }, "latest"] }) });
42
40
  }
@@ -46,7 +44,7 @@ async function send(transaction) {
46
44
  const gasPrice = transaction.gasPrice ?? BigInt(await client.request({ method: "eth_gasPrice" }));
47
45
  const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
48
46
  const ceiling = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
49
- const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) };
47
+ const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) };
50
48
  const estimated = transaction.gas ?? ((BigInt(await client.request({ method: "eth_estimateGas", params: [{ ...wire, gas: toHex(ceiling) }] })) * BigInt(10000 + Number(policy.headroomBps)) + 9999n) / 10000n);
51
49
  assert.ok(estimated <= ceiling, "Unsigned helper must still fit explicit local execution admission");
52
50
  const transactionHash = await client.request({ method: "eth_sendTransaction", params: [{ ...wire, gas: toHex(estimated) }] });
@@ -81,7 +79,7 @@ for (const row of exported.plans) for (const mode of ["atomic", "staged"]) {
81
79
  const draft = { ...plan, markets: plan.markets.map((market) => isPoolBoundV4ConfigVersion(market.configVersion) ? {
82
80
  ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: zeroHash }),
83
81
  } : market) };
84
- let planned = await prepareAndPlanLifecycleLaunch({ client, account: draft.creator, plan: draft, mode, limits, fork });
82
+ let planned = await prepareAndPlanLifecycleLaunch({ client, account: draft.creator, plan: draft, mode, limits });
85
83
  plan = planned.plan;
86
84
  assert.equal(hashLaunchPlan(plan).toLowerCase(), row.planHash.toLowerCase(), "Offchain mining reproduces the independent constructor/salt vector");
87
85
  if (mode === "staged") for (const deployment of planned.hookDeployments) {
@@ -90,22 +88,14 @@ for (const row of exported.plans) for (const mode of ["atomic", "staged"]) {
90
88
  const actual = await readPoolBoundHookDeployment({ client, plan, marketIndex: deployment.marketIndex });
91
89
  assert.equal(actual.predictedHook.toLowerCase(), deployment.predictedHook.toLowerCase());
92
90
  }
93
- if (mode === "staged" && planned.hookDeployments.length > 0) planned = await planLaunch({ client, account: plan.creator, plan, mode, limits, fork });
94
- assert.equal(planned.simulation.admitted, true, planned.simulation.reason);
95
- const atomicReceipt = planned.simulation.steps.find((step) => step.transactionId === "atomic")?.returnData;
96
- if (atomicReceipt) {
97
- const decoded = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "launchAtomic", data: atomicReceipt });
98
- assert.equal(decoded.planHash.toLowerCase(), planned.planHash.toLowerCase());
99
- for (let i = 0; i < plan.buys.length; ++i) assert.ok(decoded.tokenOut[i] >= plan.buys[i].minTokenOut);
100
- }
101
- const replay = await simulateLaunchPlan({ client, planned, limits, fork });
102
- assert.equal(replay.admitted, true, replay.reason);
91
+ if (mode === "staged" && planned.hookDeployments.length > 0) planned = await planLaunch({ client, account: plan.creator, plan, mode, limits });
92
+ assert.equal(planned.assessment.ready, true, planned.assessment.reason);
103
93
  const receipts = [];
104
94
  let activationReceipt;
105
95
  for (let guard = 0; guard < 64; ++guard) {
106
- const next = await buildNextTransaction({ client, planned, receipts, limits, fork, submissionClient: client });
96
+ const next = await buildNextTransaction({ client, planned, receipts, limits });
107
97
  if (!next) break;
108
- assert.equal(next.admission?.transportPreflight, "passed");
98
+ assert.equal(next.assessment?.ready, true);
109
99
  const submitted = await send(next);
110
100
  receipts.push(submitted.reference);
111
101
  if (["atomic", "activate"].includes(next.kind)) activationReceipt = submitted.receipt;
@@ -130,7 +120,7 @@ for (const row of exported.plans) for (const mode of ["atomic", "staged"]) {
130
120
  assert.equal(liquidity, position.liquidity); assert.equal(custody.toLowerCase(), market.prepared.custody.toLowerCase());
131
121
  }
132
122
  }
133
- assert.equal(await buildNextTransaction({ client, planned, receipts, limits, fork }), undefined);
123
+ assert.equal(await buildNextTransaction({ client, planned, receipts, limits }), undefined);
134
124
  const hub = progress.canonical.feeHub;
135
125
  assert.equal(await readContract(hub, lifecycleFeeHubAbi, "economicVersion"), 3);
136
126
  const authors = new Map();