Skip to content

Commit db490e1

Browse files
committed
Initial EIP-7702 support (#4916).
1 parent 844ae68 commit db490e1

14 files changed

+351
-20
lines changed

docs.wrm/links/specs.txt

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ link-eip-2930 [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)
3434
link-eip-4788 [EIP-4844](https://eips.ethereum.org/EIPS/eip-4788)
3535
link-eip-4844 [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)
3636
link-eip-6963 [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963)
37+
link-eip-7702 [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702)
3738

3839
# Open Standards
3940
link-base58 [Base58](https://en.bitcoinwiki.org/wiki/Base58)

src.ts/ethers.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export {
4949
export {
5050
id,
5151
ensNormalize, isValidName, namehash, dnsEncode,
52+
hashAuthorization, verifyAuthorization,
5253
hashMessage, verifyMessage,
5354
solidityPacked, solidityPackedKeccak256, solidityPackedSha256,
5455
TypedDataEncoder,
@@ -89,7 +90,7 @@ export {
8990
} from "./providers/index.js";
9091

9192
export {
92-
accessListify,
93+
accessListify, authorizationify,
9394
computeAddress, recoverAddress,
9495
Transaction
9596
} from "./transaction/index.js";
@@ -159,7 +160,9 @@ export type {
159160

160161
export type { ProgressCallback, SignatureLike } from "./crypto/index.js";
161162

162-
export type { TypedDataDomain, TypedDataField } from "./hash/index.js";
163+
export type {
164+
AuthorizationRequest, TypedDataDomain, TypedDataField
165+
} from "./hash/index.js";
163166

164167
export type {
165168
Provider, Signer,
@@ -182,6 +185,7 @@ export type {
182185

183186
export type {
184187
AccessList, AccessListish, AccessListEntry,
188+
Authorization, AuthorizationLike,
185189
Blob, BlobLike, KzgLibrary, KzgLibraryLike,
186190
TransactionLike
187191
} from "./transaction/index.js";

src.ts/hash/authorization.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getAddress } from "../address/index.js";
2+
import { keccak256 } from "../crypto/index.js";
3+
import { recoverAddress } from "../transaction/index.js";
4+
import {
5+
assertArgument, concat, encodeRlp, toBeArray
6+
} from "../utils/index.js";
7+
8+
import type { Addressable } from "../address/index.js";
9+
import type { SignatureLike } from "../crypto/index.js";
10+
import type { BigNumberish, Numeric } from "../utils/index.js";
11+
12+
export interface AuthorizationRequest {
13+
address: string | Addressable;
14+
nonce?: Numeric;
15+
chainId?: BigNumberish;
16+
}
17+
18+
/**
19+
* Computes the [[link-eip-7702]] authorization digest to sign.
20+
*/
21+
export function hashAuthorization(auth: AuthorizationRequest): string {
22+
assertArgument(typeof(auth.address) === "string", "invalid address for hashAuthorization", "auth.address", auth);
23+
return keccak256(concat([
24+
"0x05", encodeRlp([
25+
(auth.chainId != null) ? toBeArray(auth.chainId): "0x",
26+
getAddress(auth.address),
27+
(auth.nonce != null) ? toBeArray(auth.nonce): "0x",
28+
])
29+
]));
30+
}
31+
32+
/**
33+
* Return the address of the private key that produced
34+
* the signature %%sig%% during signing for %%message%%.
35+
*/
36+
export function verifyAuthorization(auth: AuthorizationRequest, sig: SignatureLike): string {
37+
return recoverAddress(hashAuthorization(auth), sig);
38+
}

src.ts/hash/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @_section: api/hashing:Hashing Utilities [about-hashing]
66
*/
77

8+
export { hashAuthorization, verifyAuthorization } from "./authorization.js";
89
export { id } from "./id.js"
910
export { ensNormalize, isValidName, namehash, dnsEncode } from "./namehash.js";
1011
export { hashMessage, verifyMessage } from "./message.js";
@@ -13,4 +14,5 @@ export {
1314
} from "./solidity.js";
1415
export { TypedDataEncoder, verifyTypedData } from "./typed-data.js";
1516

17+
export type { AuthorizationRequest } from "./authorization.js";
1618
export type { TypedDataDomain, TypedDataField } from "./typed-data.js";

src.ts/providers/abstract-signer.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ import {
1414

1515
import { copyRequest } from "./provider.js";
1616

17-
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
18-
import type { TransactionLike } from "../transaction/index.js";
17+
import type {
18+
AuthorizationRequest, TypedDataDomain, TypedDataField
19+
} from "../hash/index.js";
20+
import type { Authorization, TransactionLike } from "../transaction/index.js";
1921

2022
import type {
2123
BlockTag, Provider, TransactionRequest, TransactionResponse
2224
} from "./provider.js";
2325
import type { Signer } from "./signer.js";
2426

25-
2627
function checkProvider(signer: AbstractSigner, operation: string): Provider {
2728
if (signer.provider) { return signer.provider; }
2829
assert(false, "missing provider", "UNSUPPORTED_OPERATION", { operation });
@@ -194,7 +195,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
194195
operation: "signer.getFeeData" });
195196
}
196197

197-
} else if (pop.type === 2 || pop.type === 3) {
198+
} else if (pop.type === 2 || pop.type === 3 || pop.type === 4) {
198199
// Explicitly using EIP-1559 or EIP-4844
199200

200201
// Populate missing fee data
@@ -213,6 +214,21 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
213214
return await resolveProperties(pop);
214215
}
215216

217+
async populateAuthorization(_auth: AuthorizationRequest): Promise<AuthorizationRequest> {
218+
const auth = Object.assign({ }, _auth);
219+
220+
// Add a chain ID if not explicitly set to 0
221+
if (auth.chainId == null) {
222+
auth.chainId = (await checkProvider(this, "getNetwork").getNetwork()).chainId;
223+
}
224+
225+
// @TODO: Take chain ID into account when populating noce?
226+
227+
if (auth.nonce == null) { auth.nonce = await this.getNonce(); }
228+
229+
return auth;
230+
}
231+
216232
async estimateGas(tx: TransactionRequest): Promise<bigint> {
217233
return checkProvider(this, "estimateGas").estimateGas(await this.populateCall(tx));
218234
}
@@ -235,6 +251,12 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
235251
return await provider.broadcastTransaction(await this.signTransaction(txObj));
236252
}
237253

254+
// @TODO: in v7 move this to be abstract
255+
authorize(authorization: AuthorizationRequest): Promise<Authorization> {
256+
assert(false, "authorization not implemented for this signer",
257+
"UNSUPPORTED_OPERATION", { operation: "authorize" });
258+
}
259+
238260
abstract signTransaction(tx: TransactionRequest): Promise<string>;
239261
abstract signMessage(message: string | Uint8Array): Promise<string>;
240262
abstract signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string>;

src.ts/providers/format.ts

+9
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ export function formatTransactionResponse(value: any): TransactionResponseParams
214214
accessList: allowNull(accessListify, null),
215215
blobVersionedHashes: allowNull(arrayOf(formatHash, true), null),
216216

217+
authorizationList: allowNull(arrayOf((v: any) => {
218+
return {
219+
address: getAddress(v.address),
220+
chainId: getBigInt(v.chainId),
221+
nonce: getBigInt(v.nonce),
222+
signature: Signature.from(v.signature ? v.signature: v)
223+
};
224+
}, false), null),
225+
217226
blockHash: allowNull(formatHash, null),
218227
blockNumber: allowNull(getNumber, null),
219228
transactionIndex: allowNull(getNumber, null),

src.ts/providers/formatting.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import type { Signature } from "../crypto/index.js";
8-
import type { AccessList } from "../transaction/index.js";
8+
import type { Authorization, AccessList } from "../transaction/index.js";
99

1010

1111
//////////////////////
@@ -283,6 +283,7 @@ export interface TransactionReceiptParams {
283283
* post-byzantium blocks this is null.
284284
*/
285285
root: null | string;
286+
286287
}
287288

288289
/*
@@ -406,7 +407,12 @@ export interface TransactionResponseParams {
406407
/**
407408
* The [[link-eip-4844]] BLOb versioned hashes.
408409
*/
409-
blobVersionedHashes?: null | Array<string>;
410+
blobVersionedHashes?: null | Array<string>; // @TODO: drop the "?"? (v7)
411+
412+
/**
413+
* The [[link-eip-7702]] authorizations (if any).
414+
*/
415+
authorizationList: null | Array<Authorization>;
410416
};
411417

412418

src.ts/providers/provider-jsonrpc.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { AbiCoder } from "../abi/index.js";
1919
import { getAddress, resolveAddress } from "../address/index.js";
2020
import { TypedDataEncoder } from "../hash/index.js";
21-
import { accessListify } from "../transaction/index.js";
21+
import { accessListify, authorizationify } from "../transaction/index.js";
2222
import {
2323
defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
2424
isError, makeError, assert, assertArgument,
@@ -277,6 +277,14 @@ export interface JsonRpcTransactionRequest {
277277
* The transaction access list.
278278
*/
279279
accessList?: Array<{ address: string, storageKeys: Array<string> }>;
280+
281+
/**
282+
* The transaction authorization list.
283+
*/
284+
authorizationList?: Array<{
285+
address: string, nonce: string, chainId: string,
286+
yParity: string, r: string, s: string
287+
}>;
280288
}
281289

282290
// @TODO: Unchecked Signers
@@ -850,6 +858,20 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
850858
(<any>result)["blobVersionedHashes"] = tx.blobVersionedHashes.map(h => h.toLowerCase());
851859
}
852860

861+
if (tx.authorizationList) {
862+
result["authorizationList"] = tx.authorizationList.map((_a) => {
863+
const a = authorizationify(_a);
864+
return {
865+
address: a.address,
866+
nonce: toQuantity(a.nonce),
867+
chainId: toQuantity(a.chainId),
868+
yParity: toQuantity(a.signature.yParity),
869+
r: a.signature.r,
870+
s: a.signature.s,
871+
}
872+
});
873+
}
874+
853875
// @TODO: blobs should probably also be copied over, optionally
854876
// accounting for the kzg property to backfill blobVersionedHashes
855877
// using the commitment. Or should that be left as an exercise to

src.ts/providers/provider.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type { AddressLike, NameResolver } from "../address/index.js";
1010
import type { BigNumberish, EventEmitterable } from "../utils/index.js";
1111
import type { Signature } from "../crypto/index.js";
1212
import type {
13-
AccessList, AccessListish, BlobLike, KzgLibraryLike, TransactionLike
13+
AccessList, AccessListish, Authorization, AuthorizationLike, BlobLike,
14+
KzgLibraryLike, TransactionLike
1415
} from "../transaction/index.js";
1516

1617
import type { ContractRunner } from "./contracts.js";
@@ -241,6 +242,11 @@ export interface TransactionRequest {
241242
*/
242243
kzg?: null | KzgLibraryLike;
243244

245+
/**
246+
* The [[link-eip-7702]] authorizations (if any).
247+
*/
248+
authorizationList?: null | Array<AuthorizationLike>;
249+
244250
// Todo?
245251
//gasMultiplier?: number;
246252
};
@@ -319,6 +325,11 @@ export interface PreparedTransactionRequest {
319325
*/
320326
accessList?: AccessList;
321327

328+
/**
329+
* The [[link-eip-7702]] authorizations (if any).
330+
*/
331+
authorizationList?: Array<Authorization>;
332+
322333
/**
323334
* A custom object, which can be passed along for network-specific
324335
* values.
@@ -375,6 +386,10 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest
375386
result.accessList = accessListify(req.accessList);
376387
}
377388

389+
if (req.authorizationList) {
390+
result.authorizationList = req.authorizationList.slice();
391+
}
392+
378393
if ("blockTag" in req) { result.blockTag = req.blockTag; }
379394

380395
if ("enableCcipRead" in req) {
@@ -1375,6 +1390,11 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
13751390
*/
13761391
readonly blobVersionedHashes!: null | Array<string>;
13771392

1393+
/**
1394+
* The [[link-eip-7702]] authorizations (if any).
1395+
*/
1396+
readonly authorizationList!: null | Array<Authorization>;
1397+
13781398
#startBlock: number;
13791399

13801400
/**
@@ -1410,6 +1430,8 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
14101430
this.accessList = (tx.accessList != null) ? tx.accessList: null;
14111431
this.blobVersionedHashes = (tx.blobVersionedHashes != null) ? tx.blobVersionedHashes: null;
14121432

1433+
this.authorizationList = (tx.authorizationList != null) ? tx.authorizationList: null;
1434+
14131435
this.#startBlock = -1;
14141436
}
14151437

src.ts/providers/signer.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
import type { Addressable, NameResolver } from "../address/index.js";
3-
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
4-
import type { TransactionLike } from "../transaction/index.js";
3+
import type {
4+
AuthorizationRequest, TypedDataDomain, TypedDataField
5+
} from "../hash/index.js";
6+
import type { Authorization, TransactionLike } from "../transaction/index.js";
57

68
import type { ContractRunner } from "./contracts.js";
79
import type { BlockTag, Provider, TransactionRequest, TransactionResponse } from "./provider.js";
@@ -146,4 +148,19 @@ export interface Signer extends Addressable, ContractRunner, NameResolver {
146148
* Signs the [[link-eip-712]] typed data.
147149
*/
148150
signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string>;
151+
152+
/**
153+
* Prepares an [[AuthorizationRequest]] for authorization by
154+
* populating any missing properties:
155+
* - resolves ``address`` (if an Addressable or ENS name)
156+
* - populates ``nonce`` via ``signer.getNonce("pending")``
157+
* - populates ``chainId`` via ``signer.provider.getNetwork()``
158+
*/
159+
populateAuthorization(auth: AuthorizationRequest): Promise<AuthorizationRequest>;
160+
161+
/**
162+
* Signs an %%authorization%% to be used in [[link-eip-7702]]
163+
* transactions.
164+
*/
165+
authorize(authorization: AuthorizationRequest): Promise<Authorization>;
149166
}

src.ts/transaction/authorization.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { getAddress } from "../address/index.js";
2+
import { Signature } from "../crypto/index.js";
3+
import { getBigInt } from "../utils/index.js";
4+
5+
import type { Authorization, AuthorizationLike } from "./index.js";
6+
7+
export function authorizationify(auth: AuthorizationLike): Authorization {
8+
return {
9+
address: getAddress(auth.address),
10+
nonce: getBigInt((auth.nonce != null) ? auth.nonce: 0),
11+
chainId: getBigInt((auth.chainId != null)? auth.chainId: 0),
12+
signature: Signature.from(auth.signature)
13+
};
14+
}

src.ts/transaction/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
null;
88

9+
import type { BigNumberish } from "../utils/maths.js";
10+
import type { Signature, SignatureLike } from "../crypto/index.js";
11+
912
/**
1013
* A single [[AccessList]] entry of storage keys (slots) for an address.
1114
*/
@@ -23,8 +26,23 @@ export type AccessListish = AccessList |
2326
Array<[ string, Array<string> ]> |
2427
Record<string, Array<string>>;
2528

29+
// Keep here?
30+
export interface Authorization {
31+
address: string;
32+
nonce: bigint;
33+
chainId: bigint;
34+
signature: Signature;
35+
}
36+
37+
export type AuthorizationLike = {
38+
address: string;
39+
nonce: BigNumberish;
40+
chainId: BigNumberish;
41+
signature: SignatureLike
42+
};
2643

2744
export { accessListify } from "./accesslist.js";
45+
export { authorizationify } from "./authorization.js";
2846
export { computeAddress, recoverAddress } from "./address.js";
2947
export { Transaction } from "./transaction.js";
3048

0 commit comments

Comments
 (0)