@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/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.3";
|