Skip to content

Commit

Permalink
[SDK] Feature: Basic EIP7702 Support (#5801)
Browse files Browse the repository at this point in the history
<!-- 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 -->
  • Loading branch information
gregfromstl committed Jan 7, 2025
1 parent ddb6af1 commit 429e112
Show file tree
Hide file tree
Showing 26 changed files with 729 additions and 65 deletions.
32 changes: 32 additions & 0 deletions .changeset/clever-beds-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"thirdweb": minor
---

Feature: Adds beta support for EIP-7702 authorization lists

```ts
import { prepareTransaction, sendTransaction, signAuthorization } from "thirdweb";

const authorization = await signAuthorization({
request: {
address: "0x...",
chainId: 911867,
nonce: 100n,
},
account: myAccount,
});

const transaction = prepareTransaction({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
value: 100n,
to: TEST_WALLET_B,
authorizationList: [authorization],
});

const res = await sendTransaction({
account,
transaction,
});
```

3 changes: 3 additions & 0 deletions packages/thirdweb/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"rules": {
"nursery": {
"noProcessEnv": "off"
},
"style": {
"noUnusedTemplateLiteral": "off"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/adapters/ethers5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ export async function toEthersSigner(

const response: ethers5.ethers.providers.TransactionResponse = {
...serialized,
nonce: serialized.nonce ?? 0,
nonce: Number(serialized.nonce ?? 0),
from: account.address,
maxFeePerGas: serialized.maxFeePerGas
? ethers.BigNumber.from(serialized.maxFeePerGas)
Expand Down
8 changes: 8 additions & 0 deletions packages/thirdweb/src/exports/thirdweb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,12 @@ export {
verifyTypedData,
} from "../auth/verify-typed-data.js";

/**
* EIP-7702
*/
export type {
AuthorizationRequest,
SignedAuthorization,
} from "../transaction/actions/eip7702/authorization.js";
export { signAuthorization } from "../transaction/actions/eip7702/authorization.js";
export { deploySmartAccount } from "../wallets/smart/lib/signing.js";
9 changes: 9 additions & 0 deletions packages/thirdweb/src/exports/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ export type { GaslessOptions } from "../transaction/actions/gasless/types.js";
export type { EngineOptions } from "../transaction/actions/gasless/providers/engine.js";
export type { OpenZeppelinOptions } from "../transaction/actions/gasless/providers/openzeppelin.js";
export type { BiconomyOptions } from "../transaction/actions/gasless/providers/biconomy.js";

/**
* EIP-7702
*/
export type {
AuthorizationRequest,
SignedAuthorization,
} from "../transaction/actions/eip7702/authorization.js";
export { signAuthorization } from "../transaction/actions/eip7702/authorization.js";
5 changes: 2 additions & 3 deletions packages/thirdweb/src/gas/estimate-l1-fee.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { serializeTransaction } from "viem";
import { getContract } from "../contract/contract.js";
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
import { readContract } from "../transaction/read-contract.js";
import { serializeTransaction } from "../transaction/serialize-transaction.js";

type EstimateL1FeeOptions = {
transaction: PreparedTransaction;
Expand All @@ -29,8 +29,7 @@ export async function estimateL1Fee(options: EstimateL1FeeOptions) {
transaction,
});
const serialized = serializeTransaction({
...serializableTx,
type: "eip1559",
transaction: serializableTx,
});
//serializeTransaction(transaction);
return readContract({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { TEST_WALLET_B } from "~test/addresses.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { signAuthorization } from "./authorization.js";

describe("signAuthorization", () => {
it("should sign an authorization", async () => {
const authorization = await signAuthorization({
account: TEST_ACCOUNT_A,
request: {
address: TEST_WALLET_B,
chainId: 911867,
nonce: 0n,
},
});
expect(authorization).toMatchInlineSnapshot(`
{
"address": "0x0000000000000000000000000000000000000002",
"chainId": 911867,
"nonce": 0n,
"r": 3720526934953059641417422884731844424204826752871127418111522219225437830766n,
"s": 23451045058292828843243765241045958975073226494910356096978666517928790374894n,
"yParity": 1,
}
`);
});
});
58 changes: 58 additions & 0 deletions packages/thirdweb/src/transaction/actions/eip7702/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type * as ox__Authorization from "ox/Authorization";
import type { Address } from "../../../utils/address.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";

/**
* An EIP-7702 authorization object fully prepared and ready for signing.
*
* @beta
* @transaction
*/
export type AuthorizationRequest = {
address: Address;
chainId: number;
nonce: bigint;
};

/**
* Represents a signed EIP-7702 authorization object.
*
* @beta
* @transaction
*/
export type SignedAuthorization = ox__Authorization.ListSigned[number];

/**
* Sign the given EIP-7702 authorization object.
* @param options - The options for `signAuthorization`
* Refer to the type [`SignAuthorizationOptions`](https://portal.thirdweb.com/references/typescript/v5/SignAuthorizationOptions)
* @returns The signed authorization object
*
* ```ts
* import { signAuthorization } from "thirdweb";
*
* const authorization = await signAuthorization({
* request: {
* address: "0x...",
* chainId: 911867,
* nonce: 100n,
* },
* account: myAccount,
* });
* ```
*
* @beta
* @transaction
*/
export async function signAuthorization(options: {
account: Account;
request: AuthorizationRequest;
}): Promise<SignedAuthorization> {
const { account, request } = options;
if (typeof account.signAuthorization === "undefined") {
throw new Error(
"This account type does not yet support signing EIP-7702 authorizations",
);
}

Check warning on line 56 in packages/thirdweb/src/transaction/actions/eip7702/authorization.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/transaction/actions/eip7702/authorization.ts#L53-L56

Added lines #L53 - L56 were not covered by tests
return account.signAuthorization(request);
}
35 changes: 26 additions & 9 deletions packages/thirdweb/src/transaction/actions/estimate-gas.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as ox__Hex from "ox/Hex";
import { formatTransactionRequest } from "viem";
import { roundUpGas } from "../../gas/op-gas-fee-reducer.js";
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
Expand All @@ -18,6 +19,8 @@ export type EstimateGasOptions = Prettify<
| {
/**
* The account the transaction would be sent from.
*
* @deprecated Use `from` instead
*/
account: Account;
from?: never;
Expand All @@ -27,7 +30,7 @@ export type EstimateGasOptions = Prettify<
/**
* The address the transaction would be sent from.
*/
from?: string;
from?: string | Account;
}
)
>;
Expand Down Expand Up @@ -60,8 +63,11 @@ export async function estimateGas(
// 1. the user specified from address
// 2. the passed in account address
// 3. the passed in wallet's account address
const from = options.from ?? options.account?.address ?? undefined;
const txWithFrom = { ...options.transaction, from };
const fromAddress =
typeof options.from === "string"
? (options.from ?? undefined)
: (options.from?.address ?? options.account?.address);
const txWithFrom = { ...options.transaction, from: fromAddress };
if (cache.has(txWithFrom)) {
// biome-ignore lint/style/noNonNullAssertion: the `has` above ensures that this will always be set
return cache.get(txWithFrom)!;
Expand Down Expand Up @@ -92,11 +98,13 @@ export async function estimateGas(

// load up encode function if we need it
const { encode } = await import("./encode.js");
const [encodedData, toAddress, value] = await Promise.all([
encode(options.transaction),
resolvePromisedValue(options.transaction.to),
resolvePromisedValue(options.transaction.value),
]);
const [encodedData, toAddress, value, authorizationList] =
await Promise.all([
encode(options.transaction),
resolvePromisedValue(options.transaction.to),
resolvePromisedValue(options.transaction.value),
resolvePromisedValue(options.transaction.authorizationList),
]);

// load up the rpc client and the estimateGas function if we need it
const [{ getRpcClient }, { eth_estimateGas }] = await Promise.all([
Expand All @@ -111,10 +119,19 @@ export async function estimateGas(
formatTransactionRequest({
to: toAddress,
data: encodedData,
from,
from: fromAddress,
value,
// TODO: Remove this casting when we migrate this file to Ox
authorizationList: authorizationList?.map((auth) => ({
...auth,
r: ox__Hex.fromNumber(auth.r),
s: ox__Hex.fromNumber(auth.s),
nonce: Number(auth.nonce),
contractAddress: auth.address,
})),
}),
);

if (options.transaction.chain.experimental?.increaseZeroByteCount) {
gas = roundUpGas(gas);
}
Expand Down
34 changes: 32 additions & 2 deletions packages/thirdweb/src/transaction/actions/send-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,37 @@ export interface SendTransactionOptions {
* });
* ```
*
* ### Send an EIP-7702 Transaction
*
* **Note: This feature is in beta and is subject to breaking changes**
*
* ```ts
* import { sendTransaction, prepareTransaction, signAuthorization } from "thirdweb";
* import { sepolia } from "thirdweb/chains";
*
* const authorization = await signAuthorization({
* request: {
* address: "0x...",
* chainId: 1,
* nonce: 0n,
* },
* account: myAccount,
* });
*
* const transaction = prepareTransaction({
* chain: sepolia,
* client: client,
* to: "0x...",
* value: 0n,
* authorizationList: [authorization],
* });
*
* const { transactionHash } = await sendTransaction({
* account,
* transaction,
* });
* ```
*
* ### Gasless usage with [thirdweb Engine](https://portal.thirdweb.com/engine)
* ```ts
* const { transactionHash } = await sendTransaction({
Expand Down Expand Up @@ -166,9 +197,8 @@ export async function sendTransaction(

const serializableTransaction = await toSerializableTransaction({
transaction: transaction,
from: account.address,
from: account,
});

// branch for gasless transactions
if (gasless) {
// lazy load the gasless tx function because it's only needed for gasless transactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { beforeAll, describe, expect, test } from "vitest";
import { TEST_WALLET_B } from "../../../test/src/addresses.js";
import { FORKED_ETHEREUM_CHAIN } from "../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../../test/src/test-wallets.js";
import { arbitrumSepolia } from "../../chains/chain-definitions/arbitrum-sepolia.js";
import { toWei } from "../../utils/units.js";
import {
type PreparedTransaction,
prepareTransaction,
} from "../prepare-transaction.js";
import { serializeTransaction } from "../serialize-transaction.js";
import { signAuthorization } from "./eip7702/authorization.js";
import { toSerializableTransaction } from "./to-serializable-transaction.js";

describe.runIf(process.env.TW_SECRET_KEY)("toSerializableTransaction", () => {
Expand Down Expand Up @@ -272,6 +274,72 @@ describe.runIf(process.env.TW_SECRET_KEY)("toSerializableTransaction", () => {
});
});

describe("authorizations", () => {
test("should be able to be set", async () => {
const authorization = await signAuthorization({
account: TEST_ACCOUNT_A,
request: {
address: TEST_WALLET_B,
chainId: 1,
nonce: 100n,
},
});

const serializableTransaction = await toSerializableTransaction({
transaction: {
...transaction,
authorizationList: [authorization],
},
from: TEST_ACCOUNT_A,
});

expect(serializableTransaction.authorizationList).toMatchInlineSnapshot(`
[
{
"address": "0x0000000000000000000000000000000000000002",
"chainId": 1,
"nonce": 100n,
"r": 80806665504145908662094143605220407474886149466352261863122583017203514896219n,
"s": 35406481756212480507222011619049260135807579374282360733409834151386668114999n,
"yParity": 1,
},
]
`);
});

test("should be able to be a promised value", async () => {
const authorization = await signAuthorization({
account: TEST_ACCOUNT_A,
request: {
address: TEST_WALLET_B,
chainId: 1,
nonce: 100n,
},
});

const serializableTransaction = await toSerializableTransaction({
transaction: {
...transaction,
authorizationList: async () => Promise.resolve([authorization]),
},
from: TEST_ACCOUNT_A,
});

expect(serializableTransaction.authorizationList).toMatchInlineSnapshot(`
[
{
"address": "0x0000000000000000000000000000000000000002",
"chainId": 1,
"nonce": 100n,
"r": 80806665504145908662094143605220407474886149466352261863122583017203514896219n,
"s": 35406481756212480507222011619049260135807579374282360733409834151386668114999n,
"yParity": 1,
},
]
`);
});
});

describe("extraGas override", () => {
let gas: bigint;
beforeAll(() => {
Expand Down
Loading

0 comments on commit 429e112

Please sign in to comment.