diff --git a/.changeset/swift-lobsters-tell.md b/.changeset/swift-lobsters-tell.md new file mode 100644 index 0000000000..e689205564 --- /dev/null +++ b/.changeset/swift-lobsters-tell.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +**Experimental:** Added `addSubAccount` Action as per [ERC-7895](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md). diff --git a/package.json b/package.json index 4d6c65b325..e5b5f09e3b 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,experimental/erc7821,experimental/erc7846,linea,node,nonce,op-stack,siwe,utils,window,zksync}/index.ts!", + "{account-abstraction,accounts,actions,celo,chains,ens,experimental,experimental/erc7739,experimental/erc7821,experimental/erc7846,experimental/erc7895,linea,node,nonce,op-stack,siwe,utils,window,zksync}/index.ts!", "chains/utils.ts!" ], "ignore": ["node/trustedSetups_cjs.ts"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2e2e70105..bb203d9727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6240,6 +6240,14 @@ packages: typescript: optional: true + viem@2.28.3: + resolution: {integrity: sha512-kGYmSHNmzXqg7uZlaV6OEL1p68Z45BYTRPsUM0jYLmOn2zWy6DRA+YxntKnt6jBiCPjiWbVrbFm5QS6TWnfAXQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + viem@file:src: resolution: {directory: src, type: directory} peerDependencies: diff --git a/site/pages/experimental/erc7846/connect.md b/site/pages/experimental/erc7846/connect.md index 0548446828..cae9097607 100644 --- a/site/pages/experimental/erc7846/connect.md +++ b/site/pages/experimental/erc7846/connect.md @@ -1,10 +1,10 @@ --- -description: Requests to connect account(s). +description: Requests to connect Account(s). --- # connect -Requests to connect account(s) with optional [capabilities](#capabilities). +Requests to connect Account(s) with optional [capabilities](#capabilities). ## Usage @@ -69,7 +69,7 @@ const { accounts } = await walletClient.connect({ ### `unstable_addSubAccount` -Adds a sub-account to the connected account. [See more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) +Adds a Sub Account to the connected Account. [See more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) ```ts twoslash import { walletClient } from './config' @@ -99,7 +99,7 @@ const { accounts } = await walletClient.connect({ ### `unstable_getSubAccounts` -Returns all sub-accounts of the connected account. [See more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) +Returns all Sub Accounts of the connected Account. [See more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) ```ts twoslash import { walletClient } from './config' @@ -149,4 +149,4 @@ const { accounts } = await walletClient.connect({ ## JSON-RPC Methods - [`wallet_connect`](https://github.com/ethereum/ERCs/blob/abd1c9f4eda2d6ad06ade0e3af314637a27d1ee7/ERCS/erc-7846.md) -- Falls back to [`eth_requestAccounts`](https://eips.ethereum.org/EIPS/eip-1102) \ No newline at end of file +- Falls back to [`eth_requestAccounts`](https://eips.ethereum.org/EIPS/eip-1102) diff --git a/site/pages/experimental/erc7895/addSubAccount.md b/site/pages/experimental/erc7895/addSubAccount.md new file mode 100644 index 0000000000..2653adcc33 --- /dev/null +++ b/site/pages/experimental/erc7895/addSubAccount.md @@ -0,0 +1,211 @@ +--- +description: Requests to add a Sub Account. +--- + +# addSubAccount + +Requests to add a Sub Account. [See more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) + +[What is a Sub Account?](https://blog.base.dev/subaccounts) + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + keys: [{ + key: '0xefd5fb29a274ea6682673d8b3caa9263e936d48d', + type: 'address' + }], + type: 'create', +}) +``` + +```ts twoslash [config.ts] filename="config.ts" +import 'viem/window' +// ---cut--- +import { createWalletClient, custom } from 'viem' +import { mainnet } from 'viem/chains' +import { erc7895Actions } from 'viem/experimental' + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum!), +}).extend(erc7895Actions()) +``` + +::: + +## Returns + +The created Sub Account. + +```ts +type ReturnType = { + address: Address + factory?: Address | undefined + factoryData?: Hex | undefined +} +``` + +## Parameters + +### New Accounts + +Allows the wallet to create a Sub Account with a set of known signing keys. [Learn more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md#createaccount) + +#### `keys` + +Set of signing keys that will belong to the Sub Account. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + keys: [{ // [!code focus] + key: '0xefd5fb29a274ea6682673d8b3caa9263e936d48d486e5df68893003e01241522', // [!code focus] + type: 'p256' // [!code focus] + }], // [!code focus] + type: 'create', +}) +``` + +#### `keys.key` + +- **Type:** `Hex` + +The public key of the signing key. + +- This is a 32-byte hexadecimal string. +- For `type: "address"`, this is a 20-byte address. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + keys: [{ + key: '0xefd5fb29a274ea6682673d8b3caa9263e936d48d486e5df68893003e01241522', // [!code focus] + type: 'p256' + }], + type: 'create', +}) +``` + +#### `keys.type` + +- **Type:** `'address' | 'p256' | 'webcrypto-p256' | 'webauthn-p256'` + +The type of signing key. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + keys: [{ + key: '0xefd5fb29a274ea6682673d8b3caa9263e936d48d486e5df68893003e01241522', + type: 'p256' // [!code focus] + }], + type: 'create', +}) +``` + + +### Deployed Accounts + +An existing account that the user wants to link to their global account. [Learn more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md#deployedaccount) + +#### `address` + +Address of the deployed account. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', // [!code focus] + type: 'deployed', +}) +``` + +#### `chainId` + +The chain ID of the deployed account. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', + chainId: 1, // [!code focus] + type: 'deployed', +}) +``` + +### Undeployed Accounts + +An account that has been created, but is not yet deployed. The wallet will decide whether or not to deploy it. [Learn more](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md#undeployedaccount) + +#### `address` + +Address of the undeployed account. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', // [!code focus] + factory: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', + factoryData: '0xdeadbeef', + type: 'undeployed', +}) +``` + +#### `chainId` + +The chain ID the account will be deployed on. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', + chainId: 1, // [!code focus] + factory: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', + factoryData: '0xdeadbeef', + type: 'undeployed', +}) +``` + +#### `factory` + +The address of the factory contract. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', + factory: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', // [!code focus] + factoryData: '0xdeadbeef', + type: 'undeployed', +}) +``` + +#### `factoryData` + +The data to be passed to the factory contract. + +```ts twoslash +import { walletClient } from './config' + +const subAccount = await walletClient.addSubAccount({ + address: '0x0000000000000000000000000000000000000000', + factory: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', + factoryData: '0xdeadbeef', // [!code focus] + type: 'undeployed', +}) +``` + diff --git a/site/pages/experimental/erc7895/client.md b/site/pages/experimental/erc7895/client.md new file mode 100644 index 0000000000..2e3fc17628 --- /dev/null +++ b/site/pages/experimental/erc7895/client.md @@ -0,0 +1,19 @@ +# Extending Client with ERC-7895 Actions [Setting up your Viem Client] + +To use the experimental functionality of [ERC-7895](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md), you can extend your existing (or new) Viem Client with experimental [ERC-7895](https://github.com/ethereum/ERCs/blob/4d3d641ee3c84750baf461b8dd71d27c424417a9/ERCS/erc-7895.md) Actions. + +```ts +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import { erc7895Actions } from 'viem/experimental' // [!code focus] + +const client = createClient({ + chain: mainnet, + transport: http(), +}).extend(erc7895Actions()) // [!code focus] + +const subAccount = await client.addSubAccount({ + keys: [{ key: '0x0000000000000000000000000000000000000000', type: 'address' }], + type: 'create', +}) +``` diff --git a/site/sidebar.ts b/site/sidebar.ts index 96b6a731a2..cc0a26a92f 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1462,6 +1462,24 @@ export const sidebar = { }, ], }, + { + text: 'ERC-7895', + items: [ + { + text: 'Client', + link: '/experimental/erc7895/client', + }, + { + text: 'Actions', + items: [ + { + text: 'addSubAccount', + link: '/experimental/erc7895/addSubAccount', + }, + ], + }, + ], + }, ], }, '/op-stack': { diff --git a/src/experimental/erc7895/actions/addSubAccount.test.ts b/src/experimental/erc7895/actions/addSubAccount.test.ts new file mode 100644 index 0000000000..6edae2c933 --- /dev/null +++ b/src/experimental/erc7895/actions/addSubAccount.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from 'vitest' +import { anvilMainnet } from '../../../../test/src/anvil.js' +import { addSubAccount } from './addSubAccount.js' + +const client = anvilMainnet.getClient() + +test('default', async () => { + { + const response = await addSubAccount(client, { + keys: [ + { key: '0x0000000000000000000000000000000000000000', type: 'address' }, + ], + type: 'create', + }) + + expect(response).toMatchInlineSnapshot(` + { + "address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + } + `) + } + + { + const response = await addSubAccount(client, { + address: '0x0000000000000000000000000000000000000000', + chainId: 1, + type: 'deployed', + }) + + expect(response).toMatchInlineSnapshot(` + { + "address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + } + `) + } +}) diff --git a/src/experimental/erc7895/actions/addSubAccount.ts b/src/experimental/erc7895/actions/addSubAccount.ts new file mode 100644 index 0000000000..d979d5c354 --- /dev/null +++ b/src/experimental/erc7895/actions/addSubAccount.ts @@ -0,0 +1,84 @@ +import type { Address } from 'abitype' +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Chain } from '../../../types/chain.js' +import type { Hex } from '../../../types/misc.js' +import type { OneOf, Prettify } from '../../../types/utils.js' +import type { RequestErrorType } from '../../../utils/buildRequest.js' +import { numberToHex } from '../../../utils/index.js' + +export type AddSubAccountParameters = Prettify< + OneOf< + | { + keys: readonly { + key: Hex + type: 'address' | 'p256' | 'webcrypto-p256' | 'webauthn-p256' + }[] + type: 'create' + } + | { + address: Address + chainId?: number | undefined + type: 'deployed' + } + | { + address: Address + chainId?: number | undefined + factory: Address + factoryData: Hex + type: 'undeployed' + } + > +> + +export type AddSubAccountReturnType = Prettify<{ + address: Address + factory?: Address | undefined + factoryData?: Hex | undefined +}> + +export type AddSubAccountErrorType = RequestErrorType + +/** + * Requests to add a Sub Account. + * + * - Docs: https://viem.sh/experimental/erc7895/addSubAccount + * - JSON-RPC Methods: [`wallet_addSubAccount`](https://github.com/ethereum/ERCs/blob/abd1c9f4eda2d6ad06ade0e3af314637a27d1ee7/ERCS/erc-7895.md) + * + * @param client - Client to use + * @param parameters - {@link AddSubAccountParameters} + * @returns Sub Account. {@link AddSubAccountReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { addSubAccount } from 'viem/experimental/erc7895' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const response = await addSubAccount(client, { + * keys: [{ key: '0x0000000000000000000000000000000000000000', type: 'address' }], + * type: 'create', + * }) + */ +export async function addSubAccount( + client: Client, + parameters: AddSubAccountParameters, +): Promise { + return client.request({ + method: 'wallet_addSubAccount', + params: [ + { + account: { + ...parameters, + ...(parameters.chainId + ? { chainId: numberToHex(parameters.chainId) } + : {}), + } as never, + version: '1', + }, + ], + }) +} diff --git a/src/experimental/erc7895/decorators/erc7895.test.ts b/src/experimental/erc7895/decorators/erc7895.test.ts new file mode 100644 index 0000000000..ccafb30b7f --- /dev/null +++ b/src/experimental/erc7895/decorators/erc7895.test.ts @@ -0,0 +1,36 @@ +// TODO(v3): Remove this. + +import { describe, expect, test } from 'vitest' + +import { anvilMainnet } from '~test/src/anvil.js' +import { erc7895Actions } from './erc7895.js' + +const client = anvilMainnet.getClient().extend(erc7895Actions()) + +test('default', async () => { + expect(erc7895Actions()(client)).toMatchInlineSnapshot(` + { + "addSubAccount": [Function], + } + `) +}) + +describe('smoke test', () => { + test('addSubAccount', async () => { + expect( + await client.addSubAccount({ + keys: [ + { + key: '0x0000000000000000000000000000000000000000', + type: 'address', + }, + ], + type: 'create', + }), + ).toMatchInlineSnapshot(` + { + "address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + } + `) + }) +}) diff --git a/src/experimental/erc7895/decorators/erc7895.ts b/src/experimental/erc7895/decorators/erc7895.ts new file mode 100644 index 0000000000..9e1189a961 --- /dev/null +++ b/src/experimental/erc7895/decorators/erc7895.ts @@ -0,0 +1,70 @@ +import type { Client } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' +import type { Chain } from '../../../types/chain.js' +import { + type AddSubAccountParameters, + type AddSubAccountReturnType, + addSubAccount, +} from '../actions/addSubAccount.js' + +export type Erc7895Actions = { + /** + * Requests to add a Sub Account. + * + * - Docs: https://viem.sh/experimental/erc7895/addSubAccount + * - JSON-RPC Methods: [`wallet_addSubAccount`](https://github.com/ethereum/ERCs/blob/abd1c9f4eda2d6ad06ade0e3af314637a27d1ee7/ERCS/erc-7895.md) + * + * @param client - Client to use + * @param parameters - {@link AddSubAccountParameters} + * @returns Sub Account. {@link AddSubAccountReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { erc7895Actions } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(erc7895Actions()) + * + * const response = await client.addSubAccount({ + * keys: [{ key: '0x0000000000000000000000000000000000000000', type: 'address' }], + * type: 'create', + * }) + */ + addSubAccount: ( + parameters: AddSubAccountParameters, + ) => Promise +} + +/** + * A suite of ERC-7895 Wallet Actions. + * + * @example + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { erc7895Actions } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: http(), + * }).extend(erc7895Actions()) + * + * const response = await client.addSubAccount({ + * keys: [{ key: '0x0000000000000000000000000000000000000000', type: 'address' }], + * type: 'create', + * }) + */ +export function erc7895Actions() { + return < + transport extends Transport, + chain extends Chain | undefined = Chain | undefined, + >( + client: Client, + ): Erc7895Actions => { + return { + addSubAccount: (parameters) => addSubAccount(client, parameters), + } + } +} diff --git a/src/experimental/erc7895/index.ts b/src/experimental/erc7895/index.ts new file mode 100644 index 0000000000..aeb811b1be --- /dev/null +++ b/src/experimental/erc7895/index.ts @@ -0,0 +1,9 @@ +// biome-ignore lint/performance/noBarrelFile: entrypoint +export { + type AddSubAccountErrorType, + type AddSubAccountParameters, + type AddSubAccountReturnType, + addSubAccount, +} from './actions/addSubAccount.js' + +export { type Erc7895Actions, erc7895Actions } from './decorators/erc7895.js' diff --git a/src/experimental/erc7895/package.json b/src/experimental/erc7895/package.json new file mode 100644 index 0000000000..c31a44daa5 --- /dev/null +++ b/src/experimental/erc7895/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "types": "../../_types/experimental/erc7895/index.d.ts", + "module": "../../_esm/experimental/erc7895/index.js", + "main": "../../_cjs/experimental/erc7895/index.js" +} diff --git a/src/experimental/index.ts b/src/experimental/index.ts index c84f2a38fb..b124369dc6 100644 --- a/src/experimental/index.ts +++ b/src/experimental/index.ts @@ -180,3 +180,8 @@ export { type Erc7846Actions, erc7846Actions, } from './erc7846/decorators/erc7846.js' + +export { + type Erc7895Actions, + erc7895Actions, +} from './erc7895/decorators/erc7895.js' diff --git a/src/types/capabilities.ts b/src/types/capabilities.ts index 0515e4f6a1..252bcfa70c 100644 --- a/src/types/capabilities.ts +++ b/src/types/capabilities.ts @@ -1,8 +1,11 @@ -import type { Address } from 'abitype' +import type { + AddSubAccountParameters, + AddSubAccountReturnType, +} from '../experimental/erc7895/actions/addSubAccount.js' import type { SiweMessage } from '../utils/siwe/types.js' import type { Hex } from './misc.js' import type { ResolvedRegister } from './register.js' -import type { OneOf, Prettify, RequiredBy } from './utils.js' +import type { Prettify, RequiredBy } from './utils.js' export type CapabilitiesSchema = ResolvedRegister['CapabilitiesSchema'] @@ -11,31 +14,7 @@ export type DefaultCapabilitiesSchema = { Request: { unstable_addSubAccount?: | { - account: OneOf< - | { - address: Address - chainId?: number | undefined - type: 'deployed' - } - | { - keys: readonly { - key: Hex - type: - | 'address' - | 'p256' - | 'webcrypto-p256' - | 'webauthn-p256' - }[] - type: 'create' - } - | { - address: Address - chainId?: number | undefined - factory: Address - factoryData: Hex - type: 'undeployed' - } - > + account: AddSubAccountParameters } | undefined unstable_getSubAccounts?: boolean | undefined @@ -44,18 +23,8 @@ export type DefaultCapabilitiesSchema = { | undefined } ReturnType: { - unstable_addSubAccount?: - | { - address: Address - } - | undefined - unstable_getSubAccounts?: - | readonly { - address: Address - factory?: Address | undefined - factoryData?: Hex | undefined - }[] - | undefined + unstable_addSubAccount?: AddSubAccountReturnType | undefined + unstable_getSubAccounts?: readonly AddSubAccountReturnType[] | undefined unstable_signInWithEthereum?: | { message: string diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 7080bf6d31..17762567a6 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -1764,6 +1764,43 @@ export type WalletRpcSchema = [ Parameters: [chain: AddEthereumChainParameter] ReturnType: null }, + /** + * + */ + { + Method: 'wallet_addSubAccount' + Parameters: [ + { + account: OneOf< + | { + keys: readonly { + key: Hex + type: 'address' | 'p256' | 'webcrypto-p256' | 'webauthn-p256' + }[] + type: 'create' + } + | { + address: Address + chainId?: number | undefined + type: 'deployed' + } + | { + address: Address + chainId?: number | undefined + factory: Address + factoryData: Hex + type: 'undeployed' + } + > + version: string + }, + ] + ReturnType: { + address: Address + factory?: Address | undefined + factoryData?: Hex | undefined + } + }, /** * @description Requests to connect account(s). * @link https://github.com/ethereum/ERCs/blob/abd1c9f4eda2d6ad06ade0e3af314637a27d1ee7/ERCS/erc-7846.md diff --git a/test/src/anvil.ts b/test/src/anvil.ts index f084f1fe1f..dfca34816b 100644 --- a/test/src/anvil.ts +++ b/test/src/anvil.ts @@ -255,6 +255,10 @@ function defineAnvil( if (method === 'wallet_disconnect') { return null } + if (method === 'wallet_addSubAccount') + return { + address: accounts[1].address, + } as any return request({ method, params }, opts) },