@ninsei-labs/sdk 0.43.3 → 0.44.1

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.
package/src/codes.mjs CHANGED
@@ -125,6 +125,62 @@ export const ERROR_CODES = [
125
125
  "unknown",
126
126
  ];
127
127
 
128
+ // WHAT EACH CODE MEANS FOR THE INTERFACE: may it offer "try again", and whose problem is it. The page kept two
129
+ // hand-written lists (retryable, and "which of these are ours") next to a code dictionary; a code added to the
130
+ // vocabulary and forgotten there showed the person the wrong advice. Here the table is beside the vocabulary, and a
131
+ // machine checks that they name exactly the same codes.
132
+ //
133
+ // `retryable` answers "would another attempt have a chance"; it is not "was it transient" and it is not permission
134
+ // to retry a payment. `side` names who can act:
135
+ // caller - the person or the caller fixes an input;
136
+ // wallet - the wallet's state or the person's decision in it;
137
+ // chain - the chain, a node or a transaction;
138
+ // provider - a maker or a quote;
139
+ // service - our own service;
140
+ // unknown - the code is `unknown`, and pretending otherwise would be a guess.
141
+ export const ERROR_SIDES = ["caller", "wallet", "chain", "provider", "service", "unknown"];
142
+
143
+ export const ERROR_META = Object.freeze({
144
+ "not-implemented": { retryable: false, side: "caller" },
145
+ "bad-input": { retryable: false, side: "caller" },
146
+ "wallet-not-connected": { retryable: false, side: "wallet" },
147
+ "wallet-rejected": { retryable: false, side: "wallet" },
148
+ "wrong-chain": { retryable: false, side: "wallet" },
149
+ "quote-unavailable": { retryable: true, side: "provider" },
150
+ "quote-refused": { retryable: false, side: "provider" },
151
+ "address-denied": { retryable: false, side: "caller" },
152
+ "quote-stale": { retryable: true, side: "provider" },
153
+ "quote-signature-invalid": { retryable: false, side: "provider" },
154
+ "quote-key-mismatch": { retryable: false, side: "provider" },
155
+ "quote-field-unbound": { retryable: false, side: "provider" },
156
+ "quote-expired": { retryable: true, side: "provider" },
157
+ "quote-registry-unchecked": { retryable: true, side: "chain" },
158
+ "quote-key-unknown": { retryable: false, side: "chain" },
159
+ "quote-provider-mismatch": { retryable: false, side: "chain" },
160
+ "quote-provider-not-allowed": { retryable: false, side: "caller" },
161
+ "quote-factory-unchecked": { retryable: true, side: "chain" },
162
+ "quote-factory-unknown": { retryable: false, side: "chain" },
163
+ "quote-factory-mismatch": { retryable: false, side: "chain" },
164
+ "quote-factory-code-unknown": { retryable: false, side: "chain" },
165
+ "recovery-declined": { retryable: false, side: "wallet" },
166
+ "recovery-failed": { retryable: false, side: "wallet" },
167
+ "storage-unavailable": { retryable: true, side: "service" },
168
+ "server-unavailable": { retryable: true, side: "service" },
169
+ "server-refused": { retryable: false, side: "service" },
170
+ "xmr-timeout": { retryable: true, side: "chain" },
171
+ "xmr-locked": { retryable: true, side: "chain" },
172
+ "xmr-nodes-disagree": { retryable: true, side: "chain" },
173
+ "xmr-no-nodes": { retryable: true, side: "chain" },
174
+ "xmr-underpaid": { retryable: false, side: "chain" },
175
+ "xmr-too-late": { retryable: false, side: "chain" },
176
+ "contract-reverted": { retryable: false, side: "chain" },
177
+ "insufficient-funds": { retryable: false, side: "wallet" },
178
+ "gas-refused-by-maker": { retryable: false, side: "provider" },
179
+ "gas-not-arrived": { retryable: true, side: "chain" },
180
+ "gas-too-little": { retryable: false, side: "provider" },
181
+ "unknown": { retryable: false, side: "unknown" },
182
+ });
183
+
128
184
  // GUARD KIND -> CHECK CODE. Kinds come from the engine (www/js/core/preSignGuards.js), codes are our public
129
185
  // vocabulary. The table is checked by machine: every kind the engine can return must be here, otherwise the
130
186
  // interface gets a code that is not in the contract.
