Skip to content

Commit 429e112

Browse files
committed
[SDK] Feature: Basic EIP7702 Support (#5801)
<!-- start pr-codex --> ## PR-Codex overview This PR introduces beta support for `EIP-7702` authorization lists, enhancing transaction handling and signing within the `thirdweb` library. ### Detailed summary - Added `style` configuration in `biome.json`. - Introduced `signAuthorization` function for EIP-7702 authorizations. - Updated transaction preparation to include `authorizationList`. - Added tests for `signAuthorization` and transaction serialization. - Modified `serializeTransaction` to handle EIP-7702 transactions. - Enhanced gas estimation and transaction preparation functions to support authorization lists. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent ddb6af1 commit 429e112

26 files changed

+729
-65
lines changed

.changeset/clever-beds-knock.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Feature: Adds beta support for EIP-7702 authorization lists
6+
7+
```ts
8+
import { prepareTransaction, sendTransaction, signAuthorization } from "thirdweb";
9+
10+
const authorization = await signAuthorization({
11+
request: {
12+
address: "0x...",
13+
chainId: 911867,
14+
nonce: 100n,
15+
},
16+
account: myAccount,
17+
});
18+
19+
const transaction = prepareTransaction({
20+
chain: ANVIL_CHAIN,
21+
client: TEST_CLIENT,
22+
value: 100n,
23+
to: TEST_WALLET_B,
24+
authorizationList: [authorization],
25+
});
26+
27+
const res = await sendTransaction({
28+
account,
29+
transaction,
30+
});
31+
```
32+

packages/thirdweb/biome.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"rules": {
2424
"nursery": {
2525
"noProcessEnv": "off"
26+
},
27+
"style": {
28+
"noUnusedTemplateLiteral": "off"
2629
}
2730
}
2831
}

