@ninsei-labs/sdk 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
## 0.39.0 — 2026-10-07
|
|
2
|
+
|
|
3
|
+
THE PACKAGE IS LEVEL WITH THE MAIN REPOSITORY AGAIN. The `sdk/src` and `sdk/engine` trees were re-synced
|
|
4
|
+
byte-for-byte from the demo repository's `sdk/`, so the mirrored engine and the core carry today's changes.
|
|
5
|
+
|
|
6
|
+
- **The order-quote signature carries the XMR amount** (`src/quoteEip712Spec.mjs`): the EIP-712 message grew
|
|
7
|
+
`xmrAmount` - the XMR the level walk computes - so the provider now signs exactly the XMR the chain checks at
|
|
8
|
+
order birth. The request-shape label moved `6 -> 8` together with the field set, and the domain name renamed
|
|
9
|
+
`ArrakisEscrowFactory -> NinseiEscrowFactory`. The label and the domain version string move apart on purpose:
|
|
10
|
+
the label tracks the message shape, the domain names the factory address and the chain.
|
|
11
|
+
- **A route provider declares the SHAPE of its swap** (`src/legs/evm.mjs`): `shape: "sync"` runs the swap inside
|
|
12
|
+
the user's own transaction (the router AMMs), `"async"` settles later as a separate action (an intent
|
|
13
|
+
auction). A path that assumes "swap and funding in one transaction" asks for a synchronous provider through the
|
|
14
|
+
new `requireSyncProvider(id)` gate, which refuses an asynchronous one BY NAME (`provider-not-synchronous`,
|
|
15
|
+
`provider-unknown`) instead of assuming it synchronous.
|
|
16
|
+
- **The order flow lost a duplicated set of lock-term keys** (`src/swap-flow.mjs`): `salt`,
|
|
17
|
+
`claimWindowSeconds` and `readyWindowSeconds` were assembled into the terms twice.
|
|
18
|
+
- **The engine mirror follows the contract rename** (`engine/**`): the escrow and factory names in the comments
|
|
19
|
+
and the calldata tables read `NinseiEscrow` / `NinseiEscrowFactory`, matching the deployed contracts.
|
|
20
|
+
- **The level walk is carried over, translated, and ON THE PACKAGE SURFACE** (`src/levelsWalkSpec.mjs`): the
|
|
21
|
+
maker's level set in, the firm price out - the SAME walk the node runs (`rfq/lib/levelsWalkSpec.mjs`). Its
|
|
22
|
+
comments were translated to English, so the language check stays green, and the twin no longer forces a single
|
|
23
|
+
comment language: the guard now compares the CODE with the comments cut out, and what must never diverge is the
|
|
24
|
+
BEHAVIOUR - 32 vectors still run through BOTH copies. Exported from the entry point and declared in
|
|
25
|
+
`index.d.ts`: `checkLevels`, `xmrForAsset`, `assetForXmr`, `toBigInt`, `XMR_ATOMIC_PER_ONE`.
|
|
26
|
+
- **The docs no longer describe a provider without its `shape`:** the README requirement names `shape` (and
|
|
27
|
+
`settled(...)` for `shape: "async"`), and the `routing` doc comment in `index.d.ts` carries the four lines on
|
|
28
|
+
how a swap settles (one transaction or a separate later action).
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
THE PROGRESS SCREEN READS THE CORE. The swap-progress screen of the demo used to read the engine's record and
|
|
32
|
+
derive its state on the page; now the core owns that read and that derivation, and the screen only draws a snapshot.
|
|
33
|
+
|
|
34
|
+
- **`SwapsApi.progress(id)`** - the deal-progress snapshot: the record's own fields (`swap`), the wallet rebuilt from
|
|
35
|
+
the halves (`swapWallet`, the recovery file is assembled from it) and the DERIVED state (`view`: phase,
|
|
36
|
+
`can`, `deadlines`, `steps`, `xmr`/`xmrAmount`, `sellWindow`, `readyDeadline`, `terminal`). `null` - there is no
|
|
37
|
+
such record. New interfaces `SwapProgress` / `SwapProgressView`.
|
|
38
|
+
- **The demonstration record's actions and write are methods, not page code:**
|
|
39
|
+
`SwapsApi.confirmReady(id)`, `SwapsApi.refundEth(id, by?)` and `SwapsApi.sweepNow(id)` (mode `sim`; a live swap goes
|
|
40
|
+
through `actions`), and `SwapsApi.setDestination(id, address)` - writes the record's `receiveAddress`, which is
|
|
41
|
+
part of the recovery file.
|
|
42
|
+
- **`SwapsApi.addressNetwork(address)`** - the Monero network of an address by its shape, from the SAME module the
|
|
43
|
+
Monero wallet adapter uses, so no interface keeps its own prefix table.
|
|
44
|
+
|
|
45
|
+
THE DEAL ENGINE IS A PACKAGE MODULE. The order flow used to live on the page (`www/js/core/swap-flow.js`) and be
|
|
46
|
+
mirrored into the package; the page imported it directly. It is now `sdk/src/swap-flow.mjs` - ONE implementation
|
|
47
|
+
inside the package - and the page reaches it through a single seam (`www/js/sdk/bridge.js`, facade
|
|
48
|
+
`order.status`/`order.deadlines`). The engine file is gone, the mirror no longer carries it, and the fallback
|
|
49
|
+
"call the engine directly if the bundle did not load" is gone with it: there is no second copy to fall back to,
|
|
50
|
+
and a missing bundle is a NAMED refusal.
|
|
51
|
+
|
|
52
|
+
- **`OrderApi.status(escrow)`** - the on-chain order state (escrow `status()`): `isFunded`, `isClaimed`,
|
|
53
|
+
`isRefunded` and the remainder `balance`. Read by a CALL, not derived from logs. New interface `OrderStatus`.
|
|
54
|
+
- **`OrderApi.deadlines(escrow)`** - `t1`, `readyBy` and `termsHash` read from the chain; the screen needs them to
|
|
55
|
+
say whether the order is already in the dead zone. New interface `OrderDeadlines`.
|
|
56
|
+
- **Both reads take the same chain-read seam as the actions** (`deps.call`, bound to the caller's `evmCall` by the
|
|
57
|
+
facade), so the wallet that reads is the wallet that signs; without a caller reader they keep the engine default.
|
|
58
|
+
- **The engine seam changed.** `engine.swapFlow` is removed; the seam now exposes `orderClient` (the half worker),
|
|
59
|
+
`rfqSource` (the order quote under the deal) and `claimGas` (the shared claim gas limit) - the modules the moved
|
|
60
|
+
flow reads through it. `sdk/src/actions.mjs` and `sdk/src/lock.mjs` call the package module (`./swap-flow.mjs`)
|
|
61
|
+
instead of the mirror.
|
|
62
|
+
- **The module keeps the engine's names:** `escrowFactoryAddress`, `escrowCashierAddress`, `runAtomicOrder`,
|
|
63
|
+
`orderStatus`, `orderDeadlines`, `orderLiveSlots`, `assertOrderSlots`, `claimOrder`, `refundOrder`,
|
|
64
|
+
`markReadyOrder`. Refusals are codes (`SdkError`), not text; `onStage`/`onStep` details are tokens.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## 0.38.0 — 2026-10-02
|
|
68
|
+
|
|
69
|
+
XMR that is ON the address but BARRED by the transaction's `unlock_time` is not an arrival, and the ready mark is
|
|
70
|
+
not signed under it (issue #84).
|
|
71
|
+
|
|
72
|
+
- **A third answer with its own code.** `swaps.watch` already named two answers - the arrival (a fact) and the
|
|
73
|
+
expired wait (`xmr-timeout`). Money that is on the address but cannot be spent until a height or a date is a
|
|
74
|
+
THIRD one: the refusal `xmr-locked`, with `params.lockedXmr` and `params.untilHeight`/`untilTime` (numbers and
|
|
75
|
+
their kind, never text). A `xmr_locked` session status is enough - the core does not re-derive arrival from the
|
|
76
|
+
amount.
|
|
77
|
+
- **ONE rule, read from its owner.** "Has the XMR arrived" is decided by the service, which alone reads the
|
|
78
|
+
chain's outputs (`app/indexer.mjs`, `unlockStateOf`: `0` - arrival; below 500 000 000 - a block height; at or
|
|
79
|
+
above - a unix time). The core and the page consume the NAMED state instead of writing a second rule.
|
|
80
|
+
- **The dictionary says until WHEN.** `www/js/sdk/codes.js` carries the phrase for `xmr-locked` on both languages
|
|
81
|
+
and renders `untilTime` as a UTC date.
|
|
82
|
+
- Checks: SDK 231 (was 229), the code dictionary 29, the order flow 26, the guard 257 - all green; the new
|
|
83
|
+
`tools/check-xmr-unlock.mjs` holds the rule, the store, the core and the dictionary, plus a live section that
|
|
84
|
+
feeds a REAL chain transaction (with its `unlock_time` changed in one field) through the indexer's own scan.
|
|
85
|
+
|
|
86
|
+
## 0.37.0 — 2026-10-02
|
|
87
|
+
|
|
88
|
+
The package is SELF-SUFFICIENT: `import "@ninsei-labs/sdk"` works from a directory OUTSIDE the repository — it
|
|
89
|
+
creates a client and answers calls.
|
|
90
|
+
|
|
91
|
+
- **The engine seam no longer reaches outside the package.** `sdk/src/engine.mjs` re-exported the engine through
|
|
92
|
+
`../../www/js/...`; inside a tarball those files do not exist, so a consumer's import died with
|
|
93
|
+
`ERR_MODULE_NOT_FOUND` while the types still compiled. The modules the core depends on now live under
|
|
94
|
+
`sdk/engine/` and the seam points there.
|
|
95
|
+
- **Why a verified mirror and not a move.** The same modules are loaded by the demo page in the browser, and the
|
|
96
|
+
production web root is `www/` alone — the `sdk/` directory is not served there. Lifting the files out of `www/js`
|
|
97
|
+
would 404 every engine import on the live site, and dozens of repository checks read those sources by their
|
|
98
|
+
`www/js` path. So the single source of truth stays `www/js`; `tools/build-sdk-engine.mjs` generates the copy and
|
|
99
|
+
`tools/check-sdk-engine.mjs` verifies it byte-for-byte (the rule `build-sweep` already uses for `sweep/lib` and
|
|
100
|
+
`rfq/lib`).
|
|
101
|
+
- **37 modules are mirrored**: the 16 seam roots plus everything they import statically or dynamically inside
|
|
102
|
+
`www/js`. The heavy vendor bundles stay external (`assets/vendors/...`), loaded dynamically inside functions.
|
|
103
|
+
- **Two check expectations moved to the new location.** `tools/check-sdk.mjs` shares module state with the core
|
|
104
|
+
through `store.js`/`auth.js`; those imports now point at `sdk/engine/...`, the modules the SDK actually loads —
|
|
105
|
+
the assertion kept its meaning, only the path followed the module. `tools/check-sdk-package.mjs` scans
|
|
106
|
+
`sdk/engine` for bare imports too, and `tools/validate.mjs` fails if the mirror drifts from `www/js`.
|
|
107
|
+
- **Proven** (outside the repository): `npm pack` -> install -> import creates a client and answers six calls;
|
|
108
|
+
`tsc --noEmit` on the consumer is green; and removing one mirrored file makes the same import fail with
|
|
109
|
+
`ERR_MODULE_NOT_FOUND` naming that exact file. The rebuilt browser bundle is byte-identical to one built from the
|
|
110
|
+
`www/js` sources.
|
|
111
|
+
- Checks: SDK 229, the package 17, the engine mirror 8 (new), the guard 257, the sell page 42, the book 41, the code
|
|
112
|
+
dictionary 28, assets 13, the order flow 26, browser ones 21/6/18, the live lock run 32 — all green.
|
|
113
|
+
|
|
114
|
+
## 0.36.0 — 2026-10-02
|
|
115
|
+
|
|
116
|
+
The indicative quote book on the form comes FROM THE CORE: the form migration is on the bridge.
|
|
117
|
+
|
|
118
|
+
- **`quotes.watch` asks the book for the size the caller named.** A new optional field `QuoteRequest.size` (in units
|
|
119
|
+
of `unit`: buy — XMR, sell — token). Whoever has already aligned the volume to the provider's grid hands it here —
|
|
120
|
+
the core asks for EXACTLY it, rather than deriving the size from `amount` anew. Without `size` the behaviour is as
|
|
121
|
+
before: the size is derived from the amount and refined on the next tick.
|
|
122
|
+
- **The snapshot carries the fields the screen draws.** For an offer and for a refusal — the pair's sides
|
|
123
|
+
(`asset`/`currency`), both networks and the "named" marker (`assetNetwork`/`currencyNetwork`/`networkStated`),
|
|
124
|
+
`networksDerived`/`networksUnnamed`, the provider's reason `why`, the package's signature `signature`/`keyId`, the
|
|
125
|
+
age `ageMs` and the maturity level `finality`; a REFUSAL is returned as a whole row (the range, the price and the
|
|
126
|
+
mark are not trimmed — the screen has something to name the provider's boundary with). The previous assembled view
|
|
127
|
+
of the networks (`networks`) is kept. The selection and priority rules did not change: they stay in `readBook`.
|
|
128
|
+
- **`QuoteSnapshot.best`** — the best for the requested volume and side, AS THE SERVICE NAMED IT (pair and node).
|
|
129
|
+
The core does not recompute the price: the interface confirms its own "best" mark with it.
|
|
130
|
+
- **A named coarsening:** a connection refusal in the snapshot is called by a CODE (`quote-unavailable`), not by the
|
|
131
|
+
transport's text; the words for a person are made by the interface's dictionary (`www/js/sdk/codes.js`).
|
|
132
|
+
- Checks: SDK 229, the guard 257, the sell page 42, the book-at-the-core 14 (a new probe
|
|
133
|
+
`tools/check-quotes-core.mjs`), rfq-ui 41, the code dictionary 28, assets 13, the order flow 26, browser ones
|
|
134
|
+
21/6/18.
|
|
135
|
+
|
|
136
|
+
## 0.32.0 — 2026-10-01
|
|
137
|
+
|
|
138
|
+
The "server" step: the swap is recorded with us, the XMR arrival is watched, and the wait names itself.
|
|
139
|
+
|
|
140
|
+
- `swaps.start` NO LONGER runs into `not-implemented {step:"server"}`: after the lock the swap is recorded with our
|
|
141
|
+
service BY THE SAME request as the interface's — `POST /api/evm-swaps`, body
|
|
142
|
+
`{ id, network, escrow, quoteId, providerId, hashlock, amount, t0, t1 }` (www/js/evm/auth.js:111-125;
|
|
143
|
+
accepted by `app/server.mjs:1230`, a pass is mandatory — `app/server.mjs:1232`) — and a watch is opened on the
|
|
144
|
+
joint Monero address: `POST /api/swaps` with the address, the VIEW key and the expected amount
|
|
145
|
+
(www/js/ui/views/confirm.js:581-588, www/js/core/chainSource.js:178). The NAMED state `step: "xmr-incoming"` is
|
|
146
|
+
returned with the fields `server` and `watch`, not "success".
|
|
147
|
+
- A new `swaps.watch({ id, escrow?, pollMs?, timeoutMs?, expect?, wallet?, onStep? })`: it polls
|
|
148
|
+
`GET /api/swaps/{id}` — the same path as the page — and either returns the FACT of the arrival (the amount, the
|
|
149
|
+
confirmations, the txid) or refuses with a SEPARATE code `xmr-timeout`. An empty response is not counted as an
|
|
150
|
+
arrival (`server-unavailable { why: "bad-response" }`, answers immediately); a zero at the address is "we are
|
|
151
|
+
waiting" (the timeout). When `expect` and `wallet` are passed, after the arrival the ready mark
|
|
152
|
+
(`actions.markReady`) is called WITH THESE expectations: the slot check against the chain stands in it before the
|
|
153
|
+
signature. Expectations without a wallet (and vice versa) — `bad-input`.
|
|
154
|
+
- New codes: `server-refused` (the service answered and did not accept: the status and its reason in `params`) and
|
|
155
|
+
`xmr-timeout`. "Did not answer", "answered and refused" and "the wait expired" are three different states with
|
|
156
|
+
three codes.
|
|
157
|
+
- New options: `serverAuth` — a pass to our service (only a wallet can sign the sign-in message, so the sign-in
|
|
158
|
+
remains the interface's: www/js/evm/auth.js, `signIn`; an empty pass gives a named 401 refusal, not silence) and
|
|
159
|
+
`watch: { pollMs, timeoutMs }` — the rate and the timeout of the watch polling; the defaults are taken from the
|
|
160
|
+
engine's settings (`API.pollMs`; `blockTimeReal x confirmTarget`), not hardcoded.
|
|
161
|
+
- The "server" step's paths are named BY NAMES and declared in the routes table: `swap` (`/swaps/{id}`) and
|
|
162
|
+
`evmSwaps` (`/evm-swaps`) next to the previous `swaps`, `orderQuote`, `revealedHalf`, `swept`.
|
|
163
|
+
- The escrow address FROM THE RECEIPT and the funding height (`birthHeight`) are written into the swap record:
|
|
164
|
+
previously, after the lock, the record remained with the predicted address and the watch went out with height 0.
|
|
165
|
+
- Checks: 226 (was 200), 0 failures. New cases: the mark's path/method/body, the pass in `Authorization`, the
|
|
166
|
+
service's refusal and its unavailability by separate codes, the arrival seen as a fact, emptiness not counted as an
|
|
167
|
+
arrival, the timeout actually waited out, the ready mark signed only after the arrival and only with expectations.
|
|
168
|
+
Breaking runs: "an empty response is an arrival" (a copy with weakened parsing), a substituted mark body field, a
|
|
169
|
+
substituted mark path — all three go red; `sdk/src/swaps.mjs` was restored byte for byte.
|
|
170
|
+
- **A live run on the local contour** (`node tools/live-lock-run.mjs`, the key only from the environment/file): the
|
|
171
|
+
mark went to the same path and with the same body (`POST /api/evm-swaps -> 201, saved: true`), the watch was opened
|
|
172
|
+
(`POST /api/swaps -> 201`, a real stagenet address), the state was named "waiting for XMR at the address", and the
|
|
173
|
+
wait ended with the code `xmr-timeout` after the assigned timeout. The run's result: 32, 0 failures. There was no
|
|
174
|
+
XMR arrival at the address — that is NAMED by the waiting step, not passed off as completion.
|
|
175
|
+
|
|
176
|
+
# Changelog
|
|
177
|
+
|
|
178
|
+
Format: version — date. Before 1.0 a breaking change bumps the minor version and is accompanied by a migration
|
|
179
|
+
instruction. Entries are not deleted.
|
|
180
|
+
|
|
181
|
+
## 0.35.0 — 2026-10-02
|
|
182
|
+
|
|
183
|
+
Words instead of codes and the XMR withdrawal page on the core — the interface migration is closed.
|
|
184
|
+
|
|
185
|
+
- **The refusal dictionary**: `www/js/sdk/codes.js` — the single place where a core code becomes a phrase (en+ru),
|
|
186
|
+
with detail from the params (the step, the field, the status, what was lacking). **An unknown code is shown as is**
|
|
187
|
+
— swallowing it would mean hiding future errors. The check `tools/check-error-words.mjs` (28) requires a phrase
|
|
188
|
+
for EVERY core code and breaks if an unknown code is replaced with the word "unknown". The copy for the withdrawal
|
|
189
|
+
page is verified by sha256: there cannot be a second revision of the phrases.
|
|
190
|
+
- **The XMR withdrawal page on the core**: parsing the file — `recovery.open`, the plan — `sweep.plan`, the keys —
|
|
191
|
+
`sweep.keys`, the wallet — the Monero adapter, the balance and height — from the node, the withdrawal —
|
|
192
|
+
`sweep.sign` (signing WITHOUT sending) and `sweep.relay`, the repeat report — `sweep.report`. The order and the
|
|
193
|
+
rule "an empty remainder stops the withdrawal" are counted by the core; the page does not repeat them.
|
|
194
|
+
- The core grew for this: `sweep.sign`/`relay`/`report` (the "sign without sending" point is separated from sending —
|
|
195
|
+
otherwise the page would have to repeat the step order), the `SweepApi` contract, the XMR networks from one list
|
|
196
|
+
(`STANDARD_XMR_NETWORKS` — previously the contract promised three networks, but only one was accepted).
|
|
197
|
+
- Defects found and closed along the way: the browser bundle did not return ready-made Monero adapters; a
|
|
198
|
+
discrepancy in the XMR network list; a missing path for the repeat report.
|
|
199
|
+
- **A named incompleteness (not patched in place)**: `recovery.open` and `sweep.plan` do not carry the DISPLAY fields
|
|
200
|
+
(the receive address, the network, the date), so the page additionally parses the file with the same shared module;
|
|
201
|
+
on the withdrawal page's origin `recovery.open` does not open an old file with a mnemonic (it needs a monero-js
|
|
202
|
+
build that is not there) — the page says this aloud.
|
|
203
|
+
- Checks: SDK 229, the code dictionary 28, assets 13, the order flow 26, the guard 257, the sell page 42, the browser
|
|
204
|
+
one 21, the core in the browser 6, the deal in the browser 18.
|
|
205
|
+
## 0.34.0 — 2026-10-02
|
|
206
|
+
|
|
207
|
+
The swap path in the interface goes through the core — from the quote to waiting for the XMR.
|
|
208
|
+
|
|
209
|
+
- `www/js/ui/views/confirm.js`: the record, the order side (DLEQ, deadlines, halves, points), the lock, the mark on
|
|
210
|
+
our server and the watch — all done by the CORE through `swaps.start`. The page lost `createSwap`,
|
|
211
|
+
`runAtomicOrder`, the manual assembly of the record fields, `saveServerSwap`, the manual registration of the watch
|
|
212
|
+
and the re-issuing of the recovery file after the lock. The core hands out the recovery file BEFORE the lock via
|
|
213
|
+
`onRecoveryFile`, and the person saves it before signing — AND NOT after: a file that appears after the lock
|
|
214
|
+
promises what did not exist at the moment of signing.
|
|
215
|
+
- The bridge (`www/js/sdk/bridge.js`) is the one that knows about the core; the page gained `sdkOrderTerms`,
|
|
216
|
+
`sdkStartSwap`, `sdkWatchSwap`.
|
|
217
|
+
- **Core defects found and fixed that made the live path fail:** the firm quote rejected a claimer that came from
|
|
218
|
+
the provider; `acceptCounterparty` did not know the facade's own quote fields; the record lost the receive address
|
|
219
|
+
and the order-quote identifier.
|
|
220
|
+
- A new probe `tools/check-deal-browser.mjs` (18 checks, a real browser): it walks the path ALONG THE SCREEN —
|
|
221
|
+
connecting the wallet, the form, the quote, the sign screen, the password and both checkboxes, the signature — and
|
|
222
|
+
confirms that the wallet sent EXACTLY ONE transaction, the hash was taken FROM THE NODE, the escrow address — from
|
|
223
|
+
the receipt, and the mark and the watch were accepted by our app.
|
|
224
|
+
- Checks: SDK 229, assets 13, the order flow 26, the guard 257, the sell page 42, the browser one 21, the core in the
|
|
225
|
+
browser 6. A named incompleteness: `swapForm.js` (the indicative quote book) is NOT migrated — the book's snapshot
|
|
226
|
+
has a different shape, and that is separate display work.
|
|
227
|
+
## 0.33.0 — 2026-10-02
|
|
228
|
+
|
|
229
|
+
The core is built for the browser, the facade performs the start itself — the swap path is open.
|
|
230
|
+
|
|
231
|
+
- **Building the core for the browser**: `tools/build-sdk.mjs` + `tools/sdk-browser-entry.mjs` (esbuild, ESM,
|
|
232
|
+
261 KB) → `www/assets/vendors/sdk/sdk-browser.js` with the version `?v=<hash>` in the asset registry. The heavy
|
|
233
|
+
vendor bundles (WalletConnect, monero-js, atomic) are left EXTERNAL: without this the output was 3.9 MB, and they
|
|
234
|
+
are loaded only when needed. The bundle returns the whole facade (`createNinsei`) — start, quotes, actions,
|
|
235
|
+
recovery and the browser adapters; the test-contour and Monero (WASM) adapters are not included in it — and this is
|
|
236
|
+
named at the entry point.
|
|
237
|
+
- **The facade brings the swap to the lock itself**: the order side is built by the core ONCE and used both for the
|
|
238
|
+
record and for the lock terms (previously the record and the order could diverge, so a record injection was
|
|
239
|
+
required). The injection remains a CAPABILITY for whoever needs their own side. The contract was brought into
|
|
240
|
+
line: `StartRequest` gained the terms and the counterparty's side, `Ninsei` — the `actions` namespace.
|
|
241
|
+
- **The live run was moved onto the facade path** (without a record injection) and passes: the lock transaction
|
|
242
|
+
`0xcbe282bc…` in block 29, the escrow address taken from the `OrderCreated` event, the mark accepted by our app
|
|
243
|
+
(201), the core reached the XMR wait — 32 checks, 0 failures.
|
|
244
|
+
- **The page really loads the core**: `tools/check-sdk-browser.mjs` (6 checks, a real browser): the core loads as a
|
|
245
|
+
module, the bundle returns the facade, the swap start in the browser reaches the lock, the bridge goes into the
|
|
246
|
+
facade's `swaps.start` and answers with a CODE, not `null`. Teeth: without the bundle file — 5 failures.
|
|
247
|
+
- Why this matters: previously only the surfaces that did not drag the primitives were migrated to the core; now
|
|
248
|
+
everything is available to the page, and the swap path can be migrated as a whole.
|
|
249
|
+
- Checks: SDK 229 (was 226), assets 13, the order flow 26, the guard 257, the sell page 42, the browser one 21.
|
|
250
|
+
## 0.31.0 — 2026-10-01
|
|
251
|
+
|
|
252
|
+
The Monero wallet became a full wallet-adapter.
|
|
253
|
+
|
|
254
|
+
- A new `sdk/src/adapters/monero-wallet.mjs`: `vm`, `connect`, `disconnect`, `close`, `address`, `chainId`,
|
|
255
|
+
`unlockedBalance`, `sync`, `subscribe`, `driver`. The existing XMR withdrawal adapter is untouched: the withdrawal
|
|
256
|
+
and the leg wallet are different roles, and mixing them would mean giving the withdrawal the right to sign anything.
|
|
257
|
+
- **A live run against a local stagenet node** (not a substitution): its own address was derived by the library and
|
|
258
|
+
checked against the keys; the restore height = the node's height minus one and **read back from the wallet**; the
|
|
259
|
+
wallet reads EXACTLY this node (its height matched an independent `get_info`); the new wallet's free remainder is
|
|
260
|
+
zero FROM THE NODE — we put no money in for this, so the zero is honest.
|
|
261
|
+
- A negative control that makes the check a check: the same adapter with a dead node fails
|
|
262
|
+
("Wallet is not connected to daemon") — so the numbers are not substituted but come from the node.
|
|
263
|
+
- The key and the height are constructor fields, not `connect` arguments: the core calls `connect({})` for Monero.
|
|
264
|
+
This is named, because it departs from the common shape.
|
|
265
|
+
- `send`/`receipt` on this adapter REFUSE with a code: sending XMR is the XMR withdrawal's business
|
|
266
|
+
(`sweepUnlocked`/`relayTxs`), not the leg wallet's.
|
|
267
|
+
- A known limitation: `XMR_NETWORKS` in the registry declares only stagenet, so the adapter rejects mainnet (the type
|
|
268
|
+
in the contract names it). When the registry declares it — the adapter will pick it up with no edits.
|
|
269
|
+
- Checks: 200 (was 191). Breaking run: a removed guard in `address()` reddens two checks; the file was restored byte
|
|
270
|
+
for byte. The live part is skipped if there is no local node — the skip is printed, not passed off as success.
|
|
271
|
+
## 0.30.0 — 2026-10-01
|
|
272
|
+
|
|
273
|
+
A live run by the direct path — and a blind token substitution found along the way.
|
|
274
|
+
|
|
275
|
+
- **`swaps.start` reaches the signature by the direct path.** The guards get what the core has (the order amount in
|
|
276
|
+
wei, the native-coin marker, the test contour), and the decision is taken by `blocks === true` — the engine's rule
|
|
277
|
+
— and not by `ok === false`. Previously the start ran into "the amount to fund is unknown" before any chain.
|
|
278
|
+
- `preflight`: the `escrow-points` and `slots` checks are turned on only when their data has been brought. At the
|
|
279
|
+
order-creation step there is nothing to compare against by construction — previously the unconditional check locked
|
|
280
|
+
the signature on an empty input.
|
|
281
|
+
- `lock.factoryOf` takes the factory address from the network registry (and refuses with a code if there is none),
|
|
282
|
+
not from the module where such a function never existed.
|
|
283
|
+
- `lock.send` no longer hides the refusal reason under `server-unavailable`: the reason is named briefly and without
|
|
284
|
+
keys.
|
|
285
|
+
- **The receipt's shape is normalised at the core's boundary** (`{hash} -> hash`): the strict browser adapter
|
|
286
|
+
declares `receipt(txHash: string)`, while the engine passes an object. Previously this gave a silent substitution
|
|
287
|
+
of the PREDICTED escrow address instead of the one from the receipt.
|
|
288
|
+
- `order.acceptCounterparty`: the point of an explicitly passed side is normalised symmetrically to the proven one —
|
|
289
|
+
the same number in two forms no longer gives a false refusal.
|
|
290
|
+
- The Node test wallet gained **an explicit whitelist of test networks** (the local anvil): the engine's "production
|
|
291
|
+
network" rule today does not trigger on any recorded network, because all have `escrow.mode: simulated`.
|
|
292
|
+
- **A BLIND TOKEN SUBSTITUTION** (`sdk/src/config.mjs`): there was a call `tokensOf(chain.chainId)` — a number where
|
|
293
|
+
the engine expects a slug. The network was never found, and the engine silently returned the default network's
|
|
294
|
+
tokens: ANY network got the `arbitrum-sepolia` list, while its own identifier stood in the response. The error was
|
|
295
|
+
invisible — the list looked plausible. Now the network is looked up by number honestly, absence is a refusal, and
|
|
296
|
+
the list is checked against the record of the network ITSELF.
|
|
297
|
+
- Checks: 191 (was 179). Breaking runs: substituting the escrow address instead of the receipt reddens the run;
|
|
298
|
+
returning the previous tokens call reddens the check "no network gets the tokens of ANOTHER network" (the refusal
|
|
299
|
+
report shows: `local-anvil` got `eth|usdc|weth` when its own were `eth`).
|
|
300
|
+
- A live run by the direct path: transaction `0x639b3657…5e06`, block 17, escrow `0x13d0da53…e9b1` taken from the
|
|
301
|
+
receipt, and exactly the order amount lies on the escrow.
|
|
302
|
+
## 0.29.0 — 2026-10-01
|
|
303
|
+
|
|
304
|
+
The first LIVE run: the lock really goes on-chain.
|
|
305
|
+
|
|
306
|
+
- A new test-contour tool: `sdk/src/adapters/node-test-evm-wallet.mjs` — an EVM wallet for Node. The key comes ONLY
|
|
307
|
+
from the environment or a file (never read from arguments), refusal on an unknown network, signing with its own key
|
|
308
|
+
and reading the chain. This is a verification tool, not a path for production use.
|
|
309
|
+
- A new scenario `tools/live-lock-run.mjs` (23 checks): it runs a swap through the core, but with a REAL anvil — the
|
|
310
|
+
node and the wallet are not substituted. Exactly two seams are substituted, both named: the recovery file's
|
|
311
|
+
confirmation and the counterparty's side (the engine's builder builds it).
|
|
312
|
+
- Proof of the live run: transaction `0x11642a1a…47eb5`, block 9, status 0x1, input with the selector
|
|
313
|
+
`createOrderAndFund`; the escrow address `0x35f4421e…c9f606` IS VISIBLE IN THE RECEIPT'S LOGS and matched the one
|
|
314
|
+
the core named. Balances were checked against anvil before and after.
|
|
315
|
+
- A chain-read seam for Node: the `evmCall` option; without it the success path is unverifiable, because the engine's
|
|
316
|
+
read requires the page's wallet.
|
|
317
|
+
- Fixed along the way: the guards were failing with a `TypeError` without a code — `preflight.mjs` reads the network
|
|
318
|
+
as `config.evmNetwork`, while the configuration returns it as `settlement.chain`; the names were brought together
|
|
319
|
+
in `index.mjs`.
|
|
320
|
+
|
|
321
|
+
### What the live run revealed (substitutions did not show this)
|
|
322
|
+
|
|
323
|
+
- **The receipt flows past the adapter contract:** `funding.js` calls `receipt({hash})` with an object, while the
|
|
324
|
+
contract declares `receipt(txHash: string)`. A strict browser adapter would answer with a refusal, the refusal
|
|
325
|
+
would be swallowed by the wait, and the escrow address would be SILENTLY substituted with the predicted one. That
|
|
326
|
+
is, the declared guarantee "the address from the receipt" does not work in the browser today — and that is exactly
|
|
327
|
+
why in the live run the adapter accepts both forms.
|
|
328
|
+
- **`acceptCounterparty` is asymmetric:** the proven point is normalised to the `0x` form, but the `edPub` of an
|
|
329
|
+
explicitly passed side is not; the same number in different forms gives `quote-refused {step:"proof"}`.
|
|
330
|
+
- **The engine's points come without the `0x` prefix**, while the calldata encoder requires the prefix: the page
|
|
331
|
+
normalises them at the boundary, the core does not.
|
|
332
|
+
- **The "production network" rule does not trigger on any recorded network:** all production networks have
|
|
333
|
+
`escrow.mode: "simulated"`, so by the letter of the rule they are not production, and the test key would sign on
|
|
334
|
+
them. The real line of defense today is the refusal on an unknown network.
|
|
335
|
+
|
|
336
|
+
### Core defects found by the run (not fixed in this PR, named)
|
|
337
|
+
|
|
338
|
+
- `swaps.start` calls `preflight(quote, {})` without the interface's data and counts `ok === false` as a refusal,
|
|
339
|
+
whereas the engine locks the signature only at `blocks === true`. The start runs into "the amount is unknown"
|
|
340
|
+
before any chain.
|
|
341
|
+
- `lock.factoryOf()` always returns `null`: it looks into a module where such a function does not exist.
|
|
342
|
+
- `lock.send` turns ANY error without a code into `server-unavailable`, so the real reason does not get through.
|
|
343
|
+
## 0.28.0 — 2026-10-01
|
|
344
|
+
|
|
345
|
+
The Monero balance and switching the wallet's network — the last two named gaps.
|
|
346
|
+
|
|
347
|
+
- `wallet.balances` for a Monero wallet takes the balance from the adapter (`unlockedBalance`) and returns **atomic
|
|
348
|
+
units** (1 XMR = 10¹² piconero) with the named precision. There is no unit arithmetic of its own: the source is the
|
|
349
|
+
same method the XMR withdrawal uses.
|
|
350
|
+
- `wallet.switchChain` goes **by the same path as the page**: `wallet_switchEthereumChain`, and on an unknown network
|
|
351
|
+
(4902) — `wallet_addEthereumChain`, the network parameters taken from the engine's registry through the seam, not
|
|
352
|
+
from a private table; a user refusal — `wallet-rejected`. **After the switch the network is re-read:** "switched" is
|
|
353
|
+
the wallet's assertion, not a fact, and it cannot be trusted.
|
|
354
|
+
- The reason the switch is reproduced in the adapter, rather than taken from the engine: the engine's `switchChain` is
|
|
355
|
+
bound to the page's provider, into which the adapter's provider cannot be substituted. Taking it as is would mean
|
|
356
|
+
breaking both the SDK usage and the switching that works in the interface today.
|
|
357
|
+
- The virtual-machine name for Monero is `monero` (`XMR_VM`). There is no such name in the network registry (there it
|
|
358
|
+
is evm, bitcoin, solana, tron), so it is declared in `wallet.mjs` as a constant and explained in a comment.
|
|
359
|
+
- Checks: 179 (was 172). Breaking run: removing the network check after the switch reddens the check
|
|
360
|
+
"'switched' without a network change"; the file was restored byte for byte (the hash matched).
|
|
361
|
+
- A known gap, named plainly: **the Monero wallet as a full wallet-adapter is not done** — `moneroWallet` (for the XMR
|
|
362
|
+
withdrawal) does not implement the wallet interface (connect/address/chainId/subscribe/driver). The checks use a
|
|
363
|
+
substituted adapter. Working from the browser needs an adapter over monero-ts.
|
|
364
|
+
## 0.27.0 — 2026-10-01
|
|
365
|
+
|
|
366
|
+
The lock: the core really locks funds.
|
|
367
|
+
|
|
368
|
+
- `swaps.start` no longer throws `not-implemented { step: "lock" }`: the wallet comes from outside (`request.wallet`),
|
|
369
|
+
the core calls the finished `lock.send`, waits for the receipt and takes **the escrow address from the receipt**,
|
|
370
|
+
not from a prediction. After that the next step is named — `server` (stage 4).
|
|
371
|
+
- **Without a wallet — a refusal with a code, and nothing goes on-chain.** Without a confirmed recovery file the lock
|
|
372
|
+
is not done either: a file with nothing to open it with is worse than no money on the escrow.
|
|
373
|
+
- `lock.send(request, wallet, deps)` gained the `deps.call` seam — without it the success path is unverifiable at
|
|
374
|
+
all: the engine's chain read requires a connected provider, which bare Node does not have. Backward compatible.
|
|
375
|
+
- The lock terms are taken from `request.order` (eight fields the DLEQ proof is bound to), not from the record: at
|
|
376
|
+
this point the record has no commitments and no claimer point. `amount` is already in wei.
|
|
377
|
+
- Checks: 172 (was 164). New: a lock without a wallet and without a file sends nothing; with a wallet — exactly one
|
|
378
|
+
send, and **the escrow address is taken from the receipt, even when the prediction gives a different address**.
|
|
379
|
+
- An API-contract decision, said aloud: the wallet at `start` is passed inside the request itself (`request.wallet`),
|
|
380
|
+
not as a second argument as in `lock.send`/`actions`. That way the request stays one object together with
|
|
381
|
+
`onRecoveryFile`. If you want a uniform shape — it is a pinpoint edit.
|
|
382
|
+
## 0.26.0 — 2026-10-01
|
|
383
|
+
|
|
384
|
+
Checking the slots before signing — and the check that breaks it.
|
|
385
|
+
|
|
386
|
+
- `createActions({ call })`: what reads the chain is also a seam. This is not only for the checks: **the slot check
|
|
387
|
+
and the signature must go one way**, otherwise part of the check goes past the wallet and part does not. Default —
|
|
388
|
+
the engine's read.
|
|
389
|
+
- The check by a run covers THE VERY case that was named unclosed: **the slots on the chain are not the ones we
|
|
390
|
+
assembled**. The composition matches, the values differ — so the order is foreign or substituted, and it must not be
|
|
391
|
+
signed. The run confirms: there is no signature (`contract-reverted`), and nothing went on-chain.
|
|
392
|
+
- Why this is a separate check and not "obvious anyway": the check compares the order's points and commitments with
|
|
393
|
+
what lies on the chain, and its refusal is the only thing between the signature and a foreign order. A mark on a
|
|
394
|
+
foreign order allows a foreign address to take the settlement; claiming and refunding reveal your own half, and
|
|
395
|
+
that is irreversible.
|
|
396
|
+
- The number of checks became 143.
|
|
397
|
+
## 0.26.1 — 2026-10-01
|
|
398
|
+
|
|
399
|
+
Fixing the signing-guards tooth after the extended seam.
|
|
400
|
+
|
|
401
|
+
- The `check-signing-guards` check looks in the flow's source for the SLOT CHECK before the claim and before the
|
|
402
|
+
ready mark and makes sure its removal goes red. After the call gained a fourth argument (`deps`), the guard stopped
|
|
403
|
+
finding the call and rightly cried out: "NOT FOUND in the source" and "removal does not go red".
|
|
404
|
+
- The expectation was updated and remained strict: the call must be a separate `await` line, and the seam (`deps`) is
|
|
405
|
+
allowed. Otherwise the guard would go red from exactly what we were after — from the fact that the check went
|
|
406
|
+
through the adapter.
|
|
407
|
+
- The run: `check-signing-guards` — 257 checks, 0 failed.
|
|
408
|
+
- A lesson for the future worth keeping: the guard has expectations about the TEXT of the call, and any signature
|
|
409
|
+
edit must be accompanied by a run of exactly this guard, not just the profile suites.
|
|
410
|
+
## 0.25.0 — 2026-10-01
|
|
411
|
+
|
|
412
|
+
Order actions: marking ready, claiming, refunding — all through one wallet seam.
|
|
413
|
+
|
|
414
|
+
- `actions.markReady/claim/refund` — wrappers over the engine: the wallet comes as an adapter (`send`/`receipt`), the
|
|
415
|
+
chain read is the engine's, refusals come as CODES (`bad-input`, `contract-reverted`), and the texts remain the
|
|
416
|
+
interface's.
|
|
417
|
+
- **`expect` IS MANDATORY.** The slot check compares the order we assembled with what lies on the chain; without
|
|
418
|
+
expectations there is nothing to compare against, and the signature goes out under an order whose commitments
|
|
419
|
+
nobody checked. For the mark that is granting a foreign address the right to take the settlement; for the claim and
|
|
420
|
+
the refund it is revealing your own half (irreversible). In all three cases the transaction still looks successful.
|
|
421
|
+
- The halves are also mandatory where they are revealed: `halfClaimer` on the claim, `halfLocker` on the refund.
|
|
422
|
+
- The engine's seam (`swapFlow`) was added to `engine.mjs` — the core keeps no path of its own to the order flow.
|
|
423
|
+
- The number of checks became 141. The check by a run also makes sure that none of the refusals REACHED sending.
|
|
424
|
+
- What the check does not yet do, and this is named: the case "the slots on the chain did not match the expectations"
|
|
425
|
+
(a refusal before the signature when the slots match in composition but differ in value). This is the next step:
|
|
426
|
+
there a stand-in chain reader with an exact slot composition is needed, and I do not want to pick it at random.
|
|
427
|
+
## 0.24.0 — 2026-10-01
|
|
428
|
+
|
|
429
|
+
The wallet seam was extended to the order actions (steps 1+2 of the plan).
|
|
430
|
+
|
|
431
|
+
- `orderLiveSlots`, `assertOrderSlots`, `markReadyOrder`, `claimOrder`, `refundOrder` accept `deps` with `call` and
|
|
432
|
+
`send` (the defaults are as before). Now **all four steps** — lock, ready mark, claim, refund — can go through ONE
|
|
433
|
+
wallet seam rather than partly past it.
|
|
434
|
+
- **The slot check was added to the refund** (`refundOrder`, when expectations are given). A refund REVEALS a half,
|
|
435
|
+
that is, it is as irreversible as a claim: signing it under an order whose commitments we did not check is not
|
|
436
|
+
allowed for the same reason as a claim. The expectations are optional for the sake of the previous calls; the core
|
|
437
|
+
always passes them.
|
|
438
|
+
- Why this matters at all: the slot check goes in two stages (`assertOrderSlots` → `orderLiveSlots` → reading from the
|
|
439
|
+
chain), and without the extended seam the read would keep going to the chain by its own path — half of the step
|
|
440
|
+
through the adapter, half past it.
|
|
441
|
+
- The checks covering this already existed: `tools/check-atomic-flow.mjs` (26 checks) and the SDK suite (137). Both
|
|
442
|
+
runs are green — the behaviour for the previous calls did not change.
|
|
443
|
+
## 0.23.0 — 2026-10-01
|
|
444
|
+
|
|
445
|
+
Sending the lock: the deadlines are checked before it, the wallet is an adapter.
|
|
446
|
+
|
|
447
|
+
- `lock.send(request, wallet)` — sending the lock. The chain work is done by the engine (`fundOrder`), and the wallet
|
|
448
|
+
comes as an adapter: signing is possible only in it, and the core keeps no path into the chain of its own.
|
|
449
|
+
- **THE DEADLINES ARE CHECKED BEFORE SENDING, and the refusal names exactly the deadline**
|
|
450
|
+
(`bad-input { field: "readyBy" }`). The point here is not tidiness: a transaction with violated deadlines LOOKS
|
|
451
|
+
successful until the contract rejects it — and it will reject it later and more expensively. The check by a run
|
|
452
|
+
makes sure that on such a refusal NOTHING went on-chain.
|
|
453
|
+
- The order terms and the sending name the same field differently (`amount` versus `amountWei`): the rules are checked
|
|
454
|
+
on the sending form, the core normalises the names, and the rules stay with the engine.
|
|
455
|
+
- **The escrow address is taken from the receipt, not from a prediction:** a prediction could diverge from what was
|
|
456
|
+
actually created, and it would diverge silently.
|
|
457
|
+
- The EVM wallet role accepts `gas`: every contract call (lock, ready mark, claim, refund) has its own estimate, and
|
|
458
|
+
it comes from the engine's EVM layer together with the calldata.
|
|
459
|
+
- The number of checks became 137.
|
|
460
|
+
## 0.22.0 — 2026-10-01
|
|
461
|
+
|
|
462
|
+
The lock terms: assembled and checked BEFORE sending.
|
|
463
|
+
|
|
464
|
+
- `lock.terms(request)` — the order terms for the contract. They are assembled by the ENGINE
|
|
465
|
+
(`www/js/evm/escrow.js`, `orderTerms`), because the contract sets the rules; the core has no copy of the rules — a
|
|
466
|
+
copy would drift from the contract right where the error is visible only as a rejected transaction.
|
|
467
|
+
- Refusals come as CODES: `bad-input { field: "readyBy", why: "not-in-future" }`, `{ field: "t1",
|
|
468
|
+
why: "not-after-readyBy" }`, and incomplete terms name the missing fields. The interface need not parse foreign
|
|
469
|
+
texts, and we need not repeat their wording.
|
|
470
|
+
- `lock.factoryOf(chainSlug)` — the escrow factory address by the same path as the engine's: the core keeps no address
|
|
471
|
+
list of its own.
|
|
472
|
+
- What is NOT here, and this is named: the sending itself. It needs an EVM wallet (the `send`/`receipt` role already
|
|
473
|
+
exists) and a call to `fundOrderLive` from the engine's EVM layer — the next step, not covered by a stub.
|
|
474
|
+
- The number of checks became 135.
|
|
475
|
+
## 0.21.0 — 2026-10-01
|
|
476
|
+
|
|
477
|
+
Sending a transaction and the receipt — what is indispensable for locking funds.
|
|
478
|
+
|
|
479
|
+
- `WalletAdapter.send({ to, data, value })` — sending a leg transaction. For EVM this is `eth_sendTransaction` with
|
|
480
|
+
`{ from, to, data, value }` (the amount in wei, hexadecimal), the response — the transaction hash. Without a
|
|
481
|
+
connected wallet — the refusal `wallet-not-connected`, not sending "into the void".
|
|
482
|
+
- `WalletAdapter.receipt(txHash)` — the receipt by hash (`eth_getTransactionReceipt`).
|
|
483
|
+
- **THERE IS DELIBERATELY NO POLLING INSIDE THE ADAPTER:** `receipt` asks ONCE and returns the receipt or `null` if
|
|
484
|
+
the transaction is not in a block yet. The waiting policy (how long, how often, what on refusal) is set by the
|
|
485
|
+
caller: a policy of its own inside the adapter would mean two calls wait differently.
|
|
486
|
+
- The check by a run: the adapter calls EXACTLY those methods and with those arguments — the sender, the recipient,
|
|
487
|
+
the data and the amount in wei; the receipt by hash is returned as is; a receipt without a hash — a data error.
|
|
488
|
+
- The number of checks became 131.
|
|
489
|
+
## 0.20.0 — 2026-10-01
|
|
490
|
+
|
|
491
|
+
The heavy run of the counterparty's side with a real DLEQ proof.
|
|
492
|
+
|
|
493
|
+
- `tools/check-sdk-order-proof.mjs` — a SEPARATE run, not part of the shared suite: the proof computation takes
|
|
494
|
+
seconds (the measurement in the output: ~3.6 s and ~3.2 s), and in the battery it would slow down every edit.
|
|
495
|
+
- Three things are checked, and all three are about money: a real side for THESE terms is accepted; **a real side for
|
|
496
|
+
OTHER terms is rejected** (the proof is bound to the order context); **a substituted half with a foreign proof is
|
|
497
|
+
rejected** (the proven point must match the one that will go into the contract).
|
|
498
|
+
- All three checks go through the SDK facade (`sdk.order.verifyCounterparty`), that is, the path the interface uses is
|
|
499
|
+
checked, not the engine's internals.
|
|
500
|
+
- The shared suite still has 127 checks: the heavy run is deliberately not part of it and is started separately
|
|
501
|
+
(`node tools/check-sdk-order-proof.mjs`).
|
|
502
|
+
## 0.19.0 — 2026-10-01
|
|
503
|
+
|
|
504
|
+
The counterparty's side and the joint address — what is checked BEFORE signing.
|
|
505
|
+
|
|
506
|
+
- `order.context(terms)` — the order context string (`arrakis-order-v3`, the fields and order taken from the engine
|
|
507
|
+
module). The same context the quote signature is bound to.
|
|
508
|
+
- `order.verifyCounterparty({ side, order, sealed?, ownEncPriv? })` — TWO mandatory checks in one engine call: the
|
|
509
|
+
proof stands in the context of THIS order and the proven ed25519 point matches the one that will go into the
|
|
510
|
+
contract. It did not pass — `quote-refused { step: "proof" }`, not a string.
|
|
511
|
+
Why the second one: without it the counterparty could prove one point and substitute another. On our side
|
|
512
|
+
everything would look sound, and the discrepancy would surface when claiming the XMR, with nothing left to fix.
|
|
513
|
+
- `order.jointAddress({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network })` — the joint Monero
|
|
514
|
+
address by the same encoder as the wallet's: a second one would drift from the first right where the error is
|
|
515
|
+
visible only by sending XMR.
|
|
516
|
+
- There is no cryptography of the core's own: the engine does everything, the core passes the primitives and turns
|
|
517
|
+
the verdict into a CODE.
|
|
518
|
+
- What is deliberately ABSENT from the checks: a positive case with a real DLEQ proof. It takes seconds and would
|
|
519
|
+
slow the whole suite; its place is a separate run on the live contour. Here is checked what must be cheap and
|
|
520
|
+
breaks silently: garbage is rejected with a code, not a crash; incomplete terms — a data error; the joint address
|
|
521
|
+
needs both your half and the other's point.
|
|
522
|
+
- The number of checks became 127.
|
|
523
|
+
## 0.18.0 — 2026-10-01
|
|
524
|
+
|
|
525
|
+
Binding the quote to OUR order terms — the second half of the same rule.
|
|
526
|
+
|
|
527
|
+
- The signature confirms the provider signed THIS quote, but not that it is about our swap. We are responsible for
|
|
528
|
+
the binding: `quotes.firm` computes the context string from OUR terms with the engine module (`orderContextString`,
|
|
529
|
+
format `arrakis-order-v3`) and requires the provider to return exactly it. There is deliberately no copy of the
|
|
530
|
+
format in the SDK: a copy would silently drift from the protocol — I already proved that on myself by guessing the
|
|
531
|
+
field list.
|
|
532
|
+
- It did not match — `quote-refused { step: "context", why: "other-terms" }`. There is no context string at all —
|
|
533
|
+
also a refusal (`why: "not-reported"`): there is nothing to check the binding with, and silence is not consent. The
|
|
534
|
+
same rule as with the signature flag.
|
|
535
|
+
- Incomplete order terms — `bad-input { field: "order" }`: a data error, not a provider refusal.
|
|
536
|
+
- THE ORDER OF THE GATES: signature first, then binding — so a refusal names the nearest reason.
|
|
537
|
+
- The fields and their order are part of the protocol (`chainId, factory, locker, claimer, amount, readyBy, t1,
|
|
538
|
+
salt`): rearranging them would change the context and instantly devalue all issued quotes.
|
|
539
|
+
- The number of checks became 122.
|
|
540
|
+
## 0.17.0 — 2026-10-01
|
|
541
|
+
|
|
542
|
+
A firm quote for an order — and why the backend's silence is not counted as consent.
|
|
543
|
+
|
|
544
|
+
- `quotes.firm({ providerId, order })` works: POST to `routes.orderQuote` (default `/order-quote`) with the body
|
|
545
|
+
`{ providerId, order }` — the same path and body as the engine's (`requestOrderQuote`). Before this the facade
|
|
546
|
+
answered `not-implemented`.
|
|
547
|
+
- **A quote is accepted ONLY with a confirmed signature.** It carries a commitment under keys, so an unverified
|
|
548
|
+
signature is not "probably fine". The backend verifies the signature (it holds the provider's key) and reports the
|
|
549
|
+
result in a field; on our side it remains not to mistake silence for consent: **a missing flag counts as an
|
|
550
|
+
unverified signature** just like `false`.
|
|
551
|
+
- The different refusal reasons are named differently: `why: "not-verified"` — the backend checked and it did not
|
|
552
|
+
match; `why: "not-reported"` — nothing was said about a check. For the interface these are different pieces of
|
|
553
|
+
advice.
|
|
554
|
+
- The number of checks became 119.
|
|
555
|
+
## 0.16.0 — 2026-10-01
|
|
556
|
+
|
|
557
|
+
The swap record and our order side — the start of the second half of the work (the swap).
|
|
558
|
+
|
|
559
|
+
- `sdk/src/record.mjs`: `createRecord` creates the swap record (the engine's `createSwap`) and puts OUR order side
|
|
560
|
+
into it — a spend-key half, a view-key half and their points. The field names are the ones the engine reads them by
|
|
561
|
+
(`half`, `viewHalf`, `ownSpendPoint`, `ownViewPoint`): otherwise the recovery file would end up without halves, and
|
|
562
|
+
its own validator would reject it — rightly.
|
|
563
|
+
- `swaps.start` creates the record itself if the interface did not bring its own, and **goes further not into the file
|
|
564
|
+
but into the order**. This is not a rearrangement for convenience: the counterparty's halves and the open points,
|
|
565
|
+
without which the file is empty, appear exactly at the order step (clarified against the engine's code in 0.8.1).
|
|
566
|
+
- Naming honestly: the DLEQ proof is bound to the ORDER CONTEXT (the terms, deadlines, salt, escrow address,
|
|
567
|
+
chainid), and the terms do not exist before a firm quote. So `start` reaches the order step and names it
|
|
568
|
+
(`not-implemented`, `step: "order"`, `missing: ["counterparty-half"]`, `swapId`).
|
|
569
|
+
- The first run of the new check went red twice, and both times for a reason: the checks were written for the previous
|
|
570
|
+
behaviour, and the code itself was failing with "Cannot access 'record' before initialization" — the module import
|
|
571
|
+
was named the same as a variable inside `start`. This is visible only by a run.
|
|
572
|
+
- The number of checks became 111.
|
|
573
|
+
## 0.15.0 — 2026-10-01
|
|
574
|
+
|
|
575
|
+
The withdrawal mark — and why it cannot declare the withdrawal unsuccessful.
|
|
576
|
+
|
|
577
|
+
- `sweep.run`, after sending, tells the service about the withdrawal: POST to `routes.swept` (default
|
|
578
|
+
`/swaps/{id}/swept`) with the body `{ txid }` — the same path and body as the sweep page. As a result the `reported`
|
|
579
|
+
flag appeared.
|
|
580
|
+
- **THE REPORT DOES NOT AFFECT THE TRANSFER.** The XMR is already on the network; a failed report gives
|
|
581
|
+
`reported: false`, so the interface offers to report again. Declaring such a withdrawal unsuccessful would mean
|
|
582
|
+
lying about money, so the check by a run watches exactly this: on a service refusal the withdrawal stays successful
|
|
583
|
+
and the transactions are returned.
|
|
584
|
+
- The report is made only when there is both a swap and a transfer hash: there is nothing to report — we do not
|
|
585
|
+
report.
|
|
586
|
+
- The number of checks became 111.
|
|
587
|
+
## 0.14.0 — 2026-10-01
|
|
588
|
+
|
|
589
|
+
The withdrawal keys from the revealed half — with the check the contract cannot do.
|
|
590
|
+
|
|
591
|
+
- `sweep.keys({ file, passphrase })` assembles the spend key and the view key: 64 hexadecimal characters, byte order
|
|
592
|
+
as in Monero (little-endian — the same way the engine returns them).
|
|
593
|
+
- **The check is mandatory and is done before any syncing**: `(s_a + s_b)*G` must match the order's public spend key.
|
|
594
|
+
The contract cannot check this — it only makes sure the hash of the revealed half matched the commitment, and there
|
|
595
|
+
is no ed25519 in EVM. A half from another order — the refusal `bad-input { step: "verify" }`, not an attempt to
|
|
596
|
+
send. The check by a run builds REAL halves and makes sure a foreign one is rejected.
|
|
597
|
+
- "Not revealed yet" is a state: `bad-input { field: "revealedHalf", why: "not-revealed" }`. No one has the spend key
|
|
598
|
+
before the reveal.
|
|
599
|
+
- The halves' math is taken through the engine's seam (`halves`, `halvesSweep`), and the primitives — from the core:
|
|
600
|
+
it is verified by a run that `(a+b)G == aG + bG` on these primitives.
|
|
601
|
+
- The number of checks became 108.
|
|
602
|
+
## 0.13.0 — 2026-10-01
|
|
603
|
+
|
|
604
|
+
The counterparty's revealed half — from the chain via the app.
|
|
605
|
+
|
|
606
|
+
- `sweep.plan` asks the app for the half **by the same path and body as the sweep page**: POST to
|
|
607
|
+
`routes.revealedHalf` (default `/escrow/revealed-half`) with the body `{ network, address }`, where `address` is the
|
|
608
|
+
**escrow** address, not the Monero address (verified by reading the page's code). The `revealedHalf` flag appeared
|
|
609
|
+
in the plan.
|
|
610
|
+
- **"Not revealed yet" is a state, not an error.** The half is published by the settlement that moves the ETH (a
|
|
611
|
+
claim or a refund); before that no one has the spend key. So the app's refusal and "not revealed" both give
|
|
612
|
+
`revealedHalf: false` and `missing: ["revealed-half"]`, not an exception.
|
|
613
|
+
- The transport was taught the method and the body: `json(path, { method, headers, body })`. Without this a POST with
|
|
614
|
+
a body would silently leave as a GET, and the app would answer with a refusal to a request it did not understand —
|
|
615
|
+
the check catches this.
|
|
616
|
+
- What is not there yet, and this is named: the derivation of the spend key from the half itself
|
|
617
|
+
(`spendKeysFromRevealedHalf`, together with the mandatory check `(s_a + s_b)*G` against the order's key) — the next
|
|
618
|
+
step.
|
|
619
|
+
- The number of checks became 104.
|
|
620
|
+
## 0.12.1 — 2026-10-01
|
|
621
|
+
|
|
622
|
+
The "refusal, not a breakdown" check — in the wake of a manual merge.
|
|
623
|
+
|
|
624
|
+
- A manual branch merge left an unnamed object in `sweep.mjs`, and `sweep.run` was failing with a `ReferenceError`
|
|
625
|
+
(`api is not defined`) — that is a **breakdown, not a refusal**. The neighbouring check then went red not for its
|
|
626
|
+
own reason, and the cause had to be reached separately.
|
|
627
|
+
- A new check walks EVERY public method (18 calls) with deliberately invalid arguments and requires the answer to be
|
|
628
|
+
a value or an `SdkError` with a code. Any other error (`TypeError`, `ReferenceError`) is a breakdown and is named
|
|
629
|
+
aloud, with the method's name.
|
|
630
|
+
- Along the way the check asserts that the surface was really walked (not "a couple of methods"): otherwise the check
|
|
631
|
+
would silently degenerate into an empty one.
|
|
632
|
+
- The number of checks became 98.
|
|
633
|
+
## 0.12.0 — 2026-10-01
|
|
634
|
+
|
|
635
|
+
Addresses and paths are set from outside. The default is our service and our explorers.
|
|
636
|
+
|
|
637
|
+
- `endpoints: { explorer: { evmTx, evmAddr, evmName, xmrTx }, rpc: { evm, monero } }`: the defaults are taken from
|
|
638
|
+
the network registry (`explorerTx`, `explorerAddr`, `explorerName`, `rpcUrl`), the options override them.
|
|
639
|
+
- `routes: { swaps, revealedHalf, swept }` — **the paths are named by names, not by strings in code**:
|
|
640
|
+
`config.route("swept", { id })` substitutes and encodes the parameter. An unknown name — `bad-input`: a typo must
|
|
641
|
+
not silently fall back to the default.
|
|
642
|
+
- A RULE: **addresses are configurable, not checks**. Your own API, your own node, your own explorer, foreign paths —
|
|
643
|
+
fine; the pre-signature guards (the price, the amount coverage, the token allowance, the deadlines from the chain)
|
|
644
|
+
stay enabled under any configuration. Otherwise "maximum configurability" would become a way to switch the checks
|
|
645
|
+
off with a single field. The check by a run watches exactly this: the list of checks does not change on an override.
|
|
646
|
+
- `sweep.plan` goes by the named path, so changing the host or using a third-party server is an edit of an object, not
|
|
647
|
+
a fork of the core.
|
|
648
|
+
- The number of checks became 88.
|
|
649
|
+
## 0.11.0 — 2026-10-01
|
|
650
|
+
|
|
651
|
+
The XMR withdrawal as a whole: the wallet comes as an adapter, the core holds the order.
|
|
652
|
+
|
|
653
|
+
- `sweep.run({ file, passphrase, to, connectWallet })` works: plan -> wallet -> sync ->
|
|
654
|
+
free remainder -> **sign WITHOUT sending** (`sweepUnlocked({ relay: false })`) -> sending (`relayTxs`) -> closing.
|
|
655
|
+
The order is checked by a run against a substituted wallet.
|
|
656
|
+
- **An empty remainder stops the withdrawal BEFORE signing** (`insufficient-funds`): otherwise the wallet would sign a
|
|
657
|
+
transaction for zero, and the refusal would look like success. The check watches both this and the fact that there
|
|
658
|
+
was no signature-with-sending at zero at all.
|
|
659
|
+
- `moneroWallet` in `@ninsei-labs/sdk/adapters`: the Monero wallet role over the library the page provides. The method
|
|
660
|
+
names are the ones the sweep page already calls on monero-ts (`sync`, `getUnlockedBalance`, `sweepUnlocked`,
|
|
661
|
+
`relayTxs`, `close`), so the core stays without DOM and without a 3.6 MB worker.
|
|
662
|
+
- A half-adapter is rejected BEFORE opening the wallet and names which methods are missing.
|
|
663
|
+
- Adapter refusals — with codes, without user-facing phrases (this is checked by machine across all of `sdk/src`).
|
|
664
|
+
- The number of checks became 89.
|
|
665
|
+
## 0.10.0 — 2026-10-01
|
|
666
|
+
|
|
667
|
+
The XMR withdrawal (what the sweep page does) — the surface in the core. The user's requirement: the SDK must make
|
|
668
|
+
it possible to run the whole swap fully and programmatically, including the withdrawal.
|
|
669
|
+
|
|
670
|
+
- `sweep.plan({ file, passphrase })` works: it decrypts the recovery file, checks the composition, takes the joint
|
|
671
|
+
Monero address, asks the app for the restore height BY ADDRESS (exactly as the sweep page does — `GET /swaps`, the
|
|
672
|
+
field `restoreHeight`) and says with codes what is missing: `restore-height`, `spendable`, `revealed-half`. The
|
|
673
|
+
withdrawal steps are named: `unlock`, `sync`, `sweep`, `relay`.
|
|
674
|
+
- An unavailable app is not a refusal: the plan is returned, and the missing height is named in `missing`. Silent
|
|
675
|
+
emptiness here would be worse than a refusal: the person would not know the withdrawal will not go.
|
|
676
|
+
- `sweep.run(...)` is named honestly: it needs a Monero wallet adapter (`moneroWallet`), because syncing,
|
|
677
|
+
`sweepUnlocked` and `relayTxs` live where there is WASM. The core stays without DOM and without the 3.6 MB worker.
|
|
678
|
+
- The number of checks became 83.
|
|
679
|
+
## 0.9.0 — 2026-10-01
|
|
680
|
+
|
|
681
|
+
Atomic swap primitives — from the same npm packages as the page bundle (the user's decision: do not drag a hashed
|
|
682
|
+
build artefact into the core).
|
|
683
|
+
|
|
684
|
+
- `sdk/src/primitives.mjs`: `ed25519`, `secp256k1`, `keccak256`, `sha3_512`, `randomBytes` and the `primitives` set
|
|
685
|
+
in the shape `createOrderBuilder` expects. The order-side build modules do not import the primitives — they arrive
|
|
686
|
+
as parameters, so the core can return its own.
|
|
687
|
+
- **keccak_256 is taken from the curves-and-hashes package, not from the separate `keccak256`.** That one accepts only
|
|
688
|
+
a Buffer of its own instance and refuses `invalid type` on bytes from another module (verified by a run). The value
|
|
689
|
+
is checked against a known one: keccak256 of the empty string =
|
|
690
|
+
c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470.
|
|
691
|
+
- The check distinguishes keccak256 and SHA3-512 on values: the commitment for a half is computed with keccak256 (as
|
|
692
|
+
in Solidity), the proof — SHA3-512. Swapping one for the other looks like a typo and costs a swap.
|
|
693
|
+
- The number of checks became 75. What this check does NOT assert, and this is said aloud: the compatibility of the
|
|
694
|
+
set with the side builder (`createOrderBuilder`/`newSide`) — the next step, not a claimed property.
|
|
695
|
+
## 0.8.1 — 2026-10-01
|
|
696
|
+
|
|
697
|
+
The start step order was corrected against the engine. The key halves (`spendHalf`, `viewHalf`, the counterparty's
|
|
698
|
+
open points) are generated by the ORDER (`www/js/atomic/order-worker.js`, `createOrderBuilder`; in the engine they are
|
|
699
|
+
fetched by `halvesWalletOf`), so the recovery file cannot be assembled before the order. The order: guards -> swap
|
|
700
|
+
record -> order -> recovery file and confirmation -> LOCKING THE FUNDS -> server -> watch. The property "funds are not
|
|
701
|
+
locked before the file's confirmation" did not change.
|
|
702
|
+
|
|
703
|
+
## 0.8.0 — 2026-10-01
|
|
704
|
+
|
|
705
|
+
The recovery file and the confirmation gate — for real, not with a stub.
|
|
706
|
+
|
|
707
|
+
- `swaps.start` performs steps 2 and 3 for real: it assembles the file's passport (`buildPayload`), CHECKS it before
|
|
708
|
+
the person (`validatePayload`), encrypts it with the passphrase (`createRecoveryFile`) and hands the interface
|
|
709
|
+
`{ name, contents }`. A file that will not open is worse than no file: it will be relied upon.
|
|
710
|
+
- A passphrase shorter than 12 characters is not accepted (`bad-input` with `{ field: "passphrase", min: 12 }`): a
|
|
711
|
+
file with a weak passphrase is a file strangers will open.
|
|
712
|
+
- THE GATE: until `onRecoveryFile` returns `true`, no action that locks funds is performed. The person's refusal and
|
|
713
|
+
silence are `recovery-declined`, not consent.
|
|
714
|
+
- There is no swap record yet: only the interface with a browser wallet can do it. It arrives in `createSwaps` from
|
|
715
|
+
outside (`createRecord`), so the whole path AFTER it is written and checked already now.
|
|
716
|
+
- The number of checks became 69. The key new one: the file handed to the person is decrypted back with its own
|
|
717
|
+
passphrase and carries the halves (version 3) — that is, the file really is a file, not a string of plausible
|
|
718
|
+
length.
|
|
719
|
+
|
|
720
|
+
## 0.7.0 — 2026-10-01
|
|
721
|
+
|
|
722
|
+
Network -> virtual machine -> driver.
|
|
723
|
+
|
|
724
|
+
- The SDK keeps no network table: the source of truth is the engine's registry (`CHAINS` in
|
|
725
|
+
`www/js/core/config.js`). `vmOfChain(chainId)` returns the network's `vm`, `legForChain(chainId)` — the leg driver
|
|
726
|
+
for it.
|
|
727
|
+
- A network without a driver is a NAMED refusal `not-implemented` with `{ surface: "legs", vm, implemented: [...] }`:
|
|
728
|
+
you can see which virtual machine is not supported and which are. "Silently assumed EVM" is forbidden, and this is
|
|
729
|
+
checked across the WHOLE registry: every network either gets a driver or is refused with the vm named. Right now the
|
|
730
|
+
registry has 11 networks: the EVM ones are supported, bitcoin/tron/solana are refused by name.
|
|
731
|
+
- An unknown network — a separate code `bad-input` (this is a data error, not an unfinished capability).
|
|
732
|
+
- In the swap state `counterparty.vm` is taken from the registry; for an unknown network — `"unknown"`, not the
|
|
733
|
+
network's name and not a guess "probably EVM".
|
|
734
|
+
- The number of checks became 63. Along the way: the SDK copy in the breaking runs gained the engine seam with
|
|
735
|
+
absolute paths (otherwise relative imports from the copy's directory do not find `www/js`) — exactly why the seam is
|
|
736
|
+
made as a single file.
|
|
737
|
+
|
|
738
|
+
## 0.6.0 — 2026-10-01
|
|
739
|
+
|
|
740
|
+
Swap state outwards and the start order. The first step of stage 2.
|
|
741
|
+
|
|
742
|
+
- `swaps.list()` and `swaps.get(id)` work: swaps are raised from the core's storage, that is, after a page reload, not
|
|
743
|
+
from memory. The engine's record is translated into contract names (`toState`): the direction, the amounts, the
|
|
744
|
+
steps, the deadlines, the counterparty's leg, the allowed actions, what is simulated, the terminal state.
|
|
745
|
+
- The steps in the contract begin with `awaiting_funding`, which is not in the engine (the engine considers the swap
|
|
746
|
+
already created from the `funded` step) — waiting for payment is a state the interface must show.
|
|
747
|
+
- The step gained a `failed` flag (the maker did not lock the XMR): the interface draws a failure, not "still running".
|
|
748
|
+
- `swaps.start(request)` holds the step order: guards → record and recovery file → the person's confirmation → order
|
|
749
|
+
and halves → server → watching the Monero address. It is checked that a guard's refusal stops the swap WITH ITS OWN
|
|
750
|
+
CODE, that before the refusal the file is not shown to the person, that without the file handler the swap does not
|
|
751
|
+
start at all (`bad-input`), and that on a clean guard report the start reaches exactly the record-creation step,
|
|
752
|
+
naming the unclosed step (`not-implemented`, `step: "create-swap"`, stage 2).
|
|
753
|
+
- What is NOT in this release, and this is named: creating the swap record, the order and its halves, sending to the
|
|
754
|
+
server and the watch — they require a browser wallet and are done in stage 2 in full.
|
|
755
|
+
- The number of checks became 57.
|
|
756
|
+
|
|
757
|
+
## 0.5.0 — 2026-10-01
|
|
758
|
+
|
|
759
|
+
Storages — outside, like everything else. The first DOM seams of the engine were removed.
|
|
760
|
+
|
|
761
|
+
- `www/js/core/store.js` (the state of unfinished swaps) and `www/js/evm/auth.js` (the agent session) accept a storage
|
|
762
|
+
adapter (`get`/`set`/`remove`). By default — web storage, if it is present in the environment, otherwise process
|
|
763
|
+
memory: the module must load both in Node and in the SDK, which promises "no DOM". Silently not saving is not
|
|
764
|
+
allowed — an unfinished swap would be lost after a reload.
|
|
765
|
+
- `www/js/evm/session.js`: subscribing to window events (`pageshow`, `visibilitychange`) is done only where there is
|
|
766
|
+
a window. Previously it stood without an environment check, and the module failed on load in Node — that is, it
|
|
767
|
+
broke everything that pulls it in.
|
|
768
|
+
- The `storage` option in the SDK is no longer a promise in the types: the storage brought by the interface becomes
|
|
769
|
+
the core's storage (both swaps and the agent session).
|
|
770
|
+
- The number of checks became 44: the core's storages load in Node without DOM, the state is saved to the substituted
|
|
771
|
+
storage, clearing it wipes it, an incomplete adapter is rejected, a storage refusal on write does not break the
|
|
772
|
+
engine, and the storage from the SDK's options reaches the core.
|
|
773
|
+
|
|
774
|
+
## 0.4.0 — 2026-10-01
|
|
775
|
+
|
|
776
|
+
Balances and the route-provider registry.
|
|
777
|
+
|
|
778
|
+
- `wallet.balances(tokens?)` works: the native coin via `eth_getBalance`, tokens via `balanceOf` in `eth_call`. The
|
|
779
|
+
selector `0x70a08231` is not "from memory": it is checked against the node's registry (`rfq/evm.mjs`) by a check,
|
|
780
|
+
and an SDK copy with a substituted selector reddens it. `decimals` are taken from the registry; the chain's answer
|
|
781
|
+
carries only the number in minimal units.
|
|
782
|
+
- An unread balance comes as `null`, not zero (zero would look like an empty wallet). On a foreign network the balance
|
|
783
|
+
is not read at all: the code `wrong-chain`. Without a wallet — `wallet-not-connected`, an unknown token in the list
|
|
784
|
+
— `bad-input`.
|
|
785
|
+
- **The route-provider registry** in the EVM driver: `routeProviders()` / `routeProviderFor(id)`, a requirement for a
|
|
786
|
+
provider — `id`, `kind`, `plan(...)`. One is declared today: `declared` (our declared routes; execution — stage 3).
|
|
787
|
+
An external aggregator (1inch and the like) is added by a file and a line in the registry, and the pre-signature
|
|
788
|
+
guards stay shared by all providers.
|
|
789
|
+
- The order of preference comes through options (`routing: { prefer: [...] }`) and is checked against the registry: an
|
|
790
|
+
unknown identifier — `bad-input`, not "we'll try and see". One provider beating another is set explicitly, not by
|
|
791
|
+
whoever answered over the network first.
|
|
792
|
+
- The number of checks became 38 (13 breaking runs): reading balances with a substituted provider, the address word
|
|
793
|
+
in the calldata, refusals on a foreign network and an unknown token, the completeness of the provider registry, a
|
|
794
|
+
refusal to an unknown provider, and a breaking run on a half-added aggregator.
|
|
795
|
+
|
|
796
|
+
## 0.3.0 — 2026-10-01
|
|
797
|
+
|
|
798
|
+
Live indicative quotes: `quotes.watch` works.
|
|
799
|
+
|
|
800
|
+
- The SDK's own quote client (the package must work without `www/js`): parsing the book in three envelope forms, the
|
|
801
|
+
row's two rates, zero as "no direction", the lifetime from its own mark, snapping the size to the provider's grid
|
|
802
|
+
down. For now the demo screen computes with its own copy: it is a test bench.
|
|
803
|
+
- `quotes.watch({ direction, token, amount }, onSnapshot)` — the subscription; the last one to unsubscribe stops the
|
|
804
|
+
polling. `quotes.snapshot()` returns the last snapshot without waiting.
|
|
805
|
+
- The snapshot was extended: `size`, `unit`, `sizeCode` (`bad-size` | `below-min` | `above-max` | `no-offer`), `seq`,
|
|
806
|
+
`offers`, `refused` (the codes `malformed` | `disabled` | `stale`), `references`, `ok`, `error.code`. A connection
|
|
807
|
+
refusal is not replaced with past numbers: `ok` is cleared, `error.code` names the reason.
|
|
808
|
+
- `config.limits.quotePollMs` and `httpTimeoutMs` are taken from the engine's settings, not from numbers in the SDK.
|
|
809
|
+
- The number of checks became 25 (9 breaking runs), among the new ones: the book's rules and the derivation of numbers
|
|
810
|
+
on fixtures, a removed division-by-zero guard (the copy goes red), a removed quote-lifetime check (the copy goes
|
|
811
|
+
red), the absence of a market named by a code, not zero.
|
|
812
|
+
|
|
813
|
+
## 0.2.0 — 2026-10-01
|
|
814
|
+
|
|
815
|
+
The settlement network is named by the pair "network + virtual machine", the wallet is connected by an adapter, and
|
|
816
|
+
the swap state by a leg role. This is preparation for Tron and Solana (the first expected extensions) without
|
|
817
|
+
rewriting the core.
|
|
818
|
+
|
|
819
|
+
**Breaking changes (before 1.0 we bump the minor):**
|
|
820
|
+
|
|
821
|
+
- `evmNetwork: "arbitrum-sepolia"` → `settlement: { chain: "arbitrum-sepolia", vm?: "evm" }`. `vm` may be omitted —
|
|
822
|
+
it is taken from the engine's network registry, but a foreign one cannot be substituted: a network that has no leg
|
|
823
|
+
driver answers `not-implemented` with the fields `{ surface: "settlement.vm", vm, implemented }` instead of being
|
|
824
|
+
silently assumed EVM.
|
|
825
|
+
- `wallet.use(eip1193Provider)` → `wallet.use(adapter)`. A ready-made EVM adapter: `evmWallet(provider)` from
|
|
826
|
+
`@ninsei-labs/sdk/adapters`. A bare provider is no longer accepted: substituting the adapter with magic means
|
|
827
|
+
hiding from the interface what exactly it connected. Another VM's adapter will be a separate export of the same
|
|
828
|
+
directory.
|
|
829
|
+
- In the swap state the field `evm: { escrow, status, txs }` → `counterparty: { chain, vm, leg, status, txs }`. The
|
|
830
|
+
word "escrow" is about the EVM implementation, not the role: Tron and Solana will have their contract, Bitcoin — a
|
|
831
|
+
script.
|
|
832
|
+
- `sdk.config.evmNetwork` → `sdk.config.settlement.chain` (+ `sdk.config.settlement.vm`); every network in
|
|
833
|
+
`sdk.config.chains` gained a `vm` field.
|
|
834
|
+
|
|
835
|
+
**New:**
|
|
836
|
+
|
|
837
|
+
- The leg driver registry `sdk/src/legs/` (`vm → driver`) and a requirement for a driver: `vm`, `walletChainId`,
|
|
838
|
+
`sameChain`. Adding Tron or Solana — put a file next to it and register it; the quotes, the Monero leg and the state
|
|
839
|
+
machine do not change.
|
|
840
|
+
- The `sdk/src/adapters/` directory — the only place allowed to know about the environment: `evmWallet` (EIP-1193),
|
|
841
|
+
`localStorageAdapter`, `memoryAdapter`.
|
|
842
|
+
- `wallet.driver()` in the contract: what the adapter signs and reads the chain with (for EVM — the provider). Needed
|
|
843
|
+
by the leg driver; by the interface — only if it wants to hand it to its own extension.
|
|
844
|
+
|
|
845
|
+
**Checks:** 18 (was 10), of which 7 breaking runs. Added: the completeness of the driver registry, a refusal of a
|
|
846
|
+
network without a driver with the vm named, a breaking run on a half-added driver, connecting an adapter of any origin
|
|
847
|
+
by interface, a refusal of a foreign-vm adapter, a refusal to a bare provider.
|
|
848
|
+
|
|
849
|
+
## 0.1.0 — 2026-10-01
|
|
850
|
+
|
|
851
|
+
The first release: the contract and the skeleton.
|
|
852
|
+
|
|
853
|
+
- `index.d.ts` — the API contract: options, configuration, wallet, quotes, checks, swap, recovery, sandbox, code
|
|
854
|
+
tables. This is what the interface can lay out screens against.
|
|
855
|
+
- Implemented and works: `config` (networks, tokens, limits, codes), `wallet` (an EIP-1193 provider from outside),
|
|
856
|
+
`preflight` (the engine's guards, codes instead of text), `recovery.open`, `sim` (sandbox scenarios and clock).
|
|
857
|
+
- Declared, but answering with the code `not-implemented`: `quotes.firm`, `swaps.*` — these are stages 2 and 3: they
|
|
858
|
+
require the quote engine and the swap orchestration to move under `sdk/` (right now they live in `www/js` and are
|
|
859
|
+
loaded by relative paths through `sdk/src/engine.mjs`).
|
|
860
|
+
- The invariants are checked by machine (`tools/check-sdk.mjs`): the SDK loads in bare Node without DOM, its exported
|
|
861
|
+
surface matches `index.d.ts`, the check codes cover all the engine's guard kinds, and the SDK code has no
|
|
862
|
+
user-facing strings.
|
|
863
|
+
|
|
864
|
+
## 0.0.1 — 2026-10-01
|
|
865
|
+
|
|
866
|
+
- The `sdk/` directory, created at the interface's request (issue #32).
|