@@ -0,0 +1,46 @@
1
+ // WHERE THE WITHDRAWAL GOES, AND WHETHER THAT IS WHO THE FILE PROMISED.
2
+ //
3
+ // The recovery file names the recipient the swap was made for. The page compared the address the person typed with
4
+ // the file's and showed a warning - a rule about the file, implemented in the interface that happens to read it.
5
+ // The comparison is not presentation: sending XMR to another address is a different payment.
6
+ //
7
+ // THE SHAPE IS CHECKED AGAINST THE FILE'S NETWORK. A stagenet file and a mainnet address is not "close enough": the
8
+ // transfer would go to another chain. The checksum is checked when the library is available; when it is not, the
9
+ // answer says so (`verified: false`) instead of pretending.
10
+ //
11
+ // A FILE WITHOUT A PROMISED ADDRESS IS NOT A FAILURE: `matches` is null, and the caller decides what to say.
12
+ import { checkAddress } from "../engine/monero/address.js";
13
+
14
+ const text = (value) => (typeof value === "string" ? value.trim() : "");
15
+
16
+ /** The address the file promised, if it named one. */
17
+ export function promisedAddress(payload) {
18
+ const receive = payload && payload.receive;
19
+ const named = receive && typeof receive.address === "string" ? receive.address.trim() : "";
20
+ return named || null;
21
+ }
22
+
23
+ /**
24
+ * Compare a destination with the file's promise.
25
+ *
26
+ * `ok` answers "is this a usable address on the file's network"; `matches` answers "is it the promised one" and is
27
+ * null when the file named none. `reason` is the library's own phrase when the shape is wrong.
28
+ */
29
+ export async function checkDestination(payload, dest) {
30
+ const address = text(dest);
31
+ const promised = promisedAddress(payload);
32
+ const network = (payload && payload.moneroWallet && payload.moneroWallet.network) || null;
33
+ if (!address) {
34
+ return { ok: false, dest: address, promised, matches: promised === null ? null : false, network, verified: false, reason: "no address" };
35
+ }
36
+ const shape = await checkAddress(address, network ? { expectNetwork: network } : {});
37
+ return {
38
+ ok: shape.ok === true,
39
+ dest: address,
40
+ promised,
41
+ matches: promised === null ? null : address === promised,
42
+ network,
43
+ verified: shape.verified === true,
44
+ reason: shape.reason || null,
45
+ };
46
+ }
@@ -0,0 +1,80 @@
1
+ // WHAT A TRANSACTION WILL COST, ASKED FROM THE NODE THAT WILL CARRY IT.
2
+ //
3
+ // THE FEE = THE PRICE OF A BYTE × THE SIZE OF THE TRANSACTION. The price belongs to the network, not to the wallet:
4
+ // on Monero it moves with the median block load, so it is not set but ASKED - `get_fee_estimate` returns a price per
5
+ // kilobyte for several priorities. The size is known only after the transaction is assembled (rings, inputs, outputs),
6
+ // so this module answers the PRICE, and `feeForBytes` is the only arithmetic that turns it into a sum.
7
+ //
8
+ // The page used to learn the fee only after signing; a person deciding whether to withdraw should see it before.
9
+ //
10
+ // THE DAEMON ANSWERS IN snake_case AND THE LIBRARY RENAMES IT - both forms are accepted. A field the node did not send
11
+ // is null, never zero: zero is a legal price and would read as a free transaction.
12
+ const DEFAULT_TIMEOUT_MS = 8000;
13
+
14
+ const text = (value) => (typeof value === "string" ? value.trim() : "");
15
+
16
+ /** A per-KB price -> the fee for a transaction of `bytes`, rounded UP: one unit short is still short. */
17
+ export function feeForBytes(feePerKB, bytes) {
18
+ let perKB;
19
+ let size;
20
+ try { perKB = BigInt(feePerKB); size = BigInt(bytes); } catch { throw new RangeError("feeForBytes: whole numbers only"); }
21
+ if (perKB < 0n || size < 0n) throw new RangeError("feeForBytes: negative input");
22
+ return ((perKB * size + 1023n) / 1024n).toString();
23
+ }
24
+
25
+ const atomicField = (source, ...names) => {
26
+ for (const name of names) {
27
+ const value = source[name];
28
+ if (value === undefined || value === null || value === "") continue;
29
+ try { return BigInt(value).toString(); } catch { continue; }
30
+ }
31
+ return null;
32
+ };
33
+
34
+ /**
35
+ * ASK A NODE WHAT A TRANSACTION WILL COST.
36
+ *
37
+ * The answer is a price per kilobyte (and the median, and the daemon's own priority list). The exact sum needs the
38
+ * assembled transaction; `feeForBytes` is the arithmetic that gets there. Never throws - a refusal names the problem.
39
+ */
40
+ export async function moneroFeeEstimate({ url, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
41
+ // TRAILING SLASHES WITHOUT A REGEX: a pattern anchored on a run of slashes is flagged as a slow expression on
42
+ // hostile input, and this value can be typed by a person.
43
+ let base = text(url);
44
+ while (base.endsWith("/")) base = base.slice(0, -1);
45
+ if (!base) return { ok: false, why: "no-url" };
46
+ if (typeof fetchImpl !== "function") return { ok: false, why: "no-fetch" };
47
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
48
+ const timer = controller ? setTimeout(() => controller.abort(), Math.max(1, Number(timeoutMs) || DEFAULT_TIMEOUT_MS)) : null;
49
+ let response;
50
+ try {
51
+ response = await fetchImpl(base + "/json_rpc", {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify({ jsonrpc: "2.0", id: "0", method: "get_fee_estimate" }),
55
+ signal: controller ? controller.signal : undefined,
56
+ });
57
+ } catch (error) {
58
+ const aborted = error && (error.name === "AbortError" || error.code === "ABORT_ERR");
59
+ return { ok: false, why: aborted ? "timeout" : "unreachable" };
60
+ } finally {
61
+ if (timer) clearTimeout(timer);
62
+ }
63
+ if (!response || !response.ok) return { ok: false, why: "http", status: response ? response.status : null };
64
+ let body = null;
65
+ try { body = await response.json(); } catch { return { ok: false, why: "not-json" }; }
66
+ if (!body || body.error) return { ok: false, why: "rpc", rpc: body && body.error ? body.error.code ?? null : null };
67
+ const result = body.result || {};
68
+ const feePerKB = atomicField(result, "fee_per_kb", "feePerKB");
69
+ const medianFeePerKB = atomicField(result, "median_fee_per_kb", "medianFeePerKB");
70
+ const list = result.fees ?? result.feesPerKB ?? null;
71
+ let perPriority = null;
72
+ if (Array.isArray(list)) {
73
+ perPriority = [];
74
+ for (const value of list) {
75
+ try { perPriority.push(BigInt(value).toString()); } catch { perPriority = null; break; }
76
+ }
77
+ }
78
+ if (feePerKB === null && medianFeePerKB === null && !perPriority) return { ok: false, why: "no-estimate" };
79
+ return { ok: true, url: base, feePerKB, medianFeePerKB, perPriority };
80
+ }
package/src/index.mjs CHANGED
@@ -19,7 +19,7 @@ import { contextOf, verifyCounterparty, jointAddress } from "./order.mjs";
19
19
  import * as orderFlow from "./swap-flow.mjs";
20
20
  import { createSweep } from "./sweep.mjs";
21
21
  import { SdkError, fail } from "./errors.mjs";
22
- import { ERROR_CODES, STEP_CODES, CHECK_CODES } from "./codes.mjs";
22
+ import { ERROR_CODES, ERROR_META, ERROR_SIDES, STEP_CODES, CHECK_CODES } from "./codes.mjs";
23
23
 
24
24
  // A surface declared by the contract but answering with a code: that is more honest than returning an empty
25
25
  // value that looks like data. The stage number is named so the interface understands what to expect
@@ -232,7 +232,7 @@ export function createNinsei(options) {
232
232
  config,
233
233
  wallet,
234
234
  preflight,
235
- recovery: createRecovery(),
235
+ recovery: createRecovery({ http, config }),
236
236
 
237
237
  // THE ORDER: checking the counterparty's side and the joint address. Both checks - BEFORE signing: after it
238
238
  // there is nothing to fix.
@@ -288,7 +288,7 @@ export function createNinsei(options) {
288
288
  });
289
289
  }
