@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -1
  3. package/engine/atomic/dleq2.js +86 -111
  4. package/engine/atomic/halfenc.js +58 -72
  5. package/engine/atomic/halves.js +31 -53
  6. package/engine/atomic/order-client.js +15 -16
  7. package/engine/atomic/order.js +44 -62
  8. package/engine/atomic/orderContext.js +26 -50
  9. package/engine/atomic/orderHalfCommit.js +17 -18
  10. package/engine/core/chainSource.js +54 -69
  11. package/engine/core/clock.js +11 -12
  12. package/engine/core/config.js +554 -449
  13. package/engine/core/finality.js +34 -53
  14. package/engine/core/format.js +35 -36
  15. package/engine/core/preSignGuards.js +109 -117
  16. package/engine/core/rfqSource.js +57 -75
  17. package/engine/core/sellWindow.js +23 -24
  18. package/engine/core/store.js +20 -21
  19. package/engine/core/swap.js +242 -243
  20. package/engine/evm/amounts.js +50 -51
  21. package/engine/evm/asyncExec.js +577 -0
  22. package/engine/evm/asyncLegs.js +308 -0
  23. package/engine/evm/asyncRoute.js +179 -0
  24. package/engine/evm/auth.js +41 -48
  25. package/engine/evm/claimGas.js +28 -47
  26. package/engine/evm/depositor.js +39 -43
  27. package/engine/evm/dex.js +266 -267
  28. package/engine/evm/escrow.js +113 -129
  29. package/engine/evm/factory.js +71 -77
  30. package/engine/evm/fees.js +41 -42
  31. package/engine/evm/funding.js +160 -162
  32. package/engine/evm/gasReserve.js +67 -36
  33. package/engine/evm/index.js +46 -53
  34. package/engine/evm/injected.js +34 -42
  35. package/engine/evm/permit.js +129 -56
  36. package/engine/evm/prices.js +30 -45
  37. package/engine/evm/session.js +188 -189
  38. package/engine/mock/chain.js +17 -18
  39. package/engine/monero/address.js +33 -48
  40. package/engine/monero/halvesSweep.js +25 -43
  41. package/engine/monero/node.js +14 -15
  42. package/engine/monero/scan.js +82 -95
  43. package/engine/monero/swapKeys.js +30 -33
  44. package/engine/monero/unlock.js +17 -20
  45. package/engine/monero/wallet.js +32 -42
  46. package/engine/recovery/recoveryFile.js +154 -161
  47. package/engine/recovery/restore.js +50 -61
  48. package/engine/sdk/bridge.js +244 -234
  49. package/engine/ui/dom.js +4 -5
  50. package/engine/ui/walletModal.js +20 -31
  51. package/index.d.ts +26 -0
  52. package/package.json +1 -1
  53. package/src/adapters/evm-wallet.mjs +16 -0
  54. package/src/adapters/monero-wallet.mjs +1 -1
  55. package/src/adapters/monero.mjs +1 -1
  56. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  57. package/src/codes.mjs +5 -0
  58. package/src/config.mjs +1 -1
  59. package/src/engine.mjs +4 -0
  60. package/src/index.mjs +16 -2
  61. package/src/legs/cow-spec.mjs +254 -0
  62. package/src/legs/cow.mjs +315 -0
  63. package/src/legs/evm.mjs +86 -2
  64. package/src/legs/kyber-spec.mjs +265 -0
  65. package/src/legs/kyber.mjs +425 -0
  66. package/src/legs/shape.mjs +35 -0
  67. package/src/legs/uniswaprelay-spec.mjs +541 -0
  68. package/src/legs/uniswaprelay.mjs +422 -0
  69. package/src/legs/uniswapx-spec.mjs +329 -0
  70. package/src/legs/uniswapx.mjs +362 -0
  71. package/src/quotes.mjs +23 -5
  72. package/src/swaps.mjs +18 -1
