@ninsei-labs/sdk 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +866 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/engine/atomic/dleq2.js +330 -0
  5. package/engine/atomic/halfenc.js +205 -0
  6. package/engine/atomic/halves.js +119 -0
  7. package/engine/atomic/order-client.js +57 -0
  8. package/engine/atomic/order.js +132 -0
  9. package/engine/atomic/orderContext.js +70 -0
  10. package/engine/atomic/orderHalfCommit.js +29 -0
  11. package/engine/core/chainSource.js +306 -0
  12. package/engine/core/clock.js +40 -0
  13. package/engine/core/config.js +885 -0
  14. package/engine/core/finality.js +93 -0
  15. package/engine/core/format.js +168 -0
  16. package/engine/core/preSignGuards.js +495 -0
  17. package/engine/core/rfqSource.js +193 -0
  18. package/engine/core/sellWindow.js +43 -0
  19. package/engine/core/store.js +100 -0
  20. package/engine/core/swap.js +1153 -0
  21. package/engine/evm/amounts.js +131 -0
  22. package/engine/evm/auth.js +156 -0
  23. package/engine/evm/claimGas.js +87 -0
  24. package/engine/evm/depositor.js +127 -0
  25. package/engine/evm/dex.js +836 -0
  26. package/engine/evm/escrow.js +208 -0
  27. package/engine/evm/factory.js +149 -0
  28. package/engine/evm/fees.js +90 -0
  29. package/engine/evm/funding.js +309 -0
  30. package/engine/evm/gasReserve.js +88 -0
  31. package/engine/evm/index.js +123 -0
  32. package/engine/evm/injected.js +175 -0
  33. package/engine/evm/permit.js +125 -0
  34. package/engine/evm/prices.js +179 -0
  35. package/engine/evm/session.js +777 -0
  36. package/engine/mock/chain.js +98 -0
  37. package/engine/monero/address.js +265 -0
  38. package/engine/monero/halvesSweep.js +68 -0
  39. package/engine/monero/node.js +110 -0
  40. package/engine/monero/scan.js +325 -0
  41. package/engine/monero/swapKeys.js +69 -0
  42. package/engine/monero/unlock.js +46 -0
  43. package/engine/monero/wallet.js +168 -0
  44. package/engine/recovery/recoveryFile.js +363 -0
  45. package/engine/recovery/restore.js +176 -0
  46. package/engine/sdk/bridge.js +609 -0
  47. package/engine/ui/dom.js +90 -0
  48. package/engine/ui/walletModal.js +132 -0
  49. package/index.d.ts +1417 -0
  50. package/package.json +74 -0
  51. package/src/actions.mjs +72 -0
  52. package/src/adapters/evm-wallet.mjs +126 -0
  53. package/src/adapters/index.mjs +21 -0
  54. package/src/adapters/local-storage.mjs +32 -0
  55. package/src/adapters/monero-wallet.mjs +185 -0
  56. package/src/adapters/monero.mjs +51 -0
  57. package/src/adapters/node-test-evm-wallet.mjs +345 -0
  58. package/src/codes.mjs +188 -0
  59. package/src/config.mjs +277 -0
  60. package/src/engine.mjs +53 -0
  61. package/src/errors.mjs +24 -0
  62. package/src/http.mjs +72 -0
  63. package/src/index.mjs +256 -0
  64. package/src/legs/evm.mjs +118 -0
  65. package/src/legs/index.mjs +50 -0
  66. package/src/levelsWalkSpec.mjs +183 -0
  67. package/src/lock.mjs +140 -0
  68. package/src/order.mjs +212 -0
  69. package/src/preflight.mjs +137 -0
  70. package/src/primitives.mjs +47 -0
  71. package/src/quoteEip712.mjs +35 -0
  72. package/src/quoteEip712Spec.mjs +214 -0
  73. package/src/quotes.mjs +465 -0
  74. package/src/record.mjs +65 -0
  75. package/src/recovery.mjs +40 -0
  76. package/src/sim.mjs +37 -0
  77. package/src/swap-flow.mjs +492 -0
  78. package/src/swaps.mjs +935 -0
  79. package/src/sweep.mjs +222 -0
  80. package/src/wallet.mjs +164 -0
  81. package/src/xmr-arrival.mjs +289 -0
