@black-market/sdk 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  # Abyss TypeScript SDK
4
4
 
5
- TypeScript SDK for token launches, swaps, and lending with [viem](https://viem.sh).
6
- Import the public package as `@black-market/sdk`. Requires Node 24+ to run the examples.
5
+ Use `@black-market/sdk` with [viem](https://viem.sh) to launch tokens, swap, and read lending markets.
6
+ This guide targets SDK **0.13.0** on Robinhood Chain mainnet (**4663**).
7
+ The package supports Node 20+; running the checkout's TypeScript examples requires **Node 24+**.
7
8
 
8
9
  ## Install
9
10
 
10
- Install the package with `pnpm add @black-market/sdk viem`, or use a checkout
11
- to run and customize the examples:
11
+ ```sh
12
+ pnpm add @black-market/sdk@0.13.0 viem
13
+ ```
14
+
15
+ To run the examples, use a checkout:
12
16
 
13
17
  ```sh
14
18
  git clone https://github.com/abysshopper/black-market-node-sdk.git
@@ -17,107 +21,103 @@ pnpm install --frozen-lockfile
17
21
  pnpm build
18
22
  ```
19
23
 
20
- ### UFG treasury launches (0.12.0)
21
-
22
- Opt in with `prepareAndPlanLifecycleLaunch({ ..., ufg: { config } })`. UFG supports
23
- reward-free ERC20 launches with native Abyss QUOTE/QUOTE_ORACLE markets and the
24
- new `KnownLifecycleProfile.V4UfgFixedPool` / `V4UfgDynamicPool` presets.
25
- All reserve fees fund the upstream treasury; launch-token fees are burned.
26
- These are separate offerings, not a change to existing fixed, dynamic or HMM launches.
27
-
28
- Set `plan.creator = UFG_LAUNCH_PROXY_ADDRESS` and
29
- `plan.nonce = ufgLaunchNonce(principal, nonce96)`, while the planning account and
30
- every transaction sender remain the principal. Use `buildUfgFeeAssetPolicies`
31
- with the core's predicted token; approve the **proxy**, not the escrow.
32
- `readUfgTreasury(client, token)` returns the upstream treasury or `undefined`.
33
- The [UFG lifecycle guide](docs/launch-lifecycle.md#0120-release-opt-in-ufg-treasuries)
34
- covers restrictions, tail encoding, unsigned builders and recovery.
35
- [`ufg-launch-config.ts`](examples/ufg-launch-config.ts) is a pure, no-wallet example.
36
-
37
- ### HMM V2 pool hooks (0.10.0)
38
-
39
- Robinhood mainnet has admitted **HMM Fixed V2** and **HMM Dynamic V2** presets:
40
- `KnownLifecycleProfile.V4HmmFixedPool` / `V4HmmDynamicPool`. The generic keys and
41
- `HmmLaunchConfig` now mean V2 only. Config6 appends an exact **512-byte** tail;
42
- old HMM profiles and 448-byte payloads are not supported by these helpers.
43
- Both require zero LP and developer fees.
44
-
45
- `defaultHmmLaunchConfig({ tokenBudget, quoteAsset, quoteDecimals, maxBidQuote,
46
- referenceDeviation })` preserves existing WETH/USDG calibration, but requires
47
- explicit bid-principal and oracle-reference guards. There are no guessed V2 risk
48
- defaults. Unknown quotes still require explicit raw quote thresholds.
49
-
50
- On each swap, the configured share of fees in **both currencies**, after treasury,
51
- funds the per-market HMM manager; the remainder follows normal fee distribution.
52
- HMM does not burn fees or touch locked launch positions. The current fee owner
53
- can change the share, all 15 strategy settings, and placement pause state using
54
- the exported V2 ABIs. See [HMM configuration and migration](docs/launch-lifecycle.md#0100-release-hmm-v2-only)
55
- and the [pure, unsigned example](examples/hmm-launch-config.ts).
56
-
57
- ## Examples
58
-
59
- The most complete case is [`launch-erc20-dividends-multiquote-mixed.ts`](examples/launch-erc20-dividends-multiquote-mixed.ts).
60
- On Robinhood mainnet (**chain 4663**), it:
61
-
62
- 1. Creates an ERC20 with 1,000,000 initial tokens and holder dividends.
63
- Fee-asset allocations split 40% to rewards and 60% to the owner, not a 40% tax.
64
- 2. Splits liquidity across V4/WETH (two positions), V4/USDG (two), and Abyss/WETH
65
- (three), priced from the same whole-supply USD valuation (default $5,000).
66
- 3. Funds one opening buy per market with 0.00001 ETH each by default. ETH wraps
67
- into WETH; USDG uses an admitted Kyber conversion. ETH for gas is additional.
68
- 4. Prepares and proves the staged plan, prints the economic review, and saves
69
- artifacts. Activation outputs must fit the provider's proof budget; configure
70
- `EXAMPLE_SIMULATION_GAS_CAP` only for a provider supporting that budget.
71
- The default dry-run never signs or submits anything.
72
- 5. With authorization, stages metadata, begins and prepares the markets, then
73
- activates all seven positions and three buys together. It verifies chain
74
- results and API publication; optional images are verified before submission.
75
-
76
- ### Configure and run
77
-
78
- From the checkout root, create `.env` without overwriting existing configuration:
24
+ ## Check the deployment first
79
25
 
80
26
  ```sh
81
- cp -n examples/.env.example .env
82
- chmod 600 .env
27
+ pnpm examples
28
+ # To load configuration from an existing working-directory .env explicitly:
29
+ node --env-file=.env examples/quickstart.ts
83
30
  ```
84
31
 
85
- Set `LAUNCH_API_URL` and exactly one key source: `PRIVATE_KEY`, or
86
- `EXAMPLE_KEYSTORE_PATH` to an owner-only V3 scrypt keystore (`EXAMPLE_KEYSTORE_PASSWORD`
87
- defaults to empty). Optional `RPC_URL` overrides the official chain4663 RPC.
88
- Remote endpoints require HTTPS; existing environment values override `.env`.
32
+ `pnpm examples` builds and runs [`examples/quickstart.ts`](examples/quickstart.ts).
33
+ It reads chain identity, the orchestrator's registry, and current profile admission.
34
+ It needs no wallet and makes no writes. It reads the process environment; it does **not** load `.env` itself.
35
+ Export `RPC_URL` in your shell or use `--env-file` as shown. `CHAIN_ID` defaults to `4663`.
89
36
 
90
- ```sh
91
- # Dry run: no signing, chain submission, image upload, or publication.
92
- node examples/launch-erc20-dividends-multiquote-mixed.ts
93
- # Explicitly authorize spending funds and publishing; no confirmation prompt.
94
- node examples/launch-erc20-dividends-multiquote-mixed.ts --broadcast
95
- # After partial execution, replace this path with the ORIGINAL run directory.
96
- node examples/launch-erc20-dividends-multiquote-mixed.ts --resume "launch-results/<original-run>"
97
- ```
37
+ ## Run a one-market ERC20 launch
98
38
 
99
- Dry-run needs live RPC/API access, the creator key, funding, and admission.
100
- Review the saved `economic-review.json` in `launch-results/`. Broadcast builds a
101
- **fresh plan**; resume may spend/publish but retains the original plan and session.
102
- Keep recovery files; never commit or share keys, private artifacts, or signed bytes.
39
+ Start with [`launch-erc20-abyss.ts`](examples/launch-erc20-abyss.ts): one Abyss/WETH market,
40
+ one permanent position, one opening buy, no holder rewards, atomic execution.
103
41
 
104
- ## Creating your own
42
+ ```sh
43
+ cp -n examples/.env.example .env
44
+ chmod 600 .env
45
+ ```
105
46
 
106
- Copy the case wrapper beside its helpers, then customize it:
47
+ Set `LAUNCH_API_URL=https://api.abyss.trading` and exactly one signer source:
48
+ `PRIVATE_KEY`, or `EXAMPLE_KEYSTORE_PATH` with optional `EXAMPLE_KEYSTORE_PASSWORD`.
49
+ Use your own funded account. Keep secrets and recovery files out of git.
50
+ The launch wrappers load the working-directory `.env`; existing environment values win.
51
+ Optional `RPC_URL` selects another Robinhood RPC. Remote endpoints require HTTPS.
107
52
 
108
53
  ```sh
109
- cp examples/launch-erc20-dividends-multiquote-mixed.ts examples/launch-my-token.ts
110
- node examples/launch-my-token.ts
54
+ # Unsigned planning; no chain transactions, uploads, or publication.
55
+ node examples/launch-erc20-abyss.ts
56
+ # Authorizes spending and publication, with no further prompt.
57
+ node examples/launch-erc20-abyss.ts --broadcast
58
+ # After partial execution, use the ORIGINAL run directory.
59
+ node examples/launch-erc20-abyss.ts --resume "launch-results/<original-run-dir>"
111
60
  ```
112
61
 
113
- Edit the object passed to `runLaunchExample`: unique `id`, `tokenKind`
114
- (0 ERC20 / 1 ERC404), `rewardMode` (0 none / 1 staking / 2 dividends),
115
- `mode` (`atomic` / `staged`), `markets` (`venue`, `quote`, `positions`), and
116
- `buysPerMarket`. Supported venues are `v4`/`abyss`, quotes `WETH`/`USDG`.
117
- Customize identity, valuation, and buy input with the `EXAMPLE_*` keys in
118
- [`.env.example`](examples/.env.example). For supply, fees, budgets, or ranges,
119
- adapt `constructPlan` in `launch-example.ts` and `deriveExampleMarketEconomics`
120
- in `launch-example-support.ts`. If copying elsewhere, include the Kyber helper too.
121
-
122
- See the [launch/API guide](docs/launch-lifecycle.md), [runner configuration](examples/README.md),
123
- and [MIT license](LICENSE) for deeper reference.
62
+ **Broadcast builds a fresh plan**, including a new nonce and token identity; it does not sign the dry-run plan.
63
+ Review the broadcast's printed economics too. `--resume` can spend funds and publish;
64
+ it continues the saved identity and does not require `--broadcast`.
65
+ A dry run still needs a signer source, sufficient funding, and live RPC/API access for admission.
66
+
67
+ ### Understand the defaults
68
+
69
+ - ERC20 supply: **1,000,000 tokens**. Target FDV: **$5,000**, or **$0.005/token** before tick alignment.
70
+ - FDV is a valuation, not cash deposited. Initial positions contain launch tokens only.
71
+ - Opening buy: **0.00001 ETH per buy**. ETH wraps into WETH; USDG cases convert through Kyber.
72
+ - ETH for gas is extra. The buy's output depends on fees, ranges, and the aligned opening price.
73
+ - `EXAMPLE_TARGET_MARKET_CAP_USD` and `EXAMPLE_BUY_AMOUNT_WEI` change the target and buy input.
74
+ These defaults are examples, not a recommended valuation or an anti-sniper guarantee.
75
+
76
+ Use the [worked pricing guide](docs/launch-lifecycle.md#price-the-launch) before changing supply or ranges.
77
+
78
+ ## Choose an example
79
+
80
+ Each filename below is in `examples/`. V4 cases use the fixed-fee pool-bound profile;
81
+ quotes are WETH unless USDG is listed. Positions are permanently locked.
82
+
83
+ | File | Token / rewards | Markets / position counts | Mode / buys |
84
+ | --- | --- | --- | --- |
85
+ | `launch-erc20-abyss.ts` | ERC20 / none | Abyss: 1 | atomic / 1 |
86
+ | `launch-erc20-v4.ts` | ERC20 / none | V4: 1 | atomic / 1 |
87
+ | `launch-erc404-v4.ts` | ERC404 / none | V4: 1 | staged / 1 |
88
+ | `launch-erc404-abyss.ts` | ERC404 / none | Abyss: 1 | atomic / 1 |
89
+ | `launch-erc20-staking-v4.ts` | ERC20 / staking | V4: 2 | staged / 2 |
90
+ | `launch-erc20-dividends-abyss.ts` | ERC20 / dividends | Abyss: 3 | staged / 1 |
91
+ | `launch-erc20-burn-mixed.ts` | ERC20 / 30% token-fee burn | V4: 2; Abyss: 1 | staged / 2 |
92
+ | `launch-erc404-dividends-mixed.ts` | ERC404 / dividends | V4: 2; Abyss: 3 | staged / 2 |
93
+ | `launch-erc20-multi-quote.ts` | ERC20 / none | V4 WETH: 1; V4 USDG: 1; Abyss WETH: 1 | staged / 3 |
94
+ | `launch-erc20-depth-v4.ts` | ERC20 / none | V4: 4, Early Scarcity | staged / 1 |
95
+ | `launch-erc20-dividends-multiquote-mixed.ts` | ERC20 / dividends | V4 WETH: 2; V4 USDG: 2; Abyss WETH: 3 | staged / 3 |
96
+
97
+ ## Choose an offering for your integration
98
+
99
+ Run discovery before selecting a profile. Frozen SDK presets describe supported construction;
100
+ `readLifecycleProfiles` reports current admission.
101
+
102
+ | Offering | Choose it for | Guide |
103
+ | --- | --- | --- |
104
+ | Canonical Abyss | STANDARD / STANDARD_ORACLE / QUOTE / QUOTE_ORACLE pools | [Choose an offering](docs/launch-lifecycle.md#choose-an-offering) |
105
+ | Fixed V4 | One hook per pool, fixed hook fee, optional LP fee | [Build the plan](docs/launch-lifecycle.md#build-the-plan) |
106
+ | Dynamic V4 | Oracle-velocity hook fee between your minimum and maximum; LP fee 0 | [Fee configuration](docs/reference.md#pool-bound-v4-version-6) |
107
+ | HMM V2 | Fee-funded auxiliary bids/asks; LP and developer fees 0 | [Configure HMM V2](docs/launch-lifecycle.md#configure-hmm-v2) |
108
+ | UFG | Quote-fee treasury and holder redemption; reward-free ERC20 only | [Launch a UFG treasury](docs/launch-lifecycle.md#launch-a-ufg-treasury) |
109
+
110
+ The eleven launch wrappers do not launch HMM or UFG. Start those integrations with
111
+ [`hmm-launch-config.ts`](examples/hmm-launch-config.ts) or [`ufg-launch-config.ts`](examples/ufg-launch-config.ts)
112
+ and the linked guide. They are pure configuration examples, not deployment commands.
113
+
114
+ ## Recover and integrate
115
+
116
+ After an interrupted send, inspect the saved transaction hash and use `--resume` on the original directory.
117
+ If markets are already active but publication failed, resume publication; do not deploy another token.
118
+ The runner saves artifacts under `launch-results/`; retain its owner-only recovery file.
119
+
120
+ - [Launch guide](docs/launch-lifecycle.md): pricing → planning → signing → publication → fees and recovery.
121
+ - [Runner reference](examples/README.md): environment, eleven cases, artifacts, and resume behavior.
122
+ - [SDK reference](docs/reference.md): addresses, codecs, limits, Nitro gas, and author APIs.
123
+ - [Changelog](CHANGELOG.md) · [MIT license](LICENSE)
@@ -1 +1 @@
1
- {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AASA,OAAO,EAA0B,KAAK,2BAA2B,EAAkN,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,oCAAoC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAqQld;wFACwF;AACxF,wBAAsB,6BAA6B,CAAC,OAAO,EAAE,oCAAoC,GAAG,OAAO,CAAC,aAAa,CAAC,CA+DzH;AAED,qGAAqG;AACrG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAMnF;AA0JD;uFACuF;AACvF,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAyC1H;AAED;qFACqF;AACrF,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAWzG"}
1
+ {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AASA,OAAO,EAA0B,KAAK,2BAA2B,EAAkN,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,oCAAoC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAuQld;wFACwF;AACxF,wBAAsB,6BAA6B,CAAC,OAAO,EAAE,oCAAoC,GAAG,OAAO,CAAC,aAAa,CAAC,CAgFzH;AAED,qGAAqG;AACrG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQnF;AA4JD;uFACuF;AACvF,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAqD1H;AAED;qFACqF;AACrF,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAWzG"}
@@ -3,9 +3,9 @@ import { ABYSS_FEE_TIERS } from "../abyss.js";
3
3
  import { assertLaunchTopologyCoverage, getTickAtSqrtRatio } from "../launch.js";
4
4
  import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
5
5
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
6
- import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
6
+ import { readLaunchProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
7
7
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt } from "./markets.js";
8
- import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
8
+ import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
9
9
  import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
10
10
  import { LifecyclePlanningError } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
@@ -159,7 +159,7 @@ function orderedTransactions(plan, mode, progress, approvals, batches, ufg) {
159
159
  const commands = compiled.filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
160
160
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
161
161
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
162
- return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
162
+ return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
163
163
  }
164
164
  function attachEstimates(transactions, simulation) {
165
165
  return transactions.map((transaction) => {
@@ -180,7 +180,8 @@ function readReviewedNextTransaction(transaction) {
180
180
  typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
181
181
  typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
182
182
  typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
183
- typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n) {
183
+ typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
184
+ transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
184
185
  throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
185
186
  }
186
187
  return transaction;
@@ -188,7 +189,8 @@ function readReviewedNextTransaction(transaction) {
188
189
  function matchesReviewedNextTransaction(next, reviewed) {
189
190
  return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
190
191
  next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
191
- next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
192
+ next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
193
+ (reviewed.nonce === undefined || next.nonce === reviewed.nonce);
192
194
  }
193
195
  function assertReviewedTransactionUnchanged(options, reviewed, next) {
194
196
  if (options.planned.ufg !== undefined)
@@ -297,9 +299,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
297
299
  const { commitment, ufgConfigHash } = validatePlanningIntent(options);
298
300
  if (options.buySlippageBps !== undefined)
299
301
  validateBuySlippageBps(options.buySlippageBps);
302
+ const fixed = options.fixedFinalEnvelope;
303
+ if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
304
+ fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
305
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
306
+ }
307
+ const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
300
308
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
301
309
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
302
- if (!needsMining && !needsProtection)
310
+ if (!needsMining && !needsProtection && fixed === undefined)
303
311
  return planLaunch(options);
304
312
  const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
305
313
  const assertDraft = () => {
@@ -308,8 +316,11 @@ export async function prepareAndPlanLifecycleLaunch(options) {
308
316
  throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
309
317
  if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash)
310
318
  throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
319
+ if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
320
+ fixed.gas !== heldEnvelope.gas || fixed.gasPrice !== heldEnvelope.gasPrice)
321
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
311
322
  };
312
- return withLifecycleReadClient(options.client, async (client) => {
323
+ const result = await withLifecycleReadClient(options.client, async (client) => {
313
324
  const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
314
325
  if (chainId !== draft.chainId)
315
326
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
@@ -337,13 +348,19 @@ export async function prepareAndPlanLifecycleLaunch(options) {
337
348
  // start at a fresh post-mining snapshot and share it through final protection.
338
349
  const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
339
350
  assertDraft();
351
+ const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
352
+ const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
353
+ const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
354
+ gas: heldEnvelope.gas,
355
+ gasPrice: heldEnvelope.gasPrice ?? (price > baseFeeEnvelope ? price : baseFeeEnvelope),
356
+ });
340
357
  let plan = { ...draft, markets };
341
358
  let ownedProgress;
342
359
  if (needsProtection) {
343
360
  // A diagnostic minimum is never returned as an admitted or executable plan.
344
361
  // Price guards, funding minima, recipients and all ordered budgets stay exact.
345
362
  const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
346
- const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
363
+ const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, undefined, finalEnvelope));
347
364
  assertDraft();
348
365
  if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
349
366
  throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
@@ -352,7 +369,7 @@ export async function prepareAndPlanLifecycleLaunch(options) {
352
369
  // state. No onchain commitment existed at this exact snapshot to replace.
353
370
  ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
354
371
  }
355
- const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
372
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, undefined, finalEnvelope));
356
373
  assertDraft();
357
374
  assertFinalPlan(planned, options.signal);
358
375
  if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
@@ -366,16 +383,21 @@ export async function prepareAndPlanLifecycleLaunch(options) {
366
383
  throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
367
384
  return planned;
368
385
  });
386
+ assertDraft();
387
+ assertFinalPlan(result, options.signal);
388
+ return result;
369
389
  }
370
390
  /** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
371
391
  export async function planLaunch(options) {
372
- return withLifecycleReadClient(options.client, async (client) => {
392
+ const result = await withLifecycleReadClient(options.client, async (client) => {
373
393
  const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
374
394
  assertFinalPlan(planned, options.signal);
375
395
  return planned;
376
396
  });
397
+ assertFinalPlan(result, options.signal);
398
+ return result;
377
399
  }
378
- async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, reviewed) {
400
+ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, reviewed, fixedFinalEnvelope) {
379
401
  const { plan, account, mode, ufg } = options;
380
402
  const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
381
403
  const launchId = hashLaunchIdentity(plan);
@@ -400,7 +422,10 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
400
422
  preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
401
423
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
402
424
  lifecycleStage(client, "plan.progress", async () => {
403
- const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
425
+ const [predictedToken] = await Promise.all([
426
+ readLifecycleTokenAtBlock(client, plan, block),
427
+ ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
428
+ ]);
404
429
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
405
430
  ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
406
431
  if (ufg !== undefined)
@@ -459,6 +484,9 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
459
484
  batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
460
485
  }
461
486
  let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
487
+ if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0].kind !== "atomic")) {
488
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
489
+ }
462
490
  if (reviewed !== undefined) {
463
491
  if (!matchesReviewedNextTransaction(transactions[0], reviewed))
464
492
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
@@ -468,9 +496,10 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
468
496
  await assertLifecycleBlock(client, block, plan.chainId);
469
497
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
470
498
  }
471
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
499
+ const envelope = diagnostic ? undefined : reviewed ?? fixedFinalEnvelope;
500
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice);
472
501
  try {
473
- const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, reviewed);
502
+ const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, envelope);
474
503
  let simulation = await execute(transactions);
475
504
  assertFinalPlan(planned, options.signal);
476
505
  if (mode === "staged") {
@@ -523,6 +552,7 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
523
552
  throw new Error("Active submission RPC chain differs from the reviewed transaction");
524
553
  const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
525
554
  from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
555
+ ...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
526
556
  }, "latest"]), "submission gas estimate");
527
557
  if (estimated <= 0n || estimated > next.gas)
528
558
  throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
@@ -536,14 +566,6 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
536
566
  const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
537
567
  throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
538
568
  }
539
- const [canonical, chainId] = await Promise.all([
540
- readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
541
- lifecycleRpc(options.client, "eth_chainId"),
542
- ]);
543
- if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase())
544
- throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
545
- if (rpcQuantity(chainId, "chain ID") !== simulation.chainId)
546
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
547
569
  if (next.admission === undefined)
548
570
  throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
549
571
  assertPlanningActive(options.signal);
@@ -557,9 +579,17 @@ export async function buildNextTransaction(options) {
557
579
  // Snapshot only the reviewed wire fields; caller proofs and commands are never
558
580
  // reused, and final checks compare the mutable input with this one local copy.
559
581
  const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
560
- from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
561
- return withLifecycleReadClient(options.client, async (client) => {
582
+ from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
583
+ const result = await withLifecycleReadClient(options.client, async (client) => {
562
584
  const block = await readLifecycleBlock(client);
585
+ const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
586
+ if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
587
+ await Promise.all([
588
+ readLifecycleTokenAtBlock(client, options.planned.plan, block),
589
+ readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
590
+ resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
591
+ ]);
592
+ }
563
593
  const progress = await readLaunchProgress({ ...options, client }, block);
564
594
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
565
595
  // Only the confirmed canonical terminal phase is terminal; a head-only
@@ -580,6 +610,7 @@ export async function buildNextTransaction(options) {
580
610
  assertFinalPlan(planned, options.signal);
581
611
  const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
582
612
  : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
613
+ transaction.nonce = progress.pendingAccountNonce;
583
614
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
584
615
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
585
616
  const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
@@ -601,6 +632,10 @@ export async function buildNextTransaction(options) {
601
632
  return undefined;
602
633
  return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
603
634
  });
635
+ assertFinalPlan(options.planned, options.signal);
636
+ if (result !== undefined)
637
+ assertReviewedTransactionUnchanged(options, reviewed, result);
638
+ return result;
604
639
  }
605
640
  /** Re-simulates only canonical unfinished work with the same submitted receipt
606
641
  * evidence and confirmation depth as build-next, never silently dropping context. */