@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +866 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/engine/atomic/dleq2.js +330 -0
  5. package/engine/atomic/halfenc.js +205 -0
  6. package/engine/atomic/halves.js +119 -0
  7. package/engine/atomic/order-client.js +57 -0
  8. package/engine/atomic/order.js +132 -0
  9. package/engine/atomic/orderContext.js +70 -0
  10. package/engine/atomic/orderHalfCommit.js +29 -0
  11. package/engine/core/chainSource.js +306 -0
  12. package/engine/core/clock.js +40 -0
  13. package/engine/core/config.js +885 -0
  14. package/engine/core/finality.js +93 -0
  15. package/engine/core/format.js +168 -0
  16. package/engine/core/preSignGuards.js +495 -0
  17. package/engine/core/rfqSource.js +193 -0
  18. package/engine/core/sellWindow.js +43 -0
  19. package/engine/core/store.js +100 -0
  20. package/engine/core/swap.js +1153 -0
  21. package/engine/evm/amounts.js +131 -0
  22. package/engine/evm/auth.js +156 -0
  23. package/engine/evm/claimGas.js +87 -0
  24. package/engine/evm/depositor.js +127 -0
  25. package/engine/evm/dex.js +836 -0
  26. package/engine/evm/escrow.js +208 -0
  27. package/engine/evm/factory.js +149 -0
  28. package/engine/evm/fees.js +90 -0
  29. package/engine/evm/funding.js +309 -0
  30. package/engine/evm/gasReserve.js +88 -0
  31. package/engine/evm/index.js +123 -0
  32. package/engine/evm/injected.js +175 -0
  33. package/engine/evm/permit.js +125 -0
  34. package/engine/evm/prices.js +179 -0
  35. package/engine/evm/session.js +777 -0
  36. package/engine/mock/chain.js +98 -0
  37. package/engine/monero/address.js +265 -0
  38. package/engine/monero/halvesSweep.js +68 -0
  39. package/engine/monero/node.js +110 -0
  40. package/engine/monero/scan.js +325 -0
  41. package/engine/monero/swapKeys.js +69 -0
  42. package/engine/monero/unlock.js +46 -0
  43. package/engine/monero/wallet.js +168 -0
  44. package/engine/recovery/recoveryFile.js +363 -0
  45. package/engine/recovery/restore.js +176 -0
  46. package/engine/sdk/bridge.js +609 -0
  47. package/engine/ui/dom.js +90 -0
  48. package/engine/ui/walletModal.js +132 -0
  49. package/index.d.ts +1417 -0
  50. package/package.json +74 -0
  51. package/src/actions.mjs +72 -0
  52. package/src/adapters/evm-wallet.mjs +126 -0
  53. package/src/adapters/index.mjs +21 -0
  54. package/src/adapters/local-storage.mjs +32 -0
  55. package/src/adapters/monero-wallet.mjs +185 -0
  56. package/src/adapters/monero.mjs +51 -0
  57. package/src/adapters/node-test-evm-wallet.mjs +345 -0
  58. package/src/codes.mjs +188 -0
  59. package/src/config.mjs +277 -0
  60. package/src/engine.mjs +53 -0
  61. package/src/errors.mjs +24 -0
  62. package/src/http.mjs +72 -0
  63. package/src/index.mjs +256 -0
  64. package/src/legs/evm.mjs +118 -0
  65. package/src/legs/index.mjs +50 -0
  66. package/src/levelsWalkSpec.mjs +183 -0
  67. package/src/lock.mjs +140 -0
  68. package/src/order.mjs +212 -0
  69. package/src/preflight.mjs +137 -0
  70. package/src/primitives.mjs +47 -0
  71. package/src/quoteEip712.mjs +35 -0
  72. package/src/quoteEip712Spec.mjs +214 -0
  73. package/src/quotes.mjs +465 -0
  74. package/src/record.mjs +65 -0
  75. package/src/recovery.mjs +40 -0
  76. package/src/sim.mjs +37 -0
  77. package/src/swap-flow.mjs +492 -0
  78. package/src/swaps.mjs +935 -0
  79. package/src/sweep.mjs +222 -0
  80. package/src/wallet.mjs +164 -0
  81. package/src/xmr-arrival.mjs +289 -0
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@ninsei-labs/sdk",
3
+ "version": "0.39.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "packageManager": "pnpm@9.15.9",
9
+ "description": "XMR <-> EVM swap engine: quotes, guards, order steps, recovery (no DOM, no user-facing strings)",
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "main": "src/index.mjs",
13
+ "scripts": {
14
+ "check": "node tools/check.mjs",
15
+ "check:syntax": "node tools/check.mjs --only=syntax",
16
+ "check:import": "node tools/check.mjs --only=import",
17
+ "check:surface": "node tools/check.mjs --only=surface",
18
+ "check:mirror": "node tools/check.mjs --only=mirror",
19
+ "check:language": "node tools/check.mjs --only=language",
20
+ "check:pack": "node tools/check.mjs --only=pack",
21
+ "check:secrets": "node tools/check.mjs --only=secrets"
22
+ },
23
+ "types": "./index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./index.d.ts",
27
+ "import": "./src/index.mjs",
28
+ "default": "./src/index.mjs"
29
+ },
30
+ "./adapters": {
31
+ "default": "./src/adapters/index.mjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "src",
36
+ "engine",
37
+ "index.d.ts",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "keywords": [
46
+ "xmr",
47
+ "monero",
48
+ "evm",
49
+ "ethereum",
50
+ "atomic-swap",
51
+ "cross-chain",
52
+ "swap",
53
+ "sdk",
54
+ "quotes",
55
+ "guards",
56
+ "recovery",
57
+ "esm",
58
+ "typescript"
59
+ ],
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/Ninsei-Labs/sdk.git"
63
+ },
64
+ "homepage": "https://github.com/Ninsei-Labs/sdk#readme",
65
+ "bugs": {
66
+ "url": "https://github.com/Ninsei-Labs/sdk/issues"
67
+ },
68
+ "dependencies": {
69
+ "@noble/curves": "^2.4.0",
70
+ "@noble/hashes": "^2.4.0",
71
+ "keccak256": "^1.0.6"
72
+ },
73
+ "sideEffects": false
74
+ }
@@ -0,0 +1,72 @@
1
+ // ORDER ACTIONS: MARKING READY, CLAIMING, REFUNDING.
2
+ //
3
+ // All three go through ONE wallet seam (the `send`/`receipt` role) and through the slot check that in the engine
4
+ // stands BEFORE signing. What is added here is exactly what the engine cannot do: `expect` is MANDATORY.
5
+ //
6
+ // Why it is mandatory. The slot check compares the order we assembled with the one lying on the chain. Without
7
+ // expectations there is nothing to compare against, and the signature goes out under an order whose commitments
8
+ // nobody checked: for marking ready that is granting a foreign address the right to take the settlement, for
9
+ // claiming it is revealing your own half (irreversible), for refunding the same reveal. The transaction still
10
+ // looks successful.
11
+ import { SdkError } from "./errors.mjs";
12
+ import * as engine from "./engine.mjs";
13
+ // THE DEAL FLOW IS NOW A PACKAGE MODULE, NOT AN ENGINE FILE (#32, wave 3): www/js/core/swap-flow.js moved to
14
+ // sdk/src/swap-flow.mjs. The actions therefore call the module directly instead of reaching it through the
15
+ // mirror - there is exactly one implementation, and it lives here.
16
+ import * as flow from "./swap-flow.mjs";
17
+
18
+ const missingFields = (object, fields) =>
19
+ fields.filter((k) => object[k] === undefined || object[k] === null || object[k] === "");
20
+
21
+ const walletOrFail = (wallet) => {
22
+ const role = ["send", "receipt"];
23
+ const missing = wallet ? role.filter((k) => typeof wallet[k] !== "function") : role;
24
+ if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
25
+ return wallet;
26
+ };
27
+
28
+ const requestOrFail = (request, extra) => {
29
+ if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
30
+ const missing = missingFields(request, ["escrow", "expect", ...extra]);
31
+ if (missing.length) throw new SdkError("bad-input", { field: "request", missing });
32
+ return request;
33
+ };
34
+
35
+ // The engine's refusal arrives as text; a CODE goes out. Texts are the interface's business, the core speaks in codes.
36
+ const withCodes = async (step, work) => {
37
+ try { return await work(); }
38
+ catch (error) {
39
+ if (error && typeof error.code === "string") throw error;
40
+ throw new SdkError("contract-reverted", { step });
41
+ }
42
+ };
43
+
44
+ export function createActions({ call = null } = {}) {
45
+ // WHAT READS THE CHAIN IS ALSO A SEAM, and not only for the checks: the slot check and the signature must go
46
+ // ONE way, otherwise part of the check goes past the wallet and part does not. By default - the engine's read.
47
+ const reader = call || engine.evm.readContract;
48
+ const deps = (wallet) => ({ call: reader, send: (tx) => wallet.send(tx), receipt: (h) => wallet.receipt(h) });
49
+ return {
50
+ /** Marking "I saw the XMR on the shared address". Without it, claiming is forbidden by the contract. */
51
+ async markReady(request, wallet) {
52
+ requestOrFail(request, []);
53
+ walletOrFail(wallet);
54
+ const { escrow, expect, onStep } = request;
55
+ return await withCodes("mark-ready", () => flow.markReadyOrder({ escrow, expect, onStep, deps: deps(wallet) }));
56
+ },
57
+ /** Claiming: it reveals your own half, so expectations are mandatory. */
58
+ async claim(request, wallet) {
59
+ requestOrFail(request, ["halfClaimer"]);
60
+ walletOrFail(wallet);
61
+ const { escrow, halfClaimer, expect, onStep } = request;
62
+ return await withCodes("claim", () => flow.claimOrder({ escrow, halfClaimer, expect, onStep, deps: deps(wallet) }));
63
+ },
64
+ /** Refunding: it also reveals a half. The core passes the expectations ALWAYS, though the engine does not require them. */
65
+ async refund(request, wallet) {
66
+ requestOrFail(request, ["halfLocker"]);
67
+ walletOrFail(wallet);
68
+ const { escrow, halfLocker, expect, onStep } = request;
69
+ return await withCodes("refund", () => flow.refundOrder({ escrow, halfLocker, expect, onStep, deps: deps(wallet) }));
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,126 @@
1
+ // THE EVM WALLET: a wrapper over an EIP-1193 provider.
2
+ //
3
+ // The SDK core knows only the adapter interface (vm, connect, address, chainId, subscribe, driver). The provider is
4
+ // brought by the interface - injected, WalletConnect, anything: the SDK does not look for wallets itself and does
5
+ // not open windows.
6
+ //
7
+ // The EVM leg driver gets the provider through `driver()` and makes eth_* calls through it. For another VM its own
8
+ // adapter will appear (`tronWallet`, `solanaWallet`), and the core will not need to change for it.
9
+ const hexToInt = (value) => (typeof value === "string" && /^0x[0-9a-f]+$/i.test(value) ? Number.parseInt(value, 16) : null);
10
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
11
+
12
+ import { SdkError } from "../errors.mjs";
13
+ // THE NETWORK REGISTRY - THROUGH THE ENGINE'S SEAM. Adding a network to the wallet (wallet_addEthereumChain)
14
+ // requires the network's name, native coin, rpc and explorer, and all of that is already in the registry the page
15
+ // uses (www/js/evm/session.js, switchChain). The adapter keeps no network table of its own and invents no
16
+ // addresses.
17
+ import { config as engineConfig } from "../engine.mjs";
18
+
19
+ const hexChainId = (id) => "0x" + Number(id).toString(16);
20
+ // 4902 - the wallet does not know such a network; older wallets answer with text, so we check that too.
21
+ const CHAIN_UNKNOWN = 4902;
22
+ const isUnknownChain = (error) => {
23
+ const code = error && (error.code || (error.data && error.data.originalError && error.data.originalError.code));
24
+ return code === CHAIN_UNKNOWN || /unrecognized chain|not added/i.test(String((error && error.message) || ""));
25
+ };
26
+ const isRejected = (error) => (error && error.code === 4001) || /rejected|denied/i.test(String((error && error.message) || ""));
27
+
28
+ export function evmWallet(provider, { id = "evm" } = {}) {
29
+ const handoff = [];
30
+ return {
31
+ vm: "evm",
32
+ id,
33
+ async connect({ chainId = null } = {}) {
34
+ try {
35
+ await provider.request({ method: "eth_requestAccounts" });
36
+ } catch (error) {
37
+ const rejected = error && (error.code === 4001 || error.code === "ACTION_REJECTED");
38
+ const failure = new Error(rejected ? "wallet-rejected" : "wallet-not-connected");
39
+ failure.code = rejected ? "wallet-rejected" : "wallet-not-connected";
40
+ throw failure;
41
+ }
42
+ if (Number.isFinite(chainId)) {
43
+ const current = hexToInt(await provider.request({ method: "eth_chainId" }).catch(() => null));
44
+ if (current !== Number(chainId)) await this.switchChain(chainId);
45
+ }
46
+ },
47
+ async disconnect() {
48
+ while (handoff.length) {
49
+ const [event, handler] = handoff.pop();
50
+ if (typeof provider.removeListener === "function") provider.removeListener(event, handler);
51
+ }
52
+ },
53
+ async address() {
54
+ const accounts = await provider.request({ method: "eth_accounts" }).catch(() => null);
55
+ const first = Array.isArray(accounts) ? accounts[0] : null;
56
+ return typeof first === "string" && ADDRESS_RE.test(first) ? first : null;
57
+ },
58
+ async chainId() {
59
+ return hexToInt(await provider.request({ method: "eth_chainId" }).catch(() => null));
60
+ },
61
+ subscribe(handler) {
62
+ if (typeof provider.on !== "function") return () => {};
63
+ for (const event of ["accountsChanged", "chainChanged"]) {
64
+ const wrapped = () => handler();
65
+ provider.on(event, wrapped);
66
+ handoff.push([event, wrapped]);
67
+ }
68
+ return () => {};
69
+ },
70
+ // THE SAME PATH AS THE PAGE (www/js/evm/session.js, switchChain): first wallet_switchEthereumChain, and if the
71
+ // wallet does not know such a network - offer to add it via wallet_addEthereumChain. The data for adding is
72
+ // taken from the engine's registry, not from a private table.
73
+ async switchChain(chainId) {
74
+ const target = Number(chainId);
75
+ const hex = hexChainId(target);
76
+ try {
77
+ await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: hex }] });
78
+ } catch (error) {
79
+ if (!isUnknownChain(error)) throw new SdkError(isRejected(error) ? "wallet-rejected" : "wrong-chain", { vm: "evm" });
80
+ const chain = typeof engineConfig.chainByChainId === "function" ? engineConfig.chainByChainId(target) : null;
81
+ // The network is not in the registry - there is nothing to add it with, and the rpc/explorer must not be
82
+ // invented: we name the refusal with a code.
83
+ if (!chain) throw new SdkError("wrong-chain", { vm: "evm", chainId: target });
84
+ try {
85
+ await provider.request({
86
+ method: "wallet_addEthereumChain",
87
+ params: [{
88
+ chainId: hex,
89
+ chainName: chain.name,
90
+ nativeCurrency: { name: chain.native.symbol, symbol: chain.native.symbol, decimals: chain.native.decimals },
91
+ rpcUrls: [chain.rpcUrl],
92
+ blockExplorerUrls: [chain.explorerAddr],
93
+ }],
94
+ });
95
+ } catch (error2) {
96
+ throw new SdkError(isRejected(error2) ? "wallet-rejected" : "wrong-chain", { vm: "evm" });
97
+ }
98
+ }
99
+ },
100
+ // SENDING A TRANSACTION AND THE RECEIPT. Without them the funds cannot be locked: signing is possible only in
101
+ // the wallet, and confirmation of execution arrives as a receipt.
102
+ //
103
+ // THERE IS DELIBERATELY NO POLLING HERE: `receipt` asks once and returns the receipt, or null if the transaction
104
+ // is not in a block yet. The waiting policy (how long, how often, what on refusal) is set by the caller: a
105
+ // policy of its own inside the adapter would mean two calls wait differently.
106
+ async send({ to, data = "0x", value = 0n, gas = null }) {
107
+ if (typeof to !== "string" || !to) throw new SdkError("bad-input", { field: "send.to" });
108
+ const accounts = await provider.request({ method: "eth_accounts" });
109
+ const from = Array.isArray(accounts) ? accounts[0] : null;
110
+ if (!from) throw new SdkError("wallet-not-connected", { step: "send" });
111
+ const hex = (v) => "0x" + BigInt(v).toString(16);
112
+ // GAS IS PASSED IF THE CALLER SET IT: every contract call has its own estimate (marking ready, claiming,
113
+ // refunding), and it comes from the same place as the calldata - the engine's EVM layer.
114
+ const tx = { from, to, data, value: hex(value) };
115
+ if (gas !== null && gas !== undefined) tx.gas = hex(gas);
116
+ return await provider.request({ method: "eth_sendTransaction", params: [tx] });
117
+ },
118
+ async receipt(txHash) {
119
+ if (typeof txHash !== "string" || !txHash) throw new SdkError("bad-input", { field: "receipt.txHash" });
120
+ return await provider.request({ method: "eth_getTransactionReceipt", params: [txHash] });
121
+ },
122
+ driver() {
123
+ return provider;
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,21 @@
1
+ // ADAPTERS THE INTERFACE WIRES IN EXPLICITLY.
2
+ //
3
+ // Two different kinds of adapters, and both are here because both are about the environment, not the core:
4
+ // wallet - a wrapper over a concrete way to sign (today EVM/EIP-1193; later Tron, Solana);
5
+ // storage - where unfinished swaps live (localStorage today, memory in tests).
6
+ //
7
+ // This directory is the only place in the SDK allowed to touch the DOM and to know about the browser
8
+ // environment (enforced by machine in tools/check-sdk.mjs).
9
+ export { evmWallet } from "./evm-wallet.mjs";
10
+ export { localStorageAdapter, memoryAdapter } from "./local-storage.mjs";
11
+ export { moneroWallet } from "./monero.mjs";
12
+ // THE MONERO WALLET FOR THE CORE - SEPARATE FROM THE WITHDRAWAL WALLET. `moneroWallet` above plays the XMR
13
+ // withdrawal role (open/sync/sweepUnlocked/relay); `moneroWalletAdapter` plays the SDK wallet role (vm/connect/
14
+ // address/chainId/unlockedBalance/subscribe/driver). One file for two roles would mix their surfaces.
15
+ export { moneroWalletAdapter } from "./monero-wallet.mjs";
16
+ // A TOOL OF THE TEST CONTOUR - SEPARATE FROM THE PAGE'S WALLET. It signs with its own key (the key comes only
17
+ // from the environment or a file) and refuses to sign on a production network by the engine's marker, so it is
18
+ // only good for local runs. A separate line here also because every file in the directory must be reachable
19
+ // through the package's map: otherwise the adapter exists but cannot be imported.
20
+ export { nodeTestEvmWallet, evmRpcReader, evmRpcProvider, testPrivateKeyFromEnv, isProductionChain, addressOfPrivateKey }
21
+ from "./node-test-evm-wallet.mjs";
@@ -0,0 +1,32 @@
1
+ // A READY-MADE STORAGE ADAPTER FOR THE BROWSER. The only SDK file allowed to touch the DOM.
2
+ //
3
+ // Why it is kept separate and wired in explicitly: the SDK core must load in bare Node (this is checked by
4
+ // machine), while `localStorage` exists only in the browser. An interface that needs browser storage writes
5
+ // `import { localStorageAdapter } from "@ninsei-labs/sdk/adapters"` - that is, it makes the DOM decision.
6
+ //
7
+ // The adapter is deliberately thin: the swap storage is the interface's data, and it may replace it with
8
+ // sessionStorage, IndexedDB or memory (tests).
9
+
10
+ export function localStorageAdapter(scope = "ninsei.sdk") {
11
+ const key = (name) => scope + "." + name;
12
+ return {
13
+ get(name) {
14
+ return localStorage.getItem(key(name));
15
+ },
16
+ set(name, value) {
17
+ localStorage.setItem(key(name), String(value));
18
+ },
19
+ remove(name) {
20
+ localStorage.removeItem(key(name));
21
+ },
22
+ };
23
+ }
24
+
25
+ export function memoryAdapter(initial = {}) {
26
+ const map = new Map(Object.entries(initial));
27
+ return {
28
+ get: (name) => (map.has(name) ? map.get(name) : null),
29
+ set: (name, value) => map.set(name, String(value)),
30
+ remove: (name) => map.delete(name),
31
+ };
32
+ }
@@ -0,0 +1,185 @@
1
+ // THE MONERO WALLET ADAPTER FOR THE SDK CORE: network, its own address, free remainder, sync, stop.
2
+ //
3
+ // WHAT THIS IS AND HOW IT DIFFERS FROM ITS NEIGHBOUR. Next to it lies `monero.mjs` - it plays a DIFFERENT role: it
4
+ // is the XMR WITHDRAWAL wallet for `sweep.run` (open / sync / unlockedBalance / sweepUnlocked / relay / close).
5
+ // Here is a wallet connected to the core through THE SAME interface as the EVM wallet (`vm`, `connect`,
6
+ // `disconnect`, `address`, `chainId`, `subscribe`, `driver`) plus `unlockedBalance` - the method the core reads
7
+ // the free remainder with (sdk/src/wallet.mjs). The roles differ, and they must not be mixed: a withdrawal and a
8
+ // wallet have different surfaces.
9
+ //
10
+ // THE LIBRARY COMES FROM OUTSIDE - THERE IS NO WALLET BUILD OF ITS OWN HERE. The wallet is built BY THE SAME CALL
11
+ // the withdrawal page builds it with (sweep/sweep.js): `createWalletFull({ networkType, server: { uri }, password,
12
+ // restoreHeight, seed | privateSpendKey/privateViewKey/primaryAddress })`, then `getAddress(0, 0)`,
13
+ // `setRestoreHeight`, `getRestoreHeight`, `sync`, `getUnlockedBalance`, `close`. The names were taken from the page
14
+ // and verified by a run against the installed monero-ts 0.11.17 build, not chosen from memory. The function is
15
+ // brought through the `createWallet` parameter (in the browser - `api.createWalletFull` from the bundle, in node -
16
+ // from monero-ts): the core drags in neither WASM nor the page's 3.6 MB worker. The same technique as
17
+ // `moneroWallet` for withdrawal.
18
+ //
19
+ // THE NETWORK IS NOT INVENTED. The network name is checked against the core's registry (www/js/core/config.js,
20
+ // XMR_NETWORKS) through the sdk/src/engine.mjs seam, and the SHAPE of its own address - by the same address module
21
+ // as the demo's (www/js/monero/address.js, networkFromShape): an address of another network is rejected with a
22
+ // CODE, not returned as a balance of the wrong chain.
23
+ //
24
+ // REFUSALS - WITH CODES. No library, no network, no node, no key, no height, wallet not connected: an SdkError
25
+ // with a code and params. Neither `null` nor a zero in place of the answer: an empty address and a zero balance
26
+ // look like real data, and that is exactly why they are not returned.
27
+ import { SdkError } from "../errors.mjs";
28
+ import { config as engineConfig, xmrAddress } from "../engine.mjs";
29
+
30
+ const XMR_VM = "monero";
31
+ // The wallet session label - the same as the withdrawal page's. This is not a secret and not an address, but a session name.
32
+ const DEFAULT_PASSWORD = "arrakis-wallet-session";
33
+
34
+ /** Monero network names declared by the core's registry. The adapter keeps no table of its own. */
35
+ const knownNetworks = () => {
36
+ const declared = engineConfig.XMR_NETWORKS || {};
37
+ return [...new Set(Object.values(declared).filter((v) => typeof v === "string" && v))].sort();
38
+ };
39
+
40
+ /**
41
+ * The Monero wallet as an SDK wallet adapter.
42
+ *
43
+ * createWallet - the library function (`createWalletFull`): without it there is nothing to build the wallet with,
44
+ * and `connect` answers with a code rather than trying to guess the library;
45
+ * network - a network name from the core's registry (`stagenet`/`mainnet`/`testnet`), as monero-ts accepts it;
46
+ * serverUri - the node (in production - a proxy, on the stand - your own node);
47
+ * keys - `{ seed }` or `{ privateSpendKey, privateViewKey, primaryAddress }` - the page's shape;
48
+ * restoreHeight- the restore height: without it a scan of the whole Monero history takes tens of hours;
49
+ * expectedAddress - if set, the derived address must match (otherwise the file/key is wrong).
50
+ */
51
+ export function moneroWalletAdapter({
52
+ createWallet = null,
53
+ network = null,
54
+ serverUri = null,
55
+ password = DEFAULT_PASSWORD,
56
+ keys = {},
57
+ restoreHeight = 0,
58
+ expectedAddress = null,
59
+ } = {}) {
60
+ let wallet = null;
61
+ let address = null;
62
+ const listeners = new Set();
63
+
64
+ const notify = () => {
65
+ for (const handler of listeners) {
66
+ try { handler(); } catch { /* a subscriber must not break the wallet */ }
67
+ }
68
+ };
69
+
70
+ const need = (surface) => {
71
+ if (wallet === null) throw new SdkError("wallet-not-connected", { vm: XMR_VM, surface });
72
+ return wallet;
73
+ };
74
+
75
+ // THE KEY SHAPE - the same one the page passes to createWalletFull. The primary address is required when a view
76
+ // key is set: the page caught this with the message "must provide primary address if providing private view key".
77
+ const keyFields = () => {
78
+ if (typeof keys.seed === "string" && keys.seed) return { seed: keys.seed };
79
+ if (keys.privateSpendKey || keys.privateViewKey) {
80
+ if (!keys.primaryAddress) throw new SdkError("bad-input", { field: "primaryAddress", why: "required-with-private-keys" });
81
+ const fields = {};
82
+ if (keys.privateSpendKey) fields.privateSpendKey = keys.privateSpendKey;
83
+ if (keys.privateViewKey) fields.privateViewKey = keys.privateViewKey;
84
+ fields.primaryAddress = keys.primaryAddress;
85
+ return fields;
86
+ }
87
+ throw new SdkError("bad-input", { field: "keys", missing: ["seed", "privateSpendKey"] });
88
+ };
89
+
90
+ const close = async () => {
91
+ if (wallet === null) return undefined;
92
+ const current = wallet;
93
+ wallet = null;
94
+ address = null;
95
+ try { await current.close(); } catch { /* stopping must not break the report */ }
96
+ return undefined;
97
+ };
98
+
99
+ return {
100
+ vm: XMR_VM,
101
+ id: XMR_VM,
102
+
103
+ // CREATING THE WALLET - BY THE SAME PATH AS THE PAGE. The order of checks: library -> network -> node -> key ->
104
+ // height. Each missing item is its OWN code with a named field, not a general refusal.
105
+ async connect() {
106
+ if (typeof createWallet !== "function") throw new SdkError("bad-input", { field: "createWallet" });
107
+ const declared = knownNetworks();
108
+ if (typeof network !== "string" || !network) throw new SdkError("bad-input", { field: "network", known: declared });
109
+ if (declared.length && !declared.includes(network)) throw new SdkError("bad-input", { field: "network", known: declared, got: network });
110
+ if (typeof serverUri !== "string" || !serverUri) throw new SdkError("bad-input", { field: "serverUri" });
111
+ // THE KEY AND THE HEIGHT ARE A CONDITION OF WORK, NOT A SETTING: without the key the wallet cannot be built,
112
+ // without the height a scan of the whole Monero history takes tens of hours (the page's measurement: the height
113
+ // is not an optimization but a condition of work).
114
+ const fields = keyFields();
115
+ const height = Number(restoreHeight);
116
+ if (!Number.isFinite(height) || height <= 0) throw new SdkError("bad-input", { field: "restoreHeight", got: restoreHeight });
117
+
118
+ await close();
119
+ const created = await createWallet({
120
+ networkType: network,
121
+ server: { uri: serverUri },
122
+ password,
123
+ restoreHeight: height,
124
+ ...fields,
125
+ });
126
+ wallet = created;
127
+ // THE ADDRESS AND THE NETWORK. The address is derived from the keys and is available before syncing; its
128
+ // network must match the named one. The check is done by the page's code (www/js/monero/address.js), not by a
129
+ // private prefix form.
130
+ address = String(await wallet.getAddress(0, 0));
131
+ const shape = typeof xmrAddress.networkFromShape === "function" ? xmrAddress.networkFromShape(address) : null;
132
+ if (shape && shape !== network) { await close(); throw new SdkError("wrong-chain", { got: shape, want: network }); }
133
+ if (expectedAddress && address !== expectedAddress) { await close(); throw new SdkError("recovery-failed", { step: "address" }); }
134
+ // THE HEIGHT IS SET EXPLICITLY AND READ BACK: on a live run the height passed when the wallet was created was
135
+ // not applied (the page set it with a second call). We set it - so that the restore height becomes a FACT that
136
+ // can be read (`driver().getRestoreHeight()`), not a promise.
137
+ await wallet.setRestoreHeight(height);
138
+ notify();
139
+ return undefined;
140
+ },
141
+
142
+ async disconnect() { return await close(); },
143
+
144
+ // THE NAME FROM THE PAGE: sweep/sweep.js stops the wallet via close(). The same action, the same name.
145
+ async close() { return await close(); },
146
+
147
+ address() {
148
+ need("address");
149
+ return address;
150
+ },
151
+
152
+ // THE MONERO NETWORK IS NAMED BY A WORD (stagenet/mainnet), not by the settlement network's chainId: there is
153
+ // nothing to compare against - Monero has no leg contract (sdk/src/wallet.mjs, XMR_VM).
154
+ chainId() { return network; },
155
+
156
+ // THE FREE REMAINDER - the same method as the XMR withdrawal (the page: getUnlockedBalance), and the same
157
+ // atomic units (1 XMR = 10^12). Zero comes as zero ONLY when the wallet is connected and the node answered so.
158
+ async unlockedBalance() { return await need("unlockedBalance").getUnlockedBalance(); },
159
+
160
+ // SYNCING - a separate call (the page calls wallet.sync()). Subscribers are notified AFTER the pass.
161
+ async sync() {
162
+ await need("sync").sync();
163
+ notify();
164
+ return undefined;
165
+ },
166
+
167
+ // THE MONERO WALLET HAS NO EVENTS (monero-ts has no on/removeListener): a subscriber receives a notification
168
+ // after connect and after sync, and the subscription promises nothing more. Unsubscribing is the same as the EVM
169
+ // adapter's.
170
+ subscribe(handler) {
171
+ if (typeof handler !== "function") throw new SdkError("bad-input", { field: "handler" });
172
+ listeners.add(handler);
173
+ return () => listeners.delete(handler);
174
+ },
175
+
176
+ // SENDING A LEG TRANSACTION IS NOT THE MONERO WALLET'S BUSINESS. The XMR withdrawal can transfer XMR
177
+ // (sweepUnlocked / relayTxs, see monero.mjs). Here is a refusal with a NAMED surface, not a silent null.
178
+ async send() { throw new SdkError("not-implemented", { surface: "adapter.send", vm: XMR_VM }); },
179
+ async receipt() { throw new SdkError("not-implemented", { surface: "adapter.receipt", vm: XMR_VM }); },
180
+
181
+ // WHAT THE ADAPTER WORKS WITH: the library's wallet itself (it has getHeight, getDaemonHeight, sweepUnlocked and
182
+ // the like). Before connecting - null: there is no wallet yet, and pretending there is one is not allowed.
183
+ driver() { return wallet; },
184
+ };
185
+ }
@@ -0,0 +1,51 @@
1
+ // THE MONERO WALLET AS AN ADAPTER. The core does not drag in WASM or a worker: the library is provided by whoever
2
+ // builds the page.
3
+ //
4
+ // The method names are the ones the sweep page already calls on monero-ts (sweep/sync/getUnlockedBalance/
5
+ // sweepUnlocked/relayTxs/close), so the adapter sits on top of it without adapters, and the core stays DOM-free.
6
+ //
7
+ // IMPORTANT ABOUT WITHDRAWAL SAFETY: signing and sending are split. sweepUnlocked({ relay: false }) only signs;
8
+ // sending is a separate call. That way the interface has a point where it can show the person what exactly will
9
+ // leave, BEFORE sending rather than after.
10
+ import { SdkError } from "../errors.mjs";
11
+
12
+ export function moneroWallet({ createWallet, networkType, serverUri, password = "arrakis-sweep-session", fields = {} }) {
13
+ const build = typeof createWallet === "function" ? createWallet : null;
14
+ // Refusals - with CODES, as everywhere in the core: the phrase for a person is said by the interface, not the adapter.
15
+ const need = () => { if (!wallet) throw new SdkError("bad-input", { field: "wallet", state: "closed" }); };
16
+ let wallet = null;
17
+
18
+ return {
19
+ async open(plan) {
20
+ if (!build) throw new SdkError("bad-input", { field: "createWallet" });
21
+ wallet = await build({
22
+ networkType: plan.networkType || networkType,
23
+ server: { uri: serverUri },
24
+ password,
25
+ restoreHeight: Number(plan.restoreHeight) || 0,
26
+ address: plan.address,
27
+ ...fields,
28
+ });
29
+ return undefined;
30
+ },
31
+ async sync() {
32
+ need();
33
+ await wallet.sync();
34
+ },
35
+ async unlockedBalance() {
36
+ need();
37
+ return await wallet.getUnlockedBalance();
38
+ },
39
+ async sweepUnlocked({ address, relay }) {
40
+ need();
41
+ return await wallet.sweepUnlocked({ address, relay });
42
+ },
43
+ async relay(signed) {
44
+ need();
45
+ return await wallet.relayTxs(signed);
46
+ },
47
+ async close() {
48
+ if (wallet) { try { await wallet.close(); } catch { /* closing must not break the report */ } wallet = null; }
49
+ },
50
+ };
51
+ }