@ninsei-labs/sdk 0.39.0 → 0.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +21 -0
- package/engine/core/chainSource.js +306 -306
- package/engine/core/config.js +29 -2
- package/engine/core/sellWindow.js +43 -43
- package/engine/core/swap.js +1153 -1153
- package/engine/evm/claimGas.js +88 -87
- package/engine/evm/dex.js +836 -836
- package/engine/evm/factory.js +106 -0
- package/engine/evm/funding.js +11 -11
- package/engine/evm/gasReserve.js +88 -88
- package/engine/evm/prices.js +179 -179
- package/engine/recovery/recoveryFile.js +4 -1
- package/engine/sdk/bridge.js +7 -2
- package/index.d.ts +18 -2
- package/package.json +4 -4
- package/src/actions.mjs +72 -72
- package/src/adapters/node-test-evm-wallet.mjs +345 -345
- package/src/config.mjs +9 -6
- package/src/engine.mjs +53 -53
- package/src/index.mjs +10 -1
- package/src/levelsWalkSpec.mjs +21 -4
- package/src/lock.mjs +140 -140
- package/src/order.mjs +212 -212
- package/src/quotes.mjs +495 -465
- package/src/swaps.mjs +935 -935
- package/src/sweep.mjs +29 -4
- package/src/xmr-arrival.mjs +289 -289
package/src/order.mjs
CHANGED
|
@@ -1,212 +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
|
-
}
|
|
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
|
+
}
|