Skip to content

Commit ebd61a2

Browse files
committed
feat(sdk): basic eip7702 support
1 parent 7711efe commit ebd61a2

15 files changed

+797
-37
lines changed

.changeset/clever-beds-knock.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Feature: Adds beta support for EIP-7702 authorization lists
6+
7+
```ts
8+
import { prepareTransaction, sendTransaction } from "thirdweb";
9+
10+
const transaction = prepareTransaction({
11+
chain: ANVIL_CHAIN,
12+
client: TEST_CLIENT,
13+
value: 100n,
14+
to: TEST_WALLET_B,
15+
authorizations: [
16+
{
17+
address: "0x...",
18+
chainId: 1,
19+
nonce: 420n,
20+
},
21+
],
22+
});
23+
24+
const res = await sendTransaction({
25+
account,
26+
transaction,
27+
});
28+
```
29+
30+
You can access the underlying authorization signing functions like so:
31+
32+
```ts
33+
import { signAuthorization, signedAuthorizations } from "thirdweb";
34+
35+
const signedAuthorization = await signAuthorization({
36+
authorization: {
37+
address: "0x...",
38+
chainId: 1,
39+
nonce: 420n,
40+
},
41+
account: myAccount,
42+
});
43+
44+
const signedAuthorizations = await signedAuthorizations({
45+
authorizations: [
46+
{
47+
address: "0x...",
48+
chainId: 1,
49+
nonce: 420n,
50+
},
51+
],
52+
account: myAccount,
53+
});
54+
```

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/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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type * as ox__Authorization from "ox/Authorization";
2+
3+
/**
4+
* Represents an EIP-7702 authorization object prior to being signed.
5+
*
6+
* @beta
7+
* @transaction
8+
*/
9+
export type Authorization = ox__Authorization.List<false>[number];
10+
11+
/**
12+
* Represents a signed EIP-7702 authorization object.
13+
*
14+
* @beta
15+
* @transaction
16+
*/
17+
export type SignedAuthorization = ox__Authorization.ListSigned[number];
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
TEST_ACCOUNT_A,
4+
TEST_ACCOUNT_B,
5+
TEST_ACCOUNT_C,
6+
} from "../../../../test/src/test-wallets.js";
7+
import type { Authorization } from "./authorization.js";
8+
import { signAuthorization, signAuthorizations } from "./sign-authorization.js";
9+
10+
describe("signAuthorization", () => {
11+
it("should sign a single authorization", async () => {
12+
const signedAuth = await signAuthorization({
13+
authorization: {
14+
address: TEST_ACCOUNT_B.address,
15+
chainId: 1,
16+
nonce: 420n,
17+
},
18+
account: TEST_ACCOUNT_A,
19+
});
20+
21+
expect(signedAuth).toMatchInlineSnapshot(`
22+
{
23+
"address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
24+
"chainId": 1,
25+
"nonce": 420n,
26+
"r": 102725860776773423628916613503801445158901377966270769628473190730870496434730n,
27+
"s": 25812735492740097245003859854864857180049536150520605332837906559451311728806n,
28+
"yParity": 0,
29+
}
30+
`);
31+
});
32+
});
33+
34+
describe("signAuthorizations", () => {
35+
it("should sign multiple authorizations", async () => {
36+
const authorizations: Authorization[] = [
37+
{
38+
address: TEST_ACCOUNT_B.address,
39+
chainId: 1,
40+
nonce: 420n,
41+
},
42+
{
43+
address: TEST_ACCOUNT_C.address,
44+
chainId: 1,
45+
nonce: 421n,
46+
},
47+
];
48+
49+
const authorizationList = await signAuthorizations({
50+
authorizations,
51+
account: TEST_ACCOUNT_A,
52+
});
53+
54+
expect(authorizationList).toMatchInlineSnapshot(`
55+
[
56+
{
57+
"address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
58+
"chainId": 1,
59+
"nonce": 420n,
60+
"r": 102725860776773423628916613503801445158901377966270769628473190730870496434730n,
61+
"s": 25812735492740097245003859854864857180049536150520605332837906559451311728806n,
62+
"yParity": 0,
63+
},
64+
{
65+
"address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
66+
"chainId": 1,
67+
"nonce": 421n,
68+
"r": 108482619199996736781930236539430222748019956875459721295513303884715556878431n,
69+
"s": 17328756565811311106451462386592517658555637219672290207851357199682941049520n,
70+
"yParity": 0,
71+
},
72+
]
73+
`);
74+
});
75+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as ox__Authorization from "ox/Authorization";
2+
import * as ox__Signature from "ox/Signature";
3+
import type { Account } from "../../../wallets/interfaces/wallet.js";
4+
import type { Authorization, SignedAuthorization } from "./authorization.js";
5+
6+
/**
7+
* Signs a single EIP-7702 authorization using the provided account.
8+
*
9+
* @param options - The options for signing the authorization.
10+
* @param options.authorization - The authorization object to be signed.
11+
* @param options.account - The account used to sign the authorization.
12+
* @returns A promise that resolves to a `SignedAuthorization` object.
13+
*
14+
* @example
15+
* ```typescript
16+
* const signedAuth = await signAuthorization({
17+
* authorization: myAuthorization,
18+
* account: myAccount,
19+
* });
20+
* ```
21+
*
22+
* @beta
23+
* @transaction
24+
*/
25+
export async function signAuthorization(options: {
26+
authorization: Authorization;
27+
account: Account;
28+
}): Promise<SignedAuthorization> {
29+
const { authorization, account } = options;
30+
const hexSignature = await account.signMessage({
31+
message: { raw: ox__Authorization.getSignPayload(authorization) },
32+
});
33+
return ox__Authorization.from(authorization, {
34+
signature: ox__Signature.fromHex(hexSignature),
35+
});
36+
}
37+
38+
/**
39+
* Signs a list of EIP-7702 authorizations using the provided account.
40+
*
41+
* @param options - The options for signing the authorizations.
42+
* @param options.authorizations - An array of authorization objects to be signed.
43+
* @param options.account - The account used to sign the authorizations.
44+
* @returns A promise that resolves to an array of `SignedAuthorization` objects.
45+
*
46+
* @example
47+
* ```typescript
48+
* const signedAuths = await signAuthorizations({
49+
* authorizations: [auth1, auth2],
50+
* account: myAccount,
51+
* });
52+
* ```
53+
*
54+
* @beta
55+
* @transaction
56+
*/
57+
export async function signAuthorizations(options: {
58+
authorizations: Authorization[];
59+
account: Account;
60+
}): Promise<SignedAuthorization[]> {
61+
const { authorizations, account } = options;
62+
63+
return Promise.all(
64+
authorizations.map(async (authorization: Authorization) =>
65+
signAuthorization({
66+
authorization,
67+
account,
68+
}),
69+
),
70+
);
71+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expect, it, vi } from "vitest";
2+
import { wagmiTokenContractConfig } from "../../../test/src/abis/wagmiToken.js";
23
import { TEST_WALLET_B } from "../../../test/src/addresses.js";
34
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
45
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
56
import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js";
7+
import type { Address } from "../../utils/address.js";
68
import { prepareTransaction } from "../prepare-transaction.js";
79
import * as TransactionStore from "../transaction-store.js";
810
import { sendTransaction } from "./send-transaction.js";
@@ -28,6 +30,33 @@ describe("sendTransaction", () => {
2830
expect(res.transactionHash.length).toBe(66);
2931
});
3032

33+
it("should send an eip7702 transaction", async () => {
34+
const transaction = prepareTransaction({
35+
chain: ANVIL_CHAIN,
36+
client: TEST_CLIENT,
37+
value: 100n,
38+
to: TEST_WALLET_B,
39+
authorizations: [
40+
{
41+
address: wagmiTokenContractConfig.address.toLowerCase() as Address,
42+
chainId: 1,
43+
nonce: 420n,
44+
},
45+
{
46+
address: "0x0000000000000000000000000000000000000000",
47+
chainId: 10,
48+
nonce: 69n,
49+
},
50+
],
51+
});
52+
const res = await sendTransaction({
53+
account: TEST_ACCOUNT_A,
54+
transaction,
55+
});
56+
57+
expect(res.transactionHash.length).toBe(66);
58+
});
59+
3160
it("should add transaction to session", async () => {
3261
const transaction = prepareTransaction({
3362
chain: ANVIL_CHAIN,

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,32 @@ 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 } from "thirdweb";
119+
* import { sepolia } from "thirdweb/chains";
120+
*
121+
* const transaction = prepareTransaction({
122+
* chain: sepolia,
123+
* client: client,
124+
* authorizations: [
125+
* {
126+
* address: "0x...",
127+
* chainId: 1,
128+
* nonce: 420n,
129+
* },
130+
* ],
131+
* });
132+
*
133+
* const { transactionHash } = await sendTransaction({
134+
* account,
135+
* transaction,
136+
* });
137+
* ```
138+
*
113139
* ### Gasless usage with [thirdweb Engine](https://portal.thirdweb.com/engine)
114140
* ```ts
115141
* const { transactionHash } = await sendTransaction({
@@ -166,7 +192,7 @@ export async function sendTransaction(
166192

167193
const serializableTransaction = await toSerializableTransaction({
168194
transaction: transaction,
169-
from: account.address,
195+
from: account,
170196
});
171197

172198
// branch for gasless transactions

0 commit comments

Comments
 (0)