@ninsei-labs/sdk 0.39.3 → 0.40.0-develop.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 +35 -0
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +1 -1
- package/src/swaps.mjs +1 -1
package/engine/evm/funding.js
CHANGED
|
@@ -1,130 +1,133 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/funding.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// Funding an order in the escrow contract: real transactions, not a simulation.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
7
|
+
// Roles and why:
|
|
8
|
+
// - the order is created and the ETH deposited by whoever is recorded as `locker` in the terms (the contract
|
|
9
|
+
// requires exactly them);
|
|
10
|
+
// - the claimer takes by the secret - the address is set in the terms, the caller cannot change the recipient;
|
|
11
|
+
// - the secret at this step is generated HERE and marked as a test one: in production it is born when the XMR is
|
|
12
|
+
// spent (HTLC), and then this same function simply receives it from outside.
|
|
13
13
|
//
|
|
14
|
-
// keccak256
|
|
15
|
-
//
|
|
16
|
-
// посчитанный бандлом, совпадает с тем, что проверяет Solidity.
|
|
14
|
+
// keccak256 is passed in as a dependency, as in the address module: the caller loads the key library, not this
|
|
15
|
+
// module. Monero and Ethereum share Keccak-256, so the hash computed by the bundle matches what Solidity checks.
|
|
17
16
|
|
|
18
17
|
import { encodeCreateOrderAndFund, encodeCreateOrderAndFundByDepositor, encodePredict, decodeAddress, ORDER_CREATED_TOPIC } from "./factory.js";
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// THE DEPOSITOR BY SIGNATURE: the signature shape comes from one place (evm/depositor.js), next to the protocol
|
|
19
|
+
// shape. Checking the shape here would be a second record of the same rule.
|
|
21
20
|
import { depositorSignatureShape } from "./depositor.js";
|
|
22
21
|
import { encodeStatus, decodeStatus } from "./escrow.js";
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// THE FEE: rate, amount and recipient are PART OF THE SIGNED QUOTE. The factory no longer has them (the router was
|
|
23
|
+
// removed), so the amount comes from the quote and is cross-checked by the formula mirror. Without a quote there
|
|
24
|
+
// is nothing to fund with: the factory takes a signed set, not bare order terms.
|
|
26
25
|
import { feeTermsFromQuote } from "./fees.js";
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// PRE-SIGNING GUARDS: the order deadlines and the escrow state are pure functions, because they can be checked
|
|
27
|
+
// against the numbers that BREAK them only if they know neither the network nor the wallet.
|
|
29
28
|
import { deadlineVerdict, escrowStateVerdict } from "../core/preSignGuards.js";
|
|
30
29
|
import { SIGNING_GUARDS } from "../core/config.js";
|
|
31
30
|
|
|
32
31
|
const ATOMIC = 1_000_000_000_000_000_000n; // 1 ETH = 10^18 wei
|
|
33
32
|
|
|
34
|
-
//
|
|
33
|
+
// A test secret: 32 bytes from the browser's crypto generator.
|
|
35
34
|
export function newSecret() {
|
|
36
35
|
const bytes = new Uint8Array(32);
|
|
37
36
|
crypto.getRandomValues(bytes);
|
|
38
37
|
return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
//
|
|
40
|
+
// Order terms. Time is absolute (unix), as the contract requires.
|
|
42
41
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
42
|
+
// NO PREIMAGE HERE ANY MORE: instead, BOTH half commitments and BOTH ed25519 public points go into the order. The
|
|
43
|
+
// function REQUIRES them and fails if they are missing - on purpose: silently sending an order with missing fields
|
|
44
|
+
// would create an escrow that can neither be claimed nor have its half-to-address link proven. Better to refuse
|
|
45
|
+
// here and loudly.
|
|
47
46
|
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
47
|
+
// TWO DEADLINES THAT DO NOT COINCIDE: until readyBy the depositor marks readiness ("I see the XMR"), from t1 a
|
|
48
|
+
// claim is closed and anyone may refund. Coincidence would restore a window where both paths are valid and gas
|
|
49
|
+
// would decide the dispute.
|
|
51
50
|
export function testOrderTerms({ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, amountWei, salt, claimWindowSeconds = 3600, readyWindowSeconds = 14400, readyBy, t1 }) {
|
|
52
|
-
//
|
|
53
|
-
//
|
|
51
|
+
// THE VIEW POINT IS ON THE REQUIRED LIST: without it the Monero address cannot be assembled from the order
|
|
52
|
+
// fields, and the one who learns about it would be the claimer, after funding.
|
|
54
53
|
for (const [k, v] of Object.entries({ commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker })) {
|
|
55
|
-
if (!v) throw new Error("
|
|
54
|
+
if (!v) throw new Error("order term not set: " + k);
|
|
56
55
|
}
|
|
57
56
|
const now = Math.floor(Date.now() / 1000);
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
57
|
+
// DEADLINES MAY BE PASSED IN READY-MADE, AND THAT IS NECESSITY, NOT CONVENIENCE. They enter the canonical order
|
|
58
|
+
// context the proof is bound to, and the proof is computed BEFORE funding. Computing the deadlines here and now
|
|
59
|
+
// would put them AFTER the proof, so the proof would bind one set of deadlines while the contract gets another.
|
|
60
|
+
// The caller therefore fixes them in advance and passes them here.
|
|
62
61
|
const hasReady = readyBy !== undefined && readyBy !== null && readyBy !== "";
|
|
63
62
|
const hasT1 = t1 !== undefined && t1 !== null && t1 !== "";
|
|
64
63
|
const readyByValue = hasReady ? Number(readyBy) : now + readyWindowSeconds;
|
|
65
|
-
// t1
|
|
66
|
-
//
|
|
64
|
+
// t1 is a WINDOW AFTER readyBy, not "from now": the flat formula with a 4-hour readiness window gave t1 earlier
|
|
65
|
+
// than readyBy. Same meaning as in the package's swap flow and the node's reverse order.
|
|
67
66
|
const t1Value = hasT1 ? Number(t1) : readyByValue + claimWindowSeconds;
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
if (!(t1Value > readyByValue)) throw new Error("
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
67
|
+
// THE ORDER INVARIANT IS CHECKED HERE: readiness is marked before readyBy, a claim is closed from t1.
|
|
68
|
+
// Coincidence or a swap would give a window where both paths are valid and gas would decide the dispute.
|
|
69
|
+
if (!(t1Value > readyByValue)) throw new Error("the settlement deadline must be later than the readiness deadline: t1=" + t1Value + ", readyBy=" + readyByValue);
|
|
70
|
+
// DEADLINES ARE CHECKED NOT ONLY FOR "LATER": an order with a past deadline or too tight a window can neither be
|
|
71
|
+
// marked ready nor claimed - money hanging until refund. The rule (and the thresholds) live in the guard module;
|
|
72
|
+
// here it is a refusal in words.
|
|
74
73
|
const deadlines = deadlineVerdict({ nowSec: now, readyBy: readyByValue, t1: t1Value, ...SIGNING_GUARDS });
|
|
75
|
-
if (deadlines.blocks) throw new Error("
|
|
74
|
+
if (deadlines.blocks) throw new Error("order deadlines are not usable: " + deadlines.reason);
|
|
76
75
|
return {
|
|
77
76
|
locker,
|
|
78
77
|
claimer,
|
|
79
|
-
commitHalfLocker, //
|
|
80
|
-
commitHalfClaimer, //
|
|
81
|
-
edPointLocker, // P_a:
|
|
78
|
+
commitHalfLocker, // commitment to the depositor's half
|
|
79
|
+
commitHalfClaimer, // commitment to the claimer's half
|
|
80
|
+
edPointLocker, // P_a: the other side assembles the joint address from it
|
|
82
81
|
edPointClaimer, // P_b
|
|
83
|
-
edViewPointLocker, // V_a:
|
|
82
|
+
edViewPointLocker, // V_a: the depositor's VIEW half (needed for the Monero address)
|
|
84
83
|
amount: String(amountWei),
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// помещался, поэтому 30 - по замеру, а не «на всякий случай». Замеры и источники: док 26.
|
|
84
|
+
// THE READINESS WINDOW IS ABOUT MONERO, NOT EVM. The maker locks the XMR only after funding, and marking
|
|
85
|
+
// readiness makes sense only once the XMR is really confirmed: Monero blocks are Poisson, about 2.5 minutes on
|
|
86
|
+
// average to the first confirmation, but one slow block easily gives 10-20. Plus the maker's own steps: wallet,
|
|
87
|
+
// fee, send. Fifteen minutes did not fit the tail, so 30 - by measurement, not "just in case".
|
|
90
88
|
readyBy: String(readyByValue),
|
|
91
|
-
t1: String(t1Value), //
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
89
|
+
t1: String(t1Value), // settlement boundary: one hour by default
|
|
90
|
+
// THE SALT ENTERS THE TERMS, IT IS NOT ADDED TO THEM. It determines the escrow address (CREATE2), and predict
|
|
91
|
+
// must compute the address FROM THE SAME FIELDS that go into creation. It used to be added only to the creation
|
|
92
|
+
// call while predict ran without it - and failed its own check in the calldata encoder. "One source of fields"
|
|
93
|
+
// was broken on exactly this field.
|
|
96
94
|
salt,
|
|
97
95
|
};
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
//
|
|
98
|
+
// Amount in wei from a human value (string) and the token decimals.
|
|
101
99
|
export function toWei(human, decimals = 18) {
|
|
102
100
|
const [intPart, fracPart = ""] = String(human).split(".");
|
|
103
101
|
const frac = (fracPart + "0".repeat(decimals)).slice(0, decimals);
|
|
104
102
|
return BigInt(intPart || "0") * 10n ** BigInt(decimals) + BigInt(frac || "0");
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
//
|
|
108
|
-
// createOrderAndFund
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
105
|
+
// WE SET THE GAS OURSELVES, WITH A MARGIN - A CURE FOR A REAL BUG, NOT CAUTION.
|
|
106
|
+
// createOrderAndFund goes as ONE transaction, but the wallet estimates it at an address where the escrow does NOT
|
|
107
|
+
// exist yet: instead of cloning and recording the deposit it counts a plain transfer (21000 plus data). The wallet
|
|
108
|
+
// estimate cannot be trusted here, so the limit is set explicitly, with a margin over the MEASURED value.
|
|
109
|
+
// MEASURED: a full createOrderAndFund after the move to EIP-1167 clones - 315 530 gas (the old whole contract was
|
|
110
|
+
// 1,190,683). The limit is about twice the measured value: a shortfall costs the person a signature, while spare
|
|
111
|
+
// gas in a limit costs almost nothing.
|
|
112
|
+
// EXPORTED ON PURPOSE: the buyer's gas reserve (evm/gasReserve.js) must account for THIS deposit on the path
|
|
113
|
+
// where the wallet holds no native of its own - there the deposit is paid AFTER the swap, not before it, so
|
|
114
|
+
// its gas is part of what the wallet has to be able to afford. One constant, one source (the check ties the
|
|
115
|
+
// reserve's copy to this declaration).
|
|
116
|
+
export const GAS_CREATE_ORDER_AND_FUND = 700_000n;
|
|
117
|
+
// THERE IS NO SEPARATE "CREATE WITHOUT A DEPOSIT" CONSTANT: no whole contract is created any more - each order's
|
|
118
|
+
// escrow is an EIP-1167 clone of one shared implementation, the terms live in the clone's code, and the factory
|
|
119
|
+
// only knows createOrderAndFund. The old lock(bytes) call and its gas constant are gone: the deposit rides in the
|
|
120
|
+
// same transaction.
|
|
118
121
|
|
|
119
|
-
//
|
|
120
|
-
// topics = [topic0, escrow, locker, claimer];
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
122
|
+
// The OrderCreated event: topic0 is computed from the actual signature (also checked by the watchtower);
|
|
123
|
+
// topics = [topic0, escrow, locker, claimer]; data carries both commitments, both points, the amount, readyBy, t1,
|
|
124
|
+
// the salt. THE EVENT TOPIC COMES FROM ONE PLACE - the factory module, not a second copy here. It USED to be a
|
|
125
|
+
// second copy here, which caused a real defect: after the OrderCreated field set changed, the stale copy failed to
|
|
126
|
+
// find the event and the escrow address silently fell back to the prediction - so the "address from the receipt"
|
|
127
|
+
// check stopped working without a word.
|
|
125
128
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
129
|
+
// The ACTUAL escrow address from the receipt. Returns null when the event is absent (the transaction is not in a
|
|
130
|
+
// block, or the factory is another) - then the caller must say so aloud rather than substitute a guess.
|
|
128
131
|
export function escrowFromReceipt(receipt) {
|
|
129
132
|
if (!receipt || !Array.isArray(receipt.logs)) return null;
|
|
130
133
|
for (const log of receipt.logs) {
|
|
@@ -137,7 +140,7 @@ export function escrowFromReceipt(receipt) {
|
|
|
137
140
|
return null;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
//
|
|
143
|
+
// Wait for a receipt: right after signing there is none, which is normal. Waiting forever is not allowed - we say so honestly.
|
|
141
144
|
async function waitReceipt(receiptReader, hash, { tries = 12, delayMs = 1500 } = {}) {
|
|
142
145
|
for (let i = 0; i < tries; i += 1) {
|
|
143
146
|
const r = await receiptReader({ hash }).catch(() => null);
|
|
@@ -147,113 +150,108 @@ async function waitReceipt(receiptReader, hash, { tries = 12, delayMs = 1500 } =
|
|
|
147
150
|
return null;
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
153
|
+
// The full funding path. Steps are returned outward so the interface can show each one: predict is a free call,
|
|
154
|
+
// createOrder and lock are transactions in the user's wallet.
|
|
152
155
|
export async function fundOrder({ factory, locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, salt: providedSalt, amountWei, amountLabel, claimWindowSeconds, readyWindowSeconds, readyBy, t1, quote, depositorSignature = null, call, send, receipt, onStep }) {
|
|
153
|
-
if (!factory) throw new Error("
|
|
154
|
-
if (!locker) throw new Error("
|
|
155
|
-
if (!claimer) throw new Error("
|
|
156
|
+
if (!factory) throw new Error("no escrow factory address set for this network");
|
|
157
|
+
if (!locker) throw new Error("no EVM wallet connected: there is nobody to deposit");
|
|
158
|
+
if (!claimer) throw new Error("the order recipient (claimer) is not set");
|
|
156
159
|
if (locker.toLowerCase() === claimer.toLowerCase()) {
|
|
157
|
-
throw new Error("locker
|
|
160
|
+
throw new Error("locker and claimer must not coincide: the contract rejects such an order");
|
|
158
161
|
}
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
if (!quote || typeof quote !== "object") throw new Error("
|
|
162
|
+
// A SIGNED QUOTE IS MANDATORY: it carries the fee, the registry address and the provider, without which the
|
|
163
|
+
// order cannot be created. The fee used to be asked of the factory - now there is nothing to ask; a missing
|
|
164
|
+
// quote is a NAMED refusal, not a silent funding of the wrong amount.
|
|
165
|
+
if (!quote || typeof quote !== "object") throw new Error("a signed quote is required: the factory takes a quote, not bare order terms");
|
|
163
166
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const salt = providedSalt || newSecret(); //
|
|
167
|
+
// COMMITMENTS AND POINTS ARE MANDATORY, and checked here and not deeper: without them the order is created but
|
|
168
|
+
// can neither be claimed nor have its half-to-Monero-address link proven. Let it fail loudly.
|
|
169
|
+
// The salt determines the escrow address and must exist BEFORE the terms are assembled: the terms also go to predict.
|
|
170
|
+
const salt = providedSalt || newSecret(); // CREATE2 salt: determines the escrow address
|
|
168
171
|
const terms = testOrderTerms({
|
|
169
172
|
locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker,
|
|
170
173
|
amountWei, salt, claimWindowSeconds, readyWindowSeconds, readyBy, t1,
|
|
171
174
|
});
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// с "order is not defined", причём падало ПОСЛЕ моей же правки, которая спрятала причину в логе.
|
|
180
|
-
// Локальную переменную назвал salt, входной параметр принят как providedSalt: иначе получилась бы
|
|
181
|
-
// "мёртвая зона" const salt = salt || ... - обращение к себе до объявления.
|
|
182
|
-
// 1) адрес эскроу виден ДО создания - это и есть смысл фабрики
|
|
175
|
+
// TAKE THE SALT FROM THE DEAL IF IT ALREADY HAS ONE. The salt determines the escrow address (CREATE2), so a
|
|
176
|
+
// re-run of funding with a new salt created a NEW escrow while the deal record kept the old address - the page,
|
|
177
|
+
// the explorer link and the progress screen showed an address not on chain. Re-using the salt also means a retry
|
|
178
|
+
// hits the SAME address and, if the order is already funded, simply fails instead of quietly creating a second
|
|
179
|
+
// escrow. The parameters are destructured, so there is no `order` variable here; the local is named `salt` and
|
|
180
|
+
// the input `providedSalt` to avoid a temporal-dead-zone self-reference.
|
|
181
|
+
// 1) the escrow address is visible BEFORE creation - that is the point of the factory
|
|
183
182
|
onStep?.("computing the escrow address");
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
183
|
+
// THE PREDICTION FIELDS MUST MATCH THE CREATION FIELDS BYTE FOR BYTE - the address is a hash of the creation
|
|
184
|
+
// code. Here that is guaranteed because both predict and createOrderAndFund are built from THE SAME terms: they
|
|
185
|
+
// can diverge only if one of the two places is edited alone. They already did once (the preimage was missing from
|
|
186
|
+
// the prediction); the cure is a shared source of fields, not care.
|
|
187
|
+
// THE DEPOSITOR'S FIELDS GO AS SEPARATE FACTORY ARGUMENTS (they are not in the signed set).
|
|
189
188
|
const quoteEnc = { ...quote, commitHalfLocker: terms.commitHalfLocker, edPointLocker: terms.edPointLocker, edViewPointLocker: terms.edViewPointLocker };
|
|
190
|
-
//
|
|
189
|
+
// QUOTE AND TERMS ARE ABOUT ONE THING: a divergence moves the escrow address and signs the wrong order.
|
|
191
190
|
for (const name of ["locker", "claimer", "amount", "readyBy", "t1", "salt"]) {
|
|
192
191
|
if (String(quote[name]).toLowerCase() !== String(terms[name]).toLowerCase()) {
|
|
193
|
-
throw new Error("
|
|
192
|
+
throw new Error("the quote and the order terms diverge on field " + name);
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
195
|
const predictedRaw = await call({ to: factory, data: encodePredict(quoteEnc) });
|
|
197
196
|
const escrow = decodeAddress(predictedRaw);
|
|
198
197
|
if (!/^0x[0-9a-fA-F]{40}$/.test(escrow) || /^0x0{40}$/i.test(escrow)) {
|
|
199
|
-
throw new Error("
|
|
198
|
+
throw new Error("the factory returned an invalid escrow address: " + String(predictedRaw).slice(0, 40));
|
|
200
199
|
}
|
|
201
200
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
201
|
+
// 1b) THE ESCROW STATE IS CHECKED BEFORE SIGNING, NOT AFTER. The address is known (predict above), and if it
|
|
202
|
+
// already holds a funded, claimed or refunded order, signing is not allowed: the contract forbids a second
|
|
203
|
+
// deposit (AlreadyFunded), while a new signature (with a new salt) would create a SECOND order instead of the old
|
|
204
|
+
// one - the money would go there, not where the person thinks.
|
|
206
205
|
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// вызова - другое состояние, и она запирает: "не проверили" нельзя считать за "всё хорошо".
|
|
206
|
+
// AN EMPTY ANSWER MEANS "NO ORDER", NOT "COULD NOT CHECK": for an address that does not exist yet eth_call
|
|
207
|
+
// returns empty. A CALL ERROR is a different state and it blocks: "did not check" must not count as "all good".
|
|
210
208
|
onStep?.("checking the order is not already on chain");
|
|
211
209
|
let stateRead = null;
|
|
212
210
|
try {
|
|
213
211
|
const raw = await call({ to: escrow, data: encodeStatus() });
|
|
214
212
|
const body = String(raw || "").replace(/^0x/, "");
|
|
215
|
-
//
|
|
216
|
-
//
|
|
213
|
+
// The escrow answer must be four status() words: shorter means no contract executing status() at this
|
|
214
|
+
// address, so there is no order there and cannot be one.
|
|
217
215
|
stateRead = body.length >= 256 ? { status: decodeStatus(raw) } : { status: null };
|
|
218
216
|
} catch (e) {
|
|
219
217
|
stateRead = { error: String((e && e.message) || e) };
|
|
220
218
|
}
|
|
221
219
|
const escrowState = escrowStateVerdict({ status: stateRead.status, error: stateRead.error, address: escrow });
|
|
222
|
-
if (escrowState.blocks) throw new Error("
|
|
220
|
+
if (escrowState.blocks) throw new Error("signature not started: " + escrowState.reason);
|
|
223
221
|
|
|
224
|
-
// 2)
|
|
222
|
+
// 2) ONE TRANSACTION: creating the order TOGETHER with the deposit.
|
|
225
223
|
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// `amount + fee
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
224
|
+
// It used to be two steps (createOrder, then lock) with two defects, both seen live: the wallet estimated the
|
|
225
|
+
// gas of the second transaction before code existed at the address (the limit came out as for a plain ETH
|
|
226
|
+
// transfer and it failed), and a funding failure left an empty contract on chain. Now a failure rolls back
|
|
227
|
+
// TOGETHER with creation, and only one signature is needed. Locker cannot be passed to this function - the sender
|
|
228
|
+
// becomes it, so the refund address always belongs to whoever paid.
|
|
229
|
+
// THE AMOUNT IS SHOWN IN ETH, NOT WEI, and taken as a ready string from the caller: amount formatting lives in
|
|
230
|
+
// one place in the project, and a second formatter here would diverge from it. The label is short on purpose: it
|
|
231
|
+
// is printed ON A BUTTON and must fit one line.
|
|
232
|
+
// THE FEE IS PART OF VALUE, AND THAT IS THE MAIN CONSEQUENCE OF ITS INTRODUCTION. The contract requires
|
|
233
|
+
// depositing EXACTLY `amount + fee`, so funding for the order amount alone is rejected - and rejected after
|
|
234
|
+
// signing, because the check lives in the contract. The order amount (`amount` in the terms) does NOT change: it
|
|
235
|
+
// is exactly what the claimer receives and what enters the terms hash. The fee is added on top, taken at claim,
|
|
236
|
+
// returned on refund.
|
|
239
237
|
onStep?.("reading the escrow fee from the signed quote");
|
|
240
|
-
//
|
|
241
|
-
//
|
|
238
|
+
// THE FEE COMES FROM THE QUOTE AND IS MIRROR-CHECKED. Direction is already accounted for: a buy is amount + fee
|
|
239
|
+
// (on top), a sell is amount (the fee is deducted from the claimer's payout).
|
|
242
240
|
const fee = feeTermsFromQuote(quote);
|
|
243
241
|
const totalValueWei = fee.totalWei;
|
|
244
242
|
const feeWords = fee.feeWei > 0n
|
|
245
243
|
? " (order " + amountWei + " + fee " + fee.feeWei + ")"
|
|
246
244
|
: "";
|
|
247
245
|
onStep?.("locking " + (amountLabel || "funds") + feeWords + " in one transaction - confirm in wallet");
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
246
|
+
// WHICH FACTORY DOOR THE DEPOSIT GOES THROUGH, AND IT IS NOT DECORATION. If a depositor signature is present, we
|
|
247
|
+
// deposit via part A: createOrderAndFundByDepositor recovers the depositor FROM THE SIGNATURE over the quote
|
|
248
|
+
// digest, so anyone may send the transaction (a relayer, our service) while the `locker` role (right to refund,
|
|
249
|
+
// right to mark readiness) stays with the signer. Without a signature, the old createOrderAndFund: one wallet
|
|
250
|
+
// deposits and sends, and the direct swap works as before. On neither path may the sender change the order terms
|
|
251
|
+
// - both signatures cover THE SAME quote digest.
|
|
254
252
|
if (depositorSignature !== null && depositorSignature !== undefined && depositorSignature !== "") {
|
|
255
253
|
const shape = depositorSignatureShape(depositorSignature);
|
|
256
|
-
if (!shape.ok) throw new Error("
|
|
254
|
+
if (!shape.ok) throw new Error("the depositor signature is unusable: " + shape.reason);
|
|
257
255
|
}
|
|
258
256
|
const relayedByDepositor = Boolean(depositorSignature);
|
|
259
257
|
const createData = relayedByDepositor
|
|
@@ -266,14 +264,14 @@ export async function fundOrder({ factory, locker, claimer, commitHalfLocker, co
|
|
|
266
264
|
gas: GAS_CREATE_ORDER_AND_FUND,
|
|
267
265
|
});
|
|
268
266
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
267
|
+
// 2b) THE ESCROW ADDRESS COMES FROM THE RECEIPT, NOT THE PREDICTION. The prediction is right only while the salt
|
|
268
|
+
// and the factory code match: a re-run of funding creates an escrow at a new address while the deal record keeps
|
|
269
|
+
// the old one - and the deal looks broken (deadlines unreadable, no refund button). That is exactly what
|
|
270
|
+
// happened. A divergence from the prediction is named aloud: it is a sign of a real error.
|
|
273
271
|
let actualEscrow = null;
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
272
|
+
// THE ESCROW'S BIRTH BLOCK is the height from which the deal exists on chain, and the only source of that height
|
|
273
|
+
// independent of the Monero node. Declared outside the branch on purpose: a variable declared inside a block is
|
|
274
|
+
// not visible outside, and it must always be returned.
|
|
277
275
|
let birthBlock = null;
|
|
278
276
|
if (typeof receipt === "function") {
|
|
279
277
|
const rc = await waitReceipt(receipt, createHash);
|
|
@@ -290,20 +288,20 @@ export async function fundOrder({ factory, locker, claimer, commitHalfLocker, co
|
|
|
290
288
|
}
|
|
291
289
|
const escrowToUse = actualEscrow || escrow;
|
|
292
290
|
|
|
293
|
-
// 3)
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
291
|
+
// 3) THE SECOND TRANSACTION IS GONE: the money left together with order creation. The name lockHash is kept on
|
|
292
|
+
// purpose - callers look at it, and renaming would touch them with no benefit. THERE IS AND WILL BE NO TOP-UP:
|
|
293
|
+
// the escrow has no separate funding function, and adding one would restore the "create empty, fund by a second
|
|
294
|
+
// call" path that locked money inside a closed order. An unfunded order shows in the receipt: an order is never
|
|
295
|
+
// created without money.
|
|
298
296
|
const lockHash = createHash;
|
|
299
297
|
|
|
300
|
-
// escrow
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
298
|
+
// escrow is the ACTUAL address (from the receipt, if there was one); predictedEscrow is kept so a divergence is
|
|
299
|
+
// visible outside, not only in the log.
|
|
300
|
+
// THE FEE GOES OUTWARD: it went into the transaction and the deal record must keep it - otherwise the record
|
|
301
|
+
// cannot tell how much the person deposited, and it would diverge from what the chain shows.
|
|
304
302
|
return { feeWei: fee.feeWei, feeTotalWei: totalValueWei, feeBps: fee.bps, feeWasLegacyFactory: Boolean(fee.legacy),
|
|
305
|
-
//
|
|
306
|
-
//
|
|
303
|
+
// WHICH FACTORY PATH RAN: visible outside, not only in the wallet's calldata. By it the checks and the
|
|
304
|
+
// deal record know whether the ETH returns to the signer and not to the sender.
|
|
307
305
|
depositorPath: relayedByDepositor,
|
|
308
306
|
escrow: escrowToUse, predictedEscrow: escrow, escrowMismatch: Boolean(actualEscrow && actualEscrow !== String(escrow).toLowerCase()), salt, birthBlock, terms, createHash, lockHash };
|
|
309
307
|
}
|