@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,50 +1,38 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/chainSource.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
- // Источник данных о Monero-ноге свопа: симулятор демки или настоящий бэкенд (app/).
7
- //
8
- // Почему отдельный модуль, а не строчки в core/swap.js: у свопа две ноги, и готовность у них разная.
9
- //
10
- // EVM-нога (escrow, claim, refund) - СИМУЛЯЦИЯ: контракта ещё нет (этап 5). Каждый её ответ
11
- // помечен mock: true, и это не косметика, а то, что обязан показать UI.
12
- //
13
- // Monero-нога (пришёл ли XMR на адрес свопа и сколько у него подтверждений) - может быть
14
- // НАСТОЯЩЕЙ: бэкенд app/ заводит watch-only кошелёк по адресу и view key и следит за
15
- // поступлениями (см. app/README.md). Ключ траты при этом остаётся в браузере.
16
- //
17
- // Поэтому источник выбирается именно для Monero-ноги: 'mock' (как в демо: sim-время, без сети)
18
- // либо 'app' (HTTP к бэкенду). За источником идёт и сеть: у бэкенда на сервере поднят
19
- // stagenet-кошелёк, а мок-режим демки исторически mainnet (см. XMR_NETWORKS в config.js).
20
- //
21
- // ГРАНИЦА, ЗАКРЕПЛЁННАЯ КОДОМ: в бэкенд уходит только адрес и view key, и тело запроса
22
- // собирается заново по белому списку полей - так в него физически не может уехать ни ключ траты,
23
- // ни seed, даже если кто-то добавит их в share. Проверяется tools/check-chain-source.mjs.
5
+ // DATA SOURCE FOR THE MONERO LEG OF THE SWAP: the demo simulator or the real backend.
6
+ // TWO LEGS, DIFFERENT READINESS. The EVM leg (escrow, claim, refund) is a SIMULATION (no contract yet): every
7
+ // reply is marked mock: true and the UI must show it. The Monero leg (has XMR arrived, how many confirmations)
8
+ // can be REAL: the backend tracks a watch-only wallet by address and view key, while the spend key stays in the browser.
9
+ // So the source is chosen for the Monero leg: 'mock' (sim-time, no network) or 'app' (HTTP to the backend).
10
+ // A BOUNDARY PINNED BY CODE: only the address and view key go to the backend, and the request body is rebuilt from
11
+ // a field allowlist, so neither the spend key nor the seed can leave.
24
12
 
25
13
  import { API, XMR_NETWORKS, MONERO } from "./config.js";
26
14
  import { confirmations as mockConfirmations } from "../mock/chain.js";
27
15
 
28
- // Статусы бэкенда (app/store.mjs) - те же строки, что отдаёт API.
16
+ // Backend statuses - the same strings the API returns.
29
17
  export const XMR_STATUS = {
30
- AWAITING: "awaiting_funding", // на адрес ещё ничего не пришло (или пришло меньше ожидаемого)
31
- FUNDED: "funded", // пришло, подтверждений меньше цели
32
- // ДЕНЬГИ НА АДРЕСЕ ЕСТЬ, НО ЗАПЕРТЫ unlock_time (issue #84): это НЕ приход, и «готово» в этом состоянии не
33
- // включается. Отдельный статус, а не молчание и не ноль: бэкенд (app/watcher.mjs) называет его сам.
18
+ AWAITING: "awaiting_funding", // nothing has arrived at the address yet (or less than expected)
19
+ FUNDED: "funded", // arrived, confirmations below target
20
+ // MONEY ON THE ADDRESS BUT LOCKED by unlock_time: NOT an arrival, and 'ready' does not turn on.
21
+ // A separate status, named by the backend itself.
34
22
  LOCKED: "xmr_locked",
35
- READY: "ready", // подтверждений достаточно: пользователь может подтверждать
36
- SWEPT: "swept", // XMR выведены пользователем
37
- CLOSED: "closed", // своп закрыт (отмена или возврат)
23
+ READY: "ready", // enough confirmations: the user can confirm readiness
24
+ SWEPT: "swept", // XMR withdrawn by the user
25
+ CLOSED: "closed", // swap closed (cancelled or refunded)
38
26
  };
39
27
 
