@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
@@ -0,0 +1,345 @@
1
+ // A TEST EVM WALLET FOR NODE: KEY FROM THE ENVIRONMENT, REFUSAL ON A PRODUCTION NETWORK.
2
+ //
3
+ // WHY A SEPARATE ADAPTER. The SDK had only the browser wallet (evm-wallet.mjs): it receives the page's ready-made
4
+ // EIP-1193 provider and signs via eth_sendTransaction - that is, the person's wallet does the signing. From Node
5
+ // there is nothing to sign with, and a live run on a local chain was impossible: the SDK refused at the very first
6
+ // step that needed a signature. This file closes exactly that hole - and only it.
7
+ //
8
+ // THIS IS A TOOL OF THE TEST CONTOUR, NOT A USER WALLET. The name shows it (node-test-*), and from that come two
9
+ // rules that are not up for discussion here:
10
+ //
11
+ // 1) THE KEY COMES ONLY FROM THE ENVIRONMENT OR FROM A FILE, the path to which also lives in the environment.
12
+ // This module does NOT read command-line arguments AT ALL: a key in argv is visible in the shell history and
13
+ // in the process list, that is, it leaks before anything is signed. The key's value is never printed: only the
14
+ // address, transaction hashes and amounts go out.
15
+ //
16
+ // 2) ON A PRODUCTION NETWORK THE ADAPTER DOES NOT SIGN. The rule is not its own: it is already declared by the
17
+ // engine (sdk/src/swap-flow.mjs: production = chain.escrow.mode === "live" && !chain.testnet) and the page
18
+ // itself uses the same marker. A private "production network" marker would silently drift from the engine, and
19
+ // then the test key would sign real money. So the rule is taken from the network registry, not written here.
20
+ //
21
+ // DEPENDENCIES. The signature is assembled the same way as the provider node's (rfq/evmSend.mjs): @noble/curves
22
+ // (secp256k1) and keccak256 - they are in the repository's devDependencies and already used in the project. There
23
+ // is neither DOM nor node:fs at the top level here: the adapters directory is the only place where the environment
24
+ // is allowed, but a static node:fs import would break the package's browser build, so the file is read via a
25
+ // dynamic import and only inside the key-loading function.
26
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
27
+ import keccakPkg from "keccak256";
28
+ import { SdkError } from "../errors.mjs";
29
+ import { config as engineConfig } from "../engine.mjs";
30
+
31
+ const keccak256 = (bytes) => new Uint8Array(keccakPkg(Buffer.from(bytes)));
32
+
33
+ // THE ENVIRONMENT VARIABLE NAMES - HERE, IN ONE PLACE: the run and the adapter must look at the same name,
34
+ // otherwise the key "is there" but the adapter does not see it.
35
+ export const KEY_ENV = "ARRAKIS_TEST_EVM_KEY";
36
+ export const KEY_FILE_ENV = "ARRAKIS_TEST_EVM_KEY_FILE";
37
+ export const RPC_ENV = "ARRAKIS_TEST_EVM_RPC";
38
+ export const CHAIN_ENV = "ARRAKIS_TEST_EVM_CHAIN";
39
+ export const CLAIMER_ENV = "ARRAKIS_TEST_EVM_CLAIMER";
40
+
41
+ // AN EXPLICIT WHITELIST OF TEST NETWORKS - HERE, NOT IN THE ENGINE, and WHY. The engine's "production network" rule
42
+ // (sdk/src/swap-flow.mjs: escrow.mode === "live" && !testnet) today does not trigger ON ANY recorded network: for
43
+ // the registry's production networks the escrow is marked "simulated", that is, by the letter of the rule they are
44
+ // not production. So the test key would also sign on mainnet - the engine would not notice, and the engine's rule
45
+ // must not be changed (it is shared by the page and all legs). So the "signing is ALLOWED here" marker is set
46
+ // separately and EXPLICITLY: only what is listed, and nothing more. First - the local anvil by chainId 31337.
47
+ export const TEST_CHAIN_IDS = Object.freeze([31337]);
48
+
49
+ const envOf = (name) => {
50
+ const box = typeof process !== "undefined" && process ? process.env : null;
51
+ const value = box ? box[name] : undefined;
52
+ return typeof value === "string" && value.trim() ? value.trim() : null;
53
+ };
54
+ export const envValue = envOf;
55
+
56
+ // --- ENCODINGS -----------------------------------------------------------------------------------------
57
+ const hexToBytes = (hex) => {
58
+ const h = String(hex || "").replace(/^0x/, "");
59
+ if (h.length === 0) return new Uint8Array(0);
60
+ if (h.length % 2) throw new SdkError("bad-input", { field: "hex", why: "odd" });
61
+ if (!/^[0-9a-fA-F]+$/.test(h)) throw new SdkError("bad-input", { field: "hex", why: "not-hex" });
62
+ return Uint8Array.from(Buffer.from(h, "hex"));
63
+ };
64
+ const bytesToHex = (bytes) => "0x" + Buffer.from(bytes).toString("hex");
65
+ // Integers in RLP are encoded MINIMALLY, and zero is the empty string. A non-canonical encoding (an extra leading
66
+ // zero) makes the node reject the WHOLE transaction, even though the signature is valid.
67
+ const numToBytes = (value) => {
68
+ const v = BigInt(value);
69
+ if (v < 0n) throw new SdkError("bad-input", { field: "value", why: "negative" });
70
+ if (v === 0n) return new Uint8Array(0);
71
+ let hex = v.toString(16);
72
+ if (hex.length % 2) hex = "0" + hex;
73
+ return hexToBytes(hex);
74
+ };
75
+ const intFromBytes = (bytes) => numToBytes(BigInt(bytesToHex(bytes)));
76
+ const encodeLength = (len, offset) => {
77
+ if (len < 56) return Uint8Array.from([offset + len]);
78
+ const lenBytes = numToBytes(len);
79
+ return Uint8Array.from([offset + 55 + lenBytes.length, ...lenBytes]);
80
+ };
81
+ const rlpItem = (item) => {
82
+ if (item instanceof Uint8Array) {
83
+ if (item.length === 1 && item[0] < 0x80) return item;
84
+ return Uint8Array.from([...encodeLength(item.length, 0x80), ...item]);
85
+ }
86
+ if (Array.isArray(item)) {
87
+ const body = item.flatMap((x) => [...rlpItem(x)]);
88
+ return Uint8Array.from([...encodeLength(body.length, 0xc0), ...body]);
89
+ }
90
+ throw new SdkError("bad-input", { field: "rlp" });
91
+ };
92
+
93
+ /** The account address for a private key: the last 20 bytes of the keccak256 of the UNcompressed point. */
94
+ export function addressOfPrivateKey(privateKeyHex) {
95
+ const pub = secp256k1.getPublicKey(hexToBytes(privateKeyHex), false);
96
+ return "0x" + Buffer.from(keccak256(pub.slice(1))).toString("hex").slice(-40);
97
+ }
98
+
99
+ /**
100
+ * SIGNING A LEGACY TRANSACTION (type 0, EIP-155). Legacy specifically, not 1559: it has no dependency on fee
101
+ * history, and a local chain accepts it everywhere. Three things that are easy to get silently wrong (all three
102
+ * were already caught in rfq/evmSend.mjs by checking against the Foundry reference, and all three are repeated
103
+ * here deliberately):
104
+ * prehash: false - Ethereum signs the ALREADY computed keccak hash; the library by default hashes the passed
105
+ * data once more, and the signature becomes valid in form and completely foreign;
106
+ * recovery is the FIRST byte of the 65-byte signature (not the last);
107
+ * r and s go into RLP with minimal encoding, not full 32 bytes.
108
+ */
109
+ export function signLegacyTx({ privateKey, nonce, gasPrice, gasLimit, to, data = "0x", value = 0, chainId }) {
110
+ const signing = rlpItem([
111
+ numToBytes(nonce), numToBytes(gasPrice), numToBytes(gasLimit), hexToBytes(to),
112
+ numToBytes(value), hexToBytes(data), numToBytes(chainId), new Uint8Array(0), new Uint8Array(0),
113
+ ]);
114
+ const digest = keccak256(signing);
115
+ const sig = secp256k1.sign(digest, hexToBytes(privateKey), { format: "recovered", prehash: false });
116
+ if (sig.length !== 65) throw new SdkError("bad-input", { field: "signature", got: sig.length });
117
+ const recovery = Number(sig[0]);
118
+ if (recovery !== 0 && recovery !== 1) throw new SdkError("bad-input", { field: "signature", why: "recovery" });
119
+ const r = intFromBytes(sig.slice(1, 33));
120
+ const s = intFromBytes(sig.slice(33, 65));
121
+ const v = BigInt(recovery) + 35n + BigInt(chainId) * 2n;
122
+ const raw = rlpItem([
123
+ numToBytes(nonce), numToBytes(gasPrice), numToBytes(gasLimit), hexToBytes(to),
124
+ numToBytes(value), hexToBytes(data), numToBytes(v), r, s,
125
+ ]);
126
+ return { raw: bytesToHex(raw), hash: bytesToHex(keccak256(raw)) };
127
+ }
128
+
129
+ // --- LOADING THE KEY ------------------------------------------------------------------------------------
130
+ /**
131
+ * The key from the environment or from a file. THE LOADER LIVES NEXT TO THE ADAPTER ON PURPOSE: it is the only
132
+ * entry point for the key, and if the caller could pass the key as an argument, the rule "never from argv" would
133
+ * rest on an honest word.
134
+ * Returns { privateKey, address, from } - from names the source ("env" or "file"), the key itself goes out only in
135
+ * this field and is never printed anywhere.
136
+ */
137
+ export async function testPrivateKeyFromEnv() {
138
+ let raw = envOf(KEY_ENV);
139
+ let from = "env";
140
+ if (!raw) {
141
+ const file = envOf(KEY_FILE_ENV);
142
+ if (!file) throw new SdkError("bad-input", { field: "privateKey", missing: [KEY_ENV, KEY_FILE_ENV] });
143
+ // DYNAMIC IMPORT: a static node:fs would end up in the package's browser build, though it is needed only here.
144
+ const fs = await import("node:fs");
145
+ raw = String(fs.readFileSync(file, "utf8")).trim().split(/\r?\n/)[0].trim();
146
+ from = "file";
147
+ }
148
+ const hex = raw.replace(/^0x/, "");
149
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) throw new SdkError("bad-input", { field: "privateKey", why: "shape" });
150
+ return { privateKey: "0x" + hex.toLowerCase(), address: addressOfPrivateKey(hex), from };
151
+ }
152
+
153
+ // --- A READ PROVIDER ------------------------------------------------------------------------------------
154
+ /**
155
+ * An EIP-1193-compatible provider over HTTP JSON-RPC. Needed where the SDK reads the chain with the leg driver
156
+ * (leg.balances -> eth_getBalance/eth_call) and as the reader for the lock step. THERE IS DELIBERATELY NO SIGNING
157
+ * HERE: eth_sendTransaction answers with a refusal, because this adapter must sign with its own key, not the node -
158
+ * otherwise the key would end up with the node, and the rule "key only from the environment" would stop meaning
159
+ * anything.
160
+ */
161
+ export function evmRpcProvider(url) {
162
+ if (typeof url !== "string" || !/^https?:\/\//.test(url)) throw new SdkError("bad-input", { field: "rpcUrl" });
163
+ const call = async (method, params = []) => {
164
+ const res = await fetch(url, {
165
+ method: "POST",
166
+ headers: { "content-type": "application/json" },
167
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
168
+ });
169
+ if (!res.ok) throw new SdkError("server-unavailable", { rpc: method, status: res.status });
170
+ const body = await res.json();
171
+ if (body && body.error) throw new SdkError("server-unavailable", { rpc: method, why: String(body.error.message || "").slice(0, 80) });
172
+ return body ? body.result : null;
173
+ };
174
+ const provider = {
175
+ url,
176
+ request({ method, params = [] }) {
177
+ if (method === "eth_sendTransaction") {
178
+ throw new SdkError("not-implemented", { surface: "provider.eth_sendTransaction" });
179
+ }
180
+ if (method === "eth_requestAccounts") return Promise.resolve([]);
181
+ return call(method, params);
182
+ },
183
+ };
184
+ return provider;
185
+ }
186
+
187
+ /**
188
+ * CONTRACT READ FOR THE LOCK SEAM: { to, data } -> hex. Exactly the shape the engine expects (www/js/evm/
189
+ * session.js, readContract), so it is dropped into the SDK WITHOUT an adapter. This is a CHAIN READ, not a node
190
+ * substitution: the calls go to the real node at the rpcUrl address.
191
+ */
192
+ export function evmRpcReader({ url }) {
193
+ const provider = evmRpcProvider(url);
194
+ return ({ to, data } = {}) => {
195
+ if (!to || !data) throw new SdkError("bad-input", { field: "eth_call" });
196
+ return provider.request({ method: "eth_call", params: [{ to, data }, "latest"] });
197
+ };
198
+ }
199
+
200
+ // --- THE PRODUCTION-NETWORK MARKER ----------------------------------------------------------------------
201
+ /**
202
+ * A PRODUCTION NETWORK - BY THE ENGINE'S RULE, NOT ITS OWN. The marker is taken from www/js/core/config.js (the
203
+ * testnet field) and sdk/src/swap-flow.mjs (escrow.mode === "live"): on a production network there is a live
204
+ * escrow and NO testnet marker. An unknown network is also considered production: there is nothing to prove it is a
205
+ * test one, and an error in that direction costs a test key signing over real money.
206
+ */
207
+ export function isProductionChain(chainId) {
208
+ const chain = typeof engineConfig.chainByChainId === "function" ? engineConfig.chainByChainId(Number(chainId)) : null;
209
+ if (!chain) return { production: true, reason: "unknown-chain", chain: null };
210
+ const escrow = chain.escrow && typeof chain.escrow === "object" ? chain.escrow : null;
211
+ const production = Boolean(escrow && escrow.mode === "live" && !chain.testnet);
212
+ return { production, reason: production ? "production" : "testnet", chain };
213
+ }
214
+
215
+ // --- THE ADAPTER ----------------------------------------------------------------------------------------
216
+ /**
217
+ * A test EVM wallet: the same interface as the page's wallet (vm, connect, disconnect, address, chainId, subscribe,
218
+ * driver + send/receipt), but the signature is its own and the chain is read directly.
219
+ *
220
+ * IT ACCEPTS ONLY AN ALREADY-LOADED KEY: the loader is testPrivateKeyFromEnv above, and there is no other entry.
221
+ * rpcUrl and chainId come from outside, but as VALUES (not the key), and the adapter keeps no factory address at
222
+ * all: the addresses come from the SDK/engine's network registry, and a second source of truth about addresses must
223
+ * not be introduced.
224
+ */
225
+ export function nodeTestEvmWallet({ privateKey, rpcUrl, chainId = null, id = "node-test" } = {}) {
226
+ const hex = String(privateKey || "").replace(/^0x/, "");
227
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) throw new SdkError("bad-input", { field: "privateKey" });
228
+ const key = "0x" + hex.toLowerCase();
229
+ const owner = addressOfPrivateKey(key);
230
+ const provider = evmRpcProvider(rpcUrl);
231
+ let connected = false;
232
+
233
+ const remoteChainId = async () => Number(BigInt(await provider.request({ method: "eth_chainId" })));
234
+
235
+ // REFUSAL ON A PRODUCTION NETWORK - ONE POINT THROUGH WHICH EVERY SIGNATURE PASSES. The check stands here, not in
236
+ // connect(), deliberately: connect may happen once and early, while the node's network may change later (or turn
237
+ // out to be the wrong one at the moment of signing). We ask the network OF THE NODE right before signing.
238
+ const assertTestContour = async () => {
239
+ const remote = await remoteChainId();
240
+ // A NETWORK OUTSIDE THE WHITELIST IS A REFUSAL NAMING THE NETWORK, BEFORE ANY SIGNATURE. This is the FIRST line
241
+ // of defense and it does not depend on how the network is marked in the registry (the production marker is the
242
+ // second line below, and today it may not trigger).
243
+ if (!TEST_CHAIN_IDS.includes(Number(remote))) {
244
+ throw new SdkError("wrong-chain", {
245
+ why: "test-contour-network-not-allowed", chainId: remote, allowed: [...TEST_CHAIN_IDS],
246
+ });
247
+ }
248
+ // THE SECOND LINE - THE ENGINE'S PRODUCTION-NETWORK RULE (not rewritten here, taken from the registry). It stays
249
+ // for the case where a network from the list is later marked production: then the signature still stops.
250
+ const view = isProductionChain(remote);
251
+ if (view.production) {
252
+ throw new SdkError("wrong-chain", {
253
+ why: "test-contour-refuses-live-network", chainId: remote, reason: view.reason,
254
+ escrowMode: view.chain && view.chain.escrow ? view.chain.escrow.mode : null,
255
+ testnet: view.chain ? Boolean(view.chain.testnet) : null,
256
+ });
257
+ }
258
+ if (chainId !== null && Number(chainId) !== remote) {
259
+ throw new SdkError("wrong-chain", { got: remote, want: Number(chainId) });
260
+ }
261
+ return remote;
262
+ };
263
+
264
+ return {
265
+ vm: "evm",
266
+ id,
267
+ // The address going out is what is anyway visible in every transaction. The key is not handed out by any method.
268
+ async connect(options = {}) {
269
+ const want = options && options.chainId !== undefined && options.chainId !== null ? Number(options.chainId) : chainId;
270
+ const remote = await remoteChainId();
271
+ if (want !== null && want !== undefined && Number(want) !== remote) {
272
+ throw new SdkError("wrong-chain", { got: remote, want: Number(want) });
273
+ }
274
+ connected = true;
275
+ },
276
+ async disconnect() {
277
+ connected = false;
278
+ },
279
+ address() {
280
+ return owner;
281
+ },
282
+ async chainId() {
283
+ return remoteChainId();
284
+ },
285
+ // NO SUBSCRIPTION: a local run has no "wallet changed network" event, while a poll timer would keep the process
286
+ // alive after the last transaction. We return an unsubscribe function, as the adapter interface requires.
287
+ subscribe() {
288
+ return () => {};
289
+ },
290
+ driver() {
291
+ return provider;
292
+ },
293
+ /**
294
+ * SENDING: sign with its own key and eth_sendRawTransaction. The nonce is taken by pending, the gas price - from
295
+ * the node, the gas limit - from the caller (the lock passes its own; if it did not, we estimate with a margin).
296
+ * None of these numbers is invented: a guessed gas limit costs a signature, and a guessed nonce costs a lost
297
+ * transaction.
298
+ */
299
+ async send({ to, data = "0x", value = 0n, gas = null }) {
300
+ if (!connected) throw new SdkError("wallet-not-connected");
301
+ if (typeof to !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(to)) throw new SdkError("bad-input", { field: "send.to" });
302
+ const remote = await assertTestContour();
303
+ const valueWei = BigInt(value === undefined || value === null ? 0 : value);
304
+ const nonce = Number(BigInt(await provider.request({ method: "eth_getTransactionCount", params: [owner, "pending"] })));
305
+ const gasPrice = BigInt(await provider.request({ method: "eth_gasPrice" }));
306
+ let gasLimit = gas === null || gas === undefined ? null : BigInt(gas);
307
+ if (gasLimit === null) {
308
+ const est = BigInt(await provider.request({
309
+ method: "eth_estimateGas",
310
+ params: [{ from: owner, to, value: "0x" + valueWei.toString(16), data }],
311
+ }));
312
+ gasLimit = (est * 12n) / 10n;
313
+ }
314
+ const signed = signLegacyTx({
315
+ privateKey: key, nonce, gasPrice, gasLimit, to, data,
316
+ value: valueWei, chainId: remote,
317
+ });
318
+ const hash = await provider.request({ method: "eth_sendRawTransaction", params: [signed.raw] });
319
+ if (typeof hash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(hash)) {
320
+ throw new SdkError("server-unavailable", { step: "send-raw" });
321
+ }
322
+ // THE HASH IS CHECKED AGAINST THE LOCALLY COMPUTED ONE. A discrepancy means the node accepted a transaction
323
+ // different from the one we signed - and that must be found out here, not from missing money.
324
+ if (hash.toLowerCase() !== signed.hash.toLowerCase()) {
325
+ throw new SdkError("contract-reverted", { step: "hash-mismatch", node: hash, local: signed.hash });
326
+ }
327
+ return hash;
328
+ },
329
+ /**
330
+ * THE RECEIPT: asked once and returned as is (null - not in a block yet). THE ARGUMENT SHAPE IS ACCEPTED BOTH
331
+ * WAYS: a hash string and an object { hash }. This is not "just in case": the adapter contract declares
332
+ * receipt(txHash: string), while the lock path in the SDK calls the receipt as receipt({ hash }) - the engine's
333
+ * waitReceipt passes an object (www/js/evm/funding.js), and lock.mjs passes it into the adapter without
334
+ * normalisation. A strict adapter answers bad-input to this, the refusal is swallowed by the wait, and the
335
+ * escrow address is silently replaced with the PREDICTED one - that is, the "address from the receipt" check
336
+ * stops working without saying so.
337
+ */
338
+ async receipt(txHash) {
339
+ const hash = typeof txHash === "string" ? txHash : (txHash && typeof txHash.hash === "string" ? txHash.hash : null);
340
+ if (!hash) throw new SdkError("bad-input", { field: "receipt.txHash" });
341
+ const r = await provider.request({ method: "eth_getTransactionReceipt", params: [hash] });
342
+ return r || null;
343
+ },
344
+ };
345
+ }
package/src/codes.mjs ADDED
@@ -0,0 +1,188 @@
1
+ // CODE TABLES. The single place where the codes are named: they are read by the SDK, by the check and by the README.
2
+ //
3
+ // Why codes and not texts: wording, translations and tone are the interface's business. The core must name a
4
+ // state so that it can be translated and coloured without parsing a string (issue #32, requirement 2).
5
+
6
+ export const STEP_CODES = [
7
+ "awaiting_funding",
8
+ "funded",
9
+ "maker_locking",
10
+ "xmr_locked",
11
+ "ready",
12
+ "claimed",
13
+ "swept",
14
+ ];
15
+
16
+ export const TERMINAL_CODES = ["success", "refunded_eth", "xmr_returned", "closed"];
17
+
18
+ export const CHECK_CODES = [
19
+ "balance",
20
+ "gas",
21
+ "allowance",
22
+ "consent",
23
+ "network",
24
+ "settlement-network",
25
+ "stale",
26
+ "deadline",
27
+ "range",
28
+ "size",
29
+ "escrow-state",
30
+ "escrow-points",
31
+ "counterparty",
32
+ "slots",
33
+ "unchecked",
34
+ "unknown",
35
+ ];
36
+
37
+ export const ERROR_CODES = [
38
+ "not-implemented",
39
+ "bad-input",
40
+ "wallet-not-connected",
41
+ "wallet-rejected",
42
+ "wrong-chain",
43
+ "quote-unavailable",
44
+ "quote-refused",
45
+ "quote-stale",
46
+ // THE ORDER-QUOTE SIGNATURE (EIP-712, quote format v7). Four different refusals, because the interface owes the
47
+ // person a different piece of advice for each:
48
+ // quote-signature-invalid - the signature does not recover to any address: it is malformed or made for other bytes;
49
+ // quote-key-mismatch - the signature is valid but belongs to a key other than the one the quote names;
50
+ // quote-field-unbound - a field the signature must cover is absent, so nothing can be verified;
51
+ // quote-expired - the quote's own validUntil has passed.
52
+ "quote-signature-invalid",
53
+ "quote-key-mismatch",
54
+ "quote-field-unbound",
55
+ "quote-expired",
56
+ // WHO OWNS THE KEY - ASKED OF THE CHAIN (maker registry, providerOf). The signature alone proves only that
57
+ // SOME key signed; these four name what the registry (and the interface's policy) said:
58
+ // quote-registry-unchecked - there was no way to ask the chain, so the claim stays unproven;
59
+ // quote-key-unknown - the registry has no provider for this key (nobody owns it);
60
+ // quote-provider-mismatch - the registry maps the key to a DIFFERENT provider than the quote names;
61
+ // quote-provider-not-allowed - the chain confirms the provider, but this interface does not admit it (policy).
62
+ "quote-registry-unchecked",
63
+ "quote-key-unknown",
64
+ "quote-provider-mismatch",
65
+ "quote-provider-not-allowed",
66
+ // THE FACTORY THAT WILL VERIFY THE QUOTE AND WHERE ITS FEE GOES (issue #103). The factory is taken from
67
+ // the provider's REGISTRY RECORD and its CODE is checked against the interface's known builds (extcodehash):
68
+ // quote-factory-unchecked - no way to read the factory / its code, so the claim stays unproven;
69
+ // quote-factory-unknown - the record names no factory (zero);
70
+ // quote-factory-mismatch - the signed quote names a DIFFERENT factory than the record;
71
+ // quote-factory-code-unknown - the factory's code is not a build this interface knows.
72
+ "quote-factory-unchecked",
73
+ "quote-factory-unknown",
74
+ "quote-factory-mismatch",
75
+ "quote-factory-code-unknown",
76
+ "recovery-declined",
77
+ "recovery-failed",
78
+ "storage-unavailable",
79
+ "server-unavailable",
80
+ // A REFUSAL BY OUR SERVICE, AND IT DELIBERATELY DIFFERS FROM A CONNECTION FAILURE: the connection never got
81
+ // through - server-unavailable, while the service answered and did not accept - server-refused (with the
82
+ // status and its own reason). The advice to the interface differs: "retry" versus "investigate".
83
+ "server-refused",
84
+ // A TIMEOUT WAITING FOR XMR TO ARRIVE - a separate code. "We did not wait long enough" is a state of the
85
+ // money, not a breakdown and not an empty response: mixing it with other refusals, the interface could not
86
+ // tell "not arrived yet" from "broken".
87
+ "xmr-timeout",
88
+ // XMR IS ON THE ADDRESS BUT BARRED BY THE TRANSACTION'S unlock_time (issue #84). Distinct from `xmr-timeout`:
89
+ // that one means "nothing arrived within the wait", this one means "money is there and CANNOT be spent until a
90
+ // height or a date". Marking ready under it would hand the escrow to the maker for funds he cannot be paid from -
91
+ // so the core refuses with its own code, and the `until` boundary travels in params.
92
+ "xmr-locked",
93
+ // THE XMR ARRIVAL CHECK ON TWO NODES (issue #85). The decision "the XMR arrived" is made by the client, and a
94
+ // single node is a single truth that can lie. Each code names a different reason the button must stay closed:
95
+ // xmr-nodes-disagree - a synced node has no such transaction in that block or a different block hash there;
96
+ // xmr-no-nodes - every node in the list is unreachable, so nothing can be confirmed;
97
+ // xmr-underpaid - the arrived amount is less than the expected one;
98
+ // xmr-too-late - less time remains before the ready deadline than the required confirmations take.
99
+ "xmr-nodes-disagree",
100
+ "xmr-no-nodes",
101
+ "xmr-underpaid",
102
+ "xmr-too-late",
103
+ "contract-reverted",
104
+ "insufficient-funds",
105
+ // CLAIM GAS FOR THE XMR SELLER (issue #78). The seller comes from Monero and has no coin on the EVM side to pay
106
+ // for the claim, so the maker may send it - but only when asked for, and only after the core has CONFIRMED the
107
+ // gas actually arrived. The three codes name the three states the interface must not blur together:
108
+ // gas-refused-by-maker - the maker declines to pay the claim gas, and the refusal is shown BEFORE the deal;
109
+ // gas-not-arrived - the gas was ordered but the recipient's balance did not grow;
110
+ // gas-too-little - gas arrived, but below what a claim costs (an unmeasured requirement counts here too).
111
+ "gas-refused-by-maker",
112
+ "gas-not-arrived",
113
+ "gas-too-little",
114
+ "unknown",
115
+ ];
116
+
117
+ // GUARD KIND -> CHECK CODE. Kinds come from the engine (www/js/core/preSignGuards.js), codes are our public
118
+ // vocabulary. The table is checked by machine: every kind the engine can return must be here, otherwise the
119
+ // interface gets a code that is not in the contract.
120
+ export const KIND_TO_CHECK = {
121
+ // size and range
122
+ "in-range": "range",
123
+ "above-max": "range",
124
+ "below-min": "range",
125
+ "range-unstated": "range",
126
+ "size-unstated": "size",
127
+ "amount-unstated": "size",
128
+ "unit-mismatch": "range",
129
+ // money
130
+ "balance-enough": "balance",
131
+ "balance-short": "balance",
132
+ "balance-unchecked": "balance",
133
+ "gas-enough": "gas",
134
+ "gas-short": "gas",
135
+ "reserve-unstated": "gas",
136
+ "native-unchecked": "gas",
137
+ // token allowance
138
+ "not-needed": "allowance",
139
+ "allowed": "allowance",
140
+ "allowance-short": "allowance",
141
+ "allowance-unchecked": "allowance",
142
+ "consent-missing": "consent",
143
+ // network
144
+ "network-match": "network",
145
+ "network-unknown": "network",
146
+ "wallet-unknown": "network",
147
+ "wallet-network": "network",
148
+ "settlement-network": "settlement-network",
149
+ // counterparty: a stand-in chain or a stand-in counterparty in the sandbox
150
+ "real-provider": "counterparty",
151
+ "stand-in-provider": "counterparty",
152
+ "standin-on-production": "counterparty",
153
+ // quote freshness
154
+ "fresh": "stale",
155
+ "stale": "stale",
156
+ "no-ttl": "stale",
157
+ "untimed": "stale",
158
+ "expired": "stale",
159
+ // deadlines
160
+ "deadlines-ok": "deadline",
161
+ "deadlines-unstated": "deadline",
162
+ "ready-by-in-past": "deadline",
163
+ "t1-not-after-ready-by": "deadline",
164
+ "ready-window-too-short": "deadline",
165
+ "claim-window-too-short": "deadline",
166
+ // A DEADLINE TOO FAR AHEAD (issue #88 - a concurrent edit in this tree): the order's t1 is a year away, and the
167
+ // ETH cannot be returned until then. It shares the `deadline` code with the other two extremes.
168
+ "deadline-too-far": "deadline",
169
+ // escrow state
170
+ "new": "escrow-state",
171
+ "already-funded": "escrow-state",
172
+ "already-claimed": "escrow-state",
173
+ "already-refunded": "escrow-state",
174
+ "state-unchecked": "escrow-state",
175
+ "off-step": "escrow-state",
176
+ // points and halves
177
+ "points-match": "escrow-points",
178
+ "point-mismatch": "escrow-points",
179
+ "commit-mismatch": "escrow-points",
180
+ "points-unchecked": "escrow-points",
181
+ // order book slots
182
+ "slots-match": "slots",
183
+ "slots-unchecked": "slots",
184
+ // misc
185
+ "unchecked": "unchecked",
186
+ };
187
+
188
+ export const checkCodeFor = (kind) => KIND_TO_CHECK[kind] || "unknown";