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) +}