@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/README.md
CHANGED
|
@@ -40,6 +40,14 @@ Launch examples spend real funds and publish to your configured API.
|
|
|
40
40
|
Plans, transaction hashes, receipts, and errors are saved in `launch-results/`.
|
|
41
41
|
Keep `.env` and private recovery files out of Git.
|
|
42
42
|
|
|
43
|
+
Lifecycle planning proves exact stateful execution without requiring a policy service.
|
|
44
|
+
Robinhood admission reads native Nitro compute/poster evidence; optional caller caps
|
|
45
|
+
only tighten it. Build-next can separately preflight the immediate unsigned envelope
|
|
46
|
+
with an active `submissionClient`. Execution proof is not guaranteed wallet submission.
|
|
47
|
+
|
|
43
48
|
[Example setup](https://github.com/abysshopper/black-market-node-sdk/tree/main/examples)
|
|
44
49
|
· [API guide](https://github.com/abysshopper/black-market-node-sdk/blob/main/docs/launch-lifecycle.md)
|
|
45
50
|
· [MIT license](https://github.com/abysshopper/black-market-node-sdk/blob/main/LICENSE)
|
|
51
|
+
|
|
52
|
+
[Agent guide](https://github.com/abysshopper/black-market-node-sdk/blob/main/AGENTS.md)
|
|
53
|
+
· [Skills](https://github.com/abysshopper/black-market-node-sdk/tree/main/.agents/skills)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyX,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAK9b,OAAO,EAA0B,KAAK,2BAA2B,EAAwF,KAAK,gCAAgC,EAAyB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,qCAAqC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/lifecycle/planner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyX,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAK9b,OAAO,EAA0B,KAAK,2BAA2B,EAAwF,KAAK,gCAAgC,EAAyB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,qCAAqC,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAqQhb,0GAA0G;AAC1G,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,MAAM,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,qCAAqC,KAAK,IAAI,CAAC;CACxE,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,SAAS,gCAAgC,EAAE,CAAA;CAAE,CAAC,CAiD5F;AAED,yGAAyG;AACzG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAMnF;AAqGD,yGAAyG;AACzG,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CA4B1H;AAED;qFACqF;AACrF,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAUzG"}
|
|
@@ -3,7 +3,7 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFu
|
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecycleMode, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LifecycleVenue, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MULTI_POSITION_CAPABILITY, LIFECYCLE_REQUIRED_CAPABILITIES, marketIdentityV1Components, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
|
|
4
4
|
import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
|
|
5
5
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA } from "./markets.js";
|
|
6
|
-
import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity } from "./rpc.js";
|
|
6
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
7
7
|
import { simulateLaunchTransactions } from "./simulation.js";
|
|
8
8
|
import { LifecyclePlanningError } from "./types.js";
|
|
9
9
|
function validatePlanShape(plan) {
|
|
@@ -74,47 +74,59 @@ function validatePlanShape(plan) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
async function readFunding(client, plan, block) {
|
|
77
|
-
const escrow = await
|
|
78
|
-
|
|
77
|
+
const [escrow, registry, balanceValue] = await Promise.all([
|
|
78
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block),
|
|
79
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
|
|
80
|
+
lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block),
|
|
81
|
+
]);
|
|
82
|
+
const nativeBalance = rpcQuantity(balanceValue, "creator native balance");
|
|
83
|
+
const [authority, observations] = await Promise.all([
|
|
84
|
+
readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "core", [], block),
|
|
85
|
+
Promise.all(plan.funding.map(async (funding) => {
|
|
86
|
+
const native = funding.kind === LifecycleFundingKind.NativeWrap || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress);
|
|
87
|
+
const [input, wrapped, target] = await Promise.all([
|
|
88
|
+
native ? [nativeBalance, undefined] : Promise.all([
|
|
89
|
+
readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
|
|
90
|
+
readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
|
|
91
|
+
]),
|
|
92
|
+
funding.kind === LifecycleFundingKind.NativeWrap ? readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block) : undefined,
|
|
93
|
+
funding.kind === LifecycleFundingKind.Swap ? Promise.all([
|
|
94
|
+
readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block),
|
|
95
|
+
lifecyclePinnedRpc(client, "eth_getCode", [funding.target, toHex(block.number)], block),
|
|
96
|
+
]) : undefined,
|
|
97
|
+
]);
|
|
98
|
+
let reason;
|
|
99
|
+
if (wrapped !== undefined && (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase()))
|
|
100
|
+
reason = "Committed native-wrap output is not the escrow's supported wrapped native asset";
|
|
101
|
+
if (target !== undefined) {
|
|
102
|
+
const [[spender, codeHash, enabled], codeValue] = target;
|
|
103
|
+
const code = rpcHex(codeValue, "funding target code");
|
|
104
|
+
if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase())
|
|
105
|
+
reason = "Committed funding conversion target is not currently allowlisted with its immutable code hash";
|
|
106
|
+
}
|
|
107
|
+
const prerequisite = { funding, inputBalance: input[0], requiredInput: funding.inputAmount, allowance: input[1], spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" };
|
|
108
|
+
return { prerequisite, reason };
|
|
109
|
+
})),
|
|
110
|
+
]);
|
|
111
|
+
if (authority.toLowerCase() !== plan.orchestrator.toLowerCase())
|
|
79
112
|
throw new LifecyclePlanningError("FUNDING_BINDING", "Funding escrow authority differs from the committed orchestrator");
|
|
80
|
-
const registry = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
81
|
-
const nativeBalance = rpcQuantity(await lifecycleRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)]), "creator native balance");
|
|
82
113
|
const totals = new Map();
|
|
83
114
|
const prerequisites = [];
|
|
84
115
|
const approvals = [];
|
|
85
116
|
const reasons = [];
|
|
86
117
|
let nativeValue = 0n;
|
|
87
|
-
for (const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
for (const { prerequisite, reason } of observations) {
|
|
119
|
+
prerequisites.push(prerequisite);
|
|
120
|
+
if (reason !== undefined)
|
|
121
|
+
reasons.push(reason);
|
|
122
|
+
nativeValue += prerequisite.nativeValue;
|
|
123
|
+
if (prerequisite.allowance !== undefined) {
|
|
124
|
+
const { funding } = prerequisite;
|
|
94
125
|
const key = funding.inputAsset.toLowerCase();
|
|
95
|
-
|
|
96
|
-
if (total === undefined) {
|
|
97
|
-
inputBalance = await readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block);
|
|
98
|
-
allowance = await readLifecycleContract(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block);
|
|
99
|
-
total = { asset: funding.inputAsset, amount: 0n, balance: inputBalance, allowance };
|
|
100
|
-
totals.set(key, total);
|
|
101
|
-
}
|
|
126
|
+
const total = totals.get(key) ?? { asset: funding.inputAsset, amount: 0n, balance: prerequisite.inputBalance, allowance: prerequisite.allowance };
|
|
102
127
|
total.amount += funding.inputAmount;
|
|
103
|
-
|
|
104
|
-
allowance = total.allowance;
|
|
128
|
+
totals.set(key, total);
|
|
105
129
|
}
|
|
106
|
-
if (funding.kind === LifecycleFundingKind.NativeWrap) {
|
|
107
|
-
const wrapped = await readLifecycleContract(client, escrow, lifecycleFundingEscrowAbi, "wrappedNative", [], block);
|
|
108
|
-
if (wrapped === zeroAddress || wrapped.toLowerCase() !== funding.asset.toLowerCase())
|
|
109
|
-
reasons.push("Committed native-wrap output is not the escrow's supported wrapped native asset");
|
|
110
|
-
}
|
|
111
|
-
if (funding.kind === LifecycleFundingKind.Swap) {
|
|
112
|
-
const [spender, codeHash, enabled] = await readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingTarget", [funding.target], block);
|
|
113
|
-
const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [funding.target, toHex(block.number)]), "funding target code");
|
|
114
|
-
if (!enabled || spender === zeroAddress || code === "0x" || keccak256(code).toLowerCase() !== codeHash.toLowerCase())
|
|
115
|
-
reasons.push("Committed funding conversion target is not currently allowlisted with its immutable code hash");
|
|
116
|
-
}
|
|
117
|
-
prerequisites.push({ funding, inputBalance, requiredInput: funding.inputAmount, allowance, spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" });
|
|
118
130
|
}
|
|
119
131
|
if (nativeBalance < nativeValue)
|
|
120
132
|
reasons.push("Creator native balance does not cover committed native funding inputs");
|
|
@@ -131,17 +143,6 @@ async function readFunding(client, plan, block) {
|
|
|
131
143
|
}
|
|
132
144
|
async function validatePendingInputs(client, planned, progress, block) {
|
|
133
145
|
const { plan } = planned;
|
|
134
|
-
const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block);
|
|
135
|
-
const registry = await readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
136
|
-
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
137
|
-
if (block.timestamp > plan.deadline)
|
|
138
|
-
reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
139
|
-
for (const policy of plan.feeAssets)
|
|
140
|
-
if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
|
|
141
|
-
reasons.push("Only the launch token may have a burn fee share");
|
|
142
|
-
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase()))
|
|
143
|
-
reasons.push("The launch token must have a committed fee policy");
|
|
144
|
-
const hookDeployments = [];
|
|
145
146
|
const assets = new Set();
|
|
146
147
|
for (const policy of plan.feeAssets)
|
|
147
148
|
if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
|
|
@@ -153,90 +154,131 @@ async function validatePendingInputs(client, planned, progress, block) {
|
|
|
153
154
|
if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress)
|
|
154
155
|
assets.add(funding.inputAsset);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const [profiles, registry, assetCodes] = await Promise.all([
|
|
158
|
+
readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
|
|
159
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
|
|
160
|
+
Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
|
|
161
|
+
]);
|
|
162
|
+
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
163
|
+
if (block.timestamp > plan.deadline)
|
|
164
|
+
reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
165
|
+
for (const policy of plan.feeAssets)
|
|
166
|
+
if (policy.burnBps !== 0 && policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase())
|
|
167
|
+
reasons.push("Only the launch token may have a burn fee share");
|
|
168
|
+
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase()))
|
|
169
|
+
reasons.push("The launch token must have a committed fee policy");
|
|
170
|
+
for (const { asset, code } of assetCodes)
|
|
171
|
+
if (code === "0x")
|
|
158
172
|
reasons.push(`Asset ${asset} has no contract code`);
|
|
159
|
-
|
|
173
|
+
const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
|
|
174
|
+
const [allowedInputs, markets, escrowBalances] = await Promise.all([
|
|
175
|
+
Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
|
|
176
|
+
? readLifecycleContract(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true)),
|
|
177
|
+
Promise.all(plan.markets.map(async (market, index) => {
|
|
178
|
+
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
179
|
+
let positions = 0;
|
|
180
|
+
let deployment;
|
|
181
|
+
let identity;
|
|
182
|
+
let reason;
|
|
183
|
+
if (profile === undefined || !profile.admitted)
|
|
184
|
+
return { positions, deployment, identity, reason };
|
|
185
|
+
try {
|
|
186
|
+
if (market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() || market.configVersion !== profile.topology.configVersion)
|
|
187
|
+
throw new LifecyclePlanningError("INVALID_MARKET", "Market adapter or version differs from exact certified profile");
|
|
188
|
+
if (market.quoteAsset.toLowerCase() === planned.predictedToken.toLowerCase() || !plan.feeAssets.some((policy) => policy.asset.toLowerCase() === market.quoteAsset.toLowerCase()))
|
|
189
|
+
throw new LifecyclePlanningError("INVALID_MARKET", "Each distinct quote requires its committed fee policy");
|
|
190
|
+
const schema = profile.registration.configSchema.toLowerCase();
|
|
191
|
+
const config = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeV4LifecycleMarketConfig(market.config) : schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodePoolBoundV4LifecycleMarketConfig(market.config) : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? decodeAbyssLifecycleMarketConfig(market.config) : undefined;
|
|
192
|
+
if (config === undefined)
|
|
193
|
+
throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
|
|
194
|
+
if ("developerFeeBps" in config) {
|
|
195
|
+
validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
|
|
196
|
+
if (config.version === 4)
|
|
197
|
+
await validateV4LifecycleOracle({ client, envelope: profile.envelope, oracleConfigId: config.oracleConfigId, block });
|
|
198
|
+
}
|
|
199
|
+
if (config.positions.length === 0 || config.positions.length > 32)
|
|
200
|
+
throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
201
|
+
positions = config.positions.length;
|
|
202
|
+
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
203
|
+
if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0)
|
|
204
|
+
throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
|
|
205
|
+
const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
206
|
+
const [adapter, boundDeployment] = await Promise.all([
|
|
207
|
+
readLifecycleContract(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
|
|
208
|
+
profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
|
|
209
|
+
]);
|
|
210
|
+
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase())
|
|
211
|
+
throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
212
|
+
if (boundDeployment !== undefined) {
|
|
213
|
+
if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook))
|
|
214
|
+
throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
215
|
+
deployment = { marketIndex: index, ...boundDeployment };
|
|
216
|
+
}
|
|
217
|
+
identity = await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
|
|
218
|
+
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
219
|
+
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
220
|
+
if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0)))
|
|
221
|
+
throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
|
|
222
|
+
const prepared = progress.markets.find((item) => item.index === index);
|
|
223
|
+
if (prepared !== undefined) {
|
|
224
|
+
const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
|
|
225
|
+
const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
|
|
226
|
+
if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96)
|
|
227
|
+
throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
|
|
228
|
+
await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (failure) {
|
|
232
|
+
reason = failure instanceof Error ? failure.message : String(failure);
|
|
233
|
+
}
|
|
234
|
+
return { positions, deployment, identity, reason };
|
|
235
|
+
})),
|
|
236
|
+
progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
|
|
237
|
+
]);
|
|
238
|
+
for (const [index, funding] of plan.funding.entries()) {
|
|
160
239
|
if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase()))
|
|
161
240
|
reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
|
|
162
|
-
if (
|
|
241
|
+
if (!allowedInputs[index])
|
|
163
242
|
reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
|
|
164
243
|
}
|
|
165
|
-
const
|
|
244
|
+
const hookDeployments = [];
|
|
166
245
|
const identities = new Set();
|
|
167
246
|
const v4Quotes = new Set();
|
|
168
247
|
let positionCount = 0;
|
|
169
|
-
for (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
throw new LifecyclePlanningError("INVALID_PLAN", "Market missing from ordered plan");
|
|
173
|
-
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
174
|
-
if (profile === undefined || !profile.admitted)
|
|
248
|
+
for (const [index, result] of markets.entries()) {
|
|
249
|
+
if (positionCount + result.positions > 32) {
|
|
250
|
+
reasons.push("A launch commits at most 32 positive positions across every market/venue");
|
|
175
251
|
continue;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
192
|
-
positionCount += config.positions.length;
|
|
193
|
-
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
194
|
-
if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0)
|
|
195
|
-
throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
|
|
196
|
-
const capabilities = required | (config.positions.length > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
197
|
-
const adapter = await readLifecycleContract(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block);
|
|
198
|
-
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase())
|
|
199
|
-
throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
200
|
-
const deployment = profile.topology.hookTopology === 2 ? await readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined;
|
|
201
|
-
if (deployment !== undefined) {
|
|
202
|
-
if (!hasLifecycleV4HookPermissions(deployment.predictedHook))
|
|
203
|
-
throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
204
|
-
hookDeployments.push({ marketIndex: index, ...deployment });
|
|
205
|
-
}
|
|
206
|
-
const identity = await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
|
|
207
|
-
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
208
|
-
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
209
|
-
if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0)))
|
|
210
|
-
throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
|
|
211
|
-
if (identity.venue === LifecycleVenue.UniswapV4) {
|
|
212
|
-
const quote = market.quoteAsset.toLowerCase();
|
|
213
|
-
if (v4Quotes.has(quote))
|
|
214
|
-
throw new LifecyclePlanningError("DUPLICATE_V4_QUOTE", "Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
|
|
215
|
-
v4Quotes.add(quote);
|
|
216
|
-
}
|
|
217
|
-
if (identities.has(identity.canonicalId.toLowerCase()))
|
|
218
|
-
throw new LifecyclePlanningError("DUPLICATE_MARKET", "Two committed markets resolve to the same canonical pool");
|
|
219
|
-
identities.add(identity.canonicalId.toLowerCase());
|
|
220
|
-
const prepared = progress.markets.find((item) => item.index === index);
|
|
221
|
-
if (prepared !== undefined) {
|
|
222
|
-
const resolvedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [identity]));
|
|
223
|
-
const preparedHash = keccak256(encodeAbiParameters([{ type: "tuple", components: marketIdentityV1Components }], [prepared.prepared.identity]));
|
|
224
|
-
if (resolvedHash !== preparedHash || prepared.live === undefined || prepared.live.publicTrading || prepared.live.liquidity !== 0n || prepared.live.sqrtPriceX96 !== identity.openingSqrtPriceX96)
|
|
225
|
-
throw new LifecyclePlanningError("OPENING_CHANGED", "Prepared market is not at its committed empty canonical opening state");
|
|
226
|
-
await readLifecycleContract(client, adapter, lifecycleAdapterAbi, "validatePrepared", [planned.launchId, index, planned.predictedToken, market, prepared.prepared.identity], block);
|
|
252
|
+
}
|
|
253
|
+
positionCount += result.positions;
|
|
254
|
+
if (result.deployment !== undefined)
|
|
255
|
+
hookDeployments.push(result.deployment);
|
|
256
|
+
if (result.reason !== undefined) {
|
|
257
|
+
reasons.push(result.reason);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (result.identity === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
if (result.identity.venue === LifecycleVenue.UniswapV4) {
|
|
263
|
+
const quote = plan.markets[index].quoteAsset.toLowerCase();
|
|
264
|
+
if (v4Quotes.has(quote)) {
|
|
265
|
+
reasons.push("Only one V4 market per quote asset is allowed, regardless of adapter, manager, profile or salt");
|
|
266
|
+
continue;
|
|
227
267
|
}
|
|
268
|
+
v4Quotes.add(quote);
|
|
228
269
|
}
|
|
229
|
-
|
|
230
|
-
|
|
270
|
+
const identity = result.identity.canonicalId.toLowerCase();
|
|
271
|
+
if (identities.has(identity)) {
|
|
272
|
+
reasons.push("Two committed markets resolve to the same canonical pool");
|
|
273
|
+
continue;
|
|
231
274
|
}
|
|
275
|
+
identities.add(identity);
|
|
232
276
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
|
|
239
|
-
}
|
|
277
|
+
for (const [index, balance] of escrowBalances.entries()) {
|
|
278
|
+
const funding = plan.funding[index];
|
|
279
|
+
const needed = plan.buys.filter((buy) => plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() === funding.asset.toLowerCase()).reduce((sum, buy) => sum + buy.quoteAmountIn, 0n);
|
|
280
|
+
if (balance < needed)
|
|
281
|
+
reasons.push(`Launch-isolated ${funding.asset} escrow cannot cover every committed buy`);
|
|
240
282
|
}
|
|
241
283
|
return { profiles, hookDeployments, reasons };
|
|
242
284
|
}
|
|
@@ -267,13 +309,17 @@ function orderedTransactions(plan, mode, progress, approvals, nativeValue, batch
|
|
|
267
309
|
function attachEstimates(transactions, simulation) {
|
|
268
310
|
return transactions.map((transaction) => {
|
|
269
311
|
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
|
|
270
|
-
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, reason: simulation.reason, limits: simulation.limits } };
|
|
312
|
+
return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
|
|
271
313
|
});
|
|
272
314
|
}
|
|
273
315
|
async function readTokenFactoryBinding(client, orchestrator, block) {
|
|
274
316
|
const tokenFactory = await readLifecycleContract(client, orchestrator, launchLifecycleAbi, "tokenFactory", [], block);
|
|
275
|
-
const
|
|
276
|
-
|
|
317
|
+
const [codeValue, authority] = await Promise.all([
|
|
318
|
+
lifecyclePinnedRpc(client, "eth_getCode", [tokenFactory, toHex(block.number)], block),
|
|
319
|
+
readLifecycleContract(client, tokenFactory, lifecycleAdapterAbi, "core", [], block),
|
|
320
|
+
]);
|
|
321
|
+
const code = rpcHex(codeValue, "token factory code");
|
|
322
|
+
if (tokenFactory === zeroAddress || code === "0x" || authority.toLowerCase() !== orchestrator.toLowerCase())
|
|
277
323
|
throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Token factory is not live and bound to the exact lifecycle core");
|
|
278
324
|
return { tokenFactory, tokenFactoryCodeHash: keccak256(code) };
|
|
279
325
|
}
|
|
@@ -357,7 +403,15 @@ export async function preparePoolBoundLifecyclePlan(options) {
|
|
|
357
403
|
}
|
|
358
404
|
/** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
|
|
359
405
|
export async function planLaunch(options) {
|
|
360
|
-
|
|
406
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
407
|
+
const planned = await planLaunchWithReads(options, client);
|
|
408
|
+
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase())
|
|
409
|
+
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
|
|
410
|
+
return planned;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
async function planLaunchWithReads(options, client) {
|
|
414
|
+
const { plan, account, mode } = options;
|
|
361
415
|
if (mode !== "atomic" && mode !== "staged")
|
|
362
416
|
throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
363
417
|
if (account.toLowerCase() !== plan.creator.toLowerCase())
|
|
@@ -365,19 +419,24 @@ export async function planLaunch(options) {
|
|
|
365
419
|
if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n)
|
|
366
420
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
367
421
|
validatePlanShape(plan);
|
|
368
|
-
const block = await readLifecycleBlock(client);
|
|
369
|
-
if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== plan.chainId)
|
|
370
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
371
422
|
const planHash = hashLaunchPlan(plan);
|
|
372
423
|
const launchId = hashLaunchIdentity(plan);
|
|
373
|
-
const
|
|
374
|
-
|
|
424
|
+
const [block, chainIdValue] = await Promise.all([
|
|
425
|
+
readLifecycleBlock(client),
|
|
426
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
427
|
+
]);
|
|
428
|
+
if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId)
|
|
429
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
430
|
+
const [remoteHash, remoteIdentity, predictedToken, { tokenFactory, tokenFactoryCodeHash }, { limits }] = await Promise.all([
|
|
431
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
|
|
432
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
|
|
433
|
+
readLifecycleContract(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block),
|
|
434
|
+
readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
435
|
+
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
436
|
+
]);
|
|
375
437
|
if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase())
|
|
376
438
|
throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
|
|
377
|
-
const
|
|
378
|
-
const { tokenFactory, tokenFactoryCodeHash } = await readTokenFactoryBinding(client, plan.orchestrator, block);
|
|
379
|
-
const { limits } = await resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits);
|
|
380
|
-
const placeholder = { backend: "unavailable", confidence: "provisional", admitted: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
439
|
+
const placeholder = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
381
440
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
382
441
|
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
383
442
|
let planned = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
|
|
@@ -390,20 +449,22 @@ export async function planLaunch(options) {
|
|
|
390
449
|
}
|
|
391
450
|
// Receipt/nonce gates never silently drop the finalized deployment baseline on reload.
|
|
392
451
|
const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
|
|
393
|
-
await assertLifecycleBlock(client, block);
|
|
452
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
394
453
|
return { ...planned, hookDeployments, simulation: waiting, atomicAttempt: waiting };
|
|
395
454
|
}
|
|
396
455
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
397
|
-
await assertLifecycleBlock(client, block);
|
|
398
|
-
return { ...planned, simulation: { ...placeholder, confidence: "stateful", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
456
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
457
|
+
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
399
458
|
}
|
|
400
|
-
const prerequisites =
|
|
401
|
-
|
|
459
|
+
const [prerequisites, validation] = await Promise.all([
|
|
460
|
+
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
|
|
461
|
+
validatePendingInputs(client, planned, progress, block),
|
|
462
|
+
]);
|
|
402
463
|
const reasons = [...prerequisites.reasons, ...validation.reasons];
|
|
403
464
|
planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
|
|
404
465
|
if (reasons.length > 0) {
|
|
405
466
|
const rejected = { ...placeholder, confidence: "stateful", reason: reasons.join("; ") };
|
|
406
|
-
await assertLifecycleBlock(client, block);
|
|
467
|
+
await assertLifecycleBlock(client, block, plan.chainId);
|
|
407
468
|
return { ...planned, simulation: rejected, atomicAttempt: rejected, transactions: orderedTransactions(plan, mode, progress, prerequisites.approvals, prerequisites.nativeValue, planned.preparationBatchSize) };
|
|
408
469
|
}
|
|
409
470
|
let atomicAttempt = { ...placeholder, reason: "Atomic execution cannot be evaluated from an already begun staged launch" };
|
|
@@ -428,6 +489,40 @@ export async function planLaunch(options) {
|
|
|
428
489
|
simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
|
|
429
490
|
return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
|
|
430
491
|
}
|
|
492
|
+
async function preflightNextTransaction(options, next, simulation) {
|
|
493
|
+
if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
|
|
494
|
+
simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
|
|
495
|
+
next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
|
|
496
|
+
simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
|
|
497
|
+
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
|
|
498
|
+
}
|
|
499
|
+
const submissionClient = options.submissionClient;
|
|
500
|
+
if (submissionClient === undefined)
|
|
501
|
+
return { ...next, dependencies: [] };
|
|
502
|
+
try {
|
|
503
|
+
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
|
|
504
|
+
throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
505
|
+
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
506
|
+
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
507
|
+
}, "latest"]), "submission gas estimate");
|
|
508
|
+
if (estimated <= 0n || estimated > next.gas)
|
|
509
|
+
throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
510
|
+
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
|
|
511
|
+
throw new Error("Active submission RPC chain changed during preflight");
|
|
512
|
+
}
|
|
513
|
+
catch (failure) {
|
|
514
|
+
const reason = `Exact next-transaction submission preflight failed: ${failure instanceof Error ? failure.message : String(failure)}`;
|
|
515
|
+
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
516
|
+
}
|
|
517
|
+
const canonical = await readLifecycleBlock(options.client, toHex(simulation.blockNumber));
|
|
518
|
+
if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase())
|
|
519
|
+
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
|
|
520
|
+
if (rpcQuantity(await lifecycleRpc(options.client, "eth_chainId"), "chain ID") !== simulation.chainId)
|
|
521
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
|
|
522
|
+
if (next.admission === undefined)
|
|
523
|
+
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
524
|
+
return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
|
|
525
|
+
}
|
|
431
526
|
/** Re-reads canonical state, revalidates all prerequisites, and proves the remaining sequence afresh. */
|
|
432
527
|
export async function buildNextTransaction(options) {
|
|
433
528
|
const block = await readLifecycleBlock(options.client);
|
|
@@ -448,21 +543,22 @@ export async function buildNextTransaction(options) {
|
|
|
448
543
|
throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
|
|
449
544
|
const { planned } = options;
|
|
450
545
|
const transaction = { 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 }] };
|
|
451
|
-
const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits
|
|
546
|
+
const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork }, [transaction], block);
|
|
452
547
|
if (!simulation.admitted)
|
|
453
548
|
throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
454
|
-
|
|
549
|
+
const next = attachEstimates([transaction], simulation)[0];
|
|
550
|
+
if (next === undefined)
|
|
551
|
+
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
552
|
+
return preflightNextTransaction(options, next, simulation);
|
|
455
553
|
}
|
|
456
|
-
const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits
|
|
554
|
+
const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
|
|
457
555
|
if (!refreshed.simulation.admitted)
|
|
458
556
|
throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
|
|
459
557
|
assertPlannedDeploymentBindings(options.planned, refreshed);
|
|
460
558
|
const next = refreshed.transactions[0];
|
|
461
559
|
if (next === undefined)
|
|
462
560
|
return undefined;
|
|
463
|
-
|
|
464
|
-
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof");
|
|
465
|
-
return { ...next, dependencies: [] };
|
|
561
|
+
return preflightNextTransaction(options, next, refreshed.simulation);
|
|
466
562
|
}
|
|
467
563
|
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
468
564
|
* evidence and confirmation depth as build-next, never silently dropping context. */
|
|
@@ -472,7 +568,7 @@ export async function simulateLaunchPlan(options) {
|
|
|
472
568
|
throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
|
|
473
569
|
const current = await planLaunch({
|
|
474
570
|
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
475
|
-
limits: options.limits
|
|
571
|
+
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
476
572
|
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
|
|
477
573
|
});
|
|
478
574
|
assertPlannedDeploymentBindings(planned, current);
|