@ninsei-labs/sdk 0.39.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 +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
package/src/sweep.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// XMR WITHDRAWAL: PARSING THE FILE AND PREPARING. The withdrawal itself requires a Monero wallet - it arrives as
|
|
2
|
+
// an adapter.
|
|
3
|
+
//
|
|
4
|
+
// Here is exactly what the sweep page does before syncing: decrypt the recovery file, validate it, take the shared
|
|
5
|
+
// address, ask the app for the restore height (on the page it is taken by address) and say aloud what is missing
|
|
6
|
+
// so the withdrawal can go. Funds do not move at this step.
|
|
7
|
+
import { SdkError } from "./errors.mjs";
|
|
8
|
+
import { primitives } from "./primitives.mjs";
|
|
9
|
+
import * as engine from "./engine.mjs";
|
|
10
|
+
|
|
11
|
+
// The same boundary as when the file was issued: a passphrase shorter than twelve characters will not open the file.
|
|
12
|
+
const MIN_PASSPHRASE = 12;
|
|
13
|
+
const STEPS = ["unlock", "sync", "sweep", "relay"];
|
|
14
|
+
|
|
15
|
+
const contentsOf = (file) => (typeof file === "string" ? file : file && typeof file.contents === "string" ? file.contents : null);
|
|
16
|
+
|
|
17
|
+
export function createSweep({ http, config }) {
|
|
18
|
+
// THE NAME IS NEEDED BY THE OBJECT ITSELF: `run` calls `api.plan(...)` - the step order is held by the plan and
|
|
19
|
+
// the withdrawal living in one object, not in two copies. During a manual merge of branches the object was left
|
|
20
|
+
// unnamed, and the withdrawal failed with "api is not defined": not a refusal, but a breakdown. `node --check`
|
|
21
|
+
// misses this, there are no conflict markers in the file - only a run shows it.
|
|
22
|
+
// AN OPEN SIGN SESSION: a token -> { wallet, plan, signed }. It lives until `relay`; it keeps no keys - they are
|
|
23
|
+
// owned by the library's wallet.
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
let sessionSeq = 0;
|
|
26
|
+
|
|
27
|
+
const api = {
|
|
28
|
+
async plan(request) {
|
|
29
|
+
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
30
|
+
const contents = contentsOf(request.file);
|
|
31
|
+
if (!contents) throw new SdkError("bad-input", { field: "file" });
|
|
32
|
+
if (typeof request.passphrase !== "string" || request.passphrase.length < MIN_PASSPHRASE) {
|
|
33
|
+
throw new SdkError("bad-input", { field: "passphrase", min: MIN_PASSPHRASE });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let envelope = null;
|
|
37
|
+
try { envelope = JSON.parse(contents); } catch { throw new SdkError("recovery-failed", { step: "parse" }); }
|
|
38
|
+
let payload = null;
|
|
39
|
+
try { payload = await engine.recoveryFile.decryptFile(envelope, request.passphrase); } catch { throw new SdkError("recovery-failed", { step: "decrypt" }); }
|
|
40
|
+
// We check the composition BEFORE the work: a file that does not pass its own check is not good for withdrawal.
|
|
41
|
+
try { engine.recoveryFile.validatePayload(payload); } catch { throw new SdkError("recovery-failed", { step: "validate" }); }
|
|
42
|
+
|
|
43
|
+
const wallet = payload.moneroWallet || {};
|
|
44
|
+
const address = wallet.address || null;
|
|
45
|
+
if (!address) throw new SdkError("recovery-failed", { step: "address" });
|
|
46
|
+
|
|
47
|
+
const missing = [];
|
|
48
|
+
// WHAT WE CAN DISPOSE OF: our own half of the spend key. Without it the withdrawal is impossible under any
|
|
49
|
+
// circumstances, and that must be said before syncing, not after.
|
|
50
|
+
const spendable = !!wallet.spendHalf;
|
|
51
|
+
if (!spendable) missing.push("spendable");
|
|
52
|
+
|
|
53
|
+
// THE RESTORE HEIGHT: first from the file, then from the app BY ADDRESS - exactly as the sweep page does it.
|
|
54
|
+
// An unavailable app is a lack of data, not a refusal: the plan is returned anyway, and `missing` shows what
|
|
55
|
+
// is lacking.
|
|
56
|
+
let restoreHeight = Number(wallet.restoreHeight) || 0;
|
|
57
|
+
let row = null;
|
|
58
|
+
try {
|
|
59
|
+
// THE PATH BY NAME, not by a string: the override comes through the `routes` option.
|
|
60
|
+
const answer = await http.json(config.route("swaps"));
|
|
61
|
+
const rows = Array.isArray(answer) ? answer : ((answer && answer.swaps) || []);
|
|
62
|
+
row = rows.find((x) => String((x && x.address) || "") === String(address)) || null;
|
|
63
|
+
} catch { row = null; }
|
|
64
|
+
const fromApp = Number(row && row.restoreHeight) || 0;
|
|
65
|
+
if (fromApp > 0) restoreHeight = fromApp;
|
|
66
|
+
if (!(restoreHeight > 0)) missing.push("restore-height");
|
|
67
|
+
|
|
68
|
+
// THE COUNTERPARTY'S REVEALED HALF - FROM THE CHAIN, VIA THE APP. Exactly as the sweep page does it: POST to
|
|
69
|
+
// the named path, body { network, address }, where address is the ESCROW ADDRESS (not the Monero address).
|
|
70
|
+
// The half is published by the settlement that moves the ETH: a claim or a refund. Before that no one has the
|
|
71
|
+
// spend key, so "not revealed yet" is not an error but a state, and that is exactly where it lands in `missing`.
|
|
72
|
+
let revealed = false;
|
|
73
|
+
let revealedValue = null;
|
|
74
|
+
const escrowAddress = wallet.escrowAddress || (payload.escrow && payload.escrow.address) || null;
|
|
75
|
+
if (escrowAddress) {
|
|
76
|
+
try {
|
|
77
|
+
const half = await http.json(config.route("revealedHalf"), {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify({ network: payload.network, address: escrowAddress }),
|
|
81
|
+
});
|
|
82
|
+
revealed = !!(half && half.ok && half.revealed && half.half);
|
|
83
|
+
if (revealed) revealedValue = String(half.half);
|
|
84
|
+
} catch { revealed = false; }
|
|
85
|
+
}
|
|
86
|
+
if (!revealed) missing.push("revealed-half");
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
swapId: payload.swapId || null,
|
|
90
|
+
address,
|
|
91
|
+
spendable,
|
|
92
|
+
restoreHeight,
|
|
93
|
+
steps: STEPS,
|
|
94
|
+
// Whether the counterparty's half is revealed: this is a separate flag, not "absent from the missing list".
|
|
95
|
+
revealedHalf: revealed,
|
|
96
|
+
half: revealedValue,
|
|
97
|
+
missing,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// THE WITHDRAWAL KEYS. They are computed here and checked here too - for the reason that the contract cannot do
|
|
102
|
+
// it: it will check the hash of the revealed half, but not the link "half and the point in the address" - there
|
|
103
|
+
// is no ed25519 in EVM. So before syncing we must make sure that (s_a + s_b)*G matched the order's spend key. It
|
|
104
|
+
// did not match - we must not send: the XMR would go nowhere, while it would look like success.
|
|
105
|
+
async keys(request) {
|
|
106
|
+
const plan = await api.plan(request);
|
|
107
|
+
if (!plan.half) throw new SdkError("bad-input", { field: "revealedHalf", why: "not-revealed" });
|
|
108
|
+
const contents = contentsOf(request.file);
|
|
109
|
+
let payload = null;
|
|
110
|
+
try { payload = await engine.recoveryFile.decryptFile(JSON.parse(contents), request.passphrase); }
|
|
111
|
+
catch { throw new SdkError("recovery-failed", { step: "decrypt" }); }
|
|
112
|
+
const wallet = payload.moneroWallet || {};
|
|
113
|
+
const need = ["spendHalf", "viewHalf", "otherSpendPoint", "otherViewHalf"];
|
|
114
|
+
const missingFields = need.filter((k) => wallet[k] === undefined || wallet[k] === null || wallet[k] === "");
|
|
115
|
+
if (missingFields.length) throw new SdkError("bad-input", { field: "payload", missing: missingFields });
|
|
116
|
+
try {
|
|
117
|
+
const halves = engine.halves.createHalves(primitives);
|
|
118
|
+
const keys = engine.halvesSweep.spendKeysFromRevealedHalf({
|
|
119
|
+
halves,
|
|
120
|
+
ownSpendHalf: wallet.spendHalf, ownViewHalf: wallet.viewHalf,
|
|
121
|
+
otherSpendPoint: wallet.otherSpendPoint, otherViewHalf: wallet.otherViewHalf,
|
|
122
|
+
revealedHalf: plan.half,
|
|
123
|
+
});
|
|
124
|
+
return { privateSpendKey: keys.privateSpendKey, privateViewKey: keys.privateViewKey, address: plan.address };
|
|
125
|
+
} catch {
|
|
126
|
+
// THE CHECK DID NOT PASS: the half is from another order or was substituted. This is a refusal with a code,
|
|
127
|
+
// not an engine exception with text inside.
|
|
128
|
+
throw new SdkError("bad-input", { field: "revealedHalf", step: "verify" });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// THE WITHDRAWAL ITSELF. The wallet arrives as an adapter (the WASM and the worker live with whoever builds the
|
|
133
|
+
// page), while the core holds the order: open -> sync -> look at the free remainder -> SIGN (relay: false) ->
|
|
134
|
+
// send -> close.
|
|
135
|
+
//
|
|
136
|
+
// SIGNING AND SENDING ARE DELIBERATELY SPLIT: between them the interface has time to show the person what will
|
|
137
|
+
// leave, rather than telling them about it after sending. An empty remainder stops the withdrawal BEFORE
|
|
138
|
+
// signing - otherwise the wallet would sign a transaction for zero and the refusal would look like success.
|
|
139
|
+
// TELLING THE SERVICE ABOUT THE WITHDRAWAL - SEPARATELY AND REPEATABLY. The mark does not affect the transfer
|
|
140
|
+
// (it is already on the network), so it can be repeated: the interface calls this even after `relay` if the
|
|
141
|
+
// report did not go through then.
|
|
142
|
+
async report(request) {
|
|
143
|
+
if (!request || typeof request.swapId !== "string" || !request.swapId) throw new SdkError("bad-input", { field: "swapId" });
|
|
144
|
+
if (typeof request.txid !== "string" || !request.txid) throw new SdkError("bad-input", { field: "txid" });
|
|
145
|
+
try {
|
|
146
|
+
const answer = await http.json(config.route("swept", { id: request.swapId }), {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ txid: request.txid }),
|
|
150
|
+
});
|
|
151
|
+
return { reported: !(answer && answer.ok === false) };
|
|
152
|
+
} catch {
|
|
153
|
+
// The service is unavailable - that is `reported: false`, not a withdrawal refusal: the money has already left.
|
|
154
|
+
return { reported: false };
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// SIGNING WITHOUT SENDING - AS A SEPARATE STEP. The core HOLDS the signed transaction until the person's
|
|
159
|
+
// decision: otherwise the interface could not show what exactly will leave, and would tell about it only after
|
|
160
|
+
// sending. Here is the whole order UP TO AND INCLUDING SIGNING: plan -> wallet -> open -> sync -> free remainder
|
|
161
|
+
// (an empty one STOPS the withdrawal with a code) -> sign (relay: false). The wallet stays open until `relay`,
|
|
162
|
+
// and the session token is the only handle to it.
|
|
163
|
+
async sign(request) {
|
|
164
|
+
const plan = await api.plan(request);
|
|
165
|
+
if (!request || typeof request.to !== "string" || !request.to) throw new SdkError("bad-input", { field: "to" });
|
|
166
|
+
if (typeof request.connectWallet !== "function") throw new SdkError("bad-input", { field: "connectWallet" });
|
|
167
|
+
const wallet = await request.connectWallet(plan);
|
|
168
|
+
const role = ["open", "sync", "unlockedBalance", "sweepUnlocked", "relay", "close"];
|
|
169
|
+
const missing = role.filter((name) => !wallet || typeof wallet[name] !== "function");
|
|
170
|
+
if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
|
|
171
|
+
|
|
172
|
+
await wallet.open(plan);
|
|
173
|
+
try {
|
|
174
|
+
await wallet.sync();
|
|
175
|
+
const unlocked = BigInt(await wallet.unlockedBalance());
|
|
176
|
+
// AN EMPTY REMAINDER STOPS THE WITHDRAWAL BEFORE SIGNING: otherwise the wallet would sign a transaction for
|
|
177
|
+
// zero, and the refusal would look like success.
|
|
178
|
+
if (unlocked <= 0n) throw new SdkError("insufficient-funds", { unlocked: unlocked.toString() });
|
|
179
|
+
const signed = await wallet.sweepUnlocked({ address: request.to, relay: false });
|
|
180
|
+
const token = "sweep-" + (++sessionSeq);
|
|
181
|
+
sessions.set(token, { wallet, plan, signed });
|
|
182
|
+
return { token, amount: unlocked.toString(), to: request.to, swept: plan.address };
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// There is no signature - we do not leave an open wallet. Closing must not hide the refusal's reason.
|
|
185
|
+
try { await wallet.close(); } catch { /* the reason matters more than closing */ }
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// SENDING THE SIGNED AND MARKING THE WITHDRAWAL. Sending is split from signing by the same seam as the session:
|
|
191
|
+
// the person has time to see the transfer before it leaves.
|
|
192
|
+
async relay(request) {
|
|
193
|
+
if (!request || typeof request.token !== "string" || !request.token) throw new SdkError("bad-input", { field: "token" });
|
|
194
|
+
const session = sessions.get(request.token);
|
|
195
|
+
if (!session) throw new SdkError("bad-input", { field: "token", why: "no-open-session" });
|
|
196
|
+
sessions.delete(request.token);
|
|
197
|
+
const { wallet, plan, signed } = session;
|
|
198
|
+
try {
|
|
199
|
+
const txids = await wallet.relay(signed);
|
|
200
|
+
const list = Array.isArray(txids) ? txids : [];
|
|
201
|
+
// MARKING THE WITHDRAWAL - AFTER SENDING AND NOT AFFECTING IT. The transfer is already on the network; the
|
|
202
|
+
// report merely tells the service that the XMR left. So a failed report does NOT make the withdrawal
|
|
203
|
+
// unsuccessful: it gives `reported: false`, so the interface offers to report again - by the same path as
|
|
204
|
+
// here (`report`).
|
|
205
|
+
const reported = plan.swapId && list.length
|
|
206
|
+
? (await api.report({ swapId: plan.swapId, txid: list[0] })).reported
|
|
207
|
+
: false;
|
|
208
|
+
return { txids: list, swept: plan.address, reported };
|
|
209
|
+
} finally {
|
|
210
|
+
await wallet.close();
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// THE WITHDRAWAL AS A WHOLE - THE SAME ORDER, ONE CALL. This is a WRAPPER over sign and relay, NOT a second
|
|
215
|
+
// step order: otherwise an edit in one place would diverge from the other.
|
|
216
|
+
async run(request) {
|
|
217
|
+
const opened = await api.sign(request);
|
|
218
|
+
return await api.relay({ token: opened.token });
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
return api;
|
|
222
|
+
}
|
package/src/wallet.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// THE WALLET OUTSIDE AND WITHOUT KNOWLEDGE OF THE CHAIN (issue #32, requirement 4; extensibility for Tron and Solana).
|
|
2
|
+
//
|
|
3
|
+
// The SDK core knows neither EIP-1193, nor WalletConnect, nor the Solana wallet-standard. It knows the ADAPTER
|
|
4
|
+
// INTERFACE: vm, connect, disconnect, address, chainId, subscribe, driver - and requires the adapter to be for
|
|
5
|
+
// the SAME vm as the settlement network. The interface brings the wallet:
|
|
6
|
+
// `import { evmWallet } from "@ninsei-labs/sdk/adapters"`.
|
|
7
|
+
//
|
|
8
|
+
// It opens no windows and does not reach into others' settings: for EVM the network is switched through the
|
|
9
|
+
// provider, for other VMs it may not exist at all - then the honest answer is `not-implemented`, not a silent
|
|
10
|
+
// "nothing happened".
|
|
11
|
+
import { ERROR_CODES } from "./codes.mjs";
|
|
12
|
+
import { SdkError, fail } from "./errors.mjs";
|
|
13
|
+
|
|
14
|
+
const REQUIRED = ["connect", "disconnect", "address", "chainId", "subscribe", "driver"];
|
|
15
|
+
|
|
16
|
+
/** The adapter's refusal code, if it is declared by the contract; otherwise `unknown` (the adapter's text does not go out). */
|
|
17
|
+
const codeOf = (error) => (error && ERROR_CODES.includes(error.code) ? error.code : "unknown");
|
|
18
|
+
|
|
19
|
+
// THE OTHER SIDE OF THE SWAP IS MONERO. It cannot be the settlement network: Monero has no leg contract, so its
|
|
20
|
+
// wallet arrives as an adapter with this VM, and it reads the balance itself - there is no leg to delegate that to.
|
|
21
|
+
const XMR_VM = "monero";
|
|
22
|
+
// THE ATOMIC UNIT OF XMR: 1 XMR = 10^12 piconero (12 decimal places). monero-ts returns getUnlockedBalance in
|
|
23
|
+
// these, and sweep takes the free remainder in them too; the core does NOT reconvert these units.
|
|
24
|
+
const XMR_TOKEN = { token: "xmr", symbol: "XMR", decimals: 12 };
|
|
25
|
+
// THE STAGE AT WHICH SWITCHING THE NETWORK OF OTHER VMs WILL ARRIVE: stage 2 (sdk/README.md, "Stages") - the demo
|
|
26
|
+
// screens move onto the SDK. Until then the wallet switches the network itself, and that is NAMED by a code with
|
|
27
|
+
// the stage number, not swallowed.
|
|
28
|
+
const SWITCH_STAGE = 2;
|
|
29
|
+
|
|
30
|
+
export function createWallet({ settlement, leg }) {
|
|
31
|
+
let adapter = null;
|
|
32
|
+
let cached = { connected: false, address: null, chainId: null, chainMatches: false };
|
|
33
|
+
const listeners = new Set();
|
|
34
|
+
let detach = null;
|
|
35
|
+
|
|
36
|
+
const settle = async (notify = true) => {
|
|
37
|
+
if (!adapter) return cached;
|
|
38
|
+
const [address, chainId] = await Promise.all([
|
|
39
|
+
Promise.resolve(adapter.address()).catch(() => null),
|
|
40
|
+
Promise.resolve(adapter.chainId()).catch(() => null),
|
|
41
|
+
]);
|
|
42
|
+
const connected = typeof address === "string" && address.length > 0;
|
|
43
|
+
cached = {
|
|
44
|
+
connected,
|
|
45
|
+
address: connected ? address : null,
|
|
46
|
+
chainId: chainId === undefined ? null : chainId,
|
|
47
|
+
chainMatches: connected && leg.sameChain(chainId, settlement),
|
|
48
|
+
};
|
|
49
|
+
if (notify) for (const handler of listeners) handler(cached);
|
|
50
|
+
return cached;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const close = () => {
|
|
54
|
+
if (typeof detach === "function") detach();
|
|
55
|
+
detach = null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
use(next) {
|
|
60
|
+
if (!next || typeof next !== "object") fail("bad-input", { field: "adapter" });
|
|
61
|
+
const missing = REQUIRED.filter((name) => typeof next[name] !== "function");
|
|
62
|
+
if (missing.length) fail("bad-input", { field: "adapter", missing });
|
|
63
|
+
if (typeof next.vm !== "string" || !next.vm) fail("bad-input", { field: "adapter.vm" });
|
|
64
|
+
// ANOTHER VM IS NOT SUBSTITUTED: the wallet is good either for the settlement network or for Monero - the
|
|
65
|
+
// other side of the swap (which has neither a leg driver nor the settlement network's chainId). Anything
|
|
66
|
+
// else is a refusal naming the vm.
|
|
67
|
+
const acceptedVms = [settlement.vm, XMR_VM];
|
|
68
|
+
if (!acceptedVms.includes(next.vm)) fail("bad-input", { field: "adapter.vm", expected: settlement.vm, accepted: acceptedVms, got: next.vm });
|
|
69
|
+
close();
|
|
70
|
+
adapter = next;
|
|
71
|
+
detach = adapter.subscribe(() => { settle().catch(() => {}); });
|
|
72
|
+
cached = { connected: false, address: null, chainId: null, chainMatches: false };
|
|
73
|
+
settle().catch(() => {});
|
|
74
|
+
return undefined;
|
|
75
|
+
},
|
|
76
|
+
// THE STATE IS RETURNED FROM MEMORY: reading from the wallet is asynchronous, while the interface needs the
|
|
77
|
+
// answer in the same render tick. Fresh reads are done by connect(), switchChain() and wallet events - they
|
|
78
|
+
// update the memory.
|
|
79
|
+
status: () => cached,
|
|
80
|
+
async connect({ chainId = null } = {}) {
|
|
81
|
+
if (!adapter) fail("wallet-not-connected");
|
|
82
|
+
try {
|
|
83
|
+
// THE SETTLEMENT NETWORK IS NAMED ONLY TO ITS OWN WALLET: we do not tell the Monero wallet a foreign
|
|
84
|
+
// chainId - its network is set by the Monero network, and an EVM chainId means nothing to it.
|
|
85
|
+
const want = chainId === null && adapter.vm === settlement.vm ? leg.walletChainId(settlement) : chainId;
|
|
86
|
+
await adapter.connect(want === null ? {} : { chainId: want });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new SdkError(codeOf(error), { vm: adapter.vm });
|
|
89
|
+
}
|
|
90
|
+
return settle();
|
|
91
|
+
},
|
|
92
|
+
async disconnect() {
|
|
93
|
+
if (adapter) await Promise.resolve(adapter.disconnect()).catch(() => {});
|
|
94
|
+
close();
|
|
95
|
+
adapter = null;
|
|
96
|
+
return void (await settle());
|
|
97
|
+
},
|
|
98
|
+
address: () => cached.address,
|
|
99
|
+
async switchChain(chainId) {
|
|
100
|
+
if (!adapter) fail("wallet-not-connected");
|
|
101
|
+
if (!Number.isFinite(Number(chainId))) fail("bad-input", { field: "chainId" });
|
|
102
|
+
// THE SETTLEMENT NETWORK IS SWITCHED BY THE SETTLEMENT WALLET, and only if it can. For another VM switching
|
|
103
|
+
// the network lives only in the wallet itself - then a NAMED refusal with the stage number, not silence: the
|
|
104
|
+
// interface will show its own action instead of deciding everything has already switched.
|
|
105
|
+
if (adapter.vm !== settlement.vm || typeof adapter.switchChain !== "function") {
|
|
106
|
+
fail("not-implemented", { surface: "wallet.switchChain", vm: adapter.vm, stage: SWITCH_STAGE });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await adapter.switchChain(Number(chainId));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new SdkError(codeOf(error), { vm: adapter.vm });
|
|
112
|
+
}
|
|
113
|
+
// DO NOT TAKE IT ON TRUST - CHECK IT: the wallet could "switch" and remain on the previous network. We
|
|
114
|
+
// re-read the wallet's network (settle) and refuse if it is not the right one: otherwise a foreign balance
|
|
115
|
+
// would look like your own, and the signature would go to the wrong chain.
|
|
116
|
+
const status = await settle();
|
|
117
|
+
if (!status.chainMatches) fail("wrong-chain", { got: status.chainId, want: leg.walletChainId(settlement) });
|
|
118
|
+
return status;
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* BALANCES in minimal units (string): the native coin and the settlement network's tokens.
|
|
122
|
+
* They are read by the leg driver; an unread balance comes as null, not zero.
|
|
123
|
+
*/
|
|
124
|
+
async balances(tokens) {
|
|
125
|
+
if (!adapter) fail("wallet-not-connected");
|
|
126
|
+
const status = cached.connected ? cached : await settle(false);
|
|
127
|
+
if (!status.connected) fail("wallet-not-connected");
|
|
128
|
+
// THE MONERO BALANCE IS THE WALLET'S OWN BUSINESS. Neither the leg nor the network check apply here: Monero
|
|
129
|
+
// has no leg driver and no settlement-network chainId. The wallet returns the free remainder BY THE SAME
|
|
130
|
+
// method as the XMR withdrawal (unlockedBalance; in monero-ts that is getUnlockedBalance), and in the same
|
|
131
|
+
// atomic units.
|
|
132
|
+
// THE UNITS ARE NOT RECONVERTED: an atomic unit arrives (1 XMR = 10^12), and BigInt turns it into a string -
|
|
133
|
+
// exactly as in sweep.mjs. The core keeps no unit arithmetic of its own.
|
|
134
|
+
if (adapter.vm === XMR_VM) {
|
|
135
|
+
const wantedXmr = Array.isArray(tokens) && tokens.length ? tokens.map((t) => String(t).toLowerCase()) : [XMR_TOKEN.token];
|
|
136
|
+
const unknownXmr = wantedXmr.find((id) => id !== XMR_TOKEN.token);
|
|
137
|
+
if (unknownXmr !== undefined) fail("bad-input", { field: "tokens", unknown: unknownXmr });
|
|
138
|
+
if (typeof adapter.unlockedBalance !== "function") fail("not-implemented", { surface: "wallet.balances", vm: XMR_VM });
|
|
139
|
+
const amount = BigInt(await adapter.unlockedBalance()).toString();
|
|
140
|
+
return [{ token: XMR_TOKEN.token, symbol: XMR_TOKEN.symbol, decimals: XMR_TOKEN.decimals, amount }];
|
|
141
|
+
}
|
|
142
|
+
// ON A FOREIGN NETWORK THE BALANCE IS NOT READ: it is a number from a different network, and it would look like an empty wallet.
|
|
143
|
+
if (!status.chainMatches) fail("wrong-chain", { got: status.chainId, want: leg.walletChainId(settlement) });
|
|
144
|
+
if (typeof leg.balances !== "function") fail("not-implemented", { surface: "wallet.balances", vm: settlement.vm });
|
|
145
|
+
const all = settlement.tokens || [];
|
|
146
|
+
const wanted = Array.isArray(tokens) && tokens.length ? tokens.map((t) => String(t).toLowerCase()) : all.map((t) => t.id);
|
|
147
|
+
const picked = [];
|
|
148
|
+
for (const id of wanted) {
|
|
149
|
+
const token = all.find((t) => t.id === id);
|
|
150
|
+
if (!token) fail("bad-input", { field: "tokens", unknown: id });
|
|
151
|
+
picked.push(token);
|
|
152
|
+
}
|
|
153
|
+
return leg.balances({ driver: adapter.driver(), address: status.address, tokens: picked });
|
|
154
|
+
},
|
|
155
|
+
subscribe(handler) {
|
|
156
|
+
if (typeof handler !== "function") fail("bad-input", { field: "handler" });
|
|
157
|
+
listeners.add(handler);
|
|
158
|
+
return () => listeners.delete(handler);
|
|
159
|
+
},
|
|
160
|
+
// INTERNAL, FOR THE LEG DRIVER: the provider/wallet client itself. It is not in the contract: the interface
|
|
161
|
+
// does not need it, and the leg takes it from the SDK in one place.
|
|
162
|
+
driver: () => (adapter ? adapter.driver() : null),
|
|
163
|
+
};
|
|
164
|
+
}
|