@ninsei-labs/sdk 0.39.2 → 0.40.0-develop.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/CHANGELOG.md +63 -0
- package/README.md +4 -1
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +26 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/codes.mjs +5 -0
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/index.mjs +16 -2
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +23 -5
- package/src/swaps.mjs +18 -1
package/engine/evm/dex.js
CHANGED
|
@@ -1,84 +1,83 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/dex.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
// DEX
|
|
5
|
+
// THE DEX LEG: a Uniswap v3 quote read from the chain. READ ONLY: eth_call, not one transaction.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// WHAT IS REPLACED HERE. The price breakdown used to hold a mock: "Uniswap v3 USDC/WETH 0.05%" and the spread
|
|
8
|
+
// 0.0004 came from config constants, i.e. the pool fee and the output were INVENTED. Now both are read from the
|
|
9
|
+
// chain itself via QuoterV2 - the same path the page already uses to read price and gas (evm/prices.js): a POST
|
|
10
|
+
// to chain.rpcUrl, no new domains, no new URL parameters and no new external addresses. The host is already
|
|
11
|
+
// allowed in CSP - it comes from the network registry.
|
|
13
12
|
//
|
|
14
|
-
//
|
|
15
|
-
// * selector = keccak256(
|
|
16
|
-
//
|
|
17
|
-
// *
|
|
18
|
-
// (address,address,uint256,uint24,uint160)
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// никто не "починил" код по спецификации и не сломал котировку молча.
|
|
13
|
+
// SELECTORS AND ARGUMENT SHAPE ARE TAKEN FROM A CALL, NOT FROM MEMORY (checked 2026-09-23):
|
|
14
|
+
// * selector = keccak256(signature)[0..4]. Computed by a check from the signature string lying here next to
|
|
15
|
+
// the selector, and the selector's presence is cross-checked in the contract CODE.
|
|
16
|
+
// * THE ARGUMENTS GO AS FIVE CONSECUTIVE WORDS, WITHOUT AN OFFSET WORD. The signature formally takes a struct
|
|
17
|
+
// (address,address,uint256,uint24,uint160) and by the ABI spec the first word should be the offset 0x20. ON
|
|
18
|
+
// THE DEPLOYED QuoterV2 THAT IS FALSE: the offset variant REVERTS (execution reverted), while five consecutive
|
|
19
|
+
// words answer with four (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate). So we encode
|
|
20
|
+
// consecutively - and this is written here so nobody "fixes" the code to the spec and silently breaks the quote.
|
|
23
21
|
//
|
|
24
|
-
// ЧЕГО МОДУЛЬ НЕ ДЕЛАЕТ: не отправляет транзакции, не делает approve, не свопает и НЕ трогает путь
|
|
25
|
-
// фондирования. Оценка газа здесь - это gasEstimate, который вернул сам QuoterV2 (число из цепи), а не
|
|
26
|
-
// eth_estimateGas по чужой транзакции.
|
|
27
22
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
23
|
+
// WHAT THE MODULE DOES NOT DO: it sends no transactions, makes no approve, swaps nothing and does NOT touch the
|
|
24
|
+
// funding path. The gas figure here is the gasEstimate QuoterV2 itself returned (a number from the chain), not
|
|
25
|
+
// eth_estimateGas over someone else's transaction.
|
|
31
26
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
27
|
+
// AN HONEST "NO QUOTE" INSTEAD OF AN INVENTED NUMBER. An empty pool, no pool, a reverted quote and a price that
|
|
28
|
+
// diverged from the reference - FOUR DIFFERENT answers, each named in words (why). A number is returned only
|
|
29
|
+
// when the chain really named it and it passed the reference check.
|
|
30
|
+
//
|
|
31
|
+
// THE ROUTE IS DECLARED HERE, NOT CHOSEN. There is no runtime search for "a direct path or via a hub" and there
|
|
32
|
+
// must not be: a person declared the route in the registry (DEX_ROUTES), and the module only turns it into a path
|
|
33
|
+
// and asks the chain IN ONE CALL. If there is no declared route (or it is empty) - that is an honest "no route",
|
|
34
|
+
// the cause lies in the data (route.why), and such a leg blocks signing rather than looking for a detour.
|
|
35
|
+
//
|
|
36
|
+
// THE PRICE CHECK (priceGateVerdict) - A SECOND GUARD OF THE SAME FAMILY as coverage: a wrong price means the
|
|
37
|
+
// deal will not settle and signing does not start. It is computed two ways, neither needing a feed per token: the
|
|
38
|
+
// implied native price from the quote against the reference (Chainlink) and a "near 1:1" band on steps between
|
|
39
|
+
// stables (with no oracle at all). The thresholds are in the registry (DEX_PRICE_GATE), and the testnet exception
|
|
40
|
+
// is data too (a network's dexPriceGate): there the price is wrong by nature, and a wrong price is marked in
|
|
41
|
+
// words rather than stopping the flow. There is still no liquidity scoring here: pool depth is a person's call.
|
|
42
|
+
//
|
|
43
|
+
|
|
44
|
+
// Selectors. Next to each is the signature string it was computed from: the check recomputes keccak256 over these
|
|
45
|
+
// strings and fails if even one selector diverged from its signature.
|
|
46
|
+
// WHAT IS VERIFIED BY A CALL HERE (2026-09-23, Arbitrum One and Ethereum):
|
|
47
|
+
// * quoteExactInputSingle 0xc6a5026a - a ONE-step quote, five consecutive words with no offset;
|
|
48
|
+
// * quoteExactInput 0xcdca1753 - a WHOLE-PATH quote in one call. Signature quoteExactInput(bytes,uint256), the
|
|
49
|
+
// selector found in the QuoterV2 code on both networks, and a single-step path checked against the single
|
|
50
|
+
// quote gave EXACTLY the same number (36685744820778786 for 100 USDC), so the path encoding (20-byte address
|
|
51
|
+
// + 3-byte fee, consecutively) is confirmed by a number, not from memory. There is no reference value - there
|
|
52
|
+
// is a match of two DIFFERENT calls.
|
|
36
53
|
//
|
|
37
|
-
// ПРОВЕРКА ЦЕНЫ (priceGateVerdict) - ВТОРАЯ ЗАЩИТА ТОЙ ЖЕ СЕМЬИ, что покрытие суммы: неверная цена
|
|
38
|
-
// означает, что сделка не сойдётся, и подпись не начинается. Считается двумя способами, ни один из
|
|
39
|
-
// которых не требует фида на каждый токен: неявная цена натива из котировки против эталона (Chainlink)
|
|
40
|
-
// и полоса "около 1:1" на шагах между стейблами (без оракула вовсе). Пороги - в реестре
|
|
41
|
-
// (DEX_PRICE_GATE), исключение для тестнета - тоже данные (dexPriceGate у сети): там цена негодная по
|
|
42
|
-
// природе, и негодная цена помечается словами, а не останавливает поток. Оценок ликвидности здесь
|
|
43
|
-
// по-прежнему нет: глубину пула решает человек.
|
|
44
|
-
|
|
45
|
-
// Селекторы. Рядом с каждым - строка подписи, из которой он посчитан: проверка
|
|
46
|
-
// (tools/check-uniswap-quote.mjs) пересчитывает keccak256 от этих строк и падает, если хоть один
|
|
47
|
-
// селектор разошёлся с подписью.
|
|
48
|
-
// ЧТО ЗДЕСЬ ПРОВЕРЕНО ВЫЗОВОМ (2026-09-23, Arbitrum One и Ethereum, tools/check-uniswap-quote.mjs):
|
|
49
|
-
// * quoteExactInputSingle 0xc6a5026a - котировка ОДНОЙ ступени, пять слов подряд без смещения;
|
|
50
|
-
// * quoteExactInput 0xcdca1753 - котировка ВСЕГО ПУТИ ОДНИМ ВЫЗОВОМ. Подпись
|
|
51
|
-
// quoteExactInput(bytes,uint256), селектор найден в коде QuoterV2 в обеих сетях, а сверка
|
|
52
|
-
// одноступенчатого пути против одиночной котировки дала РОВНО то же число (36685744820778786 на
|
|
53
|
-
// 100 USDC), то есть кодирование пути (20 байт адрес + 3 байта комиссия, подряд) подтверждено числом,
|
|
54
|
-
// а не по памяти. Эталонного значения нет - есть совпадение двух РАЗНЫХ вызовов.
|
|
55
54
|
export const DEX_METHOD = {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// -
|
|
60
|
-
// -
|
|
61
|
-
//
|
|
62
|
-
//
|
|
55
|
+
// THE EXACT-OUT SHAPE. This is NOT "how much comes out" but "receive exactly this much, spending no more than
|
|
56
|
+
// that": with it the question of a difference in amounts does not exist at all.
|
|
57
|
+
// BOTH shapes are confirmed by a call on Arbitrum One and mainnet:
|
|
58
|
+
// - the single one gives amountIn for exactly 0.035 WETH;
|
|
59
|
+
// - the multi-step on a two-leg path also answers, but ITS PATH IS SET IN REVERSE - from the received token
|
|
60
|
+
// to the paid one (a forward path fails: "Unexpected error"). This is measured, not deduced: getting the
|
|
61
|
+
// direction wrong here costs a call refusal.
|
|
63
62
|
quoteExactOutputSingle: { hex: "0xbd21704a", signature: "quoteExactOutputSingle((address,address,uint256,uint24,uint160))" },
|
|
64
63
|
quoteExactOutput: { hex: "0x2f80bb1d", signature: "quoteExactOutput(bytes,uint256)" },
|
|
65
64
|
quoteExactInputSingle: { hex: "0xc6a5026a", signature: "quoteExactInputSingle((address,address,uint256,uint24,uint160))" },
|
|
66
|
-
//
|
|
67
|
-
//
|
|
65
|
+
// The multi-step shape: the path is passed PACKED (token|fee|token|fee|token), the argument is dynamic, so the
|
|
66
|
+
// offset is 0x40 here (see encodeQuoteExactInput), unlike the single shape.
|
|
68
67
|
quoteExactInput: { hex: "0xcdca1753", signature: "quoteExactInput(bytes,uint256)" },
|
|
69
68
|
getPool: { hex: "0x1698ee82", signature: "getPool(address,address,uint24)" },
|
|
70
69
|
liquidity: { hex: "0x1a686502", signature: "liquidity()" },
|
|
71
70
|
balanceOf: { hex: "0x70a08231", signature: "balanceOf(address)" },
|
|
72
|
-
// withdraw
|
|
73
|
-
//
|
|
71
|
+
// withdraw on WETH9: NOT for sending, only for estimating the unwrap gas (and only if we have a measured WETH
|
|
72
|
+
// holder - see the DEX quote check).
|
|
74
73
|
withdraw: { hex: "0x2e1a7d4d", signature: "withdraw(uint256)" },
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// (u.wrapped),
|
|
80
|
-
//
|
|
81
|
-
//
|
|
76
|
+
// THE PATH FROM THE DECLARED ROUTE (DEX_ROUTES in the registry). There is NO choice here: a person declared the
|
|
77
|
+
// steps with their fees, and we only map symbols to addresses. The wrapped native comes from the network data
|
|
78
|
+
// (u.wrapped), not written as the string "WETH": a BNB network's wrapped native is WBNB.
|
|
79
|
+
// Returns null and a cause if the declared route cannot be assembled (a step names a token this network does not
|
|
80
|
+
// have) - that is NOT "no route" but a data error, and it must be named differently.
|
|
82
81
|
export function declaredRoutePath({ token, tokens = [], wrapped, route }) {
|
|
83
82
|
if (!token || !route || !Array.isArray(route.steps) || !route.steps.length) return null;
|
|
84
83
|
const bySym = new Map((tokens || []).map((t) => [String(t.symbol).toUpperCase(), t]));
|
|
@@ -103,8 +102,8 @@ export function declaredRoutePath({ token, tokens = [], wrapped, route }) {
|
|
|
103
102
|
return { tokens: addresses, symbols, fees, pools };
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
105
|
+
// The path as one string with step fees: "USDe → USDC 0.01% → WETH 0.05%". The same record in the interface and
|
|
106
|
+
// in the report - once they diverge they explain one thing to the person and another to the operator.
|
|
108
107
|
export function routeLabel(route) {
|
|
109
108
|
if (!route || !route.symbols) return "";
|
|
110
109
|
let s = route.symbols[0];
|
|
@@ -112,13 +111,13 @@ export function routeLabel(route) {
|
|
|
112
111
|
return s;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
//
|
|
114
|
+
// Split the path into steps: [{from, to, fee, feeLabel}] - for the step-by-step line in the interface and report.
|
|
116
115
|
export function routeSteps(route) {
|
|
117
116
|
if (!route || !route.symbols) return [];
|
|
118
117
|
return route.fees.map((fee, i) => ({ from: route.symbols[i], to: route.symbols[i + 1], fee, feeLabel: feeTierLabel(fee) }));
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
const CACHE_MS = 20_000; //
|
|
120
|
+
const CACHE_MS = 20_000; // a quote lives 20 s: holding it longer is unsafe (the pool price moves), asking more often is needless
|
|
122
121
|
|
|
123
122
|
const cache = new Map();
|
|
124
123
|
|
|
@@ -135,8 +134,8 @@ export function resetDexCache() {
|
|
|
135
134
|
cache.clear();
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
137
|
+
// One eth_call to the network's node from the registry. Never throws: node silence is a UI state, not a screen
|
|
138
|
+
// crash (the same as in evm/prices.js).
|
|
140
139
|
async function rpc(chain, method, params = [], { timeoutMs = 15_000 } = {}) {
|
|
141
140
|
if (!chain || !chain.rpcUrl) return { ok: false, why: "network has no RPC endpoint" };
|
|
142
141
|
const ctl = typeof AbortController === "function" ? new AbortController() : null;
|
|
@@ -160,14 +159,14 @@ async function rpc(chain, method, params = [], { timeoutMs = 15_000 } = {}) {
|
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
// ---
|
|
162
|
+
// --- ABI encode/decode without libraries (the project has zero dependencies in app/) ----------------------
|
|
164
163
|
const hexBody = (s) => String(s || "").replace(/^0x/, "");
|
|
165
164
|
const word = (v) => hexBody(v).toLowerCase().padStart(64, "0");
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
165
|
+
// AN ADDRESS IS A WHOLE ABI WORD (24 zeros and 20 address bytes), not just 20 bytes. The bug here was exactly the
|
|
166
|
+
// opposite of the obvious one: the address was first padded with zeros to a word, and then the first 24 characters
|
|
167
|
+
// of the word were cut - i.e. the padding was thrown away and calldata carried arguments shifted by 12 bytes. The
|
|
168
|
+
// call reverted ("execution reverted") and it looked like "no liquidity in the pool" although the pool answered -
|
|
169
|
+
// our encoder lied (caught by comparing against a working call).
|
|
171
170
|
const addrWord = (a) => word(a);
|
|
172
171
|
const uintWord = (v) => {
|
|
173
172
|
let h = "";
|
|
@@ -198,16 +197,16 @@ export function encodeGetPool({ tokenA, tokenB, fee }) {
|
|
|
198
197
|
return DEX_METHOD.getPool.hex + addrWord(tokenA) + addrWord(tokenB) + uintWord(fee);
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
200
|
+
// THE INVERSE FOR ONE POOL: how much you must give to get EXACTLY amountOutWei. The layout is the same as the
|
|
201
|
+
// single input shape (the static struct goes INSIDE, with no offset word): address, address, amount, fee, price
|
|
202
|
+
// limit. Confirmed by a call, see DEX_METHOD.quoteExactOutputSingle.
|
|
204
203
|
export function encodeQuoteExactOutputSingle({ tokenIn, tokenOut, amountOutWei, fee, sqrtPriceLimitX96 = 0 }) {
|
|
205
204
|
return DEX_METHOD.quoteExactOutputSingle.hex +
|
|
206
205
|
addrWord(tokenIn) + addrWord(tokenOut) + uintWord(amountOutWei) + uintWord(fee) + uintWord(sqrtPriceLimitX96);
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
//
|
|
210
|
-
//
|
|
208
|
+
// EITHER HOW MUCH COMES OUT, OR WHY NOT - same as the input shape. Here this is NOT a forecast but the answer to
|
|
209
|
+
// "does the pool have enough to give exactly this much": the refusal IS "not enough".
|
|
211
210
|
export async function quoteExactOutSingle(chain, { tokenIn, tokenOut, amountOutWei, fee }, opts = {}) {
|
|
212
211
|
const u = chain && chain.uniswap;
|
|
213
212
|
if (!u || !u.quoter) return { ok: false, why: "no verified QuoterV2 for this network" };
|
|
@@ -224,12 +223,12 @@ export async function quoteExactOutSingle(chain, { tokenIn, tokenOut, amountOutW
|
|
|
224
223
|
};
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
226
|
+
// THE INVERSE OVER THE WHOLE PATH: how much you must give to get EXACTLY amountOutWei.
|
|
227
|
+
// THE PATH GOES IN REVERSE HERE - from the received token to the paid one, with fees in the same reverse order.
|
|
228
|
+
// This is not our convention but the contract's requirement: a forward path fails ("Unexpected error"), a reverse
|
|
229
|
+
// one answers with a number. Measured on both networks, see DEX_METHOD.
|
|
230
|
+
// THE PREFERENCE FOR THIS SHAPE: with it the output minimum is not "roughly equal" to the required amount but
|
|
231
|
+
// exactly it, and the question "what to do with the difference" does not arise at all.
|
|
233
232
|
export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {}) {
|
|
234
233
|
const u = chain && chain.uniswap;
|
|
235
234
|
if (!u || !u.quoter) return { ok: false, why: "no verified QuoterV2 for this network" };
|
|
@@ -237,12 +236,12 @@ export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {
|
|
|
237
236
|
const tokens = route.tokens.slice().reverse();
|
|
238
237
|
const fees = (route.fees || []).slice().reverse();
|
|
239
238
|
const path = encodePath({ tokens, fees });
|
|
240
|
-
//
|
|
239
|
+
// A dynamic argument (bytes) goes with an offset word: 0x40, then the exact output amount, then the path itself.
|
|
241
240
|
const data = DEX_METHOD.quoteExactOutput.hex + uintWord(0x40) + uintWord(amountOutWei) + uintWord(path.bytes) +
|
|
242
241
|
path.packed.padEnd(Math.ceil(path.packed.length / 64) * 64, "0");
|
|
243
242
|
const res = await rpc(chain, "eth_call", [{ to: u.quoter, data }, "latest"], opts);
|
|
244
243
|
if (!res.ok) return { ok: false, why: res.why };
|
|
245
|
-
const out = decodeQuoteExactInput(res.result); //
|
|
244
|
+
const out = decodeQuoteExactInput(res.result); // the answer shape is the same: amount, arrays, gas
|
|
246
245
|
if (!out || out.amountOutWei <= 0n) return { ok: false, why: "pool cannot serve exactly this output" };
|
|
247
246
|
return {
|
|
248
247
|
ok: true,
|
|
@@ -253,10 +252,10 @@ export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {
|
|
|
253
252
|
};
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
255
|
+
// THE PATH FOR A MULTI-STEP QUOTE. A Uniswap v3 path is a PACKED sequence "address(20) + fee(3)" with a trailing
|
|
256
|
+
// address: token0|fee0|token1|fee1|token2. There are no ABI words inside the path: 43 bytes per leg, 66 for two.
|
|
257
|
+
// Confirmed by a number: a single-step path through quoteExactInput returned exactly the same amountOut as
|
|
258
|
+
// quoteExactInputSingle on the same step.
|
|
260
259
|
export function encodePath({ tokens, fees }) {
|
|
261
260
|
const parts = [];
|
|
262
261
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -267,10 +266,10 @@ export function encodePath({ tokens, fees }) {
|
|
|
267
266
|
return { packed, bytes: packed.length / 2 };
|
|
268
267
|
}
|
|
269
268
|
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
269
|
+
// THE CALL quoteExactInput(bytes path, uint256 amountIn) - ONE eth_call for the WHOLE route. Built to the ABI
|
|
270
|
+
// spec because the argument is dynamic here: an offset word (0x40), then amountIn, then the path length and the
|
|
271
|
+
// path itself, padded to a word boundary. HERE, UNLIKE THE SINGLE SHAPE, THE OFFSET IS REALLY NEEDED - the single
|
|
272
|
+
// shape takes a static struct and goes as consecutive words.
|
|
274
273
|
export function encodeQuoteExactInput({ tokens, fees, amountInWei }) {
|
|
275
274
|
const { packed, bytes } = encodePath({ tokens, fees });
|
|
276
275
|
const padded = packed.padEnd(Math.ceil(bytes / 32) * 64, "0");
|
|
@@ -279,11 +278,11 @@ export function encodeQuoteExactInput({ tokens, fees, amountInWei }) {
|
|
|
279
278
|
const wordToAddress = (w) => (w === undefined || w === null ? null : "0x" + w.toString(16).padStart(40, "0"));
|
|
280
279
|
const ZERO_ADDR = "0x" + "0".repeat(40);
|
|
281
280
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
281
|
+
// The step fee in percent: 500 -> "0.05%". This is the ONLY value that used to come from the mock, and now it is
|
|
282
|
+
// computed from the step itself rather than taken as a constant.
|
|
283
|
+
// Arithmetic: fee is measured in hundredths of a basis point, 1,000,000 = 100%. So percent = fee/10,000, i.e.
|
|
284
|
+
// 500 -> 0.05%. It first had ANOTHER division by 100 here, and step 500 was labelled "0.0005%" - the fee was
|
|
285
|
+
// understated a hundredfold (caught by a run, not by eye).
|
|
287
286
|
export function feeTierPct(fee) {
|
|
288
287
|
const n = Number(fee);
|
|
289
288
|
if (!Number.isFinite(n)) return null;
|
|
@@ -296,10 +295,10 @@ export function feeTierLabel(fee) {
|
|
|
296
295
|
}
|
|
297
296
|
|
|
298
297
|
// ---------------------------------------------------------------------------
|
|
299
|
-
//
|
|
298
|
+
// Reads
|
|
300
299
|
// ---------------------------------------------------------------------------
|
|
301
300
|
|
|
302
|
-
//
|
|
301
|
+
// The pool address at the factory. null - no pool (that is an ANSWER, not an error: nothing to change it into).
|
|
303
302
|
export async function poolAddress(chain, { tokenIn, tokenOut, fee }, opts = {}) {
|
|
304
303
|
const u = chain && chain.uniswap;
|
|
305
304
|
if (!u || !u.factory) return { ok: false, why: "no verified Uniswap factory for this network" };
|
|
@@ -311,9 +310,9 @@ export async function poolAddress(chain, { tokenIn, tokenOut, fee }, opts = {})
|
|
|
311
310
|
return { ok: true, pool };
|
|
312
311
|
}
|
|
313
312
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
313
|
+
// Pool state: active liquidity and both-side reserves. Needed for an HONEST "no liquidity" answer: an empty
|
|
314
|
+
// pool quotes zero, and without this read we cannot tell "no pool" from "a pool exists but is empty". Read only
|
|
315
|
+
// on the diagnostic path (when there is no quote or it is implausible).
|
|
317
316
|
export async function poolState(chain, pool, { tokenIn, tokenOut }, opts = {}) {
|
|
318
317
|
if (!pool) return null;
|
|
319
318
|
const [liq, balIn, balOut] = await Promise.all([
|
|
@@ -329,7 +328,7 @@ export async function poolState(chain, pool, { tokenIn, tokenOut }, opts = {}) {
|
|
|
329
328
|
};
|
|
330
329
|
}
|
|
331
330
|
|
|
332
|
-
//
|
|
331
|
+
// ONE QUOTE OF ONE STEP. A reverted quote is a NAMED refusal, not an exception.
|
|
333
332
|
export async function quoteTier(chain, { tokenIn, tokenOut, amountInWei, fee }, opts = {}) {
|
|
334
333
|
const u = chain && chain.uniswap;
|
|
335
334
|
if (!u || !u.quoter) return { fee, ok: false, why: "no verified QuoterV2 for this network" };
|
|
@@ -350,10 +349,10 @@ export async function quoteTier(chain, { tokenIn, tokenOut, amountInWei, fee },
|
|
|
350
349
|
};
|
|
351
350
|
}
|
|
352
351
|
|
|
353
|
-
//
|
|
354
|
-
// uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate) -
|
|
355
|
-
//
|
|
356
|
-
//
|
|
352
|
+
// Parsing the quoteExactInput answer. The answer DIFFERS from the single shape: (uint256 amountOut,
|
|
353
|
+
// uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate) - i.e. one element
|
|
354
|
+
// per path step in two of the arrays. Offsets are read from the answer itself, not assumed: the array lengths
|
|
355
|
+
// equal the number of path legs, and that can be CHECKED, not trusted.
|
|
357
356
|
export function decodeQuoteExactInput(hex) {
|
|
358
357
|
const words = decodeWords(hex);
|
|
359
358
|
if (words.length < 4) return null;
|
|
@@ -373,8 +372,8 @@ export function decodeQuoteExactInput(hex) {
|
|
|
373
372
|
};
|
|
374
373
|
}
|
|
375
374
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
375
|
+
// A WHOLE-PATH QUOTE IN ONE CALL. There is NO chain of "step 1 quote, then step 2 from its output": such a chain
|
|
376
|
+
// prices both steps against the same pool state and gives a optimistically wrong number.
|
|
378
377
|
export async function quotePath(chain, { route, amountInWei }, opts = {}) {
|
|
379
378
|
const u = chain && chain.uniswap;
|
|
380
379
|
if (!u || !u.quoter) return { ...route, ok: false, why: "no verified QuoterV2 for this network" };
|
|
@@ -392,42 +391,42 @@ export async function quotePath(chain, { route, amountInWei }, opts = {}) {
|
|
|
392
391
|
sqrtPriceX96After: out.sqrtPriceX96After,
|
|
393
392
|
initializedTicksCrossed: out.initializedTicksCrossed,
|
|
394
393
|
hops: route.fees.length,
|
|
395
|
-
//
|
|
396
|
-
//
|
|
394
|
+
// THE ANSWER STEP COUNT IS CHECKED AGAINST THE PATH LEG COUNT: if the contract returned not one element per
|
|
395
|
+
// step, this is not an answer we can read, and it is better to say so in words than to show a number at random.
|
|
397
396
|
shapeOk: out.sqrtPriceX96After.length === route.fees.length,
|
|
398
397
|
};
|
|
399
398
|
}
|
|
400
399
|
|
|
401
|
-
//
|
|
402
|
-
//
|
|
400
|
+
// THE DECLARED-ROUTE SIGNATURE: one string showing WHAT it is assembled from. Needed for the cache key: if the
|
|
401
|
+
// declaration changed (another fee, another hub), the old answer does not fit the new question.
|
|
403
402
|
export function routeSignature(route) {
|
|
404
403
|
if (!route || !Array.isArray(route.steps) || !route.steps.length) return route && route.unwrap ? "unwrap" : "none";
|
|
405
404
|
return route.steps.map((s) => String(s.to).toUpperCase() + ":" + Number(s.fee) + (s.pool ? ":" + String(s.pool).toLowerCase() : "")).join("+");
|
|
406
405
|
}
|
|
407
406
|
|
|
408
407
|
// ---------------------------------------------------------------------------
|
|
409
|
-
//
|
|
408
|
+
// THE PRICE CHECK: IS THE DEAL WRONG OR NOT.
|
|
410
409
|
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
410
|
+
// This is NOT liquidity scoring - that is still a person's job, and there are no verdicts about pool depth here.
|
|
411
|
+
// It is a check of the same kind as amount coverage: if the price is wrong the deal will not settle and must not
|
|
412
|
+
// start. A sign of a wrong price is taken TWO ways, and neither needs a feed per token (else USDe/USDS/PyUSD
|
|
413
|
+
// without their own feed would break the check entirely):
|
|
415
414
|
//
|
|
416
|
-
// 1)
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// 2)
|
|
421
|
-
//
|
|
422
|
-
//
|
|
415
|
+
// 1) THE LAST STEP INTO NATIVE. The implied dollar price of native is derived from the quote (dollars in /
|
|
416
|
+
// native out) and compared against the native price reference - the same Chainlink the page already reads
|
|
417
|
+
// (evm/prices.js). This works for any input: a stable's dollar price is known from the registry, and the
|
|
418
|
+
// rest end at native anyway;
|
|
419
|
+
// 2) A STEP BETWEEN TWO STABLES (USDe→USDC, USDS→USDC, PyUSD→USDC): the price must be near 1:1, and that is
|
|
420
|
+
// checked BY THE QUOTE ITSELF, with no oracle at all. An absurdity like "100 USDe give 0.5 USDC" is caught
|
|
421
|
+
// exactly here.
|
|
423
422
|
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
423
|
+
// THE THRESHOLDS COME IN AS DATA (the registry: DEX_PRICE_GATE), not as numbers here: changing them is a data edit.
|
|
424
|
+
// A NETWORK MARK tolerated (registry: dexPriceGate === "tolerated") CANCELS THE BLOCK BUT NOT THE FACT: the price
|
|
425
|
+
// stays wrong and is named so (bad), and the interface must show it. Thus a testnet does not stall on a price we
|
|
426
|
+
// did not choose, and does not pretend the price is market.
|
|
428
427
|
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
428
|
+
// The function is PURE: not one network call, only the leg numbers and the reference. So it can be run on any
|
|
429
|
+
// numbers without a network - which is how the guard is proven to go red (the DEX gate check).
|
|
431
430
|
// ---------------------------------------------------------------------------
|
|
432
431
|
export function priceGateVerdict({
|
|
433
432
|
amountInWei = null, amountOutWei = null, inDecimals = null, outDecimals = null,
|
|
@@ -437,7 +436,7 @@ export function priceGateVerdict({
|
|
|
437
436
|
const band = gates && Number.isFinite(Number(gates.stableBandPct)) && Number(gates.stableBandPct) >= 0 ? Number(gates.stableBandPct) : null;
|
|
438
437
|
const why = [];
|
|
439
438
|
|
|
440
|
-
// (1)
|
|
439
|
+
// (1) STEPS BETWEEN STABLES: near 1:1, no oracle.
|
|
441
440
|
const stable = (Array.isArray(stableSteps) ? stableSteps : []).map((s) => {
|
|
442
441
|
const row = {
|
|
443
442
|
from: s.from, to: s.to, fee: s.fee, feeLabel: feeTierLabel(s.fee),
|
|
@@ -445,8 +444,8 @@ export function priceGateVerdict({
|
|
|
445
444
|
quoted: Boolean(s.ok), ok: false, ratio: null, deviationPct: null, why: null,
|
|
446
445
|
};
|
|
447
446
|
if (!s.ok) {
|
|
448
|
-
//
|
|
449
|
-
//
|
|
447
|
+
// A STABLE-STABLE STEP DID NOT ANSWER - AND THAT IS NAMED, NOT SKIPPED: an unmeasured price does not pass
|
|
448
|
+
// the check, else "we did not check" would look like "the price is fine".
|
|
450
449
|
row.why = "the 1:1 step could not be quoted (" + (s.why || "no answer") + ")";
|
|
451
450
|
why.push(row.why);
|
|
452
451
|
return row;
|
|
@@ -470,7 +469,7 @@ export function priceGateVerdict({
|
|
|
470
469
|
return row;
|
|
471
470
|
});
|
|
472
471
|
|
|
473
|
-
// (2)
|
|
472
|
+
// (2) THE IMPLIED NATIVE PRICE AGAINST THE REFERENCE.
|
|
474
473
|
const inUnits = Number.isFinite(Number(inDecimals)) && Number(amountInWei) > 0 ? Number(amountInWei) / 10 ** Number(inDecimals) : null;
|
|
475
474
|
const outUnits = Number.isFinite(Number(outDecimals)) && Number(amountOutWei) > 0 ? Number(amountOutWei) / 10 ** Number(outDecimals) : null;
|
|
476
475
|
const valueUsd = inUnits !== null && Number(tokenUsd) > 0 ? inUnits * Number(tokenUsd) : null;
|
|
@@ -481,7 +480,7 @@ export function priceGateVerdict({
|
|
|
481
480
|
if (nativeBad) {
|
|
482
481
|
why.push(`the route implies the native coin at $${impliedUsd.toFixed(2)} while the reference price is $${ref.toFixed(2)} - ${deviationPct.toFixed(1)}% apart, and the limit is ${maxDev}%: this is not the market price`);
|
|
483
482
|
} else if (deviationPct === null) {
|
|
484
|
-
//
|
|
483
|
+
// NOT CHECKED IS NOT "ALL GOOD". We name the cause that made the check impossible.
|
|
485
484
|
why.push(ref === null
|
|
486
485
|
? "the reference price of the native coin is not known, so the price cannot be checked"
|
|
487
486
|
: "the price of the token being paid is not known, so the price cannot be checked");
|
|
@@ -489,14 +488,14 @@ export function priceGateVerdict({
|
|
|
489
488
|
why.push("the price limit is not declared for this route, so the price cannot be checked");
|
|
490
489
|
}
|
|
491
490
|
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
491
|
+
// THE CHECK TOOK PLACE - so the NATIVE PART did (a reference exists, a limit is declared). Stable-stable steps
|
|
492
|
+
// do not enter here on purpose: an unmeasured or out-of-band step is already bad (see above), not "unchecked".
|
|
493
|
+
//
|
|
495
494
|
const checked = deviationPct !== null && maxDev !== null;
|
|
496
495
|
const bad = nativeBad || stable.some((r) => !r.ok);
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
496
|
+
// DOES IT BLOCK: a wrong price - yes (except a network marked tolerated); an unchecked price - also yes, by the
|
|
497
|
+
// same logic that blocks an unknown required amount: "we did not check" must not be shown as "checked and fine".
|
|
498
|
+
// On a testnet neither blocks.
|
|
500
499
|
const blocks = tolerated ? false : (bad || !checked);
|
|
501
500
|
return {
|
|
502
501
|
checked, bad, tolerated: Boolean(tolerated), blocks,
|
|
@@ -509,68 +508,68 @@ export function priceGateVerdict({
|
|
|
509
508
|
}
|
|
510
509
|
|
|
511
510
|
// ---------------------------------------------------------------------------
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
511
|
+
// WHY THERE IS NO ROUTE - IN WORDS, AND THOSE WORDS COME FROM THE DATA. The cause lies in the declaration itself
|
|
512
|
+
// (route.why): it was written by the person declaring the route, and it is confirmed by measurement. The same
|
|
513
|
+
// wording in the interface and in the report: once they diverge they explain one thing to the person and another
|
|
514
|
+
// to the operator.
|
|
515
|
+
// A route is NOT in the registry at all (the token is there, no declaration) - that is ALSO an honest answer, not
|
|
516
|
+
// "not found": the detour was not declared, and signing on it does not start.
|
|
518
517
|
export function noRouteWords({ paySymbol, wrappedSymbol, chainName, route = null }) {
|
|
519
|
-
//
|
|
520
|
-
//
|
|
518
|
+
// A TEMPLATE, NOT STRING GLUING: the text is the same, but the substring `from "` no longer appears in the source
|
|
519
|
+
// (the gluing was accidentally read by the SDK bare-import check as a specifier). Same rule, same words.
|
|
521
520
|
const head = `no route from ${paySymbol} to ${wrappedSymbol} on ${chainName}`;
|
|
522
521
|
const why = route && route.why ? String(route.why) : null;
|
|
523
|
-
//
|
|
524
|
-
//
|
|
522
|
+
// THE VISIBLE TEXT HAS NEITHER "registry" NOR "DEX_ROUTES": those are names of OUR machinery, not what a person
|
|
523
|
+
// needs to understand. The meaning is the same: a route is not found by brute force, it is declared by a person.
|
|
525
524
|
return why ? head + ": " + why : head + ": no route for " + paySymbol + " is declared - routes are declared by hand, not searched";
|
|
526
525
|
}
|
|
527
526
|
|
|
528
527
|
|
|
529
528
|
// ---------------------------------------------------------------------------
|
|
530
|
-
//
|
|
531
|
-
// *
|
|
532
|
-
// *
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
529
|
+
// THE LEG VERDICT: MAY ONE SIGN ON IT. Two guards OF ONE FAMILY, both BLOCKING signing:
|
|
530
|
+
// * AMOUNT COVERAGE - the route must give at least what the order requires (the escrow amount);
|
|
531
|
+
// * PRICE - the route price must be market (priceGateVerdict): a wrong price means the same as a shortfall -
|
|
532
|
+
// the deal will not settle.
|
|
533
|
+
// It differs from observations exactly in this: "does not cover" and "the price is wrong" are FACTS just like "no
|
|
534
|
+
// pool", and they must stop the person BEFORE signing, not be shown as a line in the price breakdown. Hence the
|
|
535
|
+
// rule: the function returns either { blocked: false } or { blocked: true, kind, reason } - and the caller MUST
|
|
536
|
+
// honour blocked.
|
|
537
|
+
// There are no liquidity verdicts here: "covers 0.4% short" is a number, not a judgement of pool quality.
|
|
538
|
+
//
|
|
539
|
+
// kind: "unquoted" - there is no quote yet (nothing to ask, no waiting allowed: block)
|
|
540
|
+
// "no-route" - no route: causes lie in leg.why (the registry declaration + the chain refusal)
|
|
541
|
+
// "price-unchecked" - the price cannot be checked (no native price reference): block - "we did not
|
|
542
|
+
// check" must not be shown as "checked and fine"
|
|
543
|
+
// "bad-price" - the price is not market: numbers in verdict.price (blocks on a live network)
|
|
544
|
+
// "unchecked" - the required amount is unknown (no XMR/native rate to compute it)
|
|
545
|
+
// "short" - a route exists but the output is LESS than required: here is by how much
|
|
546
|
+
// "bad-price-tolerated" - the price is wrong but the network is marked dexPriceGate: signing is NOT
|
|
547
|
+
// blocked, and the wrongness is NAMED (a testnet: testing the flow, not the market)
|
|
548
|
+
// "covers" - all agrees: signing allowed (the page's other guards stay in force)
|
|
539
549
|
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
// "price-unchecked" - цену проверить НЕЧЕМ (нет эталона цены натива): блокируем - "не
|
|
543
|
-
// проверили" нельзя показывать как "проверили и всё хорошо"
|
|
544
|
-
// "bad-price" - цена не рыночная: числа в verdict.price (блокирует на боевой сети)
|
|
545
|
-
// "unchecked" - требуемая сумма неизвестна (нет курса XMR/<натив>, чтобы её посчитать)
|
|
546
|
-
// "short" - маршрут есть, но выход МЕНЬШЕ требуемой суммы: вот насколько
|
|
547
|
-
// "bad-price-tolerated" - цена негодная, но у сети признак dexPriceGate: подпись НЕ блокируется,
|
|
548
|
-
// а негодность НАЗВАНА (тестнет: проверяем поток, а не рынок)
|
|
549
|
-
// "covers" - всё сходится: подпись разрешена (прочие защиты страницы в силе)
|
|
550
|
+
// COVERAGE NUMBERS ARE RETURNED WHENEVER THEY ARE KNOWN (needWei/getsWei/shortfallWei/covers) - even when signing
|
|
551
|
+
// is locked by price: the person must see WHAT the verdict came from, not only the word "blocked".
|
|
550
552
|
//
|
|
551
|
-
// ЧИСЛА ПОКРЫТИЯ ВОЗВРАЩАЮТСЯ ВСЕГДА, КОГДА ОНИ ИЗВЕСТНЫ (needWei/getsWei/shortfallWei/covers) - даже
|
|
552
|
-
// когда подпись заперта ценой: человеку должно быть видно, ИЗ ЧЕГО взялся вердикт, а не только слово
|
|
553
|
-
// "заблокировано".
|
|
554
553
|
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
554
|
+
// THE OUTPUT MINIMUM OF THE FUTURE SWAP COMES FROM HERE TOO - from the required escrow amount, not "by eye". So a
|
|
555
|
+
// shortfall is impossible by construction and any surplus stays with the user: there is NO automatic conversion of
|
|
556
|
+
// the remainder in the code and there must not be - funding deposits exactly the order amount, everything else
|
|
557
|
+
// stays with the person.
|
|
559
558
|
export function dexLegVerdict(leg, requiredOutWei = null) {
|
|
560
559
|
if (leg && leg.unwrap) return { blocked: false, kind: "unwrap", reason: null, price: null };
|
|
561
560
|
if (!leg) return { blocked: true, kind: "unquoted", reason: "the swap leg has not been quoted yet", price: null };
|
|
562
561
|
if (!leg.ok) return { blocked: true, kind: "no-route", reason: leg.why || "no route", price: null };
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
562
|
+
// THE PRICE COMES FROM THE LEG ITSELF: quoteDexOut computes it from the same chain answer as the output. A leg
|
|
563
|
+
// without a price check (someone assembled it by hand) does not confirm this guard - there is nothing here to
|
|
564
|
+
// check it with, so such a leg has no price verdict either.
|
|
566
565
|
const price = leg.price || null;
|
|
567
566
|
const priceBad = Boolean(price && price.bad);
|
|
568
567
|
const priceUnchecked = Boolean(price && price.checked === false);
|
|
569
568
|
const priceBlocks = Boolean(price && price.blocks && (priceBad || priceUnchecked));
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
569
|
+
// THE REQUIRED AMOUNT COMES IN AS AN ARGUMENT, NOT TAKEN FROM THE LEG ITSELF: the quote lives in a 20 s cache,
|
|
570
|
+
// while the person can change the order size at any moment. If we took the required amount from the cached
|
|
571
|
+
// answer, the verdict would lag behind what the person sees in the form - exactly the case where a guard "exists
|
|
572
|
+
// but does not fire".
|
|
574
573
|
const need = requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0 ? BigInt(requiredOutWei) : null;
|
|
575
574
|
const gets = BigInt(leg.amountOutWei);
|
|
576
575
|
const coverage = need === null ? null : {
|
|
@@ -584,9 +583,9 @@ export function dexLegVerdict(leg, requiredOutWei = null) {
|
|
|
584
583
|
decimals: leg.wrapped ? leg.wrapped.decimals : null,
|
|
585
584
|
wrappedSymbol: leg.wrapped ? leg.wrapped.symbol : null,
|
|
586
585
|
};
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
586
|
+
// ORDER OF CAUSES: first "signing is impossible at all" (no route - above; price unchecked; price wrong), then
|
|
587
|
+
// "the amount is short", then "all agrees". It matters: with a wrong price, the coverage numbers can no longer
|
|
588
|
+
// be believed.
|
|
590
589
|
if (priceBlocks) return { blocked: true, ...base, kind: priceBad ? "bad-price" : "price-unchecked", reason: price.why };
|
|
591
590
|
if (coverage === null) {
|
|
592
591
|
return { blocked: true, ...base, kind: "unchecked", reason: "the required escrow amount is not known yet, so coverage cannot be checked" };
|
|
@@ -597,27 +596,27 @@ export function dexLegVerdict(leg, requiredOutWei = null) {
|
|
|
597
596
|
}
|
|
598
597
|
|
|
599
598
|
// ---------------------------------------------------------------------------
|
|
600
|
-
//
|
|
599
|
+
// The main entry for the page: how much native the payment with this token gives ALONG THE DECLARED ROUTE.
|
|
601
600
|
//
|
|
602
|
-
// token -
|
|
603
|
-
// amountWei -
|
|
604
|
-
// requiredOutWei -
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
// route -
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
// tokens -
|
|
612
|
-
// referenceNativeUsd -
|
|
613
|
-
//
|
|
614
|
-
// gates -
|
|
601
|
+
// token - the payment token from the registry (address, decimals and price)
|
|
602
|
+
// amountWei - the payment amount in its smallest units
|
|
603
|
+
// requiredOutWei - HOW MUCH NATIVE THE ORDER REQUIRES (the escrow amount for this input size). This is NOT
|
|
604
|
+
// an observation but a feasibility condition: from it the "covers / does not cover" verdict
|
|
605
|
+
// is computed, and "does not cover" blocks signing (see dexLegVerdict).
|
|
606
|
+
// route - the DECLARED route from the registry (DEX_ROUTES[chainId][symbol]). There is no choice
|
|
607
|
+
// here and there must not be: what is declared is what is quoted, in ONE call. No
|
|
608
|
+
// declaration, or an empty one - an honest "no route" (the words come from route.why), and
|
|
609
|
+
// signing on it does not start.
|
|
610
|
+
// tokens - this network's tokens: the address and decimals are taken by the symbol from the step
|
|
611
|
+
// referenceNativeUsd - the REFERENCE native price in dollars (the page's Chainlink). Without it the price check
|
|
612
|
+
// cannot take place, and that is no reason to skip it (see priceGateVerdict).
|
|
613
|
+
// gates - the price-check thresholds from the registry (DEX_PRICE_GATE)
|
|
615
614
|
//
|
|
616
|
-
//
|
|
615
|
+
// THERE ARE NO HUBS HERE ANY MORE: a detour via USDC/USDT is also a declared route, not a runtime search.
|
|
617
616
|
//
|
|
618
|
-
//
|
|
619
|
-
// "
|
|
620
|
-
//
|
|
617
|
+
// Returns EITHER { ok: true, ... } with numbers from the chain, OR { ok: false, why } in words. Numbers and "why
|
|
618
|
+
// not" are not returned together: showing a person both means asking him to guess which of the two answers to
|
|
619
|
+
// believe.
|
|
621
620
|
// ---------------------------------------------------------------------------
|
|
622
621
|
export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = null, route = null, tokens = [], referenceNativeUsd = null, gates = null } = {}, opts = {}) {
|
|
623
622
|
const u = chain && chain.uniswap;
|
|
@@ -628,29 +627,29 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
628
627
|
const wrapped = u.wrapped;
|
|
629
628
|
const chainName = (chain && chain.name) || (chain && chain.id) || "this network";
|
|
630
629
|
if (String(token.address).toLowerCase() === String(wrapped.address).toLowerCase()) {
|
|
631
|
-
//
|
|
630
|
+
// The wrapped native is changed to native NOT in a pool: this is a WETH9 withdraw, one to one.
|
|
632
631
|
return { ok: false, why: "this is the wrapped native coin: no pool needed, it unwraps 1:1", unwrap: true };
|
|
633
632
|
}
|
|
634
633
|
const declared = route || null;
|
|
635
|
-
//
|
|
636
|
-
//
|
|
634
|
+
// THE NETWORK MARK: a wrong price on a testnet does not block (registry: the network's dexPriceGate). A missing
|
|
635
|
+
// mark blocks, and that is the default: a silent "it will do" on a live network is unacceptable.
|
|
637
636
|
const tolerated = String((chain && chain.dexPriceGate) || "") === "tolerated";
|
|
638
|
-
//
|
|
639
|
-
//
|
|
637
|
+
// THE DECLARATION WE USE IS RETURNED WHOLE (with steps, pools and a measurement date): the interface and the
|
|
638
|
+
// report must name not only WHAT came out but WHAT it was declared from.
|
|
640
639
|
const declaration = declared
|
|
641
640
|
? {
|
|
642
641
|
steps: (Array.isArray(declared.steps) ? declared.steps : []).map((s) => ({ to: s.to, fee: s.fee, pool: s.pool || null, verified: s.verified || null })),
|
|
643
642
|
why: declared.why || null, unwrap: Boolean(declared.unwrap),
|
|
644
643
|
}
|
|
645
644
|
: null;
|
|
646
|
-
// (1)
|
|
647
|
-
//
|
|
645
|
+
// (1) NO ROUTE IS AN ANSWER FROM THE DATA, NOT AN ERROR. Empty steps means "no declared route": why there is
|
|
646
|
+
// none is in route.why, and those words go to both the interface and the report (the same ones).
|
|
648
647
|
if (!declared || !Array.isArray(declared.steps) || !declared.steps.length) {
|
|
649
648
|
if (declared && declared.unwrap) return { ok: false, why: "this is the wrapped native coin: no pool needed, it unwraps 1:1", unwrap: true };
|
|
650
649
|
return { ok: false, why: noRouteWords({ paySymbol: token.symbol, wrappedSymbol: wrapped.symbol, chainName, route: declared }), declared: true, declaration, steps: [] };
|
|
651
650
|
}
|
|
652
|
-
// (2)
|
|
653
|
-
//
|
|
651
|
+
// (2) A PATH FROM THE DECLARED STEPS. Failing to assemble the declaration (a step names a token this network
|
|
652
|
+
// lacks) is a DATA error and must be named differently from "no route": it shows the registry needs an edit.
|
|
654
653
|
const path = declaredRoutePath({ token, tokens, wrapped, route: declared });
|
|
655
654
|
if (!path || path.error) {
|
|
656
655
|
return {
|
|
@@ -665,14 +664,14 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
665
664
|
const h = byAddr.get(String(addr).toLowerCase());
|
|
666
665
|
return h && Number.isFinite(Number(h.decimals)) ? Number(h.decimals) : null;
|
|
667
666
|
};
|
|
668
|
-
// (3)
|
|
669
|
-
//
|
|
670
|
-
//
|
|
667
|
+
// (3) A 20 s CACHE BY NETWORK, TOKEN, AMOUNT AND DECLARATION SIGNATURE. Changing the declaration asks a
|
|
668
|
+
// DIFFERENT route, and the old answer does not fit the new question. The required amount does NOT enter the
|
|
669
|
+
// key: it changes with the order size, while coverage and price are computed below - every time, from fresh numbers.
|
|
671
670
|
const key = [chain.id, token.symbol, amount.toString(), routeSignature(declared)].join("|");
|
|
672
671
|
let base = cached(key);
|
|
673
672
|
if (base === undefined) {
|
|
674
|
-
//
|
|
675
|
-
//
|
|
673
|
+
// ONE CALL FOR THE WHOLE PATH. There is no chain of "step 1 quote, then step 2 from its output" here and
|
|
674
|
+
// there must not be: such a chain prices both steps against one pool state and gives an optimistically wrong number.
|
|
676
675
|
const quote = await quotePath(chain, { route: { tokens: path.tokens, fees: path.fees, symbols: path.symbols, via: null }, amountInWei: amount }, opts);
|
|
677
676
|
if (!quote.ok) return put(key, { ok: false, why: quote.why, declared: true });
|
|
678
677
|
base = { ok: true, quote };
|
|
@@ -680,10 +679,10 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
680
679
|
}
|
|
681
680
|
if (!base.ok) return { ...base, declaration };
|
|
682
681
|
const q = base.quote;
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
682
|
+
// ROUTE POOLS ARE READ WHENEVER A QUOTE EXISTS - one record per leg. This is a FACT the person needs next to
|
|
683
|
+
// the number: "the route answered so much" by itself does not say how much lies in the pools on his path.
|
|
684
|
+
// Reserves in wei WITHOUT decimals are unreadable to a person, so decimals ride alongside - substituting 18
|
|
685
|
+
// "by similarity" would mean one day showing a 6-decimal token as 18-decimal.
|
|
687
686
|
if (base.hops === undefined) {
|
|
688
687
|
const hops = [];
|
|
689
688
|
for (let i = 0; i < path.fees.length; i++) {
|
|
@@ -697,24 +696,24 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
697
696
|
step: i + 1, from: path.symbols[i], to: path.symbols[i + 1], fee, feeLabel: feeTierLabel(fee),
|
|
698
697
|
decimals: [dec(from), dec(to)],
|
|
699
698
|
pool,
|
|
700
|
-
//
|
|
701
|
-
//
|
|
699
|
+
// CHECKING THE DECLARED POOL ADDRESS AGAINST THE CHAIN. If the factory answers another pool, the
|
|
700
|
+
// declaration is stale: in the tool's report that is a failure, here a fact next to the number (a person decides).
|
|
702
701
|
declaredPool,
|
|
703
702
|
poolMatchesDeclared: pool && declaredPool ? String(pool).toLowerCase() === String(declaredPool).toLowerCase() : null,
|
|
704
703
|
verified: declaredStep.verified || null,
|
|
705
|
-
//
|
|
706
|
-
// balanceOf(
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
704
|
+
// POOL STATE IS NO LONGER READ HERE. Three eth_call calls per leg (liquidity(), balanceOf(tokenIn),
|
|
705
|
+
// balanceOf(tokenOut)) were gathered for one line in the interface - liquidity, pool reserves - and that
|
|
706
|
+
// line is removed: after a link to the pool a person checks depth and state in the explorer. The poolState
|
|
707
|
+
// function itself stays: the tool's report calls it, and the operator needs those numbers there. Here they
|
|
708
|
+
// are needed by nobody, while the page asks for them on every refresh.
|
|
710
709
|
});
|
|
711
710
|
}
|
|
712
711
|
base.hops = hops;
|
|
713
712
|
}
|
|
714
713
|
const hops = base.hops;
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
714
|
+
// STEPS BETWEEN TWO STABLES ARE CHECKED BY THE QUOTE ITSELF (near 1:1, with no oracle at all). The step amount
|
|
715
|
+
// is the route amount: in declared routes such a step comes FIRST, so for it this is exactly its amount (and
|
|
716
|
+
// that is marked by the exact field on every row).
|
|
718
717
|
if (base.stableSteps === undefined) {
|
|
719
718
|
const stables = (gates && Array.isArray(gates.stables) ? gates.stables : []).map((s) => String(s).toUpperCase());
|
|
720
719
|
const steps = [];
|
|
@@ -731,9 +730,9 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
731
730
|
}
|
|
732
731
|
base.stableSteps = steps;
|
|
733
732
|
}
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
733
|
+
// THE PRICE CHECK. Computed EVERY TIME, not cached with the quote: the native price reference is loaded
|
|
734
|
+
// separately and may appear later than the pool answer - exactly the same reason the coverage verdict is not
|
|
735
|
+
// cached here either.
|
|
737
736
|
const price = priceGateVerdict({
|
|
738
737
|
amountInWei: amount, amountOutWei: q.amountOutWei,
|
|
739
738
|
inDecimals: token.decimals, outDecimals: wrapped.decimals,
|
|
@@ -741,19 +740,19 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
741
740
|
referenceUsd: referenceNativeUsd,
|
|
742
741
|
stableSteps: base.stableSteps, tolerated, gates,
|
|
743
742
|
});
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
743
|
+
// THE PREFERRED SHAPE: "receive EXACTLY the required amount, spending no more than entered". It is asked on the
|
|
744
|
+
// same winning path but IN REVERSE - the contract requires exactly that. The answer matters not only as a
|
|
745
|
+
// number: it removes the question of a difference in amounts, because the output minimum becomes equal to the
|
|
746
|
+
// required amount by construction. A refusal here is also an answer: the pool does not give exactly that much,
|
|
747
|
+
// and then the usual input shape remains, with a minimum from the required amount.
|
|
749
748
|
let exactOut = null;
|
|
750
749
|
if (requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0) {
|
|
751
750
|
const eo = await quoteExactOutPath(chain, { route: q, amountOutWei: BigInt(requiredOutWei) }, opts);
|
|
752
751
|
exactOut = eo.ok
|
|
753
752
|
? {
|
|
754
753
|
ok: true, amountInWei: eo.amountInWei, gasEstimate: eo.gasEstimate,
|
|
755
|
-
//
|
|
756
|
-
// "
|
|
754
|
+
// IS THE ENTERED AMOUNT ENOUGH: the input amount is compared, because with this shape the answer is
|
|
755
|
+
// exactly "how much to give". Any surplus stays with the user: no conversion of the remainder in the code.
|
|
757
756
|
enough: BigInt(eo.amountInWei) <= amount,
|
|
758
757
|
enteredWei: amount.toString(),
|
|
759
758
|
spareInWei: (BigInt(eo.amountInWei) <= amount ? amount - BigInt(eo.amountInWei) : 0n).toString(),
|
|
@@ -761,9 +760,9 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
761
760
|
}
|
|
762
761
|
: { ok: false, why: eo.why };
|
|
763
762
|
}
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
763
|
+
// COVERAGE IS COMPUTED HERE, NOT TAKEN FROM THE CACHE: the pool and input amount are cached for 20 s, but the
|
|
764
|
+
// required amount comes from the order and may change (the person adjusted the XMR size) - and then coverage
|
|
765
|
+
// must be recomputed, not left stale. Computed in whole wei.
|
|
767
766
|
const required = requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0
|
|
768
767
|
? (() => {
|
|
769
768
|
const need = BigInt(requiredOutWei);
|
|
@@ -782,10 +781,10 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
782
781
|
venue: "Uniswap v3",
|
|
783
782
|
payToken: token.symbol,
|
|
784
783
|
wrapped,
|
|
785
|
-
//
|
|
786
|
-
//
|
|
784
|
+
// THE DECLARED ROUTE THE PRICE WAS ASKED ON: the steps, their pools and the measurement date - whole, as in the registry.
|
|
785
|
+
// The interface must show not only WHAT came out but WHAT it was declared from.
|
|
787
786
|
declaredRoute: declaration,
|
|
788
|
-
//
|
|
787
|
+
// THE PATH IS THE STEPS WITH FEES, not one step: a declared detour via a stable has two.
|
|
789
788
|
route: { symbols: q.symbols.slice(), fees: q.fees.slice(), pools: path.pools.slice(), via: null, hops: q.fees.length },
|
|
790
789
|
routeLabel: routeLabel(q),
|
|
791
790
|
steps: routeSteps(q),
|
|
@@ -794,31 +793,31 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
|
|
|
794
793
|
amountInWei: amount.toString(),
|
|
795
794
|
amountOutWei: q.amountOutWei,
|
|
796
795
|
gasEstimate: q.gasEstimate,
|
|
797
|
-
//
|
|
798
|
-
//
|
|
796
|
+
// THE FEASIBILITY VERDICT: whether the route covers the required order amount. null means "the required amount
|
|
797
|
+
// was not passed" - and then signing is blocked as unverified (see dexLegVerdict), not allowed.
|
|
799
798
|
required,
|
|
800
|
-
//
|
|
799
|
+
// THE PRICE CHECK: whether it was done, whether the price looks market, and whether it blocks signing on this network.
|
|
801
800
|
price,
|
|
802
|
-
//
|
|
803
|
-
//
|
|
801
|
+
// THE PREFERRED SHAPE (exact output): how much to give to receive exactly the required amount.
|
|
802
|
+
// The question of a difference in amounts does not arise with it - the output minimum equals the required by construction.
|
|
804
803
|
exactOut,
|
|
805
804
|
hopsDetail: hops,
|
|
806
|
-
//
|
|
807
|
-
//
|
|
805
|
+
// The single step (the former answer shape) - from the first leg of the path. They must not be read as "the
|
|
806
|
+
// whole route's pool", and that is said here: a detour via a hub has two legs.
|
|
808
807
|
pool: hops.length ? hops[0].pool : null,
|
|
809
808
|
at: Date.now(),
|
|
810
809
|
};
|
|
811
810
|
}
|
|
812
811
|
|
|
813
812
|
// ---------------------------------------------------------------------------
|
|
814
|
-
// WETH -> ETH: unwrap
|
|
813
|
+
// WETH -> ETH: an unwrap at WETH9 itself, one to one, WITHOUT a pool.
|
|
815
814
|
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
815
|
+
// This is where the "unwrap, 1:1" line description comes from. The gas estimate does NOT enter here: to name the
|
|
816
|
+
// unwrap gas one needs an account that holds WETH and can pay for gas, and the page has no such account (and
|
|
817
|
+
// nothing may be sent at this step). The measured unwrap gas lies in the tool's report (the gas is measured via
|
|
818
|
+
// eth_estimateGas on a real WETH holder, which the tool finds from Transfer logs). Substituting that number into
|
|
819
|
+
// the interface as "this deal's gas" would be a fake: it is measured on someone else's account and another
|
|
820
|
+
// amount. So, honestly: gasEstimate === null.
|
|
822
821
|
// ---------------------------------------------------------------------------
|
|
823
822
|
export function unwrapLeg(chain) {
|
|
824
823
|
const u = chain && chain.uniswap;
|
|
@@ -829,7 +828,7 @@ export function unwrapLeg(chain) {
|
|
|
829
828
|
leg: "unwrap",
|
|
830
829
|
venue: w.name || w.symbol,
|
|
831
830
|
wrapped: w,
|
|
832
|
-
rate: 1, //
|
|
831
|
+
rate: 1, // one to one by WETH9's definition: withdraw(wad) returns exactly wad of native
|
|
833
832
|
gasEstimate: null,
|
|
834
833
|
why: null,
|
|
835
834
|
};
|