@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,30 +1,29 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/session.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: состояние, балансы, смена сети и два способа подключения.
5
+ // The EVM wallet: state, balances, network switching and two ways to connect.
7
6
 
8
- // ПОЧЕМУ ДВА КОННЕКТОРА. WalletConnect (Reown) нужен, когда пользователь пришёл с телефона или из
9
- // кошелька без расширения: сессия живёт по QR. Браузерный кошелёк (MetaMask, Rabby, Coinbase...),
10
- // если он установлен, подключается напрямую через EIP-1193 - это быстрее и без лишнего звена.
11
- // Оба дают один и тот же интерфейс EIP-1193, поэтому состояние, балансы и смена сети у них общие:
12
- // разница только в рукопожатии (см. www/js/evm/injected.js).
7
+ // WHY TWO CONNECTORS. WalletConnect (Reown) is for a user coming from a phone or a wallet without an extension:
8
+ // the session lives over a QR. A browser wallet (MetaMask, Rabby, Coinbase...), if installed, connects directly
9
+ // over EIP-1193 - faster and with no extra hop.
10
+ // Both expose the same EIP-1193 interface, so state, balances and network switching are shared:
11
+ // only the handshake differs (see www/js/evm/injected.js).
13
12
 
14
- // Что здесь реально, а что нет - принципиально важно:
15
- // РЕАЛЬНО: подключение (QR или браузерное расширение), адрес, chainId, балансы нативного токена и
16
- // ERC-20 (значения приходят от кошелька пользователя через его провайдер, decimals - из
17
- // конфига);
18
- // НЕ РЕАЛЬНО: переводы. Контракт escrow пока не задеплоен, адрес escrow в демке - мок, поэтому
19
- // отправка транзакции остаётся имитацией (см. evm/index.js). Реальные деньги демка не
20
- // двигает и не должна.
13
+ // What is real here and what is not matters:
14
+ // REAL: connecting (QR or browser extension), address, chainId, native-token and ERC-20 balances (values come
15
+ // from the user's wallet through its provider, decimals from the config);
16
+ //
17
+ // NOT REAL: transfers. The escrow contract is not deployed yet and the escrow address in the demo is a mock,
18
+ // so sending a transaction stays a simulation (see evm/index.js). The demo does not move real money
19
+ // and must not.
21
20
 
22
- // Модалку рисуем сами (ui/walletModal.js): у Reown она со inline-стилями, а CSP демки их запрещает.
23
- // Провайдер WalletConnect инициализируется с showQrModal: false, ссылку сессии берём из события
24
- // display_uri и кодируем в QR в data:-URL.
21
+ // The modal is drawn by us (ui/walletModal.js): Reown's own uses inline styles, which the demo CSP forbids.
22
+ // The WalletConnect provider is initialised with showQrModal: false; the session link comes from the
23
+ // display_uri event and is encoded into a QR in a data: URL.
25
24
 
26
- // Балансы кешируются в состоянии: UI спрашивает balanceOf() синхронно, а обновление идёт
27
- // через refreshBalances() (подключение, смена сети, событие кошелька).
25
+ // Balances are cached in the state: the UI asks balanceOf() synchronously, and refreshBalances() updates
26
+ // them (on connect, network change, wallet event).
28
27
  import { CHAINS, EVM, chainById, chainByChainId, tokensOf } from "../core/config.js";