40
- // Поля, которые вообще разрешено отправлять бэкенду при заведении свопа.
28
+ // The only fields allowed to be sent to the backend when a swap is created.
41
29
  export const ALLOWED_SESSION_FIELDS = ["swapId", "network", "address", "viewKey", "expectedAmountXmr", "restoreHeight", "chain"];
42
30
 
43
31
  export function isLiveSource(source = API.xmrSource) {
44
32
  return source === "app";
45
33
  }
46
34
 
47
- // Сеть Monero, которую обязан использовать адрес свопа при данном источнике.
35
+ // The Monero network the swap address must use for this source.
48
36
  export function xmrNetworkForSource(source = API.xmrSource) {
49
37
  return XMR_NETWORKS[isLiveSource(source) ? "app" : "mock"];
50
38
  }
@@ -54,18 +42,17 @@ export function xmrSourceLabel(source = API.xmrSource) {
54
42
  }
55
43
 
56
44
  // ---------------------------------------------------------------------------
57
- // HTTP к бэкенду
45
+ // HTTP to the backend
58
46
  //
59
- // Базу можно переопределить. В браузере это не нужно: "/api" - относительный путь того же
60
- // origin, и CSP ("connect-src self") его покрывает. Переопределение существует для Node-тестов
61
- // и для случая, когда фронт отдают не с боевого хоста (в обоих случаях относительный URL не парсится).
47
+ // The base can be overridden. In the browser this is not needed: "/api" is a relative path of the same origin
48
+ // and the CSP (connect-src self) covers it. The override exists for Node tests and for a non-production host.
62
49
  let apiBaseOverride = null;
63
50
  export function setApiBase(base) {
64
51
  apiBaseOverride = base ? String(base).replace(/[/]$/, "") : null;
65
52
  }
66
53
 
67
- // Настройки опроса, которые можно переопределить: нужно тестам (там интервал в миллисекундах,
68
- // а не в секундах) и настройке экрана. Значения по умолчанию - из config.js.
54
+ // Polling settings that can be overridden: needed by tests (interval in milliseconds, not seconds) and by the
55
+ // screen. Defaults come from config.
69
56
  let pollMsOverride = null;
