@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/src/config.mjs ADDED
@@ -0,0 +1,277 @@
1
+ // SDK CONFIGURATION: networks, tokens, thresholds - the engine's data, not the interface's constants.
2
+ //
3
+ // THE SETTLEMENT NETWORK IS NAMED BY THE PAIR "network + virtual machine": vm already lives in the engine's
4
+ // network registry (`vm: "evm" | "bitcoin" | "solana" | "tron"`), and the SDK does not invent it anew. If there
5
+ // is no leg driver for a vm, the call does not "quietly assume EVM" - it answers with a code naming what is
6
+ // already supported: so adding Tron or Solana will not require edits in the quotes, the Monero leg and the state
7
+ // machine.
8
+ import * as engine from "./engine.mjs";
9
+ import { CHECK_CODES, ERROR_CODES, STEP_CODES } from "./codes.mjs";
10
+ import { fail } from "./errors.mjs";
11
+ import { implementedVms, legFor } from "./legs/index.mjs";
12
+ import { routeProviders } from "./legs/evm.mjs";
13
+
14
+ // TOKENS ARE TAKEN BY AN ENGINE FUNCTION, NOT FROM A NETWORK FIELD: `tokensOf` returns the native coin first,
15
+ // followed by the declared tokens - exactly the list the interface should show. The identifier is the lowercase
16
+ // symbol ("usdc"), the same one used in quotes and in API paths.
17
+ // TOKENS ARE TAKEN BY NETWORK IDENTIFIER AND WITHOUT A SILENT DEFAULT.
18
+ //
19
+ // The engine's `tokensOf` looks the network up by slug (`chainById`), and when it does not find it, it SILENTLY
20
+ // returns the default network (`DEFAULT_CHAIN`). For the interface that makes sense: showing the default network
21
+ // is better than showing emptiness. The core must not do that. A call `tokensOf(chain.chainId)` once stood here -
22
+ // a NUMBER where a slug is expected: the network was never found, and the answer was assigned the tokens of
23
+ // ANOTHER network, while the `id` field held the correct network identifier. The error was invisible: the list
24
+ // looked plausible on an unknown network too.
25
+ //
26
+ // So the network is looked up by number honestly, absence is a refusal, and additionally it is VERIFIED that the
27
+ // list belongs to exactly this network: if the engine ever substitutes the default network again, this will show
28
+ // up here, not in a swap that went to the wrong network.
29
+ const tokensOfNetwork = (chainId) => {
30
+ const network = typeof engine.config.chainByChainId === "function" ? engine.config.chainByChainId(chainId) : null;
31
+ if (!network) fail("bad-input", { field: "chain.chainId", unknown: chainId, why: "no-network-with-this-chainId" });
32
+ const got = engine.config.tokensOf(network.id);
33
+ const own = [network.native, ...(network.tokens || [])].filter(Boolean);
34
+ const asId = (t) => String(t.symbol).toLowerCase();
35
+ if (got.map(asId).join("|") !== own.map(asId).join("|")) {
36
+ fail("bad-input", { field: "chain.tokens", unknown: chainId, why: "tokens-belong-to-another-network" });
37
+ }
38
+ return got;
39
+ };
40
+
41
+ const chainConfig = (chain) => ({
42
+ id: chain.id,
43
+ name: chain.name,
44
+ vm: chain.vm || "evm",
45
+ chainId: chain.chainId,
46
+ nativeSymbol: chain.native && chain.native.symbol ? chain.native.symbol : null,
47
+ tokens: tokensOfNetwork(chain.chainId).map((t) => ({
48
+ id: String(t.symbol).toLowerCase(),
49
+ symbol: String(t.symbol),
50
+ address: t.address || null,
51
+ decimals: Number(t.decimals),
52
+ })),
53
+ dex: chain.dex ? { routes: chain.dex.routes || [] } : null,
54
+ });
55
+
56
+ // --- ADDRESSES AND PATHS THAT ARE SUPPLIED FROM OUTSIDE ------------------------------------
57
+ //
58
+ // The default is OUR service and our explorers: everything the engine already knows. Options can name others:
59
+ // your own API, your own node, a third-party explorer, a different set of paths. This matters to whoever runs
60
+ // the swap for themselves, not to whoever uses it, so it lives in options, not in the interface.
61
+ //
62
+ // A RULE THAT IS NOT UP FOR DISCUSSION: ADDRESSES are configurable, not CHECKS. Your own API, your own node,
63
+ // your own explorer - fine. But the pre-signature guards (the price is not market, the amount is covered, the
64
+ // token allowance, the deadlines from the chain) stay enabled under any configuration: otherwise "maximum
65
+ // configurability" would become a way to switch the checks off with a single field, which is exactly what the
66
+ // core exists to prevent.
67
+ //
68
+ // PATHS ARE NAMED BY NAMES, NOT BY STRINGS IN CODE. `route("swept", { id })` substitutes the value and encodes
69
+ // it; the path holds `{id}`. So changing the host or using a third-party server is an edit of an object, not an
70
+ // edit of the core.
71
+ // WHY THESE PATHS DID NOT EXIST BEFORE AND WHERE THEY COME FROM. The "server" step (recording the swap with us
72
+ // and watching the Monero address) is done with THE SAME requests as the current interface, so the paths are
73
+ // named after its calls, not invented: the watch state - GET /swaps/{id} (www/js/core/chainSource.js:184,
74
+ // fetchSession), the factory record of a swap - POST /evm-swaps (www/js/evm/auth.js:111, saveServerSwap; in app
75
+ // the full path /api/evm-swaps is declared by the template /^\/api\/evm-swaps$/ in app/server.mjs:1230).
76
+ export const DEFAULT_ROUTES = Object.freeze({
77
+ swaps: "/swaps",
78
+ swap: "/swaps/{id}",
79
+ evmSwaps: "/evm-swaps",
80
+ // A quote for an order: the provider's commitment under keys, not a market snapshot.
81
+ orderQuote: "/order-quote",
82
+ revealedHalf: "/escrow/revealed-half",
83
+ swept: "/swaps/{id}/swept",
84
+ });
85
+
86
+ // THE MONERO NODES THE ARRIVAL IS CHECKED AGAINST. Defaults are the list verified on 2026-10-02 (issue #85):
87
+ // mainnet - four public nodes of different operators; stagenet - our node and the one independent public node.
88
+ // `ready` opens only when two nodes of DIFFERENT operators agree on the block (see sdk/src/xmr-arrival.mjs), so
89
+ // the list is a list of WITNESSES, not of equal endpoints.
90
+ //
91
+ // OVERRIDABLE BY THE INTERFACE AND THE USER: `options.nodes` replaces the list (an array for the chosen network,
92
+ // or an object keyed by network) and `options.addNodes` appends to the defaults (that is how a user adds their
93
+ // own node). The checks themselves are not configurable - only the addresses.
94
+ export const DEFAULT_XMR_NODES = Object.freeze({
95
+ mainnet: [
96
+ { url: "https://xmr-node.cakewallet.com:18081", operator: "Cake Wallet" },
97
+ { url: "https://xmr.cryptostorm.is", operator: "cryptostorm" },
98
+ { url: "https://node.monero.fail", operator: "monero.fail" },
99
+ { url: "https://mainnet.xmr.kernal.eu:18089", operator: "kernal.eu" },
100
+ ],
101
+ stagenet: [
102
+ { url: "https://xmr.arrakisswap.trade", operator: "arrakisswap" },
103
+ { url: "https://stagenet.xmr.kernal.eu:38089", operator: "kernal.eu" },
104
+ ],
105
+ });
106
+
107
+ const nodeEntryOk = (raw) => (typeof raw === "string" && raw)
108
+ || (raw && typeof raw === "object" && typeof raw.url === "string" && raw.url);
109
+ const nodesArrayOk = (a) => Array.isArray(a) && a.every(nodeEntryOk);
110
+
111
+ // The resolved node map: the defaults, then the overrides. An unknown network key is a typo, not a silent no-op.
112
+ const nodesOf = (options, selected) => {
113
+ const custom = options && options.nodes;
114
+ const extra = options && options.addNodes;
115
+ const out = { mainnet: [...DEFAULT_XMR_NODES.mainnet], stagenet: [...DEFAULT_XMR_NODES.stagenet] };
116
+ if (custom !== undefined && custom !== null) {
117
+ if (nodesArrayOk(custom)) {
118
+ if (!(selected in out)) fail("bad-input", { field: "nodes", network: selected });
119
+ out[selected] = [...custom];
120
+ } else if (custom && typeof custom === "object" && !Array.isArray(custom)) {
121
+ for (const name of Object.keys(custom)) {
122
+ if (!(name in out)) fail("bad-input", { field: "nodes", unknown: name, known: Object.keys(out) });
123
+ if (!nodesArrayOk(custom[name])) fail("bad-input", { field: "nodes." + name });
124
+ out[name] = [...custom[name]];
125
+ }
126
+ } else fail("bad-input", { field: "nodes" });
127
+ }
128
+ if (extra !== undefined && extra !== null) {
129
+ if (!nodesArrayOk(extra)) fail("bad-input", { field: "addNodes" });
130
+ if (!(selected in out)) fail("bad-input", { field: "addNodes", network: selected });
131
+ out[selected] = [...out[selected], ...extra];
132
+ }
133
+ return out;
134
+ };
135
+
136
+ const pathOf = (template, params) => template.replace(/\{([a-z]+)\}/g, (whole, name) => {
137
+ const value = params ? params[name] : undefined;
138
+ if (value === undefined || value === null || value === "") fail("bad-input", { field: "route.param", param: name });
139
+ return encodeURIComponent(String(value));
140
+ });
141
+
142
+ const routesOf = (options) => {
143
+ const custom = (options && options.routes) || {};
144
+ for (const name of Object.keys(custom)) {
145
+ // AN UNKNOWN NAME IS A TYPO, NOT "let's add our own": otherwise the path silently fails to apply, and the
146
+ // call falls back to a default nobody knows about.
147
+ if (!(name in DEFAULT_ROUTES)) fail("bad-input", { field: "routes", unknown: name, known: Object.keys(DEFAULT_ROUTES) });
148
+ if (typeof custom[name] !== "string" || !custom[name]) fail("bad-input", { field: "routes." + name });
149
+ }
150
+ return { ...DEFAULT_ROUTES, ...custom };
151
+ };
152
+
153
+ // THE POLL RATE AND TIMEOUT FOR WATCHING THE MONERO ADDRESS. The defaults come from the engine's settings, not
154
+ // from numbers in the code: the rate is the same as the page's (www/js/core/chainSource.js, pollInterval =
155
+ // API.pollMs), and the timeout is from the Monero timings (blockTimeReal x confirmTarget): that is how long it
156
+ // takes to gather the confirmations of the target, and declaring the wait expired before that would be lying
157
+ // about the money. Both numbers are overridable by the `watch` option.
158
+ const positive = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : null);
159
+ const watchOf = (options) => {
160
+ const w = (options && options.watch) || {};
161
+ const pollMs = w.pollMs === undefined ? (positive(engine.config.API && engine.config.API.pollMs) || 5000) : Number(w.pollMs);
162
+ if (!Number.isFinite(pollMs) || pollMs <= 0) fail("bad-input", { field: "watch.pollMs" });
163
+ const monero = engine.config.MONERO || {};
164
+ const fallback = (positive(monero.blockTimeReal) || 120000) * (positive(monero.confirmTarget) || 10);
165
+ const timeoutMs = w.timeoutMs === undefined ? fallback : Number(w.timeoutMs);
166
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) fail("bad-input", { field: "watch.timeoutMs" });
167
+ return { pollMs, timeoutMs };
168
+ };
169
+
170
+ const endpointsOf = (raw, options) => {
171
+ const e = (options && options.endpoints) || {};
172
+ const explorer = e.explorer || {};
173
+ const rpc = e.rpc || {};
174
+ const text = (v) => (typeof v === "string" && v ? v : null);
175
+ return {
176
+ explorer: {
177
+ evmTx: text(explorer.evmTx) || text(raw.explorerTx),
178
+ evmAddr: text(explorer.evmAddr) || text(raw.explorerAddr),
179
+ evmName: text(explorer.evmName) || text(raw.explorerName),
180
+ // The engine builds the Monero explorer from the swap's network; here you can supply your own template with `{tx}`.
181
+ xmrTx: text(explorer.xmrTx),
182
+ },
183
+ rpc: {
184
+ // The EVM node: for the engine it serves as a parameter for wallet_addEthereumChain, for the core as the
185
+ // read address if the leg driver talks to it itself. Overriding is allowed, but the network and the
186
+ // contract addresses must change together: changing the node without the network produces refusals that
187
+ // look like a code error (that is why chainId is checked separately and that check is not configurable).
188
+ evm: text(rpc.evm) || text(raw.rpcUrl),
189
+ monero: text(rpc.monero),
190
+ },
191
+ };
192
+ };
193
+
194
+ export function buildConfig(options) {
195
+ const chains = engine.config.CHAINS.map(chainConfig);
196
+ const chosen = chains.find((c) => c.id === options.settlement.chain) || null;
197
+ if (!chosen) fail("bad-input", { field: "settlement.chain", known: chains.map((c) => c.id) });
198
+ const rawChain = engine.config.CHAINS.find((c) => c.id === options.settlement.chain) || {};
199
+ const routes = routesOf(options);
200
+ const watch = watchOf(options);
201
+ const endpoints = endpointsOf(rawChain, options);
202
+ const nodes = nodesOf(options, options.xmrNetwork);
203
+ const vm = options.settlement.vm || chosen.vm;
204
+ if (!vm) fail("bad-input", { field: "settlement.vm" });
205
+ if (vm !== chosen.vm) fail("bad-input", { field: "settlement.vm", chain: chosen.id, vm: chosen.vm, got: vm });
206
+ const leg = legFor(vm);
207
+ if (!leg) fail("not-implemented", { surface: "settlement.vm", vm, implemented: implementedVms() });
208
+
209
+ // ROUTE PROVIDERS: our declared routes today, an external aggregator when it arrives. The choice comes through
210
+ // options and is checked against the registry: an unknown provider is a typo, not "we'll try and see".
211
+ // One beating another is decided by an explicit order, not by response speed: otherwise the price would change
212
+ // based on who made it over the network first.
213
+ const known = routeProviders();
214
+ const prefer = Array.isArray(options.routing && options.routing.prefer) && options.routing.prefer.length
215
+ ? options.routing.prefer.map((id) => String(id))
216
+ : known.filter((id) => id === "declared");
217
+ for (const id of prefer) {
218
+ if (!known.includes(id)) fail("bad-input", { field: "routing.prefer", unknown: id, known });
219
+ }
220
+
221
+ // MONERO NETWORKS THE CORE KNOWS. The engine's registry declares only the one the demo is configured for
222
+ // (XMR_NETWORKS: stagenet). But the SDK contract (index.d.ts, xmrNetwork) names THREE standard names -
223
+ // mainnet/stagenet/testnet - and that is not about the demo: the withdrawal page takes the network FROM THE
224
+ // NODE, and on a production network its network is mainnet, not a typo. So three standard ones are added to
225
+ // the declared ones; anything else is still a refusal with the known list.
226
+ const STANDARD_XMR_NETWORKS = ["mainnet", "stagenet", "testnet"];
227
+ const xmrNetworks = [...new Set([...Object.values(engine.config.XMR_NETWORKS || {}), ...STANDARD_XMR_NETWORKS])];
228
+ if (!xmrNetworks.includes(options.xmrNetwork)) {
229
+ fail("bad-input", { field: "xmrNetwork", known: xmrNetworks });
230
+ }
231
+ const guards = engine.config.SIGNING_GUARDS || {};
232
+ return {
233
+ settlement: { chain: chosen, vm },
234
+ chains,
235
+ xmrNetworks,
236
+ assetsBase: options.assetsBase,
237
+ apiBase: options.apiBase,
238
+ mode: options.mode,
239
+ limits: {
240
+ // SIZE LIMITS COME WITH THE LIVE QUOTE, NOT FROM A FILE: the provider node declares them, and different
241
+ // nodes have different ones. While there is no quote, the honest value is null, not an invented number.
242
+ minAmount: null,
243
+ maxAmount: null,
244
+ stepAmount: null,
245
+ xmrConfirmTarget: engine.config.MONERO.confirmTarget ?? null,
246
+ // WATCH POLLING: the values above (engine settings or the `watch` option).
247
+ watchPollMs: watch.pollMs,
248
+ watchTimeoutMs: watch.timeoutMs,
249
+ // HOW MANY CONFIRMATIONS BEFORE `ready` - a NUMBER IN THE CONFIGURATION so it changes in one place. The
250
+ // default is the engine's target (10 blocks ~ 20 minutes at 2-minute blocks, the owner's decision). The
251
+ // confirmation window must fit before the ready deadline, or a hopeless trade would start - see
252
+ // xmr-arrival.mjs (assertTimeForConfirmations) and SIGNING_GUARDS.minReadyLeadSec under www/js.
253
+ xmrConfirmations: engine.config.MONERO.confirmTarget ?? 10,
254
+ xmrBlockTimeSec: Math.round((engine.config.MONERO.blockTimeReal ?? 120000) / 1000),
255
+ readyLeadWarnSec: guards.readyLeadWarnSec ?? null,
256
+ // THE PRICE POLL RATE AND THE REQUEST TIMEOUT - from the engine's settings, not from numbers in the SDK:
257
+ // two numbers in two places would silently diverge, and polling would become more frequent than the
258
+ // backend allows.
259
+ quotePollMs: engine.config.API.pollMs ?? 5000,
260
+ httpTimeoutMs: engine.config.API.timeoutMs ?? 20000,
261
+ },
262
+ routing: { providers: known, prefer },
263
+ // NAMED PATHS AND ADDRESSES: `route` substitutes parameters, `endpoints` carries the already-resolved values
264
+ // (the default from the registry, the override from the options).
265
+ routes: { ...routes },
266
+ endpoints,
267
+ // THE NODES THE XMR ARRIVAL IS CHECKED AGAINST - by network, with the defaults from the verified list and any
268
+ // overrides the caller (or the user) brought. `nodesFor` is the pick for one network.
269
+ xmrNodes: nodes,
270
+ nodesFor: (network) => (nodes && nodes[network]) ? nodes[network] : [],
271
+ route: (name, params) => {
272
+ if (!(name in routes)) fail("bad-input", { field: "route", unknown: name, known: Object.keys(routes) });
273
+ return pathOf(routes[name], params);
274
+ },
275
+ codes: { steps: STEP_CODES, checks: CHECK_CODES, errors: ERROR_CODES },
276
+ };
277
+ }
package/src/engine.mjs ADDED
@@ -0,0 +1,53 @@
1
+ // THE ENGINE SEAM, NOW SELF-CONTAINED.
2
+ //
3
+ // The engine modules the core depends on are mirrored under sdk/engine/ - byte-for-byte copies kept in
4
+ // sync by tools/build-sdk-engine.mjs and guarded by tools/check-sdk-engine.mjs. This file is the ONLY
5
+ // place that knows where the engine lives; no other SDK module does.
6
+ //
7
+ // WHY A VERIFIED MIRROR, NOT A MOVE. The very same modules are loaded by the demo page in the browser
8
+ // (www/js), and the production web root is www/ alone - the sdk/ directory is not served there (see
9
+ // tools/serve.mjs). Lifting the files out of www/js would 404 every engine import on the live site, and
10
+ // dozens of repository checks read those sources by their www/js path. So the single source of truth
11
+ // stays www/js and the SDK carries a copy: any drift reddens check-sdk-engine, so it cannot diverge
12
+ // silently (the same rule build-sweep uses for the sweep/rfq copies).
13
+ //
14
+ // The mirror is generated - never edit sdk/engine/ by hand. Change www/js, then run:
15
+ // node tools/build-sdk-engine.mjs
16
+ export * as config from "../engine/core/config.js";
17
+ export * as guards from "../engine/core/preSignGuards.js";
18
+ export * as restore from "../engine/recovery/restore.js";
19
+ // STORES - THROUGH THIS SAME SEAM. Both modules take an adapter from outside (get/set/remove), and by default
20
+ // use web storage if it is present: without an adapter they run in memory rather than fail on a missing DOM.
21
+ export * as store from "../engine/core/store.js";
22
+ export * as auth from "../engine/evm/auth.js";
23
+ export * as swap from "../engine/core/swap.js";
24
+ export * as recoveryFile from "../engine/recovery/recoveryFile.js";
25
+ export * as halves from "../engine/atomic/halves.js";
26
+ export * as orderContext from "../engine/atomic/orderContext.js";
27
+ export * as order from "../engine/atomic/order.js";
28
+ // HALF TRANSFER: the byte format and opening are the very same ones the page worker uses
29
+ // (www/js/atomic/order-worker.js). The core keeps no copy of the format: the commitment is computed over those
30
+ // exact bytes, and a second copy would silently drift from the calldata.
31
+ export * as halfenc from "../engine/atomic/halfenc.js";
32
+ // The engine's EVM layer: talking to the chain (order terms, sending, reading) - through this seam.
33
+ export * as evm from "../engine/evm/index.js";
34
+ // THE DEAL ENGINE NO LONGER LIVES IN www/js (#32, wave 3): the order flow moved into the package
35
+ // (sdk/src/swap-flow.mjs) and is declared in sdk/index.d.ts. The page reaches it through the bridge, so the
36
+ // mirror no longer carries core/swap-flow.js - what stays in the seam is the ORDER CLIENT (the half worker),
37
+ // the ORDER QUOTE source and the shared CLAIM GAS LIMIT that the moved flow reads through this same seam.
38
+ export * as orderClient from "../engine/atomic/order-client.js";
39
+ export * as rfqSource from "../engine/core/rfqSource.js";
40
+ export * as claimGas from "../engine/evm/claimGas.js";
41
+ export * as escrow from "../engine/evm/escrow.js";
42
+ export * as halvesSweep from "../engine/monero/halvesSweep.js";
43
+ // MONERO ADDRESSES - THROUGH THE SAME MODULE THE DEMO USES. The format, the network prefixes and the address
44
+ // check live in www/js/monero/address.js (checkAddress: base58 + keccak256 + network), and the page uses it
45
+ // too. The wallet adapter needs exactly networkFromShape: its own address must belong to the network named by
46
+ // the core, and that is checked by the same code, not by a private prefix table.
47
+ export * as xmrAddress from "../engine/monero/address.js";
48
+ // THE unlock_time RULE AND THE BLOCK READER - FROM THE SAME MIRROR, NOT A COPY. A second copy of "the output
49
+ // is barred until a boundary" would be a second truth about money, so the rule lives under www/js
50
+ // (monero/unlock.js) together with the Monero block parser (monero/scan.js), and the SDK reads them through
51
+ // the engine mirror. check-sdk-engine reddens on any drift, so the two cannot diverge silently.
52
+ export * as xmrUnlock from "../engine/monero/unlock.js";
53
+ export * as xmrScan from "../engine/monero/scan.js";
package/src/errors.mjs ADDED
@@ -0,0 +1,24 @@
1
+ // SDK ERRORS: a code plus params, with no human-facing text.
2
+ //
3
+ // message is deliberately equal to the code: it is read by logs and tests. The interface must do the
4
+ // showing, taking the code and params - otherwise the wording would end up in the core, with nowhere left
5
+ // to translate it.
6
+
7
+ export class SdkError extends Error {
8
+ constructor(code, params = {}) {
9
+ super(code);
10
+ this.name = "SdkError";
11
+ this.code = code;
12
+ this.params = params;
13
+ }
14
+ toJSON() {
15
+ return { code: this.code, params: this.params };
16
+ }
17
+ }
18
+
19
+ export const fail = (code, params) => {
20
+ throw new SdkError(code, params);
21
+ };
22
+
23
+ // Refusal as a value: part of the SDK returns a refusal state instead of throwing (the quote snapshot, a guard verdict).
24
+ export const refusal = (code, params = {}) => ({ ok: false, error: { code, params } });
package/src/http.mjs ADDED
@@ -0,0 +1,72 @@
1
+ // THE SDK TRANSPORT: the API address and the fetch function COME FROM OUTSIDE.
2
+ //
3
+ // Why not a global fetch: the interface lives in different environments (browser, SSR, tests, later a mobile
4
+ // app), and requiring a global fetch of it would mean dictating its environment. Besides, the timeout and the
5
+ // response parsing must live in one place rather than in every call: otherwise one call forgets the timeout and
6
+ // another forgets the status-code check, and a network refusal looks like an empty market.
7
+ import { SdkError } from "./errors.mjs";
8
+
9
+ const defaultFetch = () => (typeof fetch === "function" ? fetch : null);
10
+
11
+ export function createHttp({ apiBase, fetchImpl = null, timeoutMs = 20_000 }) {
12
+ const impl = fetchImpl || defaultFetch();
13
+ return {
14
+ /** The response as data. A network refusal and a non-2xx are a code, not a transport exception. */
15
+ // METHOD, HEADERS AND BODY ARE PASSED FROM OUTSIDE: without that a POST with a body would silently leave as
16
+ // a GET, and the service would answer with a refusal to a request it did not understand. The defaults are
17
+ // the previous ones (GET without a body).
18
+ async json(path, { signal = null, method = "GET", headers = null, body = null } = {}) {
19
+ if (typeof impl !== "function") throw new SdkError("bad-input", { field: "fetch" });
20
+ const ctrl = new AbortController();
21
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
22
+ const abort = () => ctrl.abort();
23
+ if (signal) signal.addEventListener("abort", abort, { once: true });
24
+ try {
25
+ const init = { signal: ctrl.signal, method, headers: { accept: "application/json", ...(headers || {}) } };
26
+ if (body !== null && body !== undefined) init.body = body;
27
+ const res = await impl(apiBase + path, init);
28
+ if (!res || typeof res.ok !== "boolean") throw new SdkError("server-unavailable", { why: "bad-response" });
29
+ if (!res.ok) throw new SdkError("server-unavailable", { status: res.status });
30
+ return await res.json();
31
+ } catch (error) {
32
+ if (error instanceof SdkError) throw error;
33
+ throw new SdkError("server-unavailable", { why: error && error.name === "AbortError" ? "timeout" : "fetch" });
34
+ } finally {
35
+ clearTimeout(timer);
36
+ if (signal) signal.removeEventListener("abort", abort);
37
+ }
38
+ },
39
+ /**
40
+ * THE SAME AS `json`, BUT THE SERVICE'S REFUSAL IS RETURNED AS A VALUE: { ok, status, body }.
41
+ *
42
+ * Why a separate method. `json` throws server-unavailable {status} and LOSES the response body, while the
43
+ * body carries the service's reason (our app answers {ok:false,error:"..."}). For the "server" step the
44
+ * reason is needed in full: without it a 403 "the escrow belongs to another wallet" is indistinguishable
45
+ * from a 400 "an escrow address is required", and the interface gets "something went wrong" instead of an
46
+ * answer to its question. A connection error is still thrown as a code: "did not answer" and "answered and
47
+ * refused" are different things, and the caller must see them.
48
+ */
49
+ async tryJson(path, { signal = null, method = "GET", headers = null, body = null } = {}) {
50
+ if (typeof impl !== "function") throw new SdkError("bad-input", { field: "fetch" });
51
+ const ctrl = new AbortController();
52
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
53
+ const abort = () => ctrl.abort();
54
+ if (signal) signal.addEventListener("abort", abort, { once: true });
55
+ try {
56
+ const init = { signal: ctrl.signal, method, headers: { accept: "application/json", ...(headers || {}) } };
57
+ if (body !== null && body !== undefined) init.body = body;
58
+ const res = await impl(apiBase + path, init);
59
+ if (!res || typeof res.ok !== "boolean") throw new SdkError("server-unavailable", { why: "bad-response" });
60
+ let parsed = null;
61
+ try { parsed = await res.json(); } catch { parsed = null; }
62
+ return { ok: res.ok, status: res.status, body: parsed };
63
+ } catch (error) {
64
+ if (error instanceof SdkError) throw error;
65
+ throw new SdkError("server-unavailable", { why: error && error.name === "AbortError" ? "timeout" : "fetch" });
66
+ } finally {
67
+ clearTimeout(timer);
68
+ if (signal) signal.removeEventListener("abort", abort);
69
+ }
70
+ },
71
+ };
72
+ }