@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.
- package/CHANGELOG.md +35 -0
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +1 -1
- package/src/swaps.mjs +1 -1
package/engine/evm/auth.js
CHANGED
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/auth.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// WALLET-SIGNATURE LOGIN AND THE SERVER-SIDE SWAP LIST.
|
|
6
|
+
// Why: while swap records live only in localStorage, "my list" is tied to the browser. A server-side list
|
|
7
|
+
// fixes that; binding it to the wallet address keeps other people's swaps hidden.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// список снимает это ограничение, а привязка к адресу кошелька не даёт видеть чужое.
|
|
9
|
+
// A SIGNATURE, NOT JUST A CONNECTION. Connecting is only a channel. Proving you own the address needs a
|
|
10
|
+
// message signature, re-done every time: the pass lives in sessionStorage and dies with the tab.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// sessionStorage, то есть умирает вместе с вкладкой. Так решено сознательно: безопасность важнее удобства.
|
|
12
|
+
// WHAT GOES TO THE SERVER AND WHAT DOES NOT. Only public fields: escrow address, amounts, deadlines,
|
|
13
|
+
// hashlock, network, swap id. THE SECRET NEVER GOES: it grants the money and stays in the recovery file.
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// Сервер не сможет распорядиться средствами, даже если захочет, - у него нет ни ключей, ни секрета.
|
|
19
|
-
//
|
|
20
|
-
// ОТКАЗ СЕРВЕРА НЕ ЛОМАЕТ СДЕЛКУ. Синхронизация - удобство, а не условие: если она не удалась, сделка
|
|
21
|
-
// продолжается на локальной записи, и об этом честно сказано в консоли. Хуже было бы наоборот.
|
|
15
|
+
// A SERVER REFUSAL DOES NOT BREAK THE SWAP: sync is a convenience, and on failure the swap continues on
|
|
16
|
+
// the local record (said honestly in the console).
|
|
22
17
|
import { signMessage, address, isConnected } from "./session.js";
|
|
23
18
|
|
|
24
|
-
const TOKEN_KEY = "
|
|
25
|
-
const ADDR_KEY = "
|
|
19
|
+
const TOKEN_KEY = "ninsei.auth.token";
|
|
20
|
+
const ADDR_KEY = "ninsei.auth.address";
|
|
26
21
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
22
|
+
// The pass and the address live in sessionStorage: it survives a reload but dies with the tab - exactly the
|
|
23
|
+
// chosen behaviour ("valid while the browser is open"). SESSION STORAGE COMES FROM OUTSIDE, like the state
|
|
24
|
+
// store (www/js/core/store.js): sessionStorage in the browser, memory in the checks and the SDK - the module
|
|
25
|
+
// must load without a DOM.
|
|
31
26
|
const sessionAdapters = () => {
|
|
32
27
|
const store = typeof globalThis !== "undefined" ? globalThis.sessionStorage : undefined;
|
|
33
28
|
if (!store || typeof store.getItem !== "function") return null;
|
|
@@ -43,17 +38,17 @@ const memoryStore = () => {
|
|
|
43
38
|
};
|
|
44
39
|
let sessionStore = sessionAdapters() || memoryStore();
|
|
45
40
|
|
|
46
|
-
/**
|
|
41
|
+
/** Swap the session storage: sessionStorage, memory or an SDK adapter all qualify. */
|
|
47
42
|
export function setSessionStore(next) {
|
|
48
43
|
const ok = next && typeof next.get === "function" && typeof next.set === "function" && typeof next.remove === "function";
|
|
49
|
-
if (!ok) throw new Error("
|
|
44
|
+
if (!ok) throw new Error("session storage must support get/set/remove");
|
|
50
45
|
sessionStore = next;
|
|
51
46
|
return sessionStore;
|
|
52
47
|
}
|
|
53
48
|
export const sessionStoreInUse = () => sessionStore;
|
|
54
49
|
|
|
55
50
|
function readSession(key) { try { return sessionStore.get(key); } catch { return null; } }
|
|
56
|
-
function writeSession(key, value) { try { value == null ? sessionStore.remove(key) : sessionStore.set(key, value); } catch { /*
|
|
51
|
+
function writeSession(key, value) { try { value == null ? sessionStore.remove(key) : sessionStore.set(key, value); } catch { /* silence on purpose: an unavailable store must not break login */ } }
|
|
57
52
|
|
|
58
53
|
export function token() { return readSession(TOKEN_KEY); }
|
|
59
54
|
export function authedAddress() { return readSession(ADDR_KEY); }
|
|
@@ -62,17 +57,17 @@ export function forgetSession() { writeSession(TOKEN_KEY, null); writeSession(AD
|
|
|
62
57
|
async function api(path, init = {}) {
|
|
63
58
|
const res = await fetch(path, init);
|
|
64
59
|
let body = null;
|
|
65
|
-
try { body = await res.json(); } catch { /*
|
|
60
|
+
try { body = await res.json(); } catch { /* not JSON - returned as is below */ }
|
|
66
61
|
return { status: res.status, body };
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
64
|
+
// Login: get a one-time number, sign the message GIVEN BY THE SERVER (not our own: a one-char mismatch
|
|
65
|
+
// would give "signature does not match" with no explanation) and exchange the signature for a pass.
|
|
71
66
|
export async function signIn() {
|
|
72
|
-
if (!isConnected()) throw new Error("
|
|
67
|
+
if (!isConnected()) throw new Error("connect the wallet first");
|
|
73
68
|
const nonce = await api("/api/auth/nonce");
|
|
74
69
|
if (nonce.status !== 200 || !nonce.body || !nonce.body.message) {
|
|
75
|
-
throw new Error("
|
|
70
|
+
throw new Error("the server did not issue a message to sign (response " + nonce.status + ")");
|
|
76
71
|
}
|
|
77
72
|
const signature = await signMessage(nonce.body.message);
|
|
78
73
|
const verified = await api("/api/auth/verify", {
|
|
@@ -81,17 +76,16 @@ export async function signIn() {
|
|
|
81
76
|
body: JSON.stringify({ nonce: nonce.body.nonce, address: address(), signature }),
|
|
82
77
|
});
|
|
83
78
|
if (verified.status !== 200 || !verified.body || !verified.body.token) {
|
|
84
|
-
const why = verified.body && verified.body.error ? verified.body.error : "
|
|
85
|
-
throw new Error("
|
|
79
|
+
const why = verified.body && verified.body.error ? verified.body.error : "response " + verified.status;
|
|
80
|
+
throw new Error("login not accepted: " + why);
|
|
86
81
|
}
|
|
87
82
|
writeSession(TOKEN_KEY, verified.body.token);
|
|
88
83
|
writeSession(ADDR_KEY, verified.body.address);
|
|
89
84
|
return verified.body.address;
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// раздражало бы кошелёк запросами без причины.
|
|
87
|
+
// A request with the pass. On 401 the pass may have expired or the server restarted - try to log in once
|
|
88
|
+
// more, and only if that also fails return the refusal honestly: silent re-signing would spam the wallet.
|
|
95
89
|
async function authed(path, init = {}, retry = true) {
|
|
96
90
|
const t = token();
|
|
97
91
|
const headers = { ...(init.headers || {}) };
|
|
@@ -105,12 +99,12 @@ async function authed(path, init = {}, retry = true) {
|
|
|
105
99
|
return r;
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
102
|
+
// Send a swap record. Returns { ok, reason }: a refusal is a fact, not an exception, and the caller
|
|
103
|
+
// decides - the swap must not abort because of it.
|
|
110
104
|
export async function saveServerSwap(swap, network) {
|
|
111
105
|
const escrow = swap && swap.escrow && swap.escrow.address;
|
|
112
|
-
if (!escrow) return { ok: false, reason: "
|
|
113
|
-
if (!isConnected()) return { ok: false, reason: "
|
|
106
|
+
if (!escrow) return { ok: false, reason: "the record has no escrow address" };
|
|
107
|
+
if (!isConnected()) return { ok: false, reason: "wallet not connected" };
|
|
114
108
|
try {
|
|
115
109
|
if (!token() || authedAddress() !== String(address()).toLowerCase()) { forgetSession(); await signIn(); }
|
|
116
110
|
const r = await authed("/api/evm-swaps", {
|
|
@@ -118,9 +112,9 @@ export async function saveServerSwap(swap, network) {
|
|
|
118
112
|
headers: { "content-type": "application/json" },
|
|
119
113
|
body: JSON.stringify({
|
|
120
114
|
id: swap.id, network: network || swap.network || swap.chain, escrow,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
// QUOTE REFERENCE. Through it the server and the provider node know WHOSE order this is: without it
|
|
116
|
+
// the escrow address stays unknown to the node and it cannot claim the ETH. A missing field is not
|
|
117
|
+
// an error: old records and provider-less swaps are sent as is.
|
|
124
118
|
quoteId: swap.orderQuoteId || null,
|
|
125
119
|
providerId: swap.providerId || null,
|
|
126
120
|
hashlock: (swap.escrow && swap.escrow.hashlock) || swap.hashlock || null,
|
|
@@ -130,25 +124,24 @@ export async function saveServerSwap(swap, network) {
|
|
|
130
124
|
}),
|
|
131
125
|
});
|
|
132
126
|
if (r.status === 201 || r.status === 200) {
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// поэтому он уезжает вызывающему, а тот говорит в консоль.
|
|
127
|
+
// THE SERVER ALSO ANSWERS ABOUT THE QUOTE BINDING. Without it the provider node cannot claim the ETH.
|
|
128
|
+
// A binding refusal does not abort the swap, but must not be silent either - it goes to the caller, which logs it.
|
|
136
129
|
return { ok: true, orderBinding: (r.body && r.body.orderBinding) || null };
|
|
137
130
|
}
|
|
138
|
-
const why = r.body && r.body.error ? r.body.error : "
|
|
131
|
+
const why = r.body && r.body.error ? r.body.error : "response " + r.status;
|
|
139
132
|
return { ok: false, reason: why };
|
|
140
133
|
} catch (e) {
|
|
141
134
|
return { ok: false, reason: String((e && e.message) || e) };
|
|
142
135
|
}
|
|
143
136
|
}
|
|
144
137
|
|
|
145
|
-
//
|
|
138
|
+
// The swap list from the server. An empty result and a refusal are different, and the caller sees the difference.
|
|
146
139
|
export async function loadServerSwaps() {
|
|
147
|
-
if (!isConnected()) return { ok: false, reason: "
|
|
140
|
+
if (!isConnected()) return { ok: false, reason: "wallet not connected", swaps: [] };
|
|
148
141
|
try {
|
|
149
142
|
if (!token()) await signIn();
|
|
150
143
|
const r = await authed("/api/evm-swaps");
|
|
151
|
-
if (r.status !== 200 || !r.body) return { ok: false, reason: "
|
|
144
|
+
if (r.status !== 200 || !r.body) return { ok: false, reason: "response " + r.status, swaps: [] };
|
|
152
145
|
return { ok: true, swaps: r.body.swaps || [] };
|
|
153
146
|
} catch (e) {
|
|
154
147
|
return { ok: false, reason: String((e && e.message) || e), swaps: [] };
|
package/engine/evm/claimGas.js
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/claimGas.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (
|
|
13
|
-
//
|
|
14
|
-
// это не хватало. Теперь оба числа считает ЭТОТ модуль.
|
|
15
|
-
//
|
|
16
|
-
// ПОЧЕМУ ОТДЕЛЬНЫЙ МОДУЛЬ И ПОЧЕМУ ПОД www/js. Формула обязана быть ОДНА. Её читает и страница/SDK
|
|
17
|
-
// (движок живёт под www/js), и нода - rfq/reverseGas.mjs, а rfq уже импортирует из www/js (см.
|
|
18
|
-
// rfq/payoutFee.mjs, `../www/js/core/format.js`). Обратный импорт невозможен: боевой веб-корень - www/,
|
|
19
|
-
// каталог rfq/ браузеру не отдаётся. Поэтому единственный общий дом - www/js/evm/.
|
|
20
|
-
//
|
|
21
|
-
// ЧТО ЗДЕСЬ ОДНО. Предел газа claim, правило комиссии (2 x baseFee + чаевые) и РАЗМЕР ПОДАРКА - та же
|
|
22
|
-
// стоимость claim плюс ЯВНЫЙ ЗАПАС на рост базовой цены между выдачей билета и забором. Второго расчёта
|
|
23
|
-
// размера подарка нет ни у ноды, ни у прогонов: они зовут claimGasGiftWei отсюда.
|
|
5
|
+
// CLAIM GAS FOR THE XMR SELL (claim): ONE SOURCE OF TRUTH FOR THE CLIENT AND THE NODE.
|
|
6
|
+
// In a reverse swap the person gives XMR and claims ETH via `claim` on the settlement chain. They have no ETH,
|
|
7
|
+
// so the node sends the claim gas. The gas rules used to live in TWO places and the numbers differed ~100x, so
|
|
8
|
+
// a person with one gift could not send the claim. Now this module computes both numbers.
|
|
9
|
+
// WHY ONE MODULE UNDER www/js: the formula must be ONE. The EVM engine lives under www/js, and the node imports
|
|
10
|
+
// from www/js too; the reverse import is impossible (the browser is never served the node directory).
|
|
11
|
+
// WHAT IS ONE HERE: the claim gas limit, the fee rule (2 x baseFee + tip) and the GIFT SIZE - the same claim
|
|
12
|
+
// cost plus an EXPLICIT margin for base-fee growth between ticket and claim.
|
|
24
13
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// в кассе = SSTORE нуля), стоит 120 721 газа на живой цепи (124 072 в gas-report канонических контрактов) -
|
|
28
|
-
// больше прежних 120 000, чем законный забор и резался. Лимит задан с запасом: 200 000.
|
|
14
|
+
// THE CLAIM GAS LIMIT IS ONE NUMBER ON BOTH SIDES. MEASURED: an honest claim costs 120 721 gas on a live chain,
|
|
15
|
+
// more than the old 120 000, which cut off legitimate claims. The limit has a margin: 200 000.
|
|
29
16
|
export const CLAIM_GAS_LIMIT = 200_000n;
|
|
30
17
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// предназначен. Для Arbitrum чаевые не нужны: пол 0. Сеть со своей политикой передаёт minTipWei
|
|
35
|
-
// аргументом (session.js берёт сеть из реестра, www/js/core/config.js).
|
|
18
|
+
// THE TIP FLOOR IS NETWORK POLICY, NOT A CONSTANT IN THE SENDING CODE. A fixed 1 gwei minimum inflated
|
|
19
|
+
// maxFeePerGas ~50x on cheap-gas networks and broke exactly those the gas is for. Arbitrum needs no tip
|
|
20
|
+
// (floor 0); a network with its own policy passes minTipWei, taken from the network registry.
|
|
36
21
|
export const CLAIM_TIP_FLOOR_WEI = 0n;
|
|
37
22
|
|
|
38
23
|
const toBig = (v) => {
|
|
@@ -40,10 +25,9 @@ const toBig = (v) => {
|
|
|
40
25
|
try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
|
|
41
26
|
};
|
|
42
27
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// ноль: неизвестное обязано называться, а не выдаваться за измеренное.
|
|
28
|
+
// CLAIM FEE (EIP-1559): `maxFeePerGas = 2 x baseFee + tip`. Doubling the base fee covers its growth during the
|
|
29
|
+
// transaction; the tip is gasPrice - baseFee, not below the network floor. baseFee = 0 (no EIP-1559): no fee
|
|
30
|
+
// fields at all, and that is null, not zero.
|
|
47
31
|
export function claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei = 0n, minTipWei = CLAIM_TIP_FLOOR_WEI } = {}) {
|
|
48
32
|
const base = toBig(baseFeeWei);
|
|
49
33
|
if (base === null || base <= 0n) return null;
|
|
@@ -54,8 +38,8 @@ export function claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei = 0n, minTipWe
|
|
|
54
38
|
return base * 2n + tip;
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
41
|
+
// HOW MUCH THE BALANCE NEEDS FOR CLAIM - the node acceptance rule: `gasLimit x maxFeePerGas`. If a factor is
|
|
42
|
+
// unmeasured - null (not "zero gas").
|
|
59
43
|
export function claimRequiredWei({ gasLimit = CLAIM_GAS_LIMIT, maxFeePerGasWei } = {}) {
|
|
60
44
|
const limit = toBig(gasLimit);
|
|
61
45
|
const maxFee = toBig(maxFeePerGasWei);
|
|
@@ -63,21 +47,18 @@ export function claimRequiredWei({ gasLimit = CLAIM_GAS_LIMIT, maxFeePerGasWei }
|
|
|
63
47
|
return limit * maxFee;
|
|
64
48
|
}
|
|
65
49
|
|
|
66
|
-
//
|
|
50
|
+
// MARGIN FOR BASE-FEE GROWTH BETWEEN TICKET AND CLAIM, in basis points (10 000 = +100%).
|
|
67
51
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// достаточным, даже если цена газа к забору вырастет вдвое. Значение - настройка ноды
|
|
74
|
-
// (RFQ_REVERSE_GAS_RESERVE_BPS), а не догадка: её видно и можно переопределить без правки кода.
|
|
52
|
+
// WHY IT IS NEEDED AND WHY THIS MUCH. The claim cost is computed at TICKET time, but the claim happens later:
|
|
53
|
+
// the ticket lives RFQ_ORDER_QUOTE_TTL_MS, 40 minutes by default. The client rule (2 x baseFee) covers growth
|
|
54
|
+
// only during its own transaction, not those 40 minutes. +100% covers a doubling of the base fee over the
|
|
55
|
+
// ticket window, so the gift stays sufficient. The value is a node setting (RFQ_REVERSE_GAS_RESERVE_BPS), not
|
|
56
|
+
// a guess: it is visible and can be overridden.
|
|
75
57
|
export const DEFAULT_GIFT_RESERVE_BPS = 10_000n;
|
|
76
58
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// известны. Нечитаемая база - null, а не ноль.
|
|
59
|
+
// GIFT SIZE - the same claim cost the client requires, PLUS the margin. This is the ONLY computation of the
|
|
60
|
+
// gift size: the node and the live run call IT, not their own copy. base comes from the chain at ticket time;
|
|
61
|
+
// priorityFeeWei is the real network tip. An unreadable base is null, not zero.
|
|
81
62
|
export function claimGasGiftWei({ baseFeeWei, priorityFeeWei, minTipWei, gasLimit = CLAIM_GAS_LIMIT, reserveBps = DEFAULT_GIFT_RESERVE_BPS } = {}) {
|
|
82
63
|
const maxFee = claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei, minTipWei });
|
|
83
64
|
const need = claimRequiredWei({ gasLimit, maxFeePerGasWei: maxFee });
|
package/engine/evm/depositor.js
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/depositor.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// THE DEPOSITOR BY SIGNATURE, CLIENT SIDE: EIP-712 typed data over the QUOTE DIGEST.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// транзакция: она не стоит газа и не двигает деньги, поэтому человек БЕЗ своего ETH тоже может её дать.
|
|
7
|
+
// WHY. The factory has a path createOrderAndFundByDepositor: ANYONE may send the transaction (a relayer, our
|
|
8
|
+
// service), but the depositor is whoever signed the quote digest, so the depositor must sign first. A wallet
|
|
9
|
+
// signature is typed data, not a transaction: it costs no gas and moves no money, so a person without ETH can give it.
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// чужой адрес, и фабрика отказывает `BadDepositorSignature`. Поэтому форма лежит ОДНИМ списком ниже, а
|
|
17
|
-
// проверка (tools/check-usdc-without-eth.mjs) считает этой формой дайджест и сверяет его с дайджестом,
|
|
18
|
-
// посчитанным СПЕКОЙ ноды/SDK на той же котировке - разойтись молча они не могут.
|
|
11
|
+
// WHY THIS FORM, NOT AN INVENTED ONE. The field set and domain are PART OF THE PROTOCOL - the same OrderQuote
|
|
12
|
+
// the escrow verifies and the node signs. Two swapped fields give a DIFFERENT digest and the factory refuses
|
|
13
|
+
// BadDepositorSignature. So the form is ONE list below, and a check computes the digest with it and compares.
|
|
19
14
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// DOMAIN. verifyingContract is the FACTORY ADDRESS FROM THE QUOTE (field `factory`), chainId is live: a signature
|
|
16
|
+
// made for another factory or network will not recover here. There is no second source of the domain.
|
|
22
17
|
//
|
|
23
|
-
//
|
|
24
|
-
// только форма и её проверка, чтобы кошелёк подписал ровно то, что проверит цепь.
|
|
18
|
+
// NO CRYPTOGRAPHY HERE: the wallet computes the digest (eth_signTypedData_v4). Here is only the form and its check.
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
export const DEPOSITOR_DOMAIN_NAME = "NinseiEscrowFactory";
|
|
28
|
-
export const DEPOSITOR_DOMAIN_VERSION = "1";
|
|
20
|
+
import { SIGNING_DOMAIN } from "../core/config.js";
|
|
29
21
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
22
|
+
// The domain name and version are quote protocol v8. They are NOT brand literals here: they belong to the registry
|
|
23
|
+
// contract (it returns DOMAIN_SEPARATOR()), so they are read from the config. The defaults equal today's values.
|
|
24
|
+
export const DEPOSITOR_DOMAIN_NAME = SIGNING_DOMAIN.name;
|
|
25
|
+
export const DEPOSITOR_DOMAIN_VERSION = SIGNING_DOMAIN.version;
|
|
26
|
+
|
|
27
|
+
// FIELD SET IN CONTRACT ORDER. The order is part of the ABI: a swap gives another digest. One list feeds both
|
|
28
|
+
// the type string and the message, so they cannot diverge.
|
|
32
29
|
export const ORDER_QUOTE_TYPED_FIELDS = [
|
|
33
30
|
["provider", "address"],
|
|
34
31
|
["quoteKey", "address"],
|
|
@@ -48,47 +45,46 @@ export const ORDER_QUOTE_TYPED_FIELDS = [
|
|
|
48
45
|
["feeRecipient", "address"],
|
|
49
46
|
["validUntil", "uint64"],
|
|
50
47
|
["nonce", "uint256"],
|
|
51
|
-
// THE XMR SIDE OF THE DEAL
|
|
48
|
+
// THE XMR SIDE OF THE DEAL, in atomic units: it joined the signed set together with the contract
|
|
52
49
|
// (OrderQuote.sol), the node/SDK spec and the vector, so the depositor's signature covers the same XMR the chain
|
|
53
50
|
// verifies. A field the chain could not see used to stay OUT of this list on purpose - that is no longer the case.
|
|
54
51
|
["xmrAmount", "uint256"],
|
|
55
52
|
];
|
|
56
53
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
54
|
+
// The message type. Built from the list: editing the list changes both the type and the signature, so it cannot
|
|
55
|
+
// go unnoticed.
|
|
59
56
|
export const ORDER_QUOTE_TYPED_TYPE = "OrderQuote(" + ORDER_QUOTE_TYPED_FIELDS.map(([n, t]) => t + " " + n).join(",") + ")";
|
|
60
57
|
|
|
61
58
|
const isHex = (v, bytes) => new RegExp("^(0x)?[0-9a-fA-F]{" + bytes * 2 + "}$").test(String(v === undefined || v === null ? "" : v).trim());
|
|
62
59
|
const isNum = (v) => /^(0x[0-9a-fA-F]+|[0-9]+)$/.test(String(v === undefined || v === null ? "" : v).trim());
|
|
63
60
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// отвергаем всё, что не адрес/не bytes32/не число: молчаливое выравнивание нулями ушло бы ДРУГИМ значением.
|
|
61
|
+
// FORM IS NORMALISED, NOT DEMANDED. The quote carries ed25519 points WITHOUT the 0x prefix, while the wallet and
|
|
62
|
+
// contract expect the same BYTES. So we normalise here and reject anything that is not address/bytes32/number.
|
|
67
63
|
function normaliseField(name, type, value) {
|
|
68
64
|
const raw = String(value === undefined || value === null ? "" : value).trim();
|
|
69
|
-
if (raw === "") throw new Error("
|
|
65
|
+
if (raw === "") throw new Error("the quote carries no depositor-signature field: " + name);
|
|
70
66
|
if (type === "address") {
|
|
71
|
-
if (!isHex(raw, 20)) throw new Error("
|
|
67
|
+
if (!isHex(raw, 20)) throw new Error("field " + name + " is not an address: " + raw);
|
|
72
68
|
return "0x" + raw.replace(/^0x/, "").toLowerCase();
|
|
73
69
|
}
|
|
74
70
|
if (type === "bytes32") {
|
|
75
|
-
if (!isHex(raw, 32)) throw new Error("
|
|
71
|
+
if (!isHex(raw, 32)) throw new Error("field " + name + " is not bytes32: " + raw);
|
|
76
72
|
return "0x" + raw.replace(/^0x/, "").toLowerCase();
|
|
77
73
|
}
|
|
78
|
-
// uint64 / uint256:
|
|
79
|
-
if (!isNum(raw)) throw new Error("
|
|
74
|
+
// uint64 / uint256: the wallet accepts a string; decimal and hex are parsed the same.
|
|
75
|
+
if (!isNum(raw)) throw new Error("field " + name + " is not a number: " + raw);
|
|
80
76
|
return String(BigInt(raw));
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
/**
|
|
84
|
-
* EIP-712 typed data
|
|
85
|
-
*
|
|
80
|
+
* EIP-712 typed data that the depositor signs (eth_signTypedData_v4).
|
|
81
|
+
* The domain is built FROM THE QUOTE: verifyingContract = quote.factory, chainId = quote.chainId.
|
|
86
82
|
* @returns {{ domain: object, types: object, primaryType: string, message: object }}
|
|
87
83
|
*/
|
|
88
84
|
export function orderQuoteTypedData(quote) {
|
|
89
|
-
if (!quote || typeof quote !== "object") throw new Error("
|
|
90
|
-
if (!isHex(quote.factory, 20)) throw new Error("
|
|
91
|
-
if (!isNum(quote.chainId)) throw new Error("
|
|
85
|
+
if (!quote || typeof quote !== "object") throw new Error("no signed quote: nothing to sign");
|
|
86
|
+
if (!isHex(quote.factory, 20)) throw new Error("the quote has no factory address - cannot build the signing domain");
|
|
87
|
+
if (!isNum(quote.chainId)) throw new Error("the quote has no chainId - cannot build the signing domain");
|
|
92
88
|
const message = {};
|
|
93
89
|
for (const [name, type] of ORDER_QUOTE_TYPED_FIELDS) {
|
|
94
90
|
message[name] = normaliseField(name, type, quote[name]);
|
|
@@ -101,7 +97,7 @@ export function orderQuoteTypedData(quote) {
|
|
|
101
97
|
verifyingContract: "0x" + String(quote.factory).replace(/^0x/, "").toLowerCase(),
|
|
102
98
|
},
|
|
103
99
|
types: {
|
|
104
|
-
// EIP712Domain
|
|
100
|
+
// EIP712Domain explicit: wallets show it to the person, and it must match what the chain computes.
|
|
105
101
|
EIP712Domain: [
|
|
106
102
|
{ name: "name", type: "string" },
|
|
107
103
|
{ name: "version", type: "string" },
|
|
@@ -115,13 +111,13 @@ export function orderQuoteTypedData(quote) {
|
|
|
115
111
|
};
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
114
|
+
// SIGNATURE ON THE WIRE - 65 BYTES r || s || v. The form is checked HERE: the factory takes the signature as
|
|
115
|
+
// three words, and a "signature-like" string must not reach the contract as consent.
|
|
120
116
|
export function depositorSignatureShape(signature) {
|
|
121
117
|
const hex = String(signature === undefined || signature === null ? "" : signature).replace(/^0x/, "");
|
|
122
|
-
if (!/^[0-9a-fA-F]{130}$/.test(hex)) return { ok: false, reason: "
|
|
118
|
+
if (!/^[0-9a-fA-F]{130}$/.test(hex)) return { ok: false, reason: "the depositor signature must be 65 bytes (r || s || v), got " + hex.length / 2 + " bytes" };
|
|
123
119
|
let v = parseInt(hex.slice(128, 130), 16);
|
|
124
120
|
if (v < 27) v += 27;
|
|
125
|
-
if (v !== 27 && v !== 28) return { ok: false, reason: "
|
|
121
|
+
if (v !== 27 && v !== 28) return { ok: false, reason: "depositor signature v outside {27,28}: " + v };
|
|
126
122
|
return { ok: true, r: "0x" + hex.slice(0, 64).toLowerCase(), s: "0x" + hex.slice(64, 128).toLowerCase(), v: 27 + (v - 27) };
|
|
127
123
|
}
|