@ninsei-labs/sdk 0.39.2 → 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 +63 -0
- package/README.md +4 -1
- 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 +26 -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/codes.mjs +5 -0
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/index.mjs +16 -2
- 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 +23 -5
- package/src/swaps.mjs +18 -1
package/engine/evm/factory.js
CHANGED
|
@@ -1,51 +1,48 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/factory.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
|
+
// Calls to the NinseiSwap escrow factory contract.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
// (OrderQuote.Quote),
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// There are no EVM libraries in the project and we are not adding any. BOTH FACTORY FUNCTIONS NOW TAKE THE
|
|
8
|
+
// SIGNED QUOTE (OrderQuote.Quote), the signature as three words (r, s, v) and three depositor fields. The fee
|
|
9
|
+
// and the registry address are PART of the quote, not a contract setting: the factory no longer has feeBps()/
|
|
10
|
+
// feeFor() (the router was removed), so there is nothing to ask. All arguments are static, so calldata is a
|
|
11
|
+
// selector plus 32-byte words, built here in one place and checked mechanically.
|
|
13
12
|
//
|
|
14
13
|
// cast sig "predict((address,address,address,address,address,address,uint256,uint64,uint64,bytes32,bytes32,bytes32,uint256,uint256,uint256,address,uint64,uint256,uint256),bytes32,bytes32,uint8,bytes32,bytes32,bytes32)" -> 0x0824d205
|
|
15
|
-
// cast sig "createOrderAndFund(
|
|
14
|
+
// cast sig "createOrderAndFund(<same tuple>,...)" -> 0x4703a9d3
|
|
16
15
|
|
|
17
16
|
export const FACTORY_METHODS = {
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// THE QUOTE GREW THE xmrAmount FIELD: the XMR side of the deal is part of the signed set, so both selectors
|
|
18
|
+
// changed a second time (earlier for the cashier field).
|
|
20
19
|
predict: "0x0824d205",
|
|
21
|
-
//
|
|
20
|
+
// create the escrow AND deposit funds in ONE transaction (a value transfer with the call).
|
|
22
21
|
createOrderAndFund: "0x4703a9d3",
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// create the escrow and deposit when the DEPOSITOR is recovered FROM THE SIGNATURE over the quote digest:
|
|
23
|
+
// anyone may send the transaction, and `locker` is set to the signer.
|
|
25
24
|
createOrderAndFundByDepositor: "0x2acd187b",
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
//
|
|
27
|
+
// The OrderCreated event topic. It grew the registry address, the provider and the signing key:
|
|
29
28
|
// cast keccak "OrderCreated(address,address,address,bytes32,bytes32,bytes32,bytes32,bytes32,uint256,uint64,uint64,bytes32,address,address,address)"
|
|
30
|
-
//
|
|
31
|
-
// не могут, сверку держит проверка ABI.
|
|
29
|
+
// The topic lives in TWO places and both must match; a check pins them.
|
|
32
30
|
export const ORDER_CREATED_TOPIC =
|
|
33
31
|
"0xa22cdabaed3cce4f51dde71626a108ffe9c7bc4216fd14d5e719f892e2fd8ab0";
|
|
34
32
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
33
|
+
// QUOTE FIELDS IN CONTRACT ORDER. The order is part of the ABI: a swap gives another call. One list for both
|
|
34
|
+
// functions, so they cannot diverge.
|
|
37
35
|
export const QUOTE_FIELDS = [
|
|
38
36
|
"provider", "quoteKey", "registry", "cashier", "locker", "claimer", "amount", "readyBy", "t1", "salt",
|
|
39
37
|
"commitHalfClaimer", "edPointClaimer", "chainId", "feeBps", "fee", "feeRecipient", "validUntil", "nonce", "xmrAmount",
|
|
40
38
|
];
|
|
41
|
-
//
|
|
39
|
+
// The depositor fields are NOT in the quote: they go as separate arguments (into the escrow init code).
|
|
42
40
|
const LOCKER_HALF_FIELDS = ["commitHalfLocker", "edPointLocker", "edViewPointLocker"];
|
|
43
41
|
const ADDR_FIELDS = ["provider", "quoteKey", "registry", "cashier", "locker", "claimer", "feeRecipient"];
|
|
44
42
|
const B32_FIELDS = ["salt", "commitHalfClaimer", "edPointClaimer", "commitHalfLocker", "edPointLocker", "edViewPointLocker"];
|
|
45
43
|
|
|
46
|
-
//
|
|
47
|
-
// calldata
|
|
48
|
-
// уводило сумму в другое число. Поймано сверкой с эталоном Foundry.
|
|
44
|
+
// A calldata word: a value right-aligned to 32 bytes. Numbers arrive decimal (wei, deadlines) but must be hex in
|
|
45
|
+
// calldata: padding decimal digits as hex would change the number. Caught by comparison with a Foundry reference.
|
|
49
46
|
export function word(value, bytes = 32) {
|
|
50
47
|
const raw = String(value).trim();
|
|
51
48
|
let hex;
|
|
@@ -55,34 +52,34 @@ export function word(value, bytes = 32) {
|
|
|
55
52
|
} else if (/^[0-9]+$/.test(raw)) {
|
|
56
53
|
hex = BigInt(raw).toString(16);
|
|
57
54
|
} else {
|
|
58
|
-
throw new Error("
|
|
55
|
+
throw new Error("not a number and not hex: " + value);
|
|
59
56
|
}
|
|
60
|
-
if (!/^[0-9a-f]*$/.test(hex)) throw new Error("
|
|
61
|
-
if (hex.length > bytes * 2) throw new Error(
|
|
57
|
+
if (!/^[0-9a-f]*$/.test(hex)) throw new Error("not hex: " + value);
|
|
58
|
+
if (hex.length > bytes * 2) throw new Error(`value does not fit in ${bytes} bytes: ${value}`);
|
|
62
59
|
return hex.padStart(bytes * 2, "0");
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
62
|
+
// FORM IS NORMALISED, NOT DEMANDED: points and commitments in the engine record sit WITHOUT the 0x prefix, while
|
|
63
|
+
// word() requires it. Same bytes, so both forms encode identically.
|
|
67
64
|
const asHex32 = (v) => {
|
|
68
65
|
const x = String(v === undefined || v === null ? "" : v).trim();
|
|
69
66
|
return /^[0-9a-fA-F]{64}$/.test(x) ? "0x" + x.toLowerCase() : x;
|
|
70
67
|
};
|
|
71
68
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
69
|
+
// WORDS OF THE SIGNED SET. The signature is 65 bytes r || s || v; v is normalised to 27/28 (the contract takes
|
|
70
|
+
// both). r and s are FULL 32 bytes, v is a FULL 32-byte word (ABI uint8 is never shorter: a short word would shift
|
|
71
|
+
// the calldata tail and the call would go to a non-existent selector).
|
|
75
72
|
function quoteWords(quote) {
|
|
76
73
|
for (const name of QUOTE_FIELDS) {
|
|
77
|
-
if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("
|
|
74
|
+
if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("the quote has no field: " + name);
|
|
78
75
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
76
|
+
// FORM IS CHECKED, NOT PADDED: a short address or a truncated bytes32 would be silently zero-padded and reach
|
|
77
|
+
// the contract as a DIFFERENT value.
|
|
81
78
|
for (const name of ADDR_FIELDS) {
|
|
82
|
-
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(String(quote[name]).trim())) throw new Error("
|
|
79
|
+
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(String(quote[name]).trim())) throw new Error("quote field " + name + " is not an address");
|
|
83
80
|
}
|
|
84
81
|
for (const name of B32_FIELDS) {
|
|
85
|
-
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(String(quote[name]).trim())) throw new Error("
|
|
82
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(String(quote[name]).trim())) throw new Error("quote field " + name + " is not bytes32");
|
|
86
83
|
}
|
|
87
84
|
const map = {
|
|
88
85
|
provider: word(quote.provider, 32), quoteKey: word(quote.quoteKey, 32), registry: word(quote.registry, 32),
|
|
@@ -101,17 +98,17 @@ function quoteWords(quote) {
|
|
|
101
98
|
|
|
102
99
|
function signatureWords(signature) {
|
|
103
100
|
const hex = String(signature || "").replace(/^0x/, "");
|
|
104
|
-
if (!/^[0-9a-fA-F]{130}$/.test(hex)) throw new Error("
|
|
101
|
+
if (!/^[0-9a-fA-F]{130}$/.test(hex)) throw new Error("the quote signature must be 65 bytes: " + String(signature).slice(0, 20));
|
|
105
102
|
const r = hex.slice(0, 64), s = hex.slice(64, 128);
|
|
106
103
|
let v = parseInt(hex.slice(128, 130), 16);
|
|
107
104
|
if (v < 27) v += 27;
|
|
108
105
|
return word("0x" + r, 32) + word("0x" + s, 32) + word(v, 32);
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
//
|
|
108
|
+
// The common part of predict and createOrderAndFund.
|
|
112
109
|
function lockerHalfWords(quote) {
|
|
113
110
|
return LOCKER_HALF_FIELDS.map((name) => {
|
|
114
|
-
if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("
|
|
111
|
+
if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("the quote has no depositor field: " + name);
|
|
115
112
|
return word(asHex32(quote[name]), 32);
|
|
116
113
|
}).join("");
|
|
117
114
|
}
|
|
@@ -120,45 +117,44 @@ function quoteCallArgs(quote) {
|
|
|
120
117
|
return quoteWords(quote) + signatureWords(quote.signature) + lockerHalfWords(quote);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
120
|
+
// PREDICT THE ADDRESS. The argument set matches createOrderAndFund BYTE FOR BYTE (the address is the code hash of
|
|
121
|
+
// creation, and the signature is part of it).
|
|
125
122
|
export function encodePredict(quote) {
|
|
126
123
|
return FACTORY_METHODS.predict + quoteCallArgs(quote);
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
126
|
+
// CREATE AND DEPOSIT. The depositor becomes msg.sender; its address is already inside the quote and the factory
|
|
127
|
+
// checks it against the sender.
|
|
131
128
|
export function encodeCreateOrderAndFund(quote) {
|
|
132
129
|
return FACTORY_METHODS.createOrderAndFund + quoteCallArgs(quote);
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
132
|
+
// CREATE AND DEPOSIT when the depositor is recovered FROM ITS SIGNATURE over the quote digest. Anyone may send
|
|
133
|
+
// the transaction, so there are TWO signers: quote.signature (the provider, checked by the escrow) and
|
|
134
|
+
// depositorSignature (the depositor, checked by the factory, which requires the recovered address to equal
|
|
135
|
+
// quote.locker). Both over ONE digest.
|
|
139
136
|
export function encodeCreateOrderAndFundByDepositor(quote, depositorSignature) {
|
|
140
137
|
return FACTORY_METHODS.createOrderAndFundByDepositor + quoteWords(quote) +
|
|
141
138
|
signatureWords(quote.signature) + signatureWords(depositorSignature) + lockerHalfWords(quote);
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
//
|
|
141
|
+
// predict() answer - one address in a 32-byte word.
|
|
145
142
|
export function decodeAddress(returnedHex) {
|
|
146
143
|
const hex = String(returnedHex || "").replace(/^0x/, "");
|
|
147
|
-
if (hex.length < 64) throw new Error("
|
|
144
|
+
if (hex.length < 64) throw new Error("short answer: " + returnedHex);
|
|
148
145
|
return "0x" + hex.slice(24, 64);
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
|
|
152
149
|
// ============================================================================================
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// носитель (session.js: readContract/readCode), поэтому функция чистая и проверяется без сети.
|
|
150
|
+
// ESCROW IMPLEMENTATION: ITS ADDRESS AND CODE HASH. Every order escrow is an EIP-1167 CLONE of one shared
|
|
151
|
+
// implementation, and the implementation address and code hash live at the factory. The FACTORY checks the pair
|
|
152
|
+
// at order creation; the CLIENT must check the SAME pair, else the clone runs foreign code and the money paths
|
|
153
|
+
// "pass" for nothing (a delegatecall to an address without code SUCCEEDS). Here we only read fields and give a
|
|
154
|
+
// verdict; the chain calls are made by the carrier, so the function is pure.
|
|
159
155
|
// ============================================================================================
|
|
160
156
|
|
|
161
|
-
//
|
|
157
|
+
// Selectors are computed by a tool, not by hand:
|
|
162
158
|
// cast sig "implementation()" -> 0x5c60da1b
|
|
163
159
|
// cast sig "implementationCodeHash()" -> 0xbc0a3981
|
|
164
160
|
export const FACTORY_READS = {
|
|
@@ -166,32 +162,32 @@ export const FACTORY_READS = {
|
|
|
166
162
|
implementationCodeHash: "0xbc0a3981",
|
|
167
163
|
};
|
|
168
164
|
|
|
169
|
-
//
|
|
165
|
+
// Reading a factory field with no arguments: calldata is the selector.
|
|
170
166
|
export function encodeFactoryRead(name) {
|
|
171
167
|
const sel = FACTORY_READS[name];
|
|
172
|
-
if (typeof sel !== "string" || !/^0x[0-9a-f]{8}$/.test(sel)) throw new Error("
|
|
168
|
+
if (typeof sel !== "string" || !/^0x[0-9a-f]{8}$/.test(sel)) throw new Error("no such factory read: " + name);
|
|
173
169
|
return sel;
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
// implementation() -
|
|
172
|
+
// implementation() - one address in a 32-byte word.
|
|
177
173
|
export function decodeImplementation(returnedHex) {
|
|
178
174
|
return decodeAddress(returnedHex);
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
// implementationCodeHash() -
|
|
177
|
+
// implementationCodeHash() - one bytes32 word.
|
|
182
178
|
export function decodeImplementationCodeHash(returnedHex) {
|
|
183
179
|
const hex = String(returnedHex || "").replace(/^0x/, "");
|
|
184
|
-
if (hex.length < 64) throw new Error("
|
|
180
|
+
if (hex.length < 64) throw new Error("short answer from implementationCodeHash(): " + returnedHex);
|
|
185
181
|
return "0x" + hex.slice(0, 64).toLowerCase();
|
|
186
182
|
}
|
|
187
183
|
|
|
188
|
-
//
|
|
189
|
-
// 1)
|
|
190
|
-
//
|
|
191
|
-
// 2)
|
|
192
|
-
// 3) recorded
|
|
193
|
-
//
|
|
194
|
-
//
|
|
184
|
+
// VERDICT ON THE IMPLEMENTATION - the same three conditions the factory checks:
|
|
185
|
+
// 1) the implementation address is named and its CODE on chain is non-empty: a delegatecall to an address without
|
|
186
|
+
// code succeeds, so a clone without an implementation would take ETH and markReady/claim/refund would "pass";
|
|
187
|
+
// 2) the implementation code hash equals the factory recorded hash (compared by the factory via codehash);
|
|
188
|
+
// 3) the recorded hash is in the interface policy (a list of known builds), if set.
|
|
189
|
+
// If hashCode is given (keccak256 of the code bytes), the live code is compared too: the verdict then recomputes
|
|
190
|
+
// the hash from eth_getCode.
|
|
195
191
|
export function verifyFactoryImplementation({ implementation, implementationCodeHash, code, hashCode, policy } = {}) {
|
|
196
192
|
if (!implementation || !/^0x[0-9a-fA-F]{40}$/.test(String(implementation))) {
|
|
197
193
|
return { ok: false, why: "implementation-missing" };
|
|
@@ -215,13 +211,11 @@ export function verifyFactoryImplementation({ implementation, implementationCode
|
|
|
215
211
|
return { ok: true, codeHash: pinned };
|
|
216
212
|
}
|
|
217
213
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// чтения (SDK - evmCall/evmCode кошелька страницы, узел и сторож - свой RPC). hashCode, если передан,
|
|
224
|
-
// пересчитывает живой код: тогда вердикт не доверяет одному лишь ответу фабрики.
|
|
214
|
+
// READING THE PAIR AT THE FACTORY + VERDICT - ONE PLACE FOR ALL CARRIERS (SDK, node, watchtower). It reads the
|
|
215
|
+
// factory fields (implementation()/implementationCodeHash()) and the CODE at the implementation address, then
|
|
216
|
+
// applies the same verdict the factory applies at order creation (verifyFactoryImplementation above). The chain
|
|
217
|
+
// is injected: read - eth_call({to,data}) -> hex, code - eth_getCode(address) -> hex. The module stays pure: no
|
|
218
|
+
// import, no network call. Carriers pass their OWN reads. hashCode, if given, recomputes the live code.
|
|
225
219
|
export async function verifyFactoryImplementationOnChain({ factory, read, code, policy = null, hashCode = null } = {}) {
|
|
226
220
|
if (!factory || !/^0x[0-9a-fA-F]{40}$/.test(String(factory))) {
|
|
227
221
|
return { ok: false, why: "factory-missing" };
|
|
@@ -233,8 +227,8 @@ export async function verifyFactoryImplementationOnChain({ factory, read, code,
|
|
|
233
227
|
implRaw = await read({ to: factory, data: FACTORY_READS.implementation });
|
|
234
228
|
hashRaw = await read({ to: factory, data: FACTORY_READS.implementationCodeHash });
|
|
235
229
|
} catch {
|
|
236
|
-
//
|
|
237
|
-
//
|
|
230
|
+
// READING THE PAIR FAILED - A NAMED REFUSAL, NOT SILENCE. An old factory (without these getters) reverts:
|
|
231
|
+
// the pair is unread, there is no verdict, and the carrier must refuse.
|
|
238
232
|
return { ok: false, why: "implementation-unreadable", factory: String(factory).toLowerCase() };
|
|
239
233
|
}
|
|
240
234
|
let implementation, implementationCodeHash;
|
package/engine/evm/fees.js
CHANGED
|
@@ -1,90 +1,89 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/fees.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
|
+
// The NinseiSwap fee: PART OF THE SIGNED QUOTE (OrderQuote v7), not a contract setting.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// *
|
|
13
|
-
// *
|
|
14
|
-
// *
|
|
15
|
-
//
|
|
16
|
-
// (WrongAmount/BadFee)
|
|
7
|
+
// WHERE THE RATE LIVES. Before, it and the recipient were set by the FACTORY (or router) in immutable fields, and
|
|
8
|
+
// the client asked the chain for them (feeBps()/feeRecipient()/feeFor()). Now the contract has NO such functions:
|
|
9
|
+
// the router was removed, and the rate, amount and recipient of the fee are part of the SIGNED set
|
|
10
|
+
// of order terms. So the source of truth is ONE - the quote - and there must be no second place:
|
|
11
|
+
// * the page settings hold NO rate; a copy would diverge from the signed set;
|
|
12
|
+
// * there is nothing to ask the factory - it knows nothing about the fee;
|
|
13
|
+
// * the amount in wei is computed by the formula `amount * feeBps / 10000` (rounding DOWN) and here it is verified
|
|
14
|
+
// against that same formula by the MIRROR: a divergence must stop the signature, not turn into an escrow
|
|
15
|
+
// refusal (WrongAmount/BadFee) after it.
|
|
17
16
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// WHO PAYS. The fee is paid by the USER OF THE INTERFACE. The direction is visible from the quote roles: the
|
|
18
|
+
// provider takes ETH - a buy (fee ON TOP of the deposit), the provider deposits ETH - a sell (fee DEDUCTED from
|
|
19
|
+
// the payout, `amount - fee`). The provider is named in neither - nothing to tell, refusal.
|
|
21
20
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// ONE UNIT OF TIME. Time in the project is SECONDS (order deadlines, quote validity, block.timestamp):
|
|
22
|
+
// converting between scales was the only place where an error by a thousand passes silently.
|
|
24
23
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// The rate ceiling hard-wired in the contract (NinseiEscrow.MAX_FEE_BPS = 200 = 2%). Needed here to tell a
|
|
25
|
+
// meaningful rate from a corrupted one: reading garbage as a rate and computing a fee from it is not allowed.
|
|
27
26
|
export const MAX_FEE_BPS = 200;
|
|
28
27
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// THE MIRROR OF THE CONTRACT FORMULA. One place for the whole page: a second copy would diverge from the first
|
|
29
|
+
// exactly where it is most expensive - on the amount the escrow requires.
|
|
31
30
|
export function feeWeiFor(amountWei, feeBps) {
|
|
32
31
|
const amount = BigInt(amountWei === null || amountWei === undefined || amountWei === "" ? 0 : amountWei);
|
|
33
32
|
const bps = BigInt(Number(feeBps) || 0);
|
|
34
|
-
if (amount < 0n || bps < 0n) throw new Error("
|
|
35
|
-
return (amount * bps) / 10000n; //
|
|
33
|
+
if (amount < 0n || bps < 0n) throw new Error("fee: negative amount or rate");
|
|
34
|
+
return (amount * bps) / 10000n; // DOWN, as in Solidity: amount * feeBps / 10000
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const sameAddr = (a, b) => String(a || "").toLowerCase() === String(b || "").toLowerCase();
|
|
39
38
|
const zeroAddr = (a) => !a || /^0x0{40}$/i.test(String(a));
|
|
40
39
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
40
|
+
// MEMORY OF THE LAST SIGNED QUOTE. The rate becomes known ONLY with the quote; before it there is none,
|
|
41
|
+
// and showing zero or a config value instead would be lying. The checks insert their own values
|
|
42
|
+
// via resetFeeCache + feeTermsFromQuote.
|
|
44
43
|
let lastTerms = null;
|
|
45
44
|
|
|
46
|
-
//
|
|
45
|
+
// RESET THE MEMORY. Needed by the checks and by any retry after the quote changes.
|
|
47
46
|
export function resetFeeCache() {
|
|
48
47
|
lastTerms = null;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
50
|
+
// FEE FROM THE QUOTE. Returns the rate, the amount (verified by the mirror), the recipient and the direction;
|
|
51
|
+
// with a non-zero rate and no recipient - a refusal (the fee would burn). The result is kept in memory for the screen.
|
|
53
52
|
export function feeTermsFromQuote(quote) {
|
|
54
|
-
if (!quote || typeof quote !== "object") throw new Error("
|
|
53
|
+
if (!quote || typeof quote !== "object") throw new Error("no signed quote: there is nowhere to take the fee and the registry address");
|
|
55
54
|
const amount = BigInt(String(quote.amount));
|
|
56
55
|
const bps = Number(quote.feeBps === undefined || quote.feeBps === null ? 0 : quote.feeBps);
|
|
57
|
-
if (!Number.isFinite(bps) || bps < 0 || bps > MAX_FEE_BPS) throw new Error("
|
|
56
|
+
if (!Number.isFinite(bps) || bps < 0 || bps > MAX_FEE_BPS) throw new Error("fee rate outside the ceiling: " + quote.feeBps);
|
|
58
57
|
const fee = feeWeiFor(amount, bps);
|
|
59
58
|
if (BigInt(String(quote.fee === undefined || quote.fee === null ? 0 : quote.fee)) !== fee) {
|
|
60
|
-
throw new Error("
|
|
59
|
+
throw new Error("signed fee " + quote.fee + " diverges from the formula amount * rate (" + fee + ") - signature not started");
|
|
61
60
|
}
|
|
62
|
-
if (bps !== 0 && zeroAddr(quote.feeRecipient)) throw new Error("
|
|
61
|
+
if (bps !== 0 && zeroAddr(quote.feeRecipient)) throw new Error("with a non-zero rate the fee recipient must be named");
|
|
63
62
|
const providerIsLocker = sameAddr(quote.locker, quote.provider);
|
|
64
63
|
const providerIsClaimer = sameAddr(quote.claimer, quote.provider);
|
|
65
|
-
if (providerIsLocker === providerIsClaimer) throw new Error("
|
|
66
|
-
const feeOnTop = providerIsClaimer; //
|
|
64
|
+
if (providerIsLocker === providerIsClaimer) throw new Error("cannot tell who pays the fee: the provider is named neither as depositor nor as claimer");
|
|
65
|
+
const feeOnTop = providerIsClaimer; // the provider takes ETH -> a buy -> fee on top of the deposit
|
|
67
66
|
const terms = { bps, feeWei: fee, totalWei: feeOnTop ? amount + fee : amount, recipient: quote.feeRecipient || null, feeOnTop, legacy: false };
|
|
68
67
|
lastTerms = terms;
|
|
69
68
|
return terms;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
71
|
+
// SYNCHRONOUS ACCESS TO WHAT WAS READ. Empty means "not read yet or could not": this is NOT zero, and
|
|
72
|
+
// the caller must close the signature, not count the fee as zero.
|
|
74
73
|
export function cachedFeeTerms() {
|
|
75
74
|
return lastTerms;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
//
|
|
79
|
-
// null,
|
|
77
|
+
// RATE FOR DISPLAY. The only place the page takes the number for the price-breakdown line from. No answer -
|
|
78
|
+
// null, and the screen says the rate is not known yet.
|
|
80
79
|
export function cachedFeeRate() {
|
|
81
80
|
if (!lastTerms || !Number.isFinite(Number(lastTerms.bps))) return null;
|
|
82
81
|
return Number(lastTerms.bps) / 10000;
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
84
|
+
// THIS FUNCTION USED TO READ THE RATE FROM THE CHAIN. Now there is nothing to read: the rate is in the signed
|
|
85
|
+
// quote and arrives with it (feeTermsFromQuote). The function is kept for the screens that call it: before the
|
|
86
|
+
// quote it honestly returns what is already known (usually null - "rate unknown"), and invents nothing.
|
|
88
87
|
export async function ensureFeeTerms() {
|
|
89
88
|
return lastTerms;
|
|
90
89
|
}
|