@@ -0,0 +1,541 @@
1
+ // EIP-712 TYPED DATA FOR A UNISWAP RELAY ORDER, AND THE UNIVERSALROUTER CALLDATA - THE SPEC, IN THE SDK'S OWN STYLE.
2
+ //
3
+ // WHY A SEPARATE SPEC AND NOT A NEW LIBRARY. This repository builds EIP-712 typed data itself (the same pattern as
4
+ // src/legs/cow-spec.mjs, kyber-spec.mjs and uniswapx-spec.mjs): the domain separator, the struct hashes and the
5
+ // digest are assembled here from the repository's own crypto primitives (src/primitives.mjs), and the SIGNING reuses
6
+ // the existing signer (signQuoteDigest, which signs ANY 32-byte digest). No dependency is added.
7
+ //
8
+ // WHAT THIS IS. Uniswap's own repository Uniswap/relayer ships contracts that RELAY a transaction to the
9
+ // UniversalRouter in exchange for ERC-20 tokens. A swapper signs a RelayOrder; whoever holds that signature (the
10
+ // "relayer") calls RelayOrderReactor.execute(SignedOrder, address feeRecipient), pays the gas, and is reimbursed in
11
+ // the input token through the order's own fee. That is the KEY-FREE route: no hosted service, no API key.
12
+ //
13
+ // EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE, and where a value can be recomputed it is (the check does it):
14
+ // * the RelayOrder struct and its field order - Uniswap/relayer, src/base/ReactorStructs.sol:
15
+ // RelayOrder { RelayOrderInfo info; Input input; FeeEscalator fee; bytes universalRouterCalldata; };
16
+ // RelayOrderInfo { address reactor; address swapper; uint256 nonce; uint256 deadline; };
17
+ // Input { address token; uint256 amount; address recipient; };
18
+ // FeeEscalator { address token; uint256 startAmount; uint256 endAmount; uint256 startTime; uint256 endTime; };
19
+ // * the SAME layout in TypeScript - Uniswap/uniswapx-sdk, src/order/RelayOrder.ts (RELAY_WITNESS_TYPES), which is
20
+ // what Uniswap's own tooling signs;
21
+ // * the DOMAIN the swapper signs is PERMIT2's, not the reactor's: a relay order is a Permit2
22
+ // permitWitnessTransferFrom with the order as the WITNESS, batch flavour (two permitted tokens - the input and
23
+ // the fee). Uniswap/permit2, src/EIP712.sol: _TYPE_HASH = keccak256(EIP712Domain(string name,uint256
24
+ // chainId,address verifyingContract)), _HASHED_NAME = keccak256(Permit2) - there is no version field.
25
+ // The permit fields and the witness come from Uniswap/relayer src/lib/RelayOrderLib.sol (toTokenPermissions /
26
+ // toTransferDetails / transferInputTokens) and Uniswap/uniswapx-sdk src/order/RelayOrder.ts (toPermit:
27
+ // permitted = [{input.token, input.amount}, {fee.token, fee.endAmount}], spender = the reactor);
28
+ // * THE WITNESS TYPE STRING. Permit2 does not hash the primary type alone: hashWithWitness computes
29
+ // keccak256(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB ++ witnessTypeString)
30
+ // (Uniswap/permit2, src/libraries/PermitHash.sol). The stub is
31
+ // PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,
32
+ // and the witness type string is built by Uniswap/relayer src/lib/RelayOrderLib.sol PERMIT2_ORDER_TYPE as
33
+ // RelayOrder witness) ++ FeeEscalator ++ Input ++ RelayOrder ++ RelayOrderInfo ++ TokenPermissions, in
34
+ // EIP-712's alphabetical dependency order. Signing with only the primary part would produce a digest no permit2
35
+ // call ever verifies;
36
+ // * THE FEE. The fee is a FeeEscalator and is paid to `feeRecipient` - the address the RELAYER names in the
37
+ // execute() call, or the caller itself when the two-argument overload is used (Uniswap/relayer README
38
+ // RelayOrderReactor and src/reactors/RelayOrderReactor.sol: execute(SignedOrder) calls
39
+ // execute(signedOrder, msg.sender)). The amount actually transferred is resolved on the escalation curve at
40
+ // fill time (src/lib/FeeEscalatorLib.sol resolve); the permit signs the END amount (toTokenPermissions);
41
+ // * THE INPUT TOKEN PULL: the reactor pulls BOTH the input and the fee from the SWAPPER through Permit2's
42
+ // permitWitnessTransferFrom (src/lib/RelayOrderLib.sol transferInputTokens): input goes to input.recipient, the
43
+ // fee goes to feeRecipient. So the swapper grants a Permit2 allowance (an ERC-20 approve to Permit2) and signs
44
+ // the permit - there is no other approval mechanism;
45
+ // * THE UNIVERSALROUTER CALLDATA. The reactor makes exactly one call with order.universalRouterCalldata, and only
46
+ // when it is non-empty (src/reactors/RelayOrderReactor.sol). Uniswap's own integration tests build that calldata
47
+ // for token -> native as V3_SWAP_EXACT_IN (0x00) followed by UNWRAP_WETH (0x0c)
48
+ // (Uniswap/relayer test/integration-tests/RelayOrderReactor.test.ts basic v3 swap to native), and the command
49
+ // bytes come from Uniswap/universal-router contracts/libraries/Commands.sol. The UniversalRouter command
50
+ // placeholders ADDRESS_THIS (0x...02) and the payerIsUser=false flag come from the same tests
51
+ // (test/integration-tests/shared/constants.ts).
52
+ //
53
+ // Multi-word phrases below are built by concatenation from single-token parts, never written as one literal: the SDK
54
+ // reads such literals as user-facing text (see the demo's own www-tools check-sdk).
55
+ import { keccak256, secp256k1 } from "../primitives.mjs";
56
+ import { signQuoteDigest } from "../quoteEip712Spec.mjs";
57
+
58
+ // The signing domain is Permit2's (see the header). Name and type from Uniswap/permit2 src/EIP712.sol.
59
+ export const UNISWAPRELAY_DOMAIN_NAME = "Permit2";
60
+
61
+ // The Permit2 contract - the same address on every supported chain (Uniswap/permit2 deployments; the reactor's own
62
+ // PERMIT2 constant in Uniswap/relayer src/reactors/RelayOrderReactor.sol).
63
+ export const UNISWAPRELAY_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
64
+
65
+ // The sentinel for "this output is native coin", the same address(0) the UniversalRouter unwraps to.
66
+ export const UNISWAPRELAY_NATIVE = "0x0000000000000000000000000000000000000000";
67
+
68
+ // THE UNIVERSALROUTER PLACEHOLDER "THIS CONTRACT" - where a swap must send its output so that the following
69
+ // UNWRAP_WETH can take the whole wrapped balance (Uniswap/universal-router Constants.sol ADDRESS_THIS; the same
70
+ // placeholder the demo's own DEX leg uses, www/js/evm/dexExec.js DEX_EXEC_PLACEHOLDER).
71
+ export const UNISWAPRELAY_ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
72
+
73
+ // THE UNIVERSALROUTER ENTRY POINT execute(bytes,bytes[]) - the selector Uniswap's own relayer fixtures carry
74
+ // (0x24856bc3, test/foundry-tests/interop.json) and the ABI in test/.../RelayOrderReactor.test.ts uses.
75
+ export const UNISWAPRELAY_EXECUTE_SELECTOR = "0x24856bc3";
76
+
77
+ // THE COMMAND BYTES the UniversalRouter decodes (Uniswap/universal-router contracts/libraries/Commands.sol). Only
78
+ // the four this route can need are named; the values are the library's own constants.
79
+ export const UNISWAPRELAY_UR_COMMAND = Object.freeze({
80
+ v3SwapExactIn: 0x00,
81
+ sweep: 0x04,
82
+ wrapEth: 0x0b,
83
+ unwrapWeth: 0x0c,
84
+ });
85
+
86
+ // The V3 pool fee is a 3-byte field in the swap path (Uniswap/universal-router Path.sol / uniswapx-sdk
87
+ // test/integration-tests/shared/swapRouter02Helpers.ts encodePath).
88
+ export const UNISWAPRELAY_PATH_FEE_BYTES = 3;
89
+
90
+ // The domain type, assembled from single-token parts so no literal here is a multi-word phrase. NO version field -
91
+ // that is Permit2's own domain, not a stripped-down one.
92
+ const DOMAIN_TYPE_PARTS = ["EIP712Domain(", "string name,", "uint256 chainId,", "address verifyingContract)"];
93
+ export const UNISWAPRELAY_DOMAIN_TYPE = DOMAIN_TYPE_PARTS.join("");
94
+
95
+ // The message, field by field and IN THIS ORDER - the order is the protocol (ReactorStructs.sol / RELAY_WITNESS_TYPES).
96
+ export const UNISWAPRELAY_INFO_FIELDS = [
97
+ ["reactor", "address"],
98
+ ["swapper", "address"],
99
+ ["nonce", "uint256"],
100
+ ["deadline", "uint256"],
101
+ ];
102
+ export const UNISWAPRELAY_INPUT_FIELDS = [
103
+ ["token", "address"],
104
+ ["amount", "uint256"],
105
+ ["recipient", "address"],
106
+ ];
107
+ export const UNISWAPRELAY_FEE_FIELDS = [
108
+ ["token", "address"],
109
+ ["startAmount", "uint256"],
110
+ ["endAmount", "uint256"],
111
+ ["startTime", "uint256"],
112
+ ["endTime", "uint256"],
113
+ ];
114
+ export const UNISWAPRELAY_ORDER_FIELDS = [
115
+ ["info", "RelayOrderInfo"],
116
+ ["input", "Input"],
117
+ ["fee", "FeeEscalator"],
118
+ ["universalRouterCalldata", "bytes"],
119
+ ];
120
+ export const UNISWAPRELAY_TOKEN_PERMISSIONS_FIELDS = [
121
+ ["token", "address"],
122
+ ["amount", "uint256"],
123
+ ];
124
+ // The Permit2 wrapper the wallet actually signs: the order rides as `witness`, and `permitted` is a LIST (the input
125
+ // and the fee). Primary type = PermitBatchWitnessTransferFrom (Uniswap/relayer RelayOrderLib.transferInputTokens).
126
+ export const UNISWAPRELAY_PERMIT_FIELDS = [
127
+ ["permitted", "TokenPermissions[]"],
128
+ ["spender", "address"],
129
+ ["nonce", "uint256"],
130
+ ["deadline", "uint256"],
131
+ ["witness", "RelayOrder"],
132
+ ];
133
+
134
+ // A type string from its fields, so the field list is the single source of truth (the space between a type and its
135
+ // name is inserted here, never written inside a literal).
136
+ const typeOf = (name, fields) => name + "(" + fields.map((f) => f[1] + " " + f[0]).join(",") + ")";
137
+
138
+ export const UNISWAPRELAY_INFO_TYPE = typeOf("RelayOrderInfo", UNISWAPRELAY_INFO_FIELDS);
139
+ export const UNISWAPRELAY_INPUT_TYPE = typeOf("Input", UNISWAPRELAY_INPUT_FIELDS);
140
+ export const UNISWAPRELAY_FEE_TYPE = typeOf("FeeEscalator", UNISWAPRELAY_FEE_FIELDS);
141
+ export const UNISWAPRELAY_ORDER_TYPE = typeOf("RelayOrder", UNISWAPRELAY_ORDER_FIELDS);
142
+ export const UNISWAPRELAY_TOKEN_PERMISSIONS_TYPE = typeOf("TokenPermissions", UNISWAPRELAY_TOKEN_PERMISSIONS_FIELDS);
143
+
144
+ // The reactor's OWN hashing type string: RelayOrderLib.FULL_RELAY_ORDER_TYPESTRING = the order type ++ FeeEscalator
145
+ // ++ Input ++ RelayOrderInfo (the nested types in alphabetical order).
146
+ export const UNISWAPRELAY_HASHED_ORDER_TYPE = UNISWAPRELAY_ORDER_TYPE
147
+ + UNISWAPRELAY_FEE_TYPE + UNISWAPRELAY_INPUT_TYPE + UNISWAPRELAY_INFO_TYPE;
148
+
149
+ // The FULL permit type string Permit2 hashes: the batch witness stub (closed by the witness name) ++ the witness
150
+ // types. RelayOrderLib.PERMIT2_ORDER_TYPE, verbatim in that order.
151
+ export const UNISWAPRELAY_PERMIT_TYPE = typeOf("PermitBatchWitnessTransferFrom", UNISWAPRELAY_PERMIT_FIELDS)
152
+ + UNISWAPRELAY_FEE_TYPE + UNISWAPRELAY_INPUT_TYPE + UNISWAPRELAY_ORDER_TYPE
153
+ + UNISWAPRELAY_INFO_TYPE + UNISWAPRELAY_TOKEN_PERMISSIONS_TYPE;
154
+
155
+ // THE REACTOR'S OWN CONSTANT (RelayOrderLib.FULL_RELAY_ORDER_TYPEHASH = keccak256(FULL_RELAY_ORDER_TYPESTRING)) and
156
+ // Permit2's type hash for this witness (keccak256(PERMIT2_ORDER_TYPE)). They are NOT trusted: the check recomputes
157
+ // keccak256(...) and demands they equal these - a typo in a field list would sign orders no reactor accepts.
158
+ export const UNISWAPRELAY_ORDER_TYPE_HASH = "0x356a26e7c5955b7b24a8bff954c96c53622e6a8471513bb66b69bc7069243ff0";
159
+ export const UNISWAPRELAY_PERMIT_TYPE_HASH = "0x918f75c8e25ec281e8dc3255229111f447dc1c210102666cc63cd9da2dee620c";
160
+
161
+ // Error tokens are SINGLE WORDS (the SDK forbids multi-word literals in sdk/src): the spec names a failure by token
162
+ // and the caller maps it to its own code.
163
+ const boom = (token) => { throw new Error(token); };
164
+
165
+ const utf8 = (s) => new TextEncoder().encode(String(s));
166
+ const concat = (...arrays) => {
167
+ const total = arrays.reduce((n, a) => n + a.length, 0);
168
+ const out = new Uint8Array(total);
169
+ let at = 0;
170
+ for (const a of arrays) { out.set(a, at); at += a.length; }
171
+ return out;
172
+ };
173
+ const hexToBytes = (hex) => {
174
+ const h = String(hex).replace(/^0x/, "");
175
+ if (h.length % 2) boom("uniswaprelay-odd-hex");
176
+ const out = new Uint8Array(h.length / 2);
177
+ for (let i = 0; i < out.length; i++) {
178
+ const byte = parseInt(h.slice(i * 2, i * 2 + 2), 16);
179
+ if (Number.isNaN(byte)) boom("uniswaprelay-bad-hex");
180
+ out[i] = byte;
181
+ }
182
+ return out;
183
+ };
184
+ const bytesToHex = (bytes) => {
185
+ let s = "0x";
186
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
187
+ return s;
188
+ };
189
+ const asBigInt = (v) => {
190
+ if (typeof v === "bigint") return v;
191
+ const s = String(v === undefined || v === null ? "" : v).trim();
192
+ if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
193
+ if (/^[0-9]+$/.test(s)) return BigInt(s);
194
+ boom("uniswaprelay-bad-number");
195
+ };
196
+ const wordUint = (v, bits) => {
197
+ const n = asBigInt(v);
198
+ if (n < 0n) boom("uniswaprelay-negative");
199
+ if (n >= (1n << BigInt(bits))) boom("uniswaprelay-overflow");
200
+ return hexToBytes("0x" + n.toString(16).padStart(64, "0"));
201
+ };
202
+ const wordAddress = (v) => {
203
+ const s = String(v === undefined || v === null ? "" : v).trim();
204
+ if (!/^(0x)?[0-9a-fA-F]{40}$/.test(s)) boom("uniswaprelay-bad-address");
205
+ const out = new Uint8Array(32);
206
+ out.set(hexToBytes(s.replace(/^0x/, "")), 12);
207
+ return out;
208
+ };
209
+ const wordBool = (v) => wordUint(v ? 1 : 0, 8);
210
+ // A bytes field in EIP-712 is encoded as the keccak256 of its bytes - empty bytes hash to keccak256("").
211
+ const wordBytes = (v) => keccak256(hexToBytes(v === undefined || v === null ? "0x" : v));
212
+
213
+ // -----------------------------------------------------------------------------------------------
214
+ // THE ORDER: MISSING FIELDS, HASHES, DOMAIN, DIGEST, TYPED DATA, SIGN, RECOVER
215
+ // -----------------------------------------------------------------------------------------------
216
+
217
+ // The fields an order cannot do without. An absent field is not "probably zero": the caller refuses, not signs.
218
+ // universalRouterCalldata MAY be empty (the reactor skips the call when it is - RelayOrderReactor.execute), so like
219
+ // any bytes field only undefined/null counts as missing.
220
+ export const uniswapRelayOrderMissingFields = (order) => {
221
+ const missing = [];
222
+ for (const [name] of UNISWAPRELAY_INFO_FIELDS) {
223
+ const v = order && order.info ? order.info[name] : undefined;
224
+ if (v === undefined || v === null || v === "") missing.push("info." + name);
225
+ }
226
+ for (const [name] of UNISWAPRELAY_INPUT_FIELDS) {
227
+ const v = order && order.input ? order.input[name] : undefined;
228
+ if (v === undefined || v === null || v === "") missing.push("input." + name);
229
+ }
230
+ for (const [name] of UNISWAPRELAY_FEE_FIELDS) {
231
+ const v = order && order.fee ? order.fee[name] : undefined;
232
+ if (v === undefined || v === null || v === "") missing.push("fee." + name);
233
+ }
234
+ if (!order || order.universalRouterCalldata === undefined || order.universalRouterCalldata === null) missing.push("universalRouterCalldata");
235
+ return missing;
236
+ };
237
+
238
+ const infoHash = (order) => keccak256(concat(
239
+ keccak256(utf8(UNISWAPRELAY_INFO_TYPE)),
240
+ wordAddress(order.info.reactor),
241
+ wordAddress(order.info.swapper),
242
+ wordUint(order.info.nonce, 256),
243
+ wordUint(order.info.deadline, 256),
244
+ ));
245
+ const inputHash = (input) => keccak256(concat(
246
+ keccak256(utf8(UNISWAPRELAY_INPUT_TYPE)),
247
+ wordAddress(input.token),
248
+ wordUint(input.amount, 256),
249
+ wordAddress(input.recipient),
250
+ ));
251
+ const feeHash = (fee) => keccak256(concat(
252
+ keccak256(utf8(UNISWAPRELAY_FEE_TYPE)),
253
+ wordAddress(fee.token),
254
+ wordUint(fee.startAmount, 256),
255
+ wordUint(fee.endAmount, 256),
256
+ wordUint(fee.startTime, 256),
257
+ wordUint(fee.endTime, 256),
258
+ ));
259
+
260
+ // The ORDER HASH - what RelayOrderLib.hash() computes and what the reactor's own Relay event carries as orderHash.
261
+ // It carries NO domain: it is the EIP-712 struct hash of RelayOrder.
262
+ export const uniswapRelayOrderHash = (order) => keccak256(concat(
263
+ keccak256(utf8(UNISWAPRELAY_HASHED_ORDER_TYPE)),
264
+ infoHash(order),
265
+ inputHash(order.input),
266
+ feeHash(order.fee),
267
+ keccak256(hexToBytes(order.universalRouterCalldata)),
268
+ ));
269
+ export const uniswapRelayOrderHashHex = (order) => bytesToHex(uniswapRelayOrderHash(order));
270
+
271
+ // Permit2's domain separator: keccak256(abi.encode(TYPE_HASH, keccak256(Permit2), chainId, permit2)). chainId and the
272
+ // contract come LIVE from the order's context, not cached: the digest must change when either does.
273
+ export const uniswapRelayDomainSeparator = ({ chainId, permit2 = UNISWAPRELAY_PERMIT2 }) => keccak256(concat(
274
+ keccak256(utf8(UNISWAPRELAY_DOMAIN_TYPE)),
275
+ keccak256(utf8(UNISWAPRELAY_DOMAIN_NAME)),
276
+ wordUint(chainId, 256),
277
+ wordAddress(permit2),
278
+ ));
279
+
280
+ // The digest the swapper's key signs: EIP-712 over Permit2's domain with PermitBatchWitnessTransferFrom as the
281
+ // primary type and the order as the witness. permitted is the input plus the fee (the fee's END amount - the permit
282
+ // signs the top of the escalator; RelayOrderLib.toTokenPermissions / FeeEscalatorLib.toTokenPermissions), and the
283
+ // spender is the reactor.
284
+ export const uniswapRelayOrderDigest = ({ order, chainId, permit2 = UNISWAPRELAY_PERMIT2 }) => {
285
+ const permitted = [
286
+ { token: order.input.token, amount: order.input.amount },
287
+ { token: order.fee.token, amount: order.fee.endAmount },
288
+ ];
289
+ // An array of structs in EIP-712 is the keccak256 of the concatenation of each element's struct hash.
290
+ const permittedHash = keccak256(concat(...permitted.map((p) => keccak256(concat(
291
+ keccak256(utf8(UNISWAPRELAY_TOKEN_PERMISSIONS_TYPE)),
292
+ wordAddress(p.token),
293
+ wordUint(p.amount, 256),
294
+ )))));
295
+ const message = keccak256(concat(
296
+ keccak256(utf8(UNISWAPRELAY_PERMIT_TYPE)),
297
+ permittedHash,
298
+ wordAddress(order.info.reactor),
299
+ wordUint(order.info.nonce, 256),
300
+ wordUint(order.info.deadline, 256),
301
+ uniswapRelayOrderHash(order),
302
+ ));
303
+ return keccak256(concat(Uint8Array.from([0x19, 0x01]), uniswapRelayDomainSeparator({ chainId, permit2 }), message));
304
+ };
305
+ export const uniswapRelayOrderDigestHex = (ctx) => bytesToHex(uniswapRelayOrderDigest(ctx));
306
+
307
+ /**
308
+ * The typed data a wallet signs (eth_signTypedData_v4). The domain is Permit2's, the primary type is
309
+ * PermitBatchWitnessTransferFrom, and the order rides as the witness - the same shape Uniswap's own SDK signs.
310
+ */
311
+ export const uniswapRelayOrderTypedData = ({ order, chainId, permit2 = UNISWAPRELAY_PERMIT2 }) => ({
312
+ domain: { name: UNISWAPRELAY_DOMAIN_NAME, chainId: Number(chainId), verifyingContract: permit2 },
313
+ types: {
314
+ EIP712Domain: [
315
+ { name: "name", type: "string" },
316
+ { name: "chainId", type: "uint256" },
317
+ { name: "verifyingContract", type: "address" },
318
+ ],
319
+ PermitBatchWitnessTransferFrom: UNISWAPRELAY_PERMIT_FIELDS.map(([name, type]) => ({ name, type })),
320
+ TokenPermissions: UNISWAPRELAY_TOKEN_PERMISSIONS_FIELDS.map(([name, type]) => ({ name, type })),
321
+ RelayOrder: UNISWAPRELAY_ORDER_FIELDS.map(([name, type]) => ({ name, type })),
322
+ RelayOrderInfo: UNISWAPRELAY_INFO_FIELDS.map(([name, type]) => ({ name, type })),
323
+ Input: UNISWAPRELAY_INPUT_FIELDS.map(([name, type]) => ({ name, type })),
324
+ FeeEscalator: UNISWAPRELAY_FEE_FIELDS.map(([name, type]) => ({ name, type })),
325
+ },
326
+ primaryType: "PermitBatchWitnessTransferFrom",
327
+ message: {
328
+ permitted: [
329
+ { token: order.input.token, amount: String(order.input.amount) },
330
+ { token: order.fee.token, amount: String(order.fee.endAmount) },
331
+ ],
332
+ spender: order.info.reactor,
333
+ nonce: String(order.info.nonce),
334
+ deadline: String(order.info.deadline),
335
+ witness: {
336
+ info: {
337
+ reactor: order.info.reactor,
338
+ swapper: order.info.swapper,
339
+ nonce: String(order.info.nonce),
340
+ deadline: String(order.info.deadline),
341
+ },
342
+ input: { token: order.input.token, amount: String(order.input.amount), recipient: order.input.recipient },
343
+ fee: {
344
+ token: order.fee.token,
345
+ startAmount: String(order.fee.startAmount),
346
+ endAmount: String(order.fee.endAmount),
347
+ startTime: String(order.fee.startTime),
348
+ endTime: String(order.fee.endTime),
349
+ },
350
+ universalRouterCalldata: order.universalRouterCalldata,
351
+ },
352
+ },
353
+ });
354
+
355
+ // SIGN THE DIGEST THROUGH THE REPOSITORY'S EXISTING SIGNER: signQuoteDigest signs any 32-byte digest and returns
356
+ // r || s || v with v in {27, 28}. No second signer is written here.
357
+ export const signUniswapRelayOrderDigest = ({ digest, privateKey }) =>
358
+ signQuoteDigest({ crypto: { secp256k1 }, digest, privateKey });
359
+
360
+ // Recover the signer of a digest from a signature (the same rule the CoW, KyberSwap and UniswapX specs use):
361
+ // lowercase 0x address, or null when the signature is not usable. The caller turns null into a NAMED refusal, never
362
+ // into consent.
363
+ export const recoverUniswapRelayOrderSigner = ({ digest, signature }) => {
364
+ try {
365
+ const sig = hexToBytes(String(signature).replace(/^0x/, ""));
366
+ if (sig.length !== 65) return null;
367
+ let v = sig[64];
368
+ if (v >= 27) v -= 27;
369
+ if (v !== 0 && v !== 1) return null;
370
+ const pub = secp256k1.Signature.fromBytes(concat(sig.slice(0, 32), sig.slice(32, 64)))
371
+ .addRecoveryBit(v).recoverPublicKey(digest);
372
+ return bytesToHex(keccak256(pub.toBytes(false).slice(1)).slice(12));
373
+ } catch {
374
+ return null;
375
+ }
376
+ };
377
+
378
+ // -----------------------------------------------------------------------------------------------
379
+ // THE UNIVERSALROUTER CALLDATA: token -> NATIVE (the classic swap-exact-tokens-for-ETH shape)
380
+ // -----------------------------------------------------------------------------------------------
381
+ //
382
+ // The reactor hands the router EXACTLY this calldata (RelayOrderReactor.execute) after moving the input token to
383
+ // input.recipient, which for a relayed swap IS the UniversalRouter. So the swap must pay from the ROUTER'S OWN
384
+ // BALANCE: the V3 swap's recipient is ADDRESS_THIS and its payerIsUser flag is FALSE (Uniswap/relayer
385
+ // test/integration-tests/RelayOrderReactor.test.ts: Guidelines for relay orders - ROUTER pays, payerIsUser = false),
386
+ // and the following UNWRAP_WETH pays the native out to `recipient`.
387
+
388
+ // One right-padded hex body to a 32-byte boundary (the abi.encode padding rule for dynamic bytes).
389
+ const padRight = (hex) => {
390
+ const body = String(hex).replace(/^0x/, "").toLowerCase();
391
+ return body.length % 64 === 0 ? body : body.padEnd(Math.ceil(body.length / 64) * 64, "0");
392
+ };
393
+ const wordHex = (v) => asBigInt(v).toString(16).padStart(64, "0");
394
+
395
+ // abi.encode(bytes value) as a standalone value: offset (0x20), length, then the padded bytes.
396
+ export const abiEncodeBytes = (hex) =>
397
+ "0x" + wordHex(0x20) + wordHex(hexToBytes(hex).length) + padRight(hex);
398
+
399
+ // abi.encode(bytes[] items): length, then one offset word per item, then each item (its own length word + padded
400
+ // bytes). The offsets are relative to the START OF THE OFFSET TABLE, i.e. to the word right after the length - the
401
+ // same rule the demo's own multicall encoder follows (www/js/evm/dexExec.js encodeMulticall).
402
+ export const abiEncodeBytesArray = (items) => {
403
+ const bodies = items.map((h) => String(h).replace(/^0x/, ""));
404
+ const head = wordHex(bodies.length);
405
+ const offsets = [];
406
+ let offset = bodies.length * 32;
407
+ for (const b of bodies) {
408
+ offsets.push(wordHex(offset));
409
+ offset += 32 + Math.ceil(b.length / 64) * 32;
410
+ }
411
+ const tails = bodies.map((b) => wordHex(b.length / 2) + padRight(b)).join("");
412
+ return "0x" + head + offsets.join("") + tails;
413
+ };
414
+
415
+ // THE V3 PATH: tokenIn(20) ++ fee(3 bytes, big-endian) ++ tokenOut(20) ++ (fee ++ token)... The 3-byte fee is the
416
+ // pool fee tier (Uniswap/universal-router Path.sol; uniswapx-sdk swapRouter02Helpers.encodePath).
417
+ export const uniswapRelayPath = (tokens, fees) => {
418
+ if (!Array.isArray(tokens) || tokens.length !== fees.length + 1) boom("uniswaprelay-bad-path");
419
+ let body = "";
420
+ for (let i = 0; i < fees.length; i++) {
421
+ body += String(tokens[i]).replace(/^0x/, "").toLowerCase();
422
+ const fee = Number(fees[i]);
423
+ if (!Number.isInteger(fee) || fee < 0 || fee > 0xffffff) boom("uniswaprelay-bad-fee");
424
+ body += fee.toString(16).padStart(UNISWAPRELAY_PATH_FEE_BYTES * 2, "0");
425
+ }
426
+ body += String(tokens[tokens.length - 1]).replace(/^0x/, "").toLowerCase();
427
+ return "0x" + body;
428
+ };
429
+
430
+ // abi.encode(address recipient,uint256 amountIn,uint256 amountOutMin,bytes path,bool payerIsUser) - the parameter
431
+ // order of the UniversalRouter's V3_SWAP_EXACT_IN command (Uniswap/universal-router V3SwapRouter / IV3SwapRouter).
432
+ export const encodeV3SwapExactIn = ({ recipient, amountInWei, amountOutMinimumWei, path, payerIsUser = false }) => {
433
+ const pathBody = String(path).replace(/^0x/, "");
434
+ const head = concat(
435
+ wordAddress(recipient),
436
+ wordUint(amountInWei, 256),
437
+ wordUint(amountOutMinimumWei, 256),
438
+ wordUint(0xa0, 256),
439
+ wordBool(payerIsUser),
440
+ );
441
+ const tail = concat(wordUint(pathBody.length / 2, 256), hexToBytes("0x" + padRight(pathBody)));
442
+ return bytesToHex(concat(head, tail));
443
+ };
444
+
445
+ // abi.encode(address recipient,uint256 amountMin) - the parameter order of the UNWRAP_WETH command
446
+ // (Uniswap/universal-router V3SwapRouter.unwrapWETH9).
447
+ export const encodeUnwrapWeth = ({ recipient, amountMinimumWei }) =>
448
+ bytesToHex(concat(wordAddress(recipient), wordUint(amountMinimumWei, 256)));
449
+
450
+ /**
451
+ * BUILD THE UNIVERSALROUTER CALLDATA for a swap of tokenIn into the chain's NATIVE coin through the wrapped native
452
+ * (weth): a V3 exact-input swap paying from the router's own balance, followed by unwrapping the whole wrapped
453
+ * balance to `recipient`. This is the shape Uniswap's own relayer tests generate for token -> native.
454
+ */
455
+ export const uniswapRelayRouterCalldata = ({
456
+ tokenIn, weth, fee, amountInWei, amountOutMinimumWei, recipient,
457
+ }) => {
458
+ const path = uniswapRelayPath([tokenIn, weth], [fee]);
459
+ const commands = "0x"
460
+ + UNISWAPRELAY_UR_COMMAND.v3SwapExactIn.toString(16).padStart(2, "0")
461
+ + UNISWAPRELAY_UR_COMMAND.unwrapWeth.toString(16).padStart(2, "0");
462
+ const swap = encodeV3SwapExactIn({ recipient: UNISWAPRELAY_ADDRESS_THIS, amountInWei, amountOutMinimumWei, path, payerIsUser: false });
463
+ const unwrap = encodeUnwrapWeth({ recipient, amountMinimumWei: amountOutMinimumWei });
464
+ const commandsBody = commands.replace(/^0x/, "");
465
+ const commandsEnc = wordHex(hexToBytes(commands).length) + padRight(commandsBody);
466
+ const inputsEnc = abiEncodeBytesArray([swap, unwrap]).replace(/^0x/, "");
467
+ const offsetCommands = 0x40;
468
+ const offsetInputs = 0x40 + commandsEnc.length / 2;
469
+ return UNISWAPRELAY_EXECUTE_SELECTOR
470
+ + wordHex(offsetCommands) + wordHex(offsetInputs) + commandsEnc + inputsEnc;
471
+ };
472
+
473
+ // Reading back the words a calldata carries: the idiom the demo's own DEX leg uses (www/js/evm/dexExec.js
474
+ // decodeSwapCalldata), so a built calldata is checked against the order rather than trusted.
475
+ const wordsOf = (hex) => {
476
+ const body = String(hex).replace(/^0x/, "");
477
+ const out = [];
478
+ for (let i = 0; i + 64 <= body.length; i += 64) out.push(BigInt("0x" + body.slice(i, i + 64)));
479
+ return out;
480
+ };
481
+ const wordToAddress = (w) => "0x" + w.toString(16).padStart(40, "0");
482
+
483
+ /**
484
+ * DECODE the calldata this spec builds, back into the commands and the two command inputs. Returns null when the
485
+ * bytes are not exactly this shape (not execute(bytes,bytes[]), not two commands, not V3_SWAP_EXACT_IN +
486
+ * UNWRAP_WETH). A decode that nearly works is exactly how a substituted field reads as trusted, so every boundary
487
+ * is checked.
488
+ */
489
+ export const uniswapRelayRouterDecode = (data) => {
490
+ const body = String(data || "").replace(/^0x/, "").toLowerCase();
491
+ if (body.slice(0, 8) !== UNISWAPRELAY_EXECUTE_SELECTOR.slice(2)) return null;
492
+ const args = body.slice(8);
493
+ const w = wordsOf("0x" + args);
494
+ if (w.length < 2) return null;
495
+ const offCommands = Number(w[0]);
496
+ const offInputs = Number(w[1]);
497
+ if (offCommands % 32 !== 0 || offInputs % 32 !== 0) return null;
498
+ const commandsLen = Number(w[offCommands / 32]);
499
+ const commands = "0x" + args.slice(offCommands * 2 + 64, offCommands * 2 + 64 + commandsLen * 2);
500
+ const arrayAt = offInputs / 32;
501
+ const count = Number(w[arrayAt]);
502
+ if (count !== 2) return null;
503
+ const readItem = (i) => {
504
+ const rel = Number(w[arrayAt + 1 + i]);
505
+ const at = arrayAt + 1 + rel / 32;
506
+ const hex = "0x" + args.slice(at * 64 + 64);
507
+ return { words: w.slice(at + 1), hex };
508
+ };
509
+ const a = readItem(0);
510
+ const b = readItem(1);
511
+ const swap = {
512
+ recipient: wordToAddress(a.words[0]),
513
+ amountInWei: a.words[1],
514
+ amountOutMinimumWei: a.words[2],
515
+ pathOffset: Number(a.words[3]),
516
+ payerIsUser: a.words[4] === 1n,
517
+ };
518
+ const pathAt = swap.pathOffset / 32;
519
+ const pathLen = Number(a.words[pathAt]);
520
+ swap.path = "0x" + a.hex.replace(/^0x/, "").slice(pathAt * 64 + 64, pathAt * 64 + 64 + pathLen * 2);
521
+ const unwrap = { recipient: wordToAddress(b.words[0]), amountMinimumWei: b.words[1] };
522
+ return { commands, swap, unwrap, items: [a.hex, b.hex] };
523
+ };
524
+
525
+ // THE PATH READ BACK AS TOKENS AND FEES: the 20+3 byte walk (Uniswap/universal-router Path.sol). Returns the token
526
+ // list and the fee list, or null when an address is not 20 bytes or a fee slot is not 3 bytes.
527
+ export const uniswapRelayDecodePath = (path) => {
528
+ const body = String(path || "").replace(/^0x/, "").toLowerCase();
529
+ if (body.length < 40) return null;
530
+ const tokens = ["0x" + body.slice(0, 40)];
531
+ const fees = [];
532
+ let at = 40;
533
+ while (at < body.length) {
534
+ if (body.length - at < 6 + 40) return null;
535
+ fees.push(parseInt(body.slice(at, at + 6), 16));
536
+ at += 6;
537
+ tokens.push("0x" + body.slice(at, at + 40));
538
+ at += 40;
539
+ }
540
+ return { tokens, fees };
541
+ };