@black-market/sdk 0.5.2 → 0.6.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.
@@ -7,6 +7,88 @@ 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.1 read-pipeline changes
11
+
12
+ Existing standalone calls, deployment targets, economic commitments and admission
13
+ requirements remain compatible. Invalid plan shapes are still refused before RPC work.
14
+ The optional `LifecycleRpcClient.supportsReadBatching` transport hint enables independent
15
+ getter dependency rounds with at most eight SDK-issued requests outstanding per invocation.
16
+ Set it to `true` only when the supplied transport batches concurrent requests; the SDK
17
+ does not select or configure a transport. Other clients retain serial requests, avoiding
18
+ the measured concurrent-unbatched RPC regression. With batching enabled, initial head
19
+ and chain reads overlap. Failed reads release their slots without rejecting unrelated
20
+ queued callers or automatically retrying.
21
+
22
+ A vanilla one-pool, one-quote, one-range plan selects only its committed profile IDs.
23
+ Abyss-only profile selection no longer requests the V4-only
24
+ `protocolMaximumDeveloperFeeBps` getter. A required V4 fee-limit read still fails the
25
+ operation if unavailable. V4 oracle/deployer/terms work, extra token/position capabilities
26
+ and swap-funding checks remain conditional on the actual committed plan. There is no
27
+ weakened single-pool mode: adapter eligibility, live code hashes, authority/dependency
28
+ bindings, schema/topology and applicable frozen terms remain mandatory.
29
+
30
+ Identical fixed-state reads share observations only within one planning invocation.
31
+ Identity includes the original source client, pinned block number/hash and complete
32
+ request parameters, including caller, target, calldata, value and gas context. Rejected
33
+ reads are not retained. State changes, another caller/source/block or a subsequent public
34
+ planning invocation cannot reuse those observations; mutated plans and obsolete
35
+ completions are refused. There is no global cache or new public read-scope option.
36
+
37
+ Chain checks, latest/head/pending observations, receipt reads and canonical block rechecks
38
+ remain live. Wallet/provider preflight, non-view execution calls and stateful/fork RPC
39
+ are not cached. Nitro metering probes, discovery and exact validated replay remain
40
+ separate proofs. A protected opening-buy minimum changes the committed calldata and
41
+ still requires a second full plan and stateful simulation against a fresh latest snapshot.
42
+
43
+ Raw `request({method,params})` clients remain valid. JSON-RPC HTTP batching is optional
44
+ and transport-owned; concurrent reads allow a configured batch scheduler to combine
45
+ independent messages without changing `msg.sender` or the pinned state. No API planner
46
+ service, transport fallback, signature or broadcast is introduced. The existing Node 20
47
+ minimum and ES2022 target are unchanged; shipped scheduling does not require
48
+ `Promise.withResolvers`.
49
+
50
+ Verification on 2026-10-06: `pnpm typecheck` and `pnpm test` passed, including the build,
51
+ with **123 tests: 122 passing, one opt-in real-AMM test skipped and zero failures**.
52
+ Offline regressions exercise the vanilla
53
+ Abyss path without V4 fee metadata, mandatory V4 read failures, bounded overlap and
54
+ duplicate reads, exact combined funding, changed code/authority/terms, partial failures,
55
+ chain drift/reorgs, source/caller/call-context separation, failed-slot release and
56
+ obsolete results. These offline checks are not real-wallet or on-chain execution evidence.
57
+
58
+ A live read-only comparison against installed 0.6.0 measured **one selected canonical
59
+ Abyss profile certification only**, not whole planning, simulation or launch execution.
60
+ Both versions used Node 24.11.1, viem 2.56.3 and identical pinned chain 4663 state at
61
+ block 81455627, hash
62
+ `0x66f20d4c04b1c2357e9cd711c984a83fc8913ab208449ff1a0c8289bf2854036`.
63
+ Five runs per version and transport alternated version order; returned profiles were
64
+ deep-equal, and the chain and canonical block were independently rechecked.
65
+
66
+ | Transport | Version | Median / p95 (ms) | RPC messages | HTTP envelopes | Peak outstanding |
67
+ | --- | --- | --- | --- | --- | --- |
68
+ | Unbatched, default serial | 0.6.0 | 1496.10 / 1677.93 | 15 | 15 | 1 |
69
+ | Unbatched, default serial | 0.6.1 | 1402.10 / 1414.52 | 14 | 14 | 1 |
70
+ | Batched, explicit opt-in | 0.6.0 | 1503.38 / 1541.41 | 15 | 15 | 1 |
71
+ | Batched, explicit opt-in | 0.6.1 | 1005.11 / 1219.87 | 14 | 10 | 3 |
72
+
73
+ These scoped measurements do not establish end-to-end launch latency or a general
74
+ provider guarantee. An earlier unbatched concurrency trial regressed from a 1150.64 ms
75
+ baseline median to 3870.97 ms; explicit transport opt-in and default serial scheduling
76
+ remove that scheduling choice for existing clients. For reproduction, keep the selected
77
+ profile, pinned state, source, retry/batching settings and runtime equal; count RPC
78
+ messages separately from HTTP envelopes and retain failures as failures. Do not log RPC
79
+ URLs, accounts, calldata or provider messages, or substitute full-registry/complex-only
80
+ timing for the selected-profile path.
81
+
82
+ ## 0.6.0 admission changes
83
+
84
+ Optional caller policy no longer substitutes for, or gates, actual sequential execution proof.
85
+ Results distinguish `executionProof`, `protocolFit`, and `transportPreflight`. Robinhood
86
+ admission reads pinned Nitro compute limits and poster budgets, then requires native exact
87
+ validated replay. `buildNextTransaction` optionally preflights the immediate next envelope
88
+ through `submissionClient`. Deployment addresses, lifecycle plan/schema domains, profile
89
+ capabilities and explicit atomic/staged behavior are unchanged.
90
+
91
+
10
92
  ## Deployed infrastructure
