diff --git a/.changeset/clever-eagles-think.md b/.changeset/clever-eagles-think.md new file mode 100644 index 0000000000..4efb2e4e25 --- /dev/null +++ b/.changeset/clever-eagles-think.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added experimental support for [EIP-5792 `wallet_` methods](https://github.com/ethereum/EIPs/blob/1d759f24e6552a516091bb1fe3361d9ca44d085c/EIPS/eip-5792.md). diff --git a/src/experimental/actions/getCallsStatus.test.ts b/src/experimental/actions/getCallsStatus.test.ts new file mode 100644 index 0000000000..684fe0257c --- /dev/null +++ b/src/experimental/actions/getCallsStatus.test.ts @@ -0,0 +1,154 @@ +import { expect, test } from 'vitest' +import { accounts, localHttpUrl } from '../../../test/src/constants.js' +import { testClient } from '../../../test/src/utils.js' +import { mine } from '../../actions/index.js' +import { mainnet } from '../../chains/index.js' +import { createClient } from '../../clients/createClient.js' +import { custom } from '../../clients/transports/custom.js' +import { RpcRequestError } from '../../errors/request.js' +import type { WalletGetCallsStatusReceipt } from '../../types/eip1193.js' +import type { Hex } from '../../types/misc.js' +import { getHttpRpcClient, parseEther } from '../../utils/index.js' +import { uid } from '../../utils/uid.js' +import { getCallsStatus } from './getCallsStatus.js' +import { sendCalls } from './sendCalls.js' + +type Uid = string +type TxHashes = Hex[] +const calls = new Map() + +const getClient = ({ + onRequest, +}: { onRequest({ method, params }: any): void }) => + createClient({ + transport: custom({ + async request({ method, params }) { + onRequest({ method, params }) + + const rpcClient = getHttpRpcClient(localHttpUrl) + + if (method === 'wallet_getCallsStatus') { + const hashes = calls.get(params) + if (!hashes) return null + const receipts = await Promise.all( + hashes.map(async (hash) => { + const { result, error } = await rpcClient.request({ + body: { + method: 'eth_getTransactionReceipt', + params: [hash], + id: 0, + }, + }) + if (error) + throw new RpcRequestError({ + body: { method, params }, + error, + url: localHttpUrl, + }) + return { + blockHash: result.blockHash, + blockNumber: result.blockNumber, + gasUsed: result.gasUsed, + logs: result.logs, + status: result.status, + transactionHash: result.transactionHash, + } satisfies WalletGetCallsStatusReceipt + }), + ) + return { status: 'CONFIRMED', receipts } + } + + if (method === 'wallet_sendCalls') { + const hashes = [] + for (const call of params.calls) { + const { result, error } = await rpcClient.request({ + body: { + method: 'eth_sendTransaction', + params: [call], + id: 0, + }, + }) + if (error) + throw new RpcRequestError({ + body: { method, params }, + error, + url: localHttpUrl, + }) + hashes.push(result) + } + const uid_ = uid() + calls.set(uid_, hashes) + return uid_ + } + + return null + }, + }), + }) + +test('default', async () => { + const requests: unknown[] = [] + + const client = getClient({ + onRequest({ params }) { + requests.push(params) + }, + }) + + const id = await sendCalls(client, { + account: accounts[0].address, + calls: [ + { + to: accounts[1].address, + value: parseEther('1'), + }, + { + to: accounts[2].address, + }, + { + data: '0xcafebabe', + to: accounts[3].address, + value: parseEther('100'), + }, + ], + chain: mainnet, + }) + + expect(id).toBeDefined() + + await mine(testClient, { blocks: 1 }) + + const { status, receipts } = await getCallsStatus(client, { id }) + expect(status).toMatchInlineSnapshot(`"CONFIRMED"`) + expect(receipts![0].blockHash).toBeDefined() + expect( + receipts?.map((x) => ({ ...x, blockHash: undefined })), + ).toMatchInlineSnapshot(` + [ + { + "blockHash": undefined, + "blockNumber": 16280771n, + "gasUsed": 21000n, + "logs": [], + "status": "0x1", + "transactionHash": "0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3", + }, + { + "blockHash": undefined, + "blockNumber": 16280771n, + "gasUsed": 42000n, + "logs": [], + "status": "0x1", + "transactionHash": "0x5fafca9937b154c21e7ea896c3ca23e5076ab9ca9e466085ae45edffb96c36e7", + }, + { + "blockHash": undefined, + "blockNumber": 16280771n, + "gasUsed": 63064n, + "logs": [], + "status": "0x1", + "transactionHash": "0x84f1d37995973fa977fc45eccf3d1ac0cdf666541a7dc2613e9cd3bc356ddfa4", + }, + ] + `) +}) diff --git a/src/experimental/actions/getCallsStatus.ts b/src/experimental/actions/getCallsStatus.ts new file mode 100644 index 0000000000..226e691a16 --- /dev/null +++ b/src/experimental/actions/getCallsStatus.ts @@ -0,0 +1,60 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { ErrorType } from '../../errors/utils.js' +import type { Account } from '../../types/account.js' +import type { Chain } from '../../types/chain.js' +import type { WalletGetCallsStatusReturnType } from '../../types/eip1193.js' +import type { Prettify } from '../../types/utils.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' +import { hexToBigInt } from '../../utils/encoding/fromHex.js' + +export type GetCallsStatusParameters = { id: string } + +export type GetCallsStatusReturnType = Prettify< + WalletGetCallsStatusReturnType +> + +export type GetCallsStatusErrorType = RequestErrorType | ErrorType + +/** + * Returns the status of a call batch that was sent via `sendCalls`. + * + * - Docs: https://viem.sh/eip5792/actions/getCallsStatus + * - JSON-RPC Methods: [`wallet_getCallsStatus`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns Status of the calls. {@link GetCallsStatusReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { getCallsStatus } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const { receipts, status } = await getCallsStatus(client, { id: '0xdeadbeef' }) + */ +export async function getCallsStatus< + chain extends Chain | undefined, + account extends Account | undefined = undefined, +>( + client: Client, + parameters: GetCallsStatusParameters, +): Promise { + const { id } = parameters + const { receipts, status } = await client.request({ + method: 'wallet_getCallsStatus', + params: id, + }) + return { + status, + receipts: + receipts?.map((receipt) => ({ + ...receipt, + blockNumber: hexToBigInt(receipt.blockNumber), + gasUsed: hexToBigInt(receipt.gasUsed), + })) ?? [], + } +} diff --git a/src/experimental/actions/getCapabilities.test.ts b/src/experimental/actions/getCapabilities.test.ts new file mode 100644 index 0000000000..62217ca964 --- /dev/null +++ b/src/experimental/actions/getCapabilities.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from 'vitest' +import { createClient } from '../../clients/createClient.js' +import { custom } from '../../clients/transports/custom.js' +import { getCapabilities } from './getCapabilities.js' + +const client = createClient({ + transport: custom({ + async request({ method }) { + if (method === 'wallet_getCapabilities') + return { + '0x2105': { + paymasterService: { + supported: true, + }, + sessionKeys: { + supported: true, + }, + }, + '0x14A34': { + paymasterService: { + supported: true, + }, + }, + } + return null + }, + }), +}) + +test('default', async () => { + const capabilities = await getCapabilities(client) + expect(capabilities).toMatchInlineSnapshot(` + { + "8453": { + "paymasterService": { + "supported": true, + }, + "sessionKeys": { + "supported": true, + }, + }, + "84532": { + "paymasterService": { + "supported": true, + }, + }, + } + `) +}) diff --git a/src/experimental/actions/getCapabilities.ts b/src/experimental/actions/getCapabilities.ts new file mode 100644 index 0000000000..601ea0dae7 --- /dev/null +++ b/src/experimental/actions/getCapabilities.ts @@ -0,0 +1,56 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { ErrorType } from '../../errors/utils.js' +import type { Account } from '../../types/account.js' +import type { Chain } from '../../types/chain.js' +import type { + WalletCapabilities, + WalletCapabilitiesRecord, +} from '../../types/eip1193.js' +import type { Prettify } from '../../types/utils.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' + +export type GetCapabilitiesReturnType = Prettify< + WalletCapabilitiesRecord +> + +export type GetCapabilitiesErrorType = RequestErrorType | ErrorType + +/** + * Extract capabilities that a connected wallet supports (e.g. paymasters, session keys, etc). + * + * - Docs: https://viem.sh/eip5792/actions/getCapabilities + * - JSON-RPC Methods: [`wallet_getCapabilities`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns The wallet's capabilities. {@link GetCapabilitiesReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { getCapabilities } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const capabilities = await getCapabilities(client) + */ +export async function getCapabilities< + chain extends Chain | undefined, + account extends Account | undefined = undefined, +>( + client: Client, +): Promise { + const capabilities_raw = await client.request({ + method: 'wallet_getCapabilities', + }) + + const capabilities = {} as WalletCapabilitiesRecord< + WalletCapabilities, + number + > + for (const [key, value] of Object.entries(capabilities_raw)) + capabilities[Number(key)] = value + return capabilities +} diff --git a/src/experimental/actions/sendCalls.test.ts b/src/experimental/actions/sendCalls.test.ts new file mode 100644 index 0000000000..b809695b14 --- /dev/null +++ b/src/experimental/actions/sendCalls.test.ts @@ -0,0 +1,220 @@ +import { expect, test } from 'vitest' +import { accounts, localHttpUrl } from '../../../test/src/constants.js' +import { mainnet } from '../../chains/index.js' +import { createClient } from '../../clients/createClient.js' +import { custom } from '../../clients/transports/custom.js' +import { RpcRequestError } from '../../errors/request.js' +import { getHttpRpcClient, parseEther } from '../../utils/index.js' +import { sendCalls } from './sendCalls.js' + +const getClient = ({ + onRequest, +}: { onRequest({ method, params }: any): void }) => + createClient({ + transport: custom({ + async request({ method, params }) { + if (method !== 'wallet_sendCalls') return + + onRequest({ method, params }) + + const rpcClient = getHttpRpcClient(localHttpUrl) + for (const call of params.calls) { + const { error } = await rpcClient.request({ + body: { + method: 'eth_sendTransaction', + params: [call], + id: 0, + }, + }) + if (error) + throw new RpcRequestError({ + body: { method, params }, + error, + url: localHttpUrl, + }) + } + return '0xdeadbeef' + }, + }), + }) + +test('default', async () => { + const requests: unknown[] = [] + + const client = getClient({ + onRequest({ params }) { + requests.push(params) + }, + }) + + const id_ = await sendCalls(client, { + account: accounts[0].address, + calls: [ + { + to: accounts[1].address, + value: parseEther('1'), + }, + { + to: accounts[2].address, + }, + { + data: '0xcafebabe', + to: accounts[3].address, + value: parseEther('100'), + }, + ], + chain: mainnet, + }) + + expect(id_).toMatchInlineSnapshot(`"0xdeadbeef"`) + expect(requests).toMatchInlineSnapshot(` + [ + { + "calls": [ + { + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": "0xde0b6b3a7640000", + }, + { + "to": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", + "value": undefined, + }, + { + "data": "0xcafebabe", + "to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906", + "value": "0x56bc75e2d63100000", + }, + ], + "capabilities": undefined, + "chainId": "0x1", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "version": "1.0", + }, + ] + `) +}) + +test('error: no chain', async () => { + const requests: unknown[] = [] + + const client = getClient({ + onRequest({ params }) { + requests.push(params) + }, + }) + + await expect(() => + // @ts-expect-error + sendCalls(client, { + account: accounts[0].address, + calls: [ + { + to: accounts[1].address, + value: parseEther('1'), + }, + { + to: accounts[2].address, + value: parseEther('10'), + }, + { + data: '0xcafebabe', + to: accounts[3].address, + value: parseEther('1000000'), + }, + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [ChainNotFoundError: No chain was provided to the request. + Please provide a chain with the \`chain\` argument on the Action, or by supplying a \`chain\` to WalletClient. + + Version: viem@1.0.2] + `) +}) + +test('error: no account', async () => { + const requests: unknown[] = [] + + const client = getClient({ + onRequest({ params }) { + requests.push(params) + }, + }) + + await expect(() => + // @ts-expect-error + sendCalls(client, { + calls: [ + { + to: accounts[1].address, + value: parseEther('1'), + }, + { + to: accounts[2].address, + value: parseEther('10'), + }, + { + data: '0xcafebabe', + to: accounts[3].address, + value: parseEther('1000000'), + }, + ], + chain: mainnet, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [AccountNotFoundError: Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the WalletClient. + + Docs: https://viem.sh/eip5792/actions/sendCalls#account + Version: viem@1.0.2] + `) +}) + +test('error: insufficient funds', async () => { + const requests: unknown[] = [] + + const client = getClient({ + onRequest({ params }) { + requests.push(params) + }, + }) + + await expect(() => + sendCalls(client, { + account: accounts[0].address, + calls: [ + { + to: accounts[1].address, + value: parseEther('1'), + }, + { + to: accounts[2].address, + value: parseEther('10'), + }, + { + data: '0xcafebabe', + to: accounts[3].address, + value: parseEther('1000000'), + }, + ], + chain: mainnet, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [TransactionExecutionError: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account. + + This error could arise when the account does not have enough funds to: + - pay for the total gas fee, + - pay for the value to send. + + The cost of the transaction is calculated as \`gas * gas fee + value\`, where: + - \`gas\` is the amount of gas needed for transaction to execute, + - \`gas fee\` is the gas fee, + - \`value\` is the amount of ether to send to the recipient. + + Request Arguments: + chain: Ethereum (id: 1) + from: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Details: Insufficient funds for gas * price + value + Version: viem@1.0.2] + `) +}) diff --git a/src/experimental/actions/sendCalls.ts b/src/experimental/actions/sendCalls.ts new file mode 100644 index 0000000000..960b64e8cd --- /dev/null +++ b/src/experimental/actions/sendCalls.ts @@ -0,0 +1,126 @@ +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import { AccountNotFoundError } from '../../errors/account.js' +import type { BaseError } from '../../errors/base.js' +import { ChainNotFoundError } from '../../errors/chain.js' +import type { ErrorType } from '../../errors/utils.js' +import type { Account, GetAccountParameter } from '../../types/account.js' +import type { Chain, GetChainParameter } from '../../types/chain.js' +import type { + WalletCapabilities, + WalletSendCallsParameters, +} from '../../types/eip1193.js' +import type { Hex } from '../../types/misc.js' +import type { OneOf } from '../../types/utils.js' +import { parseAccount } from '../../utils/accounts.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' +import { numberToHex } from '../../utils/encoding/toHex.js' +import { getTransactionError } from '../../utils/index.js' + +export type SendCallsParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, +> = { + calls: OneOf< + | { + to: Hex + data?: Hex + value?: bigint + } + | { + data: Hex + } + >[] + capabilities?: + | WalletSendCallsParameters['capabilities'] + | undefined + version?: WalletSendCallsParameters['version'] | undefined +} & GetAccountParameter & + GetChainParameter + +export type SendCallsReturnType = string + +export type SendCallsErrorType = RequestErrorType | ErrorType + +/** + * Requests the connected wallet to send a batch of calls. + * + * - Docs: https://viem.sh/eip5792/actions/sendCalls + * - JSON-RPC Methods: [`wallet_sendCalls`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns Transaction identifier. {@link SendCallsReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { sendCalls } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const id = await sendCalls(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * calls: [ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * ], + * }) + */ +export async function sendCalls< + chain extends Chain | undefined, + account extends Account | undefined = undefined, + chainOverride extends Chain | undefined = undefined, +>( + client: Client, + parameters: SendCallsParameters, +): Promise { + const { + account: account_ = client.account, + calls, + capabilities, + chain = client.chain, + version = '1.0', + } = parameters + + if (!account_) + throw new AccountNotFoundError({ + docsPath: '/eip5792/actions/sendCalls', + }) + const account = parseAccount(account_) + + if (!chain) throw new ChainNotFoundError() + + try { + return await client.request( + { + method: 'wallet_sendCalls', + params: { + calls: calls.map((call) => ({ + ...call, + value: call.value ? numberToHex(call.value) : undefined, + })) as any, + capabilities, + chainId: numberToHex(chain!.id), + from: account.address, + version, + }, + }, + { retryCount: 0 }, + ) + } catch (err) { + throw getTransactionError(err as BaseError, { + ...parameters, + account, + chain: parameters.chain!, + }) + } +} diff --git a/src/experimental/decorators/eip5792.test.ts b/src/experimental/decorators/eip5792.test.ts new file mode 100644 index 0000000000..168f86fe09 --- /dev/null +++ b/src/experimental/decorators/eip5792.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from 'vitest' + +import { mainnet } from '../../chains/index.js' +import { createClient } from '../../clients/createClient.js' +import { custom } from '../../clients/transports/custom.js' +import { walletActionsEip5792 } from './eip5792.js' + +const client = createClient({ + transport: custom({ + async request({ method }) { + if (method === 'wallet_getCapabilities') + return { + '0x2105': { + paymasterService: { + supported: true, + }, + sessionKeys: { + supported: true, + }, + }, + '0x14A34': { + paymasterService: { + supported: true, + }, + }, + } + if (method === 'wallet_sendCalls') return '0x1' + if (method === 'wallet_getCallsStatus') + return { + status: 'CONFIRMED', + receipts: [ + { + blockHash: + '0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3', + blockNumber: '0x1', + gasUsed: '0x1', + logs: [], + status: '0x1', + transactionHash: + '0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3', + }, + ], + } + return null + }, + }), +}).extend(walletActionsEip5792()) + +test('default', async () => { + expect(walletActionsEip5792()(client)).toMatchInlineSnapshot(` + { + "getCallsStatus": [Function], + "getCapabilities": [Function], + "sendCalls": [Function], + } + `) +}) + +describe('smoke test', () => { + test('getCapabilities', async () => { + expect(await client.getCapabilities()).toMatchInlineSnapshot(` + { + "8453": { + "paymasterService": { + "supported": true, + }, + "sessionKeys": { + "supported": true, + }, + }, + "84532": { + "paymasterService": { + "supported": true, + }, + }, + } + `) + }) + + test('getCallsStatus', async () => { + expect(await client.getCallsStatus({ id: '0x123' })).toMatchInlineSnapshot(` + { + "receipts": [ + { + "blockHash": "0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3", + "blockNumber": 1n, + "gasUsed": 1n, + "logs": [], + "status": "0x1", + "transactionHash": "0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3", + }, + ], + "status": "CONFIRMED", + } + `) + }) + + test('sendCalls', async () => { + expect( + await client.sendCalls({ + account: '0x0000000000000000000000000000000000000000', + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chain: mainnet, + }), + ).toMatchInlineSnapshot(`"0x1"`) + }) +}) diff --git a/src/experimental/decorators/eip5792.ts b/src/experimental/decorators/eip5792.ts new file mode 100644 index 0000000000..75edf91ae2 --- /dev/null +++ b/src/experimental/decorators/eip5792.ts @@ -0,0 +1,139 @@ +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 GetCallsStatusParameters, + type GetCallsStatusReturnType, + getCallsStatus, +} from '../actions/getCallsStatus.js' +import { + type GetCapabilitiesReturnType, + getCapabilities, +} from '../actions/getCapabilities.js' +import { + type SendCallsParameters, + type SendCallsReturnType, + sendCalls, +} from '../actions/sendCalls.js' + +export type WalletActionsEip5792< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, +> = { + /** + * Returns the status of a call batch that was sent via `sendCalls`. + * + * - Docs: https://viem.sh/eip5792/actions/getCallsStatus + * - JSON-RPC Methods: [`wallet_getCallsStatus`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns Status of the calls. {@link GetCallsStatusReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsEip5792 } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(walletActionsEip5792()) + * + * const { receipts, status } = await client.getCallsStatus({ id: '0xdeadbeef' }) + */ + getCallsStatus: ( + parameters: GetCallsStatusParameters, + ) => Promise + /** + * Extract capabilities that a connected wallet supports (e.g. paymasters, session keys, etc). + * + * - Docs: https://viem.sh/eip5792/actions/getCapabilities + * - JSON-RPC Methods: [`wallet_getCapabilities`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns The wallet's capabilities. {@link GetCapabilitiesReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsEip5792 } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(walletActionsEip5792()) + * + * const capabilities = await client.getCapabilities() + */ + getCapabilities: () => Promise + /** + * Requests the connected wallet to send a batch of calls. + * + * - Docs: https://viem.sh/eip5792/actions/sendCalls + * - JSON-RPC Methods: [`wallet_sendCalls`](https://eips.ethereum.org/EIPS/eip-5792) + * + * @param client - Client to use + * @returns Transaction identifier. {@link SendCallsReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsEip5792 } from 'viem/experimental' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }).extend(walletActionsEip5792()) + * + * const id = await client.sendCalls({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * calls: [ + * { + * data: '0xdeadbeef', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * }, + * { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 69420n, + * }, + * ], + * }) + */ + sendCalls: ( + parameters: SendCallsParameters, + ) => Promise +} + +/** + * A suite of EIP-5792 Wallet Actions. + * + * - Docs: https://viem.sh/eip5792 + * + * @example + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { walletActionsEip5792 } from 'viem/experimental' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * }).extend(walletActionsEip5792()) + * + * const hash = await walletClient.sendCalls({...}) + */ +export function walletActionsEip5792() { + return < + transport extends Transport, + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + >( + client: Client, + ): WalletActionsEip5792 => { + return { + getCallsStatus: (parameters) => getCallsStatus(client, parameters), + getCapabilities: () => getCapabilities(client), + sendCalls: (parameters) => sendCalls(client, parameters), + } + } +} diff --git a/src/experimental/index.ts b/src/experimental/index.ts new file mode 100644 index 0000000000..401e374f7b --- /dev/null +++ b/src/experimental/index.ts @@ -0,0 +1,22 @@ +export { + type GetCapabilitiesErrorType, + type GetCapabilitiesReturnType, + getCapabilities, +} from './actions/getCapabilities.js' +export { + type SendCallsErrorType, + type SendCallsParameters, + type SendCallsReturnType, + sendCalls, +} from './actions/sendCalls.js' +export { + type GetCallsStatusErrorType, + type GetCallsStatusParameters, + type GetCallsStatusReturnType, + getCallsStatus, +} from './actions/getCallsStatus.js' + +export { + type WalletActionsEip5792, + walletActionsEip5792, +} from './decorators/eip5792.js' diff --git a/src/experimental/package.json b/src/experimental/package.json new file mode 100644 index 0000000000..5ff0a8469b --- /dev/null +++ b/src/experimental/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "types": "../_types/experimental/index.d.ts", + "module": "../_esm/experimental/index.js", + "main": "../_cjs/experimental/index.js" +} diff --git a/src/package.json b/src/package.json index 951034cbcc..cfd1ce72bd 100644 --- a/src/package.json +++ b/src/package.json @@ -53,6 +53,11 @@ "import": "./_esm/ens/index.js", "default": "./_cjs/ens/index.js" }, + "./experimental": { + "types": "./_types/experimental/index.d.ts", + "import": "./_esm/experimental/index.js", + "default": "./_cjs/experimental/index.js" + }, "./node": { "types": "./_types/node/index.d.ts", "import": "./_esm/node/index.js", @@ -82,39 +87,18 @@ }, "typesVersions": { "*": { - "accounts": [ - "./_types/accounts/index.d.ts" - ], - "actions": [ - "./_types/actions/index.d.ts" - ], - "celo": [ - "./_types/celo/index.d.ts" - ], - "chains": [ - "./_types/chains/index.d.ts" - ], - "chains/utils": [ - "./_types/chains/utils.d.ts" - ], - "ens": [ - "./_types/ens/index.d.ts" - ], - "node": [ - "./_types/node/index.d.ts" - ], - "op-stack": [ - "./_types/op-stack/index.d.ts" - ], - "utils": [ - "./_types/utils/index.d.ts" - ], - "window": [ - "./_types/window/index.d.ts" - ], - "zksync": [ - "./_types/zksync/index.d.ts" - ] + "accounts": ["./_types/accounts/index.d.ts"], + "actions": ["./_types/actions/index.d.ts"], + "celo": ["./_types/celo/index.d.ts"], + "chains": ["./_types/chains/index.d.ts"], + "chains/utils": ["./_types/chains/utils.d.ts"], + "ens": ["./_types/ens/index.d.ts"], + "experimental": ["./_types/experimental/index.d.ts"], + "node": ["./_types/node/index.d.ts"], + "op-stack": ["./_types/op-stack/index.d.ts"], + "utils": ["./_types/utils/index.d.ts"], + "window": ["./_types/window/index.d.ts"], + "zksync": ["./_types/zksync/index.d.ts"] } }, "peerDependencies": { @@ -138,21 +122,12 @@ "license": "MIT", "homepage": "https://viem.sh", "repository": "wevm/viem", - "authors": [ - "awkweb.eth", - "jxom.eth" - ], + "authors": ["awkweb.eth", "jxom.eth"], "funding": [ { "type": "github", "url": "https://github.com/sponsors/wevm" } ], - "keywords": [ - "eth", - "ethereum", - "dapps", - "wallet", - "web3" - ] + "keywords": ["eth", "ethereum", "dapps", "wallet", "web3"] } diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index e3ef51747a..3fb38327cc 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -107,6 +107,35 @@ export type NetworkSync = { startingBlock: Quantity } +export type WalletCapabilities = { + [capability: string]: any +} + +export type WalletCapabilitiesRecord< + capabilities extends WalletCapabilities = WalletCapabilities, + id extends string | number = Hex, +> = { + [chainId in id]: capabilities +} + +export type WalletGetCallsStatusReceipt = { + logs: { + address: Hex + data: Hex + topics: Hex[] + }[] + status: Hex + blockHash: Hex + blockNumber: quantity + gasUsed: quantity + transactionHash: Hex +} + +export type WalletGetCallsStatusReturnType = { + status: 'PENDING' | 'CONFIRMED' + receipts?: WalletGetCallsStatusReceipt[] +} + export type WalletPermissionCaveat = { type: string value: any @@ -120,6 +149,22 @@ export type WalletPermission = { parentCapability: 'eth_accounts' | string } +export type WalletSendCallsParameters< + capabilities extends WalletCapabilities = WalletCapabilities, + chainId extends Hex | number = Hex, + quantity extends Quantity | bigint = Quantity, +> = { + version: string + chainId: chainId + from: Address + calls: { + to: Address + data: Hex + value: quantity + }[] + capabilities?: capabilities | undefined +} + export type WatchAssetParams = { /** Token type. */ type: 'ERC20' @@ -1270,6 +1315,30 @@ export type WalletRpcSchema = [ Parameters: [chain: AddEthereumChainParameter] ReturnType: null }, + /** + * @description Returns the status of a call batch that was sent via `wallet_sendCalls`. + * @link https://eips.ethereum.org/EIPS/eip-5792 + * @example + * provider.request({ method: 'wallet_getCallsStatus' }) + * // => { ... } + */ + { + Method: 'wallet_getCallsStatus' + Parameters?: string + ReturnType: WalletGetCallsStatusReturnType + }, + /** + * @description Gets the connected wallet's capabilities. + * @link https://eips.ethereum.org/EIPS/eip-5792 + * @example + * provider.request({ method: 'wallet_getCapabilities' }) + * // => { ... } + */ + { + Method: 'wallet_getCapabilities' + Parameters?: undefined + ReturnType: Prettify + }, /** * @description Gets the wallets current permissions. * @link https://eips.ethereum.org/EIPS/eip-2255 @@ -1294,6 +1363,18 @@ export type WalletRpcSchema = [ Parameters: [permissions: { eth_accounts: Record }] ReturnType: WalletPermission[] }, + /** + * @description Requests the connected wallet to send a batch of calls. + * @link https://eips.ethereum.org/EIPS/eip-5792 + * @example + * provider.request({ method: 'wallet_sendCalls' }) + * // => { ... } + */ + { + Method: 'wallet_sendCalls' + Parameters?: WalletSendCallsParameters + ReturnType: string + }, /** * @description Switch the wallet to the given Ethereum chain. * @link https://eips.ethereum.org/EIPS/eip-3326