@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.
- package/README.md +8 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +237 -141
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +204 -143
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +42 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +167 -28
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +106 -33
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +29 -4
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +198 -15
- package/examples/.env.example +7 -10
- package/examples/README.md +24 -18
- package/examples/launch-example-support.mjs +8 -12
- package/examples/launch-example.mjs +15 -18
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +195 -116
- package/src/lifecycle/progress.ts +187 -126
- package/src/lifecycle/rpc.ts +156 -27
- package/src/lifecycle/simulation.ts +95 -31
- package/src/lifecycle/types.ts +23 -5
package/docs/launch-lifecycle.md
CHANGED
|
@@ -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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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`:
|
|
196
|
-
2.
|
|
197
|
-
3. Neither: unavailable/provisional/unadmitted;
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
package/examples/.env.example
CHANGED
|
@@ -5,16 +5,13 @@ LAUNCH_API_URL=
|
|
|
5
5
|
|
|
6
6
|
# Optional: blank uses the SDK official Robinhood chain-4663 mainnet RPC.
|
|
7
7
|
RPC_URL=
|
|
8
|
-
# Optional: separate owned loopback Anvil used only for SDK controlled simulation.
|
|
9
|
-
# Blank selects the actual RPC native eth_simulateV1 backend.
|
|
10
|
-
SIMULATION_RPC_URL=
|
|
11
8
|
# Optional ERC404 NFT metadata base URI that you actually host; blank means empty URI.
|
|
12
9
|
NFT_BASE_URI=
|
|
13
10
|
|
|
14
|
-
# Optional
|
|
15
|
-
#
|
|
16
|
-
# EXAMPLE_CHAIN_GAS_CAP=
|
|
17
|
-
# EXAMPLE_RPC_GAS_CAP=
|
|
18
|
-
# EXAMPLE_ACCOUNT_GAS_CAP=
|
|
19
|
-
# EXAMPLE_MAX_CALLDATA_BYTES=
|
|
20
|
-
# EXAMPLE_HEADROOM_BPS=
|
|
11
|
+
# Optional local tightening restrictions. Blank/unset means unknown, not refused.
|
|
12
|
+
# Nitro protocol compute caps and poster budgeting are read natively at the pinned block.
|
|
13
|
+
# EXAMPLE_CHAIN_GAS_CAP=
|
|
14
|
+
# EXAMPLE_RPC_GAS_CAP=
|
|
15
|
+
# EXAMPLE_ACCOUNT_GAS_CAP=
|
|
16
|
+
# EXAMPLE_MAX_CALLDATA_BYTES=
|
|
17
|
+
# EXAMPLE_HEADROOM_BPS=1500
|
package/examples/README.md
CHANGED
|
@@ -42,12 +42,15 @@ commitment before begin. `--receipts receipts.json` and `--confirmations N` rest
|
|
|
42
42
|
canonical recovery context. The CLI never signs, submits source-chain transactions,
|
|
43
43
|
uploads metadata, or publishes a launch. Modes never fall back silently.
|
|
44
44
|
|
|
45
|
-
Executable admission
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
Executable admission requires actual stateful measurement and exact validated replay.
|
|
46
|
+
`--limits current-limits.json` is optional tightening policy, not a required backend:
|
|
47
|
+
missing caps stay explicit uncertainty, while supplied malformed/stale/mismatched
|
|
48
|
+
provenance rejects. Encode bigint values as decimal strings. Robinhood `4663` reads
|
|
49
|
+
pinned native Nitro compute/poster evidence and requires native `eth_simulateV1`.
|
|
50
|
+
A **separate disposable loopback fork** (`--fork-rpc URL`) remains available for
|
|
51
|
+
generic EVM, not as a Nitro metering substitute. Unsigned review reports
|
|
52
|
+
`transportPreflight: "not-requested"` unless a consumer supplies its active submission
|
|
53
|
+
RPC to build-next. See the
|
|
51
54
|
[lifecycle guide](../docs/launch-lifecycle.md#execution-constraints-and-stateful-simulation).
|
|
52
55
|
|
|
53
56
|
## Real HTTP metadata sessions
|
|
@@ -91,7 +94,6 @@ Edit `.env` once:
|
|
|
91
94
|
PRIVATE_KEY=
|
|
92
95
|
LAUNCH_API_URL=
|
|
93
96
|
RPC_URL=
|
|
94
|
-
SIMULATION_RPC_URL=
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
Set `PRIVATE_KEY` to **your funded EOA's** 32-byte hex key and `LAUNCH_API_URL` to
|
|
@@ -111,11 +113,13 @@ Optional `NFT_BASE_URI` supplies your actually hosted ERC404 NFT metadata base U
|
|
|
111
113
|
Blank uses an empty base URI: NFT units/mirror behavior remain real, but the example
|
|
112
114
|
does not claim hosted NFT metadata exists.
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
The fixed Robinhood examples require native sequential Nitro `eth_simulateV1`, including
|
|
117
|
+
an isolated pinned ArbOS capability probe and exact fee-bearing replay. Generic
|
|
118
|
+
Anvil/Hardhat forks cannot certify Nitro compute/poster metering and are not a fallback.
|
|
117
119
|
Unsupported native simulation, insufficient funds or failed admission stop before API
|
|
118
|
-
staging/broadcast.
|
|
120
|
+
staging/broadcast. Each immediate next envelope is additionally preflighted read-only
|
|
121
|
+
through the same execution RPC before signing. There is no mode fallback, admission
|
|
122
|
+
override, automatic submission replay or reduced case.
|
|
119
123
|
|
|
120
124
|
### Commands and fixed cases
|
|
121
125
|
|
|
@@ -148,14 +152,16 @@ selected; token-only burn is 3000 bps in the burn case. V4 uses frozen registere
|
|
|
148
152
|
treasury/author terms with developer rate `0`; executor fee is 275 bps.
|
|
149
153
|
Hook salts/random identities are finalized and saved before execution.
|
|
150
154
|
|
|
151
|
-
###
|
|
155
|
+
### Optional execution restrictions
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
`
|
|
157
|
+
There are no default chain/RPC/account gas or calldata caps. The SDK reads current
|
|
158
|
+
Nitro protocol compute ceilings and exact-calldata poster budgets itself. Default
|
|
159
|
+
headroom is **1500 bps (15%)**. Set explicit decimal restrictions only when you intend
|
|
160
|
+
to tighten the complete transaction envelope: `EXAMPLE_CHAIN_GAS_CAP`,
|
|
161
|
+
`EXAMPLE_RPC_GAS_CAP`, `EXAMPLE_ACCOUNT_GAS_CAP`, `EXAMPLE_MAX_CALLDATA_BYTES`.
|
|
162
|
+
`EXAMPLE_HEADROOM_BPS` preserves your explicit 0–10000 policy. These are local
|
|
163
|
+
restrictions, not invented provider/account assurances or admission bypasses.
|
|
164
|
+
Poster allocation is already inside Nitro gas/affordability and is not charged twice.
|
|
159
165
|
|
|
160
166
|
### Evidence and failure recovery
|
|
161
167
|
|
|
@@ -70,7 +70,7 @@ export function loadExampleEnvironment() {
|
|
|
70
70
|
export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
71
71
|
redactor.addSecret(env.PRIVATE_KEY);
|
|
72
72
|
if (typeof env.PRIVATE_KEY === "string") redactor.addSecret(env.PRIVATE_KEY.slice(2));
|
|
73
|
-
for (const value of [env.RPC_URL, env.LAUNCH_API_URL, env.
|
|
73
|
+
for (const value of [env.RPC_URL, env.LAUNCH_API_URL, env.NFT_BASE_URI]) registerEndpointSecrets(redactor, value);
|
|
74
74
|
if (!env.PRIVATE_KEY?.trim()) throw exampleError("MISSING_CONFIGURATION", "PRIVATE_KEY is required in the environment or working-directory .env");
|
|
75
75
|
if (!env.LAUNCH_API_URL?.trim()) throw exampleError("MISSING_CONFIGURATION", "LAUNCH_API_URL is required in the environment or working-directory .env");
|
|
76
76
|
if (!/^0x[\da-f]{64}$/i.test(env.PRIVATE_KEY)) throw exampleError("INVALID_PRIVATE_KEY", "PRIVATE_KEY must be a 32-byte hex private key");
|
|
@@ -79,24 +79,20 @@ export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
|
79
79
|
catch { throw exampleError("INVALID_PRIVATE_KEY", "PRIVATE_KEY must be a valid 32-byte secp256k1 hex private key"); }
|
|
80
80
|
const addresses = sdk.getAddresses(4663);
|
|
81
81
|
const rpcUrl = executionRpcUrl(env.RPC_URL || sdk.robinhoodMainnet.rpcUrls.default.http[0]);
|
|
82
|
-
const forkRpcUrl = env.SIMULATION_RPC_URL ? loopbackUrl(env.SIMULATION_RPC_URL, "Simulation RPC") : undefined;
|
|
83
|
-
if (forkRpcUrl) {
|
|
84
|
-
const source = new URL(rpcUrl); const fork = new URL(forkRpcUrl);
|
|
85
|
-
const port = (url) => url.port || (url.protocol === "https:" ? "443" : "80");
|
|
86
|
-
if (["127.0.0.1", "localhost", "[::1]"].includes(source.hostname) && port(source) === port(fork)) throw exampleError("UNSAFE_ENDPOINT", "Execution and disposable simulation RPCs must use distinct loopback ports");
|
|
87
|
-
}
|
|
88
82
|
const gasCap = (key) => {
|
|
89
|
-
const value = env[key]
|
|
83
|
+
const value = env[key];
|
|
84
|
+
if (value === undefined || value === "") return undefined;
|
|
90
85
|
if (!/^[1-9]\d*$/.test(value)) throw exampleError("INVALID_CONFIGURATION", `${key} must be a positive decimal integer`);
|
|
91
86
|
return value;
|
|
92
87
|
};
|
|
93
88
|
const integer = (key, fallback, minimum, maximum) => {
|
|
89
|
+
if (fallback === undefined && (env[key] === undefined || env[key] === "")) return undefined;
|
|
94
90
|
const value = env[key] || String(fallback);
|
|
95
91
|
if (!/^(?:0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < minimum || Number(value) > maximum) throw exampleError("INVALID_CONFIGURATION", `${key} is outside its allowed integer range`);
|
|
96
92
|
return Number(value);
|
|
97
93
|
};
|
|
98
94
|
return {
|
|
99
|
-
chainId: "4663", rpcUrl,
|
|
95
|
+
chainId: "4663", rpcUrl, apiUrl: launchApiUrl(env.LAUNCH_API_URL),
|
|
100
96
|
orchestrator: addresses.launchOrchestrator, creator: account.address,
|
|
101
97
|
quoteAsset: addresses.weth, quoteDecimals: 18, oracleFactory: addresses.abyssFactory,
|
|
102
98
|
nftBaseUri: env.NFT_BASE_URI || "",
|
|
@@ -104,9 +100,9 @@ export function readExampleConfiguration(sdk, redactor, env = process.env) {
|
|
|
104
100
|
chainTransactionGasLimit: gasCap("EXAMPLE_CHAIN_GAS_CAP"),
|
|
105
101
|
rpcTransactionGasLimit: gasCap("EXAMPLE_RPC_GAS_CAP"),
|
|
106
102
|
accountTransactionGasLimit: gasCap("EXAMPLE_ACCOUNT_GAS_CAP"),
|
|
107
|
-
maxCalldataBytes: integer("EXAMPLE_MAX_CALLDATA_BYTES",
|
|
108
|
-
headroomBps: integer("EXAMPLE_HEADROOM_BPS",
|
|
109
|
-
provenance: { scope: "
|
|
103
|
+
maxCalldataBytes: integer("EXAMPLE_MAX_CALLDATA_BYTES", undefined, 1, Number.MAX_SAFE_INTEGER),
|
|
104
|
+
headroomBps: integer("EXAMPLE_HEADROOM_BPS", 1500, 0, 10000),
|
|
105
|
+
provenance: { scope: "user-ceilings", note: "Optional local tightening restrictions, not verified provider or account limits" },
|
|
110
106
|
},
|
|
111
107
|
};
|
|
112
108
|
}
|
|
@@ -60,7 +60,7 @@ function rpcClient(url, artifacts, signal, label) {
|
|
|
60
60
|
const body = await response.json();
|
|
61
61
|
if (body.error) throw exampleError(body.error.code ?? "RPC_ERROR", `${label} ${method}: ${body.error.message}`, { data: body.error.data, cause: body.error });
|
|
62
62
|
if (!("result" in body)) throw exampleError("INVALID_RPC_RESPONSE", `${label} ${method}: result was omitted`, { data: body });
|
|
63
|
-
if (method === "eth_simulateV1"
|
|
63
|
+
if (method === "eth_simulateV1") artifacts.event("rpc-observation", { backend: label, method, params, result: body.result, elapsedMs: Date.now() - started });
|
|
64
64
|
return body.result;
|
|
65
65
|
} catch (error) {
|
|
66
66
|
artifacts.event("rpc-error", { backend: label, method, params, elapsedMs: Date.now() - started, error: errorEvidence(error) });
|
|
@@ -256,20 +256,22 @@ function limitSource(fixture, artifacts) {
|
|
|
256
256
|
return async ({ block, chainId, account, orchestrator }) => {
|
|
257
257
|
const limits = {
|
|
258
258
|
chainId, account, orchestrator, observedBlockNumber: block.number, observedBlockHash: block.hash,
|
|
259
|
-
chainGasLimit:
|
|
260
|
-
|
|
259
|
+
chainGasLimit: policy.chainTransactionGasLimit === undefined ? undefined : BigInt(policy.chainTransactionGasLimit),
|
|
260
|
+
rpcGasLimit: policy.rpcTransactionGasLimit === undefined ? undefined : BigInt(policy.rpcTransactionGasLimit),
|
|
261
|
+
accountGasLimit: policy.accountTransactionGasLimit === undefined ? undefined : BigInt(policy.accountTransactionGasLimit),
|
|
262
|
+
maxCalldataBytes: policy.maxCalldataBytes, headroomBps: policy.headroomBps,
|
|
261
263
|
};
|
|
262
264
|
artifacts.event("execution-limits", { limits, provenance: policy.provenance });
|
|
263
265
|
return limits;
|
|
264
266
|
};
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits,
|
|
269
|
+
async function executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, wallet }) {
|
|
268
270
|
const references = [];
|
|
269
271
|
let activation;
|
|
270
272
|
for (let step = 0; step < 64; step += 1) {
|
|
271
273
|
artifacts.stage("build-next", { step });
|
|
272
|
-
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits,
|
|
274
|
+
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client });
|
|
273
275
|
if (!next) return { references, activation };
|
|
274
276
|
artifacts.event("next-admitted", { transaction: next });
|
|
275
277
|
assert.equal(next.chainId, Number(planned.chainId));
|
|
@@ -282,10 +284,12 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
282
284
|
const args = next.kind === "begin" ? [planned.plan, sdk.LifecycleMode.Staged] : next.kind === "prepare" ? [planned.plan, next.marketStart, next.marketCount] : [planned.plan];
|
|
283
285
|
assert.equal(next.data.toLowerCase(), encodeFunctionData({ abi: sdk.launchLifecycleAbi, functionName, args }).toLowerCase(), "Exact finalized-plan calldata");
|
|
284
286
|
assert.ok(next.gas !== undefined && (next.gasPrice !== undefined || next.maxFeePerGas !== undefined && next.maxPriorityFeePerGas !== undefined) && next.admission?.admitted === true, "Actual next transaction needs stateful gas/envelope admission");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
assert.ok(
|
|
287
|
+
assert.equal(next.admission.executionProof, "proved"); assert.equal(next.admission.protocolFit, "proved");
|
|
288
|
+
assert.equal(next.admission.transportPreflight, "passed", "Exact immediate-next submission RPC preflight is required before signing");
|
|
289
|
+
const ceiling = next.admission.limits.transactionGasCeiling;
|
|
290
|
+
assert.ok(next.gas > 0n && (ceiling === undefined || next.gas <= ceiling), "Complete gas envelope must fit supplied restrictions; Nitro compute is separately proved");
|
|
291
|
+
const calldataCap = next.admission.limits.maxCalldataBytes;
|
|
292
|
+
assert.ok(calldataCap === undefined || (next.data.length - 2) / 2 <= calldataCap, "Exact calldata must fit any supplied cap");
|
|
289
293
|
const envelope = transactionEnvelope(next);
|
|
290
294
|
const wire = { ...envelope };
|
|
291
295
|
for (const key of ["chainId", "value", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce"]) if (wire[key] !== undefined) wire[key] = toHex(BigInt(wire[key]));
|
|
@@ -529,21 +533,14 @@ async function executeExample({ fixture, scenario, sdk, artifacts, signal }) {
|
|
|
529
533
|
const { plan, predictedToken } = await constructPlan({ client, fixture, scenario, sdk, artifacts, signal });
|
|
530
534
|
artifacts.stage("execution-planning");
|
|
531
535
|
const limits = limitSource(fixture, artifacts);
|
|
532
|
-
|
|
533
|
-
if (fixture.forkRpcUrl !== undefined) {
|
|
534
|
-
const baseFork = sdk.createControlledLifecycleFork({ sourceRpcUrl: fixture.rpcUrl, forkRpcUrl: fixture.forkRpcUrl, allowTransactions: true, impersonation: "anvil", receiptTimeoutMs: 30_000 });
|
|
535
|
-
// Only the SDK's separate owned simulation fork is reset/impersonated/reverted.
|
|
536
|
-
fork = { ...baseFork, client: rpcClient(fixture.forkRpcUrl, artifacts, signal, "simulation") };
|
|
537
|
-
assert.match(await fork.client.request({ method: "web3_clientVersion" }), /anvil/i);
|
|
538
|
-
}
|
|
539
|
-
const planned = await sdk.planLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, fork, confirmations: 1 });
|
|
536
|
+
const planned = await sdk.planLaunch({ client, account: fixture.creator, plan, mode: scenario.mode, limits, confirmations: 1 });
|
|
540
537
|
artifacts.save("planning.json", planned);
|
|
541
538
|
artifacts.event("execution-plan", { mode: planned.mode, simulation: planned.simulation, atomicAttempt: planned.atomicAttempt, prerequisites: planned.prerequisites, transactions: planned.transactions });
|
|
542
539
|
assert.equal(planned.mode, scenario.mode, "Never silently change requested mode"); addressEqual(planned.predictedToken, predictedToken, "Planner token prediction");
|
|
543
540
|
if (!planned.simulation.admitted) throw exampleError("PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The actual SDK did not admit this exact launch", { simulation: planned.simulation });
|
|
544
541
|
const transport = apiTransport({ artifacts, signal, apiUrl: fixture.apiUrl });
|
|
545
542
|
const session = await stageMetadata({ fixture, plan, sdk, artifacts, transport, wallet });
|
|
546
|
-
const executed = await executePlan({ client, fixture, planned, sdk, artifacts, signal, limits,
|
|
543
|
+
const executed = await executePlan({ client, fixture, planned, sdk, artifacts, signal, limits, wallet });
|
|
547
544
|
await verifyChain({ client, planned, ...executed, fixture, scenario, sdk, artifacts });
|
|
548
545
|
await publishAndVerify({ fixture, planned, activation: executed.activation, session, sdk, artifacts, transport, signal, timeoutSeconds: 90 });
|
|
549
546
|
artifacts.stage("completed");
|