11
93
 
12
94
  Robinhood mainnet (`4663`) selects the mined `pool-launch-v1` deployment captured at block
@@ -72,7 +154,7 @@ Validation uses `validateV4LifecycleMarket`; the collector factory getter is
72
154
  | `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
155
  | `simulateLaunchPlan({client, planned, limits?, fork?, receipts?, confirmations?})` | Rereads canonical state and proves remaining work. Preserves receipt evidence, economics and selected mode. |
74
156
  | `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`. |
157
+ | `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
158
 
77
159
  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
160
 
@@ -186,15 +268,80 @@ No automatic atomic-to-staged fallback exists. Obtain separate consent to review
186
268
 
187
269
  ## Execution constraints and stateful simulation
188
270
 
189
- Admission requires current `chainGasLimit`, `rpcGasLimit`, `accountGasLimit` and `maxCalldataBytes`; block gas limit alone cannot fill missing transaction/RPC/account policy. Use a `LifecycleLimitSource` callback whose returned chain/account/core/observed block/hash match the requested pinned context and actual provider-owned policy. Static supplied facts are only current at that exact context; relabeling stale limits is not proof.
190
-
191
- The ceiling is the minimum of block and explicit transaction caps. Headroom defaults to 1,500 bps, bounded 0–10,000. The measurement pass is followed by execution at exact returned gas/fee limits. Fork execution uses gross sequential estimates rather than refund-reduced receipt gas as an executable minimum. Transactions expose `estimate.executionFee`, optional `dataFee/totalFee` and `feeConfidence: "execution-and-data" | "execution-only"`; missing rollup/data fee is not an invented all-in estimate. Limits are refreshed before every next transaction.
271
+ Optional `chainGasLimit`, `rpcGasLimit`, `accountGasLimit`, `maxCalldataBytes` and
272
+ `maxSimulationGas` tighten admission when supplied. Missing policy remains explicit in
273
+ `limits.unknownExecutionConstraints`/`unknownConstraints`; it does not reject an otherwise
274
+ proved exact sequence or invent a provider/account cap. No policy service is required.
275
+ A `LifecycleLimitSource` callback receives the canonical chain/account/core/block context.
276
+ Any supplied provenance must match it; stale/mismatched/malformed facts or a failed supplied
277
+ resolver reject rather than disappearing into optional uncertainty. Gas caps are positive
278
+ exact uint64 `bigint`; calldata is a positive safe integer byte count.
279
+ Reviewed gas envelopes are positive uint64 too; zero-metering observations cannot admit
280
+ a zero-gas transaction. Headroom calculations retain wide exact bigint intermediates.
281
+
282
+ Headroom defaults to 1,500 bps, bounded 0–10,000. Generic EVM admission uses the pinned
283
+ bounded header and supplied total-envelope restrictions plus actual validated RPC proof.
284
+ It reports an absent independent per-transaction cutoff honestly, without importing a
285
+ universal Ethereum cutoff. `executionGasCeiling` and `transactionGasCeiling` are equal on
286
+ generic EVM. The permissive measurement pass never admits a launch: replay must execute
287
+ the exact returned gas/fee envelope, every postcondition and all ordered buy bounds.
288
+ Fork discovery uses gross sequential estimates, not refund-reduced receipt gas as an
289
+ executable minimum. Explicit ceiling recovery preserves gasleft/EIP-150 validation and
290
+ carries that successfully replayed ceiling, never a smaller unproved envelope.
192
291
 
