diff --git a/.changeset/new-emus-wonder.md b/.changeset/new-emus-wonder.md new file mode 100644 index 0000000000..2215da2365 --- /dev/null +++ b/.changeset/new-emus-wonder.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added getDelegation utility for EIP7702 diff --git a/site/pages/docs/eip7702/getDelegation.md b/site/pages/docs/eip7702/getDelegation.md new file mode 100644 index 0000000000..c34bc8d4ee --- /dev/null +++ b/site/pages/docs/eip7702/getDelegation.md @@ -0,0 +1,104 @@ +--- +description: Returns the address an account has delegated to via EIP-7702. +--- + +# getDelegation + +Returns the address that an account has delegated to via [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). + +## Usage + +:::code-group + +```ts [example.ts] +import { publicClient } from './client' + +const delegation = await publicClient.getDelegation({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', +}) +// '0x1234...5678' or undefined +``` + +```ts [client.ts] +import { createPublicClient, http } from 'viem' +import { mainnet } from 'viem/chains' + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: http() +}) +``` + +::: + +## Return Value + +[`Address`](/docs/glossary/types#address) | `undefined` + +The address the account has delegated to, or `undefined` if the account is not delegated. + +## Parameters + +### address + +- **Type:** [`Address`](/docs/glossary/types#address) + +The address to check for delegation. + +```ts +const delegation = await publicClient.getDelegation({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', // [!code focus] +}) +``` + +### blockNumber (optional) + +- **Type:** `bigint` + +The block number to check the delegation at. + +```ts +const delegation = await publicClient.getDelegation({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockNumber: 15121123n, // [!code focus] +}) +``` + +### blockTag (optional) + +- **Type:** `'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'` +- **Default:** `'latest'` + +The block tag to check the delegation at. + +```ts +const delegation = await publicClient.getDelegation({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockTag: 'safe', // [!code focus] +}) +``` + +## How It Works + +This action retrieves the bytecode at the given address using [`eth_getCode`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getcode) and checks if it matches the EIP-7702 delegation designator format: + +- **Delegation designator prefix:** `0xef0100` +- **Total length:** 23 bytes (3 byte prefix + 20 byte address) + +If the bytecode matches this format, the delegated address is extracted and returned. + +## Example: Check if Account is Delegated + +```ts +import { publicClient } from './client' + +const delegation = await publicClient.getDelegation({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', +}) + +if (delegation) { + console.log(`Account is delegated to: ${delegation}`) +} else { + console.log('Account is not delegated') +} +``` diff --git a/site/sidebar.ts b/site/sidebar.ts index b8d1b901b3..d4547561f2 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -875,6 +875,10 @@ export const sidebar = { text: 'verifyAuthorization', link: '/docs/eip7702/verifyAuthorization', }, + { + text: 'getDelegation', + link: '/docs/eip7702/getDelegation', + }, ], }, ], diff --git a/src/actions/index.ts b/src/actions/index.ts index 7cbf552b88..84a1e1d0fc 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -147,6 +147,12 @@ export { type GetContractEventsReturnType, getContractEvents, } from './public/getContractEvents.js' +export { + type GetDelegationErrorType, + type GetDelegationParameters, + type GetDelegationReturnType, + getDelegation, +} from './public/getDelegation.js' export { type GetEip712DomainErrorType, type GetEip712DomainParameters, diff --git a/src/actions/public/getDelegation.test.ts b/src/actions/public/getDelegation.test.ts new file mode 100644 index 0000000000..29aa9b0654 --- /dev/null +++ b/src/actions/public/getDelegation.test.ts @@ -0,0 +1,116 @@ +import { beforeAll, describe, expect, test } from 'vitest' + +import { EoaOptional } from '~contracts/generated.js' +import { anvilMainnet } from '~test/anvil.js' +import { accounts } from '~test/constants.js' +import { deploy } from '~test/utils.js' +import { generatePrivateKey } from '../../accounts/generatePrivateKey.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { mine } from '../test/mine.js' +import { sendTransaction } from '../wallet/sendTransaction.js' +import { signAuthorization } from '../wallet/signAuthorization.js' +import { getBlockNumber } from './getBlockNumber.js' +import { getDelegation } from './getDelegation.js' + +const client = anvilMainnet.getClient({ account: true }) +const localAccount = privateKeyToAccount(accounts[0].privateKey) + +describe('getDelegation', () => { + let delegation: `0x${string}` + + beforeAll(async () => { + const { contractAddress } = await deploy(client, { + abi: EoaOptional.abi, + bytecode: EoaOptional.bytecode.object, + }) + delegation = contractAddress! + }) + + test('returns undefined for non-delegated EOA', async () => { + const result = await getDelegation(client, { + address: localAccount.address, + }) + expect(result).toBeUndefined() + }) + + test('returns undefined for contract address', async () => { + const result = await getDelegation(client, { + address: delegation, + }) + expect(result).toBeUndefined() + }) + + test('returns delegated address for EIP-7702 delegated account', async () => { + const account = privateKeyToAccount(generatePrivateKey()) + + // Create delegation + const authorization = await signAuthorization(client, { + account, + address: delegation, + }) + + await sendTransaction(client, { + authorizationList: [authorization], + gas: 1_000_000n, + }) + await mine(client, { blocks: 1 }) + + const result = await getDelegation(client, { + address: account.address, + }) + expect(result).toBe(delegation) + }) + + test('with blockNumber', async () => { + const account = privateKeyToAccount(generatePrivateKey()) + + const blockNumberBefore = await getBlockNumber(client) + + // Create delegation + const authorization = await signAuthorization(client, { + account, + address: delegation, + }) + + await sendTransaction(client, { + authorizationList: [authorization], + gas: 1_000_000n, + }) + await mine(client, { blocks: 1 }) + + // Should be undefined at the block before delegation + const resultBefore = await getDelegation(client, { + address: account.address, + blockNumber: blockNumberBefore, + }) + expect(resultBefore).toBeUndefined() + + // Should have delegation at current block + const resultAfter = await getDelegation(client, { + address: account.address, + }) + expect(resultAfter).toBe(delegation) + }) + + test('with blockTag', async () => { + const account = privateKeyToAccount(generatePrivateKey()) + + // Create delegation + const authorization = await signAuthorization(client, { + account, + address: delegation, + }) + + await sendTransaction(client, { + authorizationList: [authorization], + gas: 1_000_000n, + }) + await mine(client, { blocks: 1 }) + + const result = await getDelegation(client, { + address: account.address, + blockTag: 'latest', + }) + expect(result).toBe(delegation) + }) +}) diff --git a/src/actions/public/getDelegation.ts b/src/actions/public/getDelegation.ts new file mode 100644 index 0000000000..fde945b84f --- /dev/null +++ b/src/actions/public/getDelegation.ts @@ -0,0 +1,75 @@ +import type { Address } from 'abitype' + +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { ErrorType } from '../../errors/utils.js' +import type { BlockTag } from '../../types/block.js' +import type { Chain } from '../../types/chain.js' +import { type SizeErrorType, size } from '../../utils/data/size.js' +import { type SliceErrorType, slice } from '../../utils/data/slice.js' +import { type GetCodeErrorType, getCode } from './getCode.js' + +export type GetDelegationParameters = { + /** The address to check for delegation. */ + address: Address +} & ( + | { + blockNumber?: undefined + blockTag?: BlockTag | undefined + } + | { + blockNumber?: bigint | undefined + blockTag?: undefined + } +) + +export type GetDelegationReturnType = Address | undefined + +export type GetDelegationErrorType = + | GetCodeErrorType + | SliceErrorType + | SizeErrorType + | ErrorType + +/** + * Returns the address that an account has delegated to via EIP-7702. + * + * - Docs: https://viem.sh/docs/actions/public/getDelegation + * + * @param client - Client to use + * @param parameters - {@link GetDelegationParameters} + * @returns The delegated address, or undefined if not delegated. {@link GetDelegationReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { getDelegation } from 'viem/actions' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * const delegation = await getDelegation(client, { + * address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * }) + */ +export async function getDelegation( + client: Client, + { address, blockNumber, blockTag = 'latest' }: GetDelegationParameters, +): Promise { + const code = await getCode(client, { + address, + ...(blockNumber !== undefined ? { blockNumber } : { blockTag }), + } as GetDelegationParameters) + + if (!code) return undefined + + // EIP-7702 delegation designator: 0xef0100 prefix (3 bytes) + address (20 bytes) = 23 bytes + if (size(code) !== 23) return undefined + + // Check for EIP-7702 delegation designator prefix + if (!code.startsWith('0xef0100')) return undefined + + // Extract the delegated address (bytes 3-23) + return slice(code, 3, 23) as Address +} diff --git a/src/clients/decorators/public.ts b/src/clients/decorators/public.ts index 40a69cc286..1d8b57534a 100644 --- a/src/clients/decorators/public.ts +++ b/src/clients/decorators/public.ts @@ -116,6 +116,11 @@ import { type GetContractEventsReturnType, getContractEvents, } from '../../actions/public/getContractEvents.js' +import { + type GetDelegationParameters, + type GetDelegationReturnType, + getDelegation, +} from '../../actions/public/getDelegation.js' import { type GetEip712DomainParameters, type GetEip712DomainReturnType, @@ -758,6 +763,29 @@ export type PublicActions< * }) */ getCode: (args: GetCodeParameters) => Promise + /** + * Returns the address that an account has delegated to via EIP-7702. + * + * - Docs: https://viem.sh/docs/actions/public/getDelegation + * + * @param args - {@link GetDelegationParameters} + * @returns The delegated address, or undefined if not delegated. {@link GetDelegationReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * const delegation = await client.getDelegation({ + * address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * }) + */ + getDelegation: ( + args: GetDelegationParameters, + ) => Promise /** * Returns a list of event logs emitted by a contract. * @@ -2062,6 +2090,7 @@ export function publicActions< getBytecode: (args) => getCode(client, args), getChainId: () => getChainId(client), getCode: (args) => getCode(client, args), + getDelegation: (args) => getDelegation(client, args), getContractEvents: (args) => getContractEvents(client, args), getEip712Domain: (args) => getEip712Domain(client, args), getEnsAddress: (args) => getEnsAddress(client, args),