@ninsei-labs/sdk 0.39.0 → 0.39.1
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 +15 -0
- package/engine/core/chainSource.js +306 -306
- package/engine/core/sellWindow.js +43 -43
- package/engine/core/swap.js +1153 -1153
- package/engine/evm/claimGas.js +87 -87
- package/engine/evm/dex.js +836 -836
- package/engine/evm/gasReserve.js +88 -88
- package/engine/evm/prices.js +179 -179
- package/package.json +1 -1
- package/src/actions.mjs +72 -72
- package/src/adapters/node-test-evm-wallet.mjs +345 -345
- package/src/engine.mjs +53 -53
- package/src/lock.mjs +140 -140
- package/src/order.mjs +212 -212
- package/src/quotes.mjs +465 -465
- package/src/swaps.mjs +935 -935
- package/src/xmr-arrival.mjs +289 -289
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/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
|
+
}
|