package/src/lock.mjs ADDED
@@ -0,0 +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
+ }
package/src/order.mjs ADDED
@@ -0,0 +1,212 @@
1
+ // THE COUNTERPARTY'S SIDE AND THE JOINT ADDRESS.
2
+ //
3
+ // TWO CHECKS, BOTH MANDATORY BEFORE SIGNING - after it there is nothing to fix:
4
+ //
5
+ // 1) the DLEQ proof stands in the CONTEXT OF THIS order: the counterparty proved for our terms, not for others.
6
+ // The context is the same one the quote signature is bound to (see quotes.firm);
7
+ // 2) the proven ed25519 point is THE VERY ONE that will go into the contract and from which the address is
8
+ // assembled. Without this check the counterparty could prove one thing and substitute another: on our side
9
+ // everything would look sound, and the discrepancy would surface when claiming the XMR, with nothing left to
10
+ // fix.
11
+ //
12
+ // Both are done by the engine (order.js: the verifying code is the same for us and for the counterparty). The
13
+ // core has no cryptography of its own and should not: a copy would silently drift from the original.
14
+ import { SdkError } from "./errors.mjs";
15
+ import { primitives } from "./primitives.mjs";
16
+ import * as engine from "./engine.mjs";
17
+
18
+ const builder = () => engine.order.createOrderBuilder(primitives);
19
+
20
+ /** The order context: the string the proof and the quote signature are both bound to. */
21
+ export function contextOf(order) {
22
+ if (!order || typeof order !== "object") throw new SdkError("bad-input", { field: "order" });
23
+ try { return engine.orderContext.orderContextString(order); }
24
+ catch { throw new SdkError("bad-input", { field: "order" }); }
25
+ }
26
+
27
+ /**
28
+ * Check the counterparty's side in the order context. It passed - return the side itself; it did not - a refusal
29
+ * with a CODE (`quote-refused { step: "proof" }`). "Probably fine" does not exist here: the side is either proven
30
+ * or it is not.
31
+ */
32
+ export function verifyCounterparty({ side, order, sealed = null, ownEncPriv = null }) {
33
+ if (!side || typeof side !== "object") throw new SdkError("bad-input", { field: "side" });
34
+ const context = contextOf(order);
35
+ let verdict = null;
36
+ try { verdict = builder().verifySide(side, context, sealed, ownEncPriv); }
37
+ catch { throw new SdkError("bad-input", { field: "side" }); }
38
+ if (!verdict || verdict.ok !== true) {
39
+ throw new SdkError("quote-refused", { step: "proof", why: verdict && verdict.reason ? "rejected" : "unknown" });
40
+ }
41
+ return side;
42
+ }
43
+
44
+ /**
45
+ * The shared Monero address: your own spend half and the other's POINT (the same for the view key). We take the
46
+ * same encoder the wallet uses: a second one would drift from the first where the error is visible only by
47
+ * sending XMR.
48
+ */
49
+ export function jointAddress({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network }) {
50
+ const need = { ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint };
51
+ const missing = Object.keys(need).filter((k) => need[k] === undefined || need[k] === null || need[k] === "");
52
+ if (missing.length) throw new SdkError("bad-input", { field: "jointAddress", missing });
53
+ try {
54
+ return builder().jointAddress({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network });
55
+ } catch { throw new SdkError("bad-input", { field: "jointAddress" }); }
56
+ }
57
+
58
+ // --- THE ORDER STEP: FROM OUR SIDE TO THE JOINT ADDRESS -----------------------------------------
59
+ //
60
+ // WHY THIS IS A SINGLE CALL AND NOT THREE SEPARATE ONES. The order here IS the rule: your own side -> the
61
+ // counterparty's side (accepted and verified in THIS SAME context) -> the joint address. The address is assembled
62
+ // from YOUR half and the OTHER's point, so before the counterparty's side appears it simply does not exist - and
63
+ // refusing here is cheaper than discovering the discrepancy when claiming the XMR, with nothing left to fix.
64
+ //
65
+ // THERE IS NOT A SINGLE FORMULA OF OUR OWN HERE. The halves, the points, the DLEQ proof, the encryption key and
66
+ // the address are computed by the engine's builder (www/js/atomic/order.js) on the core's primitives. The core
67
+ // names the order and translates the result into the record's fields - exactly the names the engine reads
68
+ // (core/swap.js, halvesWalletOf).
69
+
70
+ export const norm = (h) => "0x" + String(h).replace(/^0x/i, "").toLowerCase();
71
+ // A HALF AT THE BOUNDARY IS A HEX STRING: a BigInt survives neither storage nor the recovery file
72
+ // (www/js/recovery/recoveryFile.js, bigintSafe). One form at every boundary, otherwise JSON silently loses the key.
73
+ export const halfHex = (v) => "0x" + BigInt(v).toString(16).padStart(64, "0");
74
+ const isPoint = (h) => /^0x[0-9a-f]{64}$/.test(String(h));
75
+ // THE PROVEN POINT AND THE POINT IN THE ORDER MUST BE WRITTEN IDENTICALLY. The engine compares them AS STRINGS
76
+ // (order.js, verifySide), so the record's form is part of the check, not cosmetics: without bringing both to one
77
+ // form, a correct proof would look like a discrepancy. The page does the same
78
+ // (sdk/src/swap-flow.mjs: it normalises both proof.XB and the point). One normalisation for both sides.
79
+ const sameFormAsPoint = (proof) => (proof && typeof proof === "object" && proof.XB ? { ...proof, XB: norm(proof.XB) } : proof);
80
+ // A POINT AT THE BOUNDARY IS IN CANONICAL FORM (0x + lowercase). The engine's points arrive WITHOUT the 0x prefix
81
+ // (that is how www/js/atomic/halves.js stores them), while the proof check compares points AS STRINGS. The
82
+ // normalisation must be SYMMETRIC: the proven point of the proof is already normalised by sameFormAsPoint, so the
83
+ // counterparty's declared point is normalised the same way - otherwise the same number written once with 0x and
84
+ // once without would give a false quote-refused { step: "proof" } on a matching proof. The page does the same
85
+ // (sdk/src/swap-flow.mjs, with0x). Not a string - then there is no point: we leave it as is, this is a refusal
86
+ // "no point", not "point in another form".
87
+ const pointForm = (v) => (typeof v === "string" && v ? norm(v) : v);
88
+
89
+ /**
90
+ * Our side of the order. It is built by the engine's builder: two halves, their points, the DLEQ PROOF (it is
91
+ * bound to the order terms and so is not computed without the context) and the side's encryption key.
92
+ * The long proof computation lives here - one side, one computation.
93
+ */
94
+ export function buildOwnSide(order, onBit) {
95
+ const context = contextOf(order);
96
+ const side = builder().newSide(context, false, onBit);
97
+ return { context, side, spendPoint: norm(side.pub.ed), viewPoint: norm(side.viewPub), encPub: norm(side.enc.pub) };
98
+ }
99
+
100
+ /**
101
+ * The counterparty's side: accept and verify. The check is ONE for both sides and is done by the engine
102
+ * (order.js, verifySide): the proof stands in the CONTEXT OF THIS order, the proven point matches the one that
103
+ * will go into the contract, and if a sealed package arrived it is opened with OUR encryption key and checked
104
+ * against its point. The sandbox stand-in is built here too and marked: it must not be passed off as a real
105
+ * provider.
106
+ */
107
+ export function acceptCounterparty({ context, side = null, counterparty = null, standIn = false }) {
108
+ let accepted = null;
109
+ if (counterparty && typeof counterparty === "object") {
110
+ const pub = counterparty.pub && typeof counterparty.pub === "object" ? counterparty.pub : {};
111
+ // THE ORDER-QUOTE FIELD NAMES ARE THE ONES THE CORE ITSELF RETURNS (a defect found). The counterparty's side
112
+ // comes from `quotes.firm`, and there the point is called `edPointClaimer`, the view half - `viewHalfClaimer`
113
+ // (rfq/orderQuote.mjs). While only `edPub`/`viewPub` were accepted, the facade's own side was NOT accepted:
114
+ // start answered bad-input { field: "counterparty", missing: ["edPub"] } to its own answer. The engine knew
115
+ // this mapping (sdk/src/swap-flow.mjs: edPub: quote.edPointClaimer). We accept BOTH names.
116
+ accepted = {
117
+ proof: sameFormAsPoint(counterparty.proof),
118
+ edPub: pointForm(counterparty.edPub || counterparty.edPoint || counterparty.edPointClaimer || pub.ed || null),
119
+ viewPoint: pointForm(counterparty.viewPoint || counterparty.viewPub || null),
120
+ viewHalf: counterparty.viewHalf || counterparty.viewHalfClaimer || null,
121
+ // THE COUNTERPARTY'S SPEND HALF: HIS commitment (commitHalfClaimer) is computed from it where it did not
122
+ // arrive ready-made. Without the half the commitment cannot be obtained - and that is named null, not invented.
123
+ half: counterparty.half !== undefined ? counterparty.half : null,
124
+ commitHalfClaimer: counterparty.commitHalfClaimer || null,
125
+ sealed: counterparty.sealed || null,
126
+ providerId: counterparty.providerId || null,
127
+ standIn: false,
128
+ };
129
+ } else if (standIn === true) {
130
+ // THE STAND-IN IS COMPUTED IN THE SAME CONTEXT AS THE ORDER: the string gets nothing appended. The same path
131
+ // as the page (sdk/src/swap-flow.mjs: worker.buildSide) and the flow check - otherwise what would be checked is
132
+ // not what the page runs on.
133
+ const mock = builder().newSide(context, true);
134
+ accepted = { proof: sameFormAsPoint(mock.proof), edPub: norm(mock.pub.ed), viewPoint: norm(mock.viewPub),
135
+ viewHalf: mock.viewHalf, half: mock.half, commitHalfClaimer: null, sealed: null, providerId: null, standIn: true, mock };
136
+ } else {
137
+ throw new SdkError("bad-input", { field: "counterparty" });
138
+ }
139
+ const missing = [];
140
+ if (!accepted.proof) missing.push("proof");
141
+ if (!accepted.edPub) missing.push("edPub");
142
+ if (missing.length) throw new SdkError("bad-input", { field: "counterparty", missing });
143
+ // OPENING THE SEALED PACKAGE - BY THE SAME ENGINE CALL, and the key is OURS (the package is addressed to us).
144
+ // The envelope's composition is locked separately (www/js/atomic/halfenc.js, HALFENC_LOCK_REASON): in the live
145
+ // flow the half arrives from the chain via the commit/reveal mechanism, so a locked package honestly answers
146
+ // with a refusal rather than passing itself off as a working key handover.
147
+ const ownEncPriv = accepted.sealed && side && side.enc ? side.enc.priv : null;
148
+ let verdict = null;
149
+ try { verdict = builder().verifySide({ proof: accepted.proof, pub: { ed: accepted.edPub } }, context, accepted.sealed, ownEncPriv); }
150
+ catch { throw new SdkError("bad-input", { field: "counterparty" }); }
151
+ if (!verdict || verdict.ok !== true) {
152
+ throw new SdkError("quote-refused", { step: "proof", why: verdict && verdict.reason ? "rejected" : "unknown" });
153
+ }
154
+ return accepted;
155
+ }
156
+
157
+ /**
158
+ * THE ORDER STEP AS A WHOLE: your own side (or one already built by the record), the counterparty's side and the
159
+ * joint address. It hands out the fields the record and the recovery file are assembled from; NOT A SINGLE empty
160
+ * value: an address is not assembled without the other's point, and that is a refusal, not an empty string.
161
+ */
162
+ export function orderStep({ order, side = null, counterparty = null, standIn = false, network = null }) {
163
+ const context = contextOf(order);
164
+ const own = side
165
+ ? { context, side, spendPoint: norm(side.pub && side.pub.ed), viewPoint: norm(side.viewPub),
166
+ encPub: norm(side.enc && side.enc.pub ? side.enc.pub : null) }
167
+ : buildOwnSide(order);
168
+ const lack = [];
169
+ if (own.side.half === undefined || own.side.half === null) lack.push("half");
170
+ if (own.side.viewHalf === undefined || own.side.viewHalf === null) lack.push("viewHalf");
171
+ if (!isPoint(own.spendPoint)) lack.push("spendPoint");
172
+ if (!isPoint(own.viewPoint)) lack.push("viewPoint");
173
+ if (lack.length) throw new SdkError("bad-input", { field: "side", missing: lack });
174
+
175
+ const other = acceptCounterparty({ context: own.context, side: own.side, counterparty, standIn });
176
+ const otherViewPoint = other.viewPoint || (other.viewHalf ? norm(builder().pointOf(other.viewHalf)) : null);
177
+ if (!otherViewPoint) throw new SdkError("bad-input", { field: "counterparty", missing: ["viewPoint"] });
178
+
179
+ let joint = null;
180
+ try {
181
+ joint = builder().jointAddress({
182
+ ownSpendHalf: own.side.half, ownViewHalf: own.side.viewHalf,
183
+ otherSpendPoint: other.edPub, otherViewPoint, network,
184
+ });
185
+ } catch { throw new SdkError("bad-input", { field: "network" }); }
186
+
187
+ return {
188
+ context: own.context,
189
+ own: { spendPoint: own.spendPoint, viewPoint: own.viewPoint, encPub: own.encPub, proof: own.side.proof },
190
+ side: own.side,
191
+ counterparty: other,
192
+ // READY-MADE RECORD AND FILE FIELDS: the names the engine reads (core/swap.js, halvesWalletOf) and the ones
193
+ // recovery file version 3 requires. A file without the other's view half will not be assembled - and that is
194
+ // named by the otherViewHalf field with the value null, not by an invented half.
195
+ wallet: {
196
+ spendHalf: halfHex(own.side.half), viewHalf: halfHex(own.side.viewHalf),
197
+ otherSpendPoint: other.edPub, otherViewPoint: norm(otherViewPoint),
198
+ otherViewHalf: other.viewHalf ? halfHex(other.viewHalf) : null,
199
+ address: joint.address, spendPub: norm(joint.spendPub), viewPub: norm(joint.viewPub),
200
+ otherCommitHalf: other.commitHalfClaimer || null,
201
+ // THE COMMITMENTS FOR THE HALVES - FROM THESE SAME SIDES, by the same call as the contract
202
+ // (keccak256(abi.encodePacked(bytes32 half))). One built side -> one commitment: there is no second half
203
+ // computation here. The other's half may not arrive (the order brings it) - then the counterparty's
204
+ // commitment is taken ready-made or stays null, not invented.
205
+ commitHalfLocker: norm(builder().halfCommitment(own.side.half)),
206
+ commitHalfClaimer: other.half !== undefined && other.half !== null
207
+ ? norm(builder().halfCommitment(other.half))
208
+ : (other.commitHalfClaimer || null),
209
+ network: network || null,
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,137 @@
1
+ // PRE-SIGNATURE CHECKS: the same engine guards, but the answer is codes, not texts.
2
+ //
3
+ // WHAT MATTERS HERE and why this is not a "wrapper": the unverified must be named as unverified. The engine guards
4
+ // already distinguish "good" from "not measured" (`checked: false`, the `*-unchecked` kinds), so the SDK does not
5
+ // discard such checks and does not pass them off as green: it calls the guard with what it has and passes the
6
+ // verdict on.
7
+ import * as engine from "./engine.mjs";
8
+ import { checkCodeFor } from "./codes.mjs";
9
+
10
+ // The engine's refusal reason is a phrase for a person; the SDK does not hand it out: the interface translates the code itself.
11
+ const toCheck = (verdict) => {
12
+ const v = verdict && typeof verdict === "object" ? verdict : {};
13
+ const { reason, kind, ok, checked, blocks, ...params } = v;
14
+ return {
15
+ code: checkCodeFor(kind),
16
+ ok: ok === true,
17
+ checked: checked !== false,
18
+ blocks: blocks === true,
19
+ kind: typeof kind === "string" ? kind : "unknown",
20
+ params,
21
+ };
22
+ };
23
+
24
+ // IS THERE ANYTHING TO COMPARE AGAINST. An empty value means "there was no check", and a check with nothing to
25
+ // apply to is not run at all: otherwise it locks the signature where there is NOTHING to check against by
26
+ // construction.
27
+ const hasComparison = (...values) => values.some((v) => v !== undefined && v !== null && v !== "");
28
+
29
+ const call = (fn, input) => {
30
+ try {
31
+ return fn(input);
32
+ } catch (error) {
33
+ // A guard that could not compute is "not checked", not "passed".
34
+ return { ok: false, checked: false, blocks: false, kind: "unchecked", error: String(error && error.message ? error.message : error) };
35
+ }
36
+ };
37
+
38
+ export function createPreflight({ config, wallet, limits = {} }) {
39
+ return async function preflight(offer = {}, options = {}) {
40
+ const guards = engine.config.SIGNING_GUARDS || {};
41
+ const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
42
+ const quote = offer && typeof offer === "object" ? offer : {};
43
+ const tokenIsNative = Boolean(options.tokenIsNative);
44
+ const chain = config.evmNetwork;
45
+
46
+ const checks = [
47
+ // 1. THE QUOTE IS ALIVE. The deadline is taken from the quote: an expired one must not be signed, even if its numbers are correct.
48
+ call(engine.guards.quoteFreshnessVerdict, {
49
+ at: Number.isFinite(quote.at) ? quote.at : null,
50
+ ttlMs: Number.isFinite(quote.ttlMs) ? quote.ttlMs : null,
51
+ expiresAt: Number.isFinite(quote.expiresAt) ? quote.expiresAt : null,
52
+ nowMs,
53
+ }),
54
+ // 2. DEADLINES FROM THE CHAIN: the mark window and the claim window must not overlap.
55
+ call(engine.guards.deadlineVerdict, {
56
+ nowSec: Math.floor(nowMs / 1000),
57
+ readyBy: Number.isFinite(quote.readyBy) ? quote.readyBy : null,
58
+ t1: Number.isFinite(quote.t1) ? quote.t1 : null,
59
+ minReadyLeadSec: guards.minReadyLeadSec ?? null,
60
+ minClaimWindowSec: guards.minClaimWindowSec ?? null,
61
+ }),
62
+ // 3. SIZE. The grid is declared by the provider node, so the sizes come with the quote, not from a file.
63
+ call(engine.guards.rangeVerdict, {
64
+ size: Number.isFinite(quote.amount) ? quote.amount : null,
65
+ unit: quote.token || null,
66
+ min: Number.isFinite(quote.min) ? quote.min : (limits.minAmount ?? null),
67
+ max: Number.isFinite(quote.max) ? quote.max : (limits.maxAmount ?? null),
68
+ step: Number.isFinite(quote.step) ? quote.step : (limits.stepAmount ?? null),
69
+ quoteUnit: quote.token || null,
70
+ }),
71
+ // 4. MONEY: is there enough to pay and does anything remain for gas.
72
+ call(engine.guards.payBalanceVerdict, {
73
+ payBalanceWei: options.payBalanceWei ?? null,
74
+ payAmountWei: options.payAmountWei ?? null,
75
+ symbol: tokenIsNative ? chain.nativeSymbol : (quote.token || null),
76
+ decimals: Number.isFinite(options.decimals) ? options.decimals : 18,
77
+ }),
78
+ call(engine.guards.gasReserveVerdict, {
79
+ nativeBalanceWei: options.nativeBalanceWei ?? null,
80
+ requiredNativeWei: options.requiredNativeWei ?? null,
81
+ gasReserveWei: options.gasReserveWei ?? null,
82
+ symbol: chain.nativeSymbol,
83
+ decimals: 18,
84
+ }),
85
+ // 5. TOKEN ALLOWANCE. Without the person's consent no allowance is granted - that is a separate code.
86
+ call(engine.guards.allowanceVerdict, {
87
+ tokenIsNative,
88
+ allowanceWei: options.allowanceWei ?? null,
89
+ amountWei: options.payAmountWei ?? null,
90
+ consent: options.allowanceConsent === true,
91
+ symbol: quote.token || null,
92
+ decimals: Number.isFinite(options.decimals) ? options.decimals : 18,
93
+ }),
94
+ // 6. THE WALLET NETWORK against the settlement network.
95
+ call(engine.guards.networkGateVerdict, {
96
+ walletChainId: wallet.status().chainId,
97
+ selectedChainId: chain.chainId,
98
+ settlementChainId: chain.chainId,
99
+ selectedChainName: chain.name,
100
+ }),
101
+ // 7. THE COUNTERPARTY and 8. THE ESCROW STATE, 9. THE POINTS, 10. THE ORDER-BOOK SLOTS. These are chain
102
+ // reads: while they are absent, the guards must say "not checked" rather than stay silent (stages 2 and 3).
103
+ call(engine.guards.counterpartyVerdict, {
104
+ standIn: options.standIn === true,
105
+ liveChain: options.liveChain === true,
106
+ network: config.evmNetwork.id,
107
+ chainId: chain.chainId,
108
+ }),
109
+ call(engine.guards.escrowStateVerdict, { status: options.escrowStatus ?? null, error: null, address: options.escrowAddress ?? null }),
110
+ // THE POINTS AND SLOTS ARE TURNED ON ONLY WHEN THERE IS SOMETHING TO COMPARE AGAINST. Both compare an order
111
+ // ALREADY LYING ON THE CHAIN, while at the swap-start step the order does not exist YET: it is created by
112
+ // the very signature the guards protect, so there is nothing to compare against BY CONSTRUCTION. While both
113
+ // stood in the list unconditionally, on empty data each answered "not checked" (blocks: true), and the swap
114
+ // start ran into its own guard without ever reaching the signature. The check APPEARS when its data is
115
+ // brought (the core does this on mark/claim/refund - assertOrderSlots), and then incompleteness still locks:
116
+ // this is removing a check with nothing to apply to, not weakening one that compares.
117
+ ...(hasComparison(options.point, options.commit)
118
+ ? [call(engine.guards.escrowPointsVerdict, {
119
+ role: options.pointsRole ?? null,
120
+ what: options.pointsWhat ?? null,
121
+ point: options.point ?? null,
122
+ onchainPoint: options.onchainPoint ?? null,
123
+ commit: options.commit ?? null,
124
+ onchainCommit: options.onchainCommit ?? null,
125
+ readError: options.pointsReadError ?? null,
126
+ })]
127
+ : []),
128
+ ...(hasComparison(options.slots)
129
+ ? [call(engine.guards.liveOrderSlotsVerdict, { slots: options.slots ?? null, expect: options.slotsExpect ?? null, readError: options.slotsReadError ?? null })]
130
+ : []),
131
+ ];
132
+
133
+ const mapped = checks.map(toCheck);
134
+ const blockedBy = [...new Set(mapped.filter((c) => c.blocks).map((c) => c.code))];
135
+ return { ok: blockedBy.length === 0, blockedBy, checks: mapped };
136
+ };
137
+ }
@@ -0,0 +1,47 @@
1
+ // ATOMIC SWAP PRIMITIVES - FROM THE SAME npm PACKAGES AS THE PAGE BUNDLE.
2
+ //
3
+ // The bundle entry point (tools/atomic-browser-entry.mjs) hands the page exactly five names: ed25519, secp256k1,
4
+ // keccak256, sha3_512, randomBytes. The order-side build modules (www/js/atomic/order.js) do NOT import them
5
+ // themselves - they arrive as parameters. That is what lets the SDK expose the same primitives from the same
6
+ // packages instead of dragging a hashed build artefact into the core: a package never has a "version with a
7
+ // different cache".
8
+ //
9
+ // WHY EXACTLY THESE PACKAGES, AND NOT JUST ANY. keccak256 and sha3_512 are DIFFERENT functions, and they must
10
+ // not be confused: the commitment for a half is computed with keccak256 (exactly as Solidity does it), while the
11
+ // DLEQ proof uses SHA3-512 with wide reduction. Swapping one for the other looks like a typo and costs a whole
12
+ // swap, so the difference is verified by a run on known values, not by eye.
13
+
14
+ import { ed25519 } from "@noble/curves/ed25519.js";
15
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
16
+ // keccak_256 is taken from here, and NOT from the separate keccak256 package: that one accepts only a Buffer of
17
+ // ITS OWN instance and refuses bytes from another module with "invalid type" (verified by a run). The popular
18
+ // package of the family is already present for the curves and SHA3-512, and keccak_256 from it gives the same
19
+ // thing - this is confirmed by the known value of the empty string in the check, which also shows it is not
20
+ // sha3_256.
21
+ import { keccak_256, sha3_512 } from "@noble/hashes/sha3.js";
22
+ // The core speaks in CODES, not in phrases: a message here would become UI text by accident (check-sdk
23
+ // forbids user-facing strings in sdk/src).
24
+ import { fail } from "./errors.mjs";
25
+ export { ed25519, secp256k1, sha3_512 };
26
+
27
+ // The engine passes a Uint8Array, a Buffer and a string alike. We normalise on input so the caller need not think about it.
28
+ export const keccak256 = (data) => {
29
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
30
+ return keccak_256(bytes);
31
+ };
32
+
33
+ /** Bytes: the same representation as keccak256, so the caller need not tell the source apart. */
34
+ export const keccak256Bytes = (data) => keccak256(data);
35
+
36
+ // Random bytes. We take them from the environment (both the browser and Node provide crypto.getRandomValues),
37
+ // not from a third-party library: the source of randomness is the last thing you want to shuffle around.
38
+ export const randomBytes = (length) => {
39
+ const out = new Uint8Array(length);
40
+ const source = globalThis.crypto;
41
+ if (!source || typeof source.getRandomValues !== "function") fail("bad-input", { field: "random-bytes", why: "no-source" });
42
+ source.getRandomValues(out);
43
+ return out;
44
+ };
45
+
46
+ /** The five primitives as one set - exactly the shape createOrderBuilder expects. */
47
+ export const primitives = { ed25519, secp256k1, keccak256, sha3_512, randomBytes };
@@ -0,0 +1,35 @@
1
+ // EIP-712 ORDER-QUOTE SIGNATURE - THE SDK SIDE (VERIFICATION ONLY).
2
+ //
3
+ // The SDK verifies, it does not sign: the maker's wallet key never reaches the customer's machine. The spec
4
+ // (types, domain, digest, recovery) lives in quoteEip712Spec.mjs, byte-identical to the node's copy and pinned
5
+ // by tools/check-quote-eip712.mjs. Here it is only bound to the SDK's own crypto primitives.
6
+ //
7
+ // The rule the old flag carried is kept: a quote that cannot be verified is REFUSED. A missing field, a
8
+ // missing signature or an unusable domain is not "probably fine" - the caller names it with a code.
9
+ import { keccak256, secp256k1 } from "./primitives.mjs";
10
+ import {
11
+ recoverQuoteKey as recover,
12
+ quoteDigest as digest,
13
+ quoteMissingFields,
14
+ ORDER_QUOTE_FIELDS as FIELDS,
15
+ ORDER_QUOTE_TYPE as TYPE,
16
+ EIP712_DOMAIN_TYPE,
17
+ ORDER_QUOTE_DOMAIN_NAME,
18
+ ORDER_QUOTE_DOMAIN_VERSION,
19
+ ORDER_QUOTE_VERSION,
20
+ } from "./quoteEip712Spec.mjs";
21
+
22
+ const crypto = { keccak256, secp256k1 };
23
+
24
+ export const ORDER_QUOTE_FIELDS = FIELDS;
25
+ export const ORDER_QUOTE_TYPE = TYPE;
26
+ export { EIP712_DOMAIN_TYPE, ORDER_QUOTE_DOMAIN_NAME, ORDER_QUOTE_DOMAIN_VERSION, ORDER_QUOTE_VERSION };
27
+
28
+ /** Names of the signed-set fields the quote does not carry. Not empty - the quote is not verifiable. */
29
+ export const orderQuoteMissingFields = (quote) => quoteMissingFields(quote);
30
+
31
+ /** The digest the provider signed, from the quote's own declared fields. */
32
+ export const orderQuoteDigest = (quote) => digest({ crypto, quote });
33
+
34
+ /** The address that signed the quote, or null when it cannot be recovered. */
35
+ export const recoverOrderQuoteKey = (quote) => recover({ crypto, quote });