193
292
  Sequential backends:
194
293
 
195
- 1. `eth_simulateV1`: actual ordered transactions, state carried between synthetic blocks, exact gas-envelope verification, core returns/events and ordered buy bounds.
196
- 2. Explicit separate disposable loopback fork: snapshot-isolated real transactions, exact pinned chain/header identity, canonical poststate/allowances and cleanup. Source RPC stays read-only.
197
- 3. Neither: unavailable/provisional/unadmitted; no executable next transaction.
294
+ 1. `eth_simulateV1`: ordered transactions with state carried between synthetic blocks, followed by exact validated replay.
295
+ 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.
296
+ 3. Neither: unavailable/provisional/unadmitted; disconnected dependent `eth_call` estimates cannot create an executable next transaction.
297
+
298
+ Every proof completion rechecks the planned source chain ID and pinned block hash. A
299
+ changed source transport cannot retain admission merely by returning the same fork hash.
300
+
301
+ ### Nitro compute and poster accounting
302
+
303
+ For Robinhood `4663`, each pinned context reads ArbSys (`0x64`).`arbOSVersion` and ArbGasInfo
304
+ (`0x6c`).`getMaxTxGasLimit`/`getMaxBlockGasLimit`. ArbSys returns **55 + logical ArbOS version**;
305
+ the raw value must be at least 105, and `limits.arbOSVersion` reports logical version 50+.
306
+ The positive validated getter values establish `maxTxComputeGas`, `maxBlockComputeGas`
307
+ and their minimum `executionGasCeiling`. Failed reads never fall back to the huge Nitro
308
+ header or a hard-coded 32M. `limits.protocol` identifies `"nitro"` or `"evm"`.
309
+
310
+ Nitro `transactionGasCeiling`, when present, is the minimum of supplied **complete**
311
+ chain/RPC/account gas-envelope restrictions. It is independent of the compute ceiling:
312
+ an admitted transaction's `gas` may exceed `executionGasCeiling` because poster gas is
313
+ not compute. NodeInterface (`0xc8`).`gasEstimateL1Component` is read at the same pinned
314
+ block with the exact sender, destination, calldata and value, including future dependent
315
+ steps whose execution cannot yet be independently estimated. It budgets poster gas only.
316
+ The buffered compute-discovery envelope plus buffered poster budget is carried into
317
+ exact validated replay; an approximate poster quote is never subtracted from total
318
+ gas use to certify exact compute fit.
319
+
320
+ A separate pinned validated simulation probes all three ArbOS/version/getter values
321
+ against the canonical observations. Only that isolated capability probe temporarily
322
+ overrides the probe account's balance, so probe-only fees cannot reject an exactly-funded
323
+ launch. Probe funding/nonce changes, steps and fees never enter real plan measurement,
324
+ replay, postconditions or affordability, which use actual payer state. The native Nitro
325
+ `eth_simulateV1` commit-mode ArbOS charging hook removes the **actual** poster charge and
326
+ holds excess compute gas before execution. Successful exact replay and postconditions,
327
+ not the quote or permissive pass, prove fit. Missing native metering or a mismatched
328
+ probe fails closed. Generic Anvil/Hardhat forks cannot certify Nitro execution.
329
+
330
+ Transactions expose `estimate.executionFee`, optional external `dataFee`/`totalFee` and
331
+ `feeConfidence: "execution-and-data" | "execution-only"`. On Nitro, `posterGas`/`posterFee`
332
+ are the estimated buffered allocation **inside** `gasLimit`, `dataFeeIncludedInGas` is
333
+ true, `dataFee` is zero and `totalFee` equals `gasLimit * gasPrice`. Affordability counts
334
+ this complete envelope once; adding `posterFee` again would double-charge. A caller
335
+ `estimateDataFee` is therefore rejected on Nitro. On generic EVM it retains its original
336
+ semantics as a validated external fee added outside execution gas. Missing external fee
337
+ does not manufacture an all-in estimate. All observations and limits refresh for build-next.
338
+
339
+ Source references: [ArbSys version encoding](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/main/ArbSys.sol),
340
+ [ArbGasInfo compute getters](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/main/ArbGasInfo.sol),
341
+ [native simulate commit mode](https://github.com/OffchainLabs/go-ethereum/blob/51d716ccf8293072077feb9764d63d73f5e4ff20/internal/ethapi/simulate.go),
342
+ [ArbOS actual poster/compute charging](https://github.com/OffchainLabs/nitro/blob/ccc5c828d9ea22f161efc784bac4d4a6559b7e59/arbos/tx_processor.go),
343
+ and [NodeInterface poster estimate](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/node-interface/NodeInterface.sol).
344
+ These describe backend semantics, not evidence of a live launch executed by this SDK release.
198
345
 
199
346
  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
347
 
@@ -206,12 +353,48 @@ const fork = createControlledLifecycleFork({
206
353
  const planned = await planLaunch({client, account: plan.creator, plan, mode: "staged", limits, fork});
207
354
  ```
208
355
 
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 policy does not prove live provider/account admission or imply bound topology is a gas optimization.
356
+ 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
357
 
211
358
  ## Wallet submission and canonical recovery
212
359
 
213
360
  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
361
 
362
+ Simulation and `next.admission` carry flat, independent proof outcomes:
363
+
364
+ | Field | Values | Scope |
365
+ | --- | --- | --- |
366
+ | `executionProof` | `"proved"`, `"failed"`, `"unavailable"` | Complete stateful exact-envelope execution and postconditions, never permissive measurement alone. |
367
+ | `protocolFit` | `"proved"`, `"failed"`, `"unknown"` | Applicable proven protocol and known tightening restrictions. Missing optional policy remains separately disclosed. |
368
+ | `transportPreflight` | `"not-requested"`, `"passed"`, `"failed"` | Immediate exact next-transaction acceptance estimate on the supplied submission RPC. |
369
+
370
+ `admitted` means the current exact sequence is proved to fit and affordable; it is
371
+ not guaranteed wallet submission or future execution. Admission reuses simulation
372
+ `blockNumber`, `blockHash`, `account`, `chainId` and `limits`. Plans never preflight
373
+ future dependent transactions against state that does not exist.
374
+
375
+ ```ts
376
+ const planned = await planLaunch({ client, account: plan.creator, plan, mode: "atomic" });
377
+ const submissionClient = {
378
+ request: ({ method, params }) => activeProvider.request({ method, params }),
379
+ };
380
+ const next = await buildNextTransaction({ client, planned, receipts, submissionClient });
381
+ // Freshly verify the active wallet account/connector/chain again, then review/sign next.
382
+ ```
383
+
384
+ When supplied, `submissionClient` receives only read-only `eth_chainId` and
385
+ `eth_estimateGas` for the immediate next `from/to/data/value/gas/gasPrice` at `latest`.
386
+ Its positive estimate must fit the reviewed gas exactly; gas is never silently enlarged.
387
+ Chain identity is checked before and after estimation, and the source pinned hash/chain
388
+ is rechecked before returning. Success sets `next.admission.transportPreflight` to
389
+ `"passed"`. Refusal, unsupported RPC, malformed estimate, over-envelope estimate or
390
+ chain drift raises `SUBMISSION_PREFLIGHT_FAILED` without returning a transaction.
391
+ The error's `simulation` retains execution admission and its canonical context, changing
392
+ only transport outcome/reason to `"failed"`. Cancellation uses the same preflight.
393
+ Without a submission client, `"not-requested"` remains honest and no wallet RPC is implied.
394
+ Applications still own fresh active account/provider guards around asynchronous work;
395
+ a read-only preflight does not authenticate an active wallet or authorize a signature.
396
+
397
+
215
398
  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
399
 
217
400
  `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 +493,13 @@ never changes mode, drops positions/buys or overrides failed admission.
310
493
  Optional `NFT_BASE_URI` provides an ERC404 base URI you actually host; its default is
311
494
  empty, with real NFT units/mirror behavior but no claim of hosted NFT metadata.
312
495
 
313
- Optional `SIMULATION_RPC_URL` uses the canonical SDK controlled fork on a separate
314
- owned disposable loopback Anvil. Otherwise real native sequential `eth_simulateV1` is
315
- required. Only the simulation fork is reset/impersonated/reverted. Source RPC requests
316
- have a 120-second bound and are not retried. Default gas `16000000`, calldata `131072`
317
- and headroom `1000` bps are explicitly **EXAMPLE ceilings, not verified provider/account
318
- limits**; the chain cap is bounded by observed block gas limits. Environment overrides
319
- are documented in the example guide and remain subject to full SDK admission.
496
+ The chain-4663 examples require native sequential Nitro `eth_simulateV1`; generic
497
+ Anvil/Hardhat simulation is not an ArbOS compute/poster proof. Source RPC requests
498
+ have a 120-second bound and are not retried. There are no invented default gas or
499
+ calldata caps. Explicit environment restrictions only tighten the complete envelope;
500
+ headroom defaults to 1,500 bps. SDK native pinned compute/poster reads and exact replay
501
+ remain mandatory, and build-next uses the same source RPC as the read-only submission
502
+ preflight before each local signature. Environment options are documented in the example guide.
320
503
 
321
504
  Execution rechecks chain/creator/destination and exact finalized calldata before each
322
505
  send. Exact admitted envelopes are signed locally; computed hashes and unsigned
@@ -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 EXAMPLE ceilings, NOT verified provider/account limits.
15
- # Chain cap is also bounded by every observed block gas limit.
16
- # EXAMPLE_CHAIN_GAS_CAP=16000000
17
- # EXAMPLE_RPC_GAS_CAP=16000000
18
- # EXAMPLE_ACCOUNT_GAS_CAP=16000000
19
- # EXAMPLE_MAX_CALLDATA_BYTES=131072
20
- # EXAMPLE_HEADROOM_BPS=1000
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
@@ -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 additionally requires current matching execution-limit evidence
46
- (`--limits current-limits.json`) and sequential `eth_simulateV1`, or an explicitly
47
- supplied **separate disposable loopback fork** (`--fork-rpc URL`). Limits must include
48
- chain/account/core/observed-block/hash provenance and actual chain/RPC/account/calldata
49
- caps; encode bigint values as decimal strings. Missing facts produce provisional/refused
50
- admission, not invented provider/account policy. See the
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
- Blank `SIMULATION_RPC_URL` selects the actual SDK native sequential `eth_simulateV1`
115
- backend. Set it only to a **separate owned disposable loopback Anvil** for the canonical
116
- SDK controlled fork; only this simulation environment is reset/impersonated/reverted.
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. There is no mode fallback, admission override or reduced case.
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
- ### Example execution ceilings
155
+ ### Optional execution restrictions
152
156
 
153
- Defaults are **EXAMPLE ceilings, not verified provider or account limits**:
154
- chain/RPC/account gas `16000000`, calldata `131072` bytes, headroom `1000` bps.
155
- The chain cap is additionally bounded by every observed block gas limit. If your
156
- provider/account policy differs, configure decimal environment values:
157
- `EXAMPLE_CHAIN_GAS_CAP`, `EXAMPLE_RPC_GAS_CAP`, `EXAMPLE_ACCOUNT_GAS_CAP`,
158
- `EXAMPLE_MAX_CALLDATA_BYTES`, `EXAMPLE_HEADROOM_BPS`. These do not bypass SDK admission.
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.SIMULATION_RPC_URL, env.NFT_BASE_URI]) registerEndpointSecrets(redactor, value);
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] || "16000000";
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, forkRpcUrl, apiUrl: launchApiUrl(env.LAUNCH_API_URL),
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", 131072, 1, Number.MAX_SAFE_INTEGER),
108
- headroomBps: integer("EXAMPLE_HEADROOM_BPS", 1000, 0, 10000),
109
- provenance: { scope: "example-ceilings", note: "EXAMPLE ceilings, not verified provider or account limits; bounded by each observed block gas limit" },
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" || label === "simulation" && ["eth_sendTransaction", "anvil_reset", "evm_snapshot", "evm_revert"].includes(method)) artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
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: BigInt(policy.chainTransactionGasLimit) < block.gasLimit ? BigInt(policy.chainTransactionGasLimit) : block.gasLimit, rpcGasLimit: BigInt(policy.rpcTransactionGasLimit),
260
- accountGasLimit: BigInt(policy.accountTransactionGasLimit), maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
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, fork, wallet }) {
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, fork });
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
- const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
286
- const ceiling = [BigInt(head.gasLimit), ...["chainTransactionGasLimit", "rpcTransactionGasLimit", "accountTransactionGasLimit"].map((key) => BigInt(fixture.executionLimits[key]))].reduce((left, right) => left < right ? left : right);
287
- assert.ok(next.gas > 0n && next.gas <= ceiling, "Admitted gas must fit all explicit caps");
288
- assert.ok((next.data.length - 2) / 2 <= fixture.executionLimits.maxCalldataBytes, "Exact calldata cap");
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
- let fork;
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, fork, wallet });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@black-market/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "TypeScript SDK for Abyss token launches, swaps, and lending.",
5
5
  "license": "MIT",
6
6
  "repository": {