@black-market/sdk 0.7.1 → 0.8.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 +58 -22
- package/dist/launch.d.ts +31 -5
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +71 -11
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +18 -0
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +4 -0
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/planner.d.ts +1 -1
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +58 -10
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.js +35 -35
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +10 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +137 -16
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/types.d.ts +20 -1
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +396 -64
- package/examples/.env.example +22 -2
- package/examples/README.md +315 -87
- package/examples/{launch-erc20-abyss.mjs → launch-erc20-abyss.ts} +1 -1
- package/examples/{launch-erc20-burn-mixed.mjs → launch-erc20-burn-mixed.ts} +1 -1
- package/examples/launch-erc20-depth-v4.ts +7 -0
- package/examples/{launch-erc20-dividends-abyss.mjs → launch-erc20-dividends-abyss.ts} +1 -1
- package/examples/launch-erc20-dividends-multiquote-mixed.ts +8 -0
- package/examples/launch-erc20-multi-quote.ts +8 -0
- package/examples/{launch-erc20-staking-v4.mjs → launch-erc20-staking-v4.ts} +1 -1
- package/examples/{launch-erc20-v4.mjs → launch-erc20-v4.ts} +1 -1
- package/examples/{launch-erc404-abyss.mjs → launch-erc404-abyss.ts} +1 -1
- package/examples/{launch-erc404-dividends-mixed.mjs → launch-erc404-dividends-mixed.ts} +1 -1
- package/examples/{launch-erc404-v4.mjs → launch-erc404-v4.ts} +1 -1
- package/examples/launch-example-kyber.ts +204 -0
- package/examples/launch-example-support.ts +876 -0
- package/examples/launch-example.ts +1163 -0
- package/examples/{launch-lifecycle-smoke.mjs → launch-lifecycle-smoke.ts} +1 -1
- package/examples/{launch.mjs → launch.ts} +1 -1
- package/package.json +2 -2
- package/src/launch.ts +81 -14
- package/src/lifecycle/abi.ts +4 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/planner.ts +61 -12
- package/src/lifecycle/progress.ts +35 -35
- package/src/lifecycle/rpc.ts +140 -17
- package/src/lifecycle/types.ts +19 -1
- package/examples/launch-example-support.mjs +0 -393
- package/examples/launch-example.mjs +0 -633
- /package/examples/{generate-lifecycle-commitment-fixture.mjs → generate-lifecycle-commitment-fixture.ts} +0 -0
- /package/examples/{quickstart.mjs → quickstart.ts} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { decodeFunctionData, decodeFunctionResult, encodeFunctionData, isAddress, keccak256, parseAbi, zeroAddress } from "viem";
|
|
3
|
+
import { assertFutureLaunchDeadline, exampleError, launchApiUrl } from "./launch-example-support.ts";
|
|
4
|
+
|
|
5
|
+
// Reviewed third-party deployment/entry point from abyss-app's trading/deployments.ts.
|
|
6
|
+
// Current launch registry admission and runtime, not these addresses alone, authorize funding.
|
|
7
|
+
export const EXAMPLE_KYBER_ROUTER = "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5";
|
|
8
|
+
export const EXAMPLE_KYBER_EXECUTOR = "0x8F10B468b06c6FD214B65F87778827F7D113f996";
|
|
9
|
+
export const EXAMPLE_KYBER_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
10
|
+
export const EXAMPLE_CONVERSION_SLIPPAGE_BPS = 50;
|
|
11
|
+
const EXECUTOR_IMPLEMENTATIONS = ["0x4f1423cce26c09fb31d96eafe702fc5e22421f6a", "0xd7a5c6b52756e795f680b0f7e0c2f21281dde6ef"];
|
|
12
|
+
export const exampleKyberRouterAbi = parseAbi([
|
|
13
|
+
"function swap((address callTarget, address approveTarget, bytes targetData, (address srcToken, address dstToken, address[] srcReceivers, uint256[] srcAmounts, address[] feeReceivers, uint256[] feeAmounts, address dstReceiver, uint256 amount, uint256 minReturnAmount, uint256 flags, bytes permit) desc, bytes clientData) execution) payable returns (uint256 returnAmount, uint256 gasUsed)",
|
|
14
|
+
]);
|
|
15
|
+
const escrowRegistryAbi = parseAbi(["function registry() view returns (address)"]);
|
|
16
|
+
const same = (left, right) => typeof left === "string" && typeof right === "string" && left.toLowerCase() === right.toLowerCase();
|
|
17
|
+
const uint = (value, label) => {
|
|
18
|
+
if (typeof value !== "string" || !/^(?:0|[1-9]\d{0,77})$/.test(value) || BigInt(value) >= 1n << 256n) {
|
|
19
|
+
throw exampleError("INVALID_KYBER_RESPONSE", `${label} must be a canonical uint256 decimal string`);
|
|
20
|
+
}
|
|
21
|
+
return BigInt(value);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function validateQuoteIdentity(response, quoteAsset, amountIn) {
|
|
25
|
+
if (response?.chainId !== 4663 || response.source !== "kyberswap" ||
|
|
26
|
+
!same(response.tokenIn, EXAMPLE_KYBER_NATIVE) || !same(response.tokenOut, quoteAsset) ||
|
|
27
|
+
!same(response.routerAddress, EXAMPLE_KYBER_ROUTER) || uint(response.amountIn, "Kyber input") !== amountIn ||
|
|
28
|
+
uint(response.amountOut, "Kyber output") <= 0n) {
|
|
29
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber response changes the reviewed chain, native input, quote, exact amount or router");
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(response.routePaths) || response.routePaths.length > 64 ||
|
|
32
|
+
response.routePaths.some((path) => !Array.isArray(path) || path.length < 1 || path.length > 16 ||
|
|
33
|
+
path.some((hop) => !hop || !isAddress(hop.tokenIn) || !isAddress(hop.tokenOut) ||
|
|
34
|
+
![hop.pool, hop.exchange, hop.poolType].every((text) => typeof text === "string" && text.length > 0 && text.length <= 256)))) {
|
|
35
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber response must retain bounded route-path evidence");
|
|
36
|
+
}
|
|
37
|
+
if (response.priceImpactBps !== undefined && !Number.isSafeInteger(response.priceImpactBps)) {
|
|
38
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber price impact must be a signed integer when provided; missing impact stays unknown");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Exact app launch protection and reviewed swap entry point; never rebuild opaque bytes. */
|
|
43
|
+
export function validateExampleKyberBuild({ response, quoteAsset, amountIn, escrow, deadline, nowMs = Date.now() }) {
|
|
44
|
+
assertFutureLaunchDeadline(deadline, nowMs);
|
|
45
|
+
if (amountIn <= 0n || amountIn >= 1n << 255n || deadline > BigInt(Math.floor(nowMs / 1000)) + 1200n) {
|
|
46
|
+
throw exampleError("INVALID_KYBER_INTENT", "Kyber launch input or deadline exceeds the app's supported intent bounds");
|
|
47
|
+
}
|
|
48
|
+
validateQuoteIdentity(response, quoteAsset, amountIn);
|
|
49
|
+
const minimum = uint(response.amountOutMinimum, "Kyber minimum output");
|
|
50
|
+
const expectedMinimum = uint(response.amountOut, "Kyber output") * BigInt(10000 - EXAMPLE_CONVERSION_SLIPPAGE_BPS) / 10000n;
|
|
51
|
+
const transaction = response.transaction;
|
|
52
|
+
if (minimum <= 0n || minimum !== expectedMinimum || response.expiresAt !== Number(deadline) ||
|
|
53
|
+
!same(transaction?.to, EXAMPLE_KYBER_ROUTER) || uint(transaction?.value, "Kyber transaction value") !== amountIn ||
|
|
54
|
+
typeof transaction?.data !== "string" || !/^0x(?:[\da-f]{2})+$/i.test(transaction.data) ||
|
|
55
|
+
(transaction.data.length - 2) / 2 > 16384) {
|
|
56
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber build changes the exact native value, output floor, deadline or lifecycle calldata bounds");
|
|
57
|
+
}
|
|
58
|
+
let decoded;
|
|
59
|
+
try { decoded = decodeFunctionData({ abi: exampleKyberRouterAbi, data: transaction.data }); }
|
|
60
|
+
catch { throw exampleError("INVALID_KYBER_CALLDATA", "Kyber calldata does not call the reviewed swap entry point"); }
|
|
61
|
+
if (decoded.functionName !== "swap") throw exampleError("INVALID_KYBER_CALLDATA", "Kyber calldata does not call the reviewed swap entry point");
|
|
62
|
+
const execution = decoded.args[0];
|
|
63
|
+
const desc = execution.desc;
|
|
64
|
+
if (!same(desc.srcToken, EXAMPLE_KYBER_NATIVE) || !same(desc.dstToken, quoteAsset) || !same(desc.dstReceiver, escrow) ||
|
|
65
|
+
desc.amount !== amountIn || desc.minReturnAmount !== minimum ||
|
|
66
|
+
!same(execution.callTarget, EXAMPLE_KYBER_EXECUTOR) || !same(execution.approveTarget, zeroAddress) ||
|
|
67
|
+
execution.targetData.length < 106 || (execution.targetData.length - 42) % 64 !== 0 ||
|
|
68
|
+
!EXECUTOR_IMPLEMENTATIONS.includes(execution.targetData.slice(0, 42).toLowerCase()) ||
|
|
69
|
+
(desc.flags & 0x42n) !== 0n || desc.srcReceivers.length !== 0 || desc.srcAmounts.length !== 0 ||
|
|
70
|
+
desc.feeReceivers.length !== 0 || desc.feeAmounts.length !== 0 || desc.permit !== "0x") {
|
|
71
|
+
throw exampleError("INVALID_KYBER_CALLDATA", "Kyber calldata changes a reviewed currency, escrow recipient, exact input, minimum, executor or fee policy");
|
|
72
|
+
}
|
|
73
|
+
return minimum;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function assertExampleConversionAdmission({ client, fixture, escrow, sdk, block, artifacts }) {
|
|
77
|
+
const read = async (to, abi, functionName, args = []) => decodeFunctionResult({ abi, functionName,
|
|
78
|
+
data: await client.request({ method: "eth_call", params: [{ to, data: encodeFunctionData({ abi, functionName, args }) }, block.number] }) });
|
|
79
|
+
const [core, escrowRegistry, registryCore, wrappedNative, registration, code, executorCode] = await Promise.all([
|
|
80
|
+
read(escrow, sdk.lifecycleFundingEscrowAbi, "core"), read(escrow, escrowRegistryAbi, "registry"),
|
|
81
|
+
read(fixture.implementationRegistry, sdk.lifecycleRegistryAbi, "core"), read(escrow, sdk.lifecycleFundingEscrowAbi, "wrappedNative"),
|
|
82
|
+
read(fixture.implementationRegistry, sdk.lifecycleRegistryAbi, "fundingTarget", [EXAMPLE_KYBER_ROUTER]),
|
|
83
|
+
client.request({ method: "eth_getCode", params: [EXAMPLE_KYBER_ROUTER, block.number] }),
|
|
84
|
+
client.request({ method: "eth_getCode", params: [EXAMPLE_KYBER_EXECUTOR, block.number] }),
|
|
85
|
+
]);
|
|
86
|
+
assert.ok(same(core, fixture.orchestrator) && same(escrowRegistry, fixture.implementationRegistry) &&
|
|
87
|
+
same(registryCore, fixture.orchestrator) && same(wrappedNative, fixture.quoteAsset), "Current funding escrow/registry/core/native bindings");
|
|
88
|
+
const [spender, codeHash, enabled] = registration;
|
|
89
|
+
const admitted = enabled && same(spender, EXAMPLE_KYBER_ROUTER) && code !== "0x" && same(keccak256(code), codeHash);
|
|
90
|
+
artifacts.event("kyber-conversion-admission", { target: EXAMPLE_KYBER_ROUTER, spender, codeHash, enabled, admitted,
|
|
91
|
+
nativeInput: true, blockNumber: block.number, blockHash: block.hash });
|
|
92
|
+
if (!admitted) throw exampleError("KYBER_CONVERSION_NOT_ADMITTED",
|
|
93
|
+
"Kyber conversion is not admitted by this launch deployment. No separate swap will be submitted.",
|
|
94
|
+
{ target: EXAMPLE_KYBER_ROUTER, spender, codeHash, enabled, blockNumber: block.number, blockHash: block.hash });
|
|
95
|
+
assert.notEqual(executorCode, "0x", "Reviewed Kyber executor must be deployed");
|
|
96
|
+
const canonical = await client.request({ method: "eth_getBlockByNumber", params: [block.number, false] });
|
|
97
|
+
assert.ok(same(canonical?.hash, block.hash), "Funding authority block remains canonical");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function requestKyber({ fixture, path, body, signal, fetchImpl }) {
|
|
101
|
+
const url = new URL(path, launchApiUrl(fixture.kyberApiUrl));
|
|
102
|
+
const response = await fetchImpl(url, { method: body === undefined ? "GET" : "POST", redirect: "error", cache: "no-store",
|
|
103
|
+
headers: body === undefined ? { Accept: "application/json" } : { Accept: "application/json", "Content-Type": "application/json" },
|
|
104
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
105
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000) });
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
108
|
+
throw exampleError(errorBody.code ?? "KYBER_API_UNAVAILABLE", errorBody.error ?? `Kyber API failed: HTTP ${response.status}`, { status: response.status });
|
|
109
|
+
}
|
|
110
|
+
if (response.redirected || response.url && new URL(response.url).href !== url.href || !response.body) {
|
|
111
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber API response changed its endpoint or omitted the body");
|
|
112
|
+
}
|
|
113
|
+
const reader = response.body.getReader();
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let size = 0;
|
|
116
|
+
try {
|
|
117
|
+
for (;;) {
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
size += value.length;
|
|
122
|
+
if (size > 1024 * 1024) throw exampleError("INVALID_KYBER_RESPONSE", "Kyber API response exceeds 1 MiB");
|
|
123
|
+
chunks.push(value);
|
|
124
|
+
}
|
|
125
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, size)));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await reader.cancel().catch(() => {});
|
|
128
|
+
if (error.name === "LaunchExampleError") throw error;
|
|
129
|
+
throw exampleError("INVALID_KYBER_RESPONSE", "Kyber API response is not bounded valid UTF-8 JSON");
|
|
130
|
+
} finally { reader.releaseLock(); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One fresh build per non-native quote; the API owns route preview, native inputs belong to the escrow.
|
|
134
|
+
* WETH-quote buys are funded through the payable native-wrap escrow path, not Kyber:
|
|
135
|
+
* Kyber's aggregator has no ETH->WETH swap route (it is a wrap), so a WETH quote marks its own
|
|
136
|
+
* conversion `{ nativeWrap: true }` and is consumed by buildExampleBuysAndFunding. */
|
|
137
|
+
export async function buildExampleKyberConversions({ client, fixture, scenario, markets, quotes, escrow, deadline, sdk, block, artifacts, signal, fetchImpl = fetch }) {
|
|
138
|
+
const nativeQuoteAsset = fixture.quoteAsset;
|
|
139
|
+
const kyberQuotes = quotes.filter((quote) => !same(quote.tokenAddress, nativeQuoteAsset));
|
|
140
|
+
if (kyberQuotes.length > 0) await assertExampleConversionAdmission({ client, fixture, escrow, sdk, block, artifacts });
|
|
141
|
+
const requiredNative = BigInt(fixture.buyAmountWei) * BigInt(markets.length * scenario.buysPerMarket);
|
|
142
|
+
const balance = BigInt(await client.request({ method: "eth_getBalance", params: [fixture.creator, block.number] }));
|
|
143
|
+
artifacts.event("native-funding-balance", { balance, requiredNative, blockNumber: block.number, blockHash: block.hash });
|
|
144
|
+
if (balance < requiredNative) throw exampleError("INSUFFICIENT_NATIVE_BALANCE", `Creator needs ${requiredNative} wei for all opening-buy conversions, plus gas; stopped unsigned`, { balance, requiredNative });
|
|
145
|
+
for (const quote of quotes) {
|
|
146
|
+
const buyCount = markets.filter((market) => same(market.quoteAsset, quote.tokenAddress)).length * scenario.buysPerMarket;
|
|
147
|
+
const amountIn = BigInt(fixture.buyAmountWei) * BigInt(buyCount);
|
|
148
|
+
assertFutureLaunchDeadline(deadline);
|
|
149
|
+
if (same(quote.tokenAddress, nativeQuoteAsset)) {
|
|
150
|
+
// Native-wrap funding carries the ETH as msg.value; the escrow wraps it during activation.
|
|
151
|
+
quote.conversion = { nativeWrap: true, nativeInputPerBuy: BigInt(fixture.buyAmountWei), buyCount };
|
|
152
|
+
artifacts.event("native-wrap-funding", { quoteAsset: quote.tokenAddress, buyCount, amountIn: amountIn.toString() });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const response = await requestKyber({ fixture, path: `/api/v1/quote/kyber/build?chainId=${fixture.chainId}`, signal, fetchImpl,
|
|
156
|
+
body: { tokenIn: zeroAddress, tokenOut: quote.tokenAddress, amountIn: amountIn.toString(), sender: escrow, recipient: escrow,
|
|
157
|
+
slippageBps: EXAMPLE_CONVERSION_SLIPPAGE_BPS, deadline: deadline.toString() } });
|
|
158
|
+
validateExampleKyberBuild({ response, quoteAsset: quote.tokenAddress, amountIn, escrow, deadline });
|
|
159
|
+
quote.conversion = { apiOrigin: new URL(fixture.kyberApiUrl).origin, escrow, response, nativeInputPerBuy: BigInt(fixture.buyAmountWei), buyCount,
|
|
160
|
+
slippageBps: EXAMPLE_CONVERSION_SLIPPAGE_BPS, deadline };
|
|
161
|
+
artifacts.event("kyber-funding-built", { quoteAsset: quote.tokenAddress, conversion: quote.conversion });
|
|
162
|
+
}
|
|
163
|
+
const canonical = await client.request({ method: "eth_getBlockByNumber", params: [block.number, false] });
|
|
164
|
+
assert.ok(same(canonical?.hash, block.hash), "Conversion context remains canonical after API reads");
|
|
165
|
+
assertFutureLaunchDeadline(deadline);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function assertExampleConversionBindings({ plan, quotes }) {
|
|
169
|
+
assert.equal(plan.funding.length, quotes.length, "Every funding output is reviewed once");
|
|
170
|
+
for (const quote of quotes) {
|
|
171
|
+
const funding = plan.funding.find((row) => same(row.asset, quote.tokenAddress));
|
|
172
|
+
const conversion = quote.conversion;
|
|
173
|
+
assert.ok(funding && conversion, "Every quote requires the original reviewed funding evidence");
|
|
174
|
+
if (conversion.nativeWrap === true) {
|
|
175
|
+
// Payable native-wrap funding: no Kyber calldata to bind; the payable
|
|
176
|
+
// escrow path is validated by the planner's admission, not router bytes.
|
|
177
|
+
assert.equal(funding.kind, 1, "WETH funding remains NativeWrap, never swap or direct ERC20");
|
|
178
|
+
assert.ok(same(funding.inputAsset, quote.tokenAddress) && funding.target === zeroAddress && funding.data === "0x",
|
|
179
|
+
"Native-wrap funding requires inputAsset == asset and empty target/data (escrow deposit path)");
|
|
180
|
+
assert.equal(funding.inputAmount, conversion.nativeInputPerBuy * BigInt(conversion.buyCount));
|
|
181
|
+
assert.equal(funding.amount, funding.inputAmount, "Native-wrap funding is exact, no slippage conversion");
|
|
182
|
+
const buys = plan.buys.filter((buy) => same(plan.markets[buy.marketIndex].quoteAsset, quote.tokenAddress));
|
|
183
|
+
assert.equal(buys.length, conversion.buyCount);
|
|
184
|
+
assert.ok(buys.every((buy) => same(buy.recipient, plan.creator) && buy.quoteAmountIn > 0n), "Every reviewed buy remains creator-bound and funded");
|
|
185
|
+
assert.equal(buys.reduce((sum, buy) => sum + buy.quoteAmountIn, 0n), funding.inputAmount, "Ordered native inputs exactly consume the wrap budget");
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// The conversion's own swap expiry (not the plan's execution deadline) binds
|
|
189
|
+
// the reviewed build; it must remain within the plan's lifetime.
|
|
190
|
+
assert.ok(conversion.deadline !== undefined && conversion.deadline <= plan.deadline,
|
|
191
|
+
"Conversion swap expiry must not exceed the plan deadline");
|
|
192
|
+
const minimum = validateExampleKyberBuild({ response: conversion.response, quoteAsset: quote.tokenAddress,
|
|
193
|
+
amountIn: uint(conversion.response.amountIn, "Reviewed native conversion input"), escrow: conversion.escrow, deadline: conversion.deadline });
|
|
194
|
+
assert.equal(funding.kind, 2, "Funding remains Swap, never native-wrap or direct ERC20");
|
|
195
|
+
assert.ok(same(funding.inputAsset, zeroAddress) && same(funding.target, conversion.response.transaction.to), "Exact native input and reviewed router");
|
|
196
|
+
assert.equal(funding.inputAmount, BigInt(conversion.response.amountIn));
|
|
197
|
+
assert.equal(funding.amount, minimum); assert.equal(funding.data, conversion.response.transaction.data);
|
|
198
|
+
const buys = plan.buys.filter((buy) => same(plan.markets[buy.marketIndex].quoteAsset, quote.tokenAddress));
|
|
199
|
+
assert.equal(buys.length, conversion.buyCount);
|
|
200
|
+
assert.ok(buys.every((buy) => same(buy.recipient, plan.creator) && buy.quoteAmountIn > 0n), "Every reviewed buy remains creator-bound and funded");
|
|
201
|
+
assert.equal(buys.reduce((sum, buy) => sum + buy.quoteAmountIn, 0n), minimum, "Ordered quote inputs exactly consume the API minimum-output budget");
|
|
202
|
+
assert.equal(funding.inputAmount, conversion.nativeInputPerBuy * BigInt(buys.length), "Exact native ETH input per ordered buy");
|
|
203
|
+
}
|
|
204
|
+
}
|