diff --git a/apps/laboratory/src/components/Ethers/EthersGetCallsStatusTest.tsx b/apps/laboratory/src/components/Ethers/EthersGetCallsStatusTest.tsx
new file mode 100644
index 0000000000..7ba27c16bf
--- /dev/null
+++ b/apps/laboratory/src/components/Ethers/EthersGetCallsStatusTest.tsx
@@ -0,0 +1,93 @@
+import { Button, Stack, Text, Input } from '@chakra-ui/react'
+import { useState } from 'react'
+import { useWeb3ModalAccount, useWeb3ModalProvider } from '@web3modal/ethers/react'
+import { EthereumProvider } from '@walletconnect/ethereum-provider'
+import { useChakraToast } from '../Toast'
+import { BrowserProvider } from 'ethers'
+import { type GetCallsStatusParams } from '../../types/EIP5792'
+import { EIP_5792_RPC_METHODS } from '../../utils/EIP5792Utils'
+
+export function EthersGetCallsStatusTest() {
+ const [isLoading, setLoading] = useState(false)
+ const [batchCallId, setBatchCallId] = useState('')
+
+ const { address, chainId, isConnected } = useWeb3ModalAccount()
+ const { walletProvider } = useWeb3ModalProvider()
+ const toast = useChakraToast()
+
+ async function onGetCallsStatus() {
+ try {
+ setLoading(true)
+ if (!walletProvider || !address) {
+ throw Error('user is disconnected')
+ }
+ if (!chainId) {
+ throw Error('chain not selected')
+ }
+ if (!batchCallId) {
+ throw Error('call id not valid')
+ }
+ const provider = new BrowserProvider(walletProvider, chainId)
+ const batchCallsStatus = await provider.send(EIP_5792_RPC_METHODS.WALLET_GET_CALLS_STATUS, [
+ batchCallId as GetCallsStatusParams
+ ])
+ toast({
+ title: 'Success',
+ description: JSON.stringify(batchCallsStatus),
+ type: 'success'
+ })
+ } catch {
+ toast({
+ title: 'Error',
+ description: 'Failed to get call status',
+ type: 'error'
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+ function isGetCallsStatusSupported(): boolean {
+ if (walletProvider instanceof EthereumProvider) {
+ return Boolean(
+ walletProvider?.signer?.session?.namespaces?.['eip155']?.methods?.includes(
+ EIP_5792_RPC_METHODS.WALLET_GET_CALLS_STATUS
+ )
+ )
+ }
+
+ return false
+ }
+
+ if (!isConnected || !address || !walletProvider) {
+ return (
+
+ Wallet not connected
+
+ )
+ }
+ if (!isGetCallsStatusSupported()) {
+ return (
+
+ Wallet does not support wallet_getCallsStatus rpc method
+
+ )
+ }
+
+ return (
+
+ setBatchCallId(e.target.value)}
+ value={batchCallId}
+ isDisabled={isLoading}
+ />
+
+
+ )
+}
diff --git a/apps/laboratory/src/components/Ethers/EthersSendCallsTest.tsx b/apps/laboratory/src/components/Ethers/EthersSendCallsTest.tsx
new file mode 100644
index 0000000000..d6175d2cb2
--- /dev/null
+++ b/apps/laboratory/src/components/Ethers/EthersSendCallsTest.tsx
@@ -0,0 +1,127 @@
+import { Button, Stack, Text, Spacer } from '@chakra-ui/react'
+import { useState } from 'react'
+import { useWeb3ModalAccount, useWeb3ModalProvider } from '@web3modal/ethers/react'
+import { EthereumProvider } from '@walletconnect/ethereum-provider'
+import { useChakraToast } from '../Toast'
+import { parseGwei, type Address } from 'viem'
+import { vitalikEthAddress } from '../../utils/DataUtil'
+import { BrowserProvider } from 'ethers'
+import {
+ EIP_5792_RPC_METHODS,
+ WALLET_CAPABILITIES,
+ getCapabilitySupportedChainInfo
+} from '../../utils/EIP5792Utils'
+
+export function EthersSendCallsTest() {
+ const [loading, setLoading] = useState(false)
+
+ const { address, chainId, isConnected } = useWeb3ModalAccount()
+ const { walletProvider } = useWeb3ModalProvider()
+ const toast = useChakraToast()
+
+ const atomicBatchSupportedChains =
+ address && walletProvider instanceof EthereumProvider
+ ? getCapabilitySupportedChainInfo(WALLET_CAPABILITIES.ATOMIC_BATCH, walletProvider, address)
+ : []
+
+ const atomicBatchSupportedChainNames = atomicBatchSupportedChains
+ .map(ci => ci.chainName)
+ .join(', ')
+ const currentChainsInfo = atomicBatchSupportedChains.find(
+ chainInfo => chainInfo.chainId === Number(chainId)
+ )
+
+ async function onSendCalls() {
+ try {
+ setLoading(true)
+ if (!walletProvider || !address) {
+ throw Error('user is disconnected')
+ }
+ if (!chainId) {
+ throw Error('chain not selected')
+ }
+ const provider = new BrowserProvider(walletProvider, chainId)
+ const amountToSend = parseGwei('0.001').toString(16)
+ const calls = [
+ {
+ to: vitalikEthAddress as `0x${string}`,
+ data: '0x' as `0x${string}`,
+ value: `0x${amountToSend}`
+ },
+ {
+ to: vitalikEthAddress as Address,
+ value: '0x00',
+ data: '0xdeadbeef'
+ }
+ ]
+ const sendCallsParams = {
+ version: '1.0',
+ chainId: `0x${BigInt(chainId).toString(16)}`,
+ from: address,
+ calls
+ }
+ const batchCallHash = await provider.send(EIP_5792_RPC_METHODS.WALLET_SEND_CALLS, [
+ sendCallsParams
+ ])
+ toast({
+ title: 'Success',
+ description: batchCallHash,
+ type: 'success'
+ })
+ } catch {
+ toast({
+ title: 'Error',
+ description: 'Failed to send calls',
+ type: 'error'
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+ function isSendCallsSupported(): boolean {
+ if (walletProvider instanceof EthereumProvider) {
+ return Boolean(
+ walletProvider?.signer?.session?.namespaces?.['eip155']?.methods?.includes(
+ EIP_5792_RPC_METHODS.WALLET_SEND_CALLS
+ )
+ )
+ }
+
+ return false
+ }
+
+ if (!isConnected || !walletProvider || !address) {
+ return (
+
+ Wallet not connected
+
+ )
+ }
+ if (!isSendCallsSupported()) {
+ return (
+
+ Wallet does not support wallet_sendCalls rpc
+
+ )
+ }
+ if (atomicBatchSupportedChains.length === 0) {
+ return (
+
+ Account does not support atomic batch feature
+
+ )
+ }
+
+ return currentChainsInfo ? (
+
+
+
+
+ ) : (
+
+ Switch to {atomicBatchSupportedChainNames} to test atomic batch feature
+
+ )
+}
diff --git a/apps/laboratory/src/components/Ethers/EthersSendCallsWithPaymasterServiceTest.tsx b/apps/laboratory/src/components/Ethers/EthersSendCallsWithPaymasterServiceTest.tsx
new file mode 100644
index 0000000000..9dd0858509
--- /dev/null
+++ b/apps/laboratory/src/components/Ethers/EthersSendCallsWithPaymasterServiceTest.tsx
@@ -0,0 +1,152 @@
+import { Button, Stack, Text, Input, Tooltip } from '@chakra-ui/react'
+import { useState } from 'react'
+import { useWeb3ModalAccount, useWeb3ModalProvider } from '@web3modal/ethers/react'
+import { EthereumProvider } from '@walletconnect/ethereum-provider'
+import { useChakraToast } from '../Toast'
+import { parseGwei } from 'viem'
+import { vitalikEthAddress } from '../../utils/DataUtil'
+import { BrowserProvider } from 'ethers'
+import {
+ EIP_5792_RPC_METHODS,
+ WALLET_CAPABILITIES,
+ getCapabilitySupportedChainInfo
+} from '../../utils/EIP5792Utils'
+
+export function EthersSendCallsWithPaymasterServiceTest() {
+ const [paymasterServiceUrl, setPaymasterServiceUrl] = useState('')
+ const [isLoading, setLoading] = useState(false)
+
+ const { address, chainId, isConnected } = useWeb3ModalAccount()
+ const { walletProvider } = useWeb3ModalProvider()
+ const toast = useChakraToast()
+
+ const paymasterServiceSupportedChains =
+ address && walletProvider instanceof EthereumProvider
+ ? getCapabilitySupportedChainInfo(
+ WALLET_CAPABILITIES.PAYMASTER_SERVICE,
+ walletProvider,
+ address
+ )
+ : []
+ const paymasterServiceSupportedChainNames = paymasterServiceSupportedChains
+ .map(ci => ci.chainName)
+ .join(', ')
+ const currentChainsInfo = paymasterServiceSupportedChains.find(
+ chainInfo => chainInfo.chainId === Number(chainId)
+ )
+ async function onSendCalls() {
+ try {
+ setLoading(true)
+ if (!walletProvider || !address) {
+ throw Error('user is disconnected')
+ }
+ if (!chainId) {
+ throw Error('chain not selected')
+ }
+
+ if (!paymasterServiceUrl) {
+ throw Error('paymasterServiceUrl not set')
+ }
+ const provider = new BrowserProvider(walletProvider, chainId)
+ const amountToSend = parseGwei('0.001').toString(16)
+ const calls = [
+ {
+ to: vitalikEthAddress,
+ value: `0x${amountToSend}`
+ },
+ {
+ to: vitalikEthAddress,
+ data: '0xdeadbeef'
+ }
+ ]
+ const sendCallsParams = {
+ version: '1.0',
+ chainId: `0x${BigInt(chainId).toString(16)}`,
+ from: address,
+ calls,
+ capabilities: {
+ paymasterService: {
+ url: paymasterServiceUrl
+ }
+ }
+ }
+ const batchCallHash = await provider.send(EIP_5792_RPC_METHODS.WALLET_SEND_CALLS, [
+ sendCallsParams
+ ])
+ toast({
+ title: 'SendCalls Success',
+ description: batchCallHash,
+ type: 'success'
+ })
+ } catch {
+ toast({
+ title: 'Error',
+ description: 'Failed to send calls',
+ type: 'error'
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ function isSendCallsSupported(): boolean {
+ if (walletProvider instanceof EthereumProvider) {
+ return Boolean(
+ walletProvider?.signer?.session?.namespaces?.['eip155']?.methods?.includes(
+ EIP_5792_RPC_METHODS.WALLET_SEND_CALLS
+ )
+ )
+ }
+
+ return false
+ }
+
+ if (!isConnected || !walletProvider || !address) {
+ return (
+
+ Wallet not connected
+
+ )
+ }
+ if (!isSendCallsSupported()) {
+ return (
+
+ Wallet does not support wallet_sendCalls rpc
+
+ )
+ }
+ if (paymasterServiceSupportedChains.length === 0) {
+ return (
+
+ Account does not support paymaster service feature
+
+ )
+ }
+
+ return currentChainsInfo ? (
+
+
+ setPaymasterServiceUrl(e.target.value)}
+ value={paymasterServiceUrl}
+ isDisabled={isLoading}
+ whiteSpace="nowrap"
+ textOverflow="ellipsis"
+ />
+
+
+
+ ) : (
+
+ Switch to {paymasterServiceSupportedChainNames} to test paymaster service feature
+
+ )
+}
diff --git a/apps/laboratory/src/components/Ethers/EthersTests.tsx b/apps/laboratory/src/components/Ethers/EthersTests.tsx
index 72728a429e..1e49dc1e84 100644
--- a/apps/laboratory/src/components/Ethers/EthersTests.tsx
+++ b/apps/laboratory/src/components/Ethers/EthersTests.tsx
@@ -5,6 +5,9 @@ import { EthersSignTypedDataTest } from './EthersSignTypedDataTest'
import { StackDivider, Card, CardHeader, Heading, CardBody, Box, Stack } from '@chakra-ui/react'
import { EthersTransactionTest } from './EthersTransactionTest'
import { EthersWriteContractTest } from './EthersWriteContractTest'
+import { EthersSendCallsTest } from './EthersSendCallsTest'
+import { EthersGetCallsStatusTest } from './EthersGetCallsStatusTest'
+import { EthersSendCallsWithPaymasterServiceTest } from './EthersSendCallsWithPaymasterServiceTest'
export function EthersTests() {
const [ready, setReady] = React.useState(false)
@@ -52,6 +55,24 @@ export function EthersTests() {
+
+
+ Send Calls (Atomic Batch)
+
+
+
+
+
+ Get Calls Status
+
+
+
+
+
+ Send Calls (Paymaster Service)
+
+
+
diff --git a/apps/laboratory/src/types/EIP5792.ts b/apps/laboratory/src/types/EIP5792.ts
new file mode 100644
index 0000000000..ff6b4ab02f
--- /dev/null
+++ b/apps/laboratory/src/types/EIP5792.ts
@@ -0,0 +1,21 @@
+export type CapabilityName = 'atomicBatch' | 'paymasterService' | 'sessionKey'
+export type Capabilities = {
+ [K in CapabilityName]?: {
+ supported: boolean
+ }
+}
+export type GetCapabilitiesResult = Record
+export type GetCallsStatusParams = `0x${string}`
+
+export type SendCallsParams = {
+ version: string
+ chainId: `0x${string}`
+ from: `0x${string}`
+ calls: {
+ to: `0x${string}`
+ data?: `0x${string}` | undefined
+ value?: `0x${string}` | undefined
+ }[]
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ capabilities?: Record | undefined
+}
diff --git a/apps/laboratory/src/utils/EIP5792Utils.ts b/apps/laboratory/src/utils/EIP5792Utils.ts
index b44b3328ce..ee5149b007 100644
--- a/apps/laboratory/src/utils/EIP5792Utils.ts
+++ b/apps/laboratory/src/utils/EIP5792Utils.ts
@@ -66,3 +66,19 @@ export function getProviderCachedCapabilities(
return convertCapabilitiesToRecord(accountCapabilities)
}
+
+export function getCapabilitySupportedChainInfo(
+ capability: string,
+ provider: Awaited>,
+ address: string
+): {
+ chainId: number
+ chainName: string
+}[] {
+ const perChainCapabilities = getProviderCachedCapabilities(address, provider)
+ if (!perChainCapabilities) {
+ return []
+ }
+
+ return getFilteredCapabilitySupportedChainInfo(capability, perChainCapabilities)
+}