70
57
  export function setApiOptions({ pollMs } = {}) {
71
58
  if (Number.isFinite(Number(pollMs)) && Number(pollMs) > 0) pollMsOverride = Number(pollMs);
@@ -76,12 +63,12 @@ export function pollInterval() {
76
63
  }
77
64
  // ---------------------------------------------------------------------------
78
65
 
79
- // Один запрос к API. НИКОГДА не бросает: упавшая сеть - это состояние экрана, а не падение демки,
80
- // и пользователь должен увидеть причину, а не пустую страницу.
66
+ // One API request. It NEVER throws: a failed network is a screen state, not a demo crash, and the user must
67
+ // see the reason, not a blank page.
81
68
  async function api(path, { method = "GET", body, base, timeoutMs = API.timeoutMs, fetchImpl } = {}) {
82
69
  const target = base || apiBaseOverride || API.base;
83
70
  const doFetch = fetchImpl || globalThis.fetch;
84
- if (typeof doFetch !== "function") return { ok: false, status: 0, error: "нет fetch: бэкенд недоступен" };
71
+ if (typeof doFetch !== "function") return { ok: false, status: 0, error: "no fetch: backend unavailable" };
85
72
  const ctl = typeof AbortController === "function" ? new AbortController() : null;
86
73
  const timer = ctl ? setTimeout(() => ctl.abort(), timeoutMs) : null;
87
74
  try {
@@ -101,7 +88,7 @@ async function api(path, { method = "GET", body, base, timeoutMs = API.timeoutMs
101
88
  try {
102
89
  json = text ? JSON.parse(text) : null;
103
90
  } catch {
104
- /* не JSON - причину увидим из текста */
91
+ /* not JSON - the reason will be visible from the text */
105
92
  }
106
93
  if (!res.ok) {
107
94
  const base_ = (json && (json.error || json.detail)) || text.slice(0, 200) || `HTTP ${res.status}`;
@@ -114,14 +101,14 @@ async function api(path, { method = "GET", body, base, timeoutMs = API.timeoutMs
114
101
  return {
115
102
  ok: false,
116
103
  status: 0,
117
- error: aborted ? `бэкенд не ответил за ${timeoutMs} мс` : "бэкенд недоступен: " + ((e && e.message) || e),
104
+ error: aborted ? `backend did not answer within ${timeoutMs} ms` : "backend unavailable: " + ((e && e.message) || e),
118
105
  };
119
106
  } finally {
120
107
  if (timer) clearTimeout(timer);
121
108
  }
122
109
  }
123
110
 
124
- // Ответ бэкенда -> слепок для экрана и движка состояний.
111
+ // Backend response -> a snapshot for the screen and the state engine.
125
112
  export function normalizeSession(json) {
126
113
  const j = json || {};
127
114
  return {
@@ -132,7 +119,7 @@ export function normalizeSession(json) {
132
119
  expectedAmountXmr: j.expectedAmountXmr ?? null,
133
120
  receivedXmr: j.receivedXmr ?? "0",
134
121
  receivedAtomic: j.receivedAtomic ?? "0",
135
- // ЗАПЕРТОЕ unlock_time: сумма и граница ({code,untilHeight,untilTime}) - отдельно от прихода.
122
+ // LOCKED unlock_time: the sum and the boundary ({code,untilHeight,untilTime}) - separate from the arrival.
136
123
  lockedXmr: j.lockedXmr ?? "0",
137
124
  lockedAtomic: j.lockedAtomic ?? "0",
138
125
  lockedUntil: j.lockedUntil || null,
@@ -145,10 +132,9 @@ export function normalizeSession(json) {
145
132
  };
146
133
  }
147
134
 
148
- // Заведение свопа на бэкенде. share - то, что возвращает watchOnlyShareFromWallet()
149
- // (recovery/restore.js): адрес + приватный view key. Ключ траты остаётся в браузере.
150
- // Одна и та же ли цель у свопа: сумма, валюта и сеть. Нужно, чтобы клик по Swap не заводил новый своп
151
- // на тот же адрес, если человек ничего не менял. Чистая функция - проверяется тестом без сети.
135
+ // Creating a swap on the backend. share is what watchOnlyShareFromWallet() returns: address + private view key.
136
+ // The spend key stays in the browser. The same intent (sum, currency, network) must not create a new swap on
137
+ // the same address. A pure function - checked without a network.
152
138
  export function sameSwapTarget(a, b) {
153
139
  if (!a || !b) return false;
154
140
  return String(a.payAmount) === String(b.payAmount) &&
@@ -161,17 +147,16 @@ export async function openSession({ swapId, share, expectedAmountXmr, chain, res
161
147
  const address = src.address || null;
162
148
  const viewKey = src.viewKey || src.privateViewKey || null;
163
149
  if (!address || !viewKey) {
164
- return { ok: false, status: 0, error: "в watch-only share нет address/viewKey - нечего отправлять бэкенду" };
150
+ return { ok: false, status: 0, error: "the watch-only share has no address/viewKey - nothing to send to the backend" };
165
151
  }
166
- // СУММА ОЖИДАНИЯ ОБЯЗАТЕЛЬНА и должна быть числом. Раньше здесь стояло «не число - значит поля не будет»,
167
- // и своп заводился БЕЗ суммы: сторож принимал за достаточное любое поступление, то есть недоплата
168
- // выглядела как исполненная сделка. Живой своп так и остался с пустым полем. Теперь это отказ с
169
- // объяснением: не завести наблюдение лучше, чем завести его без возможности проверить деньги.
152
+ // THE EXPECTED AMOUNT IS MANDATORY and must be a number. Before, a non-number meant no field, and the swap
153
+ // was created WITHOUT an amount: any arrival was accepted as enough, so an underpayment looked like a done
154
+ // deal. Now it is a refusal with an explanation.
170
155
  const amountXmr = Number(expectedAmountXmr);
171
156
  if (!Number.isFinite(amountXmr) || amountXmr <= 0) {
172
157
  const where = (new Error("caller").stack || "").split("\n").slice(1, 4).map((x) => x.trim().split("/").pop()).join(" <- ");
173
- console.warn("[chainSource] нет суммы ожидания, вызов из: " + where);
174
- return { ok: false, status: 0, error: "сумма ожидания XMR неизвестна (" + String(expectedAmountXmr) + "): без неё наблюдение не заводится" };
158
+ console.warn("[chainSource] no expected amount, call from: " + where);
159
+ return { ok: false, status: 0, error: "the expected XMR amount is unknown (" + String(expectedAmountXmr) + "): without it no watch is created" };
175
160
  }
176
161
  const wanted = {
177
162
  swapId: swapId || src.swapId || null,
@@ -182,7 +167,7 @@ export async function openSession({ swapId, share, expectedAmountXmr, chain, res
182
167
  restoreHeight: Number.isFinite(Number(restoreHeight)) && Number(restoreHeight) > 0 ? Number(restoreHeight) : Number(src.restoreHeight) || undefined,
183
168
  chain: chain || undefined,
184
169
  };
185
- // Тело собираем заново по белому списку: что не в списке - в запрос не попадёт никогда.
170
+ // The body is rebuilt from the allowlist: what is not in the list never reaches the request.
186
171
  const body = {};
187
172
  for (const field of ALLOWED_SESSION_FIELDS) {
188
173
  if (wanted[field] !== undefined && wanted[field] !== null) body[field] = wanted[field];
@@ -198,31 +183,31 @@ export async function fetchSession(swapId, opts = {}) {
198
183
  return { ok: true, status: res.status, session: normalizeSession(res.json) };
199
184
  }
200
185
 
201
- // Сообщить бэкенду, что XMR выведены: хеш настоящего перевода (со страницы sweep).
202
- // Нужно потому, что watch-only кошелёк исходящие переводы видеть не может в принципе.
186
+ // Tell the backend the XMR was withdrawn (the hash of the real transfer). Needed because a watch-only wallet
187
+ // cannot see outgoing transfers.
203
188
  export async function reportSwept(swapId, txid, opts = {}) {
204
189
  const res = await api(`/swaps/${encodeURIComponent(swapId)}/swept`, { method: "POST", body: { txid }, ...opts });
205
190
  return res.ok ? { ok: true, status: res.status, session: normalizeSession(res.json) } : res;
206
191
  }
207
192
 
208
- // Закрыть своп (отмена или возврат на стороне бэкенда): наблюдение прекращается.
193
+ // Close the swap (cancel or refund on the backend): the watch stops.
209
194
  export async function closeSession(swapId, reason = "cancelled", opts = {}) {
210
195
  const res = await api(`/swaps/${encodeURIComponent(swapId)}/closed`, { method: "POST", body: { reason }, ...opts });
211
196
  return res.ok ? { ok: true, status: res.status, session: normalizeSession(res.json) } : res;
212
197
  }
213
198
 
214
- // Состояние бэкенда: доступен ли он и видит ли кошелёк. Нужно экрану, чтобы показать «бэкенд
215
- // недоступен» до того, как пользователь отправит деньги, а не после.
199
+ // Backend state: is it up and does it see the wallet. The screen needs this to show 'backend unavailable'
200
+ // before the user sends money, not after.
216
201
  export async function health(opts = {}) {
217
202
  const res = await api("/health", opts);
218
203
  return res.ok ? { ok: true, ...res.json } : res;
219
204
  }
220
205
 
221
206
  // ---------------------------------------------------------------------------
222
- // Слепок Monero-ноги для движка состояний
207
+ // The Monero-leg snapshot for the state engine
223
208
  // ---------------------------------------------------------------------------
224
209
 
225
- // Мок-режим: подтверждения по sim-времени, ровно как в демке было до сих пор.
210
+ // Mock mode: confirmations by sim-time, as the demo always did.
226
211
  export function mockView(swap, simElapsedMs) {
227
212
  const confTarget = (swap.timeline && swap.timeline.confTarget) || MONERO.confirmTarget;
228
213
  const lockAt = swap.timeline ? swap.timeline.lockAtSim : 0;
@@ -238,7 +223,7 @@ export function mockView(swap, simElapsedMs) {
238
223
  live: false,
239
224
  status,
240
225
  received,
241
- // В демо-симуляторе запирания unlock_time нет: приход и разблокированное - одно и то же.
226
+ // The demo simulator has no unlock_time lock: the arrival and the unlocked are the same.
242
227
  unlocked: received,
243
228
  locked: 0,
244
229
  lockedUntil: null,
@@ -252,8 +237,8 @@ export function mockView(swap, simElapsedMs) {
252
237
  };
253
238
  }
254
239
 
255
- // Живой режим: берём последний опрос бэкенда. Его кладёт тикер (swap.js), а derive() остаётся
256
- // чистой функцией - она читает состояние, а не ходит в сеть.
240
+ // Live mode: take the latest backend poll, put there by the ticker; derive() stays a pure function - it reads
241
+ // state, not the network.
257
242
  export function liveView(swap) {
258
243
  const confTarget = (swap.timeline && swap.timeline.confTarget) || MONERO.confirmTarget;
259
244
  const s = swap.xmrSession;
@@ -277,8 +262,8 @@ export function liveView(swap) {
277
262
  return {
278
263
  live: true,
279
264
  status: swap.xmrSessionError ? "error" : s.status,
280
- // ПРИХОД И РАЗБЛОКИРОВАННОЕ - РАЗНЫЕ ФАКТЫ. received - то, что можно потратить (receivedXmr),
281
- // locked - запертое unlock_time, arrived - всё, что видно на адресе. Состояние называет бэкенд.
265
+ // ARRIVAL AND UNLOCKED ARE DIFFERENT FACTS. received - what can be spent (receivedXmr); locked - unlock_time-
266
+ // locked; arrived - everything visible at the address. The backend names the state.
282
267
  received: Number(s.receivedXmr) || 0,
283
268
  unlocked: Number(s.receivedXmr) || 0,
284
269
  locked: Number(s.lockedXmr) || 0,
@@ -297,8 +282,8 @@ export function xmrView(swap, simElapsedMs) {
297
282
  return isLiveSource(swap && swap.xmrSource) ? liveView(swap) : mockView(swap, simElapsedMs);
298
283
  }
299
284
 
300
- // Ссылка на перевод в обозревателе. В live-режиме это stagenet: подписывать её как настоящие
301
- // деньги нельзя, поэтому сеть берём из самой сессии, а не из догадки.
285
+ // A link to the transfer in an explorer. In live mode this is stagenet: it must not be signed as real money, so
286
+ // the network comes from the session.
302
287
  export function explorerXmrTx(txid, network = "stagenet") {
303
288
  if (!txid) return null;
304
289
  const prefix = network === "mainnet" ? "" : network + ".";
@@ -1,17 +1,16 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/clock.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
- // Sim-часы демки.
5
+ // Demo sim-clock.
7
6
  //
8
- // Идея: реальное время сжимается множителем speed. speed=60 означает, что одна
9
- // реальная секунда равна одной sim-минуте. Все тайминги сделки живут в sim-времени,
10
- // поэтому демку можно показывать быстро, а на speed=1 она идёт в реальном темпе
11
- // (20-40 минут на сделку, как в литпепере).
7
+ // Idea: real time is compressed by the speed multiplier. speed=60 means one
8
+ // real second equals one sim-minute. All swap timings live in sim-time,
9
+ // so the demo can be shown fast, while at speed=1 it runs at real pace
10
+ // (20-40 minutes per swap, as in the litepaper).
12
11
  //
13
- // swap.realStart - Date.now() в момент создания сделки
14
- // swap.simStartWall - настенное время демки в этот же момент (для отображения UTC)
12
+ // swap.realStart - Date.now() at the moment the swap was created
13
+ // swap.simStartWall - the demo's wall time at that same moment (for UTC display)
15
14
  // simElapsed(swap) = (Date.now() - realStart) * speed
16
15
 
17
16
  export function nowReal() {
@@ -22,15 +21,15 @@ export function simElapsed(swap, now = nowReal()) {
22
21
  return Math.max(0, (now - swap.realStart) * swap.speed);
23
22
  }
24
23
 
25
- // Настенное время демки в момент, когда прошло simMs sim-времени с начала сделки.
24
+ // The demo's wall time at the moment when simMs of sim-time has passed since the swap started.
26
25
  export function simWall(swap, simMs) {
27
26
  return swap.simStartWall + simMs;
28
27
  }
29
28
 
30
29
 
31
30
 
32
- // Пересчёт базовой точки при смене скорости: sim-прогресс сохраняется,
33
- // дальше сделка идёт с новой скоростью.
31
+ // Rescale the base point when the speed changes: sim-progress is preserved,
32
+ // after that the swap runs at the new speed.
34
33
  export function rescaleStart(swap, newSpeed, now = nowReal()) {
35
34
  const elapsed = simElapsed(swap, now);
36
35
  swap.speed = newSpeed;