@ninsei-labs/sdk 0.39.0 → 0.39.2
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 +115 -0
- package/README.md +21 -0
- package/engine/core/chainSource.js +306 -306
- package/engine/core/config.js +29 -2
- package/engine/core/sellWindow.js +43 -43
- package/engine/core/swap.js +1153 -1153
- package/engine/evm/claimGas.js +88 -87
- package/engine/evm/dex.js +836 -836
- package/engine/evm/factory.js +106 -0
- package/engine/evm/funding.js +11 -11
- package/engine/evm/gasReserve.js +88 -88
- package/engine/evm/prices.js +179 -179
- package/engine/recovery/recoveryFile.js +4 -1
- package/engine/sdk/bridge.js +7 -2
- package/index.d.ts +18 -2
- package/package.json +4 -4
- package/src/actions.mjs +72 -72
- package/src/adapters/node-test-evm-wallet.mjs +345 -345
- package/src/config.mjs +9 -6
- package/src/engine.mjs +53 -53
- package/src/index.mjs +10 -1
- package/src/levelsWalkSpec.mjs +21 -4
- package/src/lock.mjs +140 -140
- package/src/order.mjs +212 -212
- package/src/quotes.mjs +495 -465
- package/src/swaps.mjs +935 -935
- package/src/sweep.mjs +29 -4
- package/src/xmr-arrival.mjs +289 -289
package/src/config.mjs
CHANGED
|
@@ -218,12 +218,15 @@ export function buildConfig(options) {
|
|
|
218
218
|
if (!known.includes(id)) fail("bad-input", { field: "routing.prefer", unknown: id, known });
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// MONERO NETWORKS THE CORE KNOWS. The engine's registry declares only the
|
|
222
|
-
// (XMR_NETWORKS
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
221
|
+
// MONERO NETWORKS THE CORE KNOWS. The engine's registry declares only the ones the demo is configured for
|
|
222
|
+
// (XMR_NETWORKS). But the SDK contract (index.d.ts, xmrNetwork) names the STANDARD ones - mainnet/stagenet/
|
|
223
|
+
// testnet - and that is not about the demo: the withdrawal page takes the network FROM THE NODE, and on a
|
|
224
|
+
// production network its network is mainnet, not a typo.
|
|
225
|
+
// REGTEST JOINS THEM BY ITS OFFICIAL NAME. monerod in regtest mode reports its network as `fakechain`, and the
|
|
226
|
+
// withdrawal page builds the facade from the network the node hands it - so the CI contour (a regtest node)
|
|
227
|
+
// hands out `fakechain`. Without that name the core refuses its OWN recovery file and the withdrawal never
|
|
228
|
+
// starts. This is a KNOWN name, not a wildcard: anything else is still a refusal with the known list below.
|
|
229
|
+
const STANDARD_XMR_NETWORKS = ["mainnet", "stagenet", "testnet", "fakechain"];
|
|
227
230
|
const xmrNetworks = [...new Set([...Object.values(engine.config.XMR_NETWORKS || {}), ...STANDARD_XMR_NETWORKS])];
|
|
228
231
|
if (!xmrNetworks.includes(options.xmrNetwork)) {
|
|
229
232
|
fail("bad-input", { field: "xmrNetwork", known: xmrNetworks });
|
package/src/engine.mjs
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
// THE ENGINE SEAM, NOW SELF-CONTAINED.
|
|
2
|
-
//
|
|
3
|
-
// The engine modules the core depends on are mirrored under sdk/engine/ - byte-for-byte copies kept in
|
|
4
|
-
// sync by tools/build-sdk-engine.mjs and guarded by tools/check-sdk-engine.mjs. This file is the ONLY
|
|
5
|
-
// place that knows where the engine lives; no other SDK module does.
|
|
6
|
-
//
|
|
7
|
-
// WHY A VERIFIED MIRROR, NOT A MOVE. The very same modules are loaded by the demo page in the browser
|
|
8
|
-
// (www/js), and the production web root is www/ alone - the sdk/ directory is not served there (see
|
|
9
|
-
// tools/serve.mjs). Lifting the files out of www/js would 404 every engine import on the live site, and
|
|
10
|
-
// dozens of repository checks read those sources by their www/js path. So the single source of truth
|
|
11
|
-
// stays www/js and the SDK carries a copy: any drift reddens check-sdk-engine, so it cannot diverge
|
|
12
|
-
// silently (the same rule build-sweep uses for the sweep/rfq copies).
|
|
13
|
-
//
|
|
14
|
-
// The mirror is generated - never edit sdk/engine/ by hand. Change www/js, then run:
|
|
15
|
-
// node tools/build-sdk-engine.mjs
|
|
16
|
-
export * as config from "../engine/core/config.js";
|
|
17
|
-
export * as guards from "../engine/core/preSignGuards.js";
|
|
18
|
-
export * as restore from "../engine/recovery/restore.js";
|
|
19
|
-
// STORES - THROUGH THIS SAME SEAM. Both modules take an adapter from outside (get/set/remove), and by default
|
|
20
|
-
// use web storage if it is present: without an adapter they run in memory rather than fail on a missing DOM.
|
|
21
|
-
export * as store from "../engine/core/store.js";
|
|
22
|
-
export * as auth from "../engine/evm/auth.js";
|
|
23
|
-
export * as swap from "../engine/core/swap.js";
|
|
24
|
-
export * as recoveryFile from "../engine/recovery/recoveryFile.js";
|
|
25
|
-
export * as halves from "../engine/atomic/halves.js";
|
|
26
|
-
export * as orderContext from "../engine/atomic/orderContext.js";
|
|
27
|
-
export * as order from "../engine/atomic/order.js";
|
|
28
|
-
// HALF TRANSFER: the byte format and opening are the very same ones the page worker uses
|
|
29
|
-
// (www/js/atomic/order-worker.js). The core keeps no copy of the format: the commitment is computed over those
|
|
30
|
-
// exact bytes, and a second copy would silently drift from the calldata.
|
|
31
|
-
export * as halfenc from "../engine/atomic/halfenc.js";
|
|
32
|
-
// The engine's EVM layer: talking to the chain (order terms, sending, reading) - through this seam.
|
|
33
|
-
export * as evm from "../engine/evm/index.js";
|
|
34
|
-
// THE DEAL ENGINE NO LONGER LIVES IN www/js (#32, wave 3): the order flow moved into the package
|
|
35
|
-
// (sdk/src/swap-flow.mjs) and is declared in sdk/index.d.ts. The page reaches it through the bridge, so the
|
|
36
|
-
// mirror no longer carries core/swap-flow.js - what stays in the seam is the ORDER CLIENT (the half worker),
|
|
37
|
-
// the ORDER QUOTE source and the shared CLAIM GAS LIMIT that the moved flow reads through this same seam.
|
|
38
|
-
export * as orderClient from "../engine/atomic/order-client.js";
|
|
39
|
-
export * as rfqSource from "../engine/core/rfqSource.js";
|
|
40
|
-
export * as claimGas from "../engine/evm/claimGas.js";
|
|
41
|
-
export * as escrow from "../engine/evm/escrow.js";
|
|
42
|
-
export * as halvesSweep from "../engine/monero/halvesSweep.js";
|
|
43
|
-
// MONERO ADDRESSES - THROUGH THE SAME MODULE THE DEMO USES. The format, the network prefixes and the address
|
|
44
|
-
// check live in www/js/monero/address.js (checkAddress: base58 + keccak256 + network), and the page uses it
|
|
45
|
-
// too. The wallet adapter needs exactly networkFromShape: its own address must belong to the network named by
|
|
46
|
-
// the core, and that is checked by the same code, not by a private prefix table.
|
|
47
|
-
export * as xmrAddress from "../engine/monero/address.js";
|
|
48
|
-
// THE unlock_time RULE AND THE BLOCK READER - FROM THE SAME MIRROR, NOT A COPY. A second copy of "the output
|
|
49
|
-
// is barred until a boundary" would be a second truth about money, so the rule lives under www/js
|
|
50
|
-
// (monero/unlock.js) together with the Monero block parser (monero/scan.js), and the SDK reads them through
|
|
51
|
-
// the engine mirror. check-sdk-engine reddens on any drift, so the two cannot diverge silently.
|
|
52
|
-
export * as xmrUnlock from "../engine/monero/unlock.js";
|
|
53
|
-
export * as xmrScan from "../engine/monero/scan.js";
|
|
1
|
+
// THE ENGINE SEAM, NOW SELF-CONTAINED.
|
|
2
|
+
//
|
|
3
|
+
// The engine modules the core depends on are mirrored under sdk/engine/ - byte-for-byte copies kept in
|
|
4
|
+
// sync by tools/build-sdk-engine.mjs and guarded by tools/check-sdk-engine.mjs. This file is the ONLY
|
|
5
|
+
// place that knows where the engine lives; no other SDK module does.
|
|
6
|
+
//
|
|
7
|
+
// WHY A VERIFIED MIRROR, NOT A MOVE. The very same modules are loaded by the demo page in the browser
|
|
8
|
+
// (www/js), and the production web root is www/ alone - the sdk/ directory is not served there (see
|
|
9
|
+
// tools/serve.mjs). Lifting the files out of www/js would 404 every engine import on the live site, and
|
|
10
|
+
// dozens of repository checks read those sources by their www/js path. So the single source of truth
|
|
11
|
+
// stays www/js and the SDK carries a copy: any drift reddens check-sdk-engine, so it cannot diverge
|
|
12
|
+
// silently (the same rule build-sweep uses for the sweep/rfq copies).
|
|
13
|
+
//
|
|
14
|
+
// The mirror is generated - never edit sdk/engine/ by hand. Change www/js, then run:
|
|
15
|
+
// node tools/build-sdk-engine.mjs
|
|
16
|
+
export * as config from "../engine/core/config.js";
|
|
17
|
+
export * as guards from "../engine/core/preSignGuards.js";
|
|
18
|
+
export * as restore from "../engine/recovery/restore.js";
|
|
19
|
+
// STORES - THROUGH THIS SAME SEAM. Both modules take an adapter from outside (get/set/remove), and by default
|
|
20
|
+
// use web storage if it is present: without an adapter they run in memory rather than fail on a missing DOM.
|
|
21
|
+
export * as store from "../engine/core/store.js";
|
|
22
|
+
export * as auth from "../engine/evm/auth.js";
|
|
23
|
+
export * as swap from "../engine/core/swap.js";
|
|
24
|
+
export * as recoveryFile from "../engine/recovery/recoveryFile.js";
|
|
25
|
+
export * as halves from "../engine/atomic/halves.js";
|
|
26
|
+
export * as orderContext from "../engine/atomic/orderContext.js";
|
|
27
|
+
export * as order from "../engine/atomic/order.js";
|
|
28
|
+
// HALF TRANSFER: the byte format and opening are the very same ones the page worker uses
|
|
29
|
+
// (www/js/atomic/order-worker.js). The core keeps no copy of the format: the commitment is computed over those
|
|
30
|
+
// exact bytes, and a second copy would silently drift from the calldata.
|
|
31
|
+
export * as halfenc from "../engine/atomic/halfenc.js";
|
|
32
|
+
// The engine's EVM layer: talking to the chain (order terms, sending, reading) - through this seam.
|
|
33
|
+
export * as evm from "../engine/evm/index.js";
|
|
34
|
+
// THE DEAL ENGINE NO LONGER LIVES IN www/js (#32, wave 3): the order flow moved into the package
|
|
35
|
+
// (sdk/src/swap-flow.mjs) and is declared in sdk/index.d.ts. The page reaches it through the bridge, so the
|
|
36
|
+
// mirror no longer carries core/swap-flow.js - what stays in the seam is the ORDER CLIENT (the half worker),
|
|
37
|
+
// the ORDER QUOTE source and the shared CLAIM GAS LIMIT that the moved flow reads through this same seam.
|
|
38
|
+
export * as orderClient from "../engine/atomic/order-client.js";
|
|
39
|
+
export * as rfqSource from "../engine/core/rfqSource.js";
|
|
40
|
+
export * as claimGas from "../engine/evm/claimGas.js";
|
|
41
|
+
export * as escrow from "../engine/evm/escrow.js";
|
|
42
|
+
export * as halvesSweep from "../engine/monero/halvesSweep.js";
|
|
43
|
+
// MONERO ADDRESSES - THROUGH THE SAME MODULE THE DEMO USES. The format, the network prefixes and the address
|
|
44
|
+
// check live in www/js/monero/address.js (checkAddress: base58 + keccak256 + network), and the page uses it
|
|
45
|
+
// too. The wallet adapter needs exactly networkFromShape: its own address must belong to the network named by
|
|
46
|
+
// the core, and that is checked by the same code, not by a private prefix table.
|
|
47
|
+
export * as xmrAddress from "../engine/monero/address.js";
|
|
48
|
+
// THE unlock_time RULE AND THE BLOCK READER - FROM THE SAME MIRROR, NOT A COPY. A second copy of "the output
|
|
49
|
+
// is barred until a boundary" would be a second truth about money, so the rule lives under www/js
|
|
50
|
+
// (monero/unlock.js) together with the Monero block parser (monero/scan.js), and the SDK reads them through
|
|
51
|
+
// the engine mirror. check-sdk-engine reddens on any drift, so the two cannot diverge silently.
|
|
52
|
+
export * as xmrUnlock from "../engine/monero/unlock.js";
|
|
53
|
+
export * as xmrScan from "../engine/monero/scan.js";
|
package/src/index.mjs
CHANGED
|
@@ -97,6 +97,14 @@ const normalizeOptions = (options) => {
|
|
|
97
97
|
if (o.knownFactoryCodes !== undefined && o.knownFactoryCodes !== null && !Array.isArray(o.knownFactoryCodes)) {
|
|
98
98
|
fail("bad-input", { field: "knownFactoryCodes" });
|
|
99
99
|
}
|
|
100
|
+
// THE INTERFACE'S ESCROW-IMPLEMENTATION POLICY (issue #114): an array of code hashes of known implementations
|
|
101
|
+
// of the escrow - the code every order's clone delegatecalls. The client checks the pair (implementation,
|
|
102
|
+
// implementationCodeHash) the factory carries, exactly as the factory does at creation; this list pins WHICH
|
|
103
|
+
// builds the interface accepts. Same rule as knownFactoryCodes: a POLICY list, empty/absent means "no policy"
|
|
104
|
+
// (then the pair is still read and the code at the implementation's address must be non-empty).
|
|
105
|
+
if (o.knownImplementationCodes !== undefined && o.knownImplementationCodes !== null && !Array.isArray(o.knownImplementationCodes)) {
|
|
106
|
+
fail("bad-input", { field: "knownImplementationCodes" });
|
|
107
|
+
}
|
|
100
108
|
// A PASS TO OUR SERVICE - A SEAM FROM OUTSIDE, like the wallet: only a wallet can sign the sign-in message
|
|
101
109
|
// (www/js/evm/auth.js, signIn), and requiring it of the core would mean opening windows and signing messages
|
|
102
110
|
// inside the SDK. The core receives a ready pass and presents it. A missing pass is NOT an option error: the
|
|
@@ -137,6 +145,7 @@ const normalizeOptions = (options) => {
|
|
|
137
145
|
evmCode: o.evmCode || null,
|
|
138
146
|
allowedProviders: o.allowedProviders === undefined ? null : o.allowedProviders,
|
|
139
147
|
knownFactoryCodes: o.knownFactoryCodes === undefined ? null : o.knownFactoryCodes,
|
|
148
|
+
knownImplementationCodes: o.knownImplementationCodes === undefined ? null : o.knownImplementationCodes,
|
|
140
149
|
serverAuth: o.serverAuth || null,
|
|
141
150
|
watch: o.watch || null,
|
|
142
151
|
createRecord: o.createRecord || null,
|
|
@@ -186,7 +195,7 @@ export function createNinsei(options) {
|
|
|
186
195
|
return (provider) => set.has(String(provider).toLowerCase());
|
|
187
196
|
})();
|
|
188
197
|
const quotes = createQuotes({ config, http, now: o.now || (() => Date.now()), evmCall: o.evmCall || null,
|
|
189
|
-
evmCode: o.evmCode || null, providerAllowed, knownFactoryCodes: o.knownFactoryCodes });
|
|
198
|
+
evmCode: o.evmCode || null, providerAllowed, knownFactoryCodes: o.knownFactoryCodes, knownImplementationCodes: o.knownImplementationCodes });
|
|
190
199
|
// THE SAME CHAIN-READ SEAM AS THE LOCK AND THE CORE: checking the slots and signing must go one way, otherwise
|
|
191
200
|
// part of the check goes past the wallet and part does not.
|
|
192
201
|
const actions = createActions({ call: o.evmCall });
|
package/src/levelsWalkSpec.mjs
CHANGED
|
@@ -141,11 +141,22 @@ function walkCostForXmr(levels, xmrAtomic) {
|
|
|
141
141
|
return costWei;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// THE SMALLEST TRADE (rule 3). It is the size of the FIRST level the maker SENT, and the node's own
|
|
145
|
+
// deductions (outstanding quotes, a balance cut) never change it: they take depth off the TOP, and a trade
|
|
146
|
+
// that was allowed when the set arrived must stay allowed after the top was eaten. If the minimum were read
|
|
147
|
+
// off the EFFECTIVE set, eating the first level whole would RAISE it to the next, larger level, and eating it
|
|
148
|
+
// in part would LOWER it below what the maker sent - both wrong. Callers pass the sent minimum as minAtomic;
|
|
149
|
+
// absent, the set's own first level is the minimum (the historical behaviour and what the vectors pin).
|
|
150
|
+
function minOf(levels, minAtomic) {
|
|
151
|
+
if (minAtomic === null || minAtomic === undefined) return toBigInt(levels[0].amount);
|
|
152
|
+
return toBigInt(minAtomic);
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
// DIRECTION 1: "I give/take ASSET - how much XMR".
|
|
145
156
|
// buy: assetWei - the asset the taker PAYS; the walk budget = assetWei - fee;
|
|
146
157
|
// sell: assetWei - the asset the taker RECEIVES; the walk budget = assetWei + fee (the set must yield that
|
|
147
158
|
// much so that AFTER the fee is taken out exactly assetWei is left in hand).
|
|
148
|
-
export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation = "ask" }) {
|
|
159
|
+
export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation = "ask", minAtomic = null }) {
|
|
149
160
|
const verdict = checkLevels(levels, orientation);
|
|
150
161
|
if (!verdict.ok) return { ok: false, reason: verdict.reason, role, index: verdict.index };
|
|
151
162
|
if (!verdict.enabled) return { ok: false, reason: "side-off", role };
|
|
@@ -157,7 +168,12 @@ export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation =
|
|
|
157
168
|
if (budget <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
|
158
169
|
const { fullCostWei } = sideSummary(levels);
|
|
159
170
|
if (budget > fullCostWei) return { ok: false, reason: "levels-exceed-side", role };
|
|
160
|
-
|
|
171
|
+
// THE SMALLEST TRADE, from the set AS THE MAKER SENT IT (rule 3): callers pass it in minAtomic and the
|
|
172
|
+
// node's deductions never change it. Absent, the set's own first level is used, as it always was.
|
|
173
|
+
const min = minOf(levels, minAtomic);
|
|
174
|
+
// Its cost ON THE EFFECTIVE SET: after deductions the smallest trade may reach into a later level, so the
|
|
175
|
+
// cost is walked, not read off one price.
|
|
176
|
+
const minCost = walkCostForXmr(levels, min);
|
|
161
177
|
if (budget < minCost) return { ok: false, reason: "levels-below-minimum", role };
|
|
162
178
|
const xmrAtomic = walkXmrForBudget(levels, budget);
|
|
163
179
|
return { ok: true, role, xmrAtomic, assetWei: asset };
|
|
@@ -166,7 +182,7 @@ export function xmrForAsset({ levels, role, feeWei = 0n, assetWei, orientation =
|
|
|
166
182
|
// DIRECTION 2: "I give/want XMR - how much ASSET".
|
|
167
183
|
// buy: xmrAtomic - how much XMR the taker WANTS; the asset to pay = the walk + fee;
|
|
168
184
|
// sell: xmrAtomic - how much XMR the taker GIVES; the asset to receive = the walk - fee.
|
|
169
|
-
export function assetForXmr({ levels, role, feeWei = 0n, xmrAtomic, orientation = "ask" }) {
|
|
185
|
+
export function assetForXmr({ levels, role, feeWei = 0n, xmrAtomic, orientation = "ask", minAtomic = null }) {
|
|
170
186
|
const verdict = checkLevels(levels, orientation);
|
|
171
187
|
if (!verdict.ok) return { ok: false, reason: verdict.reason, role, index: verdict.index };
|
|
172
188
|
if (!verdict.enabled) return { ok: false, reason: "side-off", role };
|
|
@@ -175,7 +191,8 @@ export function assetForXmr({ levels, role, feeWei = 0n, xmrAtomic, orientation
|
|
|
175
191
|
const fee = toBigInt(feeWei === undefined || feeWei === null ? 0n : feeWei);
|
|
176
192
|
if (want === null || fee === null || fee < 0n) return { ok: false, reason: "levels-not-integer", role };
|
|
177
193
|
if (want > verdict.totalAtomic) return { ok: false, reason: "levels-exceed-side", role };
|
|
178
|
-
|
|
194
|
+
// THE SMALLEST TRADE AS THE MAKER SENT IT, never the effective set's first level (rule 3): see minOf.
|
|
195
|
+
if (want < minOf(levels, minAtomic)) return { ok: false, reason: "levels-below-minimum", role };
|
|
179
196
|
const walkWei = walkCostForXmr(levels, want);
|
|
180
197
|
const assetWei = role === "buy" ? walkWei + fee : walkWei - fee;
|
|
181
198
|
if (assetWei <= 0n) return { ok: false, reason: "levels-fee-beyond-budget", role };
|
package/src/lock.mjs
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
// LOCKING THE FUNDS: TERMS FIRST, THEN SENDING.
|
|
2
|
-
//
|
|
3
|
-
// The first half of the step is here - assembling and CHECKING the order terms BEFORE anything goes on-chain. The
|
|
4
|
-
// rules are not ours: the contract sets them, and the engine already checks them (`www/js/evm/escrow.js`,
|
|
5
|
-
// `orderTerms`). The core will not hold a copy of the rules: a copy would drift from the contract right where the
|
|
6
|
-
// error shows up only as a rejected transaction.
|
|
7
|
-
//
|
|
8
|
-
// WHAT IS HERE: both the assembly of the terms and the sending itself - it goes through the engine's EVM layer
|
|
9
|
-
// (`fundOrder`), and the EVM wallet (the `send`/`receipt` role) comes from outside. What reads the chain is also a
|
|
10
|
-
// seam: by default the engine's read, but the caller can substitute its own (as in actions.mjs).
|
|
11
|
-
import { SdkError } from "./errors.mjs";
|
|
12
|
-
import * as engine from "./engine.mjs";
|
|
13
|
-
// THE DEAL FLOW IS A PACKAGE MODULE NOW (#32, wave 3): the registry address is read through it, exactly as
|
|
14
|
-
// the actions read the order flow.
|
|
15
|
-
import * as flow from "./swap-flow.mjs";
|
|
16
|
-
|
|
17
|
-
// THE ORDER'S WORD-ADDRESSES ARE BROUGHT TO 0x HERE, AT THE BOUNDARY, NOT AT EVERY CALLER.
|
|
18
|
-
//
|
|
19
|
-
// The engine returns the points and commitments WITHOUT the 0x prefix (that is how www/js/atomic/halves.js stores
|
|
20
|
-
// them), while the calldata encoder (www/js/evm/factory.js, word) REQUIRES the prefix and refuses "not a number
|
|
21
|
-
// and not hex" on 64 digits without one. The page normalises them on its side (sdk/src/swap-flow.mjs, with0x)
|
|
22
|
-
// for exactly this reason. The core must do the same ITSELF: otherwise every caller would repeat the
|
|
23
|
-
// normalisation, and one forgetful one would break the signature with the encoder's raw error. The list is
|
|
24
|
-
// exactly the fields encoded as bytes32 that do NOT enter the order context string (otherwise the normalisation
|
|
25
|
-
// would drift from the proof, which is bound to the context): two half commitments and three ed25519 points.
|
|
26
|
-
const HEX_POLICY_FIELDS = ["commitHalfLocker", "commitHalfClaimer", "edPointLocker", "edPointClaimer", "edViewPointLocker"];
|
|
27
|
-
const with0x = (v) => (typeof v === "string" && v && !/^0x/i.test(v) ? "0x" + v : v);
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The order terms in canonical form: points and commitments - with the 0x prefix. One form for the check and for
|
|
31
|
-
* sending - otherwise one thing would be checked and another encoded.
|
|
32
|
-
*/
|
|
33
|
-
export function canonicalTerms(request) {
|
|
34
|
-
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
35
|
-
const out = { ...request };
|
|
36
|
-
for (const key of HEX_POLICY_FIELDS) out[key] = with0x(out[key]);
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The order terms for the contract. They are assembled by the engine, and its refusal (the deadlines the wrong
|
|
42
|
-
* way, readyBy in the past) is turned into a CODE: texts are the interface's business, the core speaks in codes.
|
|
43
|
-
*/
|
|
44
|
-
export function terms(request) {
|
|
45
|
-
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
46
|
-
const need = ["locker", "claimer", "commitHalfLocker", "commitHalfClaimer", "edPointLocker", "edPointClaimer", "amount", "readyBy", "t1"];
|
|
47
|
-
const missing = need.filter((k) => request[k] === undefined || request[k] === null || request[k] === "");
|
|
48
|
-
if (missing.length) throw new SdkError("bad-input", { field: "terms", missing });
|
|
49
|
-
try {
|
|
50
|
-
return engine.escrow.orderTerms(canonicalTerms(request));
|
|
51
|
-
} catch (error) {
|
|
52
|
-
// We will not parse ANOTHER's text: we name what the contract's rules check, and only if the deadlines are
|
|
53
|
-
// actually violated. Otherwise - a general refusal on the terms.
|
|
54
|
-
const seconds = Math.floor(Date.now() / 1000);
|
|
55
|
-
if (Number(request.readyBy) <= seconds) throw new SdkError("bad-input", { field: "readyBy", why: "not-in-future" });
|
|
56
|
-
if (Number(request.t1) <= Number(request.readyBy)) throw new SdkError("bad-input", { field: "t1", why: "not-after-readyBy" });
|
|
57
|
-
throw new SdkError("bad-input", { field: "terms" });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// SANITISING THE REASON. The engine's messages carry no keys, but sanitising costs one line and covers the case
|
|
62
|
-
// where the engine starts printing a value: 0x + 64 hex is a private key, and it never goes out.
|
|
63
|
-
const sanitizeWhy = (value) =>
|
|
64
|
-
String(value === undefined || value === null ? "" : value).replace(/0x[0-9a-fA-F]{64}/g, "0x<redacted>").slice(0, 200);
|
|
65
|
-
// A NETWORK REFUSAL IS CALLED A NETWORK ONE, rather than being fitted to a data error: transport and order
|
|
66
|
-
// validation are different things, and the caller needs an answer to "retry or fix".
|
|
67
|
-
const TRANSPORT_WHY = /fetch failed|network|ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket|timeout|HTTP \d|status \d|non-ok/i;
|
|
68
|
-
const transportish = (why) => TRANSPORT_WHY.test(why);
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* SEND THE LOCK. The deadlines and the composition are checked BEFORE sending (see `terms`), the chain work is
|
|
72
|
-
* done by the engine (`fundOrder`), and the wallet arrives as an adapter: signing is possible only in it, and the
|
|
73
|
-
* core keeps no path into the chain of its own.
|
|
74
|
-
*
|
|
75
|
-
* TWO THINGS ARE DELIBERATELY NOT OURS: reading from the chain is taken from the engine, and the escrow address -
|
|
76
|
-
* FROM THE RECEIPT, not from a prediction. A prediction could diverge from what was actually created, and it
|
|
77
|
-
* would diverge silently.
|
|
78
|
-
*/
|
|
79
|
-
export async function send(request, wallet, deps = {}) {
|
|
80
|
-
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
81
|
-
if (!wallet || typeof wallet.send !== "function" || typeof wallet.receipt !== "function") {
|
|
82
|
-
throw new SdkError("bad-input", { field: "wallet", missing: ["send", "receipt"] });
|
|
83
|
-
}
|
|
84
|
-
// THE TERMS ARE BROUGHT TO CANONICAL FORM BEFORE EVERYTHING, and go in one form both into the check and into
|
|
85
|
-
// sending.
|
|
86
|
-
// THE DEADLINES - HERE, BEFORE ANY SENDING, and on the form funding expects (`amountWei`, not `amount`: the
|
|
87
|
-
// order terms and the sending have different names for the same field, and they must not be confused).
|
|
88
|
-
const canonical = canonicalTerms(request);
|
|
89
|
-
terms({ ...canonical, amount: canonical.amountWei });
|
|
90
|
-
const evm = engine.evm;
|
|
91
|
-
// WHAT READS THE CHAIN IS ALSO A SEAM (as in actions.mjs): by default it is the engine's read, but the caller
|
|
92
|
-
// can substitute its own. Without this capability the lock step could not be checked beyond predicting the
|
|
93
|
-
// address - and then "the address from the receipt" would remain an argument on trust, not a verified property.
|
|
94
|
-
const reader = typeof deps.call === "function" ? deps.call : evm.readContract;
|
|
95
|
-
try {
|
|
96
|
-
return await evm.fundOrder({
|
|
97
|
-
...canonical,
|
|
98
|
-
call: reader,
|
|
99
|
-
send: (tx) => wallet.send(tx),
|
|
100
|
-
// NORMALISING THE RECEIPT ARGUMENT'S SHAPE - HERE, AT THE CORE'S BOUNDARY, NOT AT EVERY CALLER.
|
|
101
|
-
//
|
|
102
|
-
// www/js/evm/funding.js calls the receipt with an OBJECT { hash } (waitReceipt), while the adapter contract
|
|
103
|
-
// declares receipt(txHash: string). A strict adapter (sdk/src/adapters/evm-wallet.mjs) answers bad-input to
|
|
104
|
-
// an object, the refusal is SWALLOWED by the receipt wait, and the escrow address is silently replaced with
|
|
105
|
-
// the PREDICTED one - that is, the "address from the receipt" check stops working without saying so. We
|
|
106
|
-
// normalise the argument to a string here: funding.js is foreign code and must not be edited, and every
|
|
107
|
-
// adapter should not repeat the normalisation.
|
|
108
|
-
receipt: (arg) => wallet.receipt(typeof arg === "string" ? arg : (arg && typeof arg.hash === "string" ? arg.hash : arg)),
|
|
109
|
-
});
|
|
110
|
-
} catch (error) {
|
|
111
|
-
// AN ALREADY-NAMED REASON PASSES AS IS: the adapter's and the reader's codes (wrong-chain, server-unavailable,
|
|
112
|
-
// contract-reverted, bad-input) ARE the answer, and it must not be overridden.
|
|
113
|
-
if (error && typeof error.code === "string") throw error;
|
|
114
|
-
// THE REST USED TO HIDE BEHIND server-unavailable { step: "fund-order" }, and the real reason (the first live
|
|
115
|
-
// run: "locker and claimer cannot coincide") never reached the caller. Now the reason is NAMED as a string
|
|
116
|
-
// (briefly, without keys), and the code is chosen by it: transport - server-unavailable, order/data
|
|
117
|
-
// validation - bad-input.
|
|
118
|
-
const why = sanitizeWhy((error && error.message) || error);
|
|
119
|
-
throw new SdkError(transportish(why) ? "server-unavailable" : "bad-input", { step: "fund-order", why });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The escrow factory address for a network: FROM THE NETWORK REGISTRY, by the same path as the rest of the core.
|
|
125
|
-
* The registry is read through the deal-flow module (sdk/src/swap-flow.mjs, escrowFactoryAddress) - the core
|
|
126
|
-
* keeps no SECOND table of addresses.
|
|
127
|
-
*
|
|
128
|
-
* PREVIOUSLY THE FUNCTION LOOKED IN THE WRONG MODULE (www/js/evm/escrow.js, which has no such function) and
|
|
129
|
-
* ALWAYS returned null - that is, the seam stayed silent about the factory's absence, and the caller took the
|
|
130
|
-
* address elsewhere. Now the absence of a factory for a network is a REFUSAL WITH A CODE naming the network, not
|
|
131
|
-
* null.
|
|
132
|
-
*/
|
|
133
|
-
export function factoryOf(chainSlug) {
|
|
134
|
-
let address = null;
|
|
135
|
-
try { address = flow.escrowFactoryAddress(chainSlug); } catch { address = null; }
|
|
136
|
-
if (typeof address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
137
|
-
throw new SdkError("bad-input", { field: "factory", chain: typeof chainSlug === "string" ? chainSlug : null });
|
|
138
|
-
}
|
|
139
|
-
return address;
|
|
140
|
-
}
|
|
1
|
+
// LOCKING THE FUNDS: TERMS FIRST, THEN SENDING.
|
|
2
|
+
//
|
|
3
|
+
// The first half of the step is here - assembling and CHECKING the order terms BEFORE anything goes on-chain. The
|
|
4
|
+
// rules are not ours: the contract sets them, and the engine already checks them (`www/js/evm/escrow.js`,
|
|
5
|
+
// `orderTerms`). The core will not hold a copy of the rules: a copy would drift from the contract right where the
|
|
6
|
+
// error shows up only as a rejected transaction.
|
|
7
|
+
//
|
|
8
|
+
// WHAT IS HERE: both the assembly of the terms and the sending itself - it goes through the engine's EVM layer
|
|
9
|
+
// (`fundOrder`), and the EVM wallet (the `send`/`receipt` role) comes from outside. What reads the chain is also a
|
|
10
|
+
// seam: by default the engine's read, but the caller can substitute its own (as in actions.mjs).
|
|
11
|
+
import { SdkError } from "./errors.mjs";
|
|
12
|
+
import * as engine from "./engine.mjs";
|
|
13
|
+
// THE DEAL FLOW IS A PACKAGE MODULE NOW (#32, wave 3): the registry address is read through it, exactly as
|
|
14
|
+
// the actions read the order flow.
|
|
15
|
+
import * as flow from "./swap-flow.mjs";
|
|
16
|
+
|
|
17
|
+
// THE ORDER'S WORD-ADDRESSES ARE BROUGHT TO 0x HERE, AT THE BOUNDARY, NOT AT EVERY CALLER.
|
|
18
|
+
//
|
|
19
|
+
// The engine returns the points and commitments WITHOUT the 0x prefix (that is how www/js/atomic/halves.js stores
|
|
20
|
+
// them), while the calldata encoder (www/js/evm/factory.js, word) REQUIRES the prefix and refuses "not a number
|
|
21
|
+
// and not hex" on 64 digits without one. The page normalises them on its side (sdk/src/swap-flow.mjs, with0x)
|
|
22
|
+
// for exactly this reason. The core must do the same ITSELF: otherwise every caller would repeat the
|
|
23
|
+
// normalisation, and one forgetful one would break the signature with the encoder's raw error. The list is
|
|
24
|
+
// exactly the fields encoded as bytes32 that do NOT enter the order context string (otherwise the normalisation
|
|
25
|
+
// would drift from the proof, which is bound to the context): two half commitments and three ed25519 points.
|
|
26
|
+
const HEX_POLICY_FIELDS = ["commitHalfLocker", "commitHalfClaimer", "edPointLocker", "edPointClaimer", "edViewPointLocker"];
|
|
27
|
+
const with0x = (v) => (typeof v === "string" && v && !/^0x/i.test(v) ? "0x" + v : v);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The order terms in canonical form: points and commitments - with the 0x prefix. One form for the check and for
|
|
31
|
+
* sending - otherwise one thing would be checked and another encoded.
|
|
32
|
+
*/
|
|
33
|
+
export function canonicalTerms(request) {
|
|
34
|
+
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
35
|
+
const out = { ...request };
|
|
36
|
+
for (const key of HEX_POLICY_FIELDS) out[key] = with0x(out[key]);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The order terms for the contract. They are assembled by the engine, and its refusal (the deadlines the wrong
|
|
42
|
+
* way, readyBy in the past) is turned into a CODE: texts are the interface's business, the core speaks in codes.
|
|
43
|
+
*/
|
|
44
|
+
export function terms(request) {
|
|
45
|
+
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
46
|
+
const need = ["locker", "claimer", "commitHalfLocker", "commitHalfClaimer", "edPointLocker", "edPointClaimer", "amount", "readyBy", "t1"];
|
|
47
|
+
const missing = need.filter((k) => request[k] === undefined || request[k] === null || request[k] === "");
|
|
48
|
+
if (missing.length) throw new SdkError("bad-input", { field: "terms", missing });
|
|
49
|
+
try {
|
|
50
|
+
return engine.escrow.orderTerms(canonicalTerms(request));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// We will not parse ANOTHER's text: we name what the contract's rules check, and only if the deadlines are
|
|
53
|
+
// actually violated. Otherwise - a general refusal on the terms.
|
|
54
|
+
const seconds = Math.floor(Date.now() / 1000);
|
|
55
|
+
if (Number(request.readyBy) <= seconds) throw new SdkError("bad-input", { field: "readyBy", why: "not-in-future" });
|
|
56
|
+
if (Number(request.t1) <= Number(request.readyBy)) throw new SdkError("bad-input", { field: "t1", why: "not-after-readyBy" });
|
|
57
|
+
throw new SdkError("bad-input", { field: "terms" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SANITISING THE REASON. The engine's messages carry no keys, but sanitising costs one line and covers the case
|
|
62
|
+
// where the engine starts printing a value: 0x + 64 hex is a private key, and it never goes out.
|
|
63
|
+
const sanitizeWhy = (value) =>
|
|
64
|
+
String(value === undefined || value === null ? "" : value).replace(/0x[0-9a-fA-F]{64}/g, "0x<redacted>").slice(0, 200);
|
|
65
|
+
// A NETWORK REFUSAL IS CALLED A NETWORK ONE, rather than being fitted to a data error: transport and order
|
|
66
|
+
// validation are different things, and the caller needs an answer to "retry or fix".
|
|
67
|
+
const TRANSPORT_WHY = /fetch failed|network|ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket|timeout|HTTP \d|status \d|non-ok/i;
|
|
68
|
+
const transportish = (why) => TRANSPORT_WHY.test(why);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* SEND THE LOCK. The deadlines and the composition are checked BEFORE sending (see `terms`), the chain work is
|
|
72
|
+
* done by the engine (`fundOrder`), and the wallet arrives as an adapter: signing is possible only in it, and the
|
|
73
|
+
* core keeps no path into the chain of its own.
|
|
74
|
+
*
|
|
75
|
+
* TWO THINGS ARE DELIBERATELY NOT OURS: reading from the chain is taken from the engine, and the escrow address -
|
|
76
|
+
* FROM THE RECEIPT, not from a prediction. A prediction could diverge from what was actually created, and it
|
|
77
|
+
* would diverge silently.
|
|
78
|
+
*/
|
|
79
|
+
export async function send(request, wallet, deps = {}) {
|
|
80
|
+
if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
|
|
81
|
+
if (!wallet || typeof wallet.send !== "function" || typeof wallet.receipt !== "function") {
|
|
82
|
+
throw new SdkError("bad-input", { field: "wallet", missing: ["send", "receipt"] });
|
|
83
|
+
}
|
|
84
|
+
// THE TERMS ARE BROUGHT TO CANONICAL FORM BEFORE EVERYTHING, and go in one form both into the check and into
|
|
85
|
+
// sending.
|
|
86
|
+
// THE DEADLINES - HERE, BEFORE ANY SENDING, and on the form funding expects (`amountWei`, not `amount`: the
|
|
87
|
+
// order terms and the sending have different names for the same field, and they must not be confused).
|
|
88
|
+
const canonical = canonicalTerms(request);
|
|
89
|
+
terms({ ...canonical, amount: canonical.amountWei });
|
|
90
|
+
const evm = engine.evm;
|
|
91
|
+
// WHAT READS THE CHAIN IS ALSO A SEAM (as in actions.mjs): by default it is the engine's read, but the caller
|
|
92
|
+
// can substitute its own. Without this capability the lock step could not be checked beyond predicting the
|
|
93
|
+
// address - and then "the address from the receipt" would remain an argument on trust, not a verified property.
|
|
94
|
+
const reader = typeof deps.call === "function" ? deps.call : evm.readContract;
|
|
95
|
+
try {
|
|
96
|
+
return await evm.fundOrder({
|
|
97
|
+
...canonical,
|
|
98
|
+
call: reader,
|
|
99
|
+
send: (tx) => wallet.send(tx),
|
|
100
|
+
// NORMALISING THE RECEIPT ARGUMENT'S SHAPE - HERE, AT THE CORE'S BOUNDARY, NOT AT EVERY CALLER.
|
|
101
|
+
//
|
|
102
|
+
// www/js/evm/funding.js calls the receipt with an OBJECT { hash } (waitReceipt), while the adapter contract
|
|
103
|
+
// declares receipt(txHash: string). A strict adapter (sdk/src/adapters/evm-wallet.mjs) answers bad-input to
|
|
104
|
+
// an object, the refusal is SWALLOWED by the receipt wait, and the escrow address is silently replaced with
|
|
105
|
+
// the PREDICTED one - that is, the "address from the receipt" check stops working without saying so. We
|
|
106
|
+
// normalise the argument to a string here: funding.js is foreign code and must not be edited, and every
|
|
107
|
+
// adapter should not repeat the normalisation.
|
|
108
|
+
receipt: (arg) => wallet.receipt(typeof arg === "string" ? arg : (arg && typeof arg.hash === "string" ? arg.hash : arg)),
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// AN ALREADY-NAMED REASON PASSES AS IS: the adapter's and the reader's codes (wrong-chain, server-unavailable,
|
|
112
|
+
// contract-reverted, bad-input) ARE the answer, and it must not be overridden.
|
|
113
|
+
if (error && typeof error.code === "string") throw error;
|
|
114
|
+
// THE REST USED TO HIDE BEHIND server-unavailable { step: "fund-order" }, and the real reason (the first live
|
|
115
|
+
// run: "locker and claimer cannot coincide") never reached the caller. Now the reason is NAMED as a string
|
|
116
|
+
// (briefly, without keys), and the code is chosen by it: transport - server-unavailable, order/data
|
|
117
|
+
// validation - bad-input.
|
|
118
|
+
const why = sanitizeWhy((error && error.message) || error);
|
|
119
|
+
throw new SdkError(transportish(why) ? "server-unavailable" : "bad-input", { step: "fund-order", why });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The escrow factory address for a network: FROM THE NETWORK REGISTRY, by the same path as the rest of the core.
|
|
125
|
+
* The registry is read through the deal-flow module (sdk/src/swap-flow.mjs, escrowFactoryAddress) - the core
|
|
126
|
+
* keeps no SECOND table of addresses.
|
|
127
|
+
*
|
|
128
|
+
* PREVIOUSLY THE FUNCTION LOOKED IN THE WRONG MODULE (www/js/evm/escrow.js, which has no such function) and
|
|
129
|
+
* ALWAYS returned null - that is, the seam stayed silent about the factory's absence, and the caller took the
|
|
130
|
+
* address elsewhere. Now the absence of a factory for a network is a REFUSAL WITH A CODE naming the network, not
|
|
131
|
+
* null.
|
|
132
|
+
*/
|
|
133
|
+
export function factoryOf(chainSlug) {
|
|
134
|
+
let address = null;
|
|
135
|
+
try { address = flow.escrowFactoryAddress(chainSlug); } catch { address = null; }
|
|
136
|
+
if (typeof address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
137
|
+
throw new SdkError("bad-input", { field: "factory", chain: typeof chainSlug === "string" ? chainSlug : null });
|
|
138
|
+
}
|
|
139
|
+
return address;
|
|
140
|
+
}
|