290
290
 
291
- export { SdkError, ERROR_CODES, STEP_CODES, CHECK_CODES };
291
+ export { SdkError, ERROR_CODES, ERROR_META, ERROR_SIDES, STEP_CODES, CHECK_CODES };
292
292
  export { SDK_VERSION } from "./version.mjs";
293
293
 
294
294
  // THE LEVEL WALK IS ON THE PACKAGE SURFACE. A maker publishes a level set (chunks of volume with their own prices)
package/src/probes.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // IS THE NODE A WITHDRAWAL CAN USE, AND WHAT DOES IT SAY ABOUT ITSELF.
2
+ //
3
+ // The sweep page ran its own preflight: a request to get_info, a check for the height, a guess about the network.
4
+ // Every interface that withdraws needs the same answer, and a preflight that disagrees with the wallet builder is a
5
+ // page that says "ready" and then fails to open a wallet.
6
+ //
7
+ // ONE REQUEST, ONE ANSWER. `get_info` is what the node can be asked without a wallet: height, nettype, sync state and
8
+ // version. The answer names WHY it is not usable instead of a bare false - "unreachable" and "this node reports an
9
+ // unknown network" are different problems for the person.
10
+ //
11
+ // A TIMEOUT IS A REFUSAL, NOT A HANG: the request is abandoned after `timeoutMs` (8 seconds by default).
12
+ import { networkFromNode } from "../engine/monero/nodeNetwork.js";
13
+
14
+ const DEFAULT_TIMEOUT_MS = 8000;
15
+
16
+ const text = (value) => (typeof value === "string" ? value.trim() : "");
17
+
18
+ /**
19
+ * Ask a Monero node for `get_info`.
20
+ *
21
+ * `fetchImpl` is injected (the browser's `fetch` by default) so a Node consumer and a test can pass their own.
22
+ * The answer never throws: `{ ok: false, why }` names the problem.
23
+ */
24
+ export async function probeMoneroNode({ url, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
25
+ // TRAILING SLASHES WITHOUT A REGEX: a pattern anchored on a run of slashes is flagged as a slow expression on
26
+ // input a person can type, and this address is exactly that.
27
+ let base = text(url);
28
+ while (base.endsWith("/")) base = base.slice(0, -1);
29
+ if (!base) return { ok: false, why: "no-url" };
30
+ if (typeof fetchImpl !== "function") return { ok: false, why: "no-fetch" };
31
+ const started = Date.now();
32
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
33
+ const timer = controller ? setTimeout(() => controller.abort(), Math.max(1, Number(timeoutMs) || DEFAULT_TIMEOUT_MS)) : null;
34
+ let response;
35
+ try {
36
+ response = await fetchImpl(base + "/json_rpc", {
37
+ method: "POST",
38
+ headers: { "content-type": "application/json" },
39
+ body: JSON.stringify({ jsonrpc: "2.0", id: "0", method: "get_info" }),
40
+ signal: controller ? controller.signal : undefined,
41
+ });
42
+ } catch (error) {
43
+ const aborted = error && (error.name === "AbortError" || error.code === "ABORT_ERR");
44
+ return { ok: false, why: aborted ? "timeout" : "unreachable", ms: Date.now() - started };
45
+ } finally {
46
+ if (timer) clearTimeout(timer);
47
+ }
48
+ if (!response || !response.ok) {
49
+ return { ok: false, why: "http", status: response ? response.status : null, ms: Date.now() - started };
50
+ }
51
+ let body = null;
52
+ try { body = await response.json(); } catch { return { ok: false, why: "not-json", ms: Date.now() - started }; }
53
+ if (!body || body.error) {
54
+ return { ok: false, why: "rpc", rpc: body && body.error ? body.error.code ?? null : null, ms: Date.now() - started };
55
+ }
56
+ const info = body.result || {};
57
+ const height = Number(info.height);
58
+ if (!Number.isInteger(height) || height < 0) return { ok: false, why: "no-height", ms: Date.now() - started };
59
+ // THE NETWORK THE NODE NAMES must be one the wallet builder accepts; an unknown word is not "probably stagenet".
60
+ const network = networkFromNode(info);
61
+ if (!network) return { ok: false, why: "nettype", nettype: info.nettype ?? null, ms: Date.now() - started };
62
+ return {
63
+ ok: true,
64
+ url: base,
65
+ height,
66
+ network,
67
+ nettype: info.nettype ?? null,
68
+ synchronized: info.synchronized === true,
69
+ version: info.version ?? null,
70
+ status: info.status ?? null,
71
+ ms: Date.now() - started,
72
+ };
73
+ }
@@ -11,3 +11,46 @@
11
11
  * ("1,234.5" is 1234.5), so what the field shows and what the rest of the code computes cannot diverge.
12
12
  */
13
13
  export declare function parseAmount(raw: string | number | null | undefined): number | null;
14
+
15
+ /**
16
+ * The same field text reduced to something a decimal parser can read: thousands separators removed, a lone comma
17
+ * read as the decimal separator, anything after a second separator dropped. Empty when the text carries no digit.
18
+ */
19
+ export declare function normalizeAmountInput(raw: string | number | null | undefined): string;
20
+
21
+ /**
22
+ * ATOMIC UNITS -> A DECIMAL STRING. One XMR is 10^12 atomic units; the conversion uses BigInt, so a value above
23
+ * 2^53 (about 9007 XMR in piconero) is not rounded. Dust does not vanish: 1 unit is "0.000000000001". `trim`
24
+ * drops trailing zeros in the fraction; the result is always a plain decimal, never grouped.
25
+ *
26
+ * Throws a RangeError when the input is not a whole number of atomic units.
27
+ */
28
+ export declare function formatXmr(atomic: bigint | number | string, options?: { readonly trim?: boolean }): string;
29
+
30
+ /**
31
+ * A DECIMAL AMOUNT TYPED BY A PERSON -> ATOMIC UNITS, or null when the text is not an amount. A comma without a
32
+ * dot is the decimal separator ("0,05"). More than twelve decimals is null: rounding an amount is a wrong payment
33
+ * that looks successful.
34
+ */
35
+ export declare function parseXmr(text: string | number | null | undefined): bigint | null;
36
+
37
+ /** A value below this is in seconds. Separated from the millisecond threshold on purpose: the gap has no scale. */
38
+ export declare const SECONDS_SCALE_MAX: number;
39
+ /** A value from this on is in milliseconds. */
40
+ export declare const MS_SCALE_MIN: number;
41
+ export declare const DEADLINE_SCALE: Readonly<{ seconds: "seconds"; milliseconds: "milliseconds"; unknown: "unknown" }>;
42
+ export type DeadlineScale = "seconds" | "milliseconds" | "unknown";
43
+
44
+ /** THE SCALE OF A VALUE. Empty, zero, a non-number, or a value between the thresholds: unknown - never a guess. */
45
+ export declare function scaleOf(value: unknown): DeadlineScale;
46
+ export declare function isSecondsScale(value: unknown): boolean;
47
+ export declare function isMsScale(value: unknown): boolean;
48
+
49
+ /**
50
+ * THE ONLY CONVERSION from the signed order's seconds to milliseconds. Empty gives null; a value already in
51
+ * milliseconds is refused (a second conversion would multiply the thousand in silence).
52
+ */
53
+ export declare function msFromOrderSeconds(value: unknown, what?: string): number | null;
54
+
55
+ /** A write guard: throws unless the value is in milliseconds. Returns true when it passes. */
56
+ export declare function assertMsScale(value: unknown, what?: string): true;
@@ -3,4 +3,20 @@
3
3
  // One function today. It is a separate entry because it belongs to neither the XMR side nor the EVM side, and a
4
4
  // consumer forced to import it from either would learn a false lesson about where it lives.
5
5
 
6
- export { parseAmount } from "../../engine/core/format.js";
6
+ export { normalizeAmountInput, parseAmount } from "../../engine/core/format.js";
7
+
8
+ // XMR AMOUNTS IN ATOMIC UNITS: 12 decimals, bigint, no rounding. `formatXmr` for display and for a field,
9
+ // `parseXmr` for what a person typed.
10
+ export { formatXmr, parseXmr } from "../../engine/core/xmrAmount.js";
11
+
12
+ // SECONDS AT THE CHAIN, MILLISECONDS EVERYWHERE ELSE: the scale of a deadline, and the one conversion at the boundary.
13
+ export {
14
+ DEADLINE_SCALE,
15
+ SECONDS_SCALE_MAX,
16
+ MS_SCALE_MIN,
17
+ scaleOf,
18
+ isSecondsScale,
19
+ isMsScale,
20
+ msFromOrderSeconds,
21
+ assertMsScale,
22
+ } from "../../engine/core/deadlineScale.js";
@@ -0,0 +1,50 @@
1
+ // DECLARATIONS FOR THE `./networks` ENTRY.
2
+ //
3
+ // src/public/networks.mjs only NAMES what engine/core/config.js already exports. The declarations cover the fields
4
+ // a consumer of the registry reads; the engine's objects carry more (explorers, price feeds, gas hints) and a
5
+ // consumer is free to read them, but nothing here invents a field the engine does not have.
6
+
7
+ /** A token the network accepts. `address` is a 20-byte hex string; `decimals` is what its own contract reports. */
8
+ export interface ChainToken {
9
+ symbol: string;
10
+ address: string;
11
+ decimals: number;
12
+ hidden?: boolean;
13
+ }
14
+
15
+ /** The DEX deployment on a network, when there is one. */
16
+ export interface ChainUniswap {
17
+ factory?: string;
18
+ quoter?: string;
19
+ /** WETH: the wrapped form of the native coin, used when a route needs an ERC-20. */
20
+ wrapped?: { address: string; decimals: number };
21
+ [field: string]: unknown;
22
+ }
23
+
24
+ export interface Chain {
25
+ id: string;
26
+ name: string;
27
+ vm: string;
28
+ chainId: number;
29
+ caip2?: string;
30
+ testnet?: boolean;
31
+ native: { symbol: string; decimals: number };
32
+ rpcUrl?: string;
33
+ tokens?: ChainToken[];
34
+ uniswap?: ChainUniswap;
35
+ [field: string]: unknown;
36
+ }
37
+
38
+ /** One entry per async route provider (CoWSwap, UniswapX, ...): what it settles and the networks it serves. */
39
+ export interface AsyncRouteProvider {
40
+ venue?: string;
41
+ shape?: string;
42
+ settles?: string;
43
+ networks?: Record<string, unknown>;
44
+ [field: string]: unknown;
45
+ }
46
+
47
+ export declare const CHAINS: readonly Chain[];
48
+ export declare const DEX_CHAINS: readonly string[];
49
+ export declare const DEFAULT_CHAIN: string;
50
+ export declare const ASYNC_ROUTE_PROVIDERS: Readonly<Record<string, AsyncRouteProvider>>;
@@ -0,0 +1,9 @@
1
+ // THE NETWORK REGISTRY, FOR A CONSUMER THAT IS NOT THE BROWSER.
2
+ //
3
+ // The registry is one structure: each network names its chain id, its native coin, the tokens it accepts and,
4
+ // where it exists, the DEX deployment. A backend that validates a chain id and a token against it must read the
5
+ // same data the page reads, or the two drift apart silently.
6
+ //
7
+ // Names only: the engine owns the values. Nothing here is re-implemented.
8
+
9
+ export { ASYNC_ROUTE_PROVIDERS, CHAINS, DEFAULT_CHAIN, DEX_CHAINS } from "../../engine/core/config.js";
@@ -316,3 +316,180 @@ export declare function sellWindowSec(request?: {
316
316
  readonly soft?: number;
317
317
  readonly hard?: number;
318
318
  }): number;
319
+
320
+ /**
321
+ * WHAT NETWORK A NODE SAYS IT IS ON, from its `get_info` answer. The wallet must be built from this value: a
322
+ * mainnet wallet against a stagenet node looks like a broken withdrawal. An answer that is none of `mainnet`,
323
+ * `stagenet`, `testnet` comes back as null - a default would build a wallet for a chain the node may not serve.
324
+ */
325
+ export declare function networkFromNode(info: unknown): "mainnet" | "stagenet" | "testnet" | null;
326
+
327
+ /** An address read as its network and its public keys. An integrated address's payment id is not returned. */
328
+ export interface AddressKeys {
329
+ readonly network: "mainnet" | "stagenet" | "testnet" | null;
330
+ /** The first payload byte, kept for callers that meet a prefix the table does not know. */
331
+ readonly netByte: number;
332
+ readonly spendPub: string;
333
+ readonly viewPub: string;
334
+ }
335
+
336
+ /** AN ADDRESS -> ITS NETWORK AND PUBLIC KEYS. Throws when the text is not a Monero address. */
337
+ export declare function decodeAddressKeys(address: string): AddressKeys;
338
+
339
+ /** A SCALAR AS LITTLE-ENDIAN HEX, low byte first - the form the daemon expects. */
340
+ export declare function scalarToLeHex(scalar: bigint | number | string, bytes?: number): string;
341
+
342
+ /**
343
+ * WHETHER A VIEW KEY IS THE ONE THE ADDRESS NAMES: true / false, or null when there is no public key to compare.
344
+ * `halves` is what `createHalves` returns, so the comparison uses the same arithmetic as the address build.
345
+ */
346
+ export declare function viewKeyMatchesAddress(
347
+ viewScalar: bigint | number | string,
348
+ expectedViewPubHex: string,
349
+ halves: { publicHalves(scalar: bigint | number | string): { ed: string } },
350
+ ): boolean | null;
351
+
352
+ /** A boundary up to this value is a block height; from it on, a unix time. */
353
+ export declare const UNLOCK_TIME_BLOCK_MAX: number;
354
+
355
+ export interface UnlockState {
356
+ readonly code: "unlocked" | "locked_height" | "locked_time";
357
+ readonly locked: boolean;
358
+ readonly untilHeight: number | null;
359
+ readonly untilTime: number | null;
360
+ }
361
+
362
+ /**
363
+ * WHETHER A COIN IS SPENDABLE. A height that is unknown does not open the coin: without the height there is no
364
+ * fact that would. `nowSec` defaults to the wall clock.
365
+ */
366
+ export declare function unlockStateOf(
367
+ unlockTime: number | bigint | string,
368
+ options?: { height?: number | null; nowSec?: number | null },
369
+ ): UnlockState;
370
+
371
+ /** The names that count as loopback: 127.0.0.1, ::1, localhost - not all of 127.0.0.0/8. */
372
+ export declare const LOOPBACK_HOSTS: ReadonlySet<string>;
373
+ export declare function isLoopbackHost(host: string): boolean;
374
+
375
+ /** An address read as scheme and host, or null when the text is not a URL. */
376
+ export declare function hostOfUrl(url: string): { scheme: string; host: string } | null;
377
+
378
+ export type DaemonCode = "daemon-not-https" | "daemon-address-unreadable" | "daemon-scheme-unknown";
379
+ export declare const DAEMON_CODES: Readonly<{ notHttps: DaemonCode; unreadable: DaemonCode; schemeUnknown: DaemonCode }>;
380
+ export declare const DAEMON_PLAIN_HTTP_NETWORKS: readonly string[];
381
+ export declare const DAEMON_NETWORK_MAINNET: string;
382
+
383
+ /** A setting in one shape: 0/false/no/off switches it off; anything else, a typo included, leaves it on. */
384
+ export declare function plainLoopbackAllowed(raw: unknown): boolean;
385
+
386
+ export interface DaemonAddressJudgement {
387
+ readonly ok: boolean;
388
+ /** False for an empty value: nothing to judge, not a refusal. */
389
+ readonly configured: boolean;
390
+ readonly scheme: string | null;
391
+ readonly host: string | null;
392
+ readonly loopback: boolean;
393
+ readonly plainLoopbackAllowed: boolean;
394
+ /** True when a remote plain-http daemon was accepted because the network is stagenet or testnet. */
395
+ readonly plainHttpAccepted: boolean;
396
+ readonly network: string;
397
+ readonly code: DaemonCode | null;
398
+ readonly reason: string | null;
399
+ }
400
+
401
+ /**
402
+ * MAY THIS DAEMON ADDRESS BE USED. https anywhere; http on loopback; http to a remote host only on stagenet and
403
+ * testnet. An unknown network is judged as mainnet.
404
+ */
405
+ export declare function judgeDaemonAddress(
406
+ raw: string | null | undefined,
407
+ options?: { network?: string; allowPlainLoopback?: boolean },
408
+ ): DaemonAddressJudgement;
409
+
410
+ /** A per-KB price -> the fee for a transaction of `bytes`, rounded UP. Throws on a non-integer or a negative input. */
411
+ export declare function feeForBytes(feePerKB: bigint | number | string, bytes: bigint | number | string): string;
412
+
413
+ export interface FeeEstimateOk {
414
+ readonly ok: true;
415
+ readonly url: string;
416
+ /** The price per kilobyte for the next block, in atomic units, or null when the daemon did not send it. */
417
+ readonly feePerKB: string | null;
418
+ readonly medianFeePerKB: string | null;
419
+ /** The daemon's own priority list, in its order, or null. */
420
+ readonly perPriority: readonly string[] | null;
421
+ }
422
+
423
+ export interface FeeEstimateRefusal {
424
+ readonly ok: false;
425
+ /** no-url | no-fetch | timeout | unreachable | http | not-json | rpc | no-estimate */
426
+ readonly why: string;
427
+ readonly status?: number | null;
428
+ readonly rpc?: number | null;
429
+ }
430
+
431
+ /**
432
+ * ASK A NODE WHAT A TRANSACTION WILL COST (`get_fee_estimate`). The exact sum needs the assembled transaction, so
433
+ * this is a price per kilobyte; `feeForBytes` is the only arithmetic that turns it into a fee. Never throws.
434
+ */
435
+ export declare function moneroFeeEstimate(options: {
436
+ url: string;
437
+ fetchImpl?: typeof fetch;
438
+ timeoutMs?: number;
439
+ }): Promise<FeeEstimateOk | FeeEstimateRefusal>;
440
+
441
+ /** The answer to a preflight: usable, or the named reason it is not. */
442
+ export interface NodeProbeOk {
443
+ readonly ok: true;
444
+ readonly url: string;
445
+ readonly height: number;
446
+ readonly network: "mainnet" | "stagenet" | "testnet";
447
+ readonly nettype: string | null;
448
+ readonly synchronized: boolean;
449
+ readonly version: string | null;
450
+ readonly status: string | null;
451
+ readonly ms: number;
452
+ }
453
+
454
+ export interface NodeProbeRefusal {
455
+ readonly ok: false;
456
+ /** no-url | no-fetch | timeout | unreachable | http | not-json | rpc | no-height | nettype */
457
+ readonly why: string;
458
+ readonly status?: number | null;
459
+ readonly nettype?: string | null;
460
+ readonly rpc?: number | null;
461
+ readonly ms?: number;
462
+ }
463
+
464
+ /**
465
+ * ASK A NODE FOR `get_info`: the height, the network it names, whether it is synchronized. Never throws - the
466
+ * answer names the problem (`unreachable`, `timeout`, `nettype`, ...). `fetchImpl` is injected so a Node consumer
467
+ * or a test can pass its own; `timeoutMs` abandons the request (8 seconds by default).
468
+ */
469
+ export declare function probeMoneroNode(options: {
470
+ url: string;
471
+ fetchImpl?: typeof fetch;
472
+ timeoutMs?: number;
473
+ }): Promise<NodeProbeOk | NodeProbeRefusal>;
474
+
475
+ /** The watch key form that matches the address's view public key, or null when none does. */
476
+ export declare function viewKeyFormForAddress(viewSecret: string, expectedViewPubHex: string): string | null;
477
+
478
+ /** A boundary height derived from a later unlock_time, or null when the time names no height. */
479
+ export declare function laterBoundary(...args: unknown[]): unknown;
480
+
481
+ /** One transaction that carries an output for a watched address. */
482
+ export interface ScannedMatch {
483
+ [field: string]: unknown;
484
+ }
485
+
486
+ /** Walk a block range and return the transactions that pay a watched address. */
487
+ export declare function scanRange(options: {
488
+ daemonUrl: string;
489
+ fromHeight: number;
490
+ toHeight: number;
491
+ watched: unknown;
492
+ log?: (event: unknown) => void;
493
+ concurrency?: number;
494
+ nowSec?: number | null;
495
+ }): Promise<ScannedMatch[]>;
@@ -20,6 +20,9 @@ export {
20
20
  prefixFor,
21
21
  } from "../../engine/monero/address.js";
22
22
 
23
+ // THE BLOCK WALK A WATCHER RUNS: the same scanner the page uses, without a wallet.
24
+ export { laterBoundary, scanRange, viewKeyFormForAddress } from "../../engine/monero/scan.js";
25
+
23
26
  export { createHalves } from "../../engine/atomic/halves.js";
24
27
  export { createDleq2 } from "../../engine/atomic/dleq2.js";
25
28
 
@@ -27,3 +30,37 @@ export { createDleq2 } from "../../engine/atomic/dleq2.js";
27
30
  export { scalarFromMoneroHex, spendKeysFromRevealedHalf } from "../../engine/monero/halvesSweep.js";
28
31
 
29
32
  export { SELL_WINDOW, sellWindowSec } from "../../engine/core/sellWindow.js";
33
+
34
+ // WHAT NETWORK A NODE SAYS IT IS ON: the wallet must be built from that answer, and an unknown answer is null -
35
+ // a default here would build a wallet for a chain the node may not serve.
36
+ export { networkFromNode } from "../../engine/monero/nodeNetwork.js";
37
+
38
+ // READING AN ARRIVAL WITHOUT A WALLET: the address and its public keys, the little-endian scalar form the daemon
39
+ // expects, whether a view key matches the address, and whether the coin is still locked by unlock_time.
40
+ export {
41
+ decodeAddressKeys,
42
+ scalarToLeHex,
43
+ viewKeyMatchesAddress,
44
+ unlockStateOf,
45
+ UNLOCK_TIME_BLOCK_MAX,
46
+ } from "../../engine/monero/arrivalRead.js";
47
+
48
+ // WHERE A MONERO NODE MAY LIVE: https anywhere, http on loopback, http to a remote host only on stagenet/testnet -
49
+ // one policy for the node and for the page that lets an operator type an address.
50
+ export {
51
+ DAEMON_CODES,
52
+ DAEMON_NETWORK_MAINNET,
53
+ DAEMON_PLAIN_HTTP_NETWORKS,
54
+ LOOPBACK_HOSTS,
55
+ hostOfUrl,
56
+ isLoopbackHost,
57
+ judgeDaemonAddress,
58
+ plainLoopbackAllowed,
59
+ } from "../../engine/monero/daemonAddress.js";
60
+
61
+ // WHAT A TRANSACTION WILL COST: the daemon's price per kilobyte, by priority, and the arithmetic that turns it into
62
+ // a fee. The page learned the fee only after signing.
63
+ export { moneroFeeEstimate, feeForBytes } from "../feeEstimate.mjs";
64
+
65
+ // IS THE NODE USABLE, AND WHAT DOES IT SAY ABOUT ITSELF: one get_info request, an answer that names why not.
66
+ export { probeMoneroNode } from "../probes.mjs";