packages/thirdweb/src/adapters/ethers5.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ export async function toEthersSigner(
483483

484484
const response: ethers5.ethers.providers.TransactionResponse = {
485485
...serialized,
486-
nonce: serialized.nonce ?? 0,
486+
nonce: Number(serialized.nonce ?? 0),
487487
from: account.address,
488488
maxFeePerGas: serialized.maxFeePerGas
489489
? ethers.BigNumber.from(serialized.maxFeePerGas)

packages/thirdweb/src/exports/thirdweb.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,12 @@ export {
303303
verifyTypedData,
304304
} from "../auth/verify-typed-data.js";
305305

306+
/**
307+
* EIP-7702
308+
*/
309+
export type {
310+
AuthorizationRequest,
311+
SignedAuthorization,
312+
} from "../transaction/actions/eip7702/authorization.js";
313+
export { signAuthorization } from "../transaction/actions/eip7702/authorization.js";
306314
export { deploySmartAccount } from "../wallets/smart/lib/signing.js";

packages/thirdweb/src/exports/transaction.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ export type { GaslessOptions } from "../transaction/actions/gasless/types.js";
7878
export type { EngineOptions } from "../transaction/actions/gasless/providers/engine.js";
7979
export type { OpenZeppelinOptions } from "../transaction/actions/gasless/providers/openzeppelin.js";
8080
export type { BiconomyOptions } from "../transaction/actions/gasless/providers/biconomy.js";
81+
82+
/**
83+
* EIP-7702
84+
*/
85+
export type {
86+
AuthorizationRequest,
87+
SignedAuthorization,
88+
} from "../transaction/actions/eip7702/authorization.js";
89+
export { signAuthorization } from "../transaction/actions/eip7702/authorization.js";

packages/thirdweb/src/gas/estimate-l1-fee.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { serializeTransaction } from "viem";
21
import { getContract } from "../contract/contract.js";
32
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
43
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
54
import { readContract } from "../transaction/read-contract.js";
5+
import { serializeTransaction } from "../transaction/serialize-transaction.js";
66

77
type EstimateL1FeeOptions = {
88
transaction: PreparedTransaction;
@@ -29,8 +29,7 @@ export async function estimateL1Fee(options: EstimateL1FeeOptions) {
2929
transaction,
3030
});
3131
const serialized = serializeTransaction({
32-
...serializableTx,
33-
type: "eip1559",
32+
transaction: serializableTx,
3433
});
3534
//serializeTransaction(transaction);
3635
return readContract({
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from "vitest";
2+
import { TEST_WALLET_B } from "~test/addresses.js";
3+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
4+
import { signAuthorization } from "./authorization.js";
5+
6+
describe("signAuthorization", () => {
7+
it("should sign an authorization", async () => {
8+
const authorization = await signAuthorization({
9+
account: TEST_ACCOUNT_A,
10+
request: {
11+
address: TEST_WALLET_B,
12+
chainId: 911867,
13+
nonce: 0n,
14+
},
15+
});
16+
expect(authorization).toMatchInlineSnapshot(`
17+
{
18+
"address": "0x0000000000000000000000000000000000000002",
19+
"chainId": 911867,
20+
"nonce": 0n,
21+
"r": 3720526934953059641417422884731844424204826752871127418111522219225437830766n,
22+
"s": 23451045058292828843243765241045958975073226494910356096978666517928790374894n,
23+
"yParity": 1,
24+
}
25+
`);
26+
});
27+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type * as ox__Authorization from "ox/Authorization";
2+
import type { Address } from "../../../utils/address.js";
3+
import type { Account } from "../../../wallets/interfaces/wallet.js";
4+
5+
/**
6+
* An EIP-7702 authorization object fully prepared and ready for signing.
7+
*
8+
* @beta
9+
* @transaction
10+
*/
11+
export type AuthorizationRequest = {
12+
address: Address;
13+
chainId: number;
14+
nonce: bigint;
15+
};
16+
17+
/**
18+
* Represents a signed EIP-7702 authorization object.
19+
*
20+
* @beta
21+
* @transaction
22+
*/
23+
export type SignedAuthorization = ox__Authorization.ListSigned[number];
24+
25+
/**
26+
* Sign the given EIP-7702 authorization object.
27+
* @param options - The options for `signAuthorization`
28+
* Refer to the type [`SignAuthorizationOptions`](https://portal.thirdweb.com/references/typescript/v5/SignAuthorizationOptions)
29+
* @returns The signed authorization object
30+
*
31+
* ```ts
32+
* import { signAuthorization } from "thirdweb";
33+
*
34+
* const authorization = await signAuthorization({
35+
* request: {
36+
* address: "0x...",
37+
* chainId: 911867,
38+
* nonce: 100n,
39+
* },
40+
* account: myAccount,
41+
* });
42+
* ```
43+
*
44+
* @beta
45+
* @transaction
46+
*/
47+
export async function signAuthorization(options: {
48+
account: Account;
49+
request: AuthorizationRequest;
50+
}): Promise<SignedAuthorization> {
51+
const { account, request } = options;
52+
if (typeof account.signAuthorization === "undefined") {
53+
throw new Error(
54+
"This account type does not yet support signing EIP-7702 authorizations",
55+
);
56+
}
57+
return account.signAuthorization(request);
58+
}

packages/thirdweb/src/transaction/actions/estimate-gas.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as ox__Hex from "ox/Hex";
12
import { formatTransactionRequest } from "viem";
23
import { roundUpGas } from "../../gas/op-gas-fee-reducer.js";
34
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
@@ -18,6 +19,8 @@ export type EstimateGasOptions = Prettify<
1819
| {
1920
/**
2021
* The account the transaction would be sent from.
22+
*
23+
* @deprecated Use `from` instead
2124
*/
2225
account: Account;
2326
from?: never;
@@ -27,7 +30,7 @@ export type EstimateGasOptions = Prettify<
2730
/**
2831
* The address the transaction would be sent from.
2932
*/
30-
from?: string;
33+
from?: string | Account;
3134
}
3235
)
3336
>;
@@ -60,8 +63,11 @@ export async function estimateGas(
6063
// 1. the user specified from address
6164
// 2. the passed in account address
6265
// 3. the passed in wallet's account address
63-
const from = options.from ?? options.account?.address ?? undefined;
64-
const txWithFrom = { ...options.transaction, from };
66+
const fromAddress =
67+
typeof options.from === "string"
68+
? (options.from ?? undefined)
69+
: (options.from?.address ?? options.account?.address);
70+
const txWithFrom = { ...options.transaction, from: fromAddress };
6571
if (cache.has(txWithFrom)) {
6672
// biome-ignore lint/style/noNonNullAssertion: the `has` above ensures that this will always be set
6773
return cache.get(txWithFrom)!;
@@ -92,11 +98,13 @@ export async function estimateGas(
9298

9399
// load up encode function if we need it
94100
const { encode } = await import("./encode.js");
95-
const [encodedData, toAddress, value] = await Promise.all([
96-
encode(options.transaction),
97-
resolvePromisedValue(options.transaction.to),
98-
resolvePromisedValue(options.transaction.value),
99-
]);
101+
const [encodedData, toAddress, value, authorizationList] =
102+
await Promise.all([
103+
encode(options.transaction),
104+
resolvePromisedValue(options.transaction.to),
105+
resolvePromisedValue(options.transaction.value),
106+
resolvePromisedValue(options.transaction.authorizationList),
107+
]);
100108

101109
// load up the rpc client and the estimateGas function if we need it
102110
const [{ getRpcClient }, { eth_estimateGas }] = await Promise.all([
@@ -111,10 +119,19 @@ export async function estimateGas(
111119
formatTransactionRequest({
112120
to: toAddress,
113121
data: encodedData,
114-
from,
122+
from: fromAddress,
115123
value,
124+
// TODO: Remove this casting when we migrate this file to Ox
125+
authorizationList: authorizationList?.map((auth) => ({
126+
...auth,
127+
r: ox__Hex.fromNumber(auth.r),
128+
s: ox__Hex.fromNumber(auth.s),
129+
nonce: Number(auth.nonce),
130+
contractAddress: auth.address,
131+
})),
116132
}),
117133
);
134+
118135
if (options.transaction.chain.experimental?.increaseZeroByteCount) {
119136
gas = roundUpGas(gas);
120137
}

packages/thirdweb/src/transaction/actions/send-transaction.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,37 @@ export interface SendTransactionOptions {
110110
* });
111111
* ```
112112
*
113+
* ### Send an EIP-7702 Transaction
114+
*
115+
* **Note: This feature is in beta and is subject to breaking changes**
116+
*
117+
* ```ts
118+
* import { sendTransaction, prepareTransaction, signAuthorization } from "thirdweb";
119+
* import { sepolia } from "thirdweb/chains";
120+
*
121+
* const authorization = await signAuthorization({
122+
* request: {
123+
* address: "0x...",
124+
* chainId: 1,
125+
* nonce: 0n,
126+
* },
127+
* account: myAccount,
128+
* });
129+
*
130+
* const transaction = prepareTransaction({
131+
* chain: sepolia,
132+
* client: client,
133+
* to: "0x...",
134+
* value: 0n,
135+
* authorizationList: [authorization],
136+
* });
137+
*
138+
* const { transactionHash } = await sendTransaction({
139+
* account,
140+
* transaction,
141+
* });
142+
* ```
143+
*
113144
* ### Gasless usage with [thirdweb Engine](https://portal.thirdweb.com/engine)
114145
* ```ts
115146
* const { transactionHash } = await sendTransaction({
@@ -166,9 +197,8 @@ export async function sendTransaction(
166197

167198
const serializableTransaction = await toSerializableTransaction({
168199
transaction: transaction,
169-
from: account.address,
200+
from: account,
170201
});
171-
172202
// branch for gasless transactions
173203
if (gasless) {
174204
// lazy load the gasless tx function because it's only needed for gasless transactions

0 commit comments

Comments
 (0)