@ninsei-labs/sdk 0.39.3 → 0.40.0
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 +55 -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 +561 -451
- 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 +20 -8
- 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/session.js
CHANGED
|
@@ -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
|
|
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 EVM wallet: state, balances, network switching and two ways to connect.
|
|
7
6
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// display_uri
|
|
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
|
-
//
|
|
27
|
-
//
|
|
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
|
-
//
|
|
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, //
|
|
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, //
|
|
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
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
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
|
-
//
|
|
112
|
-
//
|
|
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
|
|
139
|
-
//
|
|
140
|
-
//
|
|
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
|
|
163
|
-
// session_delete -
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
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("
|
|
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]
|
|
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]
|
|
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]
|
|
211
|
+
console.log("[wc] session raised again: " + state.address);
|
|
213
212
|
} catch (e) {
|
|
214
|
-
console.log("[wc]
|
|
213
|
+
console.log("[wc] could not raise the session: " + e.message);
|
|
215
214
|
reset("idle", null);
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
246
|
-
//
|
|
247
|
-
//
|
|
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
|
-
//
|
|
255
|
-
// (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
const OPTOUT_KEY = "
|
|
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]
|
|
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
|
-
//
|
|
302
|
-
//
|
|
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:
|
|
319
|
+
console.info("WalletConnect: session restored after reload", state.address);
|
|
321
320
|
}
|
|
322
321
|
} catch (e) {
|
|
323
|
-
console.warn("WalletConnect:
|
|
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("
|
|
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]
|
|
389
|
+
console.info("[wallet] user refusal: " + message);
|
|
391
390
|
reset("idle", null);
|
|
392
391
|
return null;
|
|
393
392
|
}
|
|
394
|
-
console.warn("[wallet]
|
|
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
|
-
//
|
|
399
|
+
// Connect. chainSlug - the network selected in the form; it also becomes the primary in the session.
|
|
401
400
|
//
|
|
402
|
-
// WalletConnect
|
|
403
|
-
//
|
|
404
|
-
//
|
|
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
|
-
//
|
|
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]
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
493
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
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
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
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
|
-
//
|
|
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("
|
|
562
|
+
throw new Error("wallet not connected: typed-data signing is impossible");
|
|
564
563
|
}
|
|
565
|
-
if (!typedData || typeof typedData !== "object") throw new Error("signTypedData:
|
|
566
|
-
//
|
|
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("
|
|
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
|
-
//
|
|
589
|
-
//
|
|
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
|
-
//
|
|
603
|
-
// (
|
|
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
|
-
//
|
|
645
|
-
//
|
|
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
|
-
//
|
|
649
|
-
//
|
|
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
|
|
655
|
-
//
|
|
656
|
-
//
|
|
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
|
-
//
|
|
659
|
-
// (
|
|
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("
|
|
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
|
-
//
|
|
675
|
-
//
|
|
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("
|
|
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
|
-
//
|
|
700
|
-
// www/js/evm/claimGas.js -
|
|
701
|
-
//
|
|
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
|
-
//
|
|
708
|
-
//
|
|
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; //
|
|
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
|
|
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("
|
|
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("
|
|
726
|
+
throw new Error("the wallet returned not a transaction hash: " + String(hash).slice(0, 60));
|
|
728
727
|
}
|
|
729
728
|
return hash;
|
|
730
729
|
}
|