diff --git a/.changeset/giant-boats-wave.md b/.changeset/giant-boats-wave.md new file mode 100644 index 0000000000..f07eb7c363 --- /dev/null +++ b/.changeset/giant-boats-wave.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Experimental:** Deprecated `writeContracts`. Use `sendCalls` instead. diff --git a/.changeset/nice-trainers-ring.md b/.changeset/nice-trainers-ring.md new file mode 100644 index 0000000000..8ee6fadea9 --- /dev/null +++ b/.changeset/nice-trainers-ring.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Experimental:** Updated `sendCalls` to match the updated EIP-5792 spec (`chainId` per call). diff --git a/.changeset/violet-schools-check.md b/.changeset/violet-schools-check.md new file mode 100644 index 0000000000..3d3bb53d28 --- /dev/null +++ b/.changeset/violet-schools-check.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Experimental:** Updated `sendCalls` to also accept contract function interface. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4407d47fe..9309a2f98f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3225,7 +3225,6 @@ packages: bun@1.1.12: resolution: {integrity: sha512-NZzeZuZk7VwCs8VAXnXUHCPOlTS/IyHCscChtT1M1FLSwhBcVMsGVStYlXaaoqsinBKgp0CGJdhnJw2gR3NkDw==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -6765,6 +6764,14 @@ packages: typescript: optional: true + viem@2.19.9: + resolution: {integrity: sha512-KFPSfewr8tFaSYcLAC+sgkYXdZ1llX8rJrBjd/OMg1D+T4eeQyYy5S6iJTSnwYpjz8hrjSlL30RuFf4BF3jtMw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + viem@file:src: resolution: {directory: src, type: directory} peerDependencies: @@ -8447,7 +8454,7 @@ snapshots: pino-http: 8.6.1 pino-pretty: 10.3.1 prom-client: 14.2.0 - viem: 2.19.8(typescript@5.5.2)(zod@3.22.4) + viem: 2.19.9(typescript@5.5.2)(zod@3.22.4) yargs: 17.7.2 zod: 3.22.4 zod-validation-error: 1.5.0(zod@3.22.4) @@ -14357,37 +14364,37 @@ snapshots: - utf-8-validate - zod - viem@2.19.8(typescript@5.5.2)(zod@3.22.4): + viem@2.19.8(typescript@5.5.4)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.10.0 '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - abitype: 1.0.5(typescript@5.5.2)(zod@3.22.4) + abitype: 1.0.5(typescript@5.5.4)(zod@3.22.4) isows: 1.0.4(ws@8.17.1) webauthn-p256: 0.0.5 ws: 8.17.1 optionalDependencies: - typescript: 5.5.2 + typescript: 5.5.4 transitivePeerDependencies: - bufferutil - utf-8-validate - zod - viem@2.19.8(typescript@5.5.4)(zod@3.22.4): + viem@2.19.9(typescript@5.5.2)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.10.0 '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - abitype: 1.0.5(typescript@5.5.4)(zod@3.22.4) + abitype: 1.0.5(typescript@5.5.2)(zod@3.22.4) isows: 1.0.4(ws@8.17.1) webauthn-p256: 0.0.5 ws: 8.17.1 optionalDependencies: - typescript: 5.5.4 + typescript: 5.5.2 transitivePeerDependencies: - bufferutil - utf-8-validate diff --git a/site/pages/experimental/eip5792/sendCalls.mdx b/site/pages/experimental/eip5792/sendCalls.mdx index fd9ecdb230..1a4e512353 100644 --- a/site/pages/experimental/eip5792/sendCalls.mdx +++ b/site/pages/experimental/eip5792/sendCalls.mdx @@ -101,6 +101,82 @@ export const walletClient = createWalletClient({ ::: +### 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 { parseAbi } from 'viem' +import { walletClient } from './config' + +const abi = parseAbi([ + 'function approve(address, uint256) returns (bool)', + 'function transferFrom(address, address, uint256) returns (bool)', +]) + +const id = await walletClient.sendCalls({ // [!code focus:99] + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi, + functionName: 'approve', + args: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + 100n + ], + }, + { + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi, + functionName: 'transferFrom', + args: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + '0x0000000000000000000000000000000000000000', + 100n + ], + }, + ], +}) +``` + +```ts twoslash [abi.ts] filename="abi.ts" +export const wagmiAbi = [ + // ... + { + inputs: [], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + // ... +] as const; +``` + +```ts [config.ts] filename="config.ts" +import 'viem/window' +import { createWalletClient, custom } from 'viem' +import { walletActionsEip5792 } from 'viem/experimental' + +// Retrieve Account from an EIP-1193 Provider. +const [account] = await window.ethereum!.request({ + method: 'eth_requestAccounts' +}) + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum!) +}).extend(walletActionsEip5792()) +``` + +::: + ## Returns `string` diff --git a/site/pages/experimental/eip5792/writeContracts.mdx b/site/pages/experimental/eip5792/writeContracts.mdx deleted file mode 100644 index 9727c81306..0000000000 --- a/site/pages/experimental/eip5792/writeContracts.mdx +++ /dev/null @@ -1,425 +0,0 @@ ---- -description: Sign and broadcast a batch of write contract calls to the network. ---- - -# writeContracts - -Requests for the wallet to sign and broadcast a batch of write contract calls (transactions) to the network. - -[Read more.](https://github.com/ethereum/EIPs/blob/815028dc634463e1716fc5ce44c019a6040f0bef/EIPS/eip-5792.md#wallet_sendcalls) - -:::warning[Warning] -This is an experimental action that is not supported in most wallets. It is recommended to have a fallback mechanism if using this in production. -::: - -## Usage - -:::code-group - -```ts twoslash [example.ts] -import { parseAbi } from 'viem' -import { account, walletClient } from './config' - -const abi = parseAbi([ - 'function approve(address, uint256) returns (bool)', - 'function transferFrom(address, address, uint256) returns (bool)', -]) - -const id = await walletClient.writeContracts({ - account, - contracts: [ - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'approve', - args: [ - '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', - 100n - ], - }, - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'transferFrom', - args: [ - '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', - '0x0000000000000000000000000000000000000000', - 100n - ], - }, - ], -}) -``` - -```ts twoslash [config.ts] filename="config.ts" -import 'viem/window' -// ---cut--- -import { createWalletClient, custom } from 'viem' -import { mainnet } from 'viem/chains' -import { walletActionsEip5792 } from 'viem/experimental' - -export const walletClient = createWalletClient({ - chain: mainnet, - transport: custom(window.ethereum!), -}).extend(walletActionsEip5792()) - -export const [account] = await walletClient.getAddresses() -``` - -::: - -Notes: - -- Internally calls [`sendCalls`](/experimental/eip5792/writeContracts) -- `account` and `chain` are top level properties as all calls should be sent by the same account and chain. -- [Read `wallet_sendCalls` on EIP-5792.](https://github.com/ethereum/EIPs/blob/815028dc634463e1716fc5ce44c019a6040f0bef/EIPS/eip-5792.md#wallet_sendcalls) - -### Account Hoisting - -If you do not wish to pass an `account` to every `writeContracts`, 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 { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi([ - 'function approve(address, uint256) returns (bool)', - 'function transferFrom(address, address, uint256) returns (bool)', -]) - -const id = await walletClient.writeContracts({ - contracts: [ - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'approve', - args: [ - '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', - 100n - ], - }, - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'transferFrom', - args: [ - '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', - '0x0000000000000000000000000000000000000000', - 100n - ], - }, - ], -}) -``` - -```ts [config.ts] filename="config.ts" -import 'viem/window' -import { createWalletClient, custom } from 'viem' -import { walletActionsEip5792 } from 'viem/experimental' - -// Retrieve Account from an EIP-1193 Provider. -const [account] = await window.ethereum!.request({ - method: 'eth_requestAccounts' -}) - -export const walletClient = createWalletClient({ - account, - transport: custom(window.ethereum!) -}).extend(walletActionsEip5792()) -``` - -::: - -## Returns - -`string` - -The identifier can be any arbitrary string. The only requirement is that for a given session, consumers should be able to call `getCallsStatus` with this identifier to retrieve a batch call status and call receipts. - -## Parameters - -### account - -- **Type:** `Account | Address` - -The Account to sign & broadcast the call from. - -Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts). - -```ts twoslash -import { parseAbi, mainnet } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] - contracts: [ - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'mint', - args: [69n], - }, - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'mint', - args: [420n], - }, - ], -}) -``` - -### chain - -- **Type:** [`Chain`](/docs/glossary/types#chain) -- **Default:** `walletClient.chain` - -The target chain to broadcast the calls. - -```ts twoslash -import { parseAbi, mainnet } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - chain: mainnet, // [!code focus] - contracts: [ - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'mint', - args: [69n], - }, - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - abi, - functionName: 'mint', - args: [420n], - }, - ], -}) -``` - -### contracts - -- **Type:** `MulticallContracts[]` - -An array of write contract calls to be signed and broadcasted. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ // [!code focus] - { // [!code focus] - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] - abi, // [!code focus] - functionName: 'mint', // [!code focus] - args: [69n], // [!code focus] - }, // [!code focus] - { // [!code focus] - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] - abi, // [!code focus] - functionName: 'mint', // [!code focus] - args: [420n], // [!code focus] - }, // [!code focus] - ], -}) -``` - -#### contracts.abi - -- **Type:** `Abi` - -The contract's ABI. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - abi, // [!code focus] - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [69n], - }, - { - abi, // [!code focus] - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [420n], - }, - ], -}) -``` - -#### contracts.address - -- **Type:** `Address` - -The contract address. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] - abi, - functionName: 'mint', - args: [69n], - }, - { - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // [!code focus] - abi, - functionName: 'mint', - args: [420n], - }, - ], -}) -``` - -#### contracts.functionName - -- **Type:** `string` - -A function to extract from the ABI. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', // [!code focus] - args: [69n], - }, - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', // [!code focus] - args: [420n], - }, - ], -}) -``` - -#### contracts.args - -- **Type:** Inferred from ABI. - -Arguments to pass to function call. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [69n], // [!code focus] - }, - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [420n], // [!code focus] - }, - ], -}) -``` - -#### contracts.value - -- **Type:** `number` - -Value in wei sent with this call. - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [69n], - }, - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [420n], - value: 69420n, // [!code focus] - }, - ], -}) -``` - -### capabilities - -- **Type:** `WalletCapabilities` - -Capability metadata for the calls (e.g. specifying a paymaster). - -```ts twoslash -import { parseAbi } from 'viem' -import { walletClient } from './config' - -const abi = parseAbi(['function mint(uint256)']) - -const id = await walletClient.writeContracts({ - contracts: [ - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [69n], - }, - { - abi, - address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', - functionName: 'mint', - args: [420n], - }, - ], - capabilities: { // [!code focus] - paymasterService: { // [!code focus] - url: 'https://...' // [!code focus] - } // [!code focus] - } // [!code focus] -}) -``` - diff --git a/site/sidebar.ts b/site/sidebar.ts index 1e9cfcddc1..8c4daad846 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1242,10 +1242,6 @@ export const sidebar = { text: 'showCallsStatus', link: '/experimental/eip5792/showCallsStatus', }, - { - text: 'writeContracts', - link: '/experimental/eip5792/writeContracts', - }, ], }, ], diff --git a/site/vercel.json b/site/vercel.json index 5a023af28f..21e6d9c7ec 100644 --- a/site/vercel.json +++ b/site/vercel.json @@ -55,6 +55,10 @@ { "source": "/:match/accounts/signTypedData", "destination": "/:match/accounts/local/signTypedData" + }, + { + "source": "/:match/experimental/eip5792/writeContracts", + "destination": "/:match/experimental/eip5792/sendCalls#contract-calls" } ] } diff --git a/src/experimental/eip5792/actions/sendCalls.test.ts b/src/experimental/eip5792/actions/sendCalls.test.ts index 223c3a7e73..22d27ce76b 100644 --- a/src/experimental/eip5792/actions/sendCalls.test.ts +++ b/src/experimental/eip5792/actions/sendCalls.test.ts @@ -1,43 +1,108 @@ import { expect, test } from 'vitest' +import { wagmiContractConfig } from '../../../../test/src/abis.js' import { anvilMainnet } from '../../../../test/src/anvil.js' import { accounts } from '../../../../test/src/constants.js' -import { mainnet } from '../../../chains/index.js' -import { createClient } from '../../../clients/createClient.js' +import { reset } from '../../../actions/index.js' +import { type Chain, mainnet } from '../../../chains/index.js' +import { type Client, createClient } from '../../../clients/createClient.js' +import type { Transport } from '../../../clients/transports/createTransport.js' import { custom } from '../../../clients/transports/custom.js' import { RpcRequestError } from '../../../errors/request.js' +import type { WalletCallReceipt } 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 { sendCalls } from './sendCalls.js' -const getClient = ({ +type Uid = string +type TxHashes = Hex[] +const calls = new Map() + +const testClient = anvilMainnet.getClient() + +const getClient = ({ + chain, onRequest, -}: { onRequest({ method, params }: any): void }) => +}: { + chain?: chain | undefined + onRequest({ method, params }: any): void +}): Client => createClient({ + chain, transport: custom({ async request({ method, params }) { - if (method !== 'wallet_sendCalls') return - onRequest({ method, params }) const rpcClient = getHttpRpcClient(anvilMainnet.rpcUrl.http) - for (const call of params[0].calls) { - const { error } = await rpcClient.request({ - body: { - method: 'eth_sendTransaction', - params: [call], - id: 0, - }, - }) - if (error) - throw new RpcRequestError({ - body: { method, params }, - error, - url: anvilMainnet.rpcUrl.http, + + if (method === 'wallet_getCallsStatus') { + const hashes = calls.get(params[0]) + if (!hashes) return { status: 'PENDING', receipts: [] } + 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: anvilMainnet.rpcUrl.http, + }) + if (!result) throw new Error('receipt not found') + return { + blockHash: result.blockHash, + blockNumber: result.blockNumber, + gasUsed: result.gasUsed, + logs: result.logs, + status: result.status, + transactionHash: result.transactionHash, + } satisfies WalletCallReceipt + }), + ) + return { status: 'CONFIRMED', receipts } + } + + if (method === 'wallet_sendCalls') { + const hashes = [] + for (const call of params[0].calls) { + const callResult = await rpcClient.request({ + body: { + method: 'eth_call', + params: [{ ...call, from: params[0].from }], + id: 0, + }, }) + if (callResult.error) throw new Error(callResult.error.message) + + const { result, error } = await rpcClient.request({ + body: { + method: 'eth_sendTransaction', + params: [{ ...call, from: params[0].from }], + id: 0, + }, + }) + if (error) + throw new RpcRequestError({ + body: { method, params }, + error, + url: anvilMainnet.rpcUrl.http, + }) + hashes.push(result) + } + const uid_ = uid() + calls.set(uid_, hashes) + return uid_ } - return '0xdeadbeef' + + return null }, }), - }) + }) as never test('default', async () => { const requests: unknown[] = [] @@ -48,8 +113,14 @@ test('default', async () => { }, }) + await reset(testClient, { + blockNumber: 16280770n, + jsonRpcUrl: anvilMainnet.forkUrl, + }) + const id_ = await sendCalls(client, { account: accounts[0].address, + chain: mainnet, calls: [ { to: accounts[1].address, @@ -63,29 +134,145 @@ test('default', async () => { to: accounts[3].address, value: parseEther('100'), }, + { + abi: wagmiContractConfig.abi, + functionName: 'mint', + to: wagmiContractConfig.address, + }, + { + abi: wagmiContractConfig.abi, + functionName: 'mint', + to: wagmiContractConfig.address, + }, ], + }) + + expect(id_).toBeDefined() + expect(requests).toMatchInlineSnapshot(` + [ + [ + { + "calls": [ + { + "chainId": "0x1", + "data": undefined, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": "0xde0b6b3a7640000", + }, + { + "chainId": "0x1", + "data": undefined, + "to": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", + "value": undefined, + }, + { + "chainId": "0x1", + "data": "0xcafebabe", + "to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906", + "value": "0x56bc75e2d63100000", + }, + { + "chainId": "0x1", + "data": "0x1249c58b", + "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", + "value": undefined, + }, + { + "chainId": "0x1", + "data": "0x1249c58b", + "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", + "value": undefined, + }, + ], + "capabilities": undefined, + "chainId": "0x1", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "version": "1.0", + }, + ], + ] + `) +}) + +test('behavior: chain on client', async () => { + const requests: unknown[] = [] + + const client = getClient({ chain: mainnet, + onRequest({ params }) { + requests.push(params) + }, }) - expect(id_).toMatchInlineSnapshot(`"0xdeadbeef"`) + await reset(testClient, { + blockNumber: 16280770n, + jsonRpcUrl: anvilMainnet.forkUrl, + }) + + 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'), + }, + { + abi: wagmiContractConfig.abi, + functionName: 'mint', + to: wagmiContractConfig.address, + }, + { + abi: wagmiContractConfig.abi, + functionName: 'mint', + to: wagmiContractConfig.address, + }, + ], + }) + + expect(id_).toBeDefined() expect(requests).toMatchInlineSnapshot(` [ [ { "calls": [ { + "chainId": "0x1", + "data": undefined, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "value": "0xde0b6b3a7640000", }, { + "chainId": "0x1", + "data": undefined, "to": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", "value": undefined, }, { + "chainId": "0x1", "data": "0xcafebabe", "to": "0x90f79bf6eb2c4f870365e785982e1f101e93b906", "value": "0x56bc75e2d63100000", }, + { + "chainId": "0x1", + "data": "0x1249c58b", + "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", + "value": undefined, + }, + { + "chainId": "0x1", + "data": "0x1249c58b", + "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", + "value": undefined, + }, ], "capabilities": undefined, "chainId": "0x1", @@ -107,18 +294,25 @@ test('error: no chain', async () => { }) await expect(() => - // @ts-expect-error sendCalls(client, { account: accounts[0].address, calls: [ { + chainId: 1, to: accounts[1].address, value: parseEther('1'), }, + { + chain: mainnet, + to: accounts[1].address, + value: parseEther('1'), + }, + // @ts-expect-error { to: accounts[2].address, value: parseEther('10'), }, + // @ts-expect-error { data: '0xcafebabe', to: accounts[3].address, @@ -138,6 +332,7 @@ test('error: no account', async () => { const requests: unknown[] = [] const client = getClient({ + chain: mainnet, onRequest({ params }) { requests.push(params) }, @@ -176,6 +371,7 @@ test('error: insufficient funds', async () => { const requests: unknown[] = [] const client = getClient({ + chain: mainnet, onRequest({ params }) { requests.push(params) }, diff --git a/src/experimental/eip5792/actions/sendCalls.ts b/src/experimental/eip5792/actions/sendCalls.ts index 8c6d2d7652..8f06f1c5df 100644 --- a/src/experimental/eip5792/actions/sendCalls.ts +++ b/src/experimental/eip5792/actions/sendCalls.ts @@ -1,3 +1,4 @@ +import type { AbiStateMutability, Address, Narrow } from 'abitype' import { parseAccount } from '../../../accounts/utils/parseAccount.js' import type { Client } from '../../../clients/createClient.js' import type { Transport } from '../../../clients/transports/createTransport.js' @@ -6,13 +7,16 @@ 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 { Chain, DeriveChain } from '../../../types/chain.js' +import type { ContractFunctionParameters } from '../../../types/contract.js' import type { WalletCapabilities, WalletSendCallsParameters, } from '../../../types/eip1193.js' import type { Hex } from '../../../types/misc.js' -import type { OneOf } from '../../../types/utils.js' +import type { GetMulticallContractParameters } from '../../../types/multicall.js' +import type { MaybeRequired, OneOf, Prettify } from '../../../types/utils.js' +import { encodeFunctionData } from '../../../utils/abi/encodeFunctionData.js' import type { RequestErrorType } from '../../../utils/buildRequest.js' import { numberToHex } from '../../../utils/encoding/toHex.js' import { getTransactionError } from '../../../utils/errors/getTransactionError.js' @@ -21,23 +25,17 @@ export type SendCallsParameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, chainOverride extends Chain | undefined = Chain | undefined, + calls extends readonly unknown[] = readonly unknown[], + // + _chain extends Chain | undefined = DeriveChain, > = { - calls: OneOf< - | { - to: Hex - data?: Hex | undefined - value?: bigint | undefined - } - | { - data: Hex - } - >[] + chain?: chainOverride | Chain | undefined + calls: Calls, _chain> capabilities?: | WalletSendCallsParameters[number]['capabilities'] | undefined version?: WalletSendCallsParameters[number]['version'] | undefined -} & GetAccountParameter & - GetChainParameter +} & GetAccountParameter export type SendCallsReturnType = string @@ -76,16 +74,16 @@ export type SendCallsErrorType = RequestErrorType | ErrorType * }) */ export async function sendCalls< + const calls extends readonly unknown[], chain extends Chain | undefined, account extends Account | undefined = undefined, chainOverride extends Chain | undefined = undefined, >( client: Client, - parameters: SendCallsParameters, + parameters: SendCallsParameters, ): Promise { const { account: account_ = client.account, - calls, capabilities, chain = client.chain, version = '1.0', @@ -97,7 +95,27 @@ export async function sendCalls< }) const account = parseAccount(account_) - if (!chain) throw new ChainNotFoundError() + const calls = parameters.calls.map((call_: unknown) => { + const call = call_ as Call + + const chainId = call.chain?.id ?? call.chainId ?? chain?.id + if (!chainId) throw new ChainNotFoundError() + + const data = call.abi + ? encodeFunctionData({ + abi: call.abi, + functionName: call.functionName, + args: call.args, + }) + : call.data + + return { + chainId: numberToHex(chainId), + data, + to: call.to, + value: call.value ? numberToHex(call.value) : undefined, + } + }) try { return await client.request( @@ -105,10 +123,7 @@ export async function sendCalls< method: 'wallet_sendCalls', params: [ { - calls: calls.map((call) => ({ - ...call, - value: call.value ? numberToHex(call.value) : undefined, - })) as any, + calls, capabilities, chainId: numberToHex(chain!.id), from: account.address, @@ -126,3 +141,74 @@ export async function sendCalls< }) } } + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +type RawCall = { data?: Hex; to?: Address; value?: bigint } + +type Call< + chain extends Chain | undefined = Chain | undefined, + contractFunctionParameters = Omit, +> = OneOf< + | (contractFunctionParameters & { + to: Address + value?: bigint | undefined + }) + | RawCall +> & + OneOf< + | MaybeRequired< + { chain?: Chain | undefined }, + chain extends Chain ? false : true + > + | MaybeRequired<{ chainId?: number }, chain extends Chain ? false : true> + > + +type Calls< + calls extends readonly unknown[], + chain extends Chain | undefined, + /// + 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< + Call< + chain, + Omit< + GetMulticallContractParameters, + 'address' + > + > + >, + ] + : calls extends readonly [infer call, ...infer rest] // grab first call and recurse through `rest` + ? Calls< + [...rest], + chain, + [ + ...result, + Prettify< + Call< + chain, + Omit< + GetMulticallContractParameters, + 'address' + > + > + >, + ] + > + : 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 Call< + chain, + Omit + >)[] + ? readonly Prettify[] + : // Fallback + readonly Call>[] diff --git a/src/experimental/eip5792/actions/writeContracts.test.ts b/src/experimental/eip5792/actions/writeContracts.test.ts index 9e254abadb..94ae7e88a9 100644 --- a/src/experimental/eip5792/actions/writeContracts.test.ts +++ b/src/experimental/eip5792/actions/writeContracts.test.ts @@ -139,16 +139,19 @@ test('default', async () => { { "calls": [ { + "chainId": "0x1", "data": "0x1249c58b", "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", "value": undefined, }, { + "chainId": "0x1", "data": "0x1249c58b", "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", "value": undefined, }, { + "chainId": "0x1", "data": "0x1249c58b", "to": "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2", "value": undefined, diff --git a/src/experimental/eip5792/actions/writeContracts.ts b/src/experimental/eip5792/actions/writeContracts.ts index d2889071d7..3f94f66c48 100644 --- a/src/experimental/eip5792/actions/writeContracts.ts +++ b/src/experimental/eip5792/actions/writeContracts.ts @@ -50,6 +50,8 @@ export type WriteContractsErrorType = | ErrorType /** + * @deprecated Use {@link sendCalls} instead. See https://viem.sh/experimental/eip5792/sendCalls#contract-calls. + * * Requests for the wallet to sign and broadcast a batch of write contract calls (transactions) to the network. * * - Docs: https://viem.sh/experimental/eip5792/writeContracts diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 2f8fd437d3..d45779a1b3 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -207,18 +207,15 @@ export type WalletSendCallsParameters< quantity extends Quantity | bigint = Quantity, > = [ { - calls: OneOf< - | { - to: Address - data?: Hex | undefined - value?: quantity | undefined - } - | { - data: Hex - } - >[] + calls: readonly { + chainId?: chainId | undefined + to?: Address | undefined + data?: Hex | undefined + value?: quantity | undefined + }[] capabilities?: capabilities | undefined - chainId: chainId + /** @deprecated Use `chainId` on `calls` instead. */ + chainId?: chainId | undefined from: Address version: string },