@ninsei-labs/sdk 0.51.2 → 0.53.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 +252 -0
- package/README.md +76 -3
- package/engine/atomic/order-worker.js +29 -2
- package/engine/atomic/order.js +24 -0
- package/engine/core/config.js +16 -29
- package/engine/core/store.js +60 -12
- package/engine/evm/factory.js +9 -2
- package/engine/evm/funding.js +65 -18
- package/engine/monero/arrivalRead.js +3 -22
- package/engine/monero/halvesSweep.js +5 -1
- package/engine/monero/scan.js +144 -33
- package/index.d.ts +35 -21
- package/package.json +23 -3
- package/src/adapters/monero-wallet.mjs +13 -6
- package/src/adapters/monero.mjs +5 -7
- package/src/codes.mjs +4 -0
- package/src/config.mjs +6 -3
- package/src/decline.mjs +6 -1
- package/src/feeEstimate.mjs +20 -13
- package/src/index.mjs +5 -4
- package/src/legs/index.mjs +8 -0
- package/src/legs/profiles.mjs +51 -0
- package/src/lock.mjs +4 -6
- package/src/order.mjs +53 -7
- package/src/public/networks.d.ts +14 -1
- package/src/public/recovery.d.ts +46 -8
- package/src/public/recovery.mjs +5 -0
- package/src/public/xmr.d.ts +26 -8
- package/src/quotes.mjs +165 -2
- package/src/recovery-key.mjs +203 -54
- package/src/recovery.mjs +9 -1
- package/src/swap-flow.mjs +3 -2
- package/src/swaps.mjs +204 -79
- package/src/sweep-fees.mjs +32 -0
- package/src/sweep-restore.mjs +30 -9
- package/src/sweep.mjs +28 -5
- package/src/version.mjs +1 -1
- package/src/xmr-arrival.mjs +76 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## 0.53.0 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- A failed `rescanBlockchain` is no longer discarded: `sweep.restore` reports `scanComplete: false` with
|
|
6
|
+
`scanIncompleteReason: "rescan-failed"`, even when the wallet and node heights are equal. A wallet already at the
|
|
7
|
+
tip cannot turn a failed rescan into a successful no-op sync.
|
|
8
|
+
- A `funding-pending` start keeps the sent transaction hash and the predicted escrow on the swap record. A retry of
|
|
9
|
+
the same quote reads that receipt and continues to `xmr-incoming` when it is funded, or returns
|
|
10
|
+
`funding-pending` again; it never builds a second side or sends a second deposit.
|
|
11
|
+
- `scanRange` treats a block without its decoded `json`, a BUSY block, a transaction body count below the wrapper
|
|
12
|
+
count and an extra transaction as unreadable: the block is named in `missingHeights`, not counted as empty or as
|
|
13
|
+
money. Transaction results are mapped back by requested hash, so a partial answer cannot pass as complete.
|
|
14
|
+
- The recovery-key store distinguishes "nothing stored" from "could not read": a throwing read is
|
|
15
|
+
`storage-unavailable { step: "read" }`, corrupt or unknown JSON is `storage-unavailable { step: "parse" }`, and a
|
|
16
|
+
failed write is `storage-unavailable { step: "write" }`. A corrupt record is no longer silently replaced by the
|
|
17
|
+
next start.
|
|
18
|
+
- A new `swaps.start` uses a per-order salt by default: the order's own salt is the v2 recovery salt, and a missing
|
|
19
|
+
salt is drawn before the order context is built. A v1 recovery index is used only when the caller names one
|
|
20
|
+
explicitly, for restoring an old order.
|
|
21
|
+
- The counterparty's view material is normalised once for the guard and the builder: conflicting `viewHalf` /
|
|
22
|
+
`viewHalfClaimer` or `viewPoint` / `viewPub` aliases are refused, the point is derived from the half when only the
|
|
23
|
+
half is present, and a half whose derived point differs from the declared point is refused before funding.
|
|
24
|
+
- `quotes.sale` reads the `{ request: {...} }` envelope as the request, so a claimer named inside it is bound to the
|
|
25
|
+
signed quote. The requested XMR is compared with the signed `xmrAmount`, and a returned order is compared field by
|
|
26
|
+
field with the signed terms instead of only with the derived `context`.
|
|
27
|
+
- The order worker now compares the proven secp256k1 point with the quote's `commitHalfClaimer`, not only the DLEQ
|
|
28
|
+
equation and the ed25519 point. `fundOrder` also refuses a signed quote whose `commitHalfClaimer` or
|
|
29
|
+
`edPointClaimer` differs from the verified order terms, and `swaps.start` checks the same pair before the lock.
|
|
30
|
+
A proof for one half can no longer be funded beside a quote that commits to another.
|
|
31
|
+
- The default stagenet node is `https://xmr-stagenet.ninsei.finance`, operated by Ninsei. The old
|
|
32
|
+
`xmr.arrakisswap.trade` address is retired.
|
|
33
|
+
- `sweep.fromOrder` keeps the order's XMR amount in the recovery file. A found order carries the amount the
|
|
34
|
+
provider signed in atomic units; the file now records it in whole XMR, so a withdrawal restored from that file
|
|
35
|
+
checks the arrival against the signed figure instead of a missing or shown amount.
|
|
36
|
+
- `check:monero-live` starts against a real `monerod` in regtest: it refuses a non-regtest nettype, generates a
|
|
37
|
+
block range to an SDK-built address, runs `scanRange` over the range, and checks the daemon's `get_fee_estimate`
|
|
38
|
+
shape. It runs in the nightly workflow, not on every PR.
|
|
39
|
+
- `check:types-consumer` compiles a minimal TypeScript consumer with `tsc --noEmit` against the package's own
|
|
40
|
+
exports map. It imports the root, `./xmr` and `./recovery`, assigns the public interfaces and calls the declared
|
|
41
|
+
entry points; a declaration that parses but cannot be consumed reddens here.
|
|
42
|
+
- `check-packed-consumer` packs the package, installs that tarball into an empty project and imports every public
|
|
43
|
+
subpath, then checks the engine's relative assets. It catches a package that reads as complete but fails at the
|
|
44
|
+
consumer's first import.
|
|
45
|
+
- `check-quote-binding` mutates every signed quote field one at a time: each must change the EIP-712 digest, each
|
|
46
|
+
must be named when absent, and the signature bytes must stay outside the digest.
|
|
47
|
+
- `check-anvil-fork` now covers the refund and decline paths, a claim by a relayer with a non-zero `claimTip`, a
|
|
48
|
+
wrong half, `markReady` from a non-locker and a reused salt. The refund must publish the locker half; the decline
|
|
49
|
+
must publish none.
|
|
50
|
+
- `check-atomic-flow` calls `runAtomicOrder` itself with stand-in worker and funding seams: it proves the sequence
|
|
51
|
+
reaches funding once, carries the salt, both commitments and the view point, returns the stand-in half and joint
|
|
52
|
+
address, and refuses a rejected counterparty before funding.
|
|
53
|
+
- Pull-request CI now runs the complete registered check battery, not only the individually named steps. Storage,
|
|
54
|
+
scan-range, funding receipt, sweep window/order/adapter, claim-tip, claim-sender, quote-rank, provider, compose,
|
|
55
|
+
entries, version and settlement-profiles were registered in `check.mjs` but ran only in the release workflow.
|
|
56
|
+
- `check-live-contract` asks the configured RPC for the chain id, the factory and cashier code, and the
|
|
57
|
+
implementation code hash. It is the cheapest check against the deployed contract.
|
|
58
|
+
- `check-anvil-fork` forks Arbitrum Sepolia, registers a provider in the deployed registry, signs a quote locally,
|
|
59
|
+
runs `createOrderAndFund`, `markReady` and `claim`, and checks the event, escrow roles, revealed half and
|
|
60
|
+
dump/rewind repeat. Both checks run in the regular CI; Anvil is pinned to the same Foundry version as the
|
|
61
|
+
contract repository.
|
|
62
|
+
- `check:dex` runs the CoW, Kyber, UniswapX and Uniswap Relay agreement vectors (real order signatures, EIP-712
|
|
63
|
+
digests, calldata and refusal tables) on every pull request; they used to run only in the release workflow.
|
|
64
|
+
- `check-anvil-uniswap` forks Ethereum mainnet and executes the SDK's UniversalRouter calldata against the real
|
|
65
|
+
USDC/WETH pool, asserting native ETH reaches the recipient. It caught that Anvil's default account 2 has an
|
|
66
|
+
EIP-7702 delegation, so the check uses a plain EOA as the recipient.
|
|
67
|
+
- `check-contract-vectors` pins three half commitments, the `OrderCreated` topic and both factory selectors against
|
|
68
|
+
values produced by Foundry `cast`, not by the SDK's own curve and hash code. A copy that hashes the half number
|
|
69
|
+
instead of its secp256k1 point reddens the check.
|
|
70
|
+
- `check-order-proof` also exercises `orderStep`: a foreign commitment, a foreign point and a missing commitment are
|
|
71
|
+
refused before funding, not only by the builder in isolation.
|
|
72
|
+
- Pull-request CI now runs the guards that were previously release-only: sale authentication, backup confirmation,
|
|
73
|
+
the one-key view half, arrival consensus, the recorded arrival fixture, order proof binding, both recovery-key
|
|
74
|
+
paths and recovery file opening. `check-ci-scripts` also refuses a `check:*` script that no workflow names, with a
|
|
75
|
+
breaking run; a guard that no pull request executes no longer counts as coverage.
|
|
76
|
+
- `recovery.open` parses the envelope before handing it to `restoreFromFile`, which takes the object. Every valid
|
|
77
|
+
file used to fail as `recovery-failed { stage: "restore" }`; a non-JSON file now fails at its own `parse` stage,
|
|
78
|
+
and a valid file opens from both text and `{ contents }`.
|
|
79
|
+
- The `types` group parses each declaration file with Node's TypeScript parser, not only its text. A missing comment
|
|
80
|
+
opener in `index.d.ts` is now a named syntax failure; that opener is restored.
|
|
81
|
+
- `spendKeysFromRevealedHalf` reduces the revealed word modulo the secp256k1 order before adding it to our half. The
|
|
82
|
+
escrow accepts `h + n` in `claim`/`refund`; that alias is the same key on secp256k1 but not on edwards25519, so
|
|
83
|
+
reducing it makes such a swap withdrawable instead of refused as `revealedHalf`.
|
|
84
|
+
- The per-swap recovery file start path is gone. It was assembled before funding and could not carry the escrow
|
|
85
|
+
address, so `sweep.plan` on that file reported `revealed-half` missing. Every start now requires the one recovery
|
|
86
|
+
key; the key is shown and confirmed before the lock. `StartRequest.onRecoveryFile` is removed.
|
|
87
|
+
- `moneroFeeEstimate` reads the daemon's real units: `fee` and `fees` are atomic units per byte, and
|
|
88
|
+
`quantization_mask` is kept as `quantizationMask`. `feeForBytes` multiplies by the size instead of dividing by
|
|
89
|
+
1024, so a 1024-byte transaction at 20000/byte is 20,480,000, not 20,000. The old `feePerKB`/`medianFeePerKB`
|
|
90
|
+
fields were never sent by a daemon and are gone.
|
|
91
|
+
- `scanRange` no longer reports a partial range as complete. A block that failed, a BUSY transaction answer, and a
|
|
92
|
+
response missing a requested transaction hash now appear in `missingHeights` with `complete: false` and
|
|
93
|
+
`incompleteReason: "blocks-unreadable"`; the per-address sums are still returned.
|
|
94
|
+
- A height lock is judged at the witnessing node's tip, not at the block that mined the transfer. A transfer mined at
|
|
95
|
+
100 with `unlock_time` 101 is no longer `xmr-locked` at tip 120; confirmations still use the inclusion height.
|
|
96
|
+
`matchTransaction` takes `tipHeight` (and `scanRange` passes its top); the duplicate unlock rule in
|
|
97
|
+
`engine/monero/arrivalRead.js` now re-exports `engine/monero/unlock.js`.
|
|
98
|
+
- A split sweep reports the SUM of every signed transaction's fee, not the first one. Both wallet adapters use one
|
|
99
|
+
helper; `sweep.sign` subtracts the total from the remainder, and `sweep.relay` already returns every txid. One
|
|
100
|
+
unreadable fee makes the total unknown (`null`), never a partial figure.
|
|
101
|
+
- A failed settlement block-time read no longer drops every order already matched by `findSwaps`. The order is kept
|
|
102
|
+
with `blockTime: null` and `startHeightReason: "block-time-read"`; the other orders keep their heights.
|
|
103
|
+
- The retired backend no longer decides a swap. A service watch row is an observation only: `receivedXmr > 0` is
|
|
104
|
+
returned as `seen: false` with `next: "verify"` and never signs `markReady`, and a service `declined` status is
|
|
105
|
+
no longer written into the record as a settlement. The escrow's `Declined` event remains the decline source.
|
|
106
|
+
- Storage failures are named, not silent. A failed read, corrupt JSON, a failed write and a failed remove raise
|
|
107
|
+
`storage-unavailable` with `step` (`read`, `parse`, `write`, `serialize`, `remove`); only a missing value returns
|
|
108
|
+
the empty state. A second client with a different adapter is refused with `step: "conflict"` instead of taking
|
|
109
|
+
over the first client's records.
|
|
110
|
+
- The SDK names its own line on every body route it sends to a node: `quotes.firm`, `quotes.sale` and the funding
|
|
111
|
+
mark carry `sdkVersion` from the package (`src/version.mjs`). A default node no longer refuses a current SDK with
|
|
112
|
+
`422 sdk-version-absent`; the caller cannot claim a different line.
|
|
113
|
+
- Funding is no longer reported as complete from a sent transaction. `fundOrder` accepts only a status-1 receipt
|
|
114
|
+
with an `OrderCreated` from the factory that was called, for the predicted escrow; a reverted receipt is
|
|
115
|
+
`contract-reverted`, and a missing receipt, a missing event or a foreign event returns `pending` with a reason.
|
|
116
|
+
`swaps.start` refuses with `funding-pending` and does not save an escrow or retire a recovery number.
|
|
117
|
+
- The Monero wallet adapter accepts the standard networks `mainnet`, `stagenet`, `testnet` and `fakechain`, not only
|
|
118
|
+
the names the demo's engine registry declares. A mainnet withdrawal is no longer refused as `bad-input` before the
|
|
119
|
+
library is called; the address-shape check still rejects an address from another network.
|
|
120
|
+
- The recovery key has a salt-based v2 derivation: `ninsei-taker-v2|<chain>|<factory>|taker|<salt>`. The client draws
|
|
121
|
+
a fresh public salt per order; the salt is already in `OrderCreated`, so recovery costs one derivation per order
|
|
122
|
+
instead of an index scan. No index is reserved, and the same words on a second device or at another factory do not
|
|
123
|
+
repeat a half. Existing v1 orders still recover through `ninsei-taker-v1|<chain>|<index>`.
|
|
124
|
+
- `swaps.start` accepts the client-owned `recoverySalt` and requires it to equal `order.salt`; without it the v1 index
|
|
125
|
+
path is unchanged. `newRecoverySalt` and `recoverySaltOf` are exported for the quote client.
|
|
126
|
+
- `sweep.restore` reports `scanComplete: false` with `scanIncompleteReason: "sync-failed"`, `"behind-node"` or
|
|
127
|
+
`"no-heights"` when the wallet did not reach the node's height. A zero balance after a failed sync is no longer
|
|
128
|
+
reported as an empty wallet.
|
|
129
|
+
- `findSwaps` no longer stops after five empty numbers. It searches the whole allocator range by default and
|
|
130
|
+
returns `complete: false` with `searchedUpTo` when a caller supplies a smaller `maxIndex`; `gap` is retained
|
|
131
|
+
but ignored.
|
|
132
|
+
- `quotes.sale` authenticates the ticket instead of trusting its `signed` flag: the EIP-712 signature, expiry,
|
|
133
|
+
registry provider, interface allow-list, factory policy and the sale claimer's denylist are checked before the
|
|
134
|
+
ticket is returned.
|
|
135
|
+
- On the key path, `start` requires the counterparty's view half to be the one derived from the words, chain
|
|
136
|
+
and index. A foreign or missing half is refused before funding, so the funded address can always be rebuilt.
|
|
137
|
+
- A recovery key in storage is no longer treated as backed up. `backupConfirmed` is recorded only after
|
|
138
|
+
`onRecoveryKey` returns true; a declined or failed confirmation leaves the key unconfirmed, so the next start
|
|
139
|
+
asks again and cannot fund the escrow.
|
|
140
|
+
- The XMR arrival check requires witnesses to agree on the whole observation, not only on a block hash, and
|
|
141
|
+
verifies every decoded amount against its Pedersen commitment (`mask*G + amount*H == outPk`). One altered node
|
|
142
|
+
can no longer turn a short payment into `arrived`; an unbacked amount is `xmr-nodes-disagree`. The compact
|
|
143
|
+
RingCT encoding (types 4-6) derives the mask, and a recorded stagenet type-6 transfer is pinned as a fixture.
|
|
144
|
+
- The counterparty DLEQ proof is bound to the signed `commitHalfClaimer`: the proven secp256k1 point must match the
|
|
145
|
+
commitment the contract compares, and a side without a commitment is refused as input.
|
|
146
|
+
|
|
147
|
+
## 0.52.0 — 2026-10-26
|
|
148
|
+
|
|
149
|
+
THE NEXT CONTRACT VERSION IS WIRED IN.
|
|
150
|
+
|
|
151
|
+
- `arbitrum-sepolia` moves to factory `0x69b5a6878b542960af4d239ecc60ad1c9e3e6932`, cashier
|
|
152
|
+
`0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0`, registry
|
|
153
|
+
`0x4707a6684b654fbd74372ec624efc0f825af0537` and implementation code hash
|
|
154
|
+
`0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4`
|
|
155
|
+
(deployment block `323014007`).
|
|
156
|
+
- `OrderCreated` now carries `xmrAmount`. `findSwaps` reads it from the current event and reports
|
|
157
|
+
`xmrAmountReason: "event"`; orders from the previous event keep the clone word-18 fallback.
|
|
158
|
+
- The previous event topic is still accepted, so an old factory remains searchable.
|
|
159
|
+
|
|
1
160
|
## 0.51.2 — 2026-10-22
|
|
2
161
|
|
|
3
162
|
- The withdrawal window falls back to the full scan when `scanTxs` or the balance read fails, and reports
|
|
@@ -402,6 +561,99 @@ THE ENGINE MIRROR IS SYNCED AGAIN AFTER THE DEMO'S BUNDLE REBUILD.
|
|
|
402
561
|
|
|
403
562
|
## Unreleased
|
|
404
563
|
|
|
564
|
+
- `check:monero-live` starts against a real `monerod` in regtest: it refuses a non-regtest nettype, generates a
|
|
565
|
+
block range to an SDK-built address, runs `scanRange` over the range, and checks the daemon's `get_fee_estimate`
|
|
566
|
+
shape. It runs in the nightly workflow, not on every PR.
|
|
567
|
+
- `check:types-consumer` compiles a minimal TypeScript consumer with `tsc --noEmit` against the package's own
|
|
568
|
+
exports map. It imports the root, `./xmr` and `./recovery`, assigns the public interfaces and calls the declared
|
|
569
|
+
entry points; a declaration that parses but cannot be consumed reddens here.
|
|
570
|
+
- `check-packed-consumer` packs the package, installs that tarball into an empty project and imports every public
|
|
571
|
+
subpath, then checks the engine's relative assets. It catches a package that reads as complete but fails at the
|
|
572
|
+
consumer's first import.
|
|
573
|
+
- `check-quote-binding` mutates every signed quote field one at a time: each must change the EIP-712 digest, each
|
|
574
|
+
must be named when absent, and the signature bytes must stay outside the digest.
|
|
575
|
+
- `check-anvil-fork` now covers the refund and decline paths, a claim by a relayer with a non-zero `claimTip`, a
|
|
576
|
+
wrong half, `markReady` from a non-locker and a reused salt. The refund must publish the locker half; the decline
|
|
577
|
+
must publish none.
|
|
578
|
+
- `check-atomic-flow` calls `runAtomicOrder` itself with stand-in worker and funding seams: it proves the sequence
|
|
579
|
+
reaches funding once, carries the salt, both commitments and the view point, returns the stand-in half and joint
|
|
580
|
+
address, and refuses a rejected counterparty before funding.
|
|
581
|
+
- Pull-request CI now runs the complete registered check battery, not only the individually named steps. Storage,
|
|
582
|
+
scan-range, funding receipt, sweep window/order/adapter, claim-tip, claim-sender, quote-rank, provider, compose,
|
|
583
|
+
entries, version and settlement-profiles were registered in `check.mjs` but ran only in the release workflow.
|
|
584
|
+
- `check-live-contract` asks the configured RPC for the chain id, the factory and cashier code, and the
|
|
585
|
+
implementation code hash. It is the cheapest check against the deployed contract.
|
|
586
|
+
- `check-anvil-fork` forks Arbitrum Sepolia, registers a provider in the deployed registry, signs a quote locally,
|
|
587
|
+
runs `createOrderAndFund`, `markReady` and `claim`, and checks the event, escrow roles, revealed half and
|
|
588
|
+
dump/rewind repeat. Both checks run in the regular CI; Anvil is pinned to the same Foundry version as the
|
|
589
|
+
contract repository.
|
|
590
|
+
- `check:dex` runs the CoW, Kyber, UniswapX and Uniswap Relay agreement vectors (real order signatures, EIP-712
|
|
591
|
+
digests, calldata and refusal tables) on every pull request; they used to run only in the release workflow.
|
|
592
|
+
- `check-anvil-uniswap` forks Ethereum mainnet and executes the SDK's UniversalRouter calldata against the real
|
|
593
|
+
USDC/WETH pool, asserting native ETH reaches the recipient. It caught that Anvil's default account 2 has an
|
|
594
|
+
EIP-7702 delegation, so the check uses a plain EOA as the recipient.
|
|
595
|
+
- `check-contract-vectors` pins three half commitments, the `OrderCreated` topic and both factory selectors against
|
|
596
|
+
values produced by Foundry `cast`, not by the SDK's own curve and hash code. A copy that hashes the half number
|
|
597
|
+
instead of its secp256k1 point reddens the check.
|
|
598
|
+
- `check-order-proof` also exercises `orderStep`: a foreign commitment, a foreign point and a missing commitment are
|
|
599
|
+
refused before funding, not only by the builder in isolation.
|
|
600
|
+
- Pull-request CI now runs the guards that were previously release-only: sale authentication, backup confirmation,
|
|
601
|
+
the one-key view half, arrival consensus, the recorded arrival fixture, order proof binding, both recovery-key
|
|
602
|
+
paths and recovery file opening. `check-ci-scripts` also refuses a `check:*` script that no workflow names, with a
|
|
603
|
+
breaking run; a guard that no pull request executes no longer counts as coverage.
|
|
604
|
+
- `recovery.open` parses the envelope before handing it to `restoreFromFile`, which takes the object. Every valid
|
|
605
|
+
file used to fail as `recovery-failed { stage: "restore" }`; a non-JSON file now fails at its own `parse` stage,
|
|
606
|
+
and a valid file opens from both text and `{ contents }`.
|
|
607
|
+
- The `types` group parses each declaration file with Node's TypeScript parser, not only its text. A missing comment
|
|
608
|
+
opener in `index.d.ts` is now a named syntax failure; that opener is restored.
|
|
609
|
+
- `spendKeysFromRevealedHalf` reduces the revealed word modulo the secp256k1 order before adding it to our half. The
|
|
610
|
+
escrow accepts `h + n` in `claim`/`refund`; that alias is the same key on secp256k1 but not on edwards25519, so
|
|
611
|
+
reducing it makes such a swap withdrawable instead of refused as `revealedHalf`.
|
|
612
|
+
- The per-swap recovery file start path is gone. It was assembled before funding and could not carry the escrow
|
|
613
|
+
address, so `sweep.plan` on that file reported `revealed-half` missing. Every start now requires the one recovery
|
|
614
|
+
key; the key is shown and confirmed before the lock. `StartRequest.onRecoveryFile` is removed.
|
|
615
|
+
- `moneroFeeEstimate` reads the daemon's real units: `fee` and `fees` are atomic units per byte, and
|
|
616
|
+
`quantization_mask` is kept as `quantizationMask`. `feeForBytes` multiplies by the size instead of dividing by
|
|
617
|
+
1024, so a 1024-byte transaction at 20000/byte is 20,480,000, not 20,000. The old `feePerKB`/`medianFeePerKB`
|
|
618
|
+
fields were never sent by a daemon and are gone.
|
|
619
|
+
- `scanRange` no longer reports a partial range as complete. A block that failed, a BUSY transaction answer, and a
|
|
620
|
+
response missing a requested transaction hash now appear in `missingHeights` with `complete: false` and
|
|
621
|
+
`incompleteReason: "blocks-unreadable"`; the per-address sums are still returned.
|
|
622
|
+
- A height lock is judged at the witnessing node's tip, not at the block that mined the transfer. A transfer mined at
|
|
623
|
+
100 with `unlock_time` 101 is no longer `xmr-locked` at tip 120; confirmations still use the inclusion height.
|
|
624
|
+
`matchTransaction` takes `tipHeight` (and `scanRange` passes its top); the duplicate unlock rule in
|
|
625
|
+
`engine/monero/arrivalRead.js` now re-exports `engine/monero/unlock.js`.
|
|
626
|
+
- A split sweep reports the SUM of every signed transaction's fee, not the first one. Both wallet adapters use one
|
|
627
|
+
helper; `sweep.sign` subtracts the total from the remainder, and `sweep.relay` already returns every txid. One
|
|
628
|
+
unreadable fee makes the total unknown (`null`), never a partial figure.
|
|
629
|
+
- A failed settlement block-time read no longer drops every order already matched by `findSwaps`. The order is kept
|
|
630
|
+
with `blockTime: null` and `startHeightReason: "block-time-read"`; the other orders keep their heights.
|
|
631
|
+
- The retired backend no longer decides a swap. A service watch row is an observation only: `receivedXmr > 0` is
|
|
632
|
+
returned as `seen: false` with `next: "verify"` and never signs `markReady`, and a service `declined` status is
|
|
633
|
+
no longer written into the record as a settlement. The escrow's `Declined` event remains the decline source.
|
|
634
|
+
- Storage failures are named, not silent. A failed read, corrupt JSON, a failed write and a failed remove raise
|
|
635
|
+
`storage-unavailable` with `step` (`read`, `parse`, `write`, `serialize`, `remove`); only a missing value returns
|
|
636
|
+
the empty state. A second client with a different adapter is refused with `step: "conflict"` instead of taking
|
|
637
|
+
over the first client's records.
|
|
638
|
+
- The SDK names its own line on every body route it sends to a node: `quotes.firm`, `quotes.sale` and the funding
|
|
639
|
+
mark carry `sdkVersion` from the package (`src/version.mjs`). A default node no longer refuses a current SDK with
|
|
640
|
+
`422 sdk-version-absent`; the caller cannot claim a different line.
|
|
641
|
+
- Funding is no longer reported as complete from a sent transaction. `fundOrder` accepts only a status-1 receipt
|
|
642
|
+
with an `OrderCreated` from the factory that was called, for the predicted escrow; a reverted receipt is
|
|
643
|
+
`contract-reverted`, and a missing receipt, a missing event or a foreign event returns `pending` with a reason.
|
|
644
|
+
`swaps.start` refuses with `funding-pending` and does not save an escrow or retire a recovery number.
|
|
645
|
+
- The recovery key has a salt-based v2 derivation: `ninsei-taker-v2|<chain>|<factory>|taker|<salt>`. The client draws
|
|
646
|
+
a fresh public salt per order; the salt is already in `OrderCreated`, so recovery costs one derivation per order
|
|
647
|
+
instead of an index scan. No index is reserved, and the same words on a second device or at another factory do not
|
|
648
|
+
repeat a half. Existing v1 orders still recover through `ninsei-taker-v1|<chain>|<index>`.
|
|
649
|
+
- `swaps.start` accepts the client-owned `recoverySalt` and requires it to equal `order.salt`; without it the v1 index
|
|
650
|
+
path is unchanged. `newRecoverySalt` and `recoverySaltOf` are exported for the quote client.
|
|
651
|
+
- `sweep.restore` reports `scanComplete: false` with `scanIncompleteReason: "sync-failed"`, `"behind-node"` or
|
|
652
|
+
`"no-heights"` when the wallet did not reach the node's height. A zero balance after a failed sync is no longer
|
|
653
|
+
reported as an empty wallet.
|
|
654
|
+
- `findSwaps` no longer stops after five empty numbers. It searches the whole allocator range by default and
|
|
655
|
+
returns `complete: false` with `searchedUpTo` when a caller supplies a smaller `maxIndex`; `gap` is retained
|
|
656
|
+
but ignored.
|
|
405
657
|
THE ENGINE MIRROR (`engine/**`) CROSSES OVER IN ENGLISH, WITHOUT THE OLD BRAND AND WITHOUT A HARDCODED DOMAIN.
|
|
406
658
|
All 45 modules are copied byte-for-byte from the demo repository's `sdk/engine` (`tools/build-sdk-engine.mjs`):
|
|
407
659
|
comments are concise technical English and message strings are translated one to one, `Arrakis` is renamed to
|
package/README.md
CHANGED
|
@@ -26,6 +26,26 @@ machine). The wallet arrives as an EIP-1193 provider, storage as an adapter, and
|
|
|
26
26
|
What is not ready names itself with the code `not-implemented` with the fields `surface` and `stage`: an empty
|
|
27
27
|
value that looks like data is more dangerous than a refusal.
|
|
28
28
|
|
|
29
|
+
## Settlement legs and VM profiles
|
|
30
|
+
|
|
31
|
+
The settlement side is a driver, not a chain id switch. A VM profile says what that side looks like. The profile is
|
|
32
|
+
metadata: it does not make a network runnable, and it does not carry a finality claim - finality is a chain policy,
|
|
33
|
+
not a VM shape.
|
|
34
|
+
|
|
35
|
+
| VM | Address form | Settlement | Ref | Driver today |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| `evm` | `hex-20` | contract | address | yes |
|
|
38
|
+
| `tron` | `base58check` | contract | address | profile only |
|
|
39
|
+
| `solana` | `base58` | program | PDA | profile only |
|
|
40
|
+
| `bitcoin` | `base58check-or-bech32-or-bech32m` | UTXO script | script | profile only |
|
|
41
|
+
|
|
42
|
+
The planned order is Tron, Solana, then Bitcoin. Dogecoin is not on that list and has no profile. A profile is not
|
|
43
|
+
a driver: `LEGS` still holds only `evm`, so Tron, Solana and Bitcoin are refused by name until their own work lands.
|
|
44
|
+
|
|
45
|
+
`chainConfig(...).settlementProfile` returns the profile for the network, or `null` for an unknown VM. The check
|
|
46
|
+
`tools/check.mjs --only=settlement-profiles` guards the table, the absence of Dogecoin and the absence of a finality
|
|
47
|
+
claim. A future driver adds its own settlement methods; the profile alone never authorizes a fund, claim or refund.
|
|
48
|
+
|
|
29
49
|
## The contract
|
|
30
50
|
|
|
31
51
|
`index.d.ts` is the single source of truth about the API shape, and this is checked: `tools/check-sdk.mjs`
|
|
@@ -157,7 +177,13 @@ range is not aligned: it is the provider's refusal, and it arrives as a code. In
|
|
|
157
177
|
|
|
158
178
|
## Found orders and the paying token
|
|
159
179
|
|
|
160
|
-
`findSwaps` returns the orders whose commitment came from the recovery key.
|
|
180
|
+
`findSwaps` returns the orders whose commitment came from the recovery key. A salt-based order is found from the
|
|
181
|
+
public `salt` in its own `OrderCreated`: the binding is `ninsei-taker-v2|<chain>|<factory>|taker|<salt>`, so the same
|
|
182
|
+
words on another device or at another factory produce a different half and no number is reserved. The result says
|
|
183
|
+
`derivation: "salt-v2"` and leaves `index` null. Existing v1 orders stay on `ninsei-taker-v1|<chain>|<index>` and are
|
|
184
|
+
still found by the index path; a v2 result needs no range scan. For a v1 fallback, discovery does **not** stop after a
|
|
185
|
+
run of empty numbers: the allocator's whole range is searched by default, a smaller caller-supplied `maxIndex` returns
|
|
186
|
+
`complete: false` with `searchedUpTo`, and `gap` is ignored. Each found order reports:
|
|
161
187
|
|
|
162
188
|
- `amount` — the escrow's **native** amount in wei;
|
|
163
189
|
- `settlementToken: "native"` — what the escrow receives;
|
|
@@ -173,15 +199,62 @@ When a token is needed, the order can be joined with an off-chain quote record b
|
|
|
173
199
|
would have to put the token into the signed quote and the clone (and therefore a new order format and factory); only
|
|
174
200
|
then can `findSwaps` return it from the chain and set `payTokenReason` to something other than `not-on-chain`.
|
|
175
201
|
|
|
202
|
+
`swaps.start` takes the client-owned `recoverySalt` when the signed quote already carries that salt; it must equal
|
|
203
|
+
`order.salt`. `newRecoverySalt()` draws a fresh 32-byte value, and `takerHalvesV2()` derives the halves without
|
|
204
|
+
touching the index allocator.
|
|
205
|
+
|
|
176
206
|
The found order also carries the Monero start height and the reason it is absent. `no-block-time`,
|
|
177
207
|
`monero-nodes-unreachable` and `monero-network-mismatch` are different answers: a missing settlement timestamp is not
|
|
178
208
|
a dead node, and a node on another network is not an absent reader. A page that starts swaps without the SDK's start
|
|
179
209
|
path can call `recoveryIndexTaken` from `./recovery` before using a number; it checks the factory's `OrderCreated`
|
|
180
210
|
logs from the registry's deployment block.
|
|
181
211
|
|
|
182
|
-
|
|
212
|
+
A revealed half published as `h + n` (the alias the escrow accepts) is reduced modulo the secp256k1 order before the
|
|
213
|
+
sweep key check, so it rebuilds the same key and remains withdrawable.
|
|
214
|
+
|
|
215
|
+
`recovery.open` accepts the file as text or `{ contents }`: the envelope is parsed at the boundary, so a valid file
|
|
216
|
+
opens and a non-JSON one fails with its own `parse` stage.
|
|
217
|
+
|
|
218
|
+
The per-swap recovery file start path is gone: a start requires the one recovery key, shown and confirmed before
|
|
219
|
+
funding, and a file that could not name the escrow is no longer produced.
|
|
220
|
+
|
|
221
|
+
`moneroFeeEstimate` reports the daemon's per-byte `fee`/`fees` and its `quantization_mask`; `feeForBytes` multiplies
|
|
222
|
+
the price by the transaction size (1024 bytes at 20000/byte is 20,480,000).
|
|
223
|
+
|
|
224
|
+
A range scan says whether it was complete: `scanRange` returns `complete: false`, `missingHeights` and
|
|
225
|
+
`incompleteReason: "blocks-unreadable"` when a block, a BUSY transaction answer or a missing transaction hash leaves
|
|
226
|
+
the range partial.
|
|
227
|
+
|
|
228
|
+
A height lock is judged at the witnessing node's tip, not at the block that mined the transfer; confirmations still
|
|
229
|
+
use the inclusion height.
|
|
230
|
+
|
|
231
|
+
A split sweep reports the sum of every signed transaction's fee, not only the first; one unreadable fee makes the
|
|
232
|
+
total unknown.
|
|
233
|
+
|
|
234
|
+
A failed settlement block-time read keeps the matched order: `findSwaps` returns `blockTime: null` with
|
|
235
|
+
`startHeightReason: "block-time-read"` instead of throwing away every row.
|
|
236
|
+
|
|
237
|
+
The retired backend no longer decides a swap: its watch row may observe money, but it cannot declare readiness or
|
|
238
|
+
a decline; the client's two-node check and the escrow's own `Declined` event do.
|
|
239
|
+
|
|
240
|
+
Storage is a refusal, not a silent empty state: a failed read, corrupt JSON, a failed write or remove is
|
|
241
|
+
`storage-unavailable` with a step, and a second client with another adapter is refused rather than taking over the
|
|
242
|
+
first client's records.
|
|
243
|
+
|
|
244
|
+
Every body route the SDK sends carries `sdkVersion` from the package (`quotes.firm`, `quotes.sale` and the funding
|
|
245
|
+
mark). The caller cannot override it with a different line.
|
|
246
|
+
|
|
247
|
+
Funding is reported from the receipt, not from the send. A status-0 receipt is `contract-reverted`; a missing
|
|
248
|
+
receipt, a receipt without `OrderCreated`, or an event from another factory is `funding-pending` with the transaction
|
|
249
|
+
hash, and `swaps.start` does not save an escrow or retire a recovery number in that state.
|
|
250
|
+
|
|
251
|
+
The wallet adapter accepts the standard Monero network names (`mainnet`, `stagenet`, `testnet`, `fakechain`) and
|
|
252
|
+
still checks the address shape, so a mainnet withdrawal is not refused because the demo registry lists only
|
|
253
|
+
stagenet. The withdrawal window retries a `429`, a timeout or a `5xx` before it gives up. If the window still cannot be read,
|
|
183
254
|
`restoreWallet` falls back to the full scan and reports `window.fallback: true` with `fallbackWhy: "window-read"` and
|
|
184
|
-
the node's status; a withdrawal is not refused because the node was busy.
|
|
255
|
+
the node's status; a withdrawal is not refused because the node was busy. When the wallet cannot reach the node's
|
|
256
|
+
height, the result says `scanComplete: false` with `scanIncompleteReason: "sync-failed"`, `"behind-node"` or
|
|
257
|
+
`"no-heights"`; a zero balance in that state is not an empty wallet.
|
|
185
258
|
|
|
186
259
|
## Extending to other networks (Tron, Solana and beyond)
|
|
187
260
|
|
|
@@ -13,6 +13,20 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const builder = createOrderBuilder({ ed25519, secp256k1, keccak256, sha3_512, randomBytes });
|
|
15
15
|
|
|
16
|
+
// THE COMMITMENT THE CONTRACT WILL COMPARE, DERIVED FROM THE PROVEN POINT. The worker's vendored builder can check
|
|
17
|
+
// the DLEQ equation, but its verifySide did not compare the proven secp256k1 point with the quote's commitment.
|
|
18
|
+
// engine/atomic/order.js carries that comparison; this worker runs the prebuilt bundle, so the same comparison is
|
|
19
|
+
// made here from the bundle's own secp256k1 and keccak256. A proof that is valid for another point must not build a
|
|
20
|
+
// side that funding then signs.
|
|
21
|
+
const commitmentOfPoint = (value) => {
|
|
22
|
+
try {
|
|
23
|
+
const point = secp256k1.Point.fromHex(String(value || "").replace(/^0x/i, ""));
|
|
24
|
+
const xy = point.toBytes(false).slice(1);
|
|
25
|
+
return "0x" + Array.from(keccak256(xy), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
};
|
|
28
|
+
const low160 = (value) => String(value || "").replace(/^0x/i, "").toLowerCase().slice(-40);
|
|
29
|
+
|
|
16
30
|
// Ключи шифрования построенных сторон: приватный ключ по своему публичному. Наружу приватные не уходят -
|
|
17
31
|
// вскрытие посылки происходит здесь.
|
|
18
32
|
//
|
|
@@ -111,14 +125,27 @@ self.onmessage = async (event) => {
|
|
|
111
125
|
});
|
|
112
126
|
} else if (type === "verify") {
|
|
113
127
|
say("verify", "проверяю доказательство контрагента");
|
|
114
|
-
const side = { proof: counterparty.proof, pub: { ed: counterparty.edPub } };
|
|
128
|
+
const side = { proof: counterparty.proof, pub: { ed: counterparty.edPub }, commitHalfClaimer: counterparty.commitHalfClaimer || null };
|
|
115
129
|
// Вскрываем тем ключом, на который посылка была зашифрована в этом контексте.
|
|
116
130
|
const recipient = sealedToPub.get(String(orderContext));
|
|
117
131
|
const openWith = recipient ? encPrivByPub.get(recipient) : null;
|
|
118
132
|
if (sealedBytes && !openWith) {
|
|
119
133
|
throw new Error("нет ключа, которым вскрывать посылку: не было шифрования в этом контексте ордера");
|
|
120
134
|
}
|
|
121
|
-
|
|
135
|
+
let verdict = builder.verifySide(side, orderContext, sealedBytes || null, openWith);
|
|
136
|
+
// THE PROOF IS ABOUT THE COMMITMENT IN THE QUOTE, not only about two points that stand for one scalar. The
|
|
137
|
+
// contract pays against commitHalfClaimer; without this comparison a counterparty proves one point and funds
|
|
138
|
+
// another, and the half revealed at claim time does not open the joint address.
|
|
139
|
+
if (verdict && verdict.ok === true) {
|
|
140
|
+
if (!counterparty.commitHalfClaimer) {
|
|
141
|
+
verdict = { ok: false, reason: "the counterparty side carries no commitHalfClaimer" };
|
|
142
|
+
} else {
|
|
143
|
+
const proven = commitmentOfPoint(counterparty.proof && counterparty.proof.XA);
|
|
144
|
+
if (!proven || low160(proven) !== low160(counterparty.commitHalfClaimer)) {
|
|
145
|
+
verdict = { ok: false, reason: "the proven secp256k1 point does not match commitHalfClaimer: " + (proven || "unparsed") + " vs " + counterparty.commitHalfClaimer };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
122
149
|
self.postMessage({ id, kind: "done", result: verdict });
|
|
123
150
|
} else {
|
|
124
151
|
self.postMessage({ id, kind: "error", message: "неизвестный запрос воркеру: " + String(type) });
|
package/engine/atomic/order.js
CHANGED
|
@@ -60,6 +60,30 @@ export function createOrderBuilder({ ed25519, secp256k1, keccak256, sha3_512, ra
|
|
|
60
60
|
if (side.pub && side.pub.ed && String(side.proof.XB).toLowerCase() !== String(side.pub.ed).toLowerCase()) {
|
|
61
61
|
return { ok: false, reason: "the proven ed25519 point does not match the declared one: " + side.proof.XB + " vs " + side.pub.ed };
|
|
62
62
|
}
|
|
63
|
+
// THE COMMITMENT IS PART OF THE PROOF'S MEANING. The contract does not compare halves to a hash of a number:
|
|
64
|
+
// it compares `half*G` (through ecrecover) with `commitHalfClaimer`. A proof confirms only that XA and XB stand
|
|
65
|
+
// for one scalar; without this comparison a counterparty could prove XA, put another point in the quote, and
|
|
66
|
+
// the contract would reject the half at claim time - after the other side has paid.
|
|
67
|
+
//
|
|
68
|
+
// `proof.XA` is the compressed secp256k1 point of the half. The commitment is keccak256 of the UNCOMPRESSED
|
|
69
|
+
// 64-byte coordinates, and the contract compares only its low 160 bits (`uint160`), so the comparison is made
|
|
70
|
+
// on the same 20 bytes.
|
|
71
|
+
if (side.commitHalfClaimer) {
|
|
72
|
+
let provenCommit = null;
|
|
73
|
+
try {
|
|
74
|
+
const point = secp256k1.Point.fromHex(String(side.proof.XA).replace(/^0x/, ""));
|
|
75
|
+
const xy = point.toBytes(false).slice(1);
|
|
76
|
+
provenCommit = "0x" + Array.from(keccak256(xy), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
77
|
+
} catch { provenCommit = null; }
|
|
78
|
+
const low160 = (v) => String(v || "").replace(/^0x/, "").toLowerCase().slice(-40);
|
|
79
|
+
if (!provenCommit || low160(provenCommit) !== low160(side.commitHalfClaimer)) {
|
|
80
|
+
return { ok: false, reason: "the proven secp256k1 point does not match commitHalfClaimer: " + (provenCommit || "unparsed") + " vs " + side.commitHalfClaimer };
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// A REAL COUNTERPARTY MUST CARRY THE COMMITMENT. Accepting a proof without it leaves the link between the
|
|
84
|
+
// proven half and the contract unchecked; the stand-in path supplies its own commitment in order.mjs.
|
|
85
|
+
return { ok: false, reason: "the counterparty side carries no commitHalfClaimer" };
|
|
86
|
+
}
|
|
63
87
|
if (sealedPayloadBytes && ownEncPriv) {
|
|
64
88
|
const opened = enc.open(enc.unpackSealed(sealedPayloadBytes), ownEncPriv, side.pub.ed);
|
|
65
89
|
if (!opened.ok) return { ok: false, reason: "the counterparty envelope does not confirm the half: " + opened.reason };
|
package/engine/core/config.js
CHANGED
|
@@ -363,35 +363,22 @@ export const CHAINS = [
|
|
|
363
363
|
explorerName: "arbiscan (sepolia)",
|
|
364
364
|
gasUsd: 0.01,
|
|
365
365
|
routeType: "atomic",
|
|
366
|
-
// THE DEMO'S MAIN NETWORK: the Ninsei escrow factory and the interface's fee cashier live here
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
// PREVIOUS (ceiling 3%, no early refund): 0xEa619bDC78989627093B7830990367B2ECE52CE5 (height 314309038,
|
|
383
|
-
// code 8143 bytes). PREVIOUS live (router with a 10% ceiling): 0x9f856cd9c14fd9493961473b804039b09a4012ef
|
|
384
|
-
// (height 313597131, code 8723 bytes). PREVIOUS before it (factory without a fee):
|
|
385
|
-
// 0xcb54af9ec11344d448c7900ccb08530a23783259 (height 312222006, code 6035 bytes), and earlier ones still.
|
|
386
|
-
// mode: "live" - the form sends REAL transactions: order creation TOGETHER WITH THE MONEY in one transaction
|
|
387
|
-
// (createOrderAndFund; the escrow has no separate funding function - an order is born funded, see FACTORY_METHODS
|
|
388
|
-
// in www/js/evm/factory.js and ESCROW_METHODS in escrow.js). The old lock(sealedHalf) and hashlock are gone: the
|
|
389
|
-
// half is not sent but revealed by presentation. Switching back to "simulated" is one edit, to show a demo with
|
|
390
|
-
// no money movement. cashier: the INTERFACE's fee cashier, named in quotes and now deployed (the second CREATE of
|
|
391
|
-
// the same pass). implementationCodeHash: the escrow implementation the factory carries, checked against the chain.
|
|
392
|
-
// deploymentBlock: the height the factory was created at; a log search for its orders starts here, not at zero,
|
|
393
|
-
// because public RPCs cap a log query at about 10M blocks and would refuse a scan from genesis.
|
|
394
|
-
escrow: { address: "0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F", mode: "live", deploymentBlock: 321430239, cashier: "0x1123A3875775Da25FA181Af0d91EB278E94a5dfb", implementationCodeHash: "0xf97af2b4eed8c646d82bce001e52ff03c065d43127073f8ee52271b65c35d659" },
|
|
366
|
+
// THE DEMO'S MAIN NETWORK: the Ninsei escrow factory and the interface's fee cashier live here. The next
|
|
367
|
+
// contract version (contracts-testnet-2026.10.26) is a FACT, not an intention:
|
|
368
|
+
// factory 0x69b5a6878b542960af4d239ecc60ad1c9e3e6932 (height 323014007,
|
|
369
|
+
// transaction 0xbe8244da0cb1429edc92d18c7c8b5ba1c6b17c42a5806609d930a6496e918408);
|
|
370
|
+
// cashier 0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0;
|
|
371
|
+
// registry 0x4707a6684b654fbd74372ec624efc0f825af0537 (this release deploys it);
|
|
372
|
+
// implementation 0x2E0214c6242fB794a4cC861D609Ecee544735170, codehash
|
|
373
|
+
// 0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4.
|
|
374
|
+
// `OrderCreated` now carries xmrAmount; the SDK reads it from the event and keeps the clone fallback for the
|
|
375
|
+
// previous event. PREVIOUS factory before this version: 0x3eE25f12758ec28e5b3045d16132D89Cd0bf341F
|
|
376
|
+
// (height 321430239, cashier 0x1123A3875775Da25FA181Af0d91EB278E94a5dfb).
|
|
377
|
+
// mode: "live" - order creation TOGETHER WITH THE MONEY in one transaction (createOrderAndFund). cashier: the
|
|
378
|
+
// INTERFACE's fee cashier, named in quotes. implementationCodeHash: the escrow implementation the factory
|
|
379
|
+
// carries, checked against the chain. deploymentBlock: the height the factory was created at; a log search for
|
|
380
|
+
// its orders starts here, not at zero, because public RPCs cap a log query at about 10M blocks.
|
|
381
|
+
escrow: { address: "0x69b5a6878b542960af4d239ecc60ad1c9e3e6932", mode: "live", deploymentBlock: 323014007, cashier: "0x05b7433f66f2b75cc7ece9ec00af0fb810f9c9e0", implementationCodeHash: "0xc6cde36301b6551a6ca8bd2c47fbd5ffd01ffcdf22d2f6909be2b3b2dcf360b4" },
|
|
395
382
|
tokens: [
|
|
396
383
|
// Test USDC (Circle): decimals 6 and symbol "USDC" verified on chain by the same node.
|
|
397
384
|
{ symbol: "USDC", address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", decimals: 6, usd: 1, cls: "ar-token-usdc" },
|
package/engine/core/store.js
CHANGED
|
@@ -46,11 +46,40 @@ const webStorage = (name) => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
let adapter = webStorage("localStorage") || memoryStorage();
|
|
49
|
+
// A SECOND CLIENT WITH ANOTHER STORE USED TO TAKE OVER THE FIRST ONE'S RECORDS: the adapter was module-wide, so
|
|
50
|
+
// A listed B's swaps and A's forget deleted them. The SDK binds one explicit store to the process; a different
|
|
51
|
+
// one is refused instead of silently switching under a live client. `setStorage` remains the test/setup reset.
|
|
52
|
+
let bound = null;
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
export function setStorage(next) {
|
|
54
|
+
const checkAdapter = (next) => {
|
|
52
55
|
const ok = next && typeof next.get === "function" && typeof next.set === "function" && typeof next.remove === "function";
|
|
53
56
|
if (!ok) throw new Error("storage must support get/set/remove");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const storageFailure = (step, cause) => {
|
|
60
|
+
const error = new Error("storage-unavailable");
|
|
61
|
+
error.code = "storage-unavailable";
|
|
62
|
+
error.params = { step };
|
|
63
|
+
if (cause) error.cause = cause;
|
|
64
|
+
return error;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Swap the storage for setup and tests. It also releases the exclusive binding, so a later client may bind. */
|
|
68
|
+
export function setStorage(next) {
|
|
69
|
+
checkAdapter(next);
|
|
70
|
+
adapter = next;
|
|
71
|
+
bound = null;
|
|
72
|
+
return adapter;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Bind the store of a client instance. A second, different store is a refusal: records and recovery keys are
|
|
77
|
+
* addressed through one adapter, and switching it under a live client loses the first client's records.
|
|
78
|
+
*/
|
|
79
|
+
export function bindStorage(next) {
|
|
80
|
+
checkAdapter(next);
|
|
81
|
+
if (bound && bound !== next) throw storageFailure("conflict");
|
|
82
|
+
bound = next;
|
|
54
83
|
adapter = next;
|
|
55
84
|
return adapter;
|
|
56
85
|
}
|
|
@@ -59,14 +88,23 @@ export function setStorage(next) {
|
|
|
59
88
|
export const storage = () => adapter;
|
|
60
89
|
|
|
61
90
|
export function loadState() {
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = adapter.get(APP.storageKey);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw storageFailure("read", error);
|
|
96
|
+
}
|
|
97
|
+
// NOTHING STORED IS NOT AN UNREADABLE STORE. Only a missing value returns the empty state; a value that does
|
|
98
|
+
// not parse is a refusal, so the next write cannot silently save over a corrupt record.
|
|
99
|
+
if (raw === null || raw === undefined || raw === "") return structuredClone(EMPTY);
|
|
100
|
+
let parsed;
|
|
62
101
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { ...structuredClone(EMPTY), ...parsed, settings: { ...EMPTY.settings, ...(parsed.settings || {}) } };
|
|
67
|
-
} catch {
|
|
68
|
-
return structuredClone(EMPTY);
|
|
102
|
+
parsed = JSON.parse(raw);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw storageFailure("parse", error);
|
|
69
105
|
}
|
|
106
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw storageFailure("parse");
|
|
107
|
+
return { ...structuredClone(EMPTY), ...parsed, settings: { ...EMPTY.settings, ...(parsed.settings || {}) } };
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
// BigInt IS NOT SERIALISED IN JSON - and the key halves are BigInt. JSON.stringify FAILS on them
|
|
@@ -75,10 +113,16 @@ export function loadState() {
|
|
|
75
113
|
// We turn BigInt into a hex string: the same meaning, but serialisable, and the secret is not lost.
|
|
76
114
|
const bigintSafe = (key, value) => (typeof value === "bigint" ? "0x" + value.toString(16) : value);
|
|
77
115
|
export function saveState(state) {
|
|
116
|
+
let text;
|
|
78
117
|
try {
|
|
79
|
-
|
|
80
|
-
} catch (
|
|
81
|
-
|
|
118
|
+
text = JSON.stringify(state, bigintSafe);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw storageFailure("serialize", error);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
adapter.set(APP.storageKey, text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw storageFailure("write", error);
|
|
82
126
|
}
|
|
83
127
|
return state;
|
|
84
128
|
}
|
|
@@ -91,7 +135,11 @@ export function update(mutator) {
|
|
|
91
135
|
}
|
|
92
136
|
|
|
93
137
|
export function reset() {
|
|
94
|
-
|
|
138
|
+
try {
|
|
139
|
+
adapter.remove(APP.storageKey);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw storageFailure("remove", error);
|
|
142
|
+
}
|
|
95
143
|
}
|
|
96
144
|
|
|
97
145
|
export function exportState() {
|