@ninsei-labs/sdk 0.39.3 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/engine/atomic/dleq2.js +86 -111
  3. package/engine/atomic/halfenc.js +58 -72
  4. package/engine/atomic/halves.js +31 -53
  5. package/engine/atomic/order-client.js +15 -16
  6. package/engine/atomic/order.js +44 -62
  7. package/engine/atomic/orderContext.js +26 -50
  8. package/engine/atomic/orderHalfCommit.js +17 -18
  9. package/engine/core/chainSource.js +54 -69
  10. package/engine/core/clock.js +11 -12
  11. package/engine/core/config.js +554 -449
  12. package/engine/core/finality.js +34 -53
  13. package/engine/core/format.js +35 -36
  14. package/engine/core/preSignGuards.js +109 -117
  15. package/engine/core/rfqSource.js +57 -75
  16. package/engine/core/sellWindow.js +23 -24
  17. package/engine/core/store.js +20 -21
  18. package/engine/core/swap.js +242 -243
  19. package/engine/evm/amounts.js +50 -51
  20. package/engine/evm/asyncExec.js +577 -0
  21. package/engine/evm/asyncLegs.js +308 -0
  22. package/engine/evm/asyncRoute.js +179 -0
  23. package/engine/evm/auth.js +41 -48
  24. package/engine/evm/claimGas.js +28 -47
  25. package/engine/evm/depositor.js +39 -43
  26. package/engine/evm/dex.js +266 -267
  27. package/engine/evm/escrow.js +113 -129
  28. package/engine/evm/factory.js +71 -77
  29. package/engine/evm/fees.js +41 -42
  30. package/engine/evm/funding.js +160 -162
  31. package/engine/evm/gasReserve.js +67 -36
  32. package/engine/evm/index.js +46 -53
  33. package/engine/evm/injected.js +34 -42
  34. package/engine/evm/permit.js +129 -56
  35. package/engine/evm/prices.js +30 -45
  36. package/engine/evm/session.js +188 -189
  37. package/engine/mock/chain.js +17 -18
  38. package/engine/monero/address.js +33 -48
  39. package/engine/monero/halvesSweep.js +25 -43
  40. package/engine/monero/node.js +14 -15
  41. package/engine/monero/scan.js +82 -95
  42. package/engine/monero/swapKeys.js +30 -33
  43. package/engine/monero/unlock.js +17 -20
  44. package/engine/monero/wallet.js +32 -42
  45. package/engine/recovery/recoveryFile.js +154 -161
  46. package/engine/recovery/restore.js +50 -61
  47. package/engine/sdk/bridge.js +244 -234
  48. package/engine/ui/dom.js +4 -5
  49. package/engine/ui/walletModal.js +20 -31
  50. package/index.d.ts +8 -0
  51. package/package.json +1 -1
  52. package/src/adapters/evm-wallet.mjs +16 -0
  53. package/src/adapters/monero-wallet.mjs +1 -1
  54. package/src/adapters/monero.mjs +1 -1
  55. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  56. package/src/config.mjs +1 -1
  57. package/src/engine.mjs +4 -0
  58. package/src/legs/cow-spec.mjs +254 -0
  59. package/src/legs/cow.mjs +315 -0
  60. package/src/legs/evm.mjs +86 -2
  61. package/src/legs/kyber-spec.mjs +265 -0
  62. package/src/legs/kyber.mjs +425 -0
  63. package/src/legs/shape.mjs +35 -0
  64. package/src/legs/uniswaprelay-spec.mjs +541 -0
  65. package/src/legs/uniswaprelay.mjs +422 -0
  66. package/src/legs/uniswapx-spec.mjs +329 -0
  67. package/src/legs/uniswapx.mjs +362 -0
  68. package/src/quotes.mjs +1 -1
  69. package/src/swaps.mjs +1 -1
@@ -1,35 +1,39 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/permit.js for the SDK package.
2
- // Edit the source under www/js, then run: node tools/build-sdk-engine.mjs
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
- // РАЗРЕШЕНИЕ НА ТОКЕН ДЛЯ ПОКУПКИ БЕЗ СВОЕГО ETH (issue #97, часть C): что именно умеет токен и чего не хватает.
5
+ // TOKEN ALLOWANCE FOR BUYING WITHOUT OWN ETH: what the token supports and what is missing.
7
6
  //
