@ninsei-labs/sdk 0.42.1 → 0.43.3
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/CHANGELOG.md +80 -0
- package/README.md +19 -3
- package/engine/core/swap.js +10 -2
- package/index.d.ts +119 -5
- package/package.json +13 -3
- package/src/codes.mjs +7 -1
- package/src/decline.mjs +178 -0
- package/src/index.mjs +22 -3
- package/src/levelsWalkSpec.mjs +7 -7
- package/src/provider/envFile.mjs +106 -0
- package/src/provider/identity.mjs +43 -0
- package/src/provider/sign.mjs +164 -0
- package/src/public/order.d.ts +12 -0
- package/src/public/order.mjs +5 -0
- package/src/public/provider.d.ts +93 -0
- package/src/public/provider.mjs +39 -0
- package/src/quoteEip712.mjs +2 -2
- package/src/quoteEip712Spec.mjs +4 -4
- package/src/swap-flow.mjs +28 -0
- package/src/swaps.mjs +130 -5
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
+
## 0.43.3 — 2026-10-18
|
|
2
|
+
|
|
3
|
+
THE SAME CONTENT AS 0.43.2, PUBLISHED UNDER A NEW NUMBER. The tag-triggered release for 0.43.2 stopped in
|
|
4
|
+
`pnpm/action-setup` before building anything: the workflow named pnpm 12.10.1 while `packageManager` names
|
|
5
|
+
12.11.2, and the action refuses two versions (`ERR_PNPM_BAD_PM_VERSION`). Nothing reached the registry. The
|
|
6
|
+
workflow now reads `packageManager` and pins no version of its own; this release carries that fix and the
|
|
7
|
+
0.43.2 content unchanged.
|
|
8
|
+
|
|
9
|
+
- `./provider` (Node only) and the two bound verification helpers in `./order` are as described in 0.43.2.
|
|
10
|
+
- The display-name draw uses `randomInt`, as fixed in 0.43.2.
|
|
11
|
+
- No package code changed between 0.43.2 and 0.43.3; only the release pipeline did.
|
|
12
|
+
|
|
13
|
+
## 0.43.2 — 2026-10-18
|
|
14
|
+
|
|
15
|
+
THE PROVIDER SIDE AND THE BOUND VERIFICATION HELPERS ARE PUBLIC. Two things the demo and the node used to reach
|
|
16
|
+
through the node's own files are now package surface: `./order` exports `orderQuoteMissingFields` and
|
|
17
|
+
`recoverOrderQuoteKey` (the spec functions bound to the SDK's primitives), and a new Node-only subpath
|
|
18
|
+
`./provider` carries the provider's identity, signing and env-file helpers. A consumer no longer re-derives the
|
|
19
|
+
provider code rule or the public-key path, and the browser entries stay free of `node:fs` and `node:crypto`.
|
|
20
|
+
|
|
21
|
+
- `./order` gains two names: `orderQuoteMissingFields(quote)` and `recoverOrderQuoteKey(quote)`. Both are the
|
|
22
|
+
spec's own functions bound to `primitives.mjs`, so verification has one implementation, not two.
|
|
23
|
+
- `./provider` is a separate entry because it reaches the filesystem and `node:crypto`: the provider code of an
|
|
24
|
+
address, the public key path beside a private key, key generation, payload signing and verification, and the
|
|
25
|
+
settings-file helpers (`findEnvFile`, `readEnvFile`, `applyEnvFile`, `substituteEnvVars`).
|
|
26
|
+
- The checks gained a `provider` case: the provider code is the address's own name and stable, the public key
|
|
27
|
+
path follows the private one, a settings file is found, read and applied, and the bound quote verification
|
|
28
|
+
names what is missing and recovers nothing from an empty quote.
|
|
29
|
+
|
|
30
|
+
## 0.43.1 — 2026-10-12
|
|
31
|
+
|
|
32
|
+
THE DECLINE, THE REFUND AND THE MAKER'S RECLAIM EACH CARRY THEIR OWN STATE, AND THE TERMINAL CHECK READS THE
|
|
33
|
+
DECLARATIONS RATHER THAN ONLY THE MAP. A maker's decline is a terminal state of its own, a refund reports
|
|
34
|
+
`refunded_eth` instead of the `closed` fallback, the maker's reclaimed XMR is the phase code `xmr_reclaimed`, and
|
|
35
|
+
`xmr_returned` - a terminal no code path could ever produce - is gone from the public union and from the table.
|
|
36
|
+
Five comments that claimed this package keeps a second, byte-identical copy of the levels walk and of the EIP-712
|
|
37
|
+
spec are corrected: it keeps no such copy.
|
|
38
|
+
|
|
39
|
+
- A MAKER'S DECLINE IS A TERMINAL STATE OF ITS OWN (`declined`). The escrow's `Declined` event and the node's
|
|
40
|
+
`declined` + `compliance` status are two independent sources of the same state and either one is enough; the
|
|
41
|
+
wait for XMR stops at once, the state is written down, and it carries no reason, category or provider at any
|
|
42
|
+
depth. It is told apart from an ordinary refund, which keeps `refunded_eth`.
|
|
43
|
+
- A REFUND REPORTS ITS OWN STATE, NOT THE FALLBACK. The terminal map in src/swaps.mjs was keyed by names no code
|
|
44
|
+
path writes (`refunded` for a refund the engine writes as `kind: "refund_eth"`), so every refund missed its key
|
|
45
|
+
and told the app `closed` about an order whose ETH was already back in the wallet. The map is now keyed by the
|
|
46
|
+
kinds the engine and this package really write - `success`, `refund_eth`, `declined` - exhaustively, and a kind
|
|
47
|
+
nobody knows still comes out as the declared fallback `closed` (`closed` is a fallback, not a key).
|
|
48
|
+
- THE MAKER'S RECLAIMED XMR IS A PHASE CODE OF ITS OWN: `state.phase === "xmr_reclaimed"` (engine/core/swap.js
|
|
49
|
+
`derive`). It was visible only as a label - the shared `refund_available`, which the maker who never locked sets
|
|
50
|
+
too - so an interface had to compare `state.phaseLabel` to tell the two apart. The code is what it branches on
|
|
51
|
+
now; the maker who never locked keeps `refund_available`.
|
|
52
|
+
- `xmr_returned` IS REMOVED from the public `TerminalCode` union (index.d.ts), from the engine's `TERMINAL` table
|
|
53
|
+
and from the code table in src/codes.mjs, with the reason named where each stood. No code path anywhere wrote
|
|
54
|
+
it, and a declared terminal nothing can produce reads as coverage.
|
|
55
|
+
- BREAKING FOR A TYPESCRIPT CONSUMER, ON PURPOSE: a consumer that branched on `xmr_returned` no longer compiles,
|
|
56
|
+
because the member is gone from the union. That is the intent, not a regression - no settlement with that kind
|
|
57
|
+
exists, so no value could ever reach that branch.
|
|
58
|
+
- WHAT A CONSUMER SEES INSTEAD: a swap whose XMR the maker has taken back carries the phase code
|
|
59
|
+
`xmr_reclaimed` with `terminal` still `null` (and not settled) - the depositor's ETH is still in the escrow, so
|
|
60
|
+
the swap has NOT ended; it ends later as `refunded_eth` when the ETH goes back to the wallet.
|
|
61
|
+
- THE TERMINAL CHECK READS THE DECLARATIONS, NOT ONLY THE MAP: `node tools/check.mjs --only=terminal` compared the
|
|
62
|
+
map against the writers, which is exactly how a dead entry outside the map survived. It now reads the engine's
|
|
63
|
+
own `TERMINAL` table and the public `TerminalCode` union as well and asserts BOTH directions against the writers
|
|
64
|
+
- every kind a writer produces has an entry, and every declared terminal is one a writer can produce (or the
|
|
65
|
+
fallback an unknown kind comes out as). A declared-but-unwritable terminal is RED naming the declaration it came
|
|
66
|
+
from. The check also drives the reclaim to its own phase and holds `terminal: null` there.
|
|
67
|
+
- `tools/check-terminal-teeth.mjs` exercises it with EIGHT breaking runs: the dead key put back, the refund's code
|
|
68
|
+
sent to another declared code, a key nothing writes, the decline's own key dropped, a rename in the engine's
|
|
69
|
+
mirror, a terminal with no writer in the engine's table, the same in the public union, and the reclaim's phase
|
|
70
|
+
renamed - each RED, naming its own reason, every file restored byte for byte with sha256 compared.
|
|
71
|
+
- COMMENT-ONLY CORRECTIONS IN FIVE FILES (index.d.ts, src/index.mjs, src/levelsWalkSpec.mjs, src/quoteEip712.mjs,
|
|
72
|
+
src/quoteEip712Spec.mjs): their headers claimed this package exists as two byte-identical copies of the levels
|
|
73
|
+
walk and of the EIP-712 spec, pinned by sha256. Neither is true - only the package has the implementation, a
|
|
74
|
+
node runs it from the entry and is bound to it by the guard on its imports, and the behaviour is pinned by the
|
|
75
|
+
vectors. No assertion, no line of logic, no signature and no export name changed.
|
|
76
|
+
|
|
77
|
+
Checks: `node tools/check.mjs` - all 21 green; `node tools/check-terminal-teeth.mjs` (8 breaking runs),
|
|
78
|
+
`node tools/check-decline-teeth.mjs` (5), `node tools/check-types-teeth.mjs` (4), `node tools/check-refusals-teeth.mjs`
|
|
79
|
+
(5), `node tools/npm-tag.test.mjs` (28 cases) and `node tools/sweep-refusal.test.mjs` (11 checks) are green too.
|
|
80
|
+
|
|
1
81
|
## 0.42.1 — 2026-10-12
|
|
2
82
|
|
|
3
83
|
THE BID SIDE OF THE QUOTE WALK ROUNDED THE WRONG WAY. The specification fixes a rounding DIRECTION per side, and
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ machine). The wallet arrives as an EIP-1193 provider, storage as an adapter, and
|
|
|
20
20
|
| `quotes.firm` | answers `not-implemented` (stage 2: signing with a key and the order protocol) |
|
|
21
21
|
| `swaps.list` / `swaps.get` | read state from storage (and after a page reload) |
|
|
22
22
|
| `swaps.start` | holds the order: guards → recovery file and the person's confirmation → order → locking funds → **recording with us and watching the Monero address**; returns the named state `step: "xmr-incoming"` |
|
|
23
|
-
| `swaps.watch` | waits for XMR to arrive at the joint address with the same request as the page (`GET /swaps/{id}`): arrived — returns the fact (amount, confirmations, txid); did not wait — a refusal with the code `xmr-timeout`; already on the address but barred by `unlock_time` — a refusal with the code `xmr-locked` (the boundary is in `params.until`), and NO ready mark; with expectations and a wallet, places the ready mark after the arrival |
|
|
23
|
+
| `swaps.watch` | waits for XMR to arrive at the joint address with the same request as the page (`GET /swaps/{id}`): arrived — returns the fact (amount, confirmations, txid); did not wait — a refusal with the code `xmr-timeout`; already on the address but barred by `unlock_time` — a refusal with the code `xmr-locked` (the boundary is in `params.until`), and NO ready mark; the maker DECLINED the order — returns `SwapDeclinedWatch` (`declined: true`, `terminal: "declined"`, the node's own `code`), with every action off; with expectations and a wallet, places the ready mark after the arrival |
|
|
24
24
|
| `wallet.balances` | works: the native coin and tokens, in minimal units |
|
|
25
25
|
|
|
26
26
|
What is not ready names itself with the code `not-implemented` with the fields `surface` and `stage`: an empty
|
|
@@ -32,7 +32,7 @@ value that looks like data is more dangerous than a refusal.
|
|
|
32
32
|
compares the list of what the SDK returns with what is declared in the contract (both ways).
|
|
33
33
|
|
|
34
34
|
EVERY PUBLISHED SUBPATH CARRIES ITS OWN DECLARATIONS: `.` (`index.d.ts`), `./xmr`, `./order`, `./evm`,
|
|
35
|
-
`./core` and `./
|
|
35
|
+
`./core`, `./recovery` and `./provider` (`src/public/<entry>.d.ts`). Each of them declares exactly the symbols its barrel
|
|
36
36
|
re-exports, so a typed consumer sees the same surface the runtime has, and nothing else.
|
|
37
37
|
`tools/check.mjs --only=types` keeps the two sides equal (a subpath without a `types` target, a declarations
|
|
38
38
|
file that does not read as declarations, or a symbol declared on one side only is red), and
|
|
@@ -74,7 +74,23 @@ user-facing string.
|
|
|
74
74
|
| `claimed` | the maker took the ETH |
|
|
75
75
|
| `swept` | the XMR has been sent to the recipient's address |
|
|
76
76
|
|
|
77
|
-
Terminal states (`state.terminal`): `success`, `refunded_eth`, `
|
|
77
|
+
Terminal states (`state.terminal`): `success`, `refunded_eth`, `closed`, `declined`.
|
|
78
|
+
|
|
79
|
+
`xmr_returned` WAS HERE AND IS GONE, AND SO IS THE STATE IT NAMED AS A TERMINAL. When the depositor does not place
|
|
80
|
+
the ready mark in time the maker takes the XMR back — and the swap does NOT end there: the depositor's ETH is still
|
|
81
|
+
in the escrow, so it is a PHASE of a live swap, not a terminal. `state.phase` is then `xmr_reclaimed` (the code an
|
|
82
|
+
interface branches on, while `state.terminal` stays `null`), and the swap ends later as `refunded_eth` when the ETH
|
|
83
|
+
goes back. The code exists because that phase used to be the shared `refund_available`, which the "maker never
|
|
84
|
+
locked the XMR" situation sets too: the two could only be told apart by comparing a label.
|
|
85
|
+
|
|
86
|
+
`declined` — THE MAKER REFUSED A FUNDED ORDER (issue #40). The escrow sets `refunded` and emits
|
|
87
|
+
`Declined(claimer)`, so `status()` alone reads it exactly like any other refund; the SDK tells the two apart by
|
|
88
|
+
that EVENT (the chain, read through `evmLogs`) or by the node's status `declined` with the code `compliance` (the
|
|
89
|
+
wire). Either source stops the wait for XMR at once and ends the swap as `declined` with EVERY action off: the
|
|
90
|
+
whole balance, the fee included, is already back in the wallet and no half was revealed, so there is nothing to
|
|
91
|
+
recover — no recovery file is needed and no manual refund is offered. NO reason, category or provider travels
|
|
92
|
+
with it (the node returns only `compliance`, on purpose), and the neutral sentence a person reads is the
|
|
93
|
+
interface's: this package holds no user-facing string.
|
|
78
94
|
|
|
79
95
|
### Checks (`preflight().checks[].code`)
|
|
80
96
|
|
package/engine/core/swap.js
CHANGED
|
@@ -41,10 +41,13 @@ export const STEP_LABELS = [
|
|
|
41
41
|
["swept", "Sent to your address"],
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
+
// THE STATES A SWAP ENDS IN. `xmr_returned` STOOD HERE AND IS GONE ON PURPOSE: it was declared with a phase and a
|
|
45
|
+
// label and NO code path anywhere wrote it. The maker taking the XMR back does not END a swap - the depositor's ETH
|
|
46
|
+
// is still in the escrow - so that situation is a PHASE of a live swap (`xmr_reclaimed`, in derive below), and a
|
|
47
|
+
// terminal nothing can produce is worse than no terminal at all: a reader takes the declaration for coverage.
|
|
44
48
|
export const TERMINAL = {
|
|
45
49
|
success: { phase: "success", label: "XMR delivered" },
|
|
46
50
|
refunded_eth: { phase: "refunded_eth", label: "ETH refunded" },
|
|
47
|
-
xmr_returned: { phase: "xmr_returned", label: "Swap cancelled, funds returned" },
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export function createSwap(input) {
|
|
@@ -592,7 +595,12 @@ export function derive(swap, now = nowReal()) {
|
|
|
592
595
|
phase = "maker_locking";
|
|
593
596
|
phaseLabel = "Maker is locking XMR";
|
|
594
597
|
} else if (xmrReclaimed) {
|
|
595
|
-
|
|
598
|
+
// THE MAKER TOOK THE XMR BACK - A PHASE OF ITS OWN, NOT A TERMINAL AND NOT A STEP. The depositor's ETH is
|
|
599
|
+
// still in the escrow, so the swap is LIVE (`terminal` stays empty) and ends later as `refunded_eth`; and the
|
|
600
|
+
// ordered stepper has no passage for a deal that is taken back. It used to be the shared `refund_available`,
|
|
601
|
+
// which the "maker never locked" situation sets too, so the two could only be told apart by reading the label -
|
|
602
|
+
// `xmr_reclaimed` is the code an interface branches on.
|
|
603
|
+
phase = "xmr_reclaimed";
|
|
596
604
|
phaseLabel = "XMR returned to the maker";
|
|
597
605
|
refundAfterSim = makerReclaimAt;
|
|
598
606
|
can.refund = true;
|
package/index.d.ts
CHANGED
|
@@ -15,6 +15,25 @@
|
|
|
15
15
|
// Naming language: the values are data. `step` and `code` are stable strings from the tables below; they are
|
|
16
16
|
// translated, shown and coloured as one sees fit. They change only with a package version bump (see CHANGELOG).
|
|
17
17
|
|
|
18
|
+
/** One log as a node answers it: an emitter, its topics and its data. Only what the decline is read from is named. */
|
|
19
|
+
export interface EvmLog {
|
|
20
|
+
readonly address?: string;
|
|
21
|
+
readonly topics?: readonly string[];
|
|
22
|
+
readonly data?: string;
|
|
23
|
+
readonly blockNumber?: string;
|
|
24
|
+
readonly transactionHash?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The log read a caller may bring: the seam the decline is read through. */
|
|
28
|
+
export type EvmLogReader = (
|
|
29
|
+
request: {
|
|
30
|
+
readonly address: string;
|
|
31
|
+
readonly topics?: readonly string[];
|
|
32
|
+
readonly fromBlock?: string | number;
|
|
33
|
+
readonly toBlock?: string | number;
|
|
34
|
+
},
|
|
35
|
+
) => Promise<readonly EvmLog[]>;
|
|
36
|
+
|
|
18
37
|
export interface NinseiOptions {
|
|
19
38
|
/** The service's HTTP API base. Default is "/api" (the interface proxies it to our server). */
|
|
20
39
|
apiBase?: string;
|
|
@@ -80,6 +99,15 @@ export interface NinseiOptions {
|
|
|
80
99
|
* matched against the interface's known builds. Same rule as evmCall: a read, not a node substitution.
|
|
81
100
|
*/
|
|
82
101
|
evmCode?: (request: { readonly address: string }) => Promise<string>;
|
|
102
|
+
/**
|
|
103
|
+
* READING THE ESCROW'S LOGS FROM OUTSIDE (issue #40): `({ address, topics, fromBlock, toBlock }) -> logs`, for
|
|
104
|
+
* the MAKER'S DECLINE. The escrow emits `Declined(claimer)` and keeps NO getter that names it, while `status()`
|
|
105
|
+
* reads a decline and an ordinary refund the same way - so the event is the only on-chain fact that tells them
|
|
106
|
+
* apart. By default the logs are read through the page's wallet, the same provider `evmCall` uses, so the wallet
|
|
107
|
+
* that reads is the wallet that signs. Same rule as `evmCall`: a READ, not a node substitution - the address is
|
|
108
|
+
* the order's escrow and the topic is the package's own.
|
|
109
|
+
*/
|
|
110
|
+
evmLogs?: EvmLogReader;
|
|
83
111
|
/**
|
|
84
112
|
* THE INTERFACE'S OWN ADMISSION LIST - policy, not proof. The chain (maker registry, providerOf, taken from the
|
|
85
113
|
* quote) proves whose key signed; this says whom THIS interface trades with at all. An array of provider
|
|
@@ -709,8 +737,12 @@ export interface SwapsApi {
|
|
|
709
737
|
* IF `expect` AND `wallet` ARE PASSED, then after the arrival the ready mark (actions.markReady) is called with
|
|
710
738
|
* THESE expectations: the slot check against the chain stands in it before the signature. Without them the step
|
|
711
739
|
* names the next one (`next: "mark-ready"`) and signs nothing.
|
|
740
|
+
* THE MAKER DECLINED - THE WAIT STOPS AT ONCE (issue #40): the escrow's `Declined` event and the service's
|
|
741
|
+
* `declined` + `compliance` status are BOTH asked on every tick, and either one ends the wait with
|
|
742
|
+
* `SwapDeclinedWatch` instead of running to `xmr-timeout` about an order whose ETH is already back. Nothing is
|
|
743
|
+
* signed, and the decline is written into the record, so every later read of the swap reports the same state.
|
|
712
744
|
*/
|
|
713
|
-
watch(request: SwapWatchRequest): Promise<SwapWatch>;
|
|
745
|
+
watch(request: SwapWatchRequest): Promise<SwapWatch | SwapDeclinedWatch>;
|
|
714
746
|
/**
|
|
715
747
|
* THE XMR ARRIVAL CHECK WITHOUT THE WATCH LOOP (issue #85). The client reads the chain itself with its own view
|
|
716
748
|
* key and `ready` opens only when at least two nodes of DIFFERENT operators see the transaction IN THE SAME BLOCK
|
|
@@ -985,6 +1017,33 @@ export interface SwapWatchRequest {
|
|
|
985
1017
|
readonly readyBySec?: number;
|
|
986
1018
|
}
|
|
987
1019
|
|
|
1020
|
+
/**
|
|
1021
|
+
* THE WATCH'S ANSWER WHEN THE MAKER DECLINED (issue #40). There is no arrival to wait for, so `seen` is false and
|
|
1022
|
+
* there is no `marked`, no `next` and no `txids` - and NO field naming a reason, a category or a provider, because
|
|
1023
|
+
* the node names none. `code` is the node's own single token, unchanged; `terminal` is the state the interface
|
|
1024
|
+
* draws its own neutral sentence from; `source` says WHICH reader saw it (provenance, not a reason).
|
|
1025
|
+
*/
|
|
1026
|
+
export interface SwapDeclinedWatch {
|
|
1027
|
+
readonly id: string;
|
|
1028
|
+
/** The escrow the decline was read on, or null when the record did not know it. */
|
|
1029
|
+
readonly escrow: string | null;
|
|
1030
|
+
readonly address: string | null;
|
|
1031
|
+
/** Never true here: a decline is not an arrival. */
|
|
1032
|
+
readonly seen: false;
|
|
1033
|
+
readonly declined: true;
|
|
1034
|
+
readonly terminal: "declined";
|
|
1035
|
+
readonly code: "compliance";
|
|
1036
|
+
/** `event` - the escrow's `Declined` log; `status` - the service's status; `record` - already written down. */
|
|
1037
|
+
readonly source: "event" | "status" | "record";
|
|
1038
|
+
readonly at: number;
|
|
1039
|
+
readonly waitedMs: number;
|
|
1040
|
+
/**
|
|
1041
|
+
* WHAT THE CHAIN READ DID. `checked: false` means the escrow's event could not be read at all (no reader, an
|
|
1042
|
+
* unreachable node) - which is NOT "the escrow declined nothing" - and `error` carries the refusal's own code.
|
|
1043
|
+
*/
|
|
1044
|
+
readonly chain: { readonly checked: boolean; readonly error: string | null };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
988
1047
|
/** THE OUTCOME OF THE WATCH: the arrival is seen as a FACT, not assumed. */
|
|
989
1048
|
export interface SwapWatch {
|
|
990
1049
|
readonly seen: true;
|
|
@@ -1105,7 +1164,12 @@ export interface SwapState {
|
|
|
1105
1164
|
readonly simulated: { readonly evm: boolean; readonly xmr: boolean };
|
|
1106
1165
|
/** The refusal reason, if any: a code and params, no text. */
|
|
1107
1166
|
readonly error?: { readonly code: ErrorCode; readonly params?: Readonly<Record<string, unknown>> };
|
|
1108
|
-
/**
|
|
1167
|
+
/**
|
|
1168
|
+
* The terminal state, if the swap has ended. `declined` (issue #40) is the maker's refusal of a funded order:
|
|
1169
|
+
* the ETH, the fee included, is already back in the wallet, every flag in `actions` is OFF, and there is nothing
|
|
1170
|
+
* to recover. The interface owes the person a NEUTRAL sentence and no reason, category or provider - the node
|
|
1171
|
+
* names none, and the package forwards none.
|
|
1172
|
+
*/
|
|
1109
1173
|
readonly terminal: TerminalCode | null;
|
|
1110
1174
|
/**
|
|
1111
1175
|
* WHEN THE RECORD WAS CREATED (the record's own `createdAt`, an ISO string). null - an old record without
|
|
@@ -1114,7 +1178,12 @@ export interface SwapState {
|
|
|
1114
1178
|
readonly createdAt: string | null;
|
|
1115
1179
|
/** WHO GAVE THE PRICE. Either field may be absent: an old record or a swap without a provider. */
|
|
1116
1180
|
readonly maker: { readonly id: string | null; readonly name: string | null };
|
|
1117
|
-
/**
|
|
1181
|
+
/**
|
|
1182
|
+
* The engine's phase code and its human name: the list shows the name, the CODE is what an interface branches on.
|
|
1183
|
+
* `xmr_reclaimed` is the maker taking the XMR back before the ready mark - a LIVE state, not a terminal: the
|
|
1184
|
+
* depositor's ETH is still in the escrow, `terminal` stays null and the swap ends later as `refunded_eth`. It is
|
|
1185
|
+
* told apart by that code from the other situation that opens a refund, the maker who never locked the XMR.
|
|
1186
|
+
*/
|
|
1118
1187
|
readonly phase: string | null;
|
|
1119
1188
|
readonly phaseLabel: string | null;
|
|
1120
1189
|
/** The swap has a settlement record: the list splits live from settled by THIS flag, not by the phase's text. */
|
|
@@ -1141,7 +1210,17 @@ export type StepCode =
|
|
|
1141
1210
|
| "claimed"
|
|
1142
1211
|
| "swept";
|
|
1143
1212
|
|
|
1144
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* THE ORDER'S TERMINAL STATE. `declined` is THE MAKER'S REFUSAL OF A FUNDED ORDER (issue #40): the whole balance,
|
|
1215
|
+
* the fee included, is already back in the depositor's wallet and NO half was revealed, so there is nothing to
|
|
1216
|
+
* recover and nothing left to do. It is deliberately NOT `refunded_eth`: that one is the depositor taking the ETH
|
|
1217
|
+
* back, and it DOES reveal the depositor's half. The words a person reads are the interface's, not the package's.
|
|
1218
|
+
*
|
|
1219
|
+
* `xmr_returned` WAS A MEMBER AND IS GONE: it named the maker taking the XMR back, which does NOT end a swap - the
|
|
1220
|
+
* depositor's ETH is still in the escrow - so it is a PHASE of a live swap (`SwapState.phase`, the code
|
|
1221
|
+
* `xmr_reclaimed`), never a terminal. A member with no writer reads as coverage.
|
|
1222
|
+
*/
|
|
1223
|
+
export type TerminalCode = "success" | "refunded_eth" | "closed" | "declined";
|
|
1145
1224
|
|
|
1146
1225
|
// --- The XMR withdrawal (the sweep page) --------------------------------------------------------
|
|
1147
1226
|
|
|
@@ -1300,6 +1379,41 @@ export interface OrderApi {
|
|
|
1300
1379
|
* terms hash the recovery file is bound to. The screen uses them to say whether the person is in the dead zone.
|
|
1301
1380
|
*/
|
|
1302
1381
|
deadlines(escrow: string): Promise<OrderDeadlines>;
|
|
1382
|
+
/**
|
|
1383
|
+
* THE MAKER'S DECLINE, READ FROM THE ESCROW (issue #40): the `Declined` event THE ESCROW EMITTED. It is a
|
|
1384
|
+
* separate read because `status()` cannot answer it - the escrow has no `declined()` getter, and a decline sets
|
|
1385
|
+
* the same `refunded` flag an ordinary refund does. Read through the same seam as the state (`evmLogs`, or the
|
|
1386
|
+
* `logs` argument), so the wallet that reads is the wallet that signs.
|
|
1387
|
+
*
|
|
1388
|
+
* THE ANSWER CARRIES THE STATE AND NOTHING ELSE: no reason, no category, no provider. `checked: false` says the
|
|
1389
|
+
* chain was NOT read, which is not the same as "the escrow did not decline".
|
|
1390
|
+
*/
|
|
1391
|
+
decline(escrow: string, deps?: OrderDeclineDeps): Promise<OrderDecline>;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* WHAT IS KNOWN ABOUT A DECLINE: exactly these fields, and the SHORTNESS IS THE POINT. Anything else would be a
|
|
1396
|
+
* reason, a category or a provider, and the node names none of those on purpose.
|
|
1397
|
+
*/
|
|
1398
|
+
export interface OrderDecline {
|
|
1399
|
+
readonly declined: boolean;
|
|
1400
|
+
/** The state the interface draws from (`declined`), or null when this is not a decline. */
|
|
1401
|
+
readonly terminal: "declined" | null;
|
|
1402
|
+
/** The node's own single token, UNCHANGED (`compliance`), or null. It is a token, not a reason. */
|
|
1403
|
+
readonly code: "compliance" | null;
|
|
1404
|
+
/** `event` - the escrow's log was read; `status` - the node's status was read; null - neither. */
|
|
1405
|
+
readonly source: "event" | "status" | null;
|
|
1406
|
+
/** When it was seen (epoch ms), or null. */
|
|
1407
|
+
readonly at: number | null;
|
|
1408
|
+
/** Whether the reading happened at all: `false` is "not measured", NOT "no decline". */
|
|
1409
|
+
readonly checked: boolean;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** The seams of the decline read: the log reader and where the scan starts. */
|
|
1413
|
+
export interface OrderDeclineDeps {
|
|
1414
|
+
readonly logs?: EvmLogReader;
|
|
1415
|
+
/** The first block to scan (the escrow's birth height when it is known). Default: the whole chain. */
|
|
1416
|
+
readonly fromBlock?: string | number;
|
|
1303
1417
|
}
|
|
1304
1418
|
|
|
1305
1419
|
/** THE ORDER STATE, READ FROM THE CHAIN (escrow status()): three flags and the remainder. */
|
|
@@ -1462,7 +1576,7 @@ export declare function createNinsei(options: NinseiOptions): Ninsei;
|
|
|
1462
1576
|
/**
|
|
1463
1577
|
* THE LEVEL WALK. A maker publishes a LEVEL SET - chunks of volume with their own prices - and both the node and
|
|
1464
1578
|
* the client compute a firm price by WALKING it, before anything is signed. `levelsWalkSpec.mjs` is the ONE
|
|
1465
|
-
* implementation,
|
|
1579
|
+
* implementation, taken from the package entry by the node rather than copied, and it is exported so a
|
|
1466
1580
|
* consumer reaches the same price: the maker's set in, the walk's answer out.
|
|
1467
1581
|
*
|
|
1468
1582
|
* NUMBERS ARE INTEGERS, BigInt ONLY. XMR is in atomic units (1 XMR = 1e12); a price is in the asset's atomic units
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninsei-labs/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
7
7
|
},
|
|
8
|
-
"packageManager": "pnpm@12.
|
|
8
|
+
"packageManager": "pnpm@12.11.2",
|
|
9
9
|
"description": "XMR <-> EVM swap engine: quotes, guards, order steps, recovery (no DOM, no user-facing strings)",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"type": "module",
|
|
@@ -17,12 +17,18 @@
|
|
|
17
17
|
"check:surface": "node tools/check.mjs --only=surface",
|
|
18
18
|
"check:mirror": "node tools/check.mjs --only=mirror",
|
|
19
19
|
"check:pack": "node tools/check.mjs --only=pack",
|
|
20
|
+
"check:pack-teeth": "node tools/check-pack-teeth.mjs",
|
|
20
21
|
"check:secrets": "node tools/check.mjs --only=secrets",
|
|
21
22
|
"check:types": "node tools/check.mjs --only=types",
|
|
22
23
|
"check:types-teeth": "node tools/check-types-teeth.mjs",
|
|
23
24
|
"check:refusals": "node tools/check.mjs --only=refusals",
|
|
24
25
|
"check:refusals-teeth": "node tools/check-refusals-teeth.mjs",
|
|
25
|
-
"check:
|
|
26
|
+
"check:decline": "node tools/check.mjs --only=decline",
|
|
27
|
+
"check:decline-teeth": "node tools/check-decline-teeth.mjs",
|
|
28
|
+
"check:terminal": "node tools/check.mjs --only=terminal",
|
|
29
|
+
"check:terminal-teeth": "node tools/check-terminal-teeth.mjs",
|
|
30
|
+
"check:sweep": "node tools/sweep-refusal.test.mjs",
|
|
31
|
+
"sync:version": "node tools/sync-version.mjs"
|
|
26
32
|
},
|
|
27
33
|
"types": "./index.d.ts",
|
|
28
34
|
"exports": {
|
|
@@ -53,6 +59,10 @@
|
|
|
53
59
|
"./recovery": {
|
|
54
60
|
"types": "./src/public/recovery.d.ts",
|
|
55
61
|
"default": "./src/public/recovery.mjs"
|
|
62
|
+
},
|
|
63
|
+
"./provider": {
|
|
64
|
+
"types": "./src/public/provider.d.ts",
|
|
65
|
+
"default": "./src/public/provider.mjs"
|
|
56
66
|
}
|
|
57
67
|
},
|
|
58
68
|
"files": [
|
package/src/codes.mjs
CHANGED
|
@@ -13,7 +13,13 @@ export const STEP_CODES = [
|
|
|
13
13
|
"swept",
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// `declined` IS A TERMINAL STATE OF ITS OWN (issue #40): the maker refused a funded order, so the whole balance -
|
|
17
|
+
// the fee included - is already back in the depositor's wallet and NO half was revealed. It is told apart from
|
|
18
|
+
// `refunded_eth`, which is the depositor taking the ETH back and DOES reveal a half (src/decline.mjs).
|
|
19
|
+
// `xmr_returned` WAS DECLARED HERE AND IS GONE (it had no writer anywhere): the maker's reclaim of the XMR does
|
|
20
|
+
// NOT end a swap - the depositor's ETH is still in the escrow - so it is a PHASE of a live swap (`xmr_reclaimed`,
|
|
21
|
+
// engine/core/swap.js), never a terminal state. A declared terminal nothing can produce reads as coverage.
|
|
22
|
+
export const TERMINAL_CODES = ["success", "refunded_eth", "closed", "declined"];
|
|
17
23
|
|
|
18
24
|
export const CHECK_CODES = [
|
|
19
25
|
"balance",
|
package/src/decline.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// THE MAKER'S DECLINE: A STATE OF ITS OWN, TOLD APART FROM AN ORDINARY REFUND.
|
|
2
|
+
//
|
|
3
|
+
// WHAT HAPPENS ON THE CHAIN. A maker who refuses a funded purchase - after screening the deposited ETH, say -
|
|
4
|
+
// calls `decline()` on the escrow. The contract sets `refunded`, emits `Declined(claimer)` and sends the whole
|
|
5
|
+
// balance, the fee included, back to the locker. NO half is revealed: there is nothing to recover, and nobody
|
|
6
|
+
// assembles a Monero key from a declined order.
|
|
7
|
+
//
|
|
8
|
+
// WHY `status()` IS NOT ENOUGH TO SEE IT. `escrow.status()` answers four words - funded, claimed, refunded,
|
|
9
|
+
// balance - and a decline reads exactly like an ordinary refund there: `refunded` is set and the money is back.
|
|
10
|
+
// The two are not the same event, though. An ordinary refund is the depositor (or whoever holds their half)
|
|
11
|
+
// taking the ETH back when the maker did not claim; it REVEALS THE DEPOSITOR'S HALF (`HalfRevealed(half, false)`)
|
|
12
|
+
// so the maker can still take their XMR. A decline is the MAKER closing the order in the depositor's favour and
|
|
13
|
+
// reveals nothing. On screen the two want different things said and different buttons hidden, so the SDK tells
|
|
14
|
+
// them apart instead of reporting "refunded" for both.
|
|
15
|
+
//
|
|
16
|
+
// THE TWO SOURCES, AND EITHER ONE IS ENOUGH:
|
|
17
|
+
// 1) THE ESCROW'S `Declined` EVENT (the chain) - `declinedFromLogs`. The escrow has no getter for a decline
|
|
18
|
+
// (there is no `declined()` view), so the event is the on-chain fact that names it; it is read through the
|
|
19
|
+
// same read seam as every other chain read - an `eth_getLogs` over the escrow's address.
|
|
20
|
+
// 2) THE NODE'S SWAP STATUS `declined` WITH THE CODE `compliance` (the wire) - `declinedFromStatus`. Those ARE
|
|
21
|
+
// the two fields the node returns and nothing else, so BOTH are required: the bare word without its code is
|
|
22
|
+
// not the state the node names, and guessing at it would put a category on a refusal that carries none.
|
|
23
|
+
//
|
|
24
|
+
// WHAT IS DELIBERATELY NOT CARRIED: a reason, a category, a provider. The node returns only `compliance` on
|
|
25
|
+
// purpose - it does not name who declined or why - so the SDK reports the state, the node's own single token and
|
|
26
|
+
// which reader saw it, AND NOTHING ELSE (see DECLINE_FIELDS below). The sentence a person reads is the
|
|
27
|
+
// interface's: this package holds no user-facing string, and a decline is no exception.
|
|
28
|
+
import { SdkError } from "./errors.mjs";
|
|
29
|
+
import { keccak256 } from "./primitives.mjs";
|
|
30
|
+
import * as engine from "./engine.mjs";
|
|
31
|
+
|
|
32
|
+
// THE EVENT, AND ONLY IT. The signature is assembled from its parts and hashed HERE, once, exactly as Solidity
|
|
33
|
+
// does it: keccak256("Declined(address)"). The topic is exported so a caller filters logs with it rather than
|
|
34
|
+
// deriving it a second time - a second copy of the same event would be a second truth about it.
|
|
35
|
+
export const DECLINE_EVENT_NAME = "Declined";
|
|
36
|
+
export const DECLINE_EVENT_SIGNATURE = () => `${DECLINE_EVENT_NAME}(${["address"].join(",")})`;
|
|
37
|
+
|
|
38
|
+
const hexOf = (bytes) => {
|
|
39
|
+
let out = "0x";
|
|
40
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** keccak256("Declined(address)") - the first (and only) topic of a decline's log. */
|
|
45
|
+
export const DECLINED_TOPIC0 = hexOf(keccak256(DECLINE_EVENT_SIGNATURE()));
|
|
46
|
+
|
|
47
|
+
// THE NODE'S OWN TWO FIELDS for a declined swap: `{ status: "declined", code: "compliance" }`. The code is the
|
|
48
|
+
// node's single token, deliberately opaque: it says "a screening decision was taken", not by whom or on what.
|
|
49
|
+
export const DECLINED_STATUS = "declined";
|
|
50
|
+
export const DECLINED_CODE = "compliance";
|
|
51
|
+
|
|
52
|
+
// THE STATE'S OWN CODE - a `TerminalCode` (`index.d.ts`). The interface draws the decline screen from THIS, and
|
|
53
|
+
// phrases the sentence itself; the package contributes the state and the code, not the words.
|
|
54
|
+
export const DECLINED_TERMINAL = "declined";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* THE WHOLE SET OF FIELDS A DECLINE IS REPORTED WITH - and the list is short ON PURPOSE. Everything else would be
|
|
58
|
+
* a reason, a category or a provider, and the node names none of those:
|
|
59
|
+
* `declined` - what happened (true/false);
|
|
60
|
+
* `terminal` - the state the interface draws from (`declined`, or null);
|
|
61
|
+
* `code` - the node's own token, UNCHANGED, or null;
|
|
62
|
+
* `source` - which reader saw it: `event` (the escrow) or `status` (the node). PROVENANCE, NOT A REASON:
|
|
63
|
+
* it says where the SDK learned it, not why the maker refused;
|
|
64
|
+
* `at` - when it was seen (epoch ms), or null;
|
|
65
|
+
* `checked` - whether the chain was read at all. `false` means "not measured", which is NOT "no decline" - a
|
|
66
|
+
* caller that blurs the two would read a maker's refusal as an ordinary refund.
|
|
67
|
+
*/
|
|
68
|
+
export const DECLINE_FIELDS = ["declined", "terminal", "code", "source", "at", "checked"];
|
|
69
|
+
|
|
70
|
+
// Every verdict is built through this, so no field can creep in beside the list above: the reporter is the shape.
|
|
71
|
+
const verdictOf = (fields) => {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const key of DECLINE_FIELDS) out[key] = fields[key] === undefined ? null : fields[key];
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** A DECLINE, AS THE SDK REPORTS IT: the state, the node's token and where it was seen - and nothing more. */
|
|
78
|
+
export const declineSeen = ({ source = null, at = null } = {}) =>
|
|
79
|
+
verdictOf({ declined: true, terminal: DECLINED_TERMINAL, code: DECLINED_CODE, source, at, checked: true });
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* NO DECLINE. `checked: false` says the reading did not happen (no chain reader, no status body): the two are
|
|
83
|
+
* kept apart on purpose, because "we could not look" and "we looked and it was not a decline" are different
|
|
84
|
+
* states of knowledge, and only one of them lets a caller treat a closed escrow as an ordinary refund.
|
|
85
|
+
*/
|
|
86
|
+
export const declineNone = ({ source = null, at = null, checked = true } = {}) =>
|
|
87
|
+
verdictOf({ declined: false, terminal: null, code: null, source, at, checked });
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SOURCE 1 - THE ESCROW'S `Declined` EVENT. `logs` is what the log read answered: an ARRAY of log objects.
|
|
91
|
+
* Anything that is not an array is a refusal (`bad-input`), never "no decline": an unread chain must not be
|
|
92
|
+
* reported as an escrow that declined nothing.
|
|
93
|
+
*
|
|
94
|
+
* Only the FIRST TOPIC is compared (the event's own signature). A log from another address is skipped when the
|
|
95
|
+
* answer carries one and the caller named the escrow; a log without an address is still matched, because the
|
|
96
|
+
* request that produced it was filtered by address and dropping it would hide a real decline.
|
|
97
|
+
*/
|
|
98
|
+
export function declinedFromLogs(logs, { escrow = null, at = null } = {}) {
|
|
99
|
+
if (!Array.isArray(logs)) throw new SdkError("bad-input", { field: "logs", why: "not-array" });
|
|
100
|
+
const wanted = typeof escrow === "string" && escrow ? escrow.toLowerCase() : null;
|
|
101
|
+
for (const log of logs) {
|
|
102
|
+
if (!log || typeof log !== "object") continue;
|
|
103
|
+
if (wanted && typeof log.address === "string" && log.address && log.address.toLowerCase() !== wanted) continue;
|
|
104
|
+
const topics = Array.isArray(log.topics) ? log.topics : [];
|
|
105
|
+
if (String(topics[0] || "").toLowerCase() === DECLINED_TOPIC0) return declineSeen({ source: "event", at });
|
|
106
|
+
}
|
|
107
|
+
return declineNone({ source: "event", at });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* SOURCE 2 - THE NODE'S SWAP STATUS. `{ status: "declined", code: "compliance" }`, in either of the two shapes
|
|
112
|
+
* the wire carries it in: at the top level of the answer (the swap state a backend relays) or NESTED UNDER
|
|
113
|
+
* `userStatus` (the field the maker API names it by). BOTH fields must be there and must be exactly these tokens;
|
|
114
|
+
* anything else is not the state the node names, and a partial match is NOT a decline.
|
|
115
|
+
*
|
|
116
|
+
* A body that is not an object at all answers `checked: false` - there was nothing to read, which is not the same
|
|
117
|
+
* as having read it and found no decline.
|
|
118
|
+
*/
|
|
119
|
+
export function declinedFromStatus(body, { at = null } = {}) {
|
|
120
|
+
if (!body || typeof body !== "object") return declineNone({ source: "status", at, checked: false });
|
|
121
|
+
for (const candidate of [body, body.userStatus]) {
|
|
122
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
123
|
+
if (String(candidate.status || "") === DECLINED_STATUS && String(candidate.code || "") === DECLINED_CODE) {
|
|
124
|
+
return declineSeen({ source: "status", at });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return declineNone({ source: "status", at });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* BOTH SOURCES AT ONCE, AND EITHER ONE IS ENOUGH - the form the flow uses. The chain is asked first (the escrow's
|
|
132
|
+
* own event is the stronger fact) and the wire second; a source that was not read is named as such (its own
|
|
133
|
+
* verdict stays, with `source: null` when neither was read), and a body without the node's two fields does not
|
|
134
|
+
* cancel a chain answer.
|
|
135
|
+
*/
|
|
136
|
+
export function declineOf({ logs = undefined, status = undefined, escrow = null, at = null } = {}) {
|
|
137
|
+
const fromChain = Array.isArray(logs) ? declinedFromLogs(logs, { escrow, at }) : null;
|
|
138
|
+
if (fromChain && fromChain.declined) return fromChain;
|
|
139
|
+
const fromWire = status && typeof status === "object" ? declinedFromStatus(status, { at }) : null;
|
|
140
|
+
if (fromWire && fromWire.declined) return fromWire;
|
|
141
|
+
if (fromChain) return fromChain; // the chain was read: it answered "not declined"
|
|
142
|
+
if (fromWire) return fromWire; // only the wire was read, and it was not a decline
|
|
143
|
+
return declineNone({ checked: false, at }); // nothing was read, which is NOT "no decline"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* THE DEFAULT CHAIN LOG READER: `eth_getLogs` THROUGH THE PAGE'S WALLET - the same provider the engine's
|
|
148
|
+
* `readContract` uses, so the wallet that reads is the wallet that will sign (the rule the whole chain-read seam
|
|
149
|
+
* follows). It is a READ, not a node substitution: the address is the caller's escrow and the filter is the
|
|
150
|
+
* decline's own topic.
|
|
151
|
+
*
|
|
152
|
+
* `fromBlock` is the caller's: a watch that knows the escrow's birth height scans from there instead of walking
|
|
153
|
+
* the chain's whole history. An answer that is not an array is a REFUSAL - a node that answered something else
|
|
154
|
+
* did not answer "no decline", and a caller must not read it as one.
|
|
155
|
+
*/
|
|
156
|
+
export async function readDeclinedLogs({ address, topics = [DECLINED_TOPIC0], fromBlock = "earliest", toBlock = "latest" } = {}) {
|
|
157
|
+
if (typeof address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(address)) throw new SdkError("bad-input", { field: "logs.address" });
|
|
158
|
+
let provider = null;
|
|
159
|
+
try { provider = typeof engine.evm.currentProvider === "function" ? engine.evm.currentProvider() : null; } catch { provider = null; }
|
|
160
|
+
if (!provider || typeof provider.request !== "function") throw new SdkError("bad-input", { field: "evmLogs", why: "no-provider" });
|
|
161
|
+
const answer = await provider.request({ method: "eth_getLogs", params: [{ address, topics, fromBlock, toBlock }] });
|
|
162
|
+
if (!Array.isArray(answer)) throw new SdkError("server-unavailable", { step: "logs", why: "bad-response" });
|
|
163
|
+
return answer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The log reader a call was given: the caller's seam, or the default above. */
|
|
167
|
+
export const logReaderOf = (logs) => (typeof logs === "function" ? logs : readDeclinedLogs);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A BLOCK BOUNDARY AS THE NODE WANTS IT (hex quantity), for a caller that knows where its escrow was born.
|
|
171
|
+
* A value that is not a whole, non-negative number is the fallback - the safe end of the chain, not an invented
|
|
172
|
+
* block: scanning too far is slow, scanning too little misses the event.
|
|
173
|
+
*/
|
|
174
|
+
export const blockBoundaryOf = (height, fallback = "earliest") => {
|
|
175
|
+
const value = Number(height);
|
|
176
|
+
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
177
|
+
return "0x" + Math.trunc(value).toString(16);
|
|
178
|
+
};
|