-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathutils7702.ts
More file actions
518 lines (483 loc) · 17 KB
/
Copy pathutils7702.ts
File metadata and controls
518 lines (483 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
//https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md
//rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, yParity, r, s])
//authorization_list = [[chain_id, address, nonce, yParity, r, s], ...]
import {
encodeRlp,
getBytes,
keccak256,
signHash as ecdsaSignHash,
toBeArray,
} from "./ethereUtils";
const SET_CODE_TX_TYPE = "0x04";
/**
* An EIP-7702 delegation authorization with bigint values.
* Represents a signed authorization that delegates an EOA's code to a contract.
*/
export type Authorization7702 = {
/** The chain ID the authorization is valid for. */
chainId: bigint;
/** The contract address to delegate code from. */
address: string;
/** The EOA's nonce at the time of signing. */
nonce: bigint;
/** The parity of the signature's y-coordinate (0 or 1). */
yParity: 0 | 1;
/** The r component of the ECDSA signature. */
r: bigint;
/** The s component of the ECDSA signature. */
s: bigint;
};
/**
* An EIP-7702 delegation authorization with hex-encoded string values.
* Same as {@link Authorization7702} but with all numeric fields as hex strings.
*/
export type Authorization7702Hex = {
/** The chain ID as a hex string. */
chainId: string;
/** The contract address to delegate code from. */
address: string;
/** The EOA's nonce as a hex string. */
nonce: string;
/** The parity of the signature's y-coordinate as a hex string. */
yParity: string;
/** The r component of the ECDSA signature as a hex string. */
r: string;
/** The s component of the ECDSA signature as a hex string. */
s: string;
};
/**
* Creates and signs a legacy (pre-EIP-1559) raw transaction using RLP encoding.
* @param chainId - The chain ID for replay protection.
* @param nonce - The sender's transaction nonce.
* @param gas_price - The gas price in wei.
* @param gas_limit - The maximum gas units for the transaction.
* @param destination - The recipient address (42-character hex string).
* @param value - The amount of ETH to send in wei.
* @param data - The transaction input data.
* @param eoaPrivateKey - The sender's private key for signing.
* @returns The RLP-encoded signed transaction as a hex string.
*/
export function createAndSignLegacyRawTransaction(
chainId: bigint,
nonce: bigint,
gas_price: bigint,
gas_limit: bigint,
destination: string,
value: bigint,
data: string,
eoaPrivateKey: string,
): string {
if (chainId >= 2 ** 64) {
throw new RangeError("Invalid chainId.");
}
if (nonce >= 2 ** 64) {
throw new RangeError("Invalid nonce.");
}
if (destination.length !== 42) {
throw new RangeError("Invalid destination.");
}
let payload = [
bigintToBytes(nonce),
bigintToBytes(gas_price),
bigintToBytes(gas_limit),
destination,
bigintToBytes(value),
data,
bigintToBytes(chainId),
bigintToBytes(0n),
bigintToBytes(0n),
];
const txHash = keccak256(encodeRlp(payload));
const signature = ecdsaSignHash(eoaPrivateKey, txHash);
payload = [
bigintToBytes(nonce),
bigintToBytes(gas_price),
bigintToBytes(gas_limit),
destination,
bigintToBytes(value),
data,
bigintToBytes(BigInt(signature.yParity) + chainId * 2n + 35n),
// r and s must be minimal big-endian integers — nodes reject RLP
// scalars with leading zero bytes as non-canonical
bigintToBytes(BigInt(signature.r)),
bigintToBytes(BigInt(signature.s)),
];
const transactionPayload = encodeRlp(payload);
return transactionPayload;
}
/**
* Creates and signs an EIP-7702 delegation authorization.
* The authorization allows an EOA to delegate its code to a specified contract address.
*
* Accepts either a hex-encoded private key string or a signer callback
* `(hash: string) => Promise<string>` for use with viem, ethers Signers,
* hardware wallets, or MPC signers.
*
* The callback signs the auth hash directly — no EIP-191 / EIP-712 / message
* prefix. The returned hex string must be one of:
* - **Standard 65-byte signature** (130 hex chars after `0x`): `r (32) || s (32) || v (1)`,
* where `v` is `0`, `1`, `27`, or `28`. This is the shape every common
* library produces (e.g., ethers v6: `Signature.from(wallet.signingKey.sign(hash)).serialized`;
* viem `LocalAccount.sign({ hash })`).
* - **EIP-2098 compact 64-byte signature** (128 hex chars after `0x`): `r (32) || yParityAndS (32)`,
* where the high bit of the second 32-byte word encodes `yParity`.
*
* The `0x` prefix is optional. Other lengths or out-of-range `v` values throw.
*
* @param chainId - The chain ID the authorization is valid for.
* @param address - The contract address to delegate code from.
* @param nonce - The EOA's nonce at the time of signing.
* @param signer - The EOA's private key or a signing function returning a 65-byte standard or 64-byte EIP-2098 hex signature.
* @returns The signed authorization with all numeric values as hex strings.
*/
export function createAndSignEip7702DelegationAuthorization(
chainId: bigint,
address: string,
nonce: bigint,
signer: string,
): Authorization7702Hex;
export function createAndSignEip7702DelegationAuthorization(
chainId: bigint,
address: string,
nonce: bigint,
signer: (hash: string) => Promise<string>,
): Promise<Authorization7702Hex>;
export function createAndSignEip7702DelegationAuthorization(
chainId: bigint,
address: string,
nonce: bigint,
signer: string | ((hash: string) => Promise<string>),
): Authorization7702Hex | Promise<Authorization7702Hex> {
const authHash = createEip7702DelegationAuthorizationHash(chainId, address, nonce);
if (typeof signer === "string") {
const signature = signHash(authHash, signer);
return {
chainId: bigintToHex(chainId),
address,
nonce: bigintToHex(nonce),
yParity: bigintToHex(BigInt(signature.yParity)),
r: bigintToHex(signature.r),
s: bigintToHex(signature.s),
};
}
return signer(authHash).then((rawSig) => {
const sig = parseRawSignature(rawSig);
return {
chainId: bigintToHex(chainId),
address,
nonce: bigintToHex(nonce),
yParity: bigintToHex(BigInt(sig.yParity)),
r: bigintToHex(sig.r),
s: bigintToHex(sig.s),
};
});
}
/**
* Creates and signs an EIP-7702 delegation revocation authorization.
* Sets the delegatee address to the zero address, which revokes the delegation
* and restores the EOA to a normal account.
*
* @param chainId - The chain ID the authorization is valid for.
* @param nonce - The EOA's authorization nonce at the time of signing.
* @param eoaPrivateKey - The EOA's private key for signing.
* @returns The signed delegation revocation authorization with hex-encoded values.
*/
export function createRevokeDelegationAuthorization(
chainId: bigint,
nonce: bigint,
eoaPrivateKey: string,
): Authorization7702Hex {
const ZeroAddress = "0x0000000000000000000000000000000000000000";
return createAndSignEip7702DelegationAuthorization(chainId, ZeroAddress, nonce, eoaPrivateKey);
}
/**
* Computes the keccak256 hash of an EIP-7702 delegation authorization.
* Uses the MAGIC prefix (0x05) as defined in the EIP-7702 spec.
* @param chainId - The chain ID the authorization is valid for.
* @param address - The contract address to delegate code from.
* @param nonce - The EOA's nonce at the time of signing.
* @returns The authorization hash as a hex string.
*/
export function createEip7702DelegationAuthorizationHash(
chainId: bigint,
address: string,
nonce: bigint,
): string {
const auth_arr = [bigintToBytes(chainId), address, bigintToBytes(nonce)];
const encoded_auth = encodeRlp(auth_arr);
const MAGIC = "0x05";
return keccak256(MAGIC + encoded_auth.slice(2));
}
/**
* Signs a hash using an EOA's private key.
* @param authHash - The hash to sign.
* @param eoaPrivateKey - The EOA's private key for signing.
* @returns An object containing the signature components: yParity, r, and s.
*/
export function signHash(
authHash: string,
eoaPrivateKey: string,
): { yParity: 0 | 1; r: bigint; s: bigint } {
const signature = ecdsaSignHash(eoaPrivateKey, authHash);
return {
yParity: signature.yParity,
r: BigInt(signature.r),
s: BigInt(signature.s),
};
}
/**
* Creates and signs an EIP-7702 (set-code) raw transaction.
* Encodes the transaction with a type 0x04 prefix and includes the authorization list.
* @param chainId - The chain ID for replay protection.
* @param nonce - The sender's transaction nonce.
* @param max_priority_fee_per_gas - The maximum priority fee per gas (tip) in wei.
* @param max_fee_per_gas - The maximum total fee per gas in wei.
* @param gas_limit - The maximum gas units for the transaction.
* @param destination - The recipient address (42-character hex string).
* @param value - The amount of ETH to send in wei.
* @param data - The transaction input data.
* @param access_list - The EIP-2930 access list as [address, storageKeys] tuples.
* @param authorization_list - The list of signed EIP-7702 delegation authorizations.
* @param eoaPrivateKey - The sender's private key for signing.
* @returns The signed, RLP-encoded transaction with 0x04 type prefix.
*/
export function createAndSignEip7702RawTransaction(
chainId: bigint,
nonce: bigint,
max_priority_fee_per_gas: bigint,
max_fee_per_gas: bigint,
gas_limit: bigint,
destination: string,
value: bigint,
data: string,
access_list: [string, string[]][],
authorization_list: Authorization7702[],
eoaPrivateKey: string,
): string {
const txHash = createEip7702TransactionHash(
chainId,
nonce,
max_priority_fee_per_gas,
max_fee_per_gas,
gas_limit,
destination,
value,
data,
access_list,
authorization_list,
);
const basePayload = encodeEip7702TransactionBaseList(
chainId,
nonce,
max_priority_fee_per_gas,
max_fee_per_gas,
gas_limit,
destination,
value,
data,
access_list,
authorization_list,
);
const signature = signHash(txHash, eoaPrivateKey);
const payload = basePayload.concat([
bigintToBytes(BigInt(signature.yParity)),
bigintToBytes(signature.r),
bigintToBytes(signature.s),
]);
const transactionPayload = encodeRlp(payload);
return SET_CODE_TX_TYPE + transactionPayload.slice(2);
}
/**
* Computes the keccak256 hash of an EIP-7702 transaction for signing.
* @param chainId - The chain ID for replay protection.
* @param nonce - The sender's transaction nonce.
* @param max_priority_fee_per_gas - The maximum priority fee per gas (tip) in wei.
* @param max_fee_per_gas - The maximum total fee per gas in wei.
* @param gas_limit - The maximum gas units for the transaction.
* @param destination - The recipient address (42-character hex string).
* @param value - The amount of ETH to send in wei.
* @param data - The transaction input data.
* @param access_list - The EIP-2930 access list as [address, storageKeys] tuples.
* @param authorization_list - The list of signed EIP-7702 delegation authorizations.
* @returns The transaction hash as a hex string.
*/
export function createEip7702TransactionHash(
chainId: bigint,
nonce: bigint,
max_priority_fee_per_gas: bigint,
max_fee_per_gas: bigint,
gas_limit: bigint,
destination: string,
value: bigint,
data: string,
access_list: [string, string[]][],
authorization_list: Authorization7702[],
): string {
const payload = encodeEip7702TransactionBaseList(
chainId,
nonce,
max_priority_fee_per_gas,
max_fee_per_gas,
gas_limit,
destination,
value,
data,
access_list,
authorization_list,
);
return keccak256(SET_CODE_TX_TYPE + encodeRlp(payload).slice(2));
}
/**
* Encodes the base RLP list for an EIP-7702 transaction (without signature fields).
* Used internally to build the payload that gets hashed and signed.
*/
function encodeEip7702TransactionBaseList(
chainId: bigint,
nonce: bigint,
max_priority_fee_per_gas: bigint,
max_fee_per_gas: bigint,
gas_limit: bigint,
destination: string,
value: bigint,
data: string,
access_list: [string, string[]][],
authorization_list: Authorization7702[],
) {
if (chainId >= 2 ** 64) {
throw new RangeError("Invalid chainId.");
}
if (nonce >= 2 ** 64) {
throw new RangeError("Invalid nonce.");
}
if (destination.length !== 42) {
throw new RangeError("Invalid destination.");
}
const encoded_auth_list = encodeAuthList(authorization_list);
const encoded_access_list = encodeAccessList(access_list);
const payload = [
bigintToBytes(chainId),
bigintToBytes(nonce),
bigintToBytes(max_priority_fee_per_gas),
bigintToBytes(max_fee_per_gas),
bigintToBytes(gas_limit),
destination,
bigintToBytes(value),
data,
encoded_access_list,
encoded_auth_list,
];
return payload;
}
/** Encodes an array of EIP-7702 authorizations into RLP-compatible nested arrays. */
function encodeAuthList(authorization_list: Authorization7702[]) {
const encoded_auth_list = [];
for (const auth of authorization_list) {
if (auth.address.length !== 42) {
throw new RangeError(`Invalid authorization list address: ${auth}`);
}
const encoded_auth = [
bigintToBytes(auth.chainId),
auth.address,
bigintToBytes(auth.nonce),
bigintToBytes(BigInt(auth.yParity)),
bigintToBytes(auth.r),
bigintToBytes(auth.s),
];
encoded_auth_list.push(encoded_auth);
}
return encoded_auth_list;
}
/** Encodes an EIP-2930 access list into RLP-compatible nested arrays. */
function encodeAccessList(access_list: [string, string[]][]) {
const encoded_access_list = [];
for (const [access_add, storage_arr] of access_list) {
if (access_add.length !== 42) {
throw new RangeError(`Invalid access list address: ${access_add}`);
}
const encoded_storage_list = [];
for (const storage of storage_arr) {
if (storage.length !== 66) {
throw new RangeError(`Invalid access list storage: ${storage}`);
}
encoded_storage_list.push(getBytes(storage));
}
encoded_access_list.push([getBytes(access_add), encoded_storage_list]);
}
return encoded_access_list;
}
/** Converts a bigint to a Uint8Array of its big-endian byte representation. */
function bigintToBytes(bi: bigint) {
return getBytes(toBeArray(bi));
}
const SECP256K1_N = BigInt(
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",
);
const SECP256K1_HALF_N = SECP256K1_N / 2n;
/**
* Parse a raw ECDSA signature into its components.
* Supports standard 65-byte (r + s + v) and EIP-2098 64-byte compact formats.
*
* High-s signatures are normalized to the complementary low-s form
* (s' = n - s, flipped yParity) rather than rejected. A high-s value is not
* a signer defect: plain ECDSA produces s uniformly across the range, so
* generic signers (AWS KMS, HSMs, WebCrypto) return high-s for ~half of all
* signatures — the low-s rule is an Ethereum canonicalization convention
* (EIP-2), not part of ECDSA. Per the EIP-2 rationale, the complementary
* signature is equally valid for the same signer and payload, so the
* conversion changes the encoding, not the authorization. Rejecting instead
* would fail nondeterministically on ~half of all signatures from such
* signers, and EIP-7702 makes the unnormalized failure mode silent: nodes
* validate s <= secp256k1n/2 per tuple and skip invalid tuples without
* error, so a high-s authorization would "succeed" without ever applying
* the delegation.
*
* @see https://eips.ethereum.org/EIPS/eip-2 - s-value bound and the
* malleability rationale (flipping s to secp256k1n - s with the v flip
* "would still be valid")
* @see https://eips.ethereum.org/EIPS/eip-7702 - Behavior steps: "Verify s
* is less than or equal to secp256k1n/2" and "If any step above fails,
* immediately stop processing the tuple and continue to the next tuple"
* @param rawSig - Hex string: 128 chars (EIP-2098 compact), or 130/132 chars (standard with 0x prefix)
* @returns An object with yParity (0 or 1), r, and s components (low-s normalized)
*/
function parseRawSignature(rawSig: string): { yParity: 0 | 1; r: bigint; s: bigint } {
const sig = rawSig.startsWith("0x") ? rawSig.slice(2) : rawSig;
if (sig.length !== 128 && sig.length !== 130) {
throw new RangeError(
`invalid signature length: expected 128 (EIP-2098 compact) or 130 (standard) hex chars, got ${sig.length}`,
);
}
const r = BigInt(`0x${sig.slice(0, 64)}`);
let yParity: 0 | 1;
let s: bigint;
if (sig.length === 128) {
// EIP-2098 compact signature (64 bytes): r (32) + yParity||s (32)
const yParityAndS = BigInt(`0x${sig.slice(64, 128)}`);
yParity = Number((yParityAndS >> 255n) & 1n) as 0 | 1;
s = yParityAndS & ((1n << 255n) - 1n);
} else {
// Standard 65-byte signature: r (32) + s (32) + v (1)
s = BigInt(`0x${sig.slice(64, 128)}`);
const v = parseInt(sig.slice(128, 130), 16);
if (v !== 0 && v !== 1 && v !== 27 && v !== 28) {
throw new RangeError(`invalid signature v value: ${v}`);
}
yParity = (v >= 27 ? v - 27 : v) as 0 | 1;
}
// EIP-7702 requires s <= n/2; nodes silently skip high-s authorization
// tuples. Normalize to the complementary low-s signature.
if (s > SECP256K1_HALF_N) {
s = SECP256K1_N - s;
yParity = (1 - yParity) as 0 | 1;
}
return { yParity, r, s };
}
/**
* Converts a bigint to a 0x-prefixed hex string with even-length padding.
* @param value - The bigint value to convert.
* @returns The hex string representation (e.g., "0x01", "0xff").
*/
export function bigintToHex(value: bigint): string {
const hex = value.toString(16);
return hex.length % 2 ? `0x0${hex}` : `0x${hex}`;
}