8
- // ЧТО ЗДЕСЬ И ЧЕГО НЕТ. Здесь ТОЛЬКО селекторы, сборка calldata и ЧЕСТНЫЙ вердикт о том, хватает ли протоколу
9
- // разрешения по подписи. Ни одного вывода «у USDC permit есть» из памяти тут не записано: такой вывод -
10
- // догадка, а догадка об учётной функции токена стоит денег. Возможность ПРОВЕРЯЕТСЯ чтением цепи (есть ли у
11
- // адреса код, отвечает ли DOMAIN_SEPARATOR словом, отвечает ли nonces(owner)), и вердикт умеет говорить
12
- // «неизвестно» - третьим значением, а не одним из двух.
7
+ // WHAT IS HERE AND WHAT IS NOT. Only selectors, calldata assembly and an HONEST verdict on whether a signature
8
+ // allowance suffices. No conclusion like "USDC has permit" is written from memory: that is a guess, and a guess
9
+ // about a token's accounting function costs money. The capability is PROBED by reading the chain, and the verdict
10
+ // can say "unknown" as a third value.
13
11
  //
14
- // ЧТО УЖЕ ПРОВЕРЕНО ПРО НАШУ НОГУ ОБМЕНА (docs/48, www/js/evm/dexExec.js): разрешение нужно РОУТЕРУ обычным
15
- // approve. Роутер этой сборки Permit2 НЕ ЗНАЕТ (`permit2()` у него откатывает, перенос оплачивает
16
- // TransferHelper.safeTransferFrom от msg.sender). Значит Permit2 в реестре лежит для БУДУЩЕГО пути
17
- // финансирования, а не потому, что этот своп его вызывает.
12
+ // WHAT IS ALREADY CHECKED FOR OUR SWAP LEG: the allowance is needed by the ROUTER as a normal approve. The
13
+ // router of this build does NOT know Permit2 (permit2() reverts; the transfer is paid by TransferHelper.safeTransferFrom
14
+ // from msg.sender). So Permit2 sits in the registry for a FUTURE funding path.
18
15
  //
19
- // ПОЧЕМУ ОДНОЙ ТРАНЗАКЦИИ ЗДЕСЬ НЕТ, И ЭТО НЕ ЗАБЫВЧИВОСТЬ. Своп платит ТОТ, КОГО роутер видит `msg.sender`.
20
- // Пользователь без ETH не может отправить ни approve, ни своп, ни внесение. Внести за него и списать USDC по
21
- // его подписи может только КОНТРАКТ-исполнитель (EOA чужую подпись на списание предъявить не может). Такого
22
- // контракта в репозитории нет и план 61 его не называет, поэтому `usdcWithoutEthVerdict` ОТКАЗЫВАЕТСЯ назвать
23
- // путь исполняемым и говорит, чего не хватает. Собирать calldata «на всякий случай» - значит выдать догадку
24
- // за согласие (ровно то, что док 48 называет ловушкой подставленного получателя).
16
+ // WHY THERE IS NO SINGLE-TRANSACTION PATH HERE, AND IT IS NOT AN OVERSIGHT. The router sees msg.sender as the payer.
17
+ // A user without ETH cannot send approve, swap or deposit. Only an EXECUTOR CONTRACT could deposit for them and
18
+ // pull USDC by their signature (an EOA cannot present someone else's signature). No such contract exists here, so
19
+ // the SINGLE-TRANSACTION path stays refused and says what is missing (kind "no-orchestrator"). BUT A PERSON WITH
20
+ // NO OWN NATIVE COIN IS NO LONGER SENT AWAY BY THAT ALONE: the wallet's balance decides the path first, and an
21
+ // ASYNCHRONOUS provider (an intent auction, whose settlement arrives later) carries the purchase when the wallet
22
+ // cannot cover the gas - the swap settles first and the escrow is funded after it. See usdcWithoutEthVerdict below
23
+ // and www/js/evm/asyncRoute.js - and it is a NAMED refusal only when no provider serves the network.
25
24
  //
26
- // ПОДПИСЬ ВНОСЯЩЕГО (www/js/evm/depositor.js) - ДРУГОЕ РАЗРЕШЕНИЕ. Оно даёт релейщику право ОТПРАВИТЬ
27
- // транзакцию создания ордера, но не право списать токен. Одно другое не заменяет: списание USDC требует
28
- // отдельного разрешения ПО ТОКЕНУ (approve или permit), и оно адресуется тому, кто тянет токен.
25
+ // THE DEPOSITOR SIGNATURE IS A DIFFERENT ALLOWANCE. It lets a relayer SEND the order-creation transaction, but
26
+ // not pull the token. Pulling USDC needs a separate TOKEN allowance (approve or permit) addressed to the puller.
29
27
 
