@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 +96 -96
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +59 -24
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +3 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +120 -55
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +165 -9
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +11 -10
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +12 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +605 -1495
- package/docs/reference.md +332 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +58 -23
- package/src/lifecycle/progress.ts +110 -47
- package/src/lifecycle/rpc.ts +158 -10
- package/src/lifecycle/simulation.ts +11 -9
- package/src/lifecycle/types.ts +9 -1
package/README.md
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
# Abyss TypeScript SDK
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
42
|
+
```sh
|
|
43
|
+
cp -n examples/.env.example .env
|
|
44
|
+
chmod 600 .env
|
|
45
|
+
```
|
|
105
46
|
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
node examples/launch-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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. */
|