@black-market/sdk 0.5.0 → 0.5.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.
@@ -0,0 +1,180 @@
1
+ // Deliberately executing proof on an owned disposable loopback fixture only.
2
+ import assert from "node:assert/strict";
3
+ import { readFile } from "node:fs/promises";
4
+ import { decodeEventLog, decodeFunctionResult, encodeFunctionData, toHex, zeroHash } from "viem";
5
+ import {
6
+ buildClaimDeveloperFeesPageTransaction, buildNextTransaction, buildPoolBoundHookDeploymentTransaction,
7
+ createControlledLifecycleFork, decodeDeveloperClaimReceipt, decodePoolBoundV4LifecycleMarketConfig,
8
+ encodeLaunchPlan, encodePoolBoundV4LifecycleMarketConfig, hashLaunchIdentity, hashLaunchPlan,
9
+ launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleFeeHubAbi,
10
+ LifecyclePhase, parseLaunchPlan, planLaunch, predictLifecycleToken, preparePoolBoundLifecyclePlan,
11
+ readAuthorHubs, readDeveloperFees, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment,
12
+ simulateLaunchPlan,
13
+ } from "../dist/lifecycle/index.js";
14
+ const [manifestFile, plansFile] = process.argv.slice(2);
15
+ if (!manifestFile || !plansFile) throw new Error("Usage: node examples/launch-lifecycle-smoke.mjs manifest.json sdk-plans.json");
16
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
17
+ const exported = JSON.parse(await readFile(plansFile, "utf8"));
18
+ const rpcUrl = process.env.LAUNCH_LIFECYCLE_RPC_URL;
19
+ const forkRpcUrl = process.env.LAUNCH_LIFECYCLE_FORK_RPC_URL;
20
+ if (!rpcUrl || !forkRpcUrl || !["127.0.0.1", "localhost", "[::1]"].includes(new URL(rpcUrl).hostname) || process.env.LAUNCH_LIFECYCLE_ALLOW_LOCAL_EXECUTION !== "1" || manifest.fixtureOnly !== true) throw new Error("Smoke requires explicitly authorized owned loopback fixture execution and a separate disposable fork");
21
+ if (!Array.isArray(exported.plans) || exported.plans.length === 0) throw new Error("Current Solidity-exported reviewed plans are required");
22
+ let requestId = 0;
23
+ const client = { async request({ method, params = [] }) {
24
+ const response = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: ++requestId, method, params }) });
25
+ if (!response.ok) throw new Error(`RPC HTTP ${response.status}`);
26
+ const body = await response.json();
27
+ if (body.error) throw Object.assign(new Error(`${method}: ${body.error.message}`), { data: body.error.data });
28
+ return body.result;
29
+ } };
30
+ const chainId = BigInt(await client.request({ method: "eth_chainId" }));
31
+ assert.equal(chainId, BigInt(manifest.chainId));
32
+ const policy = manifest.executionLimits;
33
+ assert.equal(policy?.provenance?.scope, "controlled-local-measurement", "Operator policy must not invent live RPC/account limits");
34
+ const limits = async ({ block, chainId, account, orchestrator }) => ({
35
+ chainId, account, orchestrator, observedBlockNumber: block.number, observedBlockHash: block.hash,
36
+ chainGasLimit: BigInt(policy.chainTransactionGasLimit), rpcGasLimit: BigInt(policy.rpcTransactionGasLimit),
37
+ accountGasLimit: BigInt(policy.accountTransactionGasLimit), maxCalldataBytes: Number(policy.maxCalldataBytes), headroomBps: Number(policy.headroomBps),
38
+ });
39
+ const fork = createControlledLifecycleFork({ sourceRpcUrl: rpcUrl, forkRpcUrl, allowTransactions: true, impersonation: "anvil" });
40
+ async function readContract(to, abi, functionName, args = []) {
41
+ return decodeFunctionResult({ abi, functionName, data: await client.request({ method: "eth_call", params: [{ to, data: encodeFunctionData({ abi, functionName, args }) }, "latest"] }) });
42
+ }
43
+ async function send(transaction) {
44
+ assert.equal(BigInt(transaction.chainId), chainId);
45
+ const gasPrice = transaction.gasPrice ?? BigInt(await client.request({ method: "eth_gasPrice" }));
46
+ const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
47
+ const ceiling = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
48
+ const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) };
49
+ const estimated = transaction.gas ?? ((BigInt(await client.request({ method: "eth_estimateGas", params: [{ ...wire, gas: toHex(ceiling) }] })) * BigInt(10000 + Number(policy.headroomBps)) + 9999n) / 10000n);
50
+ assert.ok(estimated <= ceiling, "Unsigned helper must still fit explicit local execution admission");
51
+ const transactionHash = await client.request({ method: "eth_sendTransaction", params: [{ ...wire, gas: toHex(estimated) }] });
52
+ let receipt;
53
+ for (const deadline = Date.now() + 30000; Date.now() < deadline;) {
54
+ receipt = await client.request({ method: "eth_getTransactionReceipt", params: [transactionHash] });
55
+ if (receipt) break;
56
+ await new Promise((resolve) => setTimeout(resolve, 25));
57
+ }
58
+ assert.equal(receipt?.status, "0x1", `Actual local transaction must succeed: ${transactionHash}`);
59
+ return { receipt, reference: { transactionHash, observedBlockNumber: BigInt(receipt.blockNumber), observedBlockHash: receipt.blockHash, confirmations: 1 } };
60
+ }
61
+ function events(receipt, address, abi, name) {
62
+ return receipt.logs.filter((log) => log.address.toLowerCase() === address.toLowerCase()).flatMap((log) => {
63
+ try { const decoded = decodeEventLog({ abi, data: log.data, topics: log.topics, strict: true }); return decoded.eventName === name ? [decoded.args] : []; } catch { return []; }
64
+ });
65
+ }
66
+ const profiles = await readLifecycleProfiles({ client, orchestrator: manifest.addresses.orchestrator });
67
+ for (const row of exported.plans) for (const market of parseLaunchPlan(JSON.stringify(row.plan)).markets) {
68
+ const profile = profiles.find((profile) => profile.id.toLowerCase() === market.profileId.toLowerCase());
69
+ assert.equal(profile?.admitted, true, profile?.reason);
70
+ }
71
+ const results = [];
72
+ for (const row of exported.plans) for (const mode of ["atomic", "staged"]) {
73
+ const snapshot = await client.request({ method: "evm_snapshot" });
74
+ try {
75
+ let plan = parseLaunchPlan(JSON.stringify(row.plan));
76
+ assert.equal(encodeLaunchPlan(plan).toLowerCase(), row.encodedPlan.toLowerCase());
77
+ assert.equal(hashLaunchPlan(plan).toLowerCase(), row.planHash.toLowerCase());
78
+ assert.equal(hashLaunchIdentity(plan).toLowerCase(), row.launchId.toLowerCase());
79
+ assert.equal((await predictLifecycleToken({ client, plan })).toLowerCase(), row.predictedToken.toLowerCase());
80
+ const draft = { ...plan, markets: plan.markets.map((market) => market.configVersion === 5 ? {
81
+ ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config), hookSalt: zeroHash }),
82
+ } : market) };
83
+ const prepared = await preparePoolBoundLifecyclePlan({ client, plan: draft });
84
+ plan = prepared.plan;
85
+ assert.equal(hashLaunchPlan(plan).toLowerCase(), row.planHash.toLowerCase(), "Offchain mining reproduces the independent constructor/salt vector");
86
+ if (mode === "staged") for (const deployment of prepared.deployments) {
87
+ const unsigned = await buildPoolBoundHookDeploymentTransaction({ client, plan, marketIndex: deployment.marketIndex });
88
+ await send({ chainId: Number(chainId), from: plan.creator, ...unsigned });
89
+ const actual = await readPoolBoundHookDeployment({ client, plan, marketIndex: deployment.marketIndex });
90
+ assert.equal(actual.predictedHook.toLowerCase(), deployment.predictedHook.toLowerCase());
91
+ }
92
+ const planned = await planLaunch({ client, account: plan.creator, plan, mode, limits, fork });
93
+ assert.equal(planned.simulation.admitted, true, planned.simulation.reason);
94
+ const atomicReceipt = planned.atomicAttempt.steps.find((step) => step.transactionId === "atomic")?.returnData;
95
+ if (atomicReceipt) {
96
+ const decoded = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "launchAtomic", data: atomicReceipt });
97
+ assert.equal(decoded.planHash.toLowerCase(), planned.planHash.toLowerCase());
98
+ for (let i = 0; i < plan.buys.length; ++i) assert.ok(decoded.tokenOut[i] >= plan.buys[i].minTokenOut);
99
+ }
100
+ const replay = await simulateLaunchPlan({ client, planned, limits, fork });
101
+ assert.equal(replay.admitted, true, replay.reason);
102
+ const receipts = [];
103
+ let activationReceipt;
104
+ for (let guard = 0; guard < 64; ++guard) {
105
+ const next = await buildNextTransaction({ client, planned, receipts, limits, fork });
106
+ if (!next) break;
107
+ const submitted = await send(next);
108
+ receipts.push(submitted.reference);
109
+ if (["atomic", "activate"].includes(next.kind)) activationReceipt = submitted.receipt;
110
+ }
111
+ const progress = await readLaunchProgress({ client, planned, receipts });
112
+ assert.equal(progress.canonical.phase, LifecyclePhase.Active);
113
+ assert.equal(progress.token.toLowerCase(), row.predictedToken.toLowerCase());
114
+ assert.equal(progress.canonical.marketCount, plan.markets.length);
115
+ const buys = events(activationReceipt, plan.orchestrator, launchLifecycleAbi, "InitialBuyExecuted");
116
+ assert.equal(buys.length, plan.buys.length);
117
+ for (let i = 0; i < buys.length; ++i) {
118
+ assert.equal(buys[i].buyIndex, i); assert.equal(buys[i].marketIndex, plan.buys[i].marketIndex);
119
+ assert.ok(buys[i].tokenOut >= plan.buys[i].minTokenOut); assert.ok(buys[i].quoteSpent <= plan.buys[i].quoteAmountIn);
120
+ }
121
+ const directory = await readContract(plan.orchestrator, launchLifecycleAbi, "directory");
122
+ for (const market of progress.markets) {
123
+ assert.equal(market.live.publicTrading, true);
124
+ const positions = await readContract(directory, lifecycleDirectoryAbi, "positions", [planned.launchId, market.index, 0n, 32n]);
125
+ const profile = profiles.find((profile) => profile.id.toLowerCase() === market.prepared.identity.profileId.toLowerCase());
126
+ for (const position of positions) {
127
+ const [liquidity, custody] = await readContract(profile.adapter.implementation, lifecycleAdapterAbi, "readPosition", [position]);
128
+ assert.equal(liquidity, position.liquidity); assert.equal(custody.toLowerCase(), market.prepared.custody.toLowerCase());
129
+ }
130
+ }
131
+ assert.equal(await buildNextTransaction({ client, planned, receipts, limits, fork }), undefined);
132
+ const hub = progress.canonical.feeHub;
133
+ assert.equal(await readContract(hub, lifecycleFeeHubAbi, "economicVersion"), 3);
134
+ const authors = new Map();
135
+ for (const source of await readContract(hub, lifecycleFeeHubAbi, "sources")) {
136
+ const terms = await readContract(hub, lifecycleFeeHubAbi, "sourceTerms", [source]);
137
+ if (terms.developerFeeBps > 0) authors.set(terms.beneficiary.toLowerCase(), terms.beneficiary);
138
+ }
139
+ const ownerBefore = await Promise.all(plan.feeAssets.map((policy) => readContract(hub, lifecycleFeeHubAbi, "claimableOwnerFees", [plan.creator, policy.asset])));
140
+ const harvestData = encodeFunctionData({ abi: lifecycleFeeHubAbi, functionName: "claimAndSplit", args: [] });
141
+ const preview = decodeFunctionResult({ abi: lifecycleFeeHubAbi, functionName: "claimAndSplit", data: await client.request({ method: "eth_call", params: [{ from: plan.creator, to: hub, data: harvestData }, "latest"] }) });
142
+ const harvest = await send({ chainId: Number(chainId), from: plan.creator, to: hub, data: harvestData, value: 0n });
143
+ const distributions = events(harvest.receipt, hub, lifecycleFeeHubAbi, "Distributed");
144
+ assert.ok(distributions.some((event) => event.newlyCollected > 0n), "Real opening swaps produce actual source fees");
145
+ for (let i = 0; i < plan.feeAssets.length; ++i) {
146
+ const policy = plan.feeAssets[i];
147
+ const distribution = distributions.find((event) => event.asset.toLowerCase() === policy.asset.toLowerCase());
148
+ assert.equal(distribution.ownerAmount + distribution.developerAmount + distribution.executorAmount + distribution.rewardsAmount + distribution.burnAmount, distribution.newlyCollected);
149
+ assert.equal(preview.find((payment) => payment.asset.toLowerCase() === policy.asset.toLowerCase()).amount, distribution.executorAmount);
150
+ assert.equal(await readContract(hub, lifecycleFeeHubAbi, "claimableOwnerFees", [plan.creator, policy.asset]) - ownerBefore[i], distribution.ownerAmount);
151
+ assert.equal(await readContract(policy.asset, lifecycleErc20Abi, "balanceOf", [hub]),
152
+ await readContract(hub, lifecycleFeeHubAbi, "reservedOwnerFees", [policy.asset]) + await readContract(hub, lifecycleFeeHubAbi, "reservedDeveloperFees", [policy.asset]));
153
+ }
154
+ const claims = [];
155
+ for (const authorId of authors.values()) {
156
+ const discovered = await readAuthorHubs({ client, registry: manifest.addresses.implementationRegistry, authorId });
157
+ assert.ok(discovered.hubs.some((value) => value.toLowerCase() === hub.toLowerCase()));
158
+ const fees = await readDeveloperFees({ client, registry: discovered.registry, authorId, hub });
159
+ const balancesBefore = await Promise.all(fees.assets.map((asset) => readContract(asset, lifecycleErc20Abi, "balanceOf", [fees.payout])));
160
+ const unsigned = await buildClaimDeveloperFeesPageTransaction({ client, registry: discovered.registry, authorId, offset: 0n, limit: 10n, assets: [], account: plan.creator, chainId });
161
+ // Bound every page child call with the explicit owned-fixture operator ceiling;
162
+ // eth_estimateGas may otherwise succeed after reporting gas-starved failed rows.
163
+ const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
164
+ const pageGas = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
165
+ const claim = await send({ ...unsigned, gas: pageGas });
166
+ const result = decodeDeveloperClaimReceipt({ receipt: claim.receipt, transaction: unsigned });
167
+ assert.equal(result.cursorComplete, true); assert.equal(result.paymentsSucceeded, true);
168
+ for (let i = 0; i < fees.assets.length; ++i) {
169
+ const amount = result.results.filter((row) => row.asset.toLowerCase() === fees.assets[i].toLowerCase()).reduce((total, row) => total + row.amount, 0n);
170
+ assert.equal(await readContract(fees.assets[i], lifecycleErc20Abi, "balanceOf", [fees.payout]) - balancesBefore[i], amount);
171
+ }
172
+ claims.push({ authorId, transactionHash: claim.reference.transactionHash, result });
173
+ }
174
+ results.push({ name: row.name, mode, phase: "Active", planHash: planned.planHash, token: progress.token, backend: replay.backend, receipts, buys, distributions, claims });
175
+ } finally {
176
+ assert.equal(await client.request({ method: "evm_revert", params: [snapshot] }), true);
177
+ await client.request({ method: "evm_mine" });
178
+ }
179
+ }
180
+ console.log(JSON.stringify({ schema: "black-market.node-sdk-smoke.v1", results }, (_key, value) => typeof value === "bigint" ? value.toString() : value, 2));
@@ -0,0 +1,38 @@
1
+ // Reviewed launch planning only: no signatures, source-chain writes, uploads or publication.
2
+ import { readFile } from "node:fs/promises";
3
+ import { createPublicClient, http } from "viem";
4
+ import {
5
+ buildNextTransaction, createControlledLifecycleFork, parseLaunchPlan, planLaunch,
6
+ preparePoolBoundLifecyclePlan, readLaunchProgress, readLifecycleProfiles, serializeLaunchPlan,
7
+ } from "../dist/index.js";
8
+ const args = process.argv.slice(2);
9
+ const option = (name) => { const index = args.indexOf(name); return index === -1 ? undefined : args[index + 1]; };
10
+ const planFile = option("--plan"), rpcUrl = option("--rpc"), mode = option("--mode");
11
+ if (!planFile || !rpcUrl || !["atomic", "staged"].includes(mode)) {
12
+ throw new Error("Usage: node examples/launch.mjs --plan plan.json --rpc RPC_URL --mode atomic|staged [--limits current-limits.json] [--receipts receipts.json] [--confirmations N] [--fork-rpc LOOPBACK_RPC_URL] [--mine]");
13
+ }
14
+ const client = createPublicClient({ transport: http(rpcUrl) });
15
+ let plan = parseLaunchPlan(await readFile(planFile, "utf8"));
16
+ const profiles = await readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] });
17
+ for (const profile of profiles) console.log(profile.id, profile.venueKind, profile.topology, profile.admitted ? "admitted" : profile.reason);
18
+ if (args.includes("--mine")) plan = (await preparePoolBoundLifecyclePlan({ client, plan,
19
+ onProgress: ({ marketIndex, attempts }) => console.error("salt mining", marketIndex, attempts.toString()),
20
+ })).plan;
21
+ const limitsFile = option("--limits");
22
+ const limits = limitsFile ? JSON.parse(await readFile(limitsFile, "utf8")) : undefined;
23
+ if (limits) for (const key of ["chainId", "observedBlockNumber", "chainGasLimit", "rpcGasLimit", "accountGasLimit", "maxSimulationGas"]) {
24
+ if (limits[key] !== undefined) limits[key] = BigInt(limits[key]);
25
+ }
26
+ const receiptsFile = option("--receipts");
27
+ const receipts = receiptsFile ? JSON.parse(await readFile(receiptsFile, "utf8"), (key, value) => key === "observedBlockNumber" ? BigInt(value) : value) : [];
28
+ const confirmations = Number(option("--confirmations") ?? "1");
29
+ const forkRpcUrl = option("--fork-rpc");
30
+ const fork = forkRpcUrl ? createControlledLifecycleFork({ sourceRpcUrl: rpcUrl, forkRpcUrl, allowTransactions: true, impersonation: "anvil" }) : undefined;
31
+ const planned = await planLaunch({ client, account: plan.creator, plan, mode, limits, fork, receipts, confirmations });
32
+ const progress = await readLaunchProgress({ client, planned, receipts });
33
+ const next = planned.simulation.admitted ? await buildNextTransaction({ client, planned, limits, fork, receipts }) : undefined;
34
+ console.log(JSON.stringify({ plan: JSON.parse(serializeLaunchPlan(plan)), mode,
35
+ simulation: planned.simulation, atomicAttempt: planned.atomicAttempt, progress, next },
36
+ (_key, value) => typeof value === "bigint" ? value.toString() : value, 2));
37
+ // Persist the returned plan/mode/confirmation policy and receipt references. An application
38
+ // must separately review, authenticate its wallet chain and sign exactly `next` if admitted.
@@ -0,0 +1,37 @@
1
+ // Read-only deployed graph discovery. No signatures, transactions, uploads, or publication.
2
+ import { zeroAddress } from "viem";
3
+ import {
4
+ createProtocolPublicClient, getAddresses, isSupportedChainId,
5
+ launchLifecycleAbi, readLifecycleProfiles,
6
+ } from "../dist/index.js";
7
+
8
+ const chainId = Number(process.env.CHAIN_ID ?? "4663");
9
+ if (!isSupportedChainId(chainId)) throw new Error("CHAIN_ID must be 4663, 46631, or 31337");
10
+ const infrastructure = getAddresses(chainId);
11
+ if (infrastructure.launchOrchestrator === zeroAddress) {
12
+ throw new Error("Set LAUNCH_ORCHESTRATOR and the matching deployment addresses for this local chain; no mainnet launch address is assumed locally.");
13
+ }
14
+ const client = createProtocolPublicClient({ chainId });
15
+ if (await client.getChainId() !== chainId) throw new Error("RPC chain ID differs from CHAIN_ID");
16
+ const registry = await client.readContract({
17
+ address: infrastructure.launchOrchestrator,
18
+ abi: launchLifecycleAbi,
19
+ functionName: "registry",
20
+ });
21
+ if (registry.toLowerCase() !== infrastructure.launchImplementationRegistry.toLowerCase()) {
22
+ throw new Error("The selected orchestrator and LAUNCH_IMPLEMENTATION_REGISTRY are not the same deployment graph");
23
+ }
24
+ const profiles = await readLifecycleProfiles({ client, orchestrator: infrastructure.launchOrchestrator });
25
+ console.log("chain:", chainId);
26
+ console.log("orchestrator:", infrastructure.launchOrchestrator);
27
+ console.log("registry:", registry);
28
+ console.table(profiles.map((profile) => ({
29
+ profile: profile.id,
30
+ venue: profile.venueKind,
31
+ configVersion: profile.topology.configVersion,
32
+ admitted: profile.admitted,
33
+ reason: profile.reason ?? "",
34
+ })));
35
+ console.log("Abyss router:", infrastructure.abyssRouter);
36
+ console.log("lending pool:", infrastructure.lendingPool);
37
+ if (!profiles.some((profile) => profile.admitted)) throw new Error("No current profile was admitted by the selected deployment");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@black-market/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Canonical TypeScript SDK for Black Market on Robinhood Chain (reviewed multi-market launches, Abyss DEX, lending).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,7 +24,8 @@
24
24
  "dist",
25
25
  "src",
26
26
  "docs",
27
- "assets"
27
+ "assets",
28
+ "examples"
28
29
  ],
29
30
  "sideEffects": false,
30
31
  "engines": {
@@ -40,7 +41,6 @@
40
41
  "dev": "tsc -p tsconfig.json --watch",
41
42
  "examples": "pnpm build && node examples/quickstart.mjs",
42
43
  "lint": "tsc -p tsconfig.json --noEmit",
43
- "smoke:launch": "pnpm build && node examples/smoke-launch.mjs",
44
44
  "prepublishOnly": "pnpm build",
45
45
  "test": "pnpm test:unit",
46
46
  "test:unit": "pnpm build && node --test test/*.test.mjs",