@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,214 @@
1
+ // EIP-712 TYPED DATA FOR AN ORDER QUOTE - THE SPEC, SHARED BY THE NODE AND THE SDK.
2
+ //
3
+ // WHY THE SAME FILE EXISTS TWICE. The node (rfq/) runs on the provider's machine and never sees www/ or sdk/:
4
+ // it has to start on its own. The SDK is a package and never sees rfq/. So the spec is embodied twice and the
5
+ // two copies are pinned by sha256 (tools/check-quote-eip712.mjs), exactly like the order context. A divergence
6
+ // here would mean one side signs what the other does not verify - the failure this format exists to prevent.
7
+ //
8
+ // WHAT IS BOUND, AND WHY EACH FIELD IS INSIDE THE DIGEST (changing any of them breaks recovery):
9
+ // provider, quoteKey - whose quote it is and which key signed it. For a solo maker the two are equal; a
10
+ // delegated key is a different address, and the registry maps it back to the provider
11
+ // (the chain check itself is a later stage; here both values are only bound).
12
+ // registry - the maker registry named by the order. Bound now so a quote cannot be replayed
13
+ // from one registry into another even before anyone reads `providerOf`.
14
+ // cashier - the fee vault of the order (issue #103): the fee destination is part of the SIGNED
15
+ // terms, so the factory no longer mints one vault per maker and the fee is not
16
+ // scattered across them. Bound so a relayer cannot retarget where the fee accrues.
17
+ // locker, claimer, - the order conditions: the same terms the DLEQ context binds and the escrow's
18
+ // amount, readyBy, termsHash covers. Binding them means the quote is usable for THESE terms only.
19
+ // t1, salt
20
+ // xmrAmount - the XMR side of the deal, in ATOMIC units (#110 step 3). It is IN this list AND in the
21
+ // chain's list (OrderQuote.sol) at once: the field set is ONE, so the provider's
22
+ // signature covers exactly the XMR the chain verifies at order birth. Earlier this field
23
+ // rode in the quote object OUTSIDE the digest on purpose - a field the chain could not
24
+ // see must not be signed; once the contract learned it, it moved in here together with
25
+ // the typehash, the depositor form and the independent vector.
26
+ // commitHalfClaimer, - the provider's own commitment and ed25519 spend point: both are inputs to the
27
+ // edPointClaimer escrow's termsHash, so they are bound and cannot be swapped for another order's.
28
+ // chainId - bound in the domain AND in the message. A quote signed for one network does not
29
+ // verify on another.
30
+ // feeBps, fee, - the provider's commission: the rate, the amount in wei and the recipient. Shown to
31
+ // feeRecipient the customer before signing (#76) and frozen by the signature afterwards.
32
+ // validUntil - the quote's own expiry, IN SECONDS (the order's readyBy/t1 and block.timestamp
33
+ // scale): a quote past it is refused regardless of the price.
34
+ // nonce - replay protection: the very same signed quote cannot be presented twice.
35
+ //
36
+ // The domain's verifyingContract is the escrow FACTORY taken from the order: the contract that will verify the
37
+ // quote in EVM (stage 2). The type order, the domain name and the version are part of the protocol - they may
38
+ // change only together with the quote version.
39
+ export const ORDER_QUOTE_DOMAIN_NAME = "NinseiEscrowFactory";
40
+ export const ORDER_QUOTE_DOMAIN_VERSION = "1";
41
+ // 8: the signed EIP-712 field set grew `xmrAmount` (#110 step 3) - the XMR the level walk gives is now covered by
42
+ // the provider's signature and verified ON CHAIN. This is the request-shape generation label, and it moves WITH the
43
+ // field set (see the note above). The DOMAIN name and the domain version string are UNCHANGED by it: they name the
44
+ // domain (the factory address and the chain), which did not change. So here the label and the domain version string
45
+ // move apart, and that is deliberate - the label tracks the message shape, not the domain identity.
46
+ export const ORDER_QUOTE_VERSION = 8;
47
+
48
+ // The domain type string is assembled from parts ON PURPOSE. A single literal of it is a phrase of several
49
+ // space-separated words, and the SDK check reads such literals as user-facing text. Do not "simplify" it.
50
+ const DOMAIN_TYPE_PARTS = ["EIP712Domain(", "string name,", "string version,", "uint256 chainId,", "address verifyingContract)"];
51
+ export const EIP712_DOMAIN_TYPE = DOMAIN_TYPE_PARTS.join("");
52
+
53
+ // The message, field by field and IN THIS ORDER. The order is the protocol: a rearrangement gives another
54
+ // digest, and every already-issued quote stops verifying at once.
55
+ export const ORDER_QUOTE_FIELDS = [
56
+ ["provider", "address"],
57
+ ["quoteKey", "address"],
58
+ ["registry", "address"],
59
+ ["cashier", "address"],
60
+ ["locker", "address"],
61
+ ["claimer", "address"],
62
+ ["amount", "uint256"],
63
+ ["readyBy", "uint64"],
64
+ ["t1", "uint64"],
65
+ ["salt", "bytes32"],
66
+ ["commitHalfClaimer", "bytes32"],
67
+ ["edPointClaimer", "bytes32"],
68
+ ["chainId", "uint256"],
69
+ ["feeBps", "uint256"],
70
+ ["fee", "uint256"],
71
+ ["feeRecipient", "address"],
72
+ ["validUntil", "uint64"],
73
+ ["nonce", "uint256"],
74
+ ["xmrAmount", "uint256"],
75
+ ];
76
+ export const ORDER_QUOTE_TYPE = "OrderQuote(" + ORDER_QUOTE_FIELDS.map((f) => f[1] + " " + f[0]).join(",") + ")";
77
+
78
+ // Error tokens are SINGLE WORDS: the SDK forbids multi-word string literals in sdk/src (they would read as
79
+ // user-facing text), so the spec names its failures by token and the caller maps them to its own codes.
80
+ const boom = (token) => { throw new Error(token); };
81
+
82
+ const utf8 = (s) => new TextEncoder().encode(String(s));
83
+ const concat = (...arrays) => {
84
+ const total = arrays.reduce((n, a) => n + a.length, 0);
85
+ const out = new Uint8Array(total);
86
+ let at = 0;
87
+ for (const a of arrays) { out.set(a, at); at += a.length; }
88
+ return out;
89
+ };
90
+ const hexToBytes = (hex) => {
91
+ const h = String(hex).replace(/^0x/, "");
92
+ if (h.length % 2) boom("eip712-odd-hex");
93
+ const out = new Uint8Array(h.length / 2);
94
+ for (let i = 0; i < out.length; i++) {
95
+ const byte = parseInt(h.slice(i * 2, i * 2 + 2), 16);
96
+ if (Number.isNaN(byte)) boom("eip712-bad-hex");
97
+ out[i] = byte;
98
+ }
99
+ return out;
100
+ };
101
+ const bytesToHex = (bytes) => {
102
+ let s = "0x";
103
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
104
+ return s;
105
+ };
106
+ const asBigInt = (v) => {
107
+ if (typeof v === "bigint") return v;
108
+ const s = String(v === undefined || v === null ? "" : v).trim();
109
+ if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
110
+ if (/^[0-9]+$/.test(s)) return BigInt(s);
111
+ boom("eip712-bad-number");
112
+ };
113
+ const wordUint = (v, bits) => {
114
+ const n = asBigInt(v);
115
+ if (n < 0n) boom("eip712-negative");
116
+ if (n >= (1n << BigInt(bits))) boom("eip712-overflow");
117
+ const out = new Uint8Array(32);
118
+ let x = n;
119
+ for (let i = 31; i >= 0 && x > 0n; i--) { out[i] = Number(x & 0xffn); x >>= 8n; }
120
+ return out;
121
+ };
122
+ // FORM IS NORMALISED, NOT DEMANDED. The quote carries ed25519 points WITHOUT the 0x prefix (that is how the
123
+ // node writes them, see rfq/orderAddress.mjs), while the contract returns them WITH one. Both are the same
124
+ // bytes, so both are accepted and encoded identically - otherwise the node could not sign its own quote.
125
+ const wordAddress = (v) => {
126
+ const s = String(v === undefined || v === null ? "" : v).trim();
127
+ if (!/^(0x)?[0-9a-fA-F]{40}$/.test(s)) boom("eip712-bad-address");
128
+ const out = new Uint8Array(32);
129
+ out.set(hexToBytes(s.replace(/^0x/, "")), 12);
130
+ return out;
131
+ };
132
+ const wordBytes32 = (v) => {
133
+ const s = String(v === undefined || v === null ? "" : v).trim();
134
+ if (!/^(0x)?[0-9a-fA-F]{64}$/.test(s)) boom("eip712-bad-bytes32");
135
+ return hexToBytes(s.replace(/^0x/, ""));
136
+ };
137
+ const encodeField = (type, value) => {
138
+ if (type === "address") return wordAddress(value);
139
+ if (type === "bytes32") return wordBytes32(value);
140
+ if (type === "uint64") return wordUint(value, 64);
141
+ if (type === "uint256") return wordUint(value, 256);
142
+ boom("eip712-bad-type");
143
+ };
144
+
145
+ // The fields a signed set cannot do without. An absent field is not "probably zero": the caller must refuse,
146
+ // not verify a partial set (that is the rule the old signatureVerified witness carried).
147
+ export const quoteMissingFields = (quote) => {
148
+ const missing = [];
149
+ for (const [name] of ORDER_QUOTE_FIELDS) {
150
+ const v = quote ? quote[name] : undefined;
151
+ if (v === undefined || v === null || v === "") missing.push(name);
152
+ }
153
+ if (quote && (quote.factory === undefined || quote.factory === null || quote.factory === "")) missing.push("factory");
154
+ if (quote && (quote.signature === undefined || quote.signature === null || quote.signature === "")) missing.push("signature");
155
+ return missing;
156
+ };
157
+
158
+ // The EIP-712 domain. chainId and verifyingContract are taken LIVE from the quote, not cached: the digest must
159
+ // change when either does, otherwise a quote could move between networks or contracts.
160
+ export function quoteDomainSeparator({ crypto, quote }) {
161
+ return crypto.keccak256(concat(
162
+ crypto.keccak256(utf8(EIP712_DOMAIN_TYPE)),
163
+ crypto.keccak256(utf8(ORDER_QUOTE_DOMAIN_NAME)),
164
+ crypto.keccak256(utf8(ORDER_QUOTE_DOMAIN_VERSION)),
165
+ wordUint(quote.chainId, 256),
166
+ wordAddress(quote.factory),
167
+ ));
168
+ }
169
+
170
+ export function quoteStructHash({ crypto, quote }) {
171
+ const parts = [crypto.keccak256(utf8(ORDER_QUOTE_TYPE))];
172
+ for (const [name, type] of ORDER_QUOTE_FIELDS) parts.push(encodeField(type, quote ? quote[name] : undefined));
173
+ return crypto.keccak256(concat(...parts));
174
+ }
175
+
176
+ // The digest the wallet key signs. Anything that changes here changes the recovered address.
177
+ export function quoteDigest({ crypto, quote }) {
178
+ return crypto.keccak256(concat(Uint8Array.from([0x19, 0x01]), quoteDomainSeparator({ crypto, quote }), quoteStructHash({ crypto, quote })));
179
+ }
180
+
181
+ // Signature on the wire: r || s || v with v in {27, 28} - the shape ecrecover (and the registry contract)
182
+ // expects. The curve library returns [recovery, r, s], so the order is normalised here once, not at each call.
183
+ export function signQuoteDigest({ crypto, digest, privateKey }) {
184
+ const sig = crypto.secp256k1.sign(digest, hexToBytes(privateKey), { format: "recovered", prehash: false });
185
+ if (sig.length !== 65) boom("eip712-bad-signature");
186
+ const recovery = Number(sig[0]);
187
+ if (recovery !== 0 && recovery !== 1) boom("eip712-bad-recovery");
188
+ const out = new Uint8Array(65);
189
+ out.set(sig.slice(1, 33), 0);
190
+ out.set(sig.slice(33, 65), 32);
191
+ out[64] = 27 + recovery;
192
+ return bytesToHex(out);
193
+ }
194
+
195
+ // Recover the signer without knowing it beforehand. Returns a lowercase 0x address, or null when the quote is
196
+ // not verifiable at all (missing fields, malformed signature, unusable domain) - the caller turns that into a
197
+ // named refusal, never into consent.
198
+ export function recoverQuoteKey({ crypto, quote }) {
199
+ try {
200
+ if (quoteMissingFields(quote).length) return null;
201
+ const sig = hexToBytes(String(quote.signature).replace(/^0x/, ""));
202
+ if (sig.length !== 65) return null;
203
+ let v = sig[64];
204
+ if (v >= 27) v -= 27;
205
+ if (v !== 0 && v !== 1) return null;
206
+ const digest = quoteDigest({ crypto, quote });
207
+ const pub = crypto.secp256k1.Signature.fromBytes(concat(sig.slice(0, 32), sig.slice(32, 64)))
208
+ .addRecoveryBit(v).recoverPublicKey(digest);
209
+ const raw = pub.toBytes(false);
210
+ return bytesToHex(crypto.keccak256(raw.slice(1)).slice(12));
211
+ } catch {
212
+ return null;
213
+ }
214
+ }