30
- // Селекторы ERC-20 и EIP-2612. Рядом с каждым - строка подписи, из которой он посчитан: проверка
31
- // (tools/check-usdc-without-eth.mjs) пересчитывает keccak256 от этих строк, поэтому селектор не может
32
- // разойтись с подписью молча.
28
+ // THE BALANCE-DRIVEN PATH CHOICE. Whether a provider serves a network ASYNCHRONOUSLY (an intent auction whose
29
+ // settlement arrives later - see www/js/evm/asyncRoute.js) is what decides the path for a person with no own
30
+ // native coin: covered gas -> the ordinary route; short native + a served network -> the asynchronous route;
31
+ // short native + an unserved network -> a NAMED refusal. That answer comes from the same table the package's
32
+ // provider layer pins (sdk/src/legs/cow.mjs).
33
+ import { asyncRouteVerdict } from "./asyncRoute.js";
34
+
35
+ // ERC-20 and EIP-2612 selectors. Next to each is the signature string it was computed from, and a check
36
+ // recomputes keccak256 of those strings, so a selector cannot diverge from its signature silently.
33
37
  export const ERC20_METHOD = {
34
38
  approve: { hex: "0x095ea7b3", signature: "approve(address,uint256)" },
35
39
  allowance: { hex: "0xdd62ed3e", signature: "allowance(address,address)" },
@@ -47,20 +51,26 @@ const addrWord = (a) => hexBody(a).toLowerCase().padStart(64, "0");
47
51
  const uintWord = (v) => BigInt(v).toString(16).padStart(64, "0");
48
52
  const wordBytes = (hex) => {
49
53
  const b = hexBody(hex);
50
- // ПУСТОЙ ОТВЕТ - ЭТО «НЕ ПРОЧИТАЛИ», А НЕ «НОЛЬ БАЙТ». Так eth_call отвечает по адресу без кода:
51
- // показать это как «нет домена» значило бы принять незнание за отказ токена.
54
+ // AN EMPTY ANSWER IS "NOT READ", NOT "ZERO BYTES". This is how eth_call answers for an address without code.
52
55
  if (b.length === 0) return null;
53
56
  return b.length % 2 ? null : b.length / 2;
54
57
  };
55
58
 
56
- // calldata для разрешения по подписи (EIP-2612). Подпись - r || s || v (65 байт); v - в {27,28}.
59
+ // A quantity as BigInt, or null. An empty/unreadable value is null ("not known"), never zero: a zero balance
60
+ // and an unread balance are different states, and the path choice must not confuse them.
61
+ const toBig = (v) => {
62
+ if (v === null || v === undefined || v === "") return null;
63
+ try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
64
+ };
65
+
66
+ // calldata for a signature allowance (EIP-2612). Signature is r || s || v (65 bytes); v in {27,28}.
57
67
  export function encodePermit({ owner, spender, value, deadline, signature }) {
58
- if (!isAddr(owner) || !isAddr(spender)) throw new Error("permit: owner и spender обязаны быть адресами");
68
+ if (!isAddr(owner) || !isAddr(spender)) throw new Error("permit: owner and spender must be addresses");
59
69
  const hex = hexBody(signature);
60
- if (hex.length !== 130) throw new Error("permit: подпись обязана быть 65 байт, а пришло " + hex.length / 2);
70
+ if (hex.length !== 130) throw new Error("permit: the signature must be 65 bytes, got " + hex.length / 2);
61
71
  let v = parseInt(hex.slice(128, 130), 16);
62
72
  if (v < 27) v += 27;
63
- if (v !== 27 && v !== 28) throw new Error("permit: v вне {27,28}: " + v);
73
+ if (v !== 27 && v !== 28) throw new Error("permit: v outside {27,28}: " + v);
64
74
  return EIP2612_METHOD.permit.hex +
65
75
  addrWord(owner) + addrWord(spender) + uintWord(value) + uintWord(deadline) +
66
76
  uintWord(v) + hex.slice(0, 64) + hex.slice(64, 128);
@@ -71,55 +81,118 @@ export const encodeDomainSeparator = () => EIP2612_METHOD.DOMAIN_SEPARATOR.hex;
71
81
  export const encodeApprove = ({ spender, amountWei }) => ERC20_METHOD.approve.hex + addrWord(spender) + uintWord(amountWei);
72
82
 
73
83
  /**
74
- * Умеет ли ЭТОТ адрес разрешение по подписи (EIP-2612). Ответ - ТРИ состояния, и это не педантизм:
75
- * «не проверено» обязано отличаться от «нет».
76
- * supported === true - код есть, DOMAIN_SEPARATOR отвечает словом, nonces(owner) отвечает словом,
77
- * и в байткоде присутствует селектор permit;
78
- * supported === false - код есть, но одного из признаков НЕТ (тогда разрешение по подписи недоступно);
79
- * supported === null - прочитать не удалось (нет кода/пустой ответ): это НЕ «нет permit», это «не знаем».
84
+ * Whether THIS address supports a signature allowance (EIP-2612). The answer has THREE states: "not checked"
85
+ * must differ from "no".
86
+ * supported === true - code exists, DOMAIN_SEPARATOR answers 32 bytes, nonces(owner) answers 32 bytes, and the bytecode has the permit selector;
87
+ * supported === false - code exists, but a signal is MISSING (then a signature allowance is unavailable);
88
+ * supported === null - could not read (no code / empty answer): this is NOT "no permit", it is "unknown".
80
89
  */
81
90
  export function permitSupportVerdict({ code = null, domainSeparator = null, nonces = null, owner = null } = {}) {
82
91
  const codeHex = hexBody(code);
83
- if (codeHex.length === 0) return { supported: null, kind: "no-code", reason: "по адресу токена нет кода - это не токен, и разрешение читать не у чего" };
84
- if (!isAddr(owner)) return { supported: null, kind: "no-owner", reason: "владелец не назван - nonces(owner) прочитать нечем" };
92
+ if (codeHex.length === 0) return { supported: null, kind: "no-code", reason: "no code at the token address - not a token, the allowance cannot be read" };
93
+ if (!isAddr(owner)) return { supported: null, kind: "no-owner", reason: "no owner given - cannot read nonces(owner)" };
85
94
  const dsBytes = wordBytes(domainSeparator);
86
- if (dsBytes === null) return { supported: null, kind: "domain-unread", reason: "DOMAIN_SEPARATOR не отвечает 32 байтами: читать не удалось" };
87
- if (dsBytes !== 32) return { supported: false, kind: "domain-empty", reason: "DOMAIN_SEPARATOR вернул " + dsBytes + " байт вместо 32: EIP-712-домен не заявлен" };
95
+ if (dsBytes === null) return { supported: null, kind: "domain-unread", reason: "DOMAIN_SEPARATOR did not answer 32 bytes: could not read" };
96
+ if (dsBytes !== 32) return { supported: false, kind: "domain-empty", reason: "DOMAIN_SEPARATOR returned " + dsBytes + " bytes instead of 32: no EIP-712 domain declared" };
88
97
  const nonceBytes = wordBytes(nonces);
89
- if (nonceBytes === null) return { supported: null, kind: "nonces-unread", reason: "nonces(owner) не отвечает: читать не удалось" };
90
- if (nonceBytes !== 32) return { supported: false, kind: "nonces-empty", reason: "nonces(owner) вернул " + nonceBytes + " байт вместо 32: EIP-2612-счётчика нет" };
98
+ if (nonceBytes === null) return { supported: null, kind: "nonces-unread", reason: "nonces(owner) did not answer: could not read" };
99
+ if (nonceBytes !== 32) return { supported: false, kind: "nonces-empty", reason: "nonces(owner) returned " + nonceBytes + " bytes instead of 32: no EIP-2612 counter" };
91
100
  if (!codeHex.toLowerCase().includes(EIP2612_METHOD.permit.hex.slice(2))) {
92
- return { supported: false, kind: "permit-selector-absent", reason: "в байткоде токена нет селектора permit: разрешения по подписи токен не предоставляет" };
101
+ return { supported: false, kind: "permit-selector-absent", reason: "the token bytecode has no permit selector: no signature allowance" };
93
102
  }
94
- return { supported: true, kind: "eip2612", reason: "токен заявляет EIP-712-домен, счётчик nonces и селектор permit" };
103
+ return { supported: true, kind: "eip2612", reason: "the token declares an EIP-712 domain, a nonces counter and the permit selector" };
95
104
  }
96
105
 
97
106
  /**
98
- * Можно ли провести покупку за токен БЕЗ своего ETH ОДНОЙ транзакцией. Вердикт говорит ИМЕННО то, чего не
99
- * хватает, и НЕ выдаёт calldata, пока путь не исполним полностью.
100
- * @param {{ orchestrator?: string|null, permitSupported?: boolean|null, token?: string|null }} args
107
+ * WHICH WAY A "BUY WITHOUT OWN NATIVE COIN" GOES - DECIDED BY THE WALLET'S BALANCE, NOT BY A FIXED REFUSAL.
108
+ *
109
+ * The wallet's own native coin decides the path BEFORE any token question:
110
+ * * the wallet covers the gas the deal needs (`gasNeedWei`) - the ordinary, synchronous path is taken: the
111
+ * person pays with their own gas and nothing has to be asked of anyone (`kind: "own-gas"`, `path: "sync"`);
112
+ * * the wallet does NOT cover it - the ASYNCHRONOUS path is taken (`kind: "async-route"`, `path: "async"`):
113
+ * the swap settles FIRST and the escrow is funded AFTER it, so the deposit and its gas come later. This is
114
+ * NOT a refusal as long as a provider serves the network - the "stop being a refusal" rule;
115
+ * * no provider serves the network - a NAMED refusal (`kind: "no-async-provider"`, blocked): "the wallet cannot
116
+ * cover the gas and there is no asynchronous path here" - never silence and never a silent "yes".
117
+ *
118
+ * An unread balance or an unmeasured gas need is its OWN state: "not known" must not look like "covered", but it
119
+ * is not a refusal either - the caller reads `blocked`.
120
+ *
121
+ * The single-transaction executor branch lives BELOW and stays reachable when the balance question cannot be
122
+ * answered at all (no gas need given): an executor contract could still carry the whole purchase in one
123
+ * transaction. That branch is the older verdict and is kept so existing callers see the same kinds.
124
+ *
125
+ * @param {{ orchestrator?: string|null, permitSupported?: boolean|null, token?: string|null,
126
+ * nativeBalanceWei?: *, gasNeedWei?: *, asyncProvider?: object|null, chainId?: * }} args
101
127
  */
102
- export function usdcWithoutEthVerdict({ orchestrator = null, permitSupported = null, token = null } = {}) {
103
- // ПЕРВЫЙ И ГЛАВНЫЙ ОТКАЗ, и он не про токен. Своп платит msg.sender (роутер), а чужую подпись на списание
104
- // может предъявить только КОНТРАКТ. Без исполнителя однотранзакционный путь не существует ВООБЩЕ - и это
105
- // не зависит от того, есть ли у токена permit.
128
+ export function usdcWithoutEthVerdict({
129
+ orchestrator = null, permitSupported = null, token = null,
130
+ nativeBalanceWei = null, gasNeedWei = null, asyncProvider = null, chainId = null,
131
+ } = {}) {
132
+ const need = toBig(gasNeedWei);
133
+ // THE ASYNC ANSWER - from the caller's explicit verdict (the package layer's own answer) or from the engine's
134
+ // table by chainId. Either way it is a VALUE with a named reason, never silence.
135
+ const async = asyncProvider && typeof asyncProvider === "object"
136
+ ? asyncProvider
137
+ : (chainId === null || chainId === undefined ? null : asyncRouteVerdict({ chainId }));
138
+ const asyncOk = Boolean(async && async.ok === true);
139
+ const asyncReason = async && async.ok === false && async.reason ? String(async.reason) : "no-async-provider";
140
+
141
+ if (need !== null && need > 0n) {
142
+ const have = toBig(nativeBalanceWei);
143
+ if (have === null) {
144
+ return {
145
+ ok: false, blocked: false, checked: false, kind: "balance-unread", path: null,
146
+ gasNeedWei: need.toString(),
147
+ reason: "the wallet's native balance was not read, so it is unknown which path fits - read the balance before choosing",
148
+ };
149
+ }
150
+ if (have >= need) {
151
+ return {
152
+ ok: true, blocked: false, checked: true, kind: "own-gas", path: "sync",
153
+ nativeBalanceWei: have.toString(), gasNeedWei: need.toString(),
154
+ reason: "the wallet's own native covers the gas this deal needs, so the ordinary synchronous path is taken",
155
+ };
156
+ }
157
+ const base = {
158
+ checked: true, nativeBalanceWei: have.toString(), gasNeedWei: need.toString(),
159
+ shortfallWei: (need - have).toString(),
160
+ };
161
+ if (asyncOk) {
162
+ return {
163
+ ok: true, blocked: false, ...base, kind: "async-route", path: "async",
164
+ provider: async.provider ?? null, venue: async.venue ?? null,
165
+ reason: "the wallet's own native does not cover the gas this deal needs, but an asynchronous provider serves this network: the swap settles first and the escrow is funded after it, so the deposit's gas is paid later",
166
+ };
167
+ }
168
+ return {
169
+ ok: false, blocked: true, ...base, kind: "no-async-provider", path: null,
170
+ reason: "the wallet's own native does not cover the gas this deal needs and no asynchronous provider serves this network (" + asyncReason + "): there is no path for a purchase without own native coin here",
171
+ };
172
+ }
173
+
174
+ // GAS NOT MEASURED: the balance cannot decide anything - fall through to the single-transaction executor
175
+ // verdict (unchanged behaviour for callers that pass no gas need).
176
+ // THE REFUSAL WITHOUT AN EXECUTOR, and it is not about the token. The swap pays msg.sender (the router), and
177
+ // only a CONTRACT can present someone else's pull signature. Without an executor the one-transaction path
178
+ // does not exist AT ALL, regardless of the token.
106
179
  if (!isAddr(orchestrator)) {
107
180
  return {
108
- ok: false, blocked: true, kind: "no-orchestrator",
109
- reason: "нет контракта-исполнителя: списать токен по подписи пользователя и внести натив в эскроу одной транзакцией может только контракт, а в этой сборке его нет. Подпись вносящего без него даёт релейщику право ОТПРАВИТЬ создание ордера, но не право списать токен",
181
+ ok: false, blocked: true, kind: "no-orchestrator", path: null,
182
+ reason: "no executor contract: only a contract could pull a token by the user signature and deposit native into the escrow in one transaction, and this build has none. The depositor signature without it lets a relayer SEND the order creation, but not pull the token",
110
183
  };
111
184
  }
112
185
  if (permitSupported === false) {
113
186
  return {
114
187
  ok: false, blocked: true, kind: "token-no-permit",
115
- reason: "у токена нет разрешения по подписи (EIP-2612): разрешение придётся выдавать отдельным approve, то есть отдельной транзакцией - одной транзакцией путь не собирается",
188
+ reason: "the token has no signature allowance (EIP-2612): the allowance must be a separate approve, i.e. a separate transaction - the path does not assemble into one transaction",
116
189
  };
117
190
  }
118
191
  if (permitSupported !== true) {
119
192
  return {
120
193
  ok: false, blocked: true, kind: "permit-unproven",
121
- reason: "неизвестно, умеет ли токен разрешение по подписи: «не проверено» - это не «можно». Возможность обязана быть прочитана у цепи до сборки пути",
194
+ reason: "unknown whether the token supports a signature allowance: \"not checked\" is not \"possible\". The capability must be read from the chain before assembling the path",
122
195
  };
123
196
  }
124
- return { ok: true, blocked: false, kind: "one-tx-possible", reason: "исполнитель назван, токен заявляет EIP-2612: путь одной транзакцией собирается" };
197
+ return { ok: true, blocked: false, kind: "one-tx-possible", path: "sync", reason: "an executor is present and the token declares EIP-2612: the one-transaction path assembles" };
125
198
  }
@@ -1,32 +1,23 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/prices.js for the SDK package.
2
- // Edit the source under www/js, then run: node tools/build-sdk-engine.mjs
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
- // Живые рыночные числа для EVM-ноги: цена нейтива (Chainlink on-chain) и стоимость газа (RPC).
7
- //
8
- // ПОЧЕМУ ON-CHAIN, А НЕ API. Цена берётся вызовом контракта через тот же RPC, что уже разрешён
9
- // в CSP (`connect-src`), поэтому:
10
- // - не появляется ни одного нового внешнего домена (и ни одного нового доверенного сервиса);
11
- // - нет ключей, лимитов и «мы упали, потому что у CoinGecko 429»;
12
- // - значение нельзя перепутать с ценой другой сети: адрес фида свой у каждой цепи.
13
- // Адреса фидов НЕ выдуманы: каждый подтверждён вызовом в сети - описание `description()` должно
14
- // совпасть с ожидаемой парой, а ответ - попасть в разумный коридор и быть свежим. Проверка живёт
15
- // в tools/check-prices.mjs (работает на заглушке RPC, без сети).
16
- //
17
- // ЧЕГО ЗДЕСЬ НЕТ: если фида для сети нет или он молчит - возвращаем null, и вызывающий код обязан
18
- // показать, что число демонстрационное, а не подставить вчерашнюю константу молча.
5
+ // LIVE MARKET NUMBERS FOR THE EVM LEG: the native price (Chainlink on-chain) and the gas cost (RPC).
6
+ // WHY ON-CHAIN, NOT AN API: the price is a contract call over the same RPC already allowed by the CSP
7
+ // (connect-src), so no new external domain appears, there are no keys or rate limits, and the feed address is
8
+ // per chain. The feed addresses are NOT invented: each is confirmed by a call on chain (description() must match).
9
+ // WHAT IS NOT HERE: if a network has no feed or it is silent, return null and let the caller mark the number as demo.
19
10
 
20
11
  const SELECTOR = {
21
12
  description: "0x7284e416", // description()
22
13
  latestRoundData: "0xfeaf968c", // latestRoundData() -> (roundId, answer, startedAt, updatedAt, answeredInRound)
23
14
  };
24
15
 
25
- const CACHE_MS = 60_000; // цена: минута - компромисс между свежестью и числом запросов
26
- const GAS_CACHE_MS = 30_000; // газ меняется чаще, но и он не обязан бегать на каждый ввод символа
27
- const MAX_AGE_MS = 24 * 60 * 60 * 1000; // старше суток фид считаем несвежим и не показываем как живой
16
+ const CACHE_MS = 60_000; // price: a minute balances freshness and request count
17
+ const GAS_CACHE_MS = 30_000; // gas changes more often, but it need not run on every keystroke
18
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000; // older than a day, the feed is stale and not shown as live
28
19
 
29
- const cache = new Map(); // ключ: chainId + вид запроса
20
+ const cache = new Map(); // key: chainId + request kind
30
21
 
31
22
  function cached(key, ttlMs) {
32
23
  const hit = cache.get(key);
@@ -47,14 +38,14 @@ async function rpc(chain, method, params = []) {
47
38
  });
48
39
  if (!res.ok) throw new Error(method + ": HTTP " + res.status);
49
40
  const j = await res.json();
50
- if (j.error) throw new Error(method + ": " + (j.error.message || "ошибка RPC"));
41
+ if (j.error) throw new Error(method + ": " + (j.error.message || "RPC error"));
51
42
  return j.result;
52
43
  }
53
44
 
54
45
  const word = (data, i) => BigInt("0x" + String(data).slice(2).slice(i * 64, (i + 1) * 64));
55
46
 
56
- // Декодирование строки ABI: [offset][length][bytes]. Нужно, чтобы отличить фид ETH/USD от любого
57
- // другого, который кто-то подставил в конфиг. TextDecoder, а не Buffer: этот код живёт в браузере.
47
+ // ABI string decode: [offset][length][bytes]. Needed to tell the ETH/USD feed from another one. TextDecoder,
48
+ // not Buffer: this code runs in the browser.
58
49
  function decodeString(data) {
59
50
  const s = String(data || "");
60
51
  if (s.length < 2 + 128) return "";
@@ -65,7 +56,7 @@ function decodeString(data) {
65
56
  return new TextDecoder().decode(bytes);
66
57
  }
67
58
 
68
- // Цена нейтива в USD. null - если фида нет, он молчит или значения не проходят проверку.
59
+ // Native price in USD. null if the feed is missing, silent, or fails validation.
69
60
  export async function nativeUsd(chain, { force = false } = {}) {
70
61
  const feed = chain && chain.priceFeed;
71
62
  if (!feed || !feed.address) return null;
@@ -87,7 +78,7 @@ export async function nativeUsd(chain, { force = false } = {}) {
87
78
  const fresh = updatedAt > 0 && Date.now() - updatedAt < MAX_AGE_MS;
88
79
  const sane = value >= (feed.min || 0.01) && value <= (feed.max || 1e7);
89
80
  if (desc !== feed.pair || !fresh || !sane) {
90
- return put(key, null); // null, а не «примерно столько»: молчание честнее выдумки
81
+ return put(key, null); // null, not "roughly this": silence is more honest than a guess
91
82
  }
92
83
  return put(key, { usd: value, updatedAt, source: "chainlink", pair: desc });
93
84
  } catch {
@@ -95,9 +86,8 @@ export async function nativeUsd(chain, { force = false } = {}) {
95
86
  }
96
87
  }
97
88
 
98
- // Стоимость газа в USD: оценка лимита × цена газа × цена нейтива.
99
- // На Arbitrum в цену газа уже входит L1-составляющая (так устроен их gas price), поэтому
100
- // отдельный вызов NodeInterface не нужен - обычная оценка даёт сопоставимую с кошельками цифру.
89
+ // Gas cost in USD: limit estimate x gas price x native price. On Arbitrum the L1 part is already in the gas
90
+ // price, so no separate NodeInterface call is needed.
101
91
  export async function gasUsd(chain, { from = null, payToken = null, amountWei = null } = {}) {
102
92
  const key = chain.id + ":gas";
103
93
  const hit = cached(key, GAS_CACHE_MS);
@@ -108,15 +98,12 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
108
98
  const gasPrice = BigInt(await rpc(chain, "eth_gasPrice"));
109
99
  let gasLimit = null;
110
100
  let measured = false;
111
- // Оцениваем ту транзакцию, которую человек и подпишет: перевод оплачиваемого токена.
112
- // Эскроу ещё не задеплоен, поэтому это честная нижняя оценка, а не «газ свопа».
101
+ // We estimate the transaction the person will actually sign: a transfer of the paying token.
102
+ // The escrow is not deployed yet, so this is an honest lower bound, not the swap gas.
113
103
  try {
114
- // Без аккаунта eth_estimateGas подставляет НУЛЕВОЙ адрес, и перевод токена справедливо
115
- // падает с "ERC20: transfer from the zero address". Поэтому без from не оцениваем вовсе:
116
- // берём типовой лимит и честно помечаем, что он не измерен (в интерфейсе это видно).
117
- // ВАЖНО: признак "перевод токена" и признак "есть аккаунт" - РАЗНЫЕ условия. Раньше они
118
- // были слиты в одну ветку, и без аккаунта перевод токена получал лимит нативного перевода
119
- // (21000 вместо 65000) - поймал tools/check-prices.mjs.
104
+ // Without an account eth_estimateGas uses the ZERO address and a token transfer fails with
105
+ // "ERC20: transfer from the zero address". So without `from` we do not estimate: we take a typical
106
+ // limit and mark it as unmeasured. NOTE: "token transfer" and "has account" are DIFFERENT conditions.
120
107
  const tokenTransfer = Boolean(payToken && !payToken.native);
121
108
  if (tokenTransfer && from) {
122
109
  // transfer(address,uint256) = 0xa9059cbb
@@ -128,7 +115,7 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
128
115
  gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: from, value: "0x1" }]));
129
116
  measured = true;
130
117
  } else {
131
- // Ни аккаунта, ни измерения: типовой лимит и честный признак «не измерено».
118
+ // No account and no measurement: a typical limit and an honest 'not measured' flag.
132
119
  gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
133
120
  }
134
121
  } catch {
@@ -142,21 +129,19 @@ export async function gasUsd(chain, { from = null, payToken = null, amountWei =
142
129
  }
143
130
  }
144
131
 
145
- // Одна точка обновления: подтянуть цену и газ для сети и вернуть то, чем можно подписать интерфейс.
132
+ // One refresh point: fetch price and gas for the network and return what the interface can sign with.
146
133
  export async function refreshChainPrices(chain, opts = {}) {
147
134
  if (!chain || !chain.rpcUrl) return { nativeUsd: null, gasUsd: null, source: "none" };
148
135
  const [price, gas] = await Promise.all([nativeUsd(chain), gasUsd(chain, opts)]);
149
- // Цена нейтива - общая для всех расчётов сети (балансы, «≈ $», оценка газа), поэтому пишем её
150
- // в конфиг сети: иначе одна и та же цифра жила бы в двух местах и разъезжалась.
136
+ // The native price is shared by all network maths (balances, '~ $', gas), so it is written into the
137
+ // network config, else the same number would live in two places and drift.
151
138
  if (price && Number.isFinite(price.usd)) chain.native.usd = price.usd;
152
139
  return {
153
140
  nativeUsd: price ? price.usd : null,
154
141
  nativeUpdatedAt: price ? price.updatedAt : null,
155
142
  gasUsd: gas ? gas.usd : null,
156
- // ЦЕНА ГАЗА В ВЕЙ - НАРУЖУ, А НЕ ТОЛЬКО СТОИМОСТЬ ОЦЕНКИ. Из неё считается запас газа на действия ордера
157
- // (www/js/evm/gasReserve.js): отметка готовности и возврат - другие транзакции, у них свои пределы, и
158
- // «стоимость одной оценки» их не заменяет. Нет газа у цепи - null, и вызывающий обязан сказать, что запас
159
- // не измерен, а не подставить ноль.
143
+ // GAS PRICE IN WEI GOES OUT, not just the estimate cost. The order-action gas reserve is derived from it
144
+ // (gasReserve.js): the mark and refund are different transactions with their own limits. No gas - null.
160
145
  gasPriceWei: gas ? gas.gasPrice : null,
161
146
  gasSource: gas ? gas.source : null,
162
147
  gasMeasured: gas ? Boolean(gas.measured) : false,
@@ -164,8 +149,8 @@ export async function refreshChainPrices(chain, opts = {}) {
164
149
  };
165
150
  }
166
151
 
167
- // Коротко и в одну строку, но про ОБЕ половины: строка называется «Gas & price», значит и газ,
168
- // и цена должны быть в значении. Живая цена видна по самому тексту (Chainlink), демо - по «demo values».
152
+ // Short and one line, but covering BOTH halves: the string is "Gas & price", so both gas and price must be
153
+ // in the value. A live price shows as Chainlink, a demo one as 'demo values'.
169
154
  export function priceLabel(info) {
170
155
  if (!info || !info.source || info.source === "none") return "demo values";
171
156
  if (info.source !== "chainlink") return "demo values";