diff --git a/.changeset/tiny-badgers-fry.md b/.changeset/tiny-badgers-fry.md new file mode 100644 index 0000000000..f8c32f9402 --- /dev/null +++ b/.changeset/tiny-badgers-fry.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Experimental:** Added experimental ERC-7821 actions. diff --git a/contracts/lib/solady b/contracts/lib/solady index 86b922d52c..6c2d0da639 160000 --- a/contracts/lib/solady +++ b/contracts/lib/solady @@ -1 +1 @@ -Subproject commit 86b922d52cb3e90d8e40c5c16540d7f452456743 +Subproject commit 6c2d0da6397e3c016aabc3f298de1b92c6ce7405 diff --git a/contracts/src/ERC7821Example.sol b/contracts/src/ERC7821Example.sol new file mode 100644 index 0000000000..2ec791635b --- /dev/null +++ b/contracts/src/ERC7821Example.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.17; + +// SPDX-License-Identifier: UNLICENSED + +import "solady/accounts/ERC7821.sol"; + +contract ERC7821Example is ERC7821 { + event OpData(bytes opData); + + function _execute( + bytes32 mode, + bytes calldata executionData, + Call[] calldata calls, + bytes calldata opData + ) internal virtual override { + mode = mode; + executionData = executionData; + + require(msg.sender == address(this)); + if (opData.length > 0) { + emit OpData(opData); + } + return _execute(calls, bytes32(0)); + } +} diff --git a/package.json b/package.json index bdd7f92732..4994497d3b 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "src": { "entry": [ "index.ts!", - "{account-abstraction,accounts,actions,celo,chains,ens,experimental,experimental/erc7739,linea,node,nonce,op-stack,siwe,utils,window,zksync}/index.ts!", + "{account-abstraction,accounts,actions,celo,chains,ens,experimental,experimental/erc7739,experimental/erc7821,linea,node,nonce,op-stack,siwe,utils,window,zksync}/index.ts!", "chains/utils.ts!" ], "ignore": ["node/trustedSetups_cjs.ts"] diff --git a/site/pages/experimental/erc7821/client.md b/site/pages/experimental/erc7821/client.md new file mode 100644 index 0000000000..3959030420 --- /dev/null +++ b/site/pages/experimental/erc7821/client.md @@ -0,0 +1,16 @@ +# Extending Client with ERC-7821 Actions [Setting up your Viem Client] + +To use the experimental functionality of [ERC-7821](https://eips.ethereum.org/EIPS/eip-7821), you can extend your existing (or new) Viem Client with experimental [ERC-7821](https://eips.ethereum.org/EIPS/eip-7821) Actions. + +```ts +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import { erc7821Actions } from 'viem/experimental' // [!code focus] + +const client = createClient({ + chain: mainnet, + transport: http(), +}).extend(erc7821Actions()) // [!code focus] + +const hash = await client.execute({/* ... */}) +``` diff --git a/site/pages/experimental/erc7821/execute.md b/site/pages/experimental/erc7821/execute.md new file mode 100644 index 0000000000..03219562b7 --- /dev/null +++ b/site/pages/experimental/erc7821/execute.md @@ -0,0 +1,487 @@ +--- +description: Executes call(s) using the `execute` function on an ERC-7821-compatible contract. +--- + +# execute + +Executes call(s) using the `execute` function on an [ERC-7821-compatible contract](https://eips.ethereum.org/EIPS/eip-7821). + + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { parseEther } from 'viem' +import { account, client } from './config' + +const hash = await client.execute({ // [!code focus:99] + account, + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import { createClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { erc7821Actions } from 'viem/experimental' + +export const account = privateKeyToAccount('0x...') + +export const client = createClient({ + chain: mainnet, + transport: http(), +}).extend(erc7821Actions()) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `sendCalls`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts twoslash [example.ts] +import { parseEther } from 'viem' +import { account, client } from './config' + +const hash = await client.execute({ // [!code focus:99] + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import { createClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { erc7821Actions } from 'viem/experimental' + +export const account = privateKeyToAccount('0x...') + +export const client = createClient({ + account, + chain: mainnet, + transport: http(), +}).extend(erc7821Actions()) +``` + +::: + +### Contract Calls + +The `calls` property also accepts **Contract Calls**, and can be used via the `abi`, `functionName`, and `args` properties. + +:::code-group + +```ts twoslash [example.ts] +import { parseEther } from 'viem' +import { account, client } from './config' + +const abi = parseAbi([ + 'function approve(address, uint256) returns (bool)', + 'function transferFrom(address, address, uint256) returns (bool)', +]) + +const hash = await client.execute({ // [!code focus:99] + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + to: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi, + functionName: 'approve', + args: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + 100n + ], + }, + { + to: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi, + functionName: 'transferFrom', + args: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + '0x0000000000000000000000000000000000000000', + 100n + ], + }, + ], +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import { createClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { erc7821Actions } from 'viem/experimental' + +export const account = privateKeyToAccount('0x...') + +export const client = createClient({ + account, + chain: mainnet, + transport: http(), +}).extend(erc7821Actions()) +``` + +::: + +## Return Value + +[`Hash`](/docs/glossary/types#hash) + +A [Transaction Hash](/docs/glossary/terms#hash). + +## Parameters + +### account + +- **Type:** `Account | Address | null` + +Account to invoke the execution of the calls. + +Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts) or [Local Account (Private Key, etc)](/docs/clients/wallet#local-accounts-private-key-mnemonic-etc). If set to `null`, it is assumed that the transport will handle filling the sender of the transaction. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +### address + +- **Type:** `0x${string}` + +Address of the contract to execute the calls on. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', // [!code focus] + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +### calls + +- **Type:** `Call[]` + +Set of calls to execute. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ // [!code focus] + { // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: parseEther('1') // [!code focus] + }, // [!code focus] + { // [!code focus] + data: '0xdeadbeef', // [!code focus] + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', // [!code focus] + }, // [!code focus] + ], // [!code focus] +}) +``` + +#### calls.data + +- **Type:** `Hex` + +Calldata to broadcast (typically a contract function selector with encoded arguments, or contract deployment bytecode). + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', // [!code focus] + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +#### calls.to + +- **Type:** `Address` + +Recipient address of the call. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', // [!code focus] + }, + ], +}) +``` + +#### calls.value + +- **Type:** `Address` + +Value to send with the call. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') // [!code focus] + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +### authorizationList (optional) + +- **Type:** `AuthorizationList` + +Signed EIP-7702 Authorization list. + +```ts twoslash +// @noErrors +import { createWalletClient, http, parseEther } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet } from 'viem/chains' +import { eip7702Actions, erc7821Actions } from 'viem/experimental' + +const account = privateKeyToAccount('0x...') + +export const client = createWalletClient({ + account, + chain: mainnet, + transport: http(), +}) + .extend(eip7702Actions()) + .extend(erc7821Actions()) +// ---cut--- +const authorization = await client.signAuthorization({ + contractAddress: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', +}) + +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + authorizationList: [authorization], // [!code focus] + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], +}) +``` + +:::note +**References** +- [EIP-7702 Overview](/experimental/eip7702) +- [`signAuthorization` Docs](/experimental/eip7702/signAuthorization) +::: + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `client.chain` + +Chain to execute the calls on. + +```ts twoslash +import { client } from './config' +// ---cut--- +import { optimism } from 'viem/chains' // [!code focus] + +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + chain: optimism, // [!code focus] +}) +``` + +### gasPrice (optional) + +- **Type:** `bigint` + +The price (in wei) to pay per gas. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + gasPrice: parseGwei('20'), // [!code focus] +}) +``` + +### maxFeePerGas (optional) + +- **Type:** `bigint` + +Total fee per gas (in wei), inclusive of `maxPriorityFeePerGas`. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + maxFeePerGas: parseGwei('20'), // [!code focus] +}) +``` + +### maxPriorityFeePerGas (optional) + +- **Type:** `bigint` + +Max priority fee per gas (in wei). + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + maxFeePerGas: parseGwei('20'), + maxPriorityFeePerGas: parseGwei('2'), // [!code focus] +}) +``` + +### opData (optional) + +- **Type:** `Hex` + +Additional data to pass to execution. + +```ts twoslash +import { client } from './config' +// ---cut--- +const hash = await client.execute({ + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + opData: '0xdeadbeef', // [!code focus] +}) +``` diff --git a/site/pages/experimental/erc7821/supportsExecutionMode.md b/site/pages/experimental/erc7821/supportsExecutionMode.md new file mode 100644 index 0000000000..7f1dcc64bd --- /dev/null +++ b/site/pages/experimental/erc7821/supportsExecutionMode.md @@ -0,0 +1,52 @@ +--- +description: Checks if the contract supports the ERC-7821 execution mode. +--- + +# supportsExecutionMode + +Checks if the contract supports the [ERC-7821](https://eips.ethereum.org/EIPS/eip-7821) execution mode. + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { client } from './config' + +const supported = await client.supportsExecutionMode({ // [!code focus:99] + address: '0xcb98643b8786950F0461f3B0edf99D88F274574D', +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import { erc7821Actions } from 'viem/experimental' + +export const client = createClient({ + chain: mainnet, + transport: http(), +}).extend(erc7821Actions()) +``` + +::: + +## Returns + +`boolean` + +If the contract supports the ERC-7821 execution mode. + +## Parameters + +### address + +- **Type:** `Address` + +The address of the contract to check. + +### opData + +- **Type:** `Hex` + +Additional data to pass to execution. diff --git a/site/sidebar.ts b/site/sidebar.ts index a63a17abcc..4955144d93 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1387,6 +1387,28 @@ export const sidebar = { }, ], }, + { + text: 'ERC-7821', + items: [ + { + text: 'Client', + link: '/experimental/erc7821/client', + }, + { + text: 'Actions', + items: [ + { + text: 'execute', + link: '/experimental/erc7821/execute', + }, + { + text: 'supportsExecutionMode', + link: '/experimental/erc7821/supportsExecutionMode', + }, + ], + }, + ], + }, ], }, '/op-stack': { diff --git a/src/account-abstraction/actions/bundler/estimateUserOperationGas.ts b/src/account-abstraction/actions/bundler/estimateUserOperationGas.ts index a8e7110305..b6eb4d5970 100644 --- a/src/account-abstraction/actions/bundler/estimateUserOperationGas.ts +++ b/src/account-abstraction/actions/bundler/estimateUserOperationGas.ts @@ -8,6 +8,7 @@ import type { Transport } from '../../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../../errors/account.js' import type { BaseError } from '../../../errors/base.js' import type { ErrorType } from '../../../errors/utils.js' +import type { Calls } from '../../../types/calls.js' import type { Chain } from '../../../types/chain.js' import type { Hex } from '../../../types/misc.js' import type { StateOverride } from '../../../types/stateOverride.js' @@ -33,7 +34,6 @@ import type { import type { EstimateUserOperationGasReturnType as EstimateUserOperationGasReturnType_, UserOperation, - UserOperationCalls, UserOperationRequest, } from '../../types/userOperation.js' import { getUserOperationError } from '../../utils/errors/getUserOperationError.js' @@ -68,9 +68,7 @@ export type EstimateUserOperationGasParameters< | Assign< // Accept a partially-formed User Operation (UserOperationRequest) to be filled. UserOperationRequest<_derivedVersion>, - OneOf< - { calls: UserOperationCalls> } | { callData: Hex } - > & { + OneOf<{ calls: Calls> } | { callData: Hex }> & { paymaster?: | Address | true diff --git a/src/account-abstraction/actions/bundler/prepareUserOperation.ts b/src/account-abstraction/actions/bundler/prepareUserOperation.ts index f74032da2f..4dfcb2b612 100644 --- a/src/account-abstraction/actions/bundler/prepareUserOperation.ts +++ b/src/account-abstraction/actions/bundler/prepareUserOperation.ts @@ -12,8 +12,8 @@ import type { Client } from '../../../clients/createClient.js' import type { Transport } from '../../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../../errors/account.js' import type { ErrorType } from '../../../errors/utils.js' +import type { Call, Calls } from '../../../types/calls.js' import type { Chain } from '../../../types/chain.js' -import type { ContractFunctionParameters } from '../../../types/contract.js' import type { Hex } from '../../../types/misc.js' import type { StateOverride } from '../../../types/stateOverride.js' import type { @@ -42,8 +42,6 @@ import type { } from '../../types/entryPointVersion.js' import type { UserOperation, - UserOperationCall, - UserOperationCalls, UserOperationRequest, } from '../../types/userOperation.js' import { @@ -154,7 +152,7 @@ export type PrepareUserOperationRequest< EntryPointVersion = DeriveEntryPointVersion<_derivedAccount>, > = Assign< UserOperationRequest<_derivedVersion>, - OneOf<{ calls: UserOperationCalls> } | { callData: Hex }> & { + OneOf<{ calls: Calls> } | { callData: Hex }> & { parameters?: readonly PrepareUserOperationParameterType[] | undefined paymaster?: | Address @@ -367,16 +365,14 @@ export async function prepareUserOperation< if (parameters.calls) return account.encodeCalls( parameters.calls.map((call_) => { - const call = call_ as - | UserOperationCall - | (ContractFunctionParameters & { to: Address; value: bigint }) - if ('abi' in call) + const call = call_ as Call + if (call.abi) return { data: encodeFunctionData(call), to: call.to, value: call.value, - } as UserOperationCall - return call as UserOperationCall + } as Call + return call as Call }), ) return parameters.callData diff --git a/src/account-abstraction/actions/bundler/sendUserOperation.ts b/src/account-abstraction/actions/bundler/sendUserOperation.ts index 030a535a26..1ea0e79122 100644 --- a/src/account-abstraction/actions/bundler/sendUserOperation.ts +++ b/src/account-abstraction/actions/bundler/sendUserOperation.ts @@ -5,6 +5,7 @@ import type { Transport } from '../../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../../errors/account.js' import type { BaseError } from '../../../errors/base.js' import type { ErrorType } from '../../../errors/utils.js' +import type { Calls } from '../../../types/calls.js' import type { Chain } from '../../../types/chain.js' import type { Hex } from '../../../types/misc.js' import type { Assign, MaybeRequired, OneOf } from '../../../types/utils.js' @@ -22,7 +23,6 @@ import type { } from '../../types/entryPointVersion.js' import type { UserOperation, - UserOperationCalls, UserOperationRequest, } from '../../types/userOperation.js' import { getUserOperationError } from '../../utils/errors/getUserOperationError.js' @@ -53,9 +53,7 @@ export type SendUserOperationParameters< | Assign< // Accept a partially-formed User Operation (UserOperationRequest) to be filled. UserOperationRequest<_derivedVersion>, - OneOf< - { calls: UserOperationCalls> } | { callData: Hex } - > & { + OneOf<{ calls: Calls> } | { callData: Hex }> & { paymaster?: | Address | true diff --git a/src/account-abstraction/index.ts b/src/account-abstraction/index.ts index ef2a6e5eef..c190d9f099 100644 --- a/src/account-abstraction/index.ts +++ b/src/account-abstraction/index.ts @@ -202,8 +202,6 @@ export type { UserOperationReceipt, UserOperationRequest, PackedUserOperation, - UserOperationCall, - UserOperationCalls, } from './types/userOperation.js' export { diff --git a/src/account-abstraction/types/userOperation.ts b/src/account-abstraction/types/userOperation.ts index 6e863b2bd7..7d73dd62bb 100644 --- a/src/account-abstraction/types/userOperation.ts +++ b/src/account-abstraction/types/userOperation.ts @@ -1,10 +1,8 @@ -import type { AbiStateMutability, Address } from 'abitype' -import type { ContractFunctionParameters } from '../../types/contract.js' +import type { Address } from 'abitype' import type { Log } from '../../types/log.js' import type { Hash, Hex } from '../../types/misc.js' -import type { GetMulticallContractParameters } from '../../types/multicall.js' import type { TransactionReceipt } from '../../types/transaction.js' -import type { OneOf, Prettify, UnionPartialBy } from '../../types/utils.js' +import type { OneOf, UnionPartialBy } from '../../types/utils.js' import type { EntryPointVersion } from './entryPointVersion.js' /** @link https://eips.ethereum.org/EIPS/eip-4337#-eth_estimateuseroperationgas */ @@ -192,71 +190,3 @@ export type UserOperationReceipt< /** Hash of the user operation. */ userOpHash: Hash } - -export type UserOperationCall = { - to: Hex - data?: Hex | undefined - value?: bigint | undefined -} - -export type UserOperationCalls< - calls extends readonly unknown[], - /// - result extends readonly any[] = [], -> = calls extends readonly [] // no calls, return empty - ? readonly [] - : calls extends readonly [infer call] // one call left before returning `result` - ? readonly [ - ...result, - Prettify< - OneOf< - | (Omit< - GetMulticallContractParameters, - 'address' - > & { - to: Address - value?: bigint | undefined - }) - | UserOperationCall - > - >, - ] - : calls extends readonly [infer call, ...infer rest] // grab first call and recurse through `rest` - ? UserOperationCalls< - [...rest], - [ - ...result, - Prettify< - OneOf< - | (Omit< - GetMulticallContractParameters, - 'address' - > & { - to: Address - value?: bigint | undefined - }) - | UserOperationCall - > - >, - ] - > - : readonly unknown[] extends calls - ? calls - : // If `calls` is *some* array but we couldn't assign `unknown[]` to it, then it must hold some known/homogenous type! - // use this to infer the param types in the case of Array.map() argument - calls extends readonly (infer call extends OneOf< - | (Omit & { - to: Address - value?: bigint | undefined - }) - | UserOperationCall - >)[] - ? readonly Prettify[] - : // Fallback - readonly OneOf< - | (Omit & { - to: Address - value?: bigint | undefined - }) - | UserOperationCall - >[] diff --git a/src/account-abstraction/utils/errors/getUserOperationError.ts b/src/account-abstraction/utils/errors/getUserOperationError.ts index dbce0e927d..326b524337 100644 --- a/src/account-abstraction/utils/errors/getUserOperationError.ts +++ b/src/account-abstraction/utils/errors/getUserOperationError.ts @@ -1,4 +1,4 @@ -import type { Address } from 'abitype' +import type { Abi, Address } from 'abitype' import { BaseError } from '../../../errors/base.js' import { ContractFunctionExecutionError, @@ -6,9 +6,8 @@ import { ContractFunctionZeroDataError, } from '../../../errors/contract.js' import type { ErrorType } from '../../../errors/utils.js' -import type { ContractFunctionParameters } from '../../../types/contract.js' +import type { Call } from '../../../types/calls.js' import type { Hex } from '../../../types/misc.js' -import type { OneOf } from '../../../types/utils.js' import { decodeErrorResult } from '../../../utils/abi/decodeErrorResult.js' import type { GetContractErrorReturnType } from '../../../utils/errors/getContractError.js' import { ExecutionRevertedError } from '../../errors/bundler.js' @@ -16,22 +15,12 @@ import { UserOperationExecutionError, type UserOperationExecutionErrorType, } from '../../errors/userOperation.js' -import type { - UserOperation, - UserOperationCall, -} from '../../types/userOperation.js' +import type { UserOperation } from '../../types/userOperation.js' import { type GetBundlerErrorParameters, getBundlerError, } from './getBundlerError.js' -type Call = OneOf< - | UserOperationCall - | (ContractFunctionParameters & { - to: Address - }) -> - type GetNodeErrorReturnType = ErrorType export type GetUserOperationErrorParameters = UserOperation & { @@ -106,7 +95,7 @@ function getContractError(parameters: { const { abi, functionName, args, to } = (() => { const contractCalls = calls?.filter((call) => Boolean(call.abi), - ) as readonly (ContractFunctionParameters & { to: Address })[] + ) as readonly Call[] if (contractCalls.length === 1) return contractCalls[0] @@ -133,7 +122,12 @@ function getContractError(parameters: { args: undefined, to: undefined, } - })() + })() as { + abi: Abi + functionName: string + args: unknown[] + to: Address + } const cause = (() => { if (revertData === '0x') diff --git a/src/experimental/erc7821/actions/execute.test.ts b/src/experimental/erc7821/actions/execute.test.ts new file mode 100644 index 0000000000..24f5a324ad --- /dev/null +++ b/src/experimental/erc7821/actions/execute.test.ts @@ -0,0 +1,218 @@ +import { expect, test } from 'vitest' +import { + ERC7821Example, + ErrorsExample, +} from '../../../../contracts/generated.js' +import { wagmiContractConfig } from '../../../../test/src/abis.js' +import { anvilMainnet } from '../../../../test/src/anvil.js' +import { accounts } from '../../../../test/src/constants.js' +import { deploy, deployErrorExample } from '../../../../test/src/utils.js' +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import { + getBalance, + getTransactionReceipt, + mine, + readContract, +} from '../../../actions/index.js' +import { decodeEventLog, parseEther } from '../../../utils/index.js' +import { signAuthorization } from '../../eip7702/actions/signAuthorization.js' +import { execute } from './execute.js' + +const client = anvilMainnet.getClient({ + account: privateKeyToAccount(accounts[1].privateKey), +}) + +test('default', async () => { + const { contractAddress } = await deploy(client, { + abi: ERC7821Example.abi, + bytecode: ERC7821Example.bytecode.object, + }) + + const balances_before = await Promise.all([ + getBalance(client, { address: accounts[1].address }), + getBalance(client, { address: accounts[2].address }), + getBalance(client, { address: accounts[3].address }), + readContract(client, { + abi: wagmiContractConfig.abi, + address: wagmiContractConfig.address, + functionName: 'balanceOf', + args: [accounts[1].address], + }), + ]) + + const authorization = await signAuthorization(client, { + contractAddress: contractAddress!, + }) + await execute(client, { + address: client.account.address, + authorizationList: [authorization], + calls: [ + { + to: accounts[2].address, + value: parseEther('1'), + }, + { + to: accounts[3].address, + value: parseEther('2'), + }, + { + abi: wagmiContractConfig.abi, + functionName: 'mint', + to: wagmiContractConfig.address, + }, + ], + }) + + await mine(client, { blocks: 1 }) + + const balances_after = await Promise.all([ + getBalance(client, { address: accounts[1].address }), + getBalance(client, { address: accounts[2].address }), + getBalance(client, { address: accounts[3].address }), + readContract(client, { + abi: wagmiContractConfig.abi, + address: wagmiContractConfig.address, + functionName: 'balanceOf', + args: [accounts[1].address], + }), + ]) + + expect(balances_after[0]).toBeLessThan(balances_before[0] - parseEther('3')) + expect(balances_after[1]).toBe(balances_before[1] + parseEther('1')) + expect(balances_after[2]).toBe(balances_before[2] + parseEther('2')) + expect(balances_after[3]).toBe(balances_before[3] + 1n) +}) + +test('args: opData', async () => { + const hash = await execute(client, { + calls: [ + { + to: accounts[2].address, + value: parseEther('1'), + }, + { + to: accounts[3].address, + value: parseEther('2'), + }, + ], + opData: '0xdeadbeef', + address: client.account.address, + }) + await mine(client, { blocks: 1 }) + const receipt = await getTransactionReceipt(client, { hash }) + const event = decodeEventLog({ + abi: ERC7821Example.abi, + ...receipt?.logs[0], + }) + expect(event.args.opData).toBe('0xdeadbeef') +}) + +test('behavior: execution not supported', async () => { + await expect(() => + execute(client, { + address: '0x0000000000000000000000000000000000000000', + calls: [ + { + to: accounts[2].address, + value: 0n, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [ExecuteUnsupportedError: ERC-7821 execution is not supported. + + Version: viem@x.y.z] + `) +}) + +test('behavior: insufficient funds', async () => { + await expect(() => + execute(client, { + address: client.account.address, + calls: [ + { + to: accounts[2].address, + value: parseEther('999999'), + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [TransactionExecutionError: Execution reverted for an unknown reason. + + Request Arguments: + from: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + to: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + data: 0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000000000000000000000000d3c20dee1639f99c000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000 + + Details: execution reverted + Version: viem@x.y.z] + `) +}) + +test('behavior: unknown selector', async () => { + await expect(() => + execute(client, { + address: client.account.address, + calls: [ + { + to: accounts[2].address, + value: parseEther('1'), + }, + { + to: accounts[3].address, + value: parseEther('2'), + }, + { + abi: ErrorsExample.abi, + functionName: 'simpleCustomRead', + to: '0x0000000000000000000000000000000000000000', + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [FunctionSelectorNotRecognizedError: Function is not recognized. + + This could be due to any of the following: + - The contract does not have the function, + - The address is not a contract. + + Version: viem@x.y.z] + `) +}) + +test('behavior: revert', async () => { + const { contractAddress: errorExampleAddress } = await deployErrorExample() + + await expect(() => + execute(client, { + address: client.account.address, + calls: [ + { + to: accounts[2].address, + value: parseEther('1'), + }, + { + to: accounts[3].address, + value: parseEther('2'), + }, + { + abi: ErrorsExample.abi, + functionName: 'complexCustomWrite', + to: errorExampleAddress!, + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "complexCustomWrite" reverted. + + Error: ComplexError((address sender, uint256 bar), string message, uint256 number) + ({"sender":"0x0000000000000000000000000000000000000000","bar":"69"}, bugger, 69) + + Contract Call: + address: 0x0000000000000000000000000000000000000000 + function: complexCustomWrite() + + Docs: https://viem.sh/experimental/erc7821/execute + Version: viem@x.y.z] + `) +}) diff --git a/src/experimental/erc7821/actions/execute.ts b/src/experimental/erc7821/actions/execute.ts new file mode 100644 index 0000000000..d240946583 --- /dev/null +++ b/src/experimental/erc7821/actions/execute.ts @@ -0,0 +1,232 @@ +import type { Abi, Address, Narrow } from 'abitype' +import * as AbiError from 'ox/AbiError' +import * as AbiParameters from 'ox/AbiParameters' + +import { + type SendTransactionErrorType, + sendTransaction, +} from '../../../actions/wallet/sendTransaction.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { BaseError } from '../../../errors/base.js' +import type { ErrorType } from '../../../errors/utils.js' +import type { Account, GetAccountParameter } from '../../../types/account.js' +import type { Call, Calls } from '../../../types/calls.js' +import type { + Chain, + DeriveChain, + GetChainParameter, +} from '../../../types/chain.js' +import type { Hex } from '../../../types/misc.js' +import type { UnionEvaluate, UnionPick } from '../../../types/utils.js' +import { + type DecodeErrorResultErrorType, + decodeErrorResult, +} from '../../../utils/abi/decodeErrorResult.js' +import { + type EncodeFunctionDataErrorType, + encodeFunctionData, +} from '../../../utils/abi/encodeFunctionData.js' +import { + type GetContractErrorReturnType, + getContractError, +} from '../../../utils/errors/getContractError.js' +import type { FormattedTransactionRequest } from '../../../utils/formatters/transactionRequest.js' +import { abi, executionMode } from '../constants.js' +import { + ExecuteUnsupportedError, + FunctionSelectorNotRecognizedError, +} from '../errors.js' +import { supportsExecutionMode } from './supportsExecutionMode.js' + +export type ExecuteParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + calls extends readonly unknown[] = readonly unknown[], + _derivedChain extends Chain | undefined = DeriveChain, +> = UnionEvaluate< + UnionPick< + FormattedTransactionRequest<_derivedChain>, + | 'authorizationList' + | 'gas' + | 'gasPrice' + | 'maxFeePerGas' + | 'maxPriorityFeePerGas' + > +> & + GetAccountParameter & + GetChainParameter & { + /** Address that will execute the calls. */ + address: Address + /** Calls to execute. */ + calls: Calls> + /** Additional data to include for execution. */ + opData?: Hex | undefined + } + +export type ExecuteReturnType = Hex + +export type ExecuteErrorType = + | DecodeErrorResultErrorType + | GetContractErrorReturnType + | EncodeFunctionDataErrorType + | SendTransactionErrorType + | ErrorType + +/** + * Executes call(s) using the `execute` function on an [ERC-7821-compatible contract](https://eips.ethereum.org/EIPS/eip-7821). + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { execute } from 'viem/experimental/erc7821' + * + * const account = privateKeyToAccount('0x...') + * + * const client = createClient({ + * chain: mainnet, + * transport: http(), + * }) + * + * const hash = await execute(client, { + * account, + * calls: [{ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * }], + * to: account.address, + * }) + * ``` + * + * @example + * ```ts + * // Account Hoisting + * import { createClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { execute } from 'viem/experimental/erc7821' + * + * const account = privateKeyToAccount('0x...') + * + * const client = createClient({ + * account, + * chain: mainnet, + * transport: http(), + * }) + * + * const hash = await execute(client, { + * calls: [{ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * }], + * to: account.address, + * }) + * ``` + * + * @param client - Client to use. + * @param parameters - {@link ExecuteParameters} + * @returns Transaction hash. {@link ExecuteReturnType} + */ +export async function execute< + const calls extends readonly unknown[], + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined = undefined, +>( + client: Client, + parameters: ExecuteParameters, +): Promise { + const { address, authorizationList, opData } = parameters + + const calls = parameters.calls.map((call_) => { + const call = call_ as Call + return { + data: call.abi ? encodeFunctionData(call) : (call.data ?? '0x'), + value: call.value ?? 0n, + target: call.to, + } + }) + const mode = opData ? executionMode.opData : executionMode.default + + const encodedCalls = AbiParameters.encode( + AbiParameters.from([ + 'struct Call { address target; uint256 value; bytes data; }', + 'Call[] calls', + ...(opData ? ['bytes opData'] : []), + ]), + [calls, ...(opData ? [opData] : [])] as any, + ) + + const supported = await supportsExecutionMode(client, { + address: authorizationList?.[0]?.contractAddress ?? address, + opData, + }) + if (!supported) throw new ExecuteUnsupportedError() + + try { + return await sendTransaction(client, { + ...parameters, + to: address, + data: encodeFunctionData({ + abi, + functionName: 'execute', + args: [mode, encodedCalls], + }), + } as any) + } catch (e) { + const error = (e as BaseError).walk((e) => 'data' in (e as Error)) as + | (BaseError & { data?: Hex | undefined }) + | undefined + + if (!error?.data) throw e + if ( + error.data === + AbiError.getSelector(AbiError.from('error FnSelectorNotRecognized()')) + ) + throw new FunctionSelectorNotRecognizedError() + + const matched = parameters.calls.find((call_) => { + const call = call_ as Call + if (!call.abi) return false + try { + return Boolean( + decodeErrorResult({ + abi: call.abi, + data: error.data!, + }), + ) + } catch { + return false + } + }) as { + abi: Abi + functionName: string + args: unknown[] + to: Address + } | null + if (!matched) throw e + + throw getContractError(error as BaseError, { + abi: matched.abi, + address: matched.to, + args: matched.args, + docsPath: '/experimental/erc7821/execute', + functionName: matched.functionName, + }) + } +} diff --git a/src/experimental/erc7821/actions/supportsExecutionMode.test.ts b/src/experimental/erc7821/actions/supportsExecutionMode.test.ts new file mode 100644 index 0000000000..0062cb190e --- /dev/null +++ b/src/experimental/erc7821/actions/supportsExecutionMode.test.ts @@ -0,0 +1,61 @@ +import { expect, test } from 'vitest' +import { ERC7821Example } from '../../../../contracts/generated.js' +import { anvilMainnet } from '../../../../test/src/anvil.js' +import { accounts } from '../../../../test/src/constants.js' +import { deploy } from '../../../../test/src/utils.js' +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import { mine, sendTransaction } from '../../../actions/index.js' +import { signAuthorization } from '../../eip7702/actions/signAuthorization.js' +import { supportsExecutionMode } from './supportsExecutionMode.js' + +const client = anvilMainnet.getClient({ + account: privateKeyToAccount(accounts[0].privateKey), +}) + +test('default', async () => { + const { contractAddress } = await deploy(client, { + abi: ERC7821Example.abi, + bytecode: ERC7821Example.bytecode.object, + }) + + expect( + await supportsExecutionMode(client, { + address: contractAddress!, + }), + ).toBe(true) + expect( + await supportsExecutionMode(client, { + address: client.account.address, + }), + ).toBe(false) + + const authorization = await signAuthorization(client, { + contractAddress: contractAddress!, + }) + await sendTransaction(client, { + authorizationList: [authorization], + to: client.account.address, + }) + + await mine(client, { blocks: 1 }) + + expect( + await supportsExecutionMode(client, { + address: client.account.address, + }), + ).toBe(true) +}) + +test('args: opData', async () => { + const { contractAddress } = await deploy(client, { + abi: ERC7821Example.abi, + bytecode: ERC7821Example.bytecode.object, + }) + + expect( + await supportsExecutionMode(client, { + address: contractAddress!, + opData: '0xdeadbeef', + }), + ).toBe(true) +}) diff --git a/src/experimental/erc7821/actions/supportsExecutionMode.ts b/src/experimental/erc7821/actions/supportsExecutionMode.ts new file mode 100644 index 0000000000..cf2d9a8a80 --- /dev/null +++ b/src/experimental/erc7821/actions/supportsExecutionMode.ts @@ -0,0 +1,60 @@ +import type { Address } from '../../../accounts/index.js' +import { readContract } from '../../../actions/public/readContract.js' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { ErrorType } from '../../../errors/utils.js' +import type { Chain } from '../../../types/chain.js' +import type { Hex } from '../../../types/misc.js' +import { abi, executionMode } from '../constants.js' + +export type SupportsExecutionModeParameters = { + address: Address + opData?: Hex | undefined +} + +export type SupportsExecutionModeReturnType = boolean + +export type SupportsExecutionModeErrorType = ErrorType + +/** + * Checks if the contract supports the ERC-7821 execution mode. + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { supportsExecutionMode } from 'viem/experimental/erc7821' + * + * const client = createClient({ + * chain: mainnet, + * transport: http(), + * }) + * + * const supported = await supportsExecutionMode(client, { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }) + * ``` + * + * @param client - Client to use. + * @param parameters - {@link SupportsExecutionModeParameters} + * @returns If the execution mode is supported. {@link SupportsExecutionModeReturnType} + */ +export async function supportsExecutionMode< + chain extends Chain | undefined = Chain | undefined, +>( + client: Client, + parameters: SupportsExecutionModeParameters, +): Promise { + const { address, opData } = parameters + const mode = opData ? executionMode.opData : executionMode.default + try { + return await readContract(client, { + abi, + address, + functionName: 'supportsExecutionMode', + args: [mode], + }) + } catch { + return false + } +} diff --git a/src/experimental/erc7821/constants.ts b/src/experimental/erc7821/constants.ts new file mode 100644 index 0000000000..a9421aa8b4 --- /dev/null +++ b/src/experimental/erc7821/constants.ts @@ -0,0 +1,62 @@ +export const abi = [ + { + type: 'fallback', + stateMutability: 'payable', + }, + { + type: 'receive', + stateMutability: 'payable', + }, + { + type: 'function', + name: 'execute', + inputs: [ + { + name: 'mode', + type: 'bytes32', + internalType: 'bytes32', + }, + { + name: 'executionData', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'supportsExecutionMode', + inputs: [ + { + name: 'mode', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + outputs: [ + { + name: 'result', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'view', + }, + { + type: 'error', + name: 'FnSelectorNotRecognized', + inputs: [], + }, + { + type: 'error', + name: 'UnsupportedExecutionMode', + inputs: [], + }, +] as const + +export const executionMode = { + default: '0x0100000000000000000000000000000000000000000000000000000000000000', + opData: '0x0100000000007821000100000000000000000000000000000000000000000000', +} as const diff --git a/src/experimental/erc7821/decorators/erc7821.test.ts b/src/experimental/erc7821/decorators/erc7821.test.ts new file mode 100644 index 0000000000..42c7077227 --- /dev/null +++ b/src/experimental/erc7821/decorators/erc7821.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from 'vitest' + +import { ERC7821Example } from '../../../../contracts/generated.js' +import { anvilMainnet } from '../../../../test/src/anvil.js' +import { accounts } from '../../../../test/src/constants.js' +import { deploy } from '../../../../test/src/utils.js' +import { privateKeyToAccount } from '../../../accounts/privateKeyToAccount.js' +import { signAuthorization } from '../../eip7702/actions/signAuthorization.js' +import { erc7821Actions } from './erc7821.js' + +const client = anvilMainnet + .getClient({ account: privateKeyToAccount(accounts[0].privateKey) }) + .extend(erc7821Actions()) + +test('default', async () => { + expect(erc7821Actions()(client)).toMatchInlineSnapshot(` + { + "execute": [Function], + "supportsExecutionMode": [Function], + } + `) +}) + +describe('smoke test', () => { + test('execute', async () => { + const { contractAddress } = await deploy(client, { + abi: ERC7821Example.abi, + bytecode: ERC7821Example.bytecode.object, + }) + + const authorization = await signAuthorization(client, { + contractAddress: contractAddress!, + }) + await client.execute({ + authorizationList: [authorization], + address: client.account.address, + calls: [ + { + to: '0x0000000000000000000000000000000000000000', + data: '0x', + value: 0n, + }, + ], + }) + }) + + test('supportsExecutionMode', async () => { + expect( + await client.supportsExecutionMode({ + address: client.account.address, + }), + ).toBe(false) + }) +}) diff --git a/src/experimental/erc7821/decorators/erc7821.ts b/src/experimental/erc7821/decorators/erc7821.ts new file mode 100644 index 0000000000..84d82f64f7 --- /dev/null +++ b/src/experimental/erc7821/decorators/erc7821.ts @@ -0,0 +1,149 @@ +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Account } from '../../../types/account.js' +import type { Chain } from '../../../types/chain.js' +import { + type ExecuteParameters, + type ExecuteReturnType, + execute, +} from '../actions/execute.js' +import { + type SupportsExecutionModeParameters, + type SupportsExecutionModeReturnType, + supportsExecutionMode, +} from '../actions/supportsExecutionMode.js' + +export type Erc7821Actions< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, +> = { + /** + * Executes call(s) using the `execute` function on an [ERC-7821-compatible contract](https://eips.ethereum.org/EIPS/eip-7821). + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { erc7821Actions } from 'viem/experimental' + * + * const account = privateKeyToAccount('0x...') + * + * const client = createClient({ + * chain: mainnet, + * transport: http(), + * }).extend(erc7821Actions()) + * + * const hash = await client.execute({ + * account, + * calls: [{ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * }], + * to: account.address, + * }) + * ``` + * + * @example + * ```ts + * // Account Hoisting + * import { createClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { erc7821Actions } from 'viem/experimental' + * + * const account = privateKeyToAccount('0x...') + * + * const client = createClient({ + * account, + * chain: mainnet, + * transport: http(), + * }).extend(erc7821Actions()) + * + * const hash = await client.execute({ + * calls: [{ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * }], + * to: account.address, + * }) + * ``` + * + * @param client - Client to use. + * @param parameters - {@link ExecuteParameters} + * @returns Transaction hash. {@link ExecuteReturnType} + */ + execute: < + const calls extends readonly unknown[], + chainOverride extends Chain | undefined = undefined, + >( + parameters: ExecuteParameters, + ) => Promise + /** + * Checks if the contract supports the ERC-7821 execution mode. + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { erc7821Actions } from 'viem/experimental' + * + * const client = createClient({ + * chain: mainnet, + * transport: http(), + * }).extend(erc7821Actions()) + * + * const supported = await supportsExecutionMode(client, { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }) + * ``` + * + * @param client - Client to use. + * @param parameters - {@link SupportsExecutionModeParameters} + * @returns If the execution mode is supported. {@link SupportsExecutionModeReturnType} + */ + supportsExecutionMode: ( + parameters: SupportsExecutionModeParameters, + ) => Promise +} + +/** + * A suite of Actions for [ERC-7821](https://eips.ethereum.org/EIPS/eip-7821). + * + * @example + * import { createClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { erc7821Actions } from 'viem/experimental' + * + * const client = createClient({ + * chain: mainnet, + * transport: http(), + * }).extend(erc7821Actions()) + */ +export function erc7821Actions() { + return < + transport extends Transport, + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + >( + client: Client, + ): Erc7821Actions => { + return { + execute: (parameters) => execute(client, parameters), + supportsExecutionMode: (parameters) => + supportsExecutionMode(client, parameters), + } + } +} diff --git a/src/experimental/erc7821/errors.ts b/src/experimental/erc7821/errors.ts new file mode 100644 index 0000000000..6d57065530 --- /dev/null +++ b/src/experimental/erc7821/errors.ts @@ -0,0 +1,29 @@ +import { BaseError } from '../../errors/base.js' + +export type ExecuteUnsupportedErrorType = ExecuteUnsupportedError & { + name: 'ExecuteUnsupportedError' +} +export class ExecuteUnsupportedError extends BaseError { + constructor() { + super('ERC-7821 execution is not supported.', { + name: 'ExecuteUnsupportedError', + }) + } +} + +export type FunctionSelectorNotRecognizedErrorType = + FunctionSelectorNotRecognizedError & { + name: 'FunctionSelectorNotRecognizedError' + } +export class FunctionSelectorNotRecognizedError extends BaseError { + constructor() { + super('Function is not recognized.', { + metaMessages: [ + 'This could be due to any of the following:', + ' - The contract does not have the function,', + ' - The address is not a contract.', + ], + name: 'FunctionSelectorNotRecognizedError', + }) + } +} diff --git a/src/experimental/erc7821/index.ts b/src/experimental/erc7821/index.ts new file mode 100644 index 0000000000..c265ed025a --- /dev/null +++ b/src/experimental/erc7821/index.ts @@ -0,0 +1,23 @@ +/** */ +// biome-ignore lint/performance/noBarrelFile: entrypoint +export { + type ExecuteErrorType, + type ExecuteParameters, + type ExecuteReturnType, + execute, +} from './actions/execute.js' +export { + type SupportsExecutionModeErrorType, + type SupportsExecutionModeParameters, + type SupportsExecutionModeReturnType, + supportsExecutionMode, +} from './actions/supportsExecutionMode.js' + +export { + ExecuteUnsupportedError, + type ExecuteUnsupportedErrorType, + FunctionSelectorNotRecognizedError, + type FunctionSelectorNotRecognizedErrorType, +} from './errors.js' + +export { type Erc7821Actions, erc7821Actions } from './decorators/erc7821.js' diff --git a/src/experimental/index.ts b/src/experimental/index.ts index d8fe90474e..b835bab61f 100644 --- a/src/experimental/index.ts +++ b/src/experimental/index.ts @@ -111,6 +111,11 @@ export { erc7739Actions, } from './erc7739/decorators/erc7739.js' +export { + type Erc7821Actions, + erc7821Actions, +} from './erc7821/decorators/erc7821.js' + export { /** @deprecated This is no longer experimental – use `import type { ParseErc6492SignatureErrorType } from 'viem'` instead. */ type ParseErc6492SignatureErrorType, diff --git a/src/package.json b/src/package.json index 4350500fdd..840ad3905d 100644 --- a/src/package.json +++ b/src/package.json @@ -70,6 +70,11 @@ "import": "./_esm/experimental/erc7739/index.js", "default": "./_cjs/experimental/erc7739/index.js" }, + "./experimental/erc7821": { + "types": "./_types/experimental/erc7821/index.d.ts", + "import": "./_esm/experimental/erc7821/index.js", + "default": "./_cjs/experimental/erc7821/index.js" + }, "./linea": { "types": "./_types/linea/index.d.ts", "import": "./_esm/linea/index.js", diff --git a/src/types/calls.ts b/src/types/calls.ts new file mode 100644 index 0000000000..695cf4c0ca --- /dev/null +++ b/src/types/calls.ts @@ -0,0 +1,38 @@ +import type { AbiStateMutability, Address } from 'abitype' +import type { Hex } from './misc.js' +import type { GetMulticallContractParameters } from './multicall.js' +import type { OneOf, Prettify } from './utils.js' + +export type Call = OneOf< + | { + data?: Hex | undefined + to: Address + value?: bigint | undefined + } + | (Omit< + GetMulticallContractParameters, + 'address' + > & { + to: Address + value?: bigint | undefined + }) +> + +export type Calls< + calls extends readonly unknown[], + /// + result extends readonly any[] = [], +> = calls extends readonly [] // no calls, return empty + ? readonly [] + : calls extends readonly [infer call] // one call left before returning `result` + ? readonly [...result, Prettify>] + : calls extends readonly [infer call, ...infer rest] // grab first call and recurse through `rest` + ? Calls<[...rest], [...result, Prettify>]> + : readonly unknown[] extends calls + ? calls + : // If `calls` is *some* array but we couldn't assign `unknown[]` to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + calls extends readonly (infer call extends OneOf)[] + ? readonly Prettify[] + : // Fallback + readonly OneOf[] diff --git a/src/zksync/decorators/publicL2.test.ts b/src/zksync/decorators/publicL2.test.ts index 9388764ae3..66284c592a 100644 --- a/src/zksync/decorators/publicL2.test.ts +++ b/src/zksync/decorators/publicL2.test.ts @@ -211,4 +211,4 @@ test('getLogProof', async () => { }) expect(fee).to.deep.equal(mockProofValues) -}) +}) \ No newline at end of file