diff --git a/.changeset/shy-adults-cover.md b/.changeset/shy-adults-cover.md new file mode 100644 index 0000000000..a05e7cac1f --- /dev/null +++ b/.changeset/shy-adults-cover.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +**Experimental:** Added `forceAtomic` and `id` parameters to `sendCalls` diff --git a/.changeset/ten-pets-reply.md b/.changeset/ten-pets-reply.md new file mode 100644 index 0000000000..8274dbe879 --- /dev/null +++ b/.changeset/ten-pets-reply.md @@ -0,0 +1,48 @@ +--- +"viem": minor +--- + +**Breaking (Experimental):** Updated EIP-5792 to the latest spec changes. The following APIs have been updated: + +#### `getCallsStatus` + +```diff +const result = await client.getCallsStatus({ id }) +// ^? + { ++ atomic: boolean ++ chainId: number ++ id: string + receipts: Receipt[] +- status: 'PENDING' | 'CONFIRMED' ++ status: 'pending' | 'success' | 'failure' | undefined ++ statusCode: number ++ version: string +} +``` + +#### `sendCalls` + +```diff +const result = await client.sendCalls({ calls }) +// ^? +- string ++ { id: string, capabilities?: Capabilities } +``` + +#### `waitForCallsStatus` + +```diff +const result = await client.waitForCallsStatus({ id }) +// ^? + { ++ atomic: boolean ++ chainId: number ++ id: string + receipts: Receipt[] +- status: 'PENDING' | 'CONFIRMED' ++ status: 'pending' | 'success' | 'failure' | undefined ++ statusCode: number ++ version: string +} +``` \ No newline at end of file diff --git a/site/pages/experimental/eip5792/getCallsStatus.mdx b/site/pages/experimental/eip5792/getCallsStatus.mdx index f8902b8f49..d7bbf11289 100644 --- a/site/pages/experimental/eip5792/getCallsStatus.mdx +++ b/site/pages/experimental/eip5792/getCallsStatus.mdx @@ -1,12 +1,12 @@ --- -description: Returns the status and receipts of a call batch. +description: Returns the status of a call batch. --- # getCallsStatus -Returns the status and receipts of a call batch that was sent via `sendCalls`. +Returns the status of a call batch that was sent via `sendCalls`. -[Read more.](https://github.com/ethereum/EIPs/blob/1663ea2e7a683285f977eda51c32cec86553f585/EIPS/eip-5792.md#wallet_getcallsstatus) +[Read more.](https://eips.ethereum.org/EIPS/eip-5792#wallet_getcallsstatus) :::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. @@ -19,9 +19,17 @@ This is an experimental action that is not supported in most wallets. It is reco ```ts twoslash [example.ts] import { walletClient } from './config' -const { status, receipts } = await walletClient.getCallsStatus({ // [!code focus:99] +const result = await walletClient.getCallsStatus({ // [!code focus:99] id: '0x1234567890abcdef', }) +// @log: { +// @log: atomic: false, +// @log: chainId: 1, +// @log: id: '0x1234567890abcdef', +// @log: statusCode: 200, +// @log: status: 'success', +// @log: receipts: [{ ... }], +// @log: } ``` ```ts twoslash [config.ts] filename="config.ts" @@ -45,7 +53,7 @@ export const [account] = await walletClient.getAddresses() `WalletGetCallsStatusReturnType` -Status and receipts of the calls. +Status of the calls. ## Parameters diff --git a/site/pages/experimental/eip5792/getCapabilities.mdx b/site/pages/experimental/eip5792/getCapabilities.mdx index 50b40b5eac..09bfe64546 100644 --- a/site/pages/experimental/eip5792/getCapabilities.mdx +++ b/site/pages/experimental/eip5792/getCapabilities.mdx @@ -108,4 +108,19 @@ import { walletClient } from './config' const capabilities = await walletClient.getCapabilities({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] }) +``` + +### chainId + +- **Type:** `number` + +The chain ID to get capabilities for. + +```ts twoslash [example.ts] +import { walletClient } from './config' +// ---cut--- +const capabilities = await walletClient.getCapabilities({ + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + chainId: 8453, // [!code focus] +}) ``` \ No newline at end of file diff --git a/site/pages/experimental/eip5792/sendCalls.mdx b/site/pages/experimental/eip5792/sendCalls.mdx index 6e0cc258f3..95185fae78 100644 --- a/site/pages/experimental/eip5792/sendCalls.mdx +++ b/site/pages/experimental/eip5792/sendCalls.mdx @@ -4,9 +4,9 @@ description: Sign and broadcast a batch of calls to the network. # sendCalls -Requests for the wallet to sign and broadcast a batch of calls (transactions) to the network. +Requests for the wallet to sign and broadcast a batch of calls to the network. -[Read more.](https://github.com/ethereum/EIPs/blob/815028dc634463e1716fc5ce44c019a6040f0bef/EIPS/eip-5792.md#wallet_sendcalls) +[Read more.](https://eips.ethereum.org/EIPS/eip-5792#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. @@ -20,7 +20,7 @@ This is an experimental action that is not supported in most wallets. It is reco import { parseEther } from 'viem' import { account, walletClient } from './config' -const id = await walletClient.sendCalls({ // [!code focus:99] +const { id } = await walletClient.sendCalls({ // [!code focus:99] account, calls: [ { @@ -56,7 +56,7 @@ Notes: - `account` and `chain` are top level properties as all calls should be sent by the same account and chain. - Properties of `calls` items are only those shared by all transaction types (e.g. `data`, `to`, `value`). The Wallet should handle other required properties like gas & fees. -- [Read `wallet_sendCalls` on EIP-5792.](https://github.com/ethereum/EIPs/blob/815028dc634463e1716fc5ce44c019a6040f0bef/EIPS/eip-5792.md#wallet_sendcalls) +- [Read `wallet_sendCalls` on EIP-5792.](https://eips.ethereum.org/EIPS/eip-5792#wallet_sendcalls) ### Account Hoisting @@ -69,7 +69,7 @@ If you do not wish to pass an `account` to every `sendCalls`, you can also hoist ```ts twoslash [example.ts] import { walletClient } from './config' -const id = await walletClient.sendCalls({ // [!code focus:99] +const { id } = await walletClient.sendCalls({ // [!code focus:99] calls: [ { to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', @@ -116,7 +116,7 @@ const abi = parseAbi([ 'function transferFrom(address, address, uint256) returns (bool)', ]) -const id = await walletClient.sendCalls({ // [!code focus:99] +const { id } = await walletClient.sendCalls({ // [!code focus:99] calls: [ { to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', @@ -179,7 +179,7 @@ export const walletClient = createWalletClient({ ## Returns -`string` +`{ id: string, capabilities?: WalletCapabilities }` 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. @@ -196,7 +196,7 @@ Accepts a [JSON-RPC Account](/docs/clients/wallet#json-rpc-accounts). ```ts twoslash import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code focus] calls: [ { @@ -222,7 +222,7 @@ The target chain to broadcast the calls. import { mainnet } from 'viem/chains' import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ chain: mainnet, // [!code focus] calls: [ { @@ -247,7 +247,7 @@ An array of calls to be signed and broadcasted. import { mainnet } from 'viem/chains' import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ chain: mainnet, calls: [ // [!code focus] { // [!code focus] @@ -272,7 +272,7 @@ Calldata to broadcast (typically a contract function selector with encoded argum import { mainnet } from 'viem/chains' import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ chain: mainnet, calls: [ { @@ -297,7 +297,7 @@ Recipient address of the call. import { mainnet } from 'viem/chains' import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ chain: mainnet, calls: [ { @@ -322,7 +322,7 @@ Value to send with the call. import { mainnet } from 'viem/chains' import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ chain: mainnet, calls: [ { @@ -346,7 +346,7 @@ Capability metadata for the calls (e.g. specifying a paymaster). ```ts twoslash import { walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ calls: [ { to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', @@ -364,3 +364,52 @@ const id = await walletClient.sendCalls({ } // [!code focus] }) ``` + +### forceAtomic + +- **Type:** `boolean` +- **Default:** `false` + +Force the calls to be executed atomically. [See more](https://eips.ethereum.org/EIPS/eip-5792#call-execution-atomicity) + +```ts twoslash +import { walletClient } from './config' + +const { id } = await walletClient.sendCalls({ + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + forceAtomic: true, // [!code focus] +}) +``` + +### id + +- **Type:** `string` + +Attribute the call batch with an identifier. + +```ts twoslash +import { walletClient } from './config' + +const { id } = await walletClient.sendCalls({ + calls: [ + { + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: parseEther('1') + }, + { + data: '0xdeadbeef', + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + ], + id: '', // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/pages/experimental/eip5792/waitForCallsStatus.mdx b/site/pages/experimental/eip5792/waitForCallsStatus.mdx index 1637cc768a..892134dd7f 100644 --- a/site/pages/experimental/eip5792/waitForCallsStatus.mdx +++ b/site/pages/experimental/eip5792/waitForCallsStatus.mdx @@ -1,10 +1,10 @@ --- -description: Waits for a call bundle to be confirmed & included on a Block. +description: Waits for a call batch to be confirmed & included on a Block. --- # waitForCallsStatus -Waits for a call bundle to be confirmed & included on a [Block](/docs/glossary/terms#block) before returning the status & receipts. +Waits for a call batch to be confirmed & included on a [Block](/docs/glossary/terms#block) before returning the status & receipts. :::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. @@ -18,7 +18,7 @@ This is an experimental action that is not supported in most wallets. It is reco import { parseEther } from 'viem' import { account, walletClient } from './config' -const id = await walletClient.sendCalls({ +const { id } = await walletClient.sendCalls({ account, calls: [{ to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', @@ -26,9 +26,17 @@ const id = await walletClient.sendCalls({ }], }) -const { status, receipts } = await walletClient.waitForCallsStatus({ // [!code focus] +const result = await walletClient.waitForCallsStatus({ // [!code focus] id, // [!code focus] }) // [!code focus] +// @log: { +// @log: atomic: false, +// @log: chainId: 1, +// @log: id: '0x1234567890abcdef', +// @log: statusCode: 200, +// @log: status: 'success', +// @log: receipts: [{ ... }], +// @log: } ``` ```ts twoslash [config.ts] filename="config.ts" @@ -63,7 +71,7 @@ Status and receipts of the calls. Identifier of the call batch. ```ts -const { status, receipts } = await walletClient.waitForCallsStatus({ +const result = await walletClient.waitForCallsStatus({ id: '0xdeadbeef', // [!code focus] }) ``` @@ -76,12 +84,26 @@ const { status, receipts } = await walletClient.waitForCallsStatus({ Polling interval in milliseconds. ```ts -const { status, receipts } = await walletClient.waitForCallsStatus({ +const result = await walletClient.waitForCallsStatus({ id: '0xdeadbeef', pollingInterval: 1_000, // [!code focus] }) ``` +### status + +- **Type:** `(parameters: { statusCode: number, status: string | undefined }) => boolean` +- **Default:** `(parameters) => parameters.statusCode >= 200` + +Status to wait for. Defaults to non-pending status codes (`>=200`). + +```ts +const result = await walletClient.waitForCallsStatus({ + id: '0xdeadbeef', + status: ({ status }) => status === 'success', // [!code focus] +}) +``` + ### timeout - **Type:** `number` @@ -90,7 +112,7 @@ const { status, receipts } = await walletClient.waitForCallsStatus({ Timeout in milliseconds before `waitForCallsStatus` stops polling. ```ts -const { status, receipts } = await walletClient.waitForCallsStatus({ +const result = await walletClient.waitForCallsStatus({ id: '0xdeadbeef', timeout: 10_000, // [!code focus] }) diff --git a/src/experimental/eip5792/actions/getCallsStatus.test.ts b/src/experimental/eip5792/actions/getCallsStatus.test.ts index c684a75495..f8b86b678e 100644 --- a/src/experimental/eip5792/actions/getCallsStatus.test.ts +++ b/src/experimental/eip5792/actions/getCallsStatus.test.ts @@ -6,7 +6,10 @@ 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 { WalletCallReceipt } from '../../../types/eip1193.js' +import type { + WalletCallReceipt, + WalletGetCallsStatusReturnType, +} from '../../../types/eip1193.js' import type { Hex } from '../../../types/misc.js' import { getHttpRpcClient, parseEther } from '../../../utils/index.js' import { uid } from '../../../utils/uid.js' @@ -57,7 +60,14 @@ const getClient = ({ } satisfies WalletCallReceipt }), ) - return { status: 'CONFIRMED', receipts } + return { + atomic: false, + chainId: '0x1', + id: params[0], + receipts, + status: 200, + version: '1.0', + } satisfies WalletGetCallsStatusReturnType } if (method === 'wallet_sendCalls') { @@ -97,7 +107,7 @@ test('default', async () => { }, }) - const id = await sendCalls(client, { + const { id } = await sendCalls(client, { account: accounts[0].address, calls: [ { @@ -120,7 +130,16 @@ test('default', async () => { await mine(testClient, { blocks: 1 }) - const { status, receipts } = await getCallsStatus(client, { id }) - expect(status).toMatchInlineSnapshot(`"CONFIRMED"`) + const { id: id_, receipts, ...rest } = await getCallsStatus(client, { id }) + expect(id_).toBeDefined() + expect(rest).toMatchInlineSnapshot(` + { + "atomic": false, + "chainId": 1, + "status": "success", + "statusCode": 200, + "version": "1.0", + } + `) expect(receipts!.length).toBe(3) }) diff --git a/src/experimental/eip5792/actions/getCallsStatus.ts b/src/experimental/eip5792/actions/getCallsStatus.ts index a788f3ea1b..bdc4ae53ae 100644 --- a/src/experimental/eip5792/actions/getCallsStatus.ts +++ b/src/experimental/eip5792/actions/getCallsStatus.ts @@ -3,16 +3,30 @@ 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 { + WalletCapabilities, + 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' +import { hexToBigInt, hexToNumber } from '../../../utils/encoding/fromHex.js' import { receiptStatuses } from '../../../utils/formatters/transactionReceipt.js' export type GetCallsStatusParameters = { id: string } export type GetCallsStatusReturnType = Prettify< - WalletGetCallsStatusReturnType + Omit< + WalletGetCallsStatusReturnType< + WalletCapabilities, + number, + bigint, + 'success' | 'reverted' + >, + 'status' + > & { + statusCode: number + status: 'pending' | 'success' | 'failure' | undefined + } > export type GetCallsStatusErrorType = RequestErrorType | ErrorType @@ -44,13 +58,35 @@ export async function getCallsStatus< client: Client, parameters: GetCallsStatusParameters, ): Promise { - const { id } = parameters - const { receipts, status } = await client.request({ + const { + atomic = false, + chainId, + receipts, + version = '1.0', + ...response + } = await client.request({ method: 'wallet_getCallsStatus', - params: [id], + params: [parameters.id], }) + const [status, statusCode] = (() => { + const statusCode = response.status + if (statusCode >= 100 && statusCode < 200) + return ['pending', statusCode] as const + if (statusCode >= 200 && statusCode < 300) + return ['success', statusCode] as const + if (statusCode >= 400 && statusCode < 700) + return ['failure', statusCode] as const + // @ts-expect-error: for backwards compatibility + if (statusCode === 'CONFIRMED') return ['CONFIRMED' as never, 200] as const + // @ts-expect-error: for backwards compatibility + if (statusCode === 'PENDING') return ['PENDING' as never, 100] as const + return [undefined, statusCode] + })() return { - status, + ...response, + atomic, + // @ts-expect-error: for backwards compatibility + chainId: chainId ? hexToNumber(chainId) : undefined, receipts: receipts?.map((receipt) => ({ ...receipt, @@ -58,5 +94,8 @@ export async function getCallsStatus< gasUsed: hexToBigInt(receipt.gasUsed), status: receiptStatuses[receipt.status as '0x0' | '0x1'], })) ?? [], + statusCode, + status, + version, } } diff --git a/src/experimental/eip5792/actions/getCapabilities.test.ts b/src/experimental/eip5792/actions/getCapabilities.test.ts index facdfd1e0a..a7c1dc7be1 100644 --- a/src/experimental/eip5792/actions/getCapabilities.test.ts +++ b/src/experimental/eip5792/actions/getCapabilities.test.ts @@ -34,6 +34,27 @@ const client = createClient({ }) test('default', async () => { + const capabilities = await getCapabilities(client) + expect(capabilities).toMatchInlineSnapshot(` + { + "8453": { + "paymasterService": { + "supported": false, + }, + "sessionKeys": { + "supported": true, + }, + }, + "84532": { + "paymasterService": { + "supported": false, + }, + }, + } + `) +}) + +test('args: account', async () => { const capabilities = await getCapabilities(client, { account: accounts[0].address, }) @@ -56,7 +77,24 @@ test('default', async () => { `) }) -test('account on client', async () => { +test('args: chainId', async () => { + const capabilities = await getCapabilities(client, { + account: accounts[0].address, + chainId: 8453, + }) + expect(capabilities).toMatchInlineSnapshot(` + { + "paymasterService": { + "supported": true, + }, + "sessionKeys": { + "supported": true, + }, + } + `) +}) + +test('behavior: account on client', async () => { const client_2 = { ...client, account: accounts[1].address, diff --git a/src/experimental/eip5792/actions/getCapabilities.ts b/src/experimental/eip5792/actions/getCapabilities.ts index a8b898fffe..b7a8923718 100644 --- a/src/experimental/eip5792/actions/getCapabilities.ts +++ b/src/experimental/eip5792/actions/getCapabilities.ts @@ -3,10 +3,8 @@ import type { Address } from 'abitype' import { parseAccount } from '../../../accounts/utils/parseAccount.js' import type { Client } from '../../../clients/createClient.js' import type { Transport } from '../../../clients/transports/createTransport.js' -import { AccountNotFoundError } from '../../../errors/account.js' import type { ErrorType } from '../../../errors/utils.js' import type { Account } from '../../../types/account.js' -import type { Chain } from '../../../types/chain.js' import type { WalletCapabilities, WalletCapabilitiesRecord, @@ -14,12 +12,19 @@ import type { import type { Prettify } from '../../../types/utils.js' import type { RequestErrorType } from '../../../utils/buildRequest.js' -export type GetCapabilitiesParameters = { +export type GetCapabilitiesParameters< + chainId extends number | undefined = undefined, +> = { account?: Account | Address | undefined + chainId?: chainId | number | undefined } -export type GetCapabilitiesReturnType = Prettify< - WalletCapabilitiesRecord +export type GetCapabilitiesReturnType< + chainId extends number | undefined = undefined, +> = Prettify< + chainId extends number + ? WalletCapabilities + : WalletCapabilitiesRecord > export type GetCapabilitiesErrorType = RequestErrorType | ErrorType @@ -44,18 +49,19 @@ export type GetCapabilitiesErrorType = RequestErrorType | ErrorType * }) * const capabilities = await getCapabilities(client) */ -export async function getCapabilities( - client: Client, - parameters: GetCapabilitiesParameters = {}, -): Promise { - const account_raw = parameters?.account ?? client.account +export async function getCapabilities< + chainId extends number | undefined = undefined, +>( + client: Client, + parameters: GetCapabilitiesParameters = {}, +): Promise> { + const { account = client.account, chainId } = parameters - if (!account_raw) throw new AccountNotFoundError() - const account = parseAccount(account_raw) + const account_ = account ? parseAccount(account) : undefined const capabilities_raw = await client.request({ method: 'wallet_getCapabilities', - params: [account.address], + params: [account_?.address], }) const capabilities = {} as WalletCapabilitiesRecord< @@ -64,5 +70,7 @@ export async function getCapabilities( > for (const [key, value] of Object.entries(capabilities_raw)) capabilities[Number(key)] = value - return capabilities + return ( + typeof chainId === 'number' ? capabilities[chainId] : capabilities + ) as never } diff --git a/src/experimental/eip5792/actions/sendCalls.test.ts b/src/experimental/eip5792/actions/sendCalls.test.ts index 51567b5883..996d1fe38d 100644 --- a/src/experimental/eip5792/actions/sendCalls.test.ts +++ b/src/experimental/eip5792/actions/sendCalls.test.ts @@ -8,7 +8,10 @@ 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 { + WalletCallReceipt, + WalletGetCallsStatusReturnType, +} from '../../../types/eip1193.js' import type { Hex } from '../../../types/misc.js' import { getHttpRpcClient, parseEther } from '../../../utils/index.js' import { uid } from '../../../utils/uid.js' @@ -37,7 +40,16 @@ const getClient = ({ if (method === 'wallet_getCallsStatus') { const hashes = calls.get(params[0]) - if (!hashes) return { status: 'PENDING', receipts: [] } + if (!hashes) + return { + atomic: false, + chainId: '0x1', + id: params[0], + status: 100, + receipts: [], + version: '1.0', + } satisfies WalletGetCallsStatusReturnType + const receipts = await Promise.all( hashes.map(async (hash) => { const { result, error } = await rpcClient.request({ @@ -64,7 +76,14 @@ const getClient = ({ } satisfies WalletCallReceipt }), ) - return { status: 'CONFIRMED', receipts } + return { + atomic: false, + chainId: '0x1', + id: params[0], + status: 200, + receipts, + version: '1.0', + } satisfies WalletGetCallsStatusReturnType } if (method === 'wallet_sendCalls') { @@ -122,7 +141,7 @@ test('default', async () => { jsonRpcUrl: anvilMainnet.forkUrl, }) - const id_ = await sendCalls(client, { + const response = await sendCalls(client, { account: accounts[0].address, chain: mainnet, calls: [ @@ -151,11 +170,12 @@ test('default', async () => { ], }) - expect(id_).toBeDefined() + expect(response.id).toBeDefined() expect(requests).toMatchInlineSnapshot(` [ [ { + "atomicRequired": false, "calls": [ { "data": undefined, @@ -186,6 +206,7 @@ test('default', async () => { "capabilities": undefined, "chainId": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "id": undefined, "version": "1.0", }, ], @@ -208,7 +229,7 @@ test('behavior: chain on client', async () => { jsonRpcUrl: anvilMainnet.forkUrl, }) - const id_ = await sendCalls(client, { + const { id } = await sendCalls(client, { account: accounts[0].address, calls: [ { @@ -236,11 +257,12 @@ test('behavior: chain on client', async () => { ], }) - expect(id_).toBeDefined() + expect(id).toBeDefined() expect(requests).toMatchInlineSnapshot(` [ [ { + "atomicRequired": false, "calls": [ { "data": undefined, @@ -271,6 +293,7 @@ test('behavior: chain on client', async () => { "capabilities": undefined, "chainId": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "id": undefined, "version": "1.0", }, ], @@ -292,7 +315,7 @@ test('behavior: inferred account', async () => { jsonRpcUrl: anvilMainnet.forkUrl, }) - const id_ = await sendCalls(client, { + const { id } = await sendCalls(client, { account: null, chain: mainnet, calls: [ @@ -321,11 +344,12 @@ test('behavior: inferred account', async () => { ], }) - expect(id_).toBeDefined() + expect(id).toBeDefined() expect(requests).toMatchInlineSnapshot(` [ [ { + "atomicRequired": false, "calls": [ { "data": undefined, @@ -356,6 +380,7 @@ test('behavior: inferred account', async () => { "capabilities": undefined, "chainId": "0x1", "from": undefined, + "id": undefined, "version": "1.0", }, ], diff --git a/src/experimental/eip5792/actions/sendCalls.ts b/src/experimental/eip5792/actions/sendCalls.ts index 3206565903..0ea00e24ff 100644 --- a/src/experimental/eip5792/actions/sendCalls.ts +++ b/src/experimental/eip5792/actions/sendCalls.ts @@ -27,13 +27,16 @@ export type SendCallsParameters< > = { chain?: chainOverride | Chain | undefined calls: Calls> - capabilities?: - | WalletSendCallsParameters[number]['capabilities'] - | undefined + capabilities?: WalletCapabilities | undefined + forceAtomic?: boolean | undefined + id?: string | undefined version?: WalletSendCallsParameters[number]['version'] | undefined } & GetAccountParameter -export type SendCallsReturnType = string +export type SendCallsReturnType = { + capabilities?: WalletCapabilities | undefined + id: string +} export type SendCallsErrorType = RequestErrorType | ErrorType @@ -82,6 +85,8 @@ export async function sendCalls< account: account_ = client.account, capabilities, chain = client.chain, + forceAtomic = false, + id, version = '1.0', } = parameters @@ -110,21 +115,25 @@ export async function sendCalls< }) try { - return await client.request( + const response = await client.request( { method: 'wallet_sendCalls', params: [ { + atomicRequired: forceAtomic, calls, capabilities, chainId: numberToHex(chain!.id), from: account?.address, + id, version, }, ], }, { retryCount: 0 }, ) + if (typeof response === 'string') return { id: response } + return response } catch (err) { throw getTransactionError(err as BaseError, { ...parameters, diff --git a/src/experimental/eip5792/actions/waitForCallsStatus.test.ts b/src/experimental/eip5792/actions/waitForCallsStatus.test.ts index 4bf02b1c3e..4c737a8e06 100644 --- a/src/experimental/eip5792/actions/waitForCallsStatus.test.ts +++ b/src/experimental/eip5792/actions/waitForCallsStatus.test.ts @@ -6,7 +6,10 @@ 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 { WalletCallReceipt } from '../../../types/eip1193.js' +import type { + WalletCallReceipt, + WalletGetCallsStatusReturnType, +} from '../../../types/eip1193.js' import type { Hex } from '../../../types/misc.js' import { getHttpRpcClient, parseEther } from '../../../utils/index.js' import { uid } from '../../../utils/uid.js' @@ -32,7 +35,16 @@ const getClient = ({ if (method === 'wallet_getCallsStatus') { const hashes = calls.get(params[0]) - if (!hashes) return { status: 'PENDING', receipts: [] } + if (!hashes) + return { + atomic: false, + chainId: '0x1', + id: params[0], + receipts: [], + status: 100, + version: '1.0', + } satisfies WalletGetCallsStatusReturnType + const receipts = await Promise.all( hashes.map(async (hash) => { const { result, error } = await rpcClient.request({ @@ -58,7 +70,14 @@ const getClient = ({ } satisfies WalletCallReceipt }), ) - return { status: 'CONFIRMED', receipts } + return { + atomic: false, + chainId: '0x1', + id: params[0], + receipts, + status: 200, + version: '1.0', + } satisfies WalletGetCallsStatusReturnType } if (method === 'wallet_sendCalls') { @@ -100,7 +119,7 @@ test('default', async () => { }, }) - const id = await sendCalls(client, { + const { id } = await sendCalls(client, { account: accounts[0].address, calls: [ { @@ -123,15 +142,30 @@ test('default', async () => { await mine(testClient, { blocks: 1 }) - const { status, receipts } = await waitForCallsStatus(client, { id }) - expect(status).toMatchInlineSnapshot(`"CONFIRMED"`) + const { + id: id_, + receipts, + ...rest + } = await waitForCallsStatus(client, { + id, + }) + expect(id_).toBeDefined() + expect(rest).toMatchInlineSnapshot(` + { + "atomic": false, + "chainId": 1, + "status": "success", + "statusCode": 200, + "version": "1.0", + } + `) expect(receipts!.length).toBe(3) }) test('behavior: timeout exceeded', async () => { const client = getClient() - const id = await sendCalls(client, { + const { id } = await sendCalls(client, { account: accounts[0].address, calls: [ { @@ -175,7 +209,7 @@ test('behavior: `wallet_getCallsStatus` failure', async () => { }, }) - const id = await sendCalls(client, { + const { id } = await sendCalls(client, { account: accounts[0].address, calls: [ { diff --git a/src/experimental/eip5792/actions/waitForCallsStatus.ts b/src/experimental/eip5792/actions/waitForCallsStatus.ts index 42e6810e14..7c6dee1fc8 100644 --- a/src/experimental/eip5792/actions/waitForCallsStatus.ts +++ b/src/experimental/eip5792/actions/waitForCallsStatus.ts @@ -3,14 +3,13 @@ import type { Transport } from '../../../clients/transports/createTransport.js' import { BaseError } from '../../../errors/base.js' import type { ErrorType } from '../../../errors/utils.js' import type { Chain } from '../../../types/chain.js' -import type { WalletGetCallsStatusReturnType } from '../../../types/eip1193.js' -import type { Prettify } from '../../../types/utils.js' import { type ObserveErrorType, observe } from '../../../utils/observe.js' import { type PollErrorType, poll } from '../../../utils/poll.js' import { withResolvers } from '../../../utils/promise/withResolvers.js' import { stringify } from '../../../utils/stringify.js' import { type GetCallsStatusErrorType, + type GetCallsStatusReturnType, getCallsStatus, } from './getCallsStatus.js' @@ -26,11 +25,11 @@ export type WaitForCallsStatusParameters = { */ pollingInterval?: number | undefined /** - * The status to wait for. + * The status range to wait for. * - * @default 'CONFIRMED' + * @default (status) => status >= 200 */ - status?: 'CONFIRMED' | undefined + status?: ((parameters: GetCallsStatusReturnType) => boolean) | undefined /** * Optional timeout (in milliseconds) to wait before stopping polling. * @@ -39,9 +38,7 @@ export type WaitForCallsStatusParameters = { timeout?: number | undefined } -export type WaitForCallsStatusReturnType = Prettify< - WalletGetCallsStatusReturnType -> +export type WaitForCallsStatusReturnType = GetCallsStatusReturnType export type WaitForCallsStatusErrorType = | ObserveErrorType @@ -79,7 +76,7 @@ export async function waitForCallsStatus( const { id, pollingInterval = client.pollingInterval, - status = 'CONFIRMED', + status = ({ statusCode }) => statusCode >= 200, timeout = 60_000, } = parameters const observerId = stringify(['waitForCallsStatus', client.uid, id]) @@ -92,15 +89,19 @@ export async function waitForCallsStatus( const unobserve = observe(observerId, { resolve, reject }, (emit) => { const unpoll = poll( async () => { + const done = (fn: () => void) => { + clearTimeout(timer) + unpoll() + fn() + unobserve() + } + try { const result = await getCallsStatus(client, { id }) - if (result.status !== status) return - emit.resolve(result) + if (!status(result)) return + done(() => emit.resolve(result)) } catch (error) { - if (timer) clearTimeout(timer) - unpoll() - emit.reject(error) - unobserve() + done(() => emit.reject(error)) } }, { diff --git a/src/experimental/eip5792/actions/writeContracts.test.ts b/src/experimental/eip5792/actions/writeContracts.test.ts index 22315eac96..444059119b 100644 --- a/src/experimental/eip5792/actions/writeContracts.test.ts +++ b/src/experimental/eip5792/actions/writeContracts.test.ts @@ -113,7 +113,7 @@ test('default', async () => { jsonRpcUrl: anvilMainnet.forkUrl, }) - const id_ = await writeContracts(client, { + const { id: id_ } = await writeContracts(client, { account: accounts[0].address, chain: mainnet, contracts: [ @@ -137,6 +137,7 @@ test('default', async () => { [ [ { + "atomicRequired": false, "calls": [ { "data": "0x1249c58b", @@ -157,6 +158,7 @@ test('default', async () => { "capabilities": undefined, "chainId": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "id": undefined, "version": "1.0", }, ], diff --git a/src/experimental/eip5792/decorators/eip5792.test.ts b/src/experimental/eip5792/decorators/eip5792.test.ts index 2c7609cc5e..0fb36c42c7 100644 --- a/src/experimental/eip5792/decorators/eip5792.test.ts +++ b/src/experimental/eip5792/decorators/eip5792.test.ts @@ -88,6 +88,8 @@ describe('smoke test', () => { test('getCallsStatus', async () => { expect(await client.getCallsStatus({ id: '0x123' })).toMatchInlineSnapshot(` { + "atomic": false, + "chainId": undefined, "receipts": [ { "blockHash": "0x66a7b39a0c4635c2f30cd191d7e1fb0bd370c11dd93199f236c5bdacfc9136b3", @@ -99,6 +101,8 @@ describe('smoke test', () => { }, ], "status": "CONFIRMED", + "statusCode": 200, + "version": "1.0", } `) }) @@ -110,7 +114,11 @@ describe('smoke test', () => { calls: [{ to: '0x0000000000000000000000000000000000000000' }], chain: mainnet, }), - ).toMatchInlineSnapshot(`"0x1"`) + ).toMatchInlineSnapshot(` + { + "id": "0x1", + } + `) }) test('writeContracts', async () => { @@ -124,6 +132,10 @@ describe('smoke test', () => { { ...wagmiContractConfig, functionName: 'mint' }, ], }), - ).toMatchInlineSnapshot(`"0x1"`) + ).toMatchInlineSnapshot(` + { + "id": "0x1", + } + `) }) }) diff --git a/src/index.ts b/src/index.ts index c9aef1461c..22bc53cacf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1098,6 +1098,7 @@ export type { WalletGrantPermissionsParameters, WalletGrantPermissionsReturnType, WalletSendCallsParameters, + WalletSendCallsReturnType, WalletPermissionCaveat, WalletPermission, WalletRpcSchema, diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index bc00e02189..e4d2496deb 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -186,9 +186,19 @@ export type WalletGrantPermissionsReturnType = { | undefined } -export type WalletGetCallsStatusReturnType = { - status: 'PENDING' | 'CONFIRMED' - receipts?: WalletCallReceipt[] | undefined +export type WalletGetCallsStatusReturnType< + capabilities extends WalletCapabilities = WalletCapabilities, + numberType = Hex, + bigintType = Hex, + receiptStatus = Hex, +> = { + atomic: boolean + capabilities?: capabilities | WalletCapabilities | undefined + chainId: numberType + id: string + receipts?: WalletCallReceipt[] | undefined + status: number + version: string } export type WalletPermissionCaveat = { @@ -210,18 +220,28 @@ export type WalletSendCallsParameters< quantity extends Quantity | bigint = Quantity, > = [ { + atomicRequired: boolean calls: readonly { + capabilities?: capabilities | WalletCapabilities | undefined to?: Address | undefined data?: Hex | undefined value?: quantity | undefined }[] - capabilities?: capabilities | undefined + capabilities?: capabilities | WalletCapabilities | undefined chainId?: chainId | undefined + id?: string | undefined from?: Address | undefined version: string }, ] +export type WalletSendCallsReturnType< + capabilities extends WalletCapabilities = WalletCapabilities, +> = { + capabilities?: capabilities | undefined + id: string +} + export type WatchAssetParams = { /** Token type. */ type: 'ERC20' @@ -1775,7 +1795,7 @@ export type WalletRpcSchema = [ */ { Method: 'wallet_getCapabilities' - Parameters?: [Address] + Parameters?: [Address | undefined] | undefined ReturnType: Prettify }, /** @@ -1836,7 +1856,7 @@ export type WalletRpcSchema = [ { Method: 'wallet_sendCalls' Parameters?: WalletSendCallsParameters - ReturnType: string + ReturnType: WalletSendCallsReturnType }, /** * @description Creates, signs, and sends a new transaction to the network