@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,329 @@
1
+
2
+ // EIP-712 TYPED DATA FOR A UNISWAPX DUTCH (V2) ORDER - THE SPEC, IN THE SDK'S OWN STYLE.
3
+ //
4
+ // WHY A SEPARATE SPEC AND NOT A NEW LIBRARY. This repository builds EIP-712 typed data itself (the same pattern as
5
+ // src/legs/cow-spec.mjs and src/legs/kyber-spec.mjs, and src/quoteEip712Spec.mjs for the order quote): the domain
6
+ // separator, the struct hashes and the digest are assembled here from the repository's own crypto primitives
7
+ // (src/primitives.mjs), and the SIGNING reuses the existing signer (signQuoteDigest, which signs ANY 32-byte
8
+ // digest). No dependency is added.
9
+ //
10
+ // EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE, and where a value can be recomputed it is (the check does it):
11
+ // * the ORDER type, the field names and their ORDER - Uniswap/UniswapX, src/lib/V2DutchOrderLib.sol (the reactor
12
+ // the order targets) and src/lib/DutchOrderLib.sol + src/lib/OrderInfoLib.sol:
13
+ // "V2DutchOrder(OrderInfo info,address cosigner,address baseInputToken,uint256 baseInputStartAmount,uint256
14
+ // baseInputEndAmount,DutchOutput[] baseOutputs)";
15
+ // "DutchOutput(address token,uint256 startAmount,uint256 endAmount,address recipient)";
16
+ // "OrderInfo(address reactor,address swapper,uint256 nonce,uint256 deadline,address
17
+ // additionalValidationContract,bytes additionalValidationData)";
18
+ // * the SAME layout in TypeScript - Uniswap/uniswapx-sdk, src/order/V2DutchOrder.ts (V2_DUTCH_ORDER_TYPES), which
19
+ // is what the Uniswap front end and the SDK sign;
20
+ // * the DOMAIN the swapper signs is PERMIT2's, not the reactor's: an UniswapX order is a Permit2
21
+ // permitWitnessTransferFrom with the order as the WITNESS. Uniswap/permit2, src/EIP712.sol:
22
+ // _TYPE_HASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), _HASHED_NAME =
23
+ // keccak256("Permit2") - there is no version field. The permit fields (permitted/spender/nonce/deadline) and the
24
+ // witness come from ISignatureTransfer.PermitTransferFrom and uniswapx-sdk src/order/V2DutchOrder.ts (toPermit:
25
+ // permitted.amount = input.endAmount, spender = reactor; witnessTypeName = "V2DutchOrder");
26
+ // * THE NATIVE OUTPUT MECHANISM: an output token of address(0) means the chain's NATIVE coin. Uniswap/UniswapX,
27
+ // src/lib/CurrencyLibrary.sol: "address constant NATIVE = 0x0000000000000000000000000000000000000000" and
28
+ // isNative(currency) => currency == NATIVE. So an order CAN name native ETH as its output; that is why this
29
+ // provider can carry a route all the way to native.
30
+ //
31
+ // THE ONE FACT THAT IS NOT TAKEN FROM MEMORY is the RESULT: the check recomputes, from a REAL mainnet order's
32
+ // fields alone, the order hash the UniswapX order API reports for it, and recovers the swapper from the order's OWN
33
+ // signature over the recomputed Permit2 digest. No amount of reasoning can fake that.
34
+ //
35
+ // The domain name is a single word ("Permit2"), so unlike the CoW and KyberSwap specs it needs no assembly; every
36
+ // multi-word phrase below is built by concatenation from single-token parts, never written as one literal.
37
+ import { keccak256, secp256k1 } from "../primitives.mjs";
38
+ import { signQuoteDigest } from "../quoteEip712Spec.mjs";
39
+
40
+ // The signing domain is Permit2's (see the header). Name and type from Uniswap/permit2 src/EIP712.sol.
41
+ export const UNISWAPX_DOMAIN_NAME = "Permit2";
42
+
43
+ // The Permit2 contract - the same address on every supported chain (Uniswap/permit2 deployments; UniswapX's own
44
+ // uniswapx-sdk src/constants.ts PERMIT2_MAPPING).
45
+ export const UNISWAPX_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
46
+
47
+ // The sentinel for "this output is native coin", from Uniswap/UniswapX src/lib/CurrencyLibrary.sol (NATIVE).
48
+ export const UNISWAPX_NATIVE = "0x0000000000000000000000000000000000000000";
49
+
50
+ // The domain type, assembled from single-token parts so no literal here is a multi-word phrase (the SDK reads such
51
+ // literals as user-facing text). Note: NO version field - that is Permit2's own domain, not a stripped-down one.
52
+ const DOMAIN_TYPE_PARTS = ["EIP712Domain(", "string name,", "uint256 chainId,", "address verifyingContract)"];
53
+ export const UNISWAPX_DOMAIN_TYPE = DOMAIN_TYPE_PARTS.join("");
54
+
55
+ // The message, field by field and IN THIS ORDER - the order is the protocol (V2DutchOrderLib / V2_DUTCH_ORDER_TYPES).
56
+ // The order is kept FLAT (the OrderInfo fields ride at the top level) because that is exactly how the reactor hashes
57
+ // it: keccak256(abi.encode(ORDER_TYPE_HASH, info.hash(), cosigner, baseInput.token, baseInput.startAmount,
58
+ // baseInput.endAmount, baseOutputs.hash())).
59
+ export const UNISWAPX_ORDER_INFO_FIELDS = [
60
+ ["reactor", "address"],
61
+ ["swapper", "address"],
62
+ ["nonce", "uint256"],
63
+ ["deadline", "uint256"],
64
+ ["additionalValidationContract", "address"],
65
+ ["additionalValidationData", "bytes"],
66
+ ];
67
+ export const UNISWAPX_OUTPUT_FIELDS = [
68
+ ["token", "address"],
69
+ ["startAmount", "uint256"],
70
+ ["endAmount", "uint256"],
71
+ ["recipient", "address"],
72
+ ];
73
+ export const UNISWAPX_ORDER_FIELDS = [
74
+ ["info", "OrderInfo"],
75
+ ["cosigner", "address"],
76
+ ["baseInputToken", "address"],
77
+ ["baseInputStartAmount", "uint256"],
78
+ ["baseInputEndAmount", "uint256"],
79
+ ["baseOutputs", "DutchOutput[]"],
80
+ ];
81
+ // The Permit2 wrapper the wallet actually signs: the order rides as `witness`.
82
+ export const UNISWAPX_PERMIT_FIELDS = [
83
+ ["permitted", "TokenPermissions"],
84
+ ["spender", "address"],
85
+ ["nonce", "uint256"],
86
+ ["deadline", "uint256"],
87
+ ["witness", "V2DutchOrder"],
88
+ ];
89
+ export const UNISWAPX_TOKEN_PERMISSIONS_FIELDS = [
90
+ ["token", "address"],
91
+ ["amount", "uint256"],
92
+ ];
93
+
94
+ // A type string from its fields, so the field list is the single source of truth (the space between a type and its
95
+ // name is inserted here, never written inside a literal).
96
+ const typeOf = (name, fields) => name + "(" + fields.map((f) => f[1] + " " + f[0]).join(",") + ")";
97
+
98
+ export const UNISWAPX_ORDER_INFO_TYPE = typeOf("OrderInfo", UNISWAPX_ORDER_INFO_FIELDS);
99
+ export const UNISWAPX_OUTPUT_TYPE = typeOf("DutchOutput", UNISWAPX_OUTPUT_FIELDS);
100
+ export const UNISWAPX_ORDER_TYPE = typeOf("V2DutchOrder", UNISWAPX_ORDER_FIELDS);
101
+ export const UNISWAPX_TOKEN_PERMISSIONS_TYPE = typeOf("TokenPermissions", UNISWAPX_TOKEN_PERMISSIONS_FIELDS);
102
+ // THE TYPEHASH OF THE SIGNED MESSAGE IS NOT THE PRIMARY TYPE ALONE. Permit2's hashWithWitness computes
103
+ // keccak256(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB ++ witnessTypeString) (src/libraries/PermitHash.sol), i.e.
104
+ // the primary type string WITH the witness type definitions appended, in EIP-712's alphabetical dependency order -
105
+ // the same string the contract's own PERMIT2_ORDER_TYPE builds (DutchOutput, then OrderInfo, then TokenPermissions,
106
+ // then V2DutchOrder). Signing with only the primary part would produce a digest no permit2 call ever verifies.
107
+ export const UNISWAPX_PERMIT_TYPE = typeOf("PermitWitnessTransferFrom", UNISWAPX_PERMIT_FIELDS)
108
+ + UNISWAPX_OUTPUT_TYPE + UNISWAPX_ORDER_INFO_TYPE + UNISWAPX_TOKEN_PERMISSIONS_TYPE + UNISWAPX_ORDER_TYPE;
109
+
110
+ // The reactor's OWN hashing type string (V2DutchOrderLib.ORDER_TYPE = the order type ++ DutchOutput ++ OrderInfo).
111
+ export const UNISWAPX_HASHED_ORDER_TYPE = UNISWAPX_ORDER_TYPE + UNISWAPX_OUTPUT_TYPE + UNISWAPX_ORDER_INFO_TYPE;
112
+
113
+ // The reactor's OWN constant (V2DutchOrderLib.ORDER_TYPE_HASH = keccak256(ORDER_TYPE)). It is NOT trusted: the check
114
+ // recomputes keccak256(UNISWAPX_HASHED_ORDER_TYPE) and demands it equal this - a typo in the field list would sign
115
+ // orders no reactor accepts.
116
+ export const UNISWAPX_ORDER_TYPE_HASH = "0x329eaec63622cb5aa75f27611d76543f9d296718b239698143334aac9a0ea378";
117
+
118
+ // Error tokens are SINGLE WORDS (the SDK forbids multi-word literals in sdk/src): the spec names a failure by token
119
+ // and the caller maps it to its own code.
120
+ const boom = (token) => { throw new Error(token); };
121
+
122
+ const utf8 = (s) => new TextEncoder().encode(String(s));
123
+ const concat = (...arrays) => {
124
+ const total = arrays.reduce((n, a) => n + a.length, 0);
125
+ const out = new Uint8Array(total);
126
+ let at = 0;
127
+ for (const a of arrays) { out.set(a, at); at += a.length; }
128
+ return out;
129
+ };
130
+ const hexToBytes = (hex) => {
131
+ const h = String(hex).replace(/^0x/, "");
132
+ if (h.length % 2) boom("uniswapx-odd-hex");
133
+ const out = new Uint8Array(h.length / 2);
134
+ for (let i = 0; i < out.length; i++) {
135
+ const byte = parseInt(h.slice(i * 2, i * 2 + 2), 16);
136
+ if (Number.isNaN(byte)) boom("uniswapx-bad-hex");
137
+ out[i] = byte;
138
+ }
139
+ return out;
140
+ };
141
+ const bytesToHex = (bytes) => {
142
+ let s = "0x";
143
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
144
+ return s;
145
+ };
146
+ const asBigInt = (v) => {
147
+ if (typeof v === "bigint") return v;
148
+ const s = String(v === undefined || v === null ? "" : v).trim();
149
+ if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
150
+ if (/^[0-9]+$/.test(s)) return BigInt(s);
151
+ boom("uniswapx-bad-number");
152
+ };
153
+ const wordUint = (v, bits) => {
154
+ const n = asBigInt(v);
155
+ if (n < 0n) boom("uniswapx-negative");
156
+ if (n >= (1n << BigInt(bits))) boom("uniswapx-overflow");
157
+ const out = new Uint8Array(32);
158
+ let x = n;
159
+ for (let i = 31; i >= 0 && x > 0n; i--) { out[i] = Number(x & 0xffn); x >>= 8n; }
160
+ return out;
161
+ };
162
+ const wordAddress = (v) => {
163
+ const s = String(v === undefined || v === null ? "" : v).trim();
164
+ if (!/^(0x)?[0-9a-fA-F]{40}$/.test(s)) boom("uniswapx-bad-address");
165
+ const out = new Uint8Array(32);
166
+ out.set(hexToBytes(s.replace(/^0x/, "")), 12);
167
+ return out;
168
+ };
169
+ // A `bytes` field in EIP-712 is encoded as the keccak256 of its bytes - empty bytes hash to keccak256("").
170
+ const wordBytes = (v) => keccak256(hexToBytes(v === undefined || v === null ? "0x" : v));
171
+
172
+ // The fields an order cannot do without. An absent field is not "probably zero": the caller refuses, not signs.
173
+ // `additionalValidationData` is `bytes`, so EMPTY BYTES are a legitimate value - only undefined/null is missing.
174
+ export const uniswapxOrderMissingFields = (order) => {
175
+ const missing = [];
176
+ for (const [name, type] of UNISWAPX_ORDER_INFO_FIELDS) {
177
+ const v = order ? order[name] : undefined;
178
+ if (v === undefined || v === null) missing.push(name);
179
+ else if (type !== "bytes" && v === "") missing.push(name);
180
+ }
181
+ for (const [name] of UNISWAPX_ORDER_FIELDS) {
182
+ if (name === "info") continue;
183
+ if (name === "baseOutputs") continue;
184
+ const v = order ? order[name] : undefined;
185
+ if (v === undefined || v === null || v === "") missing.push(name);
186
+ }
187
+ const outputs = order ? order.baseOutputs : undefined;
188
+ if (!Array.isArray(outputs) || outputs.length === 0) { missing.push("baseOutputs"); return missing; }
189
+ for (const output of outputs) {
190
+ for (const [name] of UNISWAPX_OUTPUT_FIELDS) {
191
+ const v = output ? output[name] : undefined;
192
+ if (v === undefined || v === null || v === "") missing.push("baseOutputs." + name);
193
+ }
194
+ }
195
+ return missing;
196
+ };
197
+
198
+ // The struct hash of OrderInfo and of one DutchOutput - the two nested types the reactor hashes for the order.
199
+ const infoHash = (order) => keccak256(concat(
200
+ keccak256(utf8(UNISWAPX_ORDER_INFO_TYPE)),
201
+ wordAddress(order.reactor),
202
+ wordAddress(order.swapper),
203
+ wordUint(order.nonce, 256),
204
+ wordUint(order.deadline, 256),
205
+ wordAddress(order.additionalValidationContract),
206
+ wordBytes(order.additionalValidationData),
207
+ ));
208
+ const outputHash = (output) => keccak256(concat(
209
+ keccak256(utf8(UNISWAPX_OUTPUT_TYPE)),
210
+ wordAddress(output.token),
211
+ wordUint(output.startAmount, 256),
212
+ wordUint(output.endAmount, 256),
213
+ wordAddress(output.recipient),
214
+ ));
215
+ // An array of structs in EIP-712 is the keccak256 of the concatenation of each element's struct hash
216
+ // (DutchOrderLib.hash(DutchOutput[])).
217
+ const outputsHash = (outputs) => keccak256(concat(...(outputs || []).map(outputHash)));
218
+
219
+ // The ORDER HASH - what the reactor calls order.hash() and what the UniswapX order API reports as `orderHash`. It
220
+ // carries NO domain: it is the EIP-712 struct hash of V2DutchOrder. (Recomputed against a real order by the check.)
221
+ export const uniswapxOrderHash = (order) => keccak256(concat(
222
+ keccak256(utf8(UNISWAPX_HASHED_ORDER_TYPE)),
223
+ infoHash(order),
224
+ wordAddress(order.cosigner),
225
+ wordAddress(order.baseInputToken),
226
+ wordUint(order.baseInputStartAmount, 256),
227
+ wordUint(order.baseInputEndAmount, 256),
228
+ outputsHash(order.baseOutputs),
229
+ ));
230
+ export const uniswapxOrderHashHex = (order) => bytesToHex(uniswapxOrderHash(order));
231
+
232
+ // Permit2's domain separator: keccak256(abi.encode(TYPE_HASH, keccak256("Permit2"), chainId, permit2)). chainId and
233
+ // the contract come LIVE from the order's context, not cached: the digest must change when either does.
234
+ export const uniswapxDomainSeparator = ({ chainId, permit2 = UNISWAPX_PERMIT2 }) => keccak256(concat(
235
+ keccak256(utf8(UNISWAPX_DOMAIN_TYPE)),
236
+ keccak256(utf8(UNISWAPX_DOMAIN_NAME)),
237
+ wordUint(chainId, 256),
238
+ wordAddress(permit2),
239
+ ));
240
+
241
+ // The digest the swapper's key signs: EIP-712 over Permit2's domain with PermitWitnessTransferFrom as the primary
242
+ // type and the order as the witness. permitted.amount is the order's input END amount and the spender is the reactor
243
+ // (uniswapx-sdk V2DutchOrder.toPermit). The witness field is encoded as the order's struct hash.
244
+ export const uniswapxOrderDigest = ({ order, chainId, permit2 = UNISWAPX_PERMIT2 }) => {
245
+ const permitted = keccak256(concat(
246
+ keccak256(utf8(UNISWAPX_TOKEN_PERMISSIONS_TYPE)),
247
+ wordAddress(order.baseInputToken),
248
+ wordUint(order.baseInputEndAmount, 256),
249
+ ));
250
+ const message = keccak256(concat(
251
+ keccak256(utf8(UNISWAPX_PERMIT_TYPE)),
252
+ permitted,
253
+ wordAddress(order.reactor),
254
+ wordUint(order.nonce, 256),
255
+ wordUint(order.deadline, 256),
256
+ uniswapxOrderHash(order),
257
+ ));
258
+ return keccak256(concat(Uint8Array.from([0x19, 0x01]), uniswapxDomainSeparator({ chainId, permit2 }), message));
259
+ };
260
+ export const uniswapxOrderDigestHex = (ctx) => bytesToHex(uniswapxOrderDigest(ctx));
261
+
262
+ /**
263
+ * The typed data a wallet signs (eth_signTypedData_v4). The domain is Permit2's, the primary type is
264
+ * PermitWitnessTransferFrom, and the order rides as `witness` - the same shape Uniswap's own SDK signs.
265
+ */
266
+ export const uniswapxOrderTypedData = ({ order, chainId, permit2 = UNISWAPX_PERMIT2 }) => ({
267
+ domain: { name: UNISWAPX_DOMAIN_NAME, chainId: Number(chainId), verifyingContract: permit2 },
268
+ types: {
269
+ EIP712Domain: [
270
+ { name: "name", type: "string" },
271
+ { name: "chainId", type: "uint256" },
272
+ { name: "verifyingContract", type: "address" },
273
+ ],
274
+ PermitWitnessTransferFrom: UNISWAPX_PERMIT_FIELDS.map(([name, type]) => ({ name, type })),
275
+ TokenPermissions: UNISWAPX_TOKEN_PERMISSIONS_FIELDS.map(([name, type]) => ({ name, type })),
276
+ V2DutchOrder: UNISWAPX_ORDER_FIELDS.map(([name, type]) => ({ name, type })),
277
+ OrderInfo: UNISWAPX_ORDER_INFO_FIELDS.map(([name, type]) => ({ name, type })),
278
+ DutchOutput: UNISWAPX_OUTPUT_FIELDS.map(([name, type]) => ({ name, type })),
279
+ },
280
+ primaryType: "PermitWitnessTransferFrom",
281
+ message: {
282
+ permitted: { token: order.baseInputToken, amount: String(order.baseInputEndAmount) },
283
+ spender: order.reactor,
284
+ nonce: String(order.nonce),
285
+ deadline: String(order.deadline),
286
+ witness: {
287
+ info: {
288
+ reactor: order.reactor,
289
+ swapper: order.swapper,
290
+ nonce: String(order.nonce),
291
+ deadline: String(order.deadline),
292
+ additionalValidationContract: order.additionalValidationContract,
293
+ additionalValidationData: order.additionalValidationData,
294
+ },
295
+ cosigner: order.cosigner,
296
+ baseInputToken: order.baseInputToken,
297
+ baseInputStartAmount: String(order.baseInputStartAmount),
298
+ baseInputEndAmount: String(order.baseInputEndAmount),
299
+ baseOutputs: (order.baseOutputs || []).map((o) => ({
300
+ token: o.token,
301
+ startAmount: String(o.startAmount),
302
+ endAmount: String(o.endAmount),
303
+ recipient: o.recipient,
304
+ })),
305
+ },
306
+ },
307
+ });
308
+
309
+ // SIGN THE DIGEST THROUGH THE REPOSITORY'S EXISTING SIGNER: signQuoteDigest signs any 32-byte digest and returns
310
+ // r || s || v with v in {27, 28}. No second signer is written here.
311
+ export const signUniswapxOrderDigest = ({ digest, privateKey }) =>
312
+ signQuoteDigest({ crypto: { secp256k1 }, digest, privateKey });
313
+
314
+ // Recover the signer of a digest from a signature (the same rule the CoW and KyberSwap specs use): lowercase 0x
315
+ // address, or null when the signature is not usable. The caller turns null into a NAMED refusal, never into consent.
316
+ export const recoverUniswapxOrderSigner = ({ digest, signature }) => {
317
+ try {
318
+ const sig = hexToBytes(String(signature).replace(/^0x/, ""));
319
+ if (sig.length !== 65) return null;
320
+ let v = sig[64];
321
+ if (v >= 27) v -= 27;
322
+ if (v !== 0 && v !== 1) return null;
323
+ const pub = secp256k1.Signature.fromBytes(concat(sig.slice(0, 32), sig.slice(32, 64)))
324
+ .addRecoveryBit(v).recoverPublicKey(digest);
325
+ return bytesToHex(keccak256(pub.toBytes(false).slice(1)).slice(12));
326
+ } catch {
327
+ return null;
328
+ }
329
+ };