@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/index.d.ts ADDED
@@ -0,0 +1,1417 @@
1
+ // THE SDK CONTRACT: what the protocol and the interface agree on.
2
+ //
3
+ // This is the SINGLE source of truth about the API shape. The implementation must match it, and this is checked by
4
+ // machine (tools/check-sdk.mjs): the list of exports is compared against this file, and the check codes against the
5
+ // list that occurs in the engine's guards.
6
+ //
7
+ // THE RULES THE SDK HOLDS (and which are checked):
8
+ // 1) not a single user-facing string: only codes and params. Texts, wording and translations are the interface's;
9
+ // 2) not a single DOM access: `document`, `window`, `localStorage` do not occur in the SDK. Storage and the wallet
10
+ // come from outside: as an adapter and an EIP-1193 provider;
11
+ // 3) the SDK opens no windows: the interface shows the wallet modal, the SDK accepts a ready provider;
12
+ // 4) funds are not locked until the interface has confirmed the recovery file is saved (onRecoveryFile);
13
+ // 5) the guards cannot be bypassed: preflight is part of the same path, not a separate button.
14
+ //
15
+ // Naming language: the values are data. `step` and `code` are stable strings from the tables below; they are
16
+ // translated, shown and coloured as one sees fit. They change only with a package version bump (see CHANGELOG).
17
+
18
+ export interface NinseiOptions {
19
+ /** The service's HTTP API base. Default is "/api" (the interface proxies it to our server). */
20
+ apiBase?: string;
21
+ /**
22
+ * The settlement network. `chain` is an identifier from `sdk.config.chains` (e.g. "arbitrum-sepolia", "solana").
23
+ * `vm` may be omitted: it is taken from the network registry, and a foreign one cannot be substituted - a network
24
+ * without a driver answers with the code `not-implemented`, rather than being assumed EVM by default.
25
+ */
26
+ settlement: { chain: string; vm?: string };
27
+ /** The Monero network. A value from `sdk.config.xmrNetworks`. */
28
+ xmrNetwork: "mainnet" | "stagenet" | "testnet";
29
+ /** Where to load workers and WASM from (order-worker, the Monero bundles). Default "/". */
30
+ assetsBase?: string;
31
+ /** Storage for unfinished swaps and sessions. Default - a localStorage adapter, if present. */
32
+ storage?: StorageAdapter;
33
+ /** "live" - real nodes and chain; "sim" - a sandbox (mocks, accelerated time, failure scenarios). */
34
+ mode?: "live" | "sim";
35
+ /** An injected fetch function (SSR, tests, your own transport). */
36
+ fetch?: FetchLike;
37
+ /** The clock. In "sim" the SDK uses it to accelerate time. */
38
+ now?: () => number;
39
+ /**
40
+ * Route providers (the DEX/aggregator leg) and the order of preference. An unknown identifier is `bad-input`:
41
+ * the order is set explicitly, not by whoever answered over the network first.
42
+ * A provider's record also DECLARES THE SHAPE OF ITS SWAP: "sync" - the router runs in the user's own
43
+ * transaction, so the swap and the escrow funding are one transaction (Uniswap, SushiSwap); "async" - the
44
+ * settlement arrives later as a separate action, and then the record must also say what ends execution
45
+ * (CoWSwap). A path that assumes "one transaction" refuses an asynchronous provider BY NAME, not silently.
46
+ */
47
+ routing?: { prefer?: readonly string[] };
48
+ /**
49
+ * ADDRESSES AND PATHS SUPPLIED FROM OUTSIDE. The default is our service and our explorers; the options are for
50
+ * whoever runs the swap themselves. ADDRESSES ARE CONFIGURABLE, NOT CHECKS: the pre-signature guards stay enabled
51
+ * under any configuration.
52
+ */
53
+ endpoints?: {
54
+ readonly explorer?: { readonly evmTx?: string; readonly evmAddr?: string; readonly evmName?: string; readonly xmrTx?: string };
55
+ readonly rpc?: { readonly evm?: string; readonly monero?: string };
56
+ };
57
+ /**
58
+ * Renaming API paths by name: { swaps, swap, evmSwaps, orderQuote, revealedHalf, swept }.
59
+ * An unknown name is bad-input (a typo must not silently fall back to the default).
60
+ */
61
+ routes?: Readonly<Record<string, string>>;
62
+ /**
63
+ * READING THE CHAIN FROM OUTSIDE - a seam for whoever runs the SDK outside a browser. By default the chain is read
64
+ * by the page's wallet (the engine's readContract), and in Node there is no such wallet: predict, the order state
65
+ * and the fee cannot be read, so a live run from Node was impossible. Your own function ({ to, data }) -> hex goes
66
+ * both into the lock step and into the swap start. THIS IS A READ, NOT A NODE SUBSTITUTION: the addresses and the
67
+ * network are still taken from the registry and the options, and the pre-signature guards stay enabled under any
68
+ * configuration.
69
+ */
70
+ evmCall?: (request: { readonly to: string; readonly data: string }) => Promise<string>;
71
+ /**
72
+ * READING THE CONTRACT CODE FROM OUTSIDE (issue #103): ({ address }) -> hex code. Needed for the factory-code
73
+ * check - `extcodehash` is keccak256 of this code, and the factory a provider names in its registry record is
74
+ * matched against the interface's known builds. Same rule as evmCall: a read, not a node substitution.
75
+ */
76
+ evmCode?: (request: { readonly address: string }) => Promise<string>;
77
+ /**
78
+ * THE INTERFACE'S OWN ADMISSION LIST - policy, not proof. The chain (maker registry, providerOf, taken from the
79
+ * quote) proves whose key signed; this says whom THIS interface trades with at all. An array of provider
80
+ * addresses (or providerIds) or a predicate `(provider) => boolean`. A provider the chain confirms but the list
81
+ * excludes is refused with `quote-provider-not-allowed`.
82
+ */
83
+ allowedProviders?: readonly string[] | ((provider: string) => boolean);
84
+ /**
85
+ * THE INTERFACE'S FACTORY POLICY (issue #103) - policy, not proof, and sitting next to allowedProviders. The
86
+ * factory is taken from the provider's REGISTRY RECORD (`factoryOf`), the signed quote must name the same one, and
87
+ * its CODE is matched against this list of build hashes (`extcodehash` = keccak256 of the runtime code). A factory
88
+ * whose code is not on the list is refused with `quote-factory-code-unknown`. An empty/absent list means "no
89
+ * policy": the record's factory must still be named and agree with the quote.
90
+ */
91
+ knownFactoryCodes?: readonly string[];
92
+ /**
93
+ * A PASS TO OUR SERVICE for the "server" step (recording the swap with us). Only a wallet can sign the sign-in
94
+ * message, and the wallet's role in the SDK is an adapter: windows and message signatures remain the interface's,
95
+ * and the core receives a READY pass through the same seam the interface obtains it with today
96
+ * (www/js/evm/auth.js, signIn).
97
+ * A missing option is not an error: the "server" step then names the app's refusal with a code (`server-refused`,
98
+ * 401), rather than going in blind.
99
+ */
100
+ serverAuth?: () => Promise<string | null> | string | null;
101
+ /**
102
+ * POLLING THE MONERO ADDRESS: the rate and the timeout for the XMR arrival. The defaults come from the engine's
103
+ * settings (the page's rate `API.pollMs`; the timeout - `blockTimeReal x confirmTarget`, that is, the time to
104
+ * gather the target's confirmations). Zero and negative values are `bad-input`: "wait zero" is a typo, not a
105
+ * setting.
106
+ */
107
+ watch?: { readonly pollMs?: number; readonly timeoutMs?: number };
108
+ /**
109
+ * YOUR OWN SWAP RECORD AND YOUR OWN ORDER SIDE - A CAPABILITY, NOT A REQUIREMENT. By default the core builds the
110
+ * record and the order side (halves, points, the DLEQ proof) ITSELF and ONCE: this same side goes both into the
111
+ * record and into the lock terms. The function is for whoever wants to bring their own side (for example, to know
112
+ * it in advance and predict the escrow address before signing). Then the core does not build the side itself and
113
+ * takes `side` from the result; the lock terms must then carry this side's points and commitments.
114
+ */
115
+ createRecord?: (request: StartRequest) => Promise<SwapRecord> | SwapRecord;
116
+ }
117
+
118
+ /** The result of assembling the swap record: the record itself, your own side and what is missing. */
119
+ export interface SwapRecord {
120
+ readonly swap: Readonly<Record<string, unknown>>;
121
+ /** Our side of the order: halves, points, the proof and the encryption key. The shape is set by the builder. */
122
+ readonly side: unknown;
123
+ /** What the record is missing: for a start that is the counterparty's half (`counterparty-half`). */
124
+ readonly missing: readonly string[];
125
+ /** The next step the record names (for a start - `order`). */
126
+ readonly nextStep: string;
127
+ }
128
+
129
+ export interface StorageAdapter {
130
+ get(key: string): string | null | Promise<string | null>;
131
+ set(key: string, value: string): void | Promise<void>;
132
+ remove(key: string): void | Promise<void>;
133
+ }
134
+
135
+ export interface HttpResponse {
136
+ ok: boolean;
137
+ status: number;
138
+ headers?: { get(name: string): string | null };
139
+ json(): Promise<unknown>;
140
+ text(): Promise<string>;
141
+ }
142
+
143
+ export type FetchLike = (url: string, init?: unknown) => Promise<HttpResponse>;
144
+
145
+ /** EIP-1193: the minimum the SDK requires of a wallet. */
146
+ export interface Eip1193Provider {
147
+ request(args: { method: string; params?: unknown[] | object }): Promise<unknown>;
148
+ on?(event: string, handler: (...args: unknown[]) => void): void;
149
+ removeListener?(event: string, handler: (...args: unknown[]) => void): void;
150
+ }
151
+
152
+ export interface Ninsei {
153
+ /** Networks, tokens, limits, thresholds. The interface takes the lists from here, not hardcoded. */
154
+ readonly config: SdkConfig;
155
+ readonly wallet: WalletApi;
156
+ readonly quotes: QuotesApi;
157
+ /** Pre-signature checks. Returns codes, not text: showing is the interface's job. */
158
+ preflight(offer: QuoteLike, options?: PreflightOptions): Promise<PreflightResult>;
159
+ readonly swaps: SwapsApi;
160
+ /** The XMR withdrawal (what the sweep page does): parsing the file, requesting data and the withdrawal itself. */
161
+ readonly sweep: SweepApi;
162
+ /** Order checks: the counterparty's side and the joint address. */
163
+ readonly order: OrderApi;
164
+ /**
165
+ * Actions on an ALREADY created escrow: marking ready, claiming, refunding. They go through the same chain-read
166
+ * seam as the lock. The slot check against the chain stands in them BEFORE the signature, and without
167
+ * expectations (`expect`) they sign nothing.
168
+ */
169
+ readonly actions: ActionsApi;
170
+
171
+ readonly recovery: RecoveryApi;
172
+ /** Only in "sim" mode: failure scenarios and sandbox time control. */
173
+ readonly sim: SimApi | null;
174
+ }
175
+
176
+ export interface SdkConfig {
177
+ /** The settlement network chosen in the options, and its virtual machine. */
178
+ readonly settlement: { readonly chain: ChainConfig; readonly vm: string };
179
+ readonly chains: readonly ChainConfig[];
180
+ readonly xmrNetworks: readonly string[];
181
+ /** The witness nodes per Monero network: the built-in defaults merged with the `nodes` and `addNodes` options. */
182
+ readonly xmrNodes: Readonly<Record<string, readonly (XmrNode | string)[]>>;
183
+ /** The witness nodes for one Monero network; a network with no nodes answers an empty list. */
184
+ nodesFor(network: string): readonly (XmrNode | string)[];
185
+ readonly assetsBase: string;
186
+ readonly apiBase: string;
187
+ readonly mode: "live" | "sim";
188
+ /** Addresses already resolved: the default from the network registry, the override from the options. */
189
+ readonly endpoints: {
190
+ readonly explorer: { readonly evmTx: string | null; readonly evmAddr: string | null; readonly evmName: string | null; readonly xmrTx: string | null };
191
+ readonly rpc: { readonly evm: string | null; readonly monero: string | null };
192
+ };
193
+ /** Named API paths: name -> template, already with the overrides from the options. */
194
+ readonly routes: Readonly<Record<string, string>>;
195
+ /** Assemble a path by name and params (route("swept", { id })). An unknown name is bad-input. */
196
+ route(name: string, params?: Readonly<Record<string, string>>): string;
197
+ /** The protocol's thresholds (windows, confirmations, limits) - data, not interface constants. */
198
+ readonly limits: {
199
+ readonly minAmount: number | null;
200
+ readonly maxAmount: number | null;
201
+ readonly stepAmount: number | null;
202
+ readonly xmrConfirmTarget: number | null;
203
+ readonly readyLeadWarnSec: number | null;
204
+ /** How often the SDK polls the market (from the engine's settings). */
205
+ readonly quotePollMs: number;
206
+ /** The timeout of a single request to the service. */
207
+ readonly httpTimeoutMs: number;
208
+ /** How often the SDK polls the watch on the Monero address (an engine setting or the `watch` option). */
209
+ readonly watchPollMs: number;
210
+ /** How long the SDK waits for the XMR to arrive before answering with the code `xmr-timeout`. */
211
+ readonly watchTimeoutMs: number;
212
+ };
213
+ /** The available route providers and the order of preference chosen in the options. */
214
+ readonly routing: { readonly providers: readonly string[]; readonly prefer: readonly string[] };
215
+ /** The step, check and error codes the SDK works with: a table for the interface and translations. */
216
+ readonly codes: {
217
+ readonly steps: readonly string[];
218
+ readonly checks: readonly string[];
219
+ readonly errors: readonly string[];
220
+ };
221
+ }
222
+
223
+ export interface ChainConfig {
224
+ readonly id: string;
225
+ /** The network's virtual machine: "evm" today; "tron" and "solana" - when their leg drivers appear. */
226
+ readonly vm: string;
227
+ readonly chainId: number;
228
+ readonly name: string;
229
+ readonly nativeSymbol: string;
230
+ readonly tokens: readonly TokenConfig[];
231
+ readonly dex: { readonly routes: readonly unknown[] } | null;
232
+ }
233
+
234
+ export interface TokenConfig {
235
+ readonly id: string;
236
+ readonly symbol: string;
237
+ readonly address: string | null;
238
+ readonly decimals: number;
239
+ }
240
+
241
+ // --- The wallet ---------------------------------------------------------------------------------
242
+
243
+ export interface WalletApi {
244
+ /**
245
+ * Connect a wallet adapter. The SDK opens no windows - the adapter is brought by the interface
246
+ * (`evmWallet(provider)` from `@ninsei-labs/sdk/adapters`; another VM will have its own adapter).
247
+ * The adapter must be for the same vm as the settlement network: otherwise `bad-input`.
248
+ */
249
+ use(adapter: WalletAdapter): void;
250
+ /** Whether a wallet is connected and on which network. */
251
+ status(): WalletStatus;
252
+ connect(options?: { chainId?: number }): Promise<WalletStatus>;
253
+ disconnect(): Promise<void>;
254
+ address(): string | null;
255
+ /** Switch the wallet's network. Returns the state after the attempt. */
256
+ switchChain(chainId: number): Promise<WalletStatus>;
257
+ /**
258
+ * Balances of the settlement network's native and ERC-20 tokens, in minimal units as a string. For a Monero
259
+ * wallet - one XMR balance in atomic units (piconero): it is read by the wallet itself, with the method
260
+ * `unlockedBalance`. What is not read is not replaced with zero: a refusal comes as a code.
261
+ */
262
+ balances(tokens?: readonly string[]): Promise<readonly Balance[]>;
263
+ /** Subscribe to wallet changes. Returns an unsubscribe function. */
264
+ subscribe(handler: (status: WalletStatus) => void): () => void;
265
+ /**
266
+ * What the adapter signs and reads the chain with: for EVM - an EIP-1193 provider. Needed by the settlement-leg
267
+ * driver; by the interface - only if it wants to hand it to its own extension. The shape depends on the vm.
268
+ */
269
+ driver(): unknown;
270
+ }
271
+
272
+ /**
273
+ * THE WALLET ADAPTER - the only thing the SDK knows about a wallet. The core knows neither EIP-1193 nor the Solana
274
+ * wallet-standard: it requires this interface and the `vm` matching the settlement network. That is how a new VM's
275
+ * wallet is added by an adapter rather than by editing the core.
276
+ */
277
+ export interface WalletAdapter {
278
+ /**
279
+ * The adapter's virtual machine: "evm" (the settlement network) today; "monero" - the wallet of the swap's other
280
+ * side. Any other vm is not accepted: substituting a foreign one means reading the balance and signing on the
281
+ * wrong chain.
282
+ */
283
+ readonly vm: string;
284
+ /** A short name for the interface (not a user-facing text). */
285
+ readonly id?: string;
286
+ connect(options?: { chainId?: number | string }): Promise<void>;
287
+ disconnect(): Promise<void>;
288
+ /** The address or public key: the string this VM uses to name an account. */
289
+ address(): string | null | Promise<string | null>;
290
+ /** What this VM calls the network: for EVM - a chainId number, for others - their identifier. */
291
+ chainId(): number | string | null | Promise<number | string | null>;
292
+ /**
293
+ * THE FREE REMAINDER OF THE MONERO WALLET in atomic units (piconero; 1 XMR = 10^12 - 12 decimal places). The same
294
+ * method the XMR withdrawal calls (on the sweep page - `getUnlockedBalance` in monero-ts), so the adapter sits on
295
+ * top of it without an adapter. Declared only on the Monero adapter: for the others the balance is read by the leg
296
+ * driver.
297
+ */
298
+ unlockedBalance?(): Promise<bigint | number | string>;
299
+ /** Subscribe to wallet changes. Returns an unsubscribe function. */
300
+ subscribe(handler: () => void): () => void;
301
+ /**
302
+ * SYNCING WITH THE NODE, if this wallet can (Monero: `wallet.sync()`, the name from the sweep page). Declared
303
+ * optional: EVM has no syncing - the chain is read by the leg driver. No method - `not-implemented`, not silence.
304
+ * For Monero, after the pass the `subscribe` subscribers are notified.
305
+ */
306
+ sync?(): Promise<void>;
307
+ /**
308
+ * STOP THE WALLET. The name is taken from the page (sweep/sweep.js calls `wallet.close()`); `disconnect` is the
309
+ * interface's name. On the Monero adapter both lead to one: the library wallet is closed. Optional - on EVM there
310
+ * is nothing to stop.
311
+ */
312
+ close?(): Promise<void>;
313
+ /**
314
+ * Send a leg transaction (lock, marking ready, claim, refund). The request shape is what this VM calls a transfer;
315
+ * for EVM it is `{ to, data, value }` and the transaction hash in response.
316
+ */
317
+ send(request: { readonly to: string; readonly data?: string; readonly value?: bigint | number | string }): Promise<string>;
318
+ /**
319
+ * The transaction receipt: asked ONCE, returns the receipt or null (not in a block yet). The waiting policy is set
320
+ * by the caller, not the adapter - otherwise two calls would wait differently.
321
+ */
322
+ receipt(txHash: string): Promise<unknown>;
323
+ /** Switching the network, if this VM can do it from the SDK. No method - `not-implemented`, not silence. */
324
+ switchChain?(chainId: number | string): Promise<void>;
325
+ driver(): unknown;
326
+ }
327
+
328
+ export interface WalletStatus {
329
+ readonly connected: boolean;
330
+ readonly address: string | null;
331
+ readonly chainId: number | null;
332
+ /** Does the wallet's network match the settlement network. */
333
+ readonly chainMatches: boolean;
334
+ }
335
+
336
+ export interface Balance {
337
+ readonly token: string;
338
+ readonly symbol: string;
339
+ readonly decimals: number;
340
+ /**
341
+ * Minimal units as a string: numbers of this size do not fit in a number. For XMR these are atomic units
342
+ * (piconero): 1 XMR = 10^12 at decimals = 12 - the same ones the XMR withdrawal carries; the core does not
343
+ * reconvert them.
344
+ */
345
+ readonly amount: string;
346
+ }
347
+
348
+ // --- Quotes -------------------------------------------------------------------------------------
349
+
350
+ export interface FirmQuoteRequest {
351
+ /** Who gives the quote. Without it there is no one to ask - this is not "the first one at hand". */
352
+ readonly providerId: string;
353
+ /** The order terms: how much, to whom, the deadlines, the salt. The context the signature is bound to is assembled from them. */
354
+ readonly order: Readonly<Record<string, unknown>>;
355
+ }
356
+
357
+ export interface QuotesApi {
358
+ /**
359
+ * Live indicative quotes. Returns an unsubscribe function.
360
+ * A snapshot arrives on every tick and contains only data. The last one to unsubscribe stops the polling.
361
+ */
362
+ watch(request: QuoteRequest, onSnapshot: (snapshot: QuoteSnapshot) => void): () => void;
363
+ /** The last snapshot without waiting: needed by whoever subscribed later than the first poll. */
364
+ snapshot(): QuoteSnapshot | null;
365
+ /** A binding quote with a TTL. A refusal comes as a code, not text. */
366
+ firm(offer: FirmOffer): Promise<Quote>;
367
+ }
368
+
369
+ export interface QuoteRequest {
370
+ readonly direction: "buy" | "sell";
371
+ /** A token identifier from config ("usdc", "usdt", "dai", ...). For a sell - what the user receives. */
372
+ readonly token?: string;
373
+ /** The size on the user's side: for a buy - how much they pay, for a sell - how much XMR they give. */
374
+ readonly amount?: number;
375
+ /**
376
+ * THE EXPLICIT REQUEST SIZE in units of `unit` (buy - XMR, sell - token). Needed by whoever has already aligned
377
+ * the volume to the provider's grid themselves: the core asks for EXACTLY this number rather than deriving the
378
+ * size from `amount` anew.
379
+ * Not set - the size is derived from `amount`, as before.
380
+ */
381
+ readonly size?: number;
382
+ }
383
+
384
+ export interface QuoteSnapshot {
385
+ readonly direction: "buy" | "sell";
386
+ readonly token: string;
387
+ /** The amount on the user's side, as they entered it: for a buy - in the token, for a sell - in XMR. */
388
+ readonly amount: number | null;
389
+ /** The size asked of the provider, in units of `unit`. The price does not depend on it: it is needed so the
390
+ * provider says whether it takes such a size. */
391
+ readonly size: number;
392
+ /** The unit of the size: XMR for a buy, the token for a sell. */
393
+ readonly unit: string | null;
394
+ /** The best offer's price: non-XMR coins per 1 XMR. null - there is no offer (and then it is not zero). */
395
+ readonly rate: number | null;
396
+ /** How much XMR for this size (buy) or how much XMR the person gives (sell). null - not computed. */
397
+ readonly xmr: number | null;
398
+ /** When the snapshot was received, milliseconds of the epoch. */
399
+ readonly at: number;
400
+ /** The provider node whose price is in the snapshot. */
401
+ readonly providerId: string | null;
402
+ /** The best offer for the requested volume and side, AS THE SERVICE NAMED IT. The core does not recompute it:
403
+ * these are response data by which the interface confirms its own "best" mark by pair and provider. */
404
+ readonly best: QuoteBest | null;
405
+ /** Is the size ready: the node's grid and range are data, not an interface check. */
406
+ readonly sizeOk: boolean;
407
+ /** Why the size is not ready: `bad-size`, `below-min`, `above-max`, `no-offer`. `null` - ready. */
408
+ readonly sizeCode: QuoteSizeCode;
409
+ /** The provider's packet number: liveness of the market is counted by it, not by the clock. */
410
+ readonly seq: number | null;
411
+ /** All offers for this size. Indicative: only `quotes.firm` binds. */
412
+ readonly offers: readonly QuoteOffer[];
413
+ /** Book rows that did not become offers, with the reason as a code: `malformed`, `disabled`, `stale`. */
414
+ readonly refused: readonly QuoteRefusal[];
415
+ /** Reference rates (the price benchmark), as the service returned them. Not a swap offer. */
416
+ readonly references: readonly QuoteReference[];
417
+ /** Are there offers and is the service's response alive. */
418
+ readonly ok: boolean;
419
+ /** Why there is no market: a code, no text. */
420
+ readonly error?: { readonly code: ErrorCode };
421
+ }
422
+
423
+ export type QuoteSizeCode = "bad-size" | "below-min" | "above-max" | "no-offer" | null;
424
+
425
+ /** The best, as the service named it: the pair and the node. The core does not recompute the price. */
426
+ export interface QuoteBest {
427
+ readonly providerId: string;
428
+ readonly pair: string;
429
+ }
430
+
431
+ export interface QuoteOffer {
432
+ readonly providerId: string;
433
+ readonly displayName: string | null;
434
+ readonly pair: string;
435
+ readonly asset: string | null;
436
+ readonly currency: string | null;
437
+ readonly rate: number;
438
+ readonly rateQuote: number;
439
+ readonly min: number | null;
440
+ readonly max: number | null;
441
+ readonly step: number | null;
442
+ readonly ttlMs: number | null;
443
+ readonly at: number | null;
444
+ readonly seq: number | null;
445
+ readonly finality: string | null;
446
+ /** The network of the XMR side and of the settlement side, as the quote named them. Not named - null, not our network. */
447
+ readonly assetNetwork: string | null;
448
+ readonly currencyNetwork: string | null;
449
+ readonly networkStated: boolean;
450
+ /** The networks were filled in by the receiver / not named at all - this is a fact about the quote, not "matched". */
451
+ readonly networksDerived: boolean;
452
+ readonly networksUnnamed: boolean;
453
+ /** The reason named by the provider, as is; the package's signature and key; the age by its own mark. */
454
+ readonly why: string | null;
455
+ readonly signature: string | null;
456
+ readonly keyId: string | null;
457
+ readonly ageMs: number | null;
458
+ readonly networks: { readonly asset: string | null; readonly currency: string | null; readonly stated: boolean };
459
+ }
460
+
461
+ export interface QuoteRefusal {
462
+ readonly providerId: string;
463
+ readonly displayName: string | null;
464
+ readonly pair: string;
465
+ readonly asset: string | null;
466
+ readonly currency: string | null;
467
+ readonly rate: number | null;
468
+ readonly rateQuote: number | null;
469
+ readonly min: number | null;
470
+ readonly max: number | null;
471
+ readonly step: number | null;
472
+ readonly ttlMs: number | null;
473
+ readonly at: number | null;
474
+ readonly seq: number | null;
475
+ readonly ageMs: number | null;
476
+ readonly assetNetwork: string | null;
477
+ readonly currencyNetwork: string | null;
478
+ readonly networkStated: boolean;
479
+ readonly code: "malformed" | "disabled" | "stale";
480
+ /** The reason named by the provider, as is: this is data, not our wording. */
481
+ readonly why: string | null;
482
+ }
483
+
484
+ export interface QuoteReference {
485
+ readonly source: string;
486
+ readonly value: number | null;
487
+ readonly at: number | null;
488
+ }
489
+
490
+ export interface FirmOffer {
491
+ readonly direction: "buy" | "sell";
492
+ readonly token: string;
493
+ readonly amount: number;
494
+ readonly receiveTo: string;
495
+ readonly providerId?: string;
496
+ }
497
+
498
+ /**
499
+ * A binding quote. The SDK does not reassemble the signature body: the signed fields are passed on as is - otherwise
500
+ * the signature stops matching.
501
+ */
502
+ export interface Quote {
503
+ readonly id: string;
504
+ readonly direction: "buy" | "sell";
505
+ readonly providerId: string;
506
+ readonly chain: string;
507
+ readonly token: string;
508
+ readonly amount: number;
509
+ readonly xmr: number;
510
+ readonly rate: number;
511
+ /** The moment after which the quote must not be signed, milliseconds of the epoch. */
512
+ readonly expiresAt: number;
513
+ readonly receiveTo: string;
514
+ readonly signedPayload: unknown;
515
+ }
516
+
517
+ // --- Pre-signature checks -----------------------------------------------------------------------
518
+
519
+ export interface PreflightOptions {
520
+ readonly payBalanceWei?: string;
521
+ readonly payAmountWei?: string;
522
+ readonly nativeBalanceWei?: string;
523
+ readonly requiredNativeWei?: string;
524
+ readonly gasReserveWei?: string;
525
+ /** Consent to the token allowance (approve). Without it the allowance check blocks the signature. */
526
+ readonly allowanceConsent?: boolean;
527
+ readonly allowanceWei?: string;
528
+ readonly nowMs?: number;
529
+ }
530
+
531
+ export interface PreflightResult {
532
+ /** Whether it can be signed right now. */
533
+ readonly ok: boolean;
534
+ /** The codes of what stands in the way. Empty if ok. */
535
+ readonly blockedBy: readonly CheckCode[];
536
+ /** What was checked and with what verdict - in the order of passage. */
537
+ readonly checks: readonly Check[];
538
+ }
539
+
540
+ export interface Check {
541
+ /** A stable check code (the table in README). */
542
+ readonly code: CheckCode;
543
+ readonly ok: boolean;
544
+ /** Was the check run at all: `false` means "not measured", not "good". */
545
+ readonly checked: boolean;
546
+ /** Does it stand in the way of signing. */
547
+ readonly blocks: boolean;
548
+ /** The check's original name in the engine: for reports and debugging, not for showing the user. */
549
+ readonly kind: string;
550
+ /** The numbers and names the verdict was taken from: need/have/symbol/min/max/at/expiresAt etc. */
551
+ readonly params: Readonly<Record<string, unknown>>;
552
+ }
553
+
554
+ export type CheckCode =
555
+ | "balance"
556
+ | "gas"
557
+ | "allowance"
558
+ | "consent"
559
+ | "network"
560
+ | "settlement-network"
561
+ | "stale"
562
+ | "deadline"
563
+ | "range"
564
+ | "size"
565
+ | "escrow-state"
566
+ | "escrow-points"
567
+ | "counterparty"
568
+ | "slots"
569
+ | "unchecked"
570
+ | "unknown";
571
+
572
+ /** The same as Quote, but a node response body is allowed too: the SDK does not reassemble the signed fields. */
573
+ export type QuoteLike = Quote | (Record<string, unknown> & { signedPayload?: unknown });
574
+
575
+ // --- Swaps --------------------------------------------------------------------------------------
576
+
577
+ export interface SwapsApi {
578
+ /**
579
+ * Start a swap. The SDK holds the step order, not the screen:
580
+ * 1) guards (preflight); 2) the swap record (createSwap); 3) the order - it GENERATES the key halves;
581
+ * 4) the recovery file and the interface's confirmation that the file is saved; 5) locking the funds (signature);
582
+ * 6) saving on the server; 7) opening the watch on the Monero address.
583
+ * HALVES DO NOT EXIST BEFORE THE ORDER: the recovery file carries MY half of the spend key and the counterparty's
584
+ * open points, and they are taken from the signed order (www/js/atomic/order-worker.js, createOrderBuilder; in the
585
+ * engine they are fetched by halvesWalletOf). So the file is assembled AFTER the order, but BEFORE locking the
586
+ * funds.
587
+ * FUNDS ARE NOT LOCKED UNTIL onRecoveryFile ALLOWS IT: the order is like this not for convenience, but because a
588
+ * lock without a recovery file is a lock with nothing to open it with if the browser is lost.
589
+ */
590
+ start(request: StartRequest): Promise<SwapStart>;
591
+ /**
592
+ * WAIT FOR THE XMR TO ARRIVE at the order's joint address. The polling uses the same request as the page's
593
+ * (GET /swaps/{id}), the rate and the timeout - from `config.limits` and overridable here.
594
+ * It arrived - the FACT is returned (the amount, the confirmations, the txid); we did not wait - a refusal with
595
+ * the CODE `xmr-timeout`, separate from the others: "the wait expired" is a state of the money, not a breakdown.
596
+ * XMR ON THE ADDRESS BUT LOCKED BY `unlock_time` is a THIRD answer with its own code `xmr-locked` (the boundary,
597
+ * a height or a date, is in `params.until`): locked funds are NOT an arrival, and nothing is signed under them.
598
+ * IF `expect` AND `wallet` ARE PASSED, then after the arrival the ready mark (actions.markReady) is called with
599
+ * THESE expectations: the slot check against the chain stands in it before the signature. Without them the step
600
+ * names the next one (`next: "mark-ready"`) and signs nothing.
601
+ */
602
+ watch(request: SwapWatchRequest): Promise<SwapWatch>;
603
+ /**
604
+ * THE XMR ARRIVAL CHECK WITHOUT THE WATCH LOOP (issue #85). The client reads the chain itself with its own view
605
+ * key and `ready` opens only when at least two nodes of DIFFERENT operators see the transaction IN THE SAME BLOCK
606
+ * (the block hash matches) with the required confirmations. Refusals come as CODES: `xmr-nodes-disagree` (a synced
607
+ * node has no such transaction there or a different block hash), `xmr-no-nodes` (nothing reachable),
608
+ * `xmr-underpaid`, `xmr-locked`, `xmr-too-late`. A waiting state is RETURNED as `state` (`pending` | `one-node`),
609
+ * never a silent success.
610
+ */
611
+ verifyArrival(request: XmrArrivalRequest): Promise<XmrArrivalVerdict>;
612
+ /** Fewer seconds before the ready deadline than the confirmations take - a named refusal (`xmr-too-late`). */
613
+ assertTimeForConfirmations(request: XmrDeadlineRequest): number | null;
614
+ /**
615
+ * THE CLIENT'S OWN EARLY REFUND WHEN THE XMR DID NOT ARRIVE (issue #88). While the interface is open, a depositor
616
+ * who sees no XMR on the joint address takes the ETH back BEFORE `readyBy` - after it the maker may claim even
617
+ * without the ready mark, and the early window is gone. The arrival is decided by THE SAME check as the watch
618
+ * (`verifyArrival`). Only a positive `pending`/`one-node` verdict refunds; an `arrived` verdict does not; a failure
619
+ * to check (no nodes, a disagreement) throws its own code and signs nothing. It fires only when less than the
620
+ * confirmations' time is left before `readyBy` (`minConfirmations * blockTimeSec`, overridable with `leadSec`).
621
+ */
622
+ refundIfXmrMissing(request: AutoRefundRequest, deps?: AutoRefundDeps): Promise<AutoRefundVerdict>;
623
+ /** Unfinished swaps after a page reload. */
624
+ list(): Promise<readonly SwapState[]>;
625
+ get(id: string): Promise<SwapState | null>;
626
+ /**
627
+ * THE SERVICE'S SWAP LIST, MERGED INTO THE LOCAL ONE. The path is the page's (www/js/evm/auth.js,
628
+ * loadServerSwaps: GET /api/evm-swaps), and the merge rule is the ENGINE'S (core/swap.js, addServerSwaps): a
629
+ * local record always wins, the service only adds what this browser does not have, and a record that came
630
+ * from the service is marked `source: "server"`. The pass is obtained by the interface and only presented by
631
+ * the core (the `serverAuth` seam): without it the service's own refusal comes as a code. `added` says how
632
+ * many records the merge brought in - the interface decides by it whether the list changed, instead of
633
+ * guessing.
634
+ */
635
+ sync(): Promise<SwapSync>;
636
+ /**
637
+ * REMOVE A RECORD FROM THE LOCAL LIST. The record leaves THIS store alone: on the service and in the
638
+ * contract everything stays, so this is a tidy-up of the list, not a cancellation of the swap. `false` means
639
+ * there was no such record.
640
+ */
641
+ forget(id: string): Promise<boolean>;
642
+ /**
643
+ * THE DEAL-PROGRESS SNAPSHOT: the record's own facts, the wallet rebuilt from the halves and the DERIVED deal
644
+ * view (phase, allowed actions, deadlines, steps, the Monero leg, the two amounts). The progress screen draws
645
+ * this snapshot; it does not read the record or derive the state itself, so the deal's rules live in one place.
646
+ * `null` - there is no such record (this is NOT an empty state).
647
+ */
648
+ progress(id: string): Promise<SwapProgress | null>;
649
+ /**
650
+ * THE DEMONSTRATION RECORD'S ACTIONS (mode `sim`): confirm readiness, refund the ETH, sweep the XMR. In a live
651
+ * swap the same steps go through `actions` (a real escrow and the wallet adapter); these are the sandbox's record
652
+ * mutations, kept in the core rather than on the screen.
653
+ */
654
+ confirmReady(id: string): Promise<unknown>;
655
+ refundEth(id: string, by?: string): Promise<unknown>;
656
+ sweepNow(id: string): Promise<unknown>;
657
+ /**
658
+ * WHERE TO WITHDRAW THE XMR: writes the record's `receiveAddress` (it is part of the recovery file). The order
659
+ * and the chain are untouched. `null` - there is no such record.
660
+ */
661
+ setDestination(id: string, address: string): Promise<unknown>;
662
+ /**
663
+ * THE MONERO NETWORK OF AN ADDRESS, BY ITS SHAPE (`mainnet` | `stagenet` | `testnet`), or `null`. The format and
664
+ * the prefixes live in one module - the same one the Monero wallet adapter uses - so no interface keeps its own
665
+ * prefix table.
666
+ */
667
+ addressNetwork(address: string): Promise<string | null>;
668
+ }
669
+
670
+ /**
671
+ * THE DEAL-PROGRESS SNAPSHOT. `swap` carries the record's own fields (what the interface shows and hands back as
672
+ * order expectations); `swapWallet` is the wallet rebuilt from the halves (the recovery file is assembled from it);
673
+ * `view` is the DERIVED deal state. The names in `view` are the engine's derive output: `phase`/`phaseLabel`,
674
+ * `can` (the allowed actions), `deadlines`, `steps`, `xmr`/`xmrAmount`, `sellWindow`, `readyDeadline`, `terminal`.
675
+ */
676
+ export interface SwapProgress {
677
+ readonly id: string;
678
+ readonly swap: Readonly<Record<string, unknown>>;
679
+ readonly swapWallet: unknown;
680
+ readonly view: SwapProgressView;
681
+ }
682
+
683
+ /** The derived deal state the progress screen draws. Loose by design: the fields are the engine's derive output. */
684
+ export interface SwapProgressView {
685
+ readonly swap: unknown;
686
+ readonly phase: string;
687
+ readonly phaseLabel: string;
688
+ readonly note: string | null;
689
+ readonly sim: number;
690
+ readonly simWallTime: number;
691
+ readonly conf: number;
692
+ readonly confTarget: number;
693
+ readonly confProgress: number;
694
+ readonly can: Readonly<Record<string, unknown>>;
695
+ readonly deadlines: Readonly<Record<string, unknown>>;
696
+ readonly steps: readonly Readonly<Record<string, unknown>>[];
697
+ readonly xmr: Readonly<Record<string, unknown>>;
698
+ /** The amount promised by the quote and the one seen at the address: the interface shows both. */
699
+ readonly xmrAmount: { readonly expected: number | null; readonly seen: number | null };
700
+ readonly sellWindow: unknown;
701
+ readonly readyDeadline: Readonly<Record<string, unknown>>;
702
+ readonly xmrReclaimed: boolean;
703
+ readonly simulated: { readonly evm: boolean; readonly xmr: boolean };
704
+ /** The terminal state (the record's settlement), or null while the swap is live. */
705
+ readonly terminal: unknown;
706
+ }
707
+
708
+ /** The outcome of merging the service's list: how many rows the service gave, how many were new locally. */
709
+ export interface SwapSync {
710
+ readonly ok: true;
711
+ readonly added: number;
712
+ readonly seen: number;
713
+ }
714
+
715
+ /** ONE WITNESS NODE: the address, and whose node it is (two nodes of one operator are one opinion). */
716
+ export interface XmrNode {
717
+ readonly url: string;
718
+ readonly operator?: string;
719
+ }
720
+
721
+ /**
722
+ * THE ARRIVAL CHECK'S INPUTS. The joint address, the joint view SECRET and the joint SPEND PUBLIC key - all of them
723
+ * the client has and the backend does not need. `txid` is only a HINT from the maker's node (a fabricated one fails
724
+ * the check); without it blocks are walked from `fromHeight`.
725
+ */
726
+ export interface XmrArrivalRequest {
727
+ readonly nodes: readonly (XmrNode | string)[];
728
+ readonly address: string;
729
+ readonly viewSecret: string | bigint;
730
+ readonly spendPub: string;
731
+ readonly expectedAtomic: string | number | bigint;
732
+ readonly minConfirmations?: number;
733
+ readonly txid?: string | null;
734
+ readonly fromHeight?: number | null;
735
+ readonly nowSec?: number;
736
+ readonly timeoutMs?: number;
737
+ }
738
+
739
+ /** The XMR state the watch hands to the interface: the fact, the waiting state, or a refusal with a code. */
740
+ export interface XmrArrivalVerdict {
741
+ readonly seen: boolean;
742
+ readonly state: "arrived" | "pending" | "one-node";
743
+ readonly txid: string | null;
744
+ readonly height?: number;
745
+ readonly blockHash?: string;
746
+ readonly amountAtomic?: string;
747
+ readonly confirmations?: number;
748
+ readonly needConfirmations?: number;
749
+ readonly operators?: readonly string[];
750
+ readonly agreedBy?: readonly { readonly url: string; readonly operator: string; readonly blockHash: string; readonly confirmations: number }[];
751
+ }
752
+
753
+ /** What the client's own check established on the agreed block. */
754
+ export interface XmrArrivalFact {
755
+ readonly height: number;
756
+ readonly blockHash: string;
757
+ readonly operators: readonly string[];
758
+ readonly agreedBy: readonly { readonly url: string; readonly operator: string; readonly blockHash: string; readonly confirmations: number }[];
759
+ }
760
+
761
+ export interface XmrDeadlineRequest {
762
+ readonly readyBySec?: number | null;
763
+ readonly nowSec?: number;
764
+ readonly minConfirmations?: number;
765
+ readonly blockTimeSec?: number;
766
+ }
767
+
768
+ /**
769
+ * THE INPUTS OF THE CLIENT'S OWN EARLY REFUND. The joint address, the joint view secret, the joint spend public key
770
+ * and the expected sum are taken from `swap` when it is given (the same derivation as the watch), or passed
771
+ * explicitly. `expect` (the order expectations) and a wallet are mandatory: the refund reveals the depositor's half
772
+ * and checks the order's slots before signing.
773
+ */
774
+ export interface AutoRefundRequest {
775
+ /** The swap record; the joint address, view secret, spend point, half and XMR amount are read from it. */
776
+ readonly swap?: unknown;
777
+ readonly escrow?: string;
778
+ readonly halfLocker?: string;
779
+ readonly expect?: unknown;
780
+ readonly wallet?: unknown;
781
+ readonly address?: string;
782
+ readonly viewSecret?: string | bigint;
783
+ readonly spendPub?: string;
784
+ readonly expectedAtomic?: string | number | bigint;
785
+ readonly nodes: readonly (XmrNode | string)[];
786
+ readonly readyBySec?: number;
787
+ readonly fromHeight?: number | null;
788
+ readonly txid?: string | null;
789
+ readonly minConfirmations?: number;
790
+ readonly blockTimeSec?: number;
791
+ /** How long before `readyBy` the refund becomes due. Defaults to `minConfirmations * blockTimeSec`. */
792
+ readonly leadSec?: number;
793
+ readonly timeoutMs?: number;
794
+ readonly nowSec?: number;
795
+ readonly onStep?: (step: string) => void;
796
+ }
797
+
798
+ /** The seams the checks substitute; in production they are the real arrival check and the real refund. */
799
+ export interface AutoRefundDeps {
800
+ readonly verify?: (request: unknown) => Promise<{ seen: boolean }>;
801
+ readonly refund?: (request: unknown, wallet: unknown) => Promise<{ hash?: string | null }>;
802
+ readonly now?: () => number;
803
+ }
804
+
805
+ /** What the client's own refund decided: refunded, waiting, still time, or the XMR is there. */
806
+ export interface AutoRefundVerdict {
807
+ readonly refunded: boolean;
808
+ readonly state: "refunded" | "waiting" | "claim-window" | "arrived";
809
+ readonly hash?: string | null;
810
+ readonly leftSec: number;
811
+ readonly needSec?: number;
812
+ readonly readyBy?: number;
813
+ readonly nowSec?: number;
814
+ }
815
+
816
+ /**
817
+ * THE OUTCOME OF A START: funds locked, the swap recorded with us, the address watch opened.
818
+ * There is NO XMR arrival here YET - the state is named (`step: "xmr-incoming"`), not passed off as completion.
819
+ */
820
+ export interface SwapStart {
821
+ readonly id: string | null;
822
+ /** The escrow address FROM THE RECEIPT, not from a prediction. */
823
+ readonly escrow: string | null;
824
+ /** The order's joint Monero address - the one the XMR arrives at. */
825
+ readonly address: string;
826
+ readonly step: "xmr-incoming";
827
+ readonly server: {
828
+ readonly marked: boolean;
829
+ /** The service's response code to the mark. */
830
+ readonly httpStatus: number;
831
+ /** What the service said about tying the quote to the escrow (its refusal does not abort the swap). */
832
+ readonly orderBinding: unknown;
833
+ };
834
+ readonly watch: {
835
+ readonly registered: boolean;
836
+ readonly httpStatus: number;
837
+ /** The watch record's identifier at the service: the state is queried by it. */
838
+ readonly id: string | null;
839
+ readonly address: string;
840
+ readonly status: string | null;
841
+ readonly expectedXmr: number;
842
+ readonly receivedXmr: number;
843
+ readonly confirmations: number;
844
+ readonly txids: readonly string[];
845
+ };
846
+ /** The named next step: wait for the arrival. */
847
+ readonly next: "watch";
848
+ }
849
+
850
+ export interface SwapWatchRequest {
851
+ /** The watch record's identifier at the service (from SwapStart.watch.id). */
852
+ readonly id: string;
853
+ /** The escrow address for the ready mark (mandatory when `expect` and `wallet` are passed). */
854
+ readonly escrow?: string;
855
+ /** How often to ask the state. Default - `config.limits.watchPollMs`. */
856
+ readonly pollMs?: number;
857
+ /** How long to wait for the arrival. Default - `config.limits.watchTimeoutMs`. */
858
+ readonly timeoutMs?: number;
859
+ /** The order expectations for the slot check: passed TOGETHER with `wallet`, or not passed at all. */
860
+ readonly expect?: Readonly<Record<string, unknown>>;
861
+ /** The wallet for the ready mark. */
862
+ readonly wallet?: WalletAdapter;
863
+ /** Messages about the signing stage (codes, not user-facing texts). */
864
+ readonly onStep?: (stage: string, detail?: string) => void;
865
+ /**
866
+ * CLIENT-SIDE ARRIVAL CHECK (issue #85). With this the CLIENT decides: it reads the chain itself and `ready`
867
+ * opens only when two operators agree. The backend's `seen` is then only a notification and does not open the
868
+ * button. Without it the watch keeps the service's answer.
869
+ */
870
+ readonly verify?: Omit<XmrArrivalRequest, "minConfirmations">;
871
+ /** The confirmation target before `ready`. Default - `config.limits.xmrConfirmations` (10). */
872
+ readonly minConfirmations?: number;
873
+ /** The ready deadline in unix seconds; with less time left than the confirmations take it refuses with a code. */
874
+ readonly readyBySec?: number;
875
+ }
876
+
877
+ /** THE OUTCOME OF THE WATCH: the arrival is seen as a FACT, not assumed. */
878
+ export interface SwapWatch {
879
+ readonly seen: true;
880
+ readonly id: string;
881
+ readonly address: string | null;
882
+ /** The watch state at the service (awaiting_funding | funded | ready | ...). */
883
+ readonly status: string | null;
884
+ readonly expectedXmr: number | null;
885
+ readonly receivedXmr: number;
886
+ readonly confirmations: number;
887
+ readonly txids: readonly string[];
888
+ readonly at: number;
889
+ /** What the ready mark returned, if it was passed. */
890
+ readonly marked?: unknown;
891
+ /** The next step, when the mark was not passed. */
892
+ readonly next?: "mark-ready";
893
+ /** THE CLIENT'S OWN EVIDENCE, when the arrival was checked on two nodes (issue #85). */
894
+ readonly verified?: XmrArrivalFact;
895
+ /** Who decided: "client" when the two-node check was used. */
896
+ readonly source?: "client";
897
+ }
898
+
899
+ export interface StartRequest {
900
+ readonly quote: Quote;
901
+ /**
902
+ * THE ORDER TERMS - the context the DLEQ proof and the firm quote's signature are bound to
903
+ * (`orderContext`, fields: chainId, factory, locker, claimer, amount, readyBy, t1, salt). The core ALSO builds our
904
+ * order side from them and takes the points and commitments for the lock terms, so without them the start refuses,
905
+ * naming the field `order`, rather than substituting empty values.
906
+ */
907
+ readonly order: Readonly<Record<string, unknown>>;
908
+ /**
909
+ * THE COUNTERPARTY'S SIDE, if it already exists. In the sandbox the engine builds it as a stand-in (standIn); on a
910
+ * live chain a stand-in would mean money against a non-existent side - there its absence is a refusal. The core's
911
+ * own side does not depend on this: the core builds it.
912
+ */
913
+ readonly counterparty?: unknown;
914
+ /**
915
+ * THE SIGNED ORDER QUOTE - the provider's binding commitment (www/js/sdk/bridge.js, sdkOrderQuote). It is a
916
+ * DIFFERENT thing from `quote`: `quote` is the indicative book quote used by the guards (freshness, deadlines),
917
+ * while this one is the SIGNED SET the factory takes - the fee, its recipient, the registry address and the
918
+ * provider. The lock step reads it as `request.orderQuote` (sdk/src/swaps.mjs → lockRequest.quote) and refuses
919
+ * without it (`bad-input { step: "fund-order" }`): the factory takes a signed quote, not bare order terms.
920
+ */
921
+ readonly orderQuote?: QuoteLike;
922
+ /** The swap direction for the record. Default - `buy`. */
923
+ readonly side?: "buy" | "sell";
924
+ /** Where to receive: for a purchase - the Monero address, for a sale - the EVM address. */
925
+ readonly receiveTo: string;
926
+ /** The recovery file's passphrase. In memory; the interface puts it on disk if it deems fit. */
927
+ readonly passphrase?: string;
928
+ /**
929
+ * The EVM wallet for LOCKING THE FUNDS: an adapter with `send` and `receipt` (the same role as WalletAdapter).
930
+ * It comes from outside and is NOT substituted by default: without it the lock step answers with a code
931
+ * (`bad-input`), rather than going in blind. The recovery file has by now been confirmed by the person.
932
+ */
933
+ readonly wallet?: WalletAdapter;
934
+ /**
935
+ * Hand the recovery file to the interface and wait for the person's decision.
936
+ * `true` - the person confirmed the file is saved; `false` - the swap is not started.
937
+ */
938
+ onRecoveryFile(file: RecoveryFile): Promise<boolean>;
939
+ }
940
+
941
+ export interface RecoveryFile {
942
+ readonly name: string;
943
+ /** The file's contents as a string: the interface decides how to hand it to the user. */
944
+ readonly contents: string;
945
+ }
946
+
947
+ export interface Swap {
948
+ readonly id: string;
949
+ /** The current state. Also arrives in the subscription. */
950
+ state(): SwapState;
951
+ /** Subscribe to changes. Returns an unsubscribe function. */
952
+ on(event: "update", handler: (state: SwapState) => void): () => void;
953
+ /** The "checked by you" mark: opens the claim to the maker. */
954
+ markReady(): Promise<SwapState>;
955
+ /** Withdraw the XMR to the recipient's address. */
956
+ sweep(): Promise<SwapState>;
957
+ /** Refund the ETH when the claim did not happen. */
958
+ refund(): Promise<SwapState>;
959
+ }
960
+
961
+ export interface SwapState {
962
+ readonly id: string;
963
+ readonly direction: "buy" | "sell";
964
+ readonly pay: { readonly token: string; readonly amount: number };
965
+ readonly get: { readonly xmr: number };
966
+ /** The current step's code. The texts are the interface's. */
967
+ readonly step: StepCode;
968
+ /** All steps in the order of passage: what is done, what is running now, what failed. */
969
+ readonly steps: readonly StepState[];
970
+ /** The deadlines from the chain, seconds of the epoch. null - the contract has not reported them yet. */
971
+ readonly deadlines: { readonly readyBy: number | null; readonly t1: number | null };
972
+ readonly xmr: {
973
+ readonly confirmations: number;
974
+ readonly target: number;
975
+ readonly txid: string | null;
976
+ /** The amount promised by the quote and the one seen at the address: the interface shows both. */
977
+ readonly amount: { readonly expected: number | null; readonly seen: number | null };
978
+ };
979
+ /**
980
+ * THE COUNTERPARTY'S SETTLEMENT LEG. Named by the role, not the chain: for EVM `leg` is the escrow, for Tron and
981
+ * Solana it will be their contract, for Bitcoin - a script. The field `vm` says what to expect in `leg`.
982
+ */
983
+ readonly counterparty: {
984
+ readonly chain: string;
985
+ readonly vm: string;
986
+ /** The address of whatever secures the leg (the escrow on EVM), null - not created yet or not read. */
987
+ readonly leg: string | null;
988
+ readonly status: string | null;
989
+ readonly txs: readonly string[];
990
+ };
991
+ /** What is currently allowed to be done. The interface draws the buttons by these flags. */
992
+ readonly actions: { readonly markReady: boolean; readonly sweep: boolean; readonly refund: boolean };
993
+ /** What in this state is simulated and what is real. */
994
+ readonly simulated: { readonly evm: boolean; readonly xmr: boolean };
995
+ /** The refusal reason, if any: a code and params, no text. */
996
+ readonly error?: { readonly code: ErrorCode; readonly params?: Readonly<Record<string, unknown>> };
997
+ /** The terminal state, if the swap has ended. */
998
+ readonly terminal: TerminalCode | null;
999
+ /**
1000
+ * WHEN THE RECORD WAS CREATED (the record's own `createdAt`, an ISO string). null - an old record without
1001
+ * one: the list shows a dash rather than a date it does not have.
1002
+ */
1003
+ readonly createdAt: string | null;
1004
+ /** WHO GAVE THE PRICE. Either field may be absent: an old record or a swap without a provider. */
1005
+ readonly maker: { readonly id: string | null; readonly name: string | null };
1006
+ /** The engine's phase code and its human name: the list shows the name. */
1007
+ readonly phase: string | null;
1008
+ readonly phaseLabel: string | null;
1009
+ /** The swap has a settlement record: the list splits live from settled by THIS flag, not by the phase's text. */
1010
+ readonly settled: boolean;
1011
+ /** WHAT HAS ALREADY MOVED: the sum seen at the address and whether the record holds a lock or a secret. */
1012
+ readonly funds: { readonly receivedXmr: number; readonly escrowLocked: boolean };
1013
+ }
1014
+
1015
+ export interface StepState {
1016
+ readonly code: StepCode;
1017
+ readonly done: boolean;
1018
+ readonly active: boolean;
1019
+ /** The step failed (e.g. the maker did not lock the XMR): the interface draws a failure, not "still running". */
1020
+ readonly failed?: boolean;
1021
+ readonly at?: number | null;
1022
+ }
1023
+
1024
+ export type StepCode =
1025
+ | "awaiting_funding"
1026
+ | "funded"
1027
+ | "maker_locking"
1028
+ | "xmr_locked"
1029
+ | "ready"
1030
+ | "claimed"
1031
+ | "swept";
1032
+
1033
+ export type TerminalCode = "success" | "refunded_eth" | "xmr_returned" | "closed";
1034
+
1035
+ // --- The XMR withdrawal (the sweep page) --------------------------------------------------------
1036
+
1037
+ /**
1038
+ * The XMR withdrawal. Parsing and preparation do not move funds: these are reads. The withdrawal itself requires a
1039
+ * Monero wallet, which arrives as an adapter (like the EVM wallet), because syncing and signing live where there is
1040
+ * WASM - while the core stays DOM-free.
1041
+ */
1042
+ export interface SweepApi {
1043
+ /** What is known about the withdrawal from the recovery file: the address, the restore height, what we can dispose of. */
1044
+ plan(request: SweepPlanRequest): Promise<SweepPlan>;
1045
+ /**
1046
+ * Assemble the spend and view keys from the revealed half - with the mandatory check `(s_a + s_b)*G` against the
1047
+ * order's public key. It did not match - a refusal, not "let us try to send".
1048
+ */
1049
+ keys(request: SweepPlanRequest): Promise<SweepKeys>;
1050
+ /**
1051
+ * SIGN WITHOUT SENDING. Holds everything up to and including signing (the free remainder -> sign with
1052
+ * `relay: false`), and HOLDS the signed transaction, returning the open session's token. An empty remainder stops
1053
+ * the withdrawal here with the code `insufficient-funds`. `relay` sends it - so the interface has a place to show
1054
+ * the person what will leave, BEFORE sending.
1055
+ */
1056
+ sign(request: SweepRunRequest): Promise<SweepSignResult>;
1057
+ /** Send what `sign` signed, close the wallet and tell the service about the withdrawal. */
1058
+ relay(request: SweepRelayRequest): Promise<SweepRunResult>;
1059
+ /**
1060
+ * TELL THE SERVICE ABOUT THE WITHDRAWAL. The mark does not affect the transfer - it is already on the network - so
1061
+ * it can be repeated: the interface calls this even after `relay` if the report did not go through then. A
1062
+ * connection refusal gives `reported: false`.
1063
+ */
1064
+ report(request: SweepReportRequest): Promise<SweepReportResult>;
1065
+ /** Perform the withdrawal as a whole (`sign` + `relay`). */
1066
+ run(request: SweepRunRequest): Promise<SweepRunResult>;
1067
+ }
1068
+
1069
+ /** The withdrawal mark: the swap identifier and the transfer hash. */
1070
+ export interface SweepReportRequest {
1071
+ readonly swapId: string;
1072
+ readonly txid: string;
1073
+ }
1074
+
1075
+ export interface SweepReportResult {
1076
+ readonly reported: boolean;
1077
+ }
1078
+
1079
+ export interface SweepPlanRequest {
1080
+ readonly file: { readonly contents: string } | string;
1081
+ readonly passphrase: string;
1082
+ }
1083
+
1084
+ export interface SweepPlan {
1085
+ readonly swapId: string | null;
1086
+ /** The joint Monero address the XMR arrived at. */
1087
+ readonly address: string;
1088
+ /** Whether there is enough data to dispose of the funds (whether our own spend-key half is present). */
1089
+ readonly spendable: boolean;
1090
+ /** The restore height: 0 - unknown. */
1091
+ readonly restoreHeight: number;
1092
+ /** The order of the withdrawal steps. The texts are the interface's. */
1093
+ readonly steps: readonly SweepStep[];
1094
+ /** What is missing for the withdrawal to go. Codes, no texts. */
1095
+ readonly missing: readonly SweepMissing[];
1096
+ /** The counterparty's revealed half, if the app returned it. This is a public value: it lies on the chain. */
1097
+ readonly half: string | null;
1098
+ }
1099
+
1100
+ /**
1101
+ * The withdrawal keys. THEY ARE COMPUTED HERE AND CHECKED HERE TOO: the contract only makes sure the hash of the
1102
+ * revealed half matched the commitment, but cannot check the link "revealed half and the point in the address" -
1103
+ * there is no ed25519 in EVM. So we check it, and before any syncing.
1104
+ */
1105
+ export interface SweepKeys {
1106
+ /** The spend key: 64 hexadecimal characters, byte order as in Monero (little-endian). */
1107
+ readonly privateSpendKey: string;
1108
+ readonly privateViewKey: string;
1109
+ /** The joint Monero address the keys were assembled for. */
1110
+ readonly address: string;
1111
+ }
1112
+
1113
+ export type SweepStep = "unlock" | "sync" | "sweep" | "relay";
1114
+ export type SweepMissing = "restore-height" | "spendable" | "revealed-half";
1115
+
1116
+ export interface SweepRunRequest extends SweepPlanRequest {
1117
+ /** Where to withdraw the XMR to. */
1118
+ readonly to: string;
1119
+ /** Returns a Monero wallet: the core does not drag in WASM. */
1120
+ readonly connectWallet: (plan: SweepPlan) => Promise<MoneroWalletAdapter>;
1121
+ }
1122
+
1123
+ /**
1124
+ * THE ROLE OF A MONERO WALLET, not a library. The method names are the ones the sweep page already calls on monero-ts
1125
+ * (`sync`, `getUnlockedBalance`, `sweepUnlocked`, `relayTxs`, `close`), so the adapter can be assembled over it or over
1126
+ * anything else, and the core needs neither WASM nor a 3.6 MB worker.
1127
+ */
1128
+ export interface MoneroWalletAdapter {
1129
+ /** Open the wallet by the withdrawal plan (address, restore). */
1130
+ open(plan: SweepPlan): Promise<void>;
1131
+ /** Sync with the node. */
1132
+ sync(): Promise<void>;
1133
+ /** The free remainder in atomic units. */
1134
+ unlockedBalance(): Promise<bigint | number | string>;
1135
+ /** Prepare the withdrawal: relay: false - sign only, no sending. */
1136
+ sweepUnlocked(request: { readonly address: string; readonly relay: boolean }): Promise<unknown>;
1137
+ /** Send what was signed. Returns the transaction identifiers. */
1138
+ relay(signed: unknown): Promise<readonly string[]>;
1139
+ /** Close the wallet. */
1140
+ close(): Promise<void>;
1141
+ }
1142
+
1143
+ /** What signing returned: the open session's token and what lies under it. */
1144
+ export interface SweepSignResult {
1145
+ /** The open session's token for `relay`. It contains no keys - they are owned by the library wallet. */
1146
+ readonly token: string;
1147
+ /** The free remainder at the moment of signing, in atomic units (string). */
1148
+ readonly amount: string;
1149
+ /** Where the transfer was signed to. */
1150
+ readonly to: string;
1151
+ /** The address the XMR leaves from (the swap's joint address). */
1152
+ readonly swept: string;
1153
+ }
1154
+
1155
+ /** Sending what was signed: the session token `sign` returned. */
1156
+ export interface SweepRelayRequest {
1157
+ readonly token: string;
1158
+ }
1159
+
1160
+ export interface SweepRunResult {
1161
+ readonly txids: readonly string[];
1162
+ readonly swept: string;
1163
+ /**
1164
+ * Whether the service was told about the withdrawal. THE REPORT DOES NOT AFFECT THE TRANSFER - it is already on
1165
+ * the network - so a failed report does not make the withdrawal unsuccessful: it gives `reported: false`, so the
1166
+ * interface offers to report again.
1167
+ */
1168
+ readonly reported: boolean;
1169
+ }
1170
+
1171
+ // --- Recovery// --- Recovery ------------------------------------------------------------------------------
1172
+
1173
+ /** Order checks. Both are BEFORE the signature: after it there is nothing to fix. */
1174
+ export interface OrderApi {
1175
+ /** The order context string the proof and the quote signature are bound to. */
1176
+ context(terms: Readonly<Record<string, unknown>>): string;
1177
+ /** The counterparty's side: the proof in the context of THIS order and the proven ed25519 point matching. */
1178
+ verifyCounterparty(request: { readonly side: unknown; readonly order: Readonly<Record<string, unknown>>; readonly sealed?: unknown; readonly ownEncPriv?: unknown }): unknown;
1179
+ /** The joint Monero address from your own spend half and the other's point. */
1180
+ jointAddress(request: { readonly ownSpendHalf: string; readonly ownViewHalf: string; readonly otherSpendPoint: string; readonly otherViewPoint: string; readonly network?: string }): unknown;
1181
+ /**
1182
+ * The order state, READ FROM THE CHAIN (escrow status()): which of the four states the order is in. The state is
1183
+ * read by a call, not guessed from logs. It is read by the same chain-read seam as the actions (`evmCall`), so
1184
+ * the wallet that reads is the wallet that will sign.
1185
+ */
1186
+ status(escrow: string): Promise<OrderStatus>;
1187
+ /**
1188
+ * The order deadlines from the chain: t1 (the settlement boundary), readyBy (the ready-mark boundary) and the
1189
+ * terms hash the recovery file is bound to. The screen uses them to say whether the person is in the dead zone.
1190
+ */
1191
+ deadlines(escrow: string): Promise<OrderDeadlines>;
1192
+ }
1193
+
1194
+ /** THE ORDER STATE, READ FROM THE CHAIN (escrow status()): three flags and the remainder. */
1195
+ export interface OrderStatus {
1196
+ readonly isFunded: boolean;
1197
+ readonly isClaimed: boolean;
1198
+ readonly isRefunded: boolean;
1199
+ readonly balance: bigint;
1200
+ }
1201
+
1202
+ /** THE ORDER DEADLINES FROM THE CHAIN: the settlement boundary t1, the ready-mark boundary readyBy and termsHash. */
1203
+ export interface OrderDeadlines {
1204
+ readonly t1: number;
1205
+ readonly readyBy: number;
1206
+ readonly termsHash: string;
1207
+ }
1208
+
1209
+ /** Order actions on a ready escrow. The slot check against the chain stands in them BEFORE the signature. */
1210
+ export interface ActionsApi {
1211
+ /** The "I saw the XMR at the joint address" mark. Without it claiming is forbidden by the contract. */
1212
+ markReady(request: ActionsRequest, wallet: WalletAdapter): Promise<unknown>;
1213
+ /** Claiming: it reveals your own half, so expectations are mandatory. */
1214
+ claim(request: ActionsRequest & { readonly halfClaimer: string }, wallet: WalletAdapter): Promise<unknown>;
1215
+ /** Refunding: it also reveals a half. */
1216
+ refund(request: ActionsRequest & { readonly halfLocker: string }, wallet: WalletAdapter): Promise<unknown>;
1217
+ }
1218
+
1219
+ /** The shared part of an action request: the escrow address and the slot expectations checked against the chain before signing. */
1220
+ export interface ActionsRequest {
1221
+ readonly escrow: string;
1222
+ readonly expect: Readonly<Record<string, unknown>>;
1223
+ readonly onStep?: (stage: string, detail?: string) => void;
1224
+ }
1225
+
1226
+ export interface RecoveryApi {
1227
+ /** Parse the recovery file. Without the right passphrase - a refusal with a code, not an exception. */
1228
+ open(file: { readonly contents: string } | string, passphrase?: string): Promise<RecoveredWallet>;
1229
+ }
1230
+
1231
+ export interface RecoveredWallet {
1232
+ /** The wallet address that is restored from the file. */
1233
+ readonly address: string;
1234
+ /** Whether there is enough data to dispose of the funds, or this is observation only. */
1235
+ readonly spendable: boolean;
1236
+ /** The swap the file relates to, if the file names it. */
1237
+ readonly swapId: string | null;
1238
+ }
1239
+
1240
+ // --- The sandbox ("sim" mode) -------------------------------------------------------------------
1241
+
1242
+ export interface SimApi {
1243
+ /** Failure scenarios the interface lays out failure screens for. */
1244
+ scenarios(): readonly string[];
1245
+ /** Enable a scenario by code. */
1246
+ apply(scenario: string): void;
1247
+ /** Accelerate the sandbox time: a multiplier to the real one. */
1248
+ speed(multiplier: number): void;
1249
+ /** Move the sandbox time forward, milliseconds. */
1250
+ advance(ms: number): void;
1251
+ /** What is chosen now: the scenario, the speed, the time shift. This is NOT the swap state. */
1252
+ debug(): { scenario: string | null; speed: number; advancedMs: number };
1253
+ }
1254
+
1255
+ // --- Error codes --------------------------------------------------------------------------------
1256
+
1257
+ export type ErrorCode =
1258
+ | "not-implemented"
1259
+ | "bad-input"
1260
+ | "wallet-not-connected"
1261
+ | "wallet-rejected"
1262
+ | "wrong-chain"
1263
+ | "quote-unavailable"
1264
+ | "quote-refused"
1265
+ | "quote-stale"
1266
+ /** The order-quote signature (EIP-712, v6) did not recover to any address. */
1267
+ | "quote-signature-invalid"
1268
+ /** The signature is valid but belongs to a key other than the one the quote names. */
1269
+ | "quote-key-mismatch"
1270
+ /** A field the signature must cover is absent: the quote is not verifiable at all. */
1271
+ | "quote-field-unbound"
1272
+ /** The quote's own `validUntil` has passed. */
1273
+ | "quote-expired"
1274
+ /** There was no chain read to ask the registry whose key it is: the claim stays unproven. */
1275
+ | "quote-registry-unchecked"
1276
+ /** The registry has no provider for this key: nobody owns it. */
1277
+ | "quote-key-unknown"
1278
+ /** The registry maps the key to a different provider than the quote names. */
1279
+ | "quote-provider-mismatch"
1280
+ /** The chain confirms the provider, but this interface's allow-list does not admit it (policy). */
1281
+ | "quote-provider-not-allowed"
1282
+ /** There was no way to read the factory or its code: the claim stays unproven (issue #103). */
1283
+ | "quote-factory-unchecked"
1284
+ /** The provider's registry record names no factory (zero). */
1285
+ | "quote-factory-unknown"
1286
+ /** The signed quote names a different factory than the registry record. */
1287
+ | "quote-factory-mismatch"
1288
+ /** The factory's runtime code is not a build this interface knows (`extcodehash`). */
1289
+ | "quote-factory-code-unknown"
1290
+ | "recovery-declined"
1291
+ | "recovery-failed"
1292
+ | "storage-unavailable"
1293
+ | "server-unavailable"
1294
+ /** The service answered and did not accept: the connection got through, the request did not (status and reason in params). */
1295
+ | "server-refused"
1296
+ /** The wait for the XMR to arrive expired: a separate code, because this is a state of the money, not a breakdown. */
1297
+ | "xmr-timeout"
1298
+ /**
1299
+ * XMR is on the address but BARRED by the transaction's `unlock_time` (issue #84): the funds cannot be spent
1300
+ * until a height or a date, which arrives in `params.until`. Distinct from `xmr-timeout` - money is there, and
1301
+ * the ready mark must not be signed under it.
1302
+ */
1303
+ | "xmr-locked"
1304
+ /** A synced witness node disagrees about the block or the transaction (issue #85). */
1305
+ | "xmr-nodes-disagree"
1306
+ /** Every witness node is unreachable, so nothing can be confirmed. */
1307
+ | "xmr-no-nodes"
1308
+ /** The amount that arrived is below the expected one. */
1309
+ | "xmr-underpaid"
1310
+ /** Less time remains before the ready deadline than the required confirmations take. */
1311
+ | "xmr-too-late"
1312
+ | "contract-reverted"
1313
+ | "insufficient-funds"
1314
+ /** The maker declines to pay the claim gas; the refusal is shown before the deal (issue #78). */
1315
+ | "gas-refused-by-maker"
1316
+ /** The claim gas was ordered but the recipient's balance did not grow. */
1317
+ | "gas-not-arrived"
1318
+ /** Gas arrived, but below what a claim costs. */
1319
+ | "gas-too-little"
1320
+ | "unknown";
1321
+
1322
+ /**
1323
+ * An SDK refusal or breakdown, raised with a code and params and NO human-facing text: the wording is the
1324
+ * interface's. `message` is deliberately equal to `code`, so it reads cleanly in logs.
1325
+ */
1326
+ export declare class SdkError extends Error {
1327
+ readonly code: ErrorCode;
1328
+ readonly params: Readonly<Record<string, unknown>>;
1329
+ constructor(code: ErrorCode, params?: Readonly<Record<string, unknown>>);
1330
+ toJSON(): { readonly code: ErrorCode; readonly params: Readonly<Record<string, unknown>> };
1331
+ }
1332
+
1333
+ /** The step codes the SDK works with (`state.step`): the same list as the `StepCode` type. */
1334
+ export declare const STEP_CODES: readonly StepCode[];
1335
+ /** The check codes a preflight verdict names (`checks[].code`): the same list as the `CheckCode` type. */
1336
+ export declare const CHECK_CODES: readonly CheckCode[];
1337
+ /** The error codes the SDK refuses with (`error.code`, `SdkError.code`): the same list as the `ErrorCode` type. */
1338
+ export declare const ERROR_CODES: readonly ErrorCode[];
1339
+
1340
+ export declare function createNinsei(options: NinseiOptions): Ninsei;
1341
+
1342
+ // --- The maker's level set: the level walk the node and the client agree on ------------------------
1343
+
1344
+ /**
1345
+ * THE LEVEL WALK. A maker publishes a LEVEL SET - chunks of volume with their own prices - and both the node and
1346
+ * the client compute a firm price by WALKING it, before anything is signed. `levelsWalkSpec.mjs` is the ONE
1347
+ * implementation, a byte-identical twin of the node's copy (`rfq/lib/levelsWalkSpec.mjs`), and it is exported so a
1348
+ * consumer reaches the same price: the maker's set in, the walk's answer out.
1349
+ *
1350
+ * NUMBERS ARE INTEGERS, BigInt ONLY. XMR is in atomic units (1 XMR = 1e12); a price is in the asset's atomic units
1351
+ * per 1 WHOLE XMR. A refusal comes back as a NAMED `reason` token, never as an exception.
1352
+ */
1353
+
1354
+ /** One level: the NEXT chunk of volume, not a running total. Amounts are parsed with {@link toBigInt}. */
1355
+ export interface Level {
1356
+ /** The chunk's volume in XMR atomic units. */
1357
+ readonly amount: bigint | number | string;
1358
+ /** The chunk's price in the asset's atomic units per 1 whole XMR. */
1359
+ readonly price: bigint | number | string;
1360
+ }
1361
+
1362
+ /** The orientation of a set: `ask` - the best price is the LOWEST; `bid` - the HIGHEST. */
1363
+ export type LevelOrientation = "ask" | "bid";
1364
+
1365
+ /** The side the taker is on: `buy` pays the asset, `sell` receives it. */
1366
+ export type LevelRole = "buy" | "sell";
1367
+
1368
+ /**
1369
+ * A NAMED refusal over a level set (returned, not thrown). `index` is the guilty level in the ORIGINAL numbering.
1370
+ * `side-off` is not an error: an empty set means the side is disabled.
1371
+ */
1372
+ export type LevelReason =
1373
+ | "levels-not-array"
1374
+ | "levels-bad-orientation"
1375
+ | "levels-bad-level"
1376
+ | "levels-not-integer"
1377
+ | "levels-not-positive"
1378
+ | "levels-not-best-first"
1379
+ | "levels-bad-role"
1380
+ | "levels-fee-beyond-budget"
1381
+ | "levels-exceed-side"
1382
+ | "levels-below-minimum"
1383
+ | "side-off";
1384
+
1385
+ /** The verdict on a level set: the side off, a valid set with its volume and full cost, or a named refusal. */
1386
+ export type LevelVerdict =
1387
+ | { readonly ok: true; readonly enabled: false; readonly count: 0; readonly minAtomic: 0n; readonly totalAtomic: 0n }
1388
+ | { readonly ok: true; readonly enabled: true; readonly count: number; readonly minAtomic: bigint; readonly totalAtomic: bigint }
1389
+ | { readonly ok: false; readonly reason: LevelReason; readonly index?: number };
1390
+
1391
+ /** A walk answer: how much XMR and how much asset, or a named refusal. */
1392
+ export type LevelWalkResult =
1393
+ | { readonly ok: true; readonly role: LevelRole; readonly xmrAtomic: bigint; readonly assetWei: bigint }
1394
+ | { readonly ok: false; readonly reason: LevelReason; readonly role: LevelRole; readonly index?: number };
1395
+
1396
+ /** XMR atomic units per 1 whole XMR. */
1397
+ export declare const XMR_ATOMIC_PER_ONE: bigint;
1398
+
1399
+ /** Parse an integer into BigInt. Fractional / NaN / garbage -> null (the caller names the refusal, it does not guess). */
1400
+ export declare function toBigInt(value: bigint | number | string): bigint | null;
1401
+
1402
+ /** Check a set: the side off, the set valid, or a NAMED reason. Equal prices are allowed; a worse price is not. */
1403
+ export declare function checkLevels(levels: readonly Level[], orientation?: LevelOrientation): LevelVerdict;
1404
+
1405
+ /**
1406
+ * DIRECTION 1 - "I give/take ASSET": how much XMR the asset buys or sells. The fee is ADDED for `buy` and
1407
+ * SUBTRACTED for `sell`.
1408
+ */
1409
+ export declare function xmrForAsset(request: { readonly levels: readonly Level[]; readonly role: LevelRole;
1410
+ readonly feeWei?: bigint | string; readonly assetWei: bigint | string; readonly orientation?: LevelOrientation }): LevelWalkResult;
1411
+
1412
+ /**
1413
+ * DIRECTION 2 - "I give/want XMR": how much asset the XMR costs. The fee is ADDED for `buy` and SUBTRACTED for
1414
+ * `sell`.
1415
+ */
1416
+ export declare function assetForXmr(request: { readonly levels: readonly Level[]; readonly role: LevelRole;
1417
+ readonly feeWei?: bigint | string; readonly xmrAtomic: bigint | string; readonly orientation?: LevelOrientation }): LevelWalkResult;