@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/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 one the demo is configured for
222
- // (XMR_NETWORKS: stagenet). But the SDK contract (index.d.ts, xmrNetwork) names THREE standard names -
223
- // mainnet/stagenet/testnet - and that is not about the demo: the withdrawal page takes the network FROM THE
224
- // NODE, and on a production network its network is mainnet, not a typo. So three standard ones are added to
225
- // the declared ones; anything else is still a refusal with the known list.
226
- const STANDARD_XMR_NETWORKS = ["mainnet", "stagenet", "testnet"];
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 });
@@ -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
- const minCost = levelCostWei(toBigInt(levels[0].amount), toBigInt(levels[0].price));
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
- if (want < verdict.minAtomic) return { ok: false, reason: "levels-below-minimum", role };
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
+ }