29
28
  import {
30
29
  discoverWallets,
@@ -35,18 +34,18 @@ import {
35
34
  } from "./injected.js";
36
35
  import { openWalletModal } from "../ui/walletModal.js";
37
36
  import { toHuman } from "./amounts.js";
38
- // ПРАВИЛО КОМИССИИ CLAIM - ИЗ ОБЩЕГО МОДУЛЯ (issue #127): та же формула, по которой нода считает подарок.
37
+ // THE CLAIM FEE RULE - FROM THE SHARED MODULE: the same formula the node uses for its gift.
39
38
  import { claimMaxFeePerGasWei } from "./claimGas.js";
40
39
 
41
40
  const listeners = new Set();
42
41
 
43
42
  const state = {
44
43
  status: "idle", // idle | pairing | connected | error
45
- kind: null, // walletconnect | injected - каким коннектором подключены
46
- label: null, // имя кошелька для UI: "WalletConnect", "MetaMask", "Rabbit"...
44
+ kind: null, // walletconnect | injected - which connector is in use
45
+ label: null, // wallet name for the UI: "WalletConnect", "MetaMask", "Rabby"...
47
46
  address: null,
48
- chainId: null, // числовой chainId сети, в которой сейчас кошелёк
49
- balances: {}, // "42161:USDC" -> человекочитаемое число
47
+ chainId: null, // numeric chainId of the network the wallet is on
48
+ balances: {}, // "42161:USDC" -> a human-readable number
50
49
  error: null,
51
50
  lastRefresh: 0,
52
51
  };
@@ -90,10 +89,10 @@ export function chainIdOf() {
90
89
  return state.chainId;
91
90
  }
92
91
 
93
- // ПРОВАЙДЕР КОШЕЛЬКА НАРУЖУ - ЯДРУ, А НЕ ЭКРАНАМ. Ядро (sdk/) получает кошелёк адаптером и окон не
94
- // открывает: ему нужен тот же EIP-1193 провайдер, которым подписывает страница. Второй провайдер заводить
95
- // нельзя - подпись ушла бы не тем ключом. Для экранов это имя ничего не значит: состояние кошелька они
96
- // читают через status()/isConnected(), как и раньше.
92
+ // THE WALLET PROVIDER GOES OUTWARD - TO THE CORE, NOT TO SCREENS. The core (sdk/) gets the wallet as an
93
+ // adapter and opens no windows: it needs the same EIP-1193 provider the page signs with. A second provider is
94
+ // not allowed - the signature would leave with the wrong key. For screens this name means nothing: they read
95
+ // wallet state through status()/isConnected(), as before.
97
96
  export function currentProvider() {
98
97
  return provider;
99
98
  }
@@ -108,8 +107,8 @@ const hexChainId = (id) => "0x" + Number(id).toString(16);
108
107
 
109
108
  async function loadLib() {
110
109
  if (!lib) {
111
- // Динамический импорт своего же файла: CSP script-src 'self' это разрешает,
112
- // и 2 МБ бандла грузятся только когда пользователь реально жмёт Connect.
110
+ // A dynamic import of our own file: CSP script-src 'self' allows it,
111
+ // and the 2 MB bundle loads only when the user actually presses Connect.
113
112
  lib = await import("../../assets/vendors/walletconnect/wc-browser.js?v=13fe06e5");
114
113
  }
115
114
  return lib;
@@ -133,11 +132,11 @@ function reset(status = "idle", error = null) {
133
132
  emit();
134
133
  }
135
134
 
136
- // --- общая часть обоих коннекторов ----------------------------------------------------------
135
+ // --- the part shared by both connectors ------------------------------------------------------
137
136
  //
138
- // WalletConnect и браузерный кошелёк дают один и тот же интерфейс EIP-1193, поэтому события,
139
- // состояние и чтение балансов у них общие. Различается только рукопожатие, поэтому здесь -
140
- // обработчики, а в connect()/connectInjected() - как именно получен провайдер.
137
+ // WalletConnect and a browser wallet expose the same EIP-1193 interface, so events, state and balance reading
138
+ // are shared. Only the handshake differs, so here are the handlers and in connect()/
139
+ // connectInjected() - how exactly the provider was obtained.
141
140
 
142
141
  function wireProvider(p) {
143
142
  p.on("chainChanged", (id) => {
@@ -148,8 +147,8 @@ function wireProvider(p) {
148
147
  });
149
148
  p.on("accountsChanged", (accounts) => {
150
149
  const next = (accounts && accounts[0]) || null;
151
- // Пустой список - пользователь отключил сайт в кошельке. Это не «подключено без адреса»,
152
- // а именно отключение: иначе экран показывал бы пустой кошелёк как рабочий.
150
+ // An empty list - the user disconnected the site in the wallet. That is not "connected without an address"
151
+ // but a real disconnect: otherwise the screen would show an empty wallet as working.
153
152
  if (!next) {
154
153
  if (modal) modal.close();
155
154
  reset("idle", null);
@@ -159,8 +158,8 @@ function wireProvider(p) {
159
158
  emit();
160
159
  refreshBalances();
161
160
  });
162
- // disconnect приходит от WalletConnect и от браузерного кошелька (например, при сбросе);
163
- // session_delete - только WalletConnect. Оба означают одно: сессии больше нет.
161
+ // disconnect comes from both WalletConnect and a browser wallet (e.g. on reset);
162
+ // session_delete - only WalletConnect. Both mean one thing: the session is gone.
164
163
  p.on("disconnect", () => {
165
164
  if (modal) modal.close();
166
165
  reset("idle", null);
@@ -171,13 +170,13 @@ function wireProvider(p) {
171
170
  });
172
171
  }
173
172
 
174
- // ВОЗВРАТ СТРАНИЦЫ ИЗ BFCACHE. Chrome (с 149) разрывает WebSocket, когда страница уходит в кэш
175
- // "вперёд-назад", и делает это НАМЕРЕННО: в консоли это выглядит как "failed: Page entered Back-Forward
176
- // Cache", хотя сеть ни при чём. Следствие: провайдер WalletConnect кэширован в переменной, его сокет уже
177
- // закрыт, а повторы релея замерли вместе со страницей - и после возврата сессия ВЫГЛЯДИТ живой, а запросы
178
- // к кошельку не доходят. Ни одноразовый restore(), ни события провайдера этого не замечают. Чиним тем же
179
- // механизмом, на который restore() опирается: init() поднимает сессию из localStorage. disconnect() здесь
180
- // НЕ вызываем - он удаляет сессию из хранилища, то есть сломал бы ровно то, что восстанавливаем.
173
+ // RETURNING TO THE PAGE FROM BFCACHE. Chrome (since 149) tears down the WebSocket when the page goes into the
174
+ // back-forward cache, deliberately: in the console it looks like "failed: Page entered Back-Forward Cache",
175
+ // although the network is not to blame. Consequence: the WalletConnect provider is cached in a variable, its
176
+ // socket is closed and the relay retries froze with the page - so after returning the session LOOKS alive while
177
+ // requests to the wallet do not get through. Neither the one-shot restore() nor provider events notice it. We
178
+ // fix it with the same mechanism restore() relies on: init() raises the session from localStorage.
179
+ // disconnect() is NOT called here - it deletes the session from storage, i.e. would break what we restore.
181
180
  let resumeInFlight = false;
182
181
 
183
182
  export async function resumeConnection() {
@@ -187,45 +186,45 @@ export async function resumeConnection() {
187
186
  try {
188
187
  await Promise.race([
189
188
  p.request({ method: "eth_accounts" }),
190
- new Promise((_, rej) => setTimeout(() => rej(new Error("релей не ответил за 4 с")), 4000)),
189
+ new Promise((_, rej) => setTimeout(() => rej(new Error("the relay did not answer in 4 s")), 4000)),
191
190
  ]);
192
- console.log("[wc] после возврата страницы соединение живо");
191
+ console.log("[wc] the connection is alive after returning to the page");
193
192
  return;
194
193
  } catch (e) {
195
- console.log("[wc] после возврата страницы релей не отвечает (" + e.message + ") - поднимаю сессию заново");
194
+ console.log("[wc] the relay does not answer after returning (" + e.message + ") - raising the session again");
196
195
  } finally {
197
196
  resumeInFlight = false;
198
197
  }
199
- provider = null; // у старого сокета браузер закрыл соединение, кэш смысла не имеет
198
+ provider = null; // the browser closed the old socket, so caching it is pointless
200
199
  try {
201
200
  const want = state.chainId || (CHAINS.find((c) => c.escrow) || {}).chainId;
202
201
  const fresh = await ensureProvider(want);
203
202
  const accounts = await fresh.request({ method: "eth_accounts" });
204
203
  if (!Array.isArray(accounts) || !accounts.length) {
205
- console.log("[wc] сессии в хранилище не осталось - нужен новый QR");
204
+ console.log("[wc] no session left in storage - a new QR is needed");
206
205
  reset("idle", null);
207
206
  return;
208
207
  }
209
208
  state.address = accounts[0];
210
209
  emit();
211
210
  await refreshBalances();
212
- console.log("[wc] сессия поднята заново: " + state.address);
211
+ console.log("[wc] session raised again: " + state.address);
213
212
  } catch (e) {
214
- console.log("[wc] поднять сессию не удалось: " + e.message);
213
+ console.log("[wc] could not raise the session: " + e.message);
215
214
  reset("idle", null);
216
215
  }
217
216
  }
218
217
 
219
- // Возврат страницы: pageshow (в том числе из bfcache) и появление вкладки. Дебаунс, чтобы переключение
220
- // вкладок не дёргало релей на каждый чих.
218
+ // Returning to the page: pageshow (including from bfcache) and tab visibility. Debounced so switching
219
+ // tabs does not poke the relay on every twitch.
221
220
  let resumeTimer = null;
222
221
  const scheduleResume = () => {
223
222
  clearTimeout(resumeTimer);
224
223
  resumeTimer = setTimeout(() => { resumeConnection(); }, 400);
225
224
  };
226
- // ПОДПИСКА НА СОБЫТИЯ ОКНА - ТОЛЬКО ТАМ, ГДЕ ОКНО ЕСТЬ. Раньше это стояло без проверки среды, и модуль
227
- // падал при загрузке в Node (проверки репозитория) и в SDK, который обещает «без DOM»: падение приходило
228
- // не в момент подписки, а на импорте файла - то есть ломало всё, что его подтягивает.
225
+ // WINDOW-EVENT SUBSCRIPTION ONLY WHERE THERE IS A WINDOW. It used to run with no environment check, and the
226
+ // module failed on load in Node (repo checks) and in the SDK, which promises "no DOM": the failure came not at
227
+ // subscription time but on importing the file - i.e. it broke everything that pulls it in.
229
228
  if (typeof window !== "undefined" && typeof document !== "undefined") {
230
229
  window.addEventListener("pageshow", () => scheduleResume());
231
230
  document.addEventListener("visibilitychange", () => { if (!document.hidden) scheduleResume(); });
@@ -237,22 +236,22 @@ async function ensureProvider(wantChainId) {
237
236
  provider = await EthereumProvider.init({
238
237
  projectId: EVM.projectId,
239
238
  chains: [wantChainId],
240
- // Только сети, где демка может сеттлиться: сеть без эскроу предлагать кошельку незачем (и
241
- // проект WalletConnect может её не знать, что сломало бы подключение).
239
+ // Only networks the demo can settle on: there is no point offering the wallet a network without an escrow
240
+ // (and the WalletConnect project may not know it, which would break the connection).
242
241
  optionalChains: CHAINS.filter((c) => c.escrow).map((c) => c.chainId),
243
242
  showQrModal: false,
244
243
  rpcMap: rpcMap(),
245
- // url в метаданных WalletConnect сверяет с фактическим origin страницы (проверка
246
- // происхождения приложения). Если подставить боевой домен, локально и на тестовом
247
- // домене сессия срывается - поэтому берём origin, а не константу.
244
+ // The url in the WalletConnect metadata is compared against the page's actual origin (the app-origin
245
+ // check). Substituting the production domain breaks the session locally and on a test domain - hence
246
+ // the origin is taken, not a constant.
248
247
  metadata: { ...EVM.metadata, url: location.origin },
249
248
  });
250
249
 
251
250
  provider.on("display_uri", (uri) => showUri(uri));
252
251
  wireProvider(provider);
253
- // kind/label здесь НЕ выставляем: провайдер создан, но подключения ещё нет. Иначе после
254
- // перезагрузки без сессии состояние было бы idle, а UI считал бы кошелёк подключённым
255
- // (это ловится проверкой в браузере: status=idle, kind=walletconnect).
252
+ // kind/label are NOT set here: the provider is created, but there is no connection yet. Otherwise, after
253
+ // a reload without a session the state would be idle while the UI counted the wallet as connected
254
+ // (a browser check catches this: status=idle, kind=walletconnect).
256
255
  return provider;
257
256
  }
258
257
 
@@ -268,46 +267,46 @@ async function showUri(uri) {
268
267
  });
269
268
  modal.update({ qrDataUrl, uri, status: "Waiting for the wallet to approve..." });
270
269
  } catch (e) {
271
- // QR не собрался - ссылку всё равно показываем, её можно скопировать.
270
+ // The QR did not render - the link is shown anyway, it can be copied.
272
271
  modal.update({ uri, status: "Scan failed to render, copy the link instead", error: true });
273
272
  console.warn("qr render failed", e);
274
273
  }
275
274
  }
276
275
 
277
- // Восстановление сессии после перезагрузки страницы.
276
+ // Restoring the session after a page reload.
277
+ //
278
+ // WalletConnect keeps the session in localStorage, and EthereumProvider.init() raises it back.
279
+ // But our provider was created LAZILY - only on "Connect wallet" - so after F5 the session lay in storage while
280
+ // the page knew nothing: it showed "Connect wallet" and offered a new QR. Restoration is an explicit step at
281
+ // load: raise the provider and, if a session exists, declare it connected (address, network, balances).
278
282
  //
279
- // WalletConnect держит сессию в localStorage, и EthereumProvider.init() поднимает её обратно.
280
- // Но у нас провайдер создавался ЛЕНИВО - только при нажатии «Connect wallet», - поэтому после F5
281
- // сессия лежала в хранилище, а страница о ней не знала: показывала «Connect wallet» и предлагала
282
- // новый QR. Восстановление делаем явным шагом при загрузке: поднять провайдер и, если сессия есть,
283
- // объявить её подключённой (адрес, сеть, балансы).
284
283
  //
285
- // Состояние читаем ПОСЛЕ init, а не по событию: при восстановлении событие может прийти раньше,
286
- // чем страница успела подписаться на провайдера.
284
+ // State is read AFTER init, not by event: during restoration the event may arrive before
285
+ // the page has subscribed to the provider.
287
286
  let restoreTried = false;
288
287
 
289
- // ПОМЕТКА "ВЫШЕЛ ВРУЧНУЮ". WalletConnect держит сессию в localStorage, поэтому одного disconnect() мало:
290
- // при следующей загрузке restore() поднимет её обратно, и выход окажется фиктивным. Пометку снимаем
291
- // ТОЛЬКО при осознанном подключении.
292
- const OPTOUT_KEY = "arrakis.wallet.optOut";
288
+ // THE "OPTED OUT MANUALLY" MARK. WalletConnect keeps the session in localStorage, so one disconnect() is not
289
+ // enough: on the next load restore() would raise it back and the logout would prove false. The mark is cleared
290
+ // ONLY on a deliberate connect.
291
+ const OPTOUT_KEY = "ninsei.wallet.optOut";
293
292
  const isOptedOut = () => { try { return localStorage.getItem(OPTOUT_KEY) === "1"; } catch { return false; } };
294
293
  const setOptOut = (on) => { try { on ? localStorage.setItem(OPTOUT_KEY, "1") : localStorage.removeItem(OPTOUT_KEY); } catch {} };
295
294
 
296
295
  export async function restore() {
297
- // Вышел вручную - не подхватываем: иначе кнопка "выйти" ничего не значит.
298
- if (isOptedOut()) { restoreTried = true; console.log("[wc] восстановление пропущено: кошелёк отключён вручную"); return publicState(); }
296
+ // Opted out manually - do not pick it up: otherwise the "log out" button means nothing.
297
+ if (isOptedOut()) { restoreTried = true; console.log("[wc] restore skipped: wallet disconnected manually"); return publicState(); }
299
298
  if (restoreTried || state.status === "connected") return publicState();
300
299
  restoreTried = true;
301
- // Порядок: сначала сессия WalletConnect (лежит в localStorage), затем браузерный кошелёк.
302
- // Второй шаг тихий: eth_accounts не показывает окно, поэтому загрузка страницы не превращается
303
- // в «подключите кошелёк» для тех, кто уже подключил его раньше.
300
+ // Order: first the WalletConnect session (in localStorage), then a browser wallet.
301
+ // The second step is silent: eth_accounts shows no window, so page load does not turn into
302
+ // "connect your wallet" for those who already connected before.
304
303
  if (EVM.projectId) {
305
304
  try {
306
305
  const want = chainById(state.chainId || undefined);
307
306
  const p = await ensureProvider(want.chainId);
308
307
  const accounts = (p && p.accounts) || [];
309
- // Пусто - сессии в хранилище нет (или она была отключена). Это нормальное состояние: ниже
310
- // пробуем браузерный кошелёк и, если и его нет, страница предложит подключиться.
308
+ // Empty - there is no session in storage (or it was disconnected). This is normal: below we
309
+ // try a browser wallet and, if there is none either, the page will offer to connect.
311
310
  if (accounts.length) {
312
311
  state.kind = "walletconnect";
313
312
  state.label = "WalletConnect";
@@ -317,17 +316,17 @@ export async function restore() {
317
316
  state.error = null;
318
317
  emit();
319
318
  await refreshBalances();
320
- console.info("WalletConnect: сессия восстановлена после перезагрузки", state.address);
319
+ console.info("WalletConnect: session restored after reload", state.address);
321
320
  }
322
321
  } catch (e) {
323
- console.warn("WalletConnect: восстановить сессию не удалось", e && e.message ? e.message : e);
322
+ console.warn("WalletConnect: could not restore the session", e && e.message ? e.message : e);
324
323
  }
325
324
  }
326
325
  if (!isConnected()) return restoreInjected();
327
326
  return publicState();
328
327
  }
329
328
 
330
- // Тихая проверка браузерных кошельков: тот, кто уже разрешён сайту, возвращает адрес без окна.
329
+ // A silent check for browser wallets: one already allowed for the site returns an address without a window.
331
330
  export async function restoreInjected({ wallets } = {}) {
332
331
  if (isConnected()) return publicState();
333
332
  const list = wallets || (await discoverWallets());
@@ -348,17 +347,17 @@ export async function restoreInjected({ wallets } = {}) {
348
347
  state.error = null;
349
348
  emit();
350
349
  await refreshBalances();
351
- console.info("Браузерный кошелёк восстановлен без запроса: " + state.address);
350
+ console.info("Browser wallet restored without a prompt: " + state.address);
352
351
  return publicState();
353
352
  }
354
353
  return publicState();
355
354
  }
356
355
 
357
- // Подключение браузерного кошелька: здесь кошелёк покажет окно разрешения.
356
+ // Connecting a browser wallet: here the wallet shows a permission window.
358
357
  export async function connectInjected({ wallet, chainSlug } = {}) {
359
- setOptOut(false); // осознанное подключение снимает пометку
358
+ setOptOut(false); // a deliberate connect clears the mark
360
359
  if (!wallet || !wallet.provider) {
361
- throw new Error("Расширение кошелька не найдено в этом браузере");
360
+ throw new Error("No wallet extension found in this browser");
362
361
  }
363
362
  if (isConnected() && state.kind === "injected" && state.label === (wallet.name || null)) {
364
363
  return { address: state.address, chainId: state.chainId };
@@ -373,35 +372,35 @@ export async function connectInjected({ wallet, chainSlug } = {}) {
373
372
  wireProvider(provider);
374
373
  try {
375
374
  const res = await injectedHandshake(provider);
376
- if (!res.address) throw new Error("Кошелёк не вернул ни одного адреса");
375
+ if (!res.address) throw new Error("The wallet returned no address");
377
376
  state.address = res.address;
378
377
  state.chainId = res.chainId || want.chainId;
379
378
  state.status = "connected";
380
379
  state.error = null;
381
380
  emit();
382
- // Сеть приводим к выбранной в форме: пользователь мог подключиться в другой сети.
381
+ // The network is brought to the one selected in the form: the user may have connected on another network.
383
382
  if (state.chainId !== want.chainId) await switchChain(want);
384
383
  await refreshBalances();
385
384
  return { address: state.address, chainId: state.chainId };
386
385
  } catch (e) {
387
- const label = state.label || wallet.name || "Кошелёк";
386
+ const label = state.label || wallet.name || "Wallet";
388
387
  const message = explainInjectedError(e, label);
389
388
  if (isUserRejection(e)) {
390
- console.info("[wallet] отказ пользователя: " + message);
389
+ console.info("[wallet] user refusal: " + message);
391
390
  reset("idle", null);
392
391
  return null;
393
392
  }
394
- console.warn("[wallet] подключение не состоялось: " + message, e);
393
+ console.warn("[wallet] connect did not happen: " + message, e);
395
394
  reset("error", message);
396
395
  throw new Error(message);
397
396
  }
398
397
  }
399
398
 
400
- // Подключение. chainSlug - сеть, выбранная в форме; она же становится основной в сессии.
399
+ // Connect. chainSlug - the network selected in the form; it also becomes the primary in the session.
401
400
  //
402
- // WalletConnect здесь НЕ стартует сразу: сначала показывается выбор - браузерные кошельки, найденные
403
- // по EIP-6963, и WalletConnect отдельной строкой. Так пользователю с расширением не создаётся сессия
404
- // WalletConnect, которую он не просил, а с телефона не приходится искать расширение.
401
+ // WalletConnect does NOT start immediately here: first a choice is shown - browser wallets found via EIP-6963
402
+ // and WalletConnect as a separate row. So a user with an extension is not given a WalletConnect session he did
403
+ // not ask for, and a phone user does not have to hunt for an extension.
405
404
  export async function connect({ chainSlug } = {}) {
406
405
  if (isConnected()) return { address: state.address, chainId: state.chainId };
407
406
  const want = chainById(chainSlug || state.chainId || undefined);
@@ -411,16 +410,16 @@ export async function connect({ chainSlug } = {}) {
411
410
  emit();
412
411
 
413
412
  const wallets = await discoverWallets();
414
- // Журнал обнаружения: первое, что нужно при «кошелёк не открывается» - кого мы вообще нашли.
415
- // У старых расширений (без EIP-6963) источник legacy: имя взято по флагам, и провайдер может
416
- // принадлежать другому расширению - тогда окно открывает не тот кошелёк, на который нажали.
413
+ // A discovery log: the first thing needed for "the wallet does not open" is who we actually found.
414
+ // For old extensions (no EIP-6963) the source is legacy: the name is taken from flags, and the provider may
415
+ // belong to another extension - then the window opens a different wallet than the one clicked.
417
416
  if (wallets.length) {
418
417
  console.info(
419
- "[wallet] обнаружены в браузере: " +
418
+ "[wallet] found in the browser: " +
420
419
  wallets.map((w) => w.name + " [" + w.source + (w.rdns ? " " + w.rdns : "") + "]").join(", ")
421
420
  );
422
421
  } else {
423
- console.info("[wallet] браузерных кошельков не найдено (остаётся WalletConnect)");
422
+ console.info("[wallet] no browser wallets found (WalletConnect remains)");
424
423
  }
425
424
  modal = openWalletModal({
426
425
  title: "Connect a wallet",
@@ -429,14 +428,14 @@ export async function connect({ chainSlug } = {}) {
429
428
  });
430
429
 
431
430
  const choice = await new Promise((resolve) => {
432
- // Отмена (крестик, клик по фону, Cancel) - это не ошибка приложения, а решение пользователя.
431
+ // Cancel (the cross, a click on the backdrop, Cancel) is the user's decision, not an app error.
433
432
  modal.setOnCancel(() => resolve({ type: "cancel" }));
434
433
  const options = wallets.map((w) => ({
435
434
  key: w.rdns || w.name,
436
435
  name: w.name,
437
436
  icon: w.icon,
438
- // Честная пометка: кошелёк, найденный через window.ethereum, мы называем по его флагам, но
439
- // подтвердить, что это именно он, не можем - поэтому и подпись соответствующая.
437
+ // An honest mark: a wallet found through window.ethereum is named by its flags, but we cannot
438
+ // confirm it is exactly that one - hence the matching label.
440
439
  hint: w.source === "eip6963" ? null : "detected via window.ethereum",
441
440
  onClick: () => resolve({ type: "injected", wallet: w }),
442
441
  }));
@@ -462,15 +461,15 @@ export async function connect({ chainSlug } = {}) {
462
461
  return null;
463
462
  }
464
463
 
465
- // --- браузерный кошелёк: окно разрешения показывает сам кошелёк ---
464
+ // --- browser wallet: the wallet itself shows the permission window ---
466
465
  if (choice.type === "injected") {
467
466
  try {
468
467
  const res = await connectInjected({ wallet: choice.wallet, chainSlug: want.id });
469
468
  if (modal) modal.close();
470
469
  return res;
471
470
  } catch (e) {
472
- // Модалку НЕ закрываем: причина отказа должна остаться на экране. Иначе получается
473
- // «нажал - ничего не произошло», и понять, что случилось, неоткуда (ловится проверкой).
471
+ // The modal is NOT closed: the refusal cause must stay on screen. Otherwise it is
472
+ // "pressed - nothing happened", and there is nowhere to learn what happened (a check catches this).
474
473
  const message = String((e && e.message) || e);
475
474
  if (modal) {
476
475
  modal.update({
@@ -482,23 +481,23 @@ export async function connect({ chainSlug } = {}) {
482
481
  }
483
482
  }
484
483
 
485
- // --- WalletConnect: прежний путь, включая мгновенную отмену ---
484
+ // --- WalletConnect: the previous path, including instant cancel ---
486
485
  if (!EVM.projectId) {
487
486
  if (modal) modal.close();
488
487
  reset("error", "no-project-id");
489
488
  throw new Error("WalletConnect projectId is not configured");
490
489
  }
491
490
  modal.update({ status: "Preparing the session..." });
492
- // Отмена должна НЕМЕДЛЕННО завершить подключение. Без этого await provider.connect()
493
- // висит до таймаута сессии, кнопка "Connect wallet" остаётся disabled, и демка выглядит
494
- // сломанной (баг поймался проверкой в браузере: после отмены кнопка не оживала).
491
+ // Cancel must finish the connect IMMEDIATELY. Without this, await provider.connect() hangs until the
492
+ // session timeout, the "Connect wallet" button stays disabled, and the demo looks broken (the bug was
493
+ // caught by a browser check: after cancel the button did not revive).
495
494
  let cancelReject = null;
496
495
  const cancelled = new Promise((_, reject) => {
497
496
  cancelReject = () => reject(new Error("Connection cancelled"));
498
497
  });
499
498
 
500
499
  modal.setOnCancel(() => {
501
- // Пользователь закрыл модалку: сессию надо погасить, иначе она останется висеть.
500
+ // The user closed the modal: the session must be extinguished, else it hangs.
502
501
  const p = provider;
503
502
  provider = null;
504
503
  if (p) Promise.resolve(p.disconnect()).catch(() => {});
@@ -515,7 +514,7 @@ export async function connect({ chainSlug } = {}) {
515
514
  state.chainId = Number(p.chainId);
516
515
  state.status = "connected";
517
516
  state.error = null;
518
- // Именно close(), а не cancel(): подключение состоялось, гасить сессию не нужно.
517
+ // Exactly close(), not cancel(): the connection happened, the session must not be extinguished.
519
518
  if (modal) modal.close();
520
519
  emit();
521
520
  await refreshBalances();
@@ -523,7 +522,7 @@ export async function connect({ chainSlug } = {}) {
523
522
  } catch (e) {
524
523
  const message = e && e.message ? String(e.message) : String(e);
525
524
  console.warn("WalletConnect connect failed:", message);
526
- // Отказ пользователя - это не ошибка приложения.
525
+ // The user's refusal is not an app error.
527
526
  const rejected = isUserRejection(e) || /cancell?ed/i.test(message);
528
527
  if (modal) modal.close();
529
528
  reset(rejected ? "idle" : "error", rejected ? null : message);
@@ -532,50 +531,50 @@ export async function connect({ chainSlug } = {}) {
532
531
  }
533
532
  }
534
533
 
535
- // ПОДПИСЬ СООБЩЕНИЯ кошельком (EIP-191). Это НЕ транзакция: она ничего не двигает, не стоит газа и не
536
- // может быть предъявлена контракту - она доказывает ровно одно: владение адресом. На этом держится вход
537
- // в свой список сделок, потому что подключение кошелька само по себе ничего не доказывает: сервер по нему
538
- // не знает, кто на другом конце.
534
+ // SIGNING A MESSAGE with the wallet (EIP-191). This is NOT a transaction: it moves nothing, costs no gas and
535
+ // cannot be presented to a contract - it proves exactly one thing: ownership of the address. Entry into your
536
+ // own deal list rests on this, because connecting a wallet proves nothing by itself: the server cannot tell
537
+ // from it who is on the other end.
539
538
  //
540
- // Метод personal_sign поддержан собранным SDK - проверено поиском по бандлу, а не по памяти.
539
+ // personal_sign is supported by the built SDK - verified by searching the bundle, not from memory.
541
540
  export async function signMessage(message) {
542
541
  if (!provider || state.status !== "connected") {
543
- throw new Error("кошелёк не подключён: подпись невозможна");
542
+ throw new Error("wallet not connected: signing is impossible");
544
543
  }
545
- // СООБЩЕНИЕ ПЕРЕДАЁМ В HEX, А НЕ ТЕКСТОМ. Так требует EIP-191: первым аргументом personal_sign идут
546
- // байты сообщения в hex. MetaMask принимает и текст (поэтому на живом кошельке это годами не всплывало),
547
- // а узел отвечает отказом: "invalid value: string ..., expected a valid hex string". Проверено вызовом:
548
- // текст - отказ -32602, hex - подпись. Порядок аргументов [данные, адрес] - как у MetaMask и WalletConnect.
549
- // Если кошелёк ответит ошибкой, показываем её КАК ЕСТЬ: по тексту сразу видно причину.
544
+ // THE MESSAGE IS PASSED IN HEX, NOT AS TEXT. EIP-191 requires it: the first personal_sign argument is the
545
+ // message bytes in hex. MetaMask accepts text too (which is why this never surfaced on a live wallet for
546
+ // years), while a node refuses: "invalid value: string ..., expected a valid hex string". Verified by a call:
547
+ // text - refusal -32602, hex - a signature. The argument order [data, address] is as in MetaMask and WalletConnect.
548
+ // If the wallet answers with an error, we show it AS IS: the cause is visible from the text.
550
549
  const hex = "0x" + Array.from(new TextEncoder().encode(String(message)), (b) => b.toString(16).padStart(2, "0")).join("");
551
550
  return await provider.request({ method: "personal_sign", params: [hex, state.address] });
552
551
  }
553
552
 
554
- // ПОДПИСЬ TYPED DATA КОШЕЛЬКОМ (EIP-712, eth_signTypedData_v4). ЭТО ТОЖЕ НЕ ТРАНЗАКЦИЯ: подпись не
555
- // двигает деньги и не стоит газа, поэтому её может дать человек БЕЗ своего ETH. Ею вносящий разрешает
556
- // фабрике записать его адрес в ордер (issue #97, часть A/C), а отправить транзакцию за него вправе кто
557
- // угодно (путь createOrderAndFundByDepositor).
553
+ // SIGNING TYPED DATA with the wallet (EIP-712, eth_signTypedData_v4). This is also NOT a transaction: the
554
+ // signature moves no money and costs no gas, so a person WITHOUT their own ETH can give it. With it the
555
+ // depositor lets the factory record his address in the order, and anyone may send the transaction for him
556
+ // (the createOrderAndFundByDepositor path).
558
557
  //
559
- // ЧТО ИМЕННО ПОДПИСЫВАЕТСЯ, РЕШАЕТ ВЫЗЫВАЮЩИЙ: сюда приходит ГОТОВОЕ typed data (www/js/evm/depositor.js),
560
- // а не строка. Собирать форму здесь значило бы завести вторую запись протокола рядом с настоящей.
558
+ // WHAT EXACTLY IS SIGNED IS DECIDED BY THE CALLER: ready typed data arrives here (www/js/evm/depositor.js),
559
+ // not a string. Assembling the shape here would be a second record of the protocol next to the real one.
561
560
  export async function signTypedData(typedData) {
562
561
  if (!provider || state.status !== "connected") {
563
- throw new Error("кошелёк не подключён: подпись typed data невозможна");
562
+ throw new Error("wallet not connected: typed-data signing is impossible");
564
563
  }
565
- if (!typedData || typeof typedData !== "object") throw new Error("signTypedData: не передано typed data");
566
- // Порядок аргументов [адрес, JSON] - как требует EIP-712 и как принимают MetaMask и WalletConnect.
564
+ if (!typedData || typeof typedData !== "object") throw new Error("signTypedData: no typed data passed");
565
+ // The argument order [address, JSON] is as EIP-712 requires and as MetaMask and WalletConnect accept.
567
566
  const signature = await provider.request({
568
567
  method: "eth_signTypedData_v4",
569
568
  params: [state.address, JSON.stringify(typedData)],
570
569
  });
571
570
  if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) {
572
- throw new Error("кошелёк вернул не 65-байтовую подпись typed data: " + String(signature).slice(0, 20));
571
+ throw new Error("the wallet returned a non-65-byte typed-data signature: " + String(signature).slice(0, 20));
573
572
  }
574
573
  return signature;
575
574
  }
576
575
 
577
576
  export async function disconnect() {
578
- setOptOut(true); // запоминаем решение: после перезагрузки кошелёк не подхватится
577
+ setOptOut(true); // remember the decision: after a reload the wallet is not picked up
579
578
  const p = provider;
580
579
  const kind = state.kind;
581
580
  if (!p) {
@@ -584,9 +583,9 @@ export async function disconnect() {
584
583
  }
585
584
  try {
586
585
  if (kind === "injected") {
587
- // У браузерного кошелька нет «сессии» на нашей стороне: мы можем только попросить отозвать
588
- // разрешение (wallet_revokePermissions) и забыть адрес у себя. Честно: следующие
589
- // eth_accounts могут снова вернуть адрес, если кошелёк помнит сайт.
586
+ // A browser wallet has no "session" on our side: we can only ask it to revoke the permission
587
+ // (wallet_revokePermissions) and forget the address locally. Honestly: the next eth_accounts may
588
+ // return the address again if the wallet remembers the site.
590
589
  if (typeof p.revokePermissions === "function") {
591
590
  await p.request({ method: "wallet_revokePermissions", params: [{ eth_accounts: {} }] });
592
591
  }
@@ -599,8 +598,8 @@ export async function disconnect() {
599
598
  reset("idle", null);
600
599
  }
601
600
 
602
- // Смена сети в кошельке. Если сети у пользователя нет - предлагаем добавить её с нашим rpcUrl
603
- // (это параметр кошелька, страница по этому адресу не ходит - CSP не при чём).
601
+ // Switching the network in the wallet. If the user lacks it - we offer to add it with our rpcUrl
602
+ // (this is a wallet parameter; the page does not go to that address - CSP is not involved).
604
603
  export async function switchChain(chainSlugOrId) {
605
604
  const chain = typeof chainSlugOrId === "object" ? chainSlugOrId : chainById(chainSlugOrId);
606
605
  if (!provider || !isConnected()) return false;
@@ -641,90 +640,90 @@ export async function switchChain(chainSlugOrId) {
641
640
  return true;
642
641
  }
643
642
 
644
- // Чтение балансов через провайдер кошелька: нативный - eth_getBalance, ERC-20 - balanceOf
645
- // через eth_call (селектор 0x70a08231), decimals берём из конфига (в них и была ловушка BSC).
646
- // Отправить транзакцию из кошелька ПОЛЬЗОВАТЕЛЯ и вернуть её хеш.
643
+ // Reading balances through the wallet provider: native via eth_getBalance, ERC-20 via balanceOf
644
+ // through eth_call (selector 0x70a08231); decimals come from the config (where the BSC trap was).
645
+ // Send a transaction from the USER's wallet and return its hash.
647
646
  //
648
- // Метод EIP-1193, один для WalletConnect и для браузерных кошельков: и те, и другие отдают
649
- // провайдера с request(). Подпись происходит в кошельке, наш код ключей не касается.
647
+ // An EIP-1193 method, one for WalletConnect and browser wallets: both expose
648
+ // a provider with request(). The signature happens in the wallet, our code never touches keys.
650
649
  //
651
- // Вызов возвращается не сразу: пользователь подтверждает транзакцию в своём кошельке, и до этого
652
- // момента промис висит. Вызывающий код обязан показать это в интерфейсе, иначе шаг выглядит зависшим.
650
+ // The call does not return at once: the user confirms the transaction in their wallet, and until
651
+ // then the promise hangs. The caller must show this in the interface, else the step looks stuck.
653
652
  //
654
- // value передаётся как hex-строка (wei). Для вызова payable-функции эскроу (lock) сумма уходит в value,
655
- // у самой функции аргументов нет; для createOrder у фабрики value не нужен вовсе.
656
- // Прочитать контракт: вызов view-функции без транзакции и без газа.
653
+ // value is passed as a hex string (wei). For calling a payable escrow function (lock) the amount goes into
654
+ // value and the function takes no arguments; for the factory's createOrder value is not needed at all.
655
+ // Read a contract: calling a view function without a transaction and without gas.
657
656
  //
658
- // Нужен, чтобы узнать адрес эскроу ДО его создания (фабрика.predict) и чтобы читать состояние
659
- // (эскроу.status) при опросе. Ключей не требует, кошелёк не спрашивает - только провайдер.
657
+ // Needed to learn the escrow address BEFORE it is created (factory.predict) and to read state
658
+ // (escrow.status) while polling. Requires no keys, asks the wallet nothing - only the provider.
660
659
  export async function readContract({ to, data } = {}) {
661
- if (!provider) throw new Error("кошелёк не подключён: читать контракт нечем");
662
- if (!to) throw new Error("не указан адрес контракта");
663
- if (!data) throw new Error("не указаны данные вызова");
660
+ if (!provider) throw new Error("wallet not connected: nothing to read the contract with");
661
+ if (!to) throw new Error("no contract address given");
662
+ if (!data) throw new Error("no call data given");
664
663
  const result = await provider.request({ method: "eth_call", params: [{ to, data }, "latest"] });
665
664
  if (typeof result !== "string" || !result.startsWith("0x")) {
666
- throw new Error("неожиданный ответ на вызов контракта: " + String(result).slice(0, 60));
665
+ throw new Error("unexpected answer to a contract call: " + String(result).slice(0, 60));
667
666
  }
668
667
  return result;
669
668
  }
670
669
 
671
- // КВИТАНЦИЯ ТРАНЗАКЦИИ. Нужна там, где хеша мало: у создания ордера в её событиях лежит ФАКТИЧЕСКИЙ адрес
672
- // эскроу. Предсказанный адрес - это надежда (он зависит от соли и кода фабрики), а событие - факт.
673
- // Читается тем же провайдером, что и вызовы контракта: второго способа обращаться к цепи не заводим.
674
- // КОД КОНТРАКТА (eth_getCode). Нужен проверке кода фабрики (#103): extcodehash - это keccak256 от этого
675
- // кода, и по нему интерфейс сверяет фабрику со списком известных сборок. eth_call сюда не годится: код
676
- // читается ДРУГИМ методом, поэтому и функция отдельная.
670
+ // THE TRANSACTION RECEIPT. Needed where a hash is not enough: in the order-creation events it carries the
671
+ // ACTUAL escrow address. A predicted address is a hope (it depends on the salt and the factory code), an event is a fact.
672
+ // Read by the same provider as the contract calls: we keep no second way to reach the chain.
673
+ // CONTRACT CODE (eth_getCode). Needed by the factory-code check: extcodehash is keccak256 of this code,
674
+ // and the interface compares the factory against the list of known builds by it. eth_call will not do: code
675
+ // is read by a DIFFERENT method, hence a separate function.
677
676
  export async function readCode({ address } = {}) {
678
- if (!provider) throw new Error("кошелёк не подключён: читать код контракта нечем");
679
- if (!address || !/^0x[0-9a-fA-F]{40}$/.test(String(address))) throw new Error("не адрес для чтения кода: " + String(address));
677
+ if (!provider) throw new Error("wallet not connected: nothing to read the contract code with");
678
+ if (!address || !/^0x[0-9a-fA-F]{40}$/.test(String(address))) throw new Error("not an address for reading code: " + String(address));
680
679
  return await provider.request({ method: "eth_getCode", params: [address, "latest"] });
681
680
  }
682
681
 
683
682
  export async function readReceipt({ hash } = {}) {
684
- if (!provider) throw new Error("кошелёк не подключён: квитанцию читать нечем");
685
- if (!hash) throw new Error("не указан хеш транзакции");
683
+ if (!provider) throw new Error("wallet not connected: nothing to read the receipt with");
684
+ if (!hash) throw new Error("no transaction hash given");
686
685
  const r = await provider.request({ method: "eth_getTransactionReceipt", params: [hash] });
687
- if (!r || typeof r !== "object") return null; // ещё не в блоке - это НЕ ошибка, а ожидание
686
+ if (!r || typeof r !== "object") return null; // not in a block yet - NOT an error but a wait
688
687
  return r;
689
688
  }
690
689
 
691
690
  export async function sendTransaction({ to, data, value = "0x0", gas } = {}) {
692
- if (!provider) throw new Error("кошелёк не подключён: отправлять нечем");
693
- if (!to) throw new Error("не указан адрес контракта");
694
- if (!data) throw new Error("не указаны данные вызова");
691
+ if (!provider) throw new Error("wallet not connected: nothing to send with");
692
+ if (!to) throw new Error("no contract address given");
693
+ if (!data) throw new Error("no call data given");
695
694
  const from = address();
696
- if (!from) throw new Error("кошелёк не подключён: нет адреса отправителя");
695
+ if (!from) throw new Error("wallet not connected: no sender address");
697
696
  const tx = { from, to, data, value: typeof value === "bigint" ? "0x" + value.toString(16) : value };
698
697
  if (gas) tx.gas = typeof gas === "bigint" ? "0x" + gas.toString(16) : gas;
699
- // КОМИССИЮ СЧИТАЕМ САМИ И ОДНИМ ПРАВИЛОМ. Формула (2 x baseFee + чаевые) вынесена в
700
- // www/js/evm/claimGas.js - в тот же модуль, по которому нода считает ПОДАРОК на claim. Второй расчёт
701
- // здесь разошёлся бы с подарком (issue #127). Пол чаевых - политика сети (Arbitrum: 0), не 1 gwei.
698
+ // WE COMPUTE THE FEE OURSELVES WITH ONE RULE. The formula (2 x baseFee + tip) is in
699
+ // www/js/evm/claimGas.js - the same module by which the node computes the GIFT at claim. A second
700
+ // computation here would diverge from the gift. The tip floor is a network policy (Arbitrum: 0), not 1 gwei.
702
701
  try {
703
702
  const latest = await provider.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
704
703
  const baseFee = latest && latest.baseFeePerGas ? BigInt(latest.baseFeePerGas) : 0n;
705
704
  let priorityFeeWei = 0n;
706
705
  try {
707
- // Чаевые берём из eth_gasPrice: этот метод есть у любого провайдера. eth_maxPriorityFeePerGas
708
- // поддерживают не все, и MetaMask отвечает на него ошибкой -32601, которая лезет в консоль пользователя.
706
+ // The tip is taken from eth_gasPrice: every provider has this method. eth_maxPriorityFeePerGas
707
+ // is not supported by all, and MetaMask answers it with error -32601, which lands in the user's console.
709
708
  const gp = await provider.request({ method: "eth_gasPrice", params: [] });
710
709
  if (typeof gp === "string" && /^0x[0-9a-fA-F]+$/.test(gp)) {
711
710
  const diff = BigInt(gp) - baseFee;
712
- if (diff > 0n) priorityFeeWei = diff; // разница gasPrice - baseFee и есть чаевые
711
+ if (diff > 0n) priorityFeeWei = diff; // gasPrice - baseFee is exactly the tip
713
712
  }
714
- } catch { /* не подсказал - чаевые остаются нулевыми */ }
713
+ } catch { /* no hint given - the tip stays zero */ }
715
714
  const maxFee = claimMaxFeePerGasWei({ baseFeeWei: baseFee, priorityFeeWei });
716
- // maxFee === null значит сеть без EIP-1559 (baseFee = 0): поля не выставляем, решает кошелёк.
715
+ // maxFee === null means a network without EIP-1559 (baseFee = 0): fields are not set, the wallet decides.
717
716
  if (maxFee !== null) {
718
717
  tx.maxPriorityFeePerGas = "0x" + (maxFee - baseFee * 2n).toString(16);
719
718
  tx.maxFeePerGas = "0x" + maxFee.toString(16);
720
719
  }
721
720
  } catch (feeErr) {
722
- // Не смогли посчитать - отдаём решение кошельку, как раньше. Молча падать из-за этого нельзя.
723
- console.warn("не удалось посчитать комиссию, решает кошелёк: " + ((feeErr && feeErr.message) || feeErr));
721
+ // Could not compute - leave the decision to the wallet, as before. Failing silently over this is not allowed.
722
+ console.warn("could not compute the fee, the wallet decides: " + ((feeErr && feeErr.message) || feeErr));
724
723
  }
725
724
  const hash = await provider.request({ method: "eth_sendTransaction", params: [tx] });
726
725
  if (typeof hash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(hash)) {
727
- throw new Error("кошелёк вернул не хеш транзакции: " + String(hash).slice(0, 60));
726
+ throw new Error("the wallet returned not a transaction hash: " + String(hash).slice(0, 60));
728
727
  }
729
728
  return hash;
730
729
  }