@ninsei-labs/sdk 0.42.1 → 0.43.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/CHANGELOG.md +51 -0
- package/README.md +18 -2
- package/engine/core/swap.js +10 -2
- package/index.d.ts +119 -5
- package/package.json +5 -1
- 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/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,54 @@
|
|
|
1
|
+
## 0.43.1 — 2026-10-12
|
|
2
|
+
|
|
3
|
+
THE DECLINE, THE REFUND AND THE MAKER'S RECLAIM EACH CARRY THEIR OWN STATE, AND THE TERMINAL CHECK READS THE
|
|
4
|
+
DECLARATIONS RATHER THAN ONLY THE MAP. A maker's decline is a terminal state of its own, a refund reports
|
|
5
|
+
`refunded_eth` instead of the `closed` fallback, the maker's reclaimed XMR is the phase code `xmr_reclaimed`, and
|
|
6
|
+
`xmr_returned` - a terminal no code path could ever produce - is gone from the public union and from the table.
|
|
7
|
+
Five comments that claimed this package keeps a second, byte-identical copy of the levels walk and of the EIP-712
|
|
8
|
+
spec are corrected: it keeps no such copy.
|
|
9
|
+
|
|
10
|
+
- A MAKER'S DECLINE IS A TERMINAL STATE OF ITS OWN (`declined`). The escrow's `Declined` event and the node's
|
|
11
|
+
`declined` + `compliance` status are two independent sources of the same state and either one is enough; the
|
|
12
|
+
wait for XMR stops at once, the state is written down, and it carries no reason, category or provider at any
|
|
13
|
+
depth. It is told apart from an ordinary refund, which keeps `refunded_eth`.
|
|
14
|
+
- A REFUND REPORTS ITS OWN STATE, NOT THE FALLBACK. The terminal map in src/swaps.mjs was keyed by names no code
|
|
15
|
+
path writes (`refunded` for a refund the engine writes as `kind: "refund_eth"`), so every refund missed its key
|
|
16
|
+
and told the app `closed` about an order whose ETH was already back in the wallet. The map is now keyed by the
|
|
17
|
+
kinds the engine and this package really write - `success`, `refund_eth`, `declined` - exhaustively, and a kind
|
|
18
|
+
nobody knows still comes out as the declared fallback `closed` (`closed` is a fallback, not a key).
|
|
19
|
+
- THE MAKER'S RECLAIMED XMR IS A PHASE CODE OF ITS OWN: `state.phase === "xmr_reclaimed"` (engine/core/swap.js
|
|
20
|
+
`derive`). It was visible only as a label - the shared `refund_available`, which the maker who never locked sets
|
|
21
|
+
too - so an interface had to compare `state.phaseLabel` to tell the two apart. The code is what it branches on
|
|
22
|
+
now; the maker who never locked keeps `refund_available`.
|
|
23
|
+
- `xmr_returned` IS REMOVED from the public `TerminalCode` union (index.d.ts), from the engine's `TERMINAL` table
|
|
24
|
+
and from the code table in src/codes.mjs, with the reason named where each stood. No code path anywhere wrote
|
|
25
|
+
it, and a declared terminal nothing can produce reads as coverage.
|
|
26
|
+
- BREAKING FOR A TYPESCRIPT CONSUMER, ON PURPOSE: a consumer that branched on `xmr_returned` no longer compiles,
|
|
27
|
+
because the member is gone from the union. That is the intent, not a regression - no settlement with that kind
|
|
28
|
+
exists, so no value could ever reach that branch.
|
|
29
|
+
- WHAT A CONSUMER SEES INSTEAD: a swap whose XMR the maker has taken back carries the phase code
|
|
30
|
+
`xmr_reclaimed` with `terminal` still `null` (and not settled) - the depositor's ETH is still in the escrow, so
|
|
31
|
+
the swap has NOT ended; it ends later as `refunded_eth` when the ETH goes back to the wallet.
|
|
32
|
+
- THE TERMINAL CHECK READS THE DECLARATIONS, NOT ONLY THE MAP: `node tools/check.mjs --only=terminal` compared the
|
|
33
|
+
map against the writers, which is exactly how a dead entry outside the map survived. It now reads the engine's
|
|
34
|
+
own `TERMINAL` table and the public `TerminalCode` union as well and asserts BOTH directions against the writers
|
|
35
|
+
- every kind a writer produces has an entry, and every declared terminal is one a writer can produce (or the
|
|
36
|
+
fallback an unknown kind comes out as). A declared-but-unwritable terminal is RED naming the declaration it came
|
|
37
|
+
from. The check also drives the reclaim to its own phase and holds `terminal: null` there.
|
|
38
|
+
- `tools/check-terminal-teeth.mjs` exercises it with EIGHT breaking runs: the dead key put back, the refund's code
|
|
39
|
+
sent to another declared code, a key nothing writes, the decline's own key dropped, a rename in the engine's
|
|
40
|
+
mirror, a terminal with no writer in the engine's table, the same in the public union, and the reclaim's phase
|
|
41
|
+
renamed - each RED, naming its own reason, every file restored byte for byte with sha256 compared.
|
|
42
|
+
- COMMENT-ONLY CORRECTIONS IN FIVE FILES (index.d.ts, src/index.mjs, src/levelsWalkSpec.mjs, src/quoteEip712.mjs,
|
|
43
|
+
src/quoteEip712Spec.mjs): their headers claimed this package exists as two byte-identical copies of the levels
|
|
44
|
+
walk and of the EIP-712 spec, pinned by sha256. Neither is true - only the package has the implementation, a
|
|
45
|
+
node runs it from the entry and is bound to it by the guard on its imports, and the behaviour is pinned by the
|
|
46
|
+
vectors. No assertion, no line of logic, no signature and no export name changed.
|
|
47
|
+
|
|
48
|
+
Checks: `node tools/check.mjs` - all 21 green; `node tools/check-terminal-teeth.mjs` (8 breaking runs),
|
|
49
|
+
`node tools/check-decline-teeth.mjs` (5), `node tools/check-types-teeth.mjs` (4), `node tools/check-refusals-teeth.mjs`
|
|
50
|
+
(5), `node tools/npm-tag.test.mjs` (28 cases) and `node tools/sweep-refusal.test.mjs` (11 checks) are green too.
|
|
51
|
+
|
|
1
52
|
## 0.42.1 — 2026-10-12
|
|
2
53
|
|
|
3
54
|
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
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninsei-labs/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"check:types-teeth": "node tools/check-types-teeth.mjs",
|
|
23
23
|
"check:refusals": "node tools/check.mjs --only=refusals",
|
|
24
24
|
"check:refusals-teeth": "node tools/check-refusals-teeth.mjs",
|
|
25
|
+
"check:decline": "node tools/check.mjs --only=decline",
|
|
26
|
+
"check:decline-teeth": "node tools/check-decline-teeth.mjs",
|
|
27
|
+
"check:terminal": "node tools/check.mjs --only=terminal",
|
|
28
|
+
"check:terminal-teeth": "node tools/check-terminal-teeth.mjs",
|
|
25
29
|
"check:sweep": "node tools/sweep-refusal.test.mjs", "sync:version": "node tools/sync-version.mjs"
|
|
26
30
|
},
|
|
27
31
|
"types": "./index.d.ts",
|
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
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -85,6 +85,12 @@ const normalizeOptions = (options) => {
|
|
|
85
85
|
// THE CODE-READ SEAM (issue #103): ({address}) -> hex code, for the extcodehash check of the factory. Same
|
|
86
86
|
// rule as evmCall - a read brought from outside, not a substitution of the chain.
|
|
87
87
|
if (o.evmCode !== undefined && typeof o.evmCode !== "function") fail("bad-input", { field: "evmCode" });
|
|
88
|
+
// THE LOG-READ SEAM (issue #40): ({ address, topics, fromBlock, toBlock }) -> logs. Needed for the maker's
|
|
89
|
+
// decline, which is the escrow's `Declined` EVENT - the escrow has no getter that names it, and `status()` reads
|
|
90
|
+
// a decline and an ordinary refund the same way. By default the logs are read through the page's wallet, the
|
|
91
|
+
// same provider the chain read uses. Same rule as evmCall: a READ of the escrow's own event, not a node
|
|
92
|
+
// substitution.
|
|
93
|
+
if (o.evmLogs !== undefined && typeof o.evmLogs !== "function") fail("bad-input", { field: "evmLogs" });
|
|
88
94
|
// THE INTERFACE'S ALLOW-LIST (POLICY, NOT PROOF): an array of provider addresses (or providerIds) OR a
|
|
89
95
|
// predicate. It is NOT the check itself - that is `providerOf` on chain, taken from the quote - but the
|
|
90
96
|
// interface's own decision about whom it trades with. A wrong shape is a typo, not a silent no-op.
|
|
@@ -149,6 +155,7 @@ const normalizeOptions = (options) => {
|
|
|
149
155
|
addNodes: o.addNodes === undefined ? null : o.addNodes,
|
|
150
156
|
evmCall: o.evmCall || null,
|
|
151
157
|
evmCode: o.evmCode || null,
|
|
158
|
+
evmLogs: o.evmLogs || null,
|
|
152
159
|
allowedProviders: o.allowedProviders === undefined ? null : o.allowedProviders,
|
|
153
160
|
knownFactoryCodes: o.knownFactoryCodes === undefined ? null : o.knownFactoryCodes,
|
|
154
161
|
knownImplementationCodes: o.knownImplementationCodes === undefined ? null : o.knownImplementationCodes,
|
|
@@ -216,6 +223,10 @@ export function createNinsei(options) {
|
|
|
216
223
|
// THE SAME CHAIN-READ SEAM FOR THE ORDER READS: state and deadlines must be read by the wallet that will sign,
|
|
217
224
|
// not by a second provider. Without a caller reader the module keeps its engine default.
|
|
218
225
|
const orderDeps = typeof o.evmCall === "function" ? { call: o.evmCall } : {};
|
|
226
|
+
// THE LOG SEAM GOES WITH THE ORDER READS: the decline is read by the same caller-brought reader as the state,
|
|
227
|
+
// for the same reason (the wallet that reads must be the wallet that signs), and it is the READER that is
|
|
228
|
+
// substituted - the addresses and the topic stay the core's.
|
|
229
|
+
const logDeps = typeof o.evmLogs === "function" ? { logs: o.evmLogs } : {};
|
|
219
230
|
|
|
220
231
|
return Object.freeze({
|
|
221
232
|
config,
|
|
@@ -238,6 +249,11 @@ export function createNinsei(options) {
|
|
|
238
249
|
// THE ORDER DEADLINES (t1, readyBy, termsHash) from the chain: the screen needs them to say whether the
|
|
239
250
|
// person is in the dead zone.
|
|
240
251
|
deadlines: (escrow) => orderFlow.orderDeadlines(escrow, orderDeps),
|
|
252
|
+
// THE MAKER'S DECLINE, READ FROM THE ESCROW (issue #40): the `Declined` event, which is the on-chain fact
|
|
253
|
+
// that tells a decline apart from an ordinary refund. The answer carries the state and the node's own token
|
|
254
|
+
// and NOTHING ELSE - no reason, no category, no provider. `checked: false` means the chain was not read,
|
|
255
|
+
// which is NOT the same as "the escrow did not decline".
|
|
256
|
+
decline: (escrow, deps = {}) => orderFlow.orderDecline(escrow, { ...logDeps, ...(deps || {}) }),
|
|
241
257
|
},
|
|
242
258
|
sim,
|
|
243
259
|
quotes: {
|
|
@@ -260,6 +276,9 @@ export function createNinsei(options) {
|
|
|
260
276
|
// would not know where to knock, and without the pass there is nothing to present to the service (and that
|
|
261
277
|
// is a refusal with a CODE, not an invented path).
|
|
262
278
|
swaps: createSwaps({ preflight, http, mode: o.mode, xmrNetwork: o.xmrNetwork, call: o.evmCall,
|
|
279
|
+
// THE LOG SEAM FOR THE DECLINE CHECK: the watch asks the escrow for its `Declined` event on every tick, so
|
|
280
|
+
// a declined order stops the wait at once instead of running to `xmr-timeout`.
|
|
281
|
+
logs: o.evmLogs || null,
|
|
263
282
|
config, auth: o.serverAuth, denylist, watch: o.watch,
|
|
264
283
|
// THE RECORD IS BUILT HERE ITSELF, and one brought from outside is only the caller's preference.
|
|
265
284
|
createRecord: o.createRecord }),
|
|
@@ -274,7 +293,7 @@ export { SDK_VERSION } from "./version.mjs";
|
|
|
274
293
|
|
|
275
294
|
// THE LEVEL WALK IS ON THE PACKAGE SURFACE. A maker publishes a level set (chunks of volume with their own prices)
|
|
276
295
|
// and both the node and the client compute a firm price by WALKING it - the client must reach the SAME price before
|
|
277
|
-
// signing. `levelsWalkSpec.mjs` is the ONE implementation,
|
|
278
|
-
// exported so a consumer can run the same walk: the set in, the answer out. Answers are values
|
|
279
|
-
// token on refusal, never exceptions.
|
|
296
|
+
// signing. `levelsWalkSpec.mjs` is the ONE implementation, which the node takes from the package entry rather than
|
|
297
|
+
// copying, and it is exported so a consumer can run the same walk: the set in, the answer out. Answers are values
|
|
298
|
+
// with a `reason` token on refusal, never exceptions.
|
|
280
299
|
export { checkLevels, xmrForAsset, assetForXmr, toBigInt, XMR_ATOMIC_PER_ONE } from "./levelsWalkSpec.mjs";
|
package/src/levelsWalkSpec.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// THE MAKER'S LEVEL SET: THE REFERENCE WALK OVER IT (issue #110, part B).
|
|
2
2
|
//
|
|
3
3
|
// WHY. The maker sends the node a LEVEL SET - chunks of volume with their own prices - and the node computes a
|
|
4
|
-
// firm price by WALKING it. The client (SDK) needs the same walk to show the same price before signing. This
|
|
5
|
-
// the SINGLE implementation of the walk and of the level-set rules
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
4
|
+
// firm price by WALKING it. The client (SDK) needs the same walk to show the same price before signing. This file
|
|
5
|
+
// is the SINGLE implementation of the walk and of the level-set rules, and BOTH sides run it from here: the node
|
|
6
|
+
// imports the package entry, so it keeps no copy of its own. A second copy would let the node and the client name
|
|
7
|
+
// a DIFFERENT price for the same set.
|
|
8
|
+
// WHAT HOLDS IT: the node is bound to the package by the guard on its imports (no copy may come back under
|
|
9
|
+
// rfq/lib), and the behaviour of this one implementation is pinned by the vectors in tools/check-levels-walk.mjs,
|
|
10
|
+
// computed independently of this code - not by comparing two files.
|
|
11
11
|
//
|
|
12
12
|
// NUMBERS ARE INTEGERS ONLY, BigInt ONLY (rule 10). XMR is in atomic units (1 XMR = 1e12). A price is in the
|
|
13
13
|
// asset's atomic units PER 1 WHOLE XMR (for a wei asset that is wei per 1 XMR). No Number for money.
|
package/src/quoteEip712.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// EIP-712 ORDER-QUOTE SIGNATURE - THE SDK SIDE (VERIFICATION ONLY).
|
|
2
2
|
//
|
|
3
3
|
// The SDK verifies, it does not sign: the maker's wallet key never reaches the customer's machine. The spec
|
|
4
|
-
// (types, domain, digest, recovery) lives in quoteEip712Spec.mjs
|
|
5
|
-
//
|
|
4
|
+
// (types, domain, digest, recovery) lives in quoteEip712Spec.mjs - the ONE embodiment: the node takes it from the
|
|
5
|
+
// package entry, so there is no second copy (tools/check-quote-eip712.mjs). Here it is only bound to SDK primitives.
|
|
6
6
|
//
|
|
7
7
|
// The rule the old flag carried is kept: a quote that cannot be verified is REFUSED. A missing field, a
|
|
8
8
|
// missing signature or an unusable domain is not "probably fine" - the caller names it with a code.
|
package/src/quoteEip712Spec.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// EIP-712 TYPED DATA FOR AN ORDER QUOTE - THE SPEC, SHARED BY THE NODE AND THE SDK.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// WHO HOLDS IT. The node (rfq/) runs on the provider's machine and does not read our files; it takes this spec
|
|
4
|
+
// from the package entry (@ninsei-labs/sdk/order), so there is ONE embodiment here and no copy on the node's side
|
|
5
|
+
// (that wiring is pinned by tools/check-quote-eip712.mjs). A divergence here would mean one side signs what the
|
|
6
|
+
// other does not verify - the failure this format exists to prevent.
|
|
7
7
|
//
|
|
8
8
|
// WHAT IS BOUND, AND WHY EACH FIELD IS INSIDE THE DIGEST (changing any of them breaks recovery):
|
|
9
9
|
// provider, quoteKey - whose quote it is and which key signed it. For a solo maker the two are equal; a
|
package/src/swap-flow.mjs
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
// is the RULES, not the crypto.
|
|
29
29
|
import { SdkError } from "./errors.mjs";
|
|
30
30
|
import * as engine from "./engine.mjs";
|
|
31
|
+
// THE MAKER'S DECLINE (issue #40) - the escrow's `Declined` event and the neutral vocabulary the state is
|
|
32
|
+
// reported in. It is its own module because TWO surfaces ask the same question (this flow and the swaps watch)
|
|
33
|
+
// and a second implementation of the same event would be a second truth about it.
|
|
34
|
+
import { declinedFromLogs, logReaderOf, blockBoundaryOf, DECLINED_TOPIC0 } from "./decline.mjs";
|
|
31
35
|
|
|
32
36
|
// THE ENGINE SURFACES, TAKEN THROUGH THE ONE SEAM (./engine.mjs) - exactly as the rest of the core does.
|
|
33
37
|
const { chainById, DEFAULT_CHAIN, MONERO } = engine.config;
|
|
@@ -354,6 +358,30 @@ export async function orderStatus(escrow, deps = {}) {
|
|
|
354
358
|
return decodeStatus(raw);
|
|
355
359
|
}
|
|
356
360
|
|
|
361
|
+
/**
|
|
362
|
+
* THE ESCROW'S `Declined` EVENT - THE MAKER'S OWN CLOSING OF A FUNDED ORDER (issue #40).
|
|
363
|
+
*
|
|
364
|
+
* WHY THE EVENT AND NOT `status()`. Read on its own, `status()` cannot tell a decline from an ordinary refund:
|
|
365
|
+
* both set `refunded` and send the whole balance back to the locker. The escrow keeps NO getter that names a
|
|
366
|
+
* decline (there is no `declined()` view), so the event is the on-chain fact that does - `decline()` emits
|
|
367
|
+
* `Declined(claimer)` and a refund emits `HalfRevealed(half, false)` instead.
|
|
368
|
+
*
|
|
369
|
+
* THE SEAM IS THE READER, AS EVERYWHERE ELSE: `deps.logs` is `({ address, topics, fromBlock, toBlock }) -> logs`
|
|
370
|
+
* and by default the chain is read through the page's wallet (`readDeclinedLogs`), so the wallet that reads is
|
|
371
|
+
* the wallet that would sign. `deps.fromBlock` is the caller's: a caller that knows the escrow's birth height
|
|
372
|
+
* scans from there instead of walking the whole chain.
|
|
373
|
+
*
|
|
374
|
+
* An answer that is not a list of logs is a REFUSAL (`bad-input`), never "no decline": an unread chain must not
|
|
375
|
+
* be reported as an escrow that declined nothing.
|
|
376
|
+
*/
|
|
377
|
+
export async function orderDecline(escrow, deps = {}) {
|
|
378
|
+
if (typeof escrow !== "string" || !escrow) throw new SdkError("bad-input", { field: "escrow" });
|
|
379
|
+
const logs = logReaderOf(deps.logs);
|
|
380
|
+
const fromBlock = deps.fromBlock === undefined ? "earliest" : blockBoundaryOf(deps.fromBlock);
|
|
381
|
+
const answer = await logs({ address: escrow, topics: [DECLINED_TOPIC0], fromBlock, toBlock: "latest" });
|
|
382
|
+
return declinedFromLogs(answer, { escrow, at: Number.isFinite(Number(deps.at)) ? Number(deps.at) : null });
|
|
383
|
+
}
|
|
384
|
+
|
|
357
385
|
// THE ORDER DEADLINE IS ONE - t1. Before it the claimer takes the ETH by its half and only with the ready mark
|
|
358
386
|
// (:215); from t1 claiming is forbidden (:216), and anyone may refund by PRESENTING THE DEPOSITOR'S HALF
|
|
359
387
|
// (:232-235) - the watchtower does not have it, and that limitation is named in doc 24 section 1. The deadline
|
package/src/swaps.mjs
CHANGED
|
@@ -25,9 +25,44 @@ import { primitives } from "./primitives.mjs";
|
|
|
25
25
|
import { createActions } from "./actions.mjs";
|
|
26
26
|
// THE XMR ARRIVAL CHECK ON TWO NODES (issue #85) - IN THE CORE, from the same mirrored rule and block parser.
|
|
27
27
|
import * as xmrArrival from "./xmr-arrival.mjs";
|
|
28
|
+
// THE MAKER'S DECLINE (issue #40): the escrow's `Declined` event and the node's `declined` + `compliance` status
|
|
29
|
+
// are TWO independent sources of the same state, and either one is enough. The detectors and the neutral
|
|
30
|
+
// vocabulary live in one module, so this file names the state instead of re-reading the two.
|
|
31
|
+
import { declineOf, logReaderOf, blockBoundaryOf, DECLINED_TOPIC0, DECLINED_TERMINAL, DECLINED_CODE } from "./decline.mjs";
|
|
28
32
|
|
|
29
|
-
//
|
|
30
|
-
|
|
33
|
+
// THE ENGINE'S SETTLEMENT KINDS -> THE CONTRACT'S CODES: THE KEY IS THE KIND THE ENGINE REALLY WRITES.
|
|
34
|
+
//
|
|
35
|
+
// The map used to be keyed by names no code path writes - `refunded` for a refund (the engine writes
|
|
36
|
+
// `kind: "refund_eth"`), and `xmr_returned` and `closed`, for which no settlement with that kind is written
|
|
37
|
+
// anywhere in the engine or in this package. A REFUND therefore never met its key and fell into the fallback:
|
|
38
|
+
// get/list/progress told the app `closed` about an order whose ETH was already back in the wallet - the very
|
|
39
|
+
// distinction that became load-bearing once the maker's decline was made a state of its own (issue #40).
|
|
40
|
+
//
|
|
41
|
+
// WHAT CAN BE A KIND, EXHAUSTIVELY. The record's settlement belongs to the engine (engine/core/swap.js, the only
|
|
42
|
+
// module that assigns swap.settlement), and `declined` is the one kind written by us (src/decline.mjs):
|
|
43
|
+
// `success` - the swap completed and the XMR is on the depositor's address (sweepNow, the auto-sweep in the
|
|
44
|
+
// ticker, and the backend-reported sweep on the live path);
|
|
45
|
+
// `refund_eth` - the ETH went back to the depositor. TWO WRITERS, ONE KIND: the person's own refund (refundEth)
|
|
46
|
+
// and the watchtower inside the ticker both write `kind: "refund_eth"` with the phase
|
|
47
|
+
// `refunded_eth`, so there is exactly ONE refund-shaped kind here and nothing is collapsed;
|
|
48
|
+
// `declined` - the maker refused a funded order. Not the engine's: the escrow reads `refunded` either way, so
|
|
49
|
+
// the engine cannot tell a decline from a refund, and the SDK writes this kind itself when it SEES
|
|
50
|
+
// the decline (src/decline.mjs). From then on every surface that reads the record reports it
|
|
51
|
+
// without asking the chain again.
|
|
52
|
+
//
|
|
53
|
+
// AND WHAT IS NOT A KEY: an unknown kind is named, not invented - it comes out as "closed", the fallback in
|
|
54
|
+
// toState below, which is why `closed` is a code here and not a key (no settlement carries that kind; the string
|
|
55
|
+
// `closed` in the engine is the XMR SESSION's status vocabulary, not a settlement). The maker taking the XMR back
|
|
56
|
+
// without a ready mark is NOT a kind either: it does not end a swap - the depositor's ETH is still in the escrow -
|
|
57
|
+
// so it is a PHASE of a live swap (`xmr_reclaimed`, engine/core/swap.js, told apart by that code from the maker who
|
|
58
|
+
// never locked), and the swap ends later as `refunded_eth`. `xmr_returned`, which stood in the engine's terminal
|
|
59
|
+
// table and in the `TerminalCode` type as a state nothing could produce, is gone with it. Every one of these facts
|
|
60
|
+
// is asserted by the `terminal` check rather than left to a reader.
|
|
61
|
+
const TERMINAL = {
|
|
62
|
+
success: "success",
|
|
63
|
+
refund_eth: "refunded_eth",
|
|
64
|
+
[DECLINED_TERMINAL]: DECLINED_TERMINAL,
|
|
65
|
+
};
|
|
31
66
|
|
|
32
67
|
const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; };
|
|
33
68
|
|
|
@@ -159,6 +194,10 @@ export function toState(swap) {
|
|
|
159
194
|
});
|
|
160
195
|
}
|
|
161
196
|
const active = steps.find((s) => s.active) || [...steps].reverse().find((s) => s.done) || steps[0];
|
|
197
|
+
// THE TERMINAL STATE IS TRANSLATED ONCE, AND THE DECLINE IS READ OFF IT. A declined order is NOT a refunded
|
|
198
|
+
// one, and the difference is drawn here and in the action flags below, not left to the screen.
|
|
199
|
+
const terminal = d.terminal ? (TERMINAL[d.terminal.kind] ?? "closed") : null;
|
|
200
|
+
const declined = terminal === DECLINED_TERMINAL;
|
|
162
201
|
const chain = swap.network || swap.chain || null;
|
|
163
202
|
// The virtual machine is taken from the network registry. An unknown network has none, and "unknown" is more honest
|
|
164
203
|
// than the network's name: it is visible in the state and does not turn into "probably EVM".
|
|
@@ -214,9 +253,16 @@ export function toState(swap) {
|
|
|
214
253
|
status: d.phase || null,
|
|
215
254
|
txs: (swap.escrow && (swap.escrow.txids || swap.escrow.txs)) || [],
|
|
216
255
|
},
|
|
217
|
-
|
|
256
|
+
// A DECLINED ORDER OFFERS NOTHING, AND THAT IS THE POINT OF THE STATE (issue #40). The ETH - the fee
|
|
257
|
+
// included - is already back in the depositor's wallet and no half was revealed, so there is no ready mark to
|
|
258
|
+
// place, no XMR to sweep and NO REFUND TO SIGN: the flags are OFF rather than left to whatever the engine's
|
|
259
|
+
// `can` says about an order whose escrow merely reads `refunded`. The app needs no second source for "there
|
|
260
|
+
// is nothing to do": this state plus the terminal code says it.
|
|
261
|
+
actions: declined
|
|
262
|
+
? { markReady: false, sweep: false, refund: false }
|
|
263
|
+
: { markReady: !!(d.can && d.can.ready), sweep: !!(d.can && d.can.sweep), refund: !!(d.can && d.can.refund) },
|
|
218
264
|
simulated: { evm: !!(d.simulated && d.simulated.evm), xmr: !!(d.simulated && d.simulated.xmr) },
|
|
219
|
-
terminal
|
|
265
|
+
terminal,
|
|
220
266
|
};
|
|
221
267
|
}
|
|
222
268
|
|
|
@@ -268,11 +314,18 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
268
314
|
// The check on the ACTUAL sender is done in start() below, ONE place, right before the send - the wallet adapter
|
|
269
315
|
// only HANDS OUT the current account (`address()`), it does not carry the policy.
|
|
270
316
|
denylist = null,
|
|
271
|
-
watch: watchOptions = null
|
|
317
|
+
watch: watchOptions = null,
|
|
318
|
+
// THE CHAIN LOG READER FOR THE DECLINE CHECK (issue #40). A seam, as the chain read is: by default the logs are
|
|
319
|
+
// read through the page's wallet (`readDeclinedLogs`), and from outside a caller brings its own
|
|
320
|
+
// ({ address, topics, fromBlock, toBlock }) -> logs. It is a READ of the escrow's own event, not a node
|
|
321
|
+
// substitution - the address is the escrow of the swap being watched.
|
|
322
|
+
logs: chainLogs = null } = {}) {
|
|
272
323
|
// WHAT READS THE CHAIN AT THE LOCK IS A SEAM, as in actions.mjs: by default the engine's read, from outside you can
|
|
273
324
|
// substitute your own. Otherwise the lock step could not be checked beyond predicting the address - and "the address
|
|
274
325
|
// from the receipt" would remain an argument on trust, not a verified property.
|
|
275
326
|
const reader = typeof call === "function" ? call : engine.evm.readContract;
|
|
327
|
+
// THE SAME RULE FOR THE LOGS: one reader per facade, chosen once.
|
|
328
|
+
const logReader = logReaderOf(chainLogs);
|
|
276
329
|
// MARKING READY GOES THROUGH THE SAME READ SEAM as the lock: checking the slots and signing must go one way,
|
|
277
330
|
// otherwise part of the check goes past the wallet and part does not (see actions.mjs).
|
|
278
331
|
const actions = createActions({ call: typeof call === "function" ? call : null });
|
|
@@ -880,10 +933,82 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
880
933
|
if (readyBy) xmrArrival.assertTimeForConfirmations({ readyBySec: readyBy, nowSec: Math.floor(Date.now() / 1000), minConfirmations, blockTimeSec: limitOf("xmrBlockTimeSec", 120) });
|
|
881
934
|
}
|
|
882
935
|
const started = Date.now();
|
|
936
|
+
// --- THE MAKER'S DECLINE: THE WAIT IS NOT STARTED AT ALL (issue #40). -------------------------------------
|
|
937
|
+
// A decline leaves the escrow `refunded`, so nothing in the state says "do not wait any longer": without
|
|
938
|
+
// this, the wait runs to its timeout and the app is told the generic `xmr-timeout` about an order that is
|
|
939
|
+
// CLOSED and whose ETH is already back - or, worse, the app offers the recovery file and a manual refund
|
|
940
|
+
// for money nobody can recover.
|
|
941
|
+
//
|
|
942
|
+
// THE DECLINE IS ASKED WITH TWO INDEPENDENT SOURCES AND EITHER ONE ENDS THE WAIT:
|
|
943
|
+
// * THE CHAIN - the escrow's own `Declined` event, through the same log reader the rest of the core uses;
|
|
944
|
+
// * THE WIRE - the status the service answers with (`declined` with the code `compliance`).
|
|
945
|
+
// A CHAIN READ THAT FAILS is neither a decline nor a refusal of the swap: the wait goes on and the wire may
|
|
946
|
+
// still answer. What the chain read did is carried out with the result (`chain.checked` / `chain.error`), so
|
|
947
|
+
// a broken reader is visible instead of silent.
|
|
948
|
+
// A STORAGE THAT CANNOT BE READ IS NOT A REASON TO REFUSE THE WAIT: the record is what makes the state
|
|
949
|
+
// durable, and the escrow may also arrive in the request. The watch is not a new user of the store - and it
|
|
950
|
+
// must not start failing where it did not before.
|
|
951
|
+
let record = null;
|
|
952
|
+
try { record = engine.swap.getSwap(id); } catch { record = null; }
|
|
953
|
+
const escrowAddress = request.escrow || (record && record.escrow && record.escrow.address) || null;
|
|
954
|
+
// FROM THE ESCROW'S BIRTH HEIGHT when the record knows it: the event cannot be older than the funding, and
|
|
955
|
+
// walking the whole chain every tick is a cost with no answer behind it.
|
|
956
|
+
const fromBlock = blockBoundaryOf(record && record.escrow ? record.escrow.birthHeight : null);
|
|
957
|
+
const chainDecline = { checked: false, error: null, logs: undefined };
|
|
958
|
+
const readDeclineLogs = async () => {
|
|
959
|
+
chainDecline.checked = false; chainDecline.error = null; chainDecline.logs = undefined;
|
|
960
|
+
// NO ESCROW KNOWN, NO CHAIN ANSWER: named, so `checked: false` is never a silent "nothing there".
|
|
961
|
+
if (!escrowAddress) { chainDecline.error = "no-escrow"; return; }
|
|
962
|
+
try {
|
|
963
|
+
chainDecline.logs = await logReader({ address: escrowAddress, topics: [DECLINED_TOPIC0], fromBlock, toBlock: "latest" });
|
|
964
|
+
chainDecline.checked = true;
|
|
965
|
+
} catch (error) {
|
|
966
|
+
chainDecline.error = (error && error.code) || "logs-unread";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
// WRITTEN, NAMED, NOT INFERRED - the rule the record's destination follows too. The decline becomes the
|
|
970
|
+
// record's OWN settlement once, so later reads of the record report the same state without asking the chain
|
|
971
|
+
// again, and the state survives a page reload.
|
|
972
|
+
const settleDecline = (swap, at) => {
|
|
973
|
+
if (!swap || swap.settlement) return;
|
|
974
|
+
swap.settlement = { kind: DECLINED_TERMINAL, phase: DECLINED_TERMINAL, at };
|
|
975
|
+
try { engine.swap.saveSwap(swap); } catch { throw new SdkError("storage-unavailable", { step: "decline" }); }
|
|
976
|
+
};
|
|
977
|
+
// THE STATE THE WATCH REPORTS FOR A DECLINE: no arrival, the state's own code, the node's own token and
|
|
978
|
+
// WHERE it was seen ("event", "status", or "record" when it was already written down). NO reason, category
|
|
979
|
+
// or provider travels here - the node names none, and the sentence a person reads is the interface's.
|
|
980
|
+
const declineResult = ({ source, at, waitedMs, address, chainChecked, chainError }) => ({
|
|
981
|
+
id,
|
|
982
|
+
escrow: escrowAddress,
|
|
983
|
+
address: address || null,
|
|
984
|
+
seen: false,
|
|
985
|
+
declined: true,
|
|
986
|
+
terminal: DECLINED_TERMINAL,
|
|
987
|
+
code: DECLINED_CODE,
|
|
988
|
+
source,
|
|
989
|
+
at,
|
|
990
|
+
waitedMs,
|
|
991
|
+
chain: { checked: Boolean(chainChecked), error: chainError || null },
|
|
992
|
+
});
|
|
993
|
+
if (record && record.settlement && record.settlement.kind === DECLINED_TERMINAL) {
|
|
994
|
+
const at = Number(record.settlement.at);
|
|
995
|
+
return declineResult({ source: "record", at: Number.isFinite(at) ? at : Date.now(), waitedMs: 0,
|
|
996
|
+
address: null, chainChecked: false, chainError: null });
|
|
997
|
+
}
|
|
883
998
|
let last = null;
|
|
884
999
|
for (;;) {
|
|
1000
|
+
await readDeclineLogs();
|
|
885
1001
|
const body = await readWatch(id);
|
|
886
1002
|
last = incomingOf(body);
|
|
1003
|
+
// THE DECLINE IS DECIDED BEFORE THE ARRIVAL IS LOOKED AT, and before the "foreign response" refusal below:
|
|
1004
|
+
// a declined swap's answer carries no `receivedXmr` at all, and refusing it as a bad response would name
|
|
1005
|
+
// the wrong thing (the state is known - it is a decline, not a broken answer).
|
|
1006
|
+
const declineNow = declineOf({ logs: chainDecline.logs, status: body, escrow: escrowAddress, at: Date.now() });
|
|
1007
|
+
if (declineNow.declined) {
|
|
1008
|
+
settleDecline(record, declineNow.at);
|
|
1009
|
+
return declineResult({ source: declineNow.source, at: declineNow.at, waitedMs: Date.now() - started,
|
|
1010
|
+
address: body && body.address, chainChecked: chainDecline.checked, chainError: chainDecline.error });
|
|
1011
|
+
}
|
|
887
1012
|
// AN EMPTY OR FOREIGN RESPONSE IS NOT AN ARRIVAL: counting it as incoming would mean declaring money that was
|
|
888
1013
|
// never seen. This is a named refusal, not "let us wait some more".
|
|
889
1014
|
if (!last) throw new SdkError("server-unavailable", { step: "watch", why: "bad-response", id });
|
package/src/version.mjs
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// tools/sync-version.mjs. Do not edit it by hand - set the version in package.json and run `npm run sync:version`.
|
|
7
7
|
// The `version` check in tools/check.mjs refuses a mismatch, and refuses a runtime read of package.json here.
|
|
8
8
|
|
|
9
|
-
export const SDK_VERSION = "0.
|
|
9
|
+
export const SDK_VERSION = "0.43.1";
|