From b48343c2b4db70f663144b1f4c80d5e9caf62125 Mon Sep 17 00:00:00 2001 From: tomiir Date: Wed, 31 Jan 2024 20:59:26 -0600 Subject: [PATCH] chore: kristoph refactor + multi chain support --- .../src/components/ChainSmartAddressMini.tsx | 6 +- .../src/components/SmartAccountCard.tsx | 6 +- .../react-wallet-v2/src/data/EIP155Data.ts | 4 +- .../src/hooks/useInitialization.ts | 2 +- .../src/hooks/useSmartAccount.ts | 21 +- .../hooks/useWalletConnectEventsManager.ts | 2 +- .../src/lib/SmartAccountLib.ts | 317 ++++++++---------- .../react-wallet-v2/src/pages/_app.tsx | 4 +- .../src/utils/EIP155RequestHandlerUtil.ts | 8 +- .../src/utils/ERC20PaymasterUtil.ts | 75 ----- .../src/utils/SmartAccountUtils.ts | 132 ++++++++ .../src/utils/WalletConnectUtil.ts | 4 +- .../src/views/SessionProposalModal.tsx | 4 +- 13 files changed, 306 insertions(+), 279 deletions(-) delete mode 100644 advanced/wallets/react-wallet-v2/src/utils/ERC20PaymasterUtil.ts create mode 100644 advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts diff --git a/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx b/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx index cc33b2435..51f0f6bd1 100644 --- a/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx +++ b/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx @@ -3,6 +3,9 @@ import { Hex } from 'viem' import ChainAddressMini from './ChainAddressMini' import { createOrRestoreEIP155Wallet, eip155Wallets } from '@/utils/EIP155WalletUtil' import { Spinner } from '@nextui-org/react' +import { Chain, allowedChains } from '@/utils/SmartAccountUtils' +import { useSnapshot } from 'valtio' +import SettingsStore from '@/store/SettingsStore' interface Props { namespace: string @@ -18,7 +21,8 @@ const getKey = (namespace?: string) => { } export default function ChainSmartAddressMini({ namespace }: Props) { - const { address } = useSmartAccount(getKey(namespace) as `0x${string}`) + const { activeChainId } = useSnapshot(SettingsStore.state) + const { address } = useSmartAccount(getKey(namespace) as `0x${string}`, allowedChains.find((c) => c.id.toString() === activeChainId) as Chain) if (!address) return return ( diff --git a/advanced/wallets/react-wallet-v2/src/components/SmartAccountCard.tsx b/advanced/wallets/react-wallet-v2/src/components/SmartAccountCard.tsx index 3c3354bb8..f361a5cf8 100644 --- a/advanced/wallets/react-wallet-v2/src/components/SmartAccountCard.tsx +++ b/advanced/wallets/react-wallet-v2/src/components/SmartAccountCard.tsx @@ -5,9 +5,10 @@ import { updateSignClientChainId } from '@/utils/WalletConnectUtil' import { Avatar, Button, Text, Tooltip, Loading } from '@nextui-org/react' import { eip155Wallets } from '@/utils/EIP155WalletUtil' import Image from 'next/image' -import { useState, useEffect } from 'react' +import { useState } from 'react' import { useSnapshot } from 'valtio' import useSmartAccount from '@/hooks/useSmartAccount' +import { Chain, allowedChains } from '@/utils/SmartAccountUtils' interface Props { name: string @@ -28,13 +29,14 @@ export default function SmartAccountCard({ }: Props) { const [copied, setCopied] = useState(false) const { activeChainId } = useSnapshot(SettingsStore.state) + const chain = allowedChains.find((c) => c.id.toString() === chainId.split(':')[0]) as Chain const { deploy, isDeployed, address: smartAccountAddress, loading, sendTestTransaction, - } = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`) + } = useSmartAccount(eip155Wallets[address].getPrivateKey() as `0x${string}`, chain) function onCopy() { navigator?.clipboard?.writeText(address) diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts index fbeb31d1e..0c16d6ba5 100644 --- a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts +++ b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts @@ -81,6 +81,7 @@ export const EIP155_TEST_CHAINS: Record = { rgb: '99, 125, 234', rpc: 'https://rpc.sepolia.org', namespace: 'eip155', + smartAccountEnabled: true, }, 'eip155:43113': { chainId: 43113, @@ -96,7 +97,8 @@ export const EIP155_TEST_CHAINS: Record = { logo: '/chain-logos/eip155-137.png', rgb: '130, 71, 229', rpc: 'https://matic-mumbai.chainstacklabs.com', - namespace: 'eip155' + namespace: 'eip155', + smartAccountEnabled: true, }, 'eip155:420': { chainId: 420, diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts b/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts index d96955fe7..9f378ab93 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useInitialization.ts @@ -49,7 +49,7 @@ export default function useInitialization() { // restart transport if relayer region changes const onRelayerRegionChange = useCallback(() => { try { - web3wallet.core.relayer.restartTransport(relayerRegionURL) + web3wallet?.core?.relayer.restartTransport(relayerRegionURL) prevRelayerURLValue.current = relayerRegionURL } catch (err: unknown) { alert(err) diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts b/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts index b282422dd..2f383d153 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts @@ -1,10 +1,11 @@ import { SmartAccountLib } from "@/lib/SmartAccountLib"; import SettingsStore from "@/store/SettingsStore"; +import { Chain, VITALIK_ADDRESS } from "@/utils/SmartAccountUtils"; import { useCallback, useEffect, useState } from "react"; import { useSnapshot } from "valtio"; import { Hex } from "viem"; -export default function useSmartAccount(signerPrivateKey?: Hex) { +export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) { const [loading, setLoading] = useState(false) const [client, setClient] = useState(); const [isDeployed, setIsDeployed] = useState(false) @@ -14,7 +15,8 @@ export default function useSmartAccount(signerPrivateKey?: Hex) { const execute = useCallback(async (callback: () => void) => { try { setLoading(true) - await callback() + const res = await callback() + console.log('result:', res) setLoading(false) } catch (e) { @@ -30,18 +32,23 @@ export default function useSmartAccount(signerPrivateKey?: Hex) { const sendTestTransaction = useCallback(async () => { if (!client) return - execute(client?.sendTestTransaction) + execute(() => client?.sendTransaction({ + to: VITALIK_ADDRESS, + value: 0n, + data: '0x', + })) }, [client, execute]) useEffect(() => { - if (!signerPrivateKey) return + console.log('chain', chain) + if (!signerPrivateKey || !chain) return const smartAccountClient = new SmartAccountLib({ + chain, privateKey: signerPrivateKey, - chain: 'goerli', sponsored: smartAccountSponsorshipEnabled, }) setClient(smartAccountClient) - }, [signerPrivateKey, smartAccountSponsorshipEnabled]) + }, [signerPrivateKey, smartAccountSponsorshipEnabled, chain]) useEffect(() => { client?.checkIfSmartAccountDeployed() @@ -49,7 +56,7 @@ export default function useSmartAccount(signerPrivateKey?: Hex) { setIsDeployed(deployed) setAddress(client?.address) }) - }, [client]) + }, [client, chain]) return { diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts index af4f6c2be..a860ba2ac 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts @@ -117,7 +117,7 @@ export default function useWalletConnectEventsManager(initialized: boolean) { * Set up WalletConnect event listeners *****************************************************************************/ useEffect(() => { - if (initialized) { + if (initialized && web3wallet) { //sign web3wallet.on('session_proposal', onSessionProposal) web3wallet.on('session_request', onSessionRequest) diff --git a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts index db38ea7c8..5f0e65f89 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts @@ -1,23 +1,20 @@ -import { createSmartAccountClient, getAccountNonce, signUserOperationHashWithECDSA } from 'permissionless' +import { BundlerActions, BundlerClient, bundlerActions, createSmartAccountClient, getAccountNonce } from 'permissionless' import { privateKeyToSafeSmartAccount } from 'permissionless/accounts' import * as chains from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' -import { type Chain, createWalletClient, formatEther, createPublicClient, http, Address, Hex } from 'viem' -import { createPimlicoBundlerClient, createPimlicoPaymasterClient } from 'permissionless/clients/pimlico' +import { createWalletClient, formatEther, createPublicClient, http, Address, Hex, PublicClient, createClient, WalletClient } from 'viem' +import { PimlicoPaymasterClient, createPimlicoPaymasterClient } from 'permissionless/clients/pimlico' import { UserOperation } from 'permissionless/types' -import { GOERLI_PAYMASTER_ADDRESS, GOERLI_USDC_ADDRESS, genereteApproveCallData, genereteDummyCallData } from '@/utils/ERC20PaymasterUtil' import { providers } from 'ethers' +import { PimlicoBundlerActions, pimlicoBundlerActions } from 'permissionless/actions/pimlico' +import { Chain, ENTRYPOINT_ADDRESSES, PAYMASTER_ADDRESSES, USDC_ADDRESSES, VITALIK_ADDRESS, approveUSDCSpendCallData, bundlerUrl, paymasterUrl, publicRPCUrl } from '@/utils/SmartAccountUtils' -export const smartAccountEnabledChains = ['sepolia', 'goerli'] as const -export type SmartAccountEnabledChains = typeof smartAccountEnabledChains[number] type SmartAccountLibOptions = { privateKey: `0x${string}` - chain: SmartAccountEnabledChains + chain: Chain sponsored?: boolean }; -const ENTRY_POINT_ADDRESS = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' - // -- Helpers ----------------------------------------------------------------- const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY const projectId = process.env.NEXT_PUBLIC_PROJECT_ID @@ -25,192 +22,105 @@ const projectId = process.env.NEXT_PUBLIC_PROJECT_ID // -- Sdk --------------------------------------------------------------------- export class SmartAccountLib { public chain: Chain - private pimlicoApiKey: string - #signerPrivateKey: `0x${string}`; public isDeployed: boolean = false; public address?: `0x${string}`; public sponsored: boolean = true; + + private publicClient: PublicClient + private paymasterClient: PimlicoPaymasterClient + private bundlerClient: BundlerClient & BundlerActions & PimlicoBundlerActions + private signerClient: WalletClient + + #signerPrivateKey: `0x${string}`; public constructor({ privateKey, chain, sponsored = true }: SmartAccountLibOptions) { if (!pimlicoApiKey) { - throw new Error('Missing required data in SmartAccountSdk') + throw new Error('A Pimlico API Key is required') } - this.pimlicoApiKey = pimlicoApiKey - this.chain = chains[chain] as Chain - this.#signerPrivateKey = privateKey + this.chain = chain this.sponsored = sponsored - } - - - // -- Private ----------------------------------------------------------------- - private get walletConnectTransport() { - return http( - `https://rpc.walletconnect.com/v1/?chainId=EIP155:${this.chain.id}&projectId=${projectId}`, - { retryDelay: 1000 } - ) - } - - private get bundlerTransport() { - return http( - `https://api.pimlico.io/v1/${this.chain.name.toLowerCase()}/rpc?apikey=${this.pimlicoApiKey}`, - { retryDelay: 1000 } - ) - } - - private get paymasterTransport() { - return http( - `https://api.pimlico.io/v2/${this.chain.name.toLowerCase()}/rpc?apikey=${this.pimlicoApiKey}`, - { retryDelay: 1000 } - ) - } - - - private get bundlerClient() { - return createPimlicoBundlerClient({ - chain: this.chain, - transport: this.bundlerTransport + this.#signerPrivateKey = privateKey + this.publicClient = createPublicClient({ + transport: http(publicRPCUrl({ chain: this.chain })) }) - } - private get publicClient() { - return createPublicClient({ - chain: this.chain, - transport: this.walletConnectTransport + this.paymasterClient = createPimlicoPaymasterClient({ + transport: http(paymasterUrl({ chain: this.chain })) }) - } - private get paymasterClient() { - return createPimlicoPaymasterClient({ - chain: this.chain, - transport: this.paymasterTransport + this.bundlerClient = createClient({ + transport: http(bundlerUrl({ chain: this.chain })), + chain: this.chain }) - } + .extend(bundlerActions) + .extend(pimlicoBundlerActions) - private get signerClient(){ - const signerAccount = privateKeyToAccount(this.#signerPrivateKey) - return createWalletClient({ - account: signerAccount, + this.signerClient = createWalletClient({ + account: privateKeyToAccount(this.#signerPrivateKey), chain: this.chain, - transport: this.walletConnectTransport + transport: http(publicRPCUrl({ chain: this.chain })) }) - } - private getAccountNonce() { - return getAccountNonce(this.publicClient, { - entryPoint: ENTRY_POINT_ADDRESS, - sender: this.address as Address - }) } - private getSmartAccountClient = async () => { - const smartAccount = await privateKeyToSafeSmartAccount(this.publicClient, { - privateKey: this.#signerPrivateKey, - safeVersion: '1.4.1', - entryPoint: ENTRY_POINT_ADDRESS - }) + // -- Private ----------------------------------------------------------------- + private getSmartAccountClient = async ( + sponsorUserOperation?: (args: { + userOperation: UserOperation + entryPoint: Address + }) => Promise + ) => { + const account = await this.getAccount() return createSmartAccountClient({ - account: smartAccount, + account, chain: this.chain, - transport: this.bundlerTransport, - sponsorUserOperation: this.sponsored ? this.paymasterClient.sponsorUserOperation : undefined, + transport: http(bundlerUrl({ chain: this.chain })), + sponsorUserOperation: sponsorUserOperation + ? sponsorUserOperation + : this.sponsored ? this.paymasterClient.sponsorUserOperation : undefined + }).extend(pimlicoBundlerActions) + } + + public getNonce = async () => { + const smartAccountClient = await this.getSmartAccountClient() + return getAccountNonce(this.publicClient, { + sender: smartAccountClient.account.address as Hex, + entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name] }) } private prefundSmartAccount = async (address: `0x${string}`) => { - const signerAccountViemClient = this.signerClient - const publicClient = this.publicClient; - const bundlerClient = this.bundlerClient; - const smartAccountBalance = await publicClient.getBalance({ address }) + if (this.sponsored) { + return + } + + const smartAccountBalance = await this.publicClient.getBalance({ address }) console.log(`Smart Account Balance: ${formatEther(smartAccountBalance)} ETH`) if (smartAccountBalance < 1n) { console.log(`Smart Account has no balance. Starting prefund`) - const { fast: fastPrefund } = await bundlerClient.getUserOperationGasPrice() - const prefundHash = await signerAccountViemClient.sendTransaction({ + const { fast: fastPrefund } = await this.bundlerClient.getUserOperationGasPrice() + const prefundHash = await this.signerClient.sendTransaction({ to: address, + chain: this.chain, + account: this.signerClient.account!, value: 10000000000000000n, maxFeePerGas: fastPrefund.maxFeePerGas, maxPriorityFeePerGas: fastPrefund.maxPriorityFeePerGas }) - await publicClient.waitForTransactionReceipt({ hash: prefundHash }) + await this.publicClient.waitForTransactionReceipt({ hash: prefundHash }) console.log(`Prefunding Success`) - const newSmartAccountBalance = await publicClient.getBalance({ address }) + const newSmartAccountBalance = await this.publicClient.getBalance({ address }) console.log( `Smart Account Balance: ${formatEther(newSmartAccountBalance)} ETH` ) } } - private prepareSponsoredUserOperation = async (callData: Hex, paymaster?: Hex) => { - // 1. Get new nonce - const newNonce = await this.getAccountNonce() - - // 2. Get gas price - const { fast } = await this.bundlerClient.getUserOperationGasPrice() - - // 3. Generate dummy user operation with empty transfer to vitalik - const sponsoredUserOperation: UserOperation = { - sender: this.address as Address, - nonce: newNonce, - initCode: "0x", - callData: callData, - callGasLimit: 100_000n, // hardcode it for now at a high value - verificationGasLimit: 500_000n, // hardcode it for now at a high value - preVerificationGas: 50_000n, // hardcode it for now at a high value - maxFeePerGas: fast.maxFeePerGas, - maxPriorityFeePerGas: fast.maxPriorityFeePerGas, - paymasterAndData: paymaster ?? '0x', // to use the erc20 paymaster, put its address in the paymasterAndData field - signature: "0x" - } - - //4. Sign the userop - sponsoredUserOperation.signature = await signUserOperationHashWithECDSA({ - account: this.signerClient.account, - userOperation: sponsoredUserOperation, - chainId: this.chain.id, - entryPoint: ENTRY_POINT_ADDRESS - }) - - return sponsoredUserOperation; - } - - private submitUserOperation = async (userOperation: UserOperation) => { - const userOperationHash = await this.bundlerClient.sendUserOperation({ - userOperation, - entryPoint: ENTRY_POINT_ADDRESS - }) - console.log(`UserOperation submitted. Hash: ${userOperationHash}`) - - console.log("Querying for receipts...") - const receipt = await this.bundlerClient.waitForUserOperationReceipt({ - hash: userOperationHash - }) - console.log(`Receipt found!\nTransaction hash: ${receipt.receipt.transactionHash}`) - } - - private approveUSDC = async () => { - const approveCallData = genereteApproveCallData(GOERLI_USDC_ADDRESS, GOERLI_PAYMASTER_ADDRESS) - const gasPriceResult = await this.bundlerClient.getUserOperationGasPrice() - const nonce = await this.getAccountNonce() - - const smartAccountClient = await this.getSmartAccountClient(); - console.log(`Approving USDC for ${this.address} with nonce ${nonce}`) - const hash = await smartAccountClient.sendTransaction({ - to: GOERLI_USDC_ADDRESS, - value: 0n, - maxFeePerGas: gasPriceResult.fast.maxFeePerGas, - maxPriorityFeePerGas: gasPriceResult.fast.maxPriorityFeePerGas, - data: approveCallData, - }) - - await this.publicClient.waitForTransactionReceipt({ hash }) - console.log(`USDC Approval Success`) - } - private getSmartAccountUSDCBalance = async () => { const params = { abi: [ @@ -221,7 +131,7 @@ export class SmartAccountLib { type: "function", } ], - address: GOERLI_USDC_ADDRESS as Hex, + address: USDC_ADDRESSES[this.chain.name] as Hex, functionName: "balanceOf", args: [this.address!] } @@ -229,10 +139,48 @@ export class SmartAccountLib { return usdcBalance } + private sponsorUserOperation = async ({ userOperation }: { userOperation: UserOperation }) => { + const userOperationWithPaymasterAndData = { + ...userOperation, + paymasterAndData: PAYMASTER_ADDRESSES[this.chain.name] + } + + console.log('Estimating gas limits...', userOperationWithPaymasterAndData) + + const gasLimits = await this.bundlerClient.estimateUserOperationGas({ + userOperation: userOperationWithPaymasterAndData, + entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name] + }) + + return { + ...userOperationWithPaymasterAndData, + callGasLimit: gasLimits.callGasLimit, + verificationGasLimit: gasLimits.verificationGasLimit, + preVerificationGas: gasLimits.preVerificationGas + } + } + // -- Public ------------------------------------------------------------------ - static isSmartAccount = async (address: Address, chain: SmartAccountEnabledChains) => { + public getAccount = async () => + privateKeyToSafeSmartAccount(this.publicClient, { + privateKey: this.#signerPrivateKey, + safeVersion: '1.4.1', // simple version + entryPoint: ENTRYPOINT_ADDRESSES[this.chain.name], // global entrypoint + setupTransactions: [ + { + to: USDC_ADDRESSES[this.chain.name], + value: 0n, + data: approveUSDCSpendCallData({ + to: PAYMASTER_ADDRESSES[this.chain.name], + amount: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn + }) + } + ] + }) + + static isSmartAccount = async (address: Address, chain: Chain) => { const client = createPublicClient({ - chain: chains[chain], + chain, transport: http( `https://rpc.walletconnect.com/v1/?chainId=EIP155:${chains.goerli.id}&projectId=${projectId}`, { retryDelay: 1000 } @@ -242,24 +190,21 @@ export class SmartAccountLib { return Boolean(bytecode) } - // By default first transaction will deploy the smart contract if it hasn't been deployed yet - public sendTestTransaction = async () => { - const publicClient = this.publicClient; - const bundlerClient = this.bundlerClient; - const smartAccountClient = await this.getSmartAccountClient(); - const { fast: testGas, } = await bundlerClient.getUserOperationGasPrice() - - const testHash = await smartAccountClient.sendTransaction({ - to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as `0x${string}`, - value: 0n, - maxFeePerGas: testGas.maxFeePerGas, - maxPriorityFeePerGas: testGas.maxPriorityFeePerGas, + public sendTransaction = async ({ + to, + value, + data + }: { to: Address; value: bigint; data: Hex }) => { + console.log(`Sending Transaction to ${to} with value ${value.toString()} and data ${data}`) + const smartAccountClient = await this.getSmartAccountClient() + const gasPrices = await smartAccountClient.getUserOperationGasPrice() + return smartAccountClient.sendTransaction({ + to, + value, + data, + maxFeePerGas: gasPrices.fast.maxFeePerGas, + maxPriorityFeePerGas: gasPrices.fast.maxPriorityFeePerGas }) - - console.log(`Sending Test Transaction With Hash: ${testHash}`) - - await publicClient.waitForTransactionReceipt({ hash: testHash }) - console.log(`Test Transaction Success`) } public signMessage = async (message: string) => { @@ -280,7 +225,11 @@ export class SmartAccountLib { return smartAccountClient.account.signTransaction(transaction) } - public sendUSDCSponsoredTransaction = async () => { + public sendUSDCSponsoredTransaction = async ({ + to, + value, + data + }: { to: Address; value: bigint; data: Hex }) => { // 1. Check USDC Balance on smart account const usdcBalance = await this.getSmartAccountUSDCBalance() @@ -292,22 +241,22 @@ export class SmartAccountLib { ) } + const smartAccountClient = await this.getSmartAccountClient(this.sponsorUserOperation) + const gasPrices = await smartAccountClient.getUserOperationGasPrice() - - // 2. Approve USDC usage (currently sponsored by Pimlico veridfy paymaster) - await this.approveUSDC() - - // 3. Send transaction - const dummyCallData = genereteDummyCallData() - const userOperation = await this.prepareSponsoredUserOperation(dummyCallData, GOERLI_PAYMASTER_ADDRESS) - - await this.submitUserOperation(userOperation) + return smartAccountClient.sendTransaction({ + to, + value, + data, + maxFeePerGas: gasPrices.fast.maxFeePerGas, + maxPriorityFeePerGas: gasPrices.fast.maxPriorityFeePerGas + }) } public checkIfSmartAccountDeployed = async () => { const smartAccountClient = await this.getSmartAccountClient(); - console.log('checking if deployed', smartAccountClient.account.address) + console.log('checking if deployed', smartAccountClient.account.address, this.chain.name) const bytecode = await this.publicClient.getBytecode({ address: smartAccountClient.account.address }) this.isDeployed = Boolean(bytecode) @@ -328,7 +277,11 @@ export class SmartAccountLib { await this.prefundSmartAccount(smartAccountClient.account.address) // Step 4: Create account by sending test tx - await this.sendTestTransaction() + await this.sendTransaction({ + to: VITALIK_ADDRESS, + value: 0n, + data: '0x' + }) await this.checkIfSmartAccountDeployed() console.log(`Account Created`) } diff --git a/advanced/wallets/react-wallet-v2/src/pages/_app.tsx b/advanced/wallets/react-wallet-v2/src/pages/_app.tsx index 8de06ad42..8cf397609 100644 --- a/advanced/wallets/react-wallet-v2/src/pages/_app.tsx +++ b/advanced/wallets/react-wallet-v2/src/pages/_app.tsx @@ -20,11 +20,11 @@ export default function App({ Component, pageProps }: AppProps) { useWalletConnectEventsManager(initialized) useEffect(() => { if (!initialized) return - web3wallet.core.relayer.on(RELAYER_EVENTS.connect, () => { + web3wallet?.core.relayer.on(RELAYER_EVENTS.connect, () => { styledToast('Network connection is restored!', 'success') }) - web3wallet.core.relayer.on(RELAYER_EVENTS.disconnect, () => { + web3wallet?.core.relayer.on(RELAYER_EVENTS.disconnect, () => { styledToast('Network connection lost.', 'error') }) }, [initialized]) diff --git a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts index 74af1d164..6cc20b5bd 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts @@ -12,6 +12,7 @@ import { SignClientTypes } from '@walletconnect/types' import { getSdkError } from '@walletconnect/utils' import { providers } from 'ethers' import { Hex } from 'viem' +import { allowedChains } from './SmartAccountUtils' type RequestEventArgs = Omit @@ -22,12 +23,11 @@ const getWallet = async (params: any) => { return eoaWallet } - // TODO improve for multichain const deployedSmartAccounts = await Promise.all(Object.values(eip155Wallets).map(async (wallet) => { const smartAccount = new SmartAccountLib({ privateKey: wallet.getPrivateKey() as Hex, - chain: 'goerli', - sponsored: true, // Sponsor for now but should be dynamic according to SettingsStore + chain: allowedChains[0], // TODO: FIX FOR MULTI NETWORK + sponsored: true, // TODO: Sponsor for now but should be dynamic according to SettingsStore }) const isDeployed = await smartAccount.checkIfSmartAccountDeployed() if (isDeployed) { @@ -35,7 +35,7 @@ const getWallet = async (params: any) => { } return null })); - const validSmartAccounts = deployedSmartAccounts.filter(val => !!val) as Array + const validSmartAccounts = deployedSmartAccounts.filter(Boolean) as Array const smartAccountAddress = getWalletAddressFromParams(validSmartAccounts.map(acc => acc.address!), params) return validSmartAccounts.find((smartAccount) => smartAccount?.address === smartAccountAddress) as SmartAccountLib diff --git a/advanced/wallets/react-wallet-v2/src/utils/ERC20PaymasterUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/ERC20PaymasterUtil.ts deleted file mode 100644 index aa890260e..000000000 --- a/advanced/wallets/react-wallet-v2/src/utils/ERC20PaymasterUtil.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Address, Hex, encodeFunctionData } from "viem" - -export const VITALIK_ADDRESS = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as Hex -export const GOERLI_PAYMASTER_ADDRESS = '0xEc43912D8C772A0Eba5a27ea5804Ba14ab502009' -export const GOERLI_USDC_ADDRESS = '0x07865c6e87b9f70255377e024ace6630c1eaa37f' - -export const genereteDummyCallData = () => { - // SEND EMPTY CALL TO VITALIK - const to = VITALIK_ADDRESS - const value = 0n - const data = "0x" - - const callData = encodeFunctionData({ - abi: [ - { - inputs: [ - { name: "dest", type: "address" }, - { name: "value", type: "uint256" }, - { name: "func", type: "bytes" } - ], - name: "execute", - outputs: [], - stateMutability: "nonpayable", - type: "function" - } - ], - args: [to, value, data] - }) - - return callData -} - - -export const genereteApproveCallData = (erc20TokenAddress: Address, paymasterAddress: Address) => { - const approveData = encodeFunctionData({ - abi: [ - { - inputs: [ - { name: "_spender", type: "address" }, - { name: "_value", type: "uint256" } - ], - name: "approve", - outputs: [{ name: "", type: "bool" }], - payable: false, - stateMutability: "nonpayable", - type: "function" - } - ], - args: [paymasterAddress, 0xffffffffffffn] - }) - - // GENERATE THE CALLDATA TO APPROVE THE USDC - const to = erc20TokenAddress - const value = 0n - const data = approveData - - const callData = encodeFunctionData({ - abi: [ - { - inputs: [ - { name: "dest", type: "address" }, - { name: "value", type: "uint256" }, - { name: "func", type: "bytes" } - ], - name: "execute", - outputs: [], - stateMutability: "nonpayable", - type: "function" - } - ], - args: [to, value, data] - }) - - return callData - } \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts new file mode 100644 index 000000000..9378e4ca8 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts @@ -0,0 +1,132 @@ +import { Hex, createPublicClient, encodeFunctionData, http } from "viem" +import { goerli, polygonMumbai, sepolia } from 'viem/chains' + +const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY + +// Types +export const allowedChains = [sepolia, polygonMumbai, goerli] as const +export type Chain = (typeof allowedChains)[number] +export type UrlConfig = { + chain: Chain +} + +// Entrypoints [I think this is constant but JIC] +export const ENTRYPOINT_ADDRESSES: Record = { + Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + 'Polygon Mumbai': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + 'Goerli': '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' +} + +// Paymasters +// https://docs.pimlico.io/paymaster/erc20-paymaster/contract-addresses +export const PAYMASTER_ADDRESSES: Record = { + Sepolia: '0x0000000000325602a77416A16136FDafd04b299f', + 'Polygon Mumbai': '0x000000000009B901DeC1aaB9389285965F49D387', + Goerli: '0xEc43912D8C772A0Eba5a27ea5804Ba14ab502009' +} + +// USDC +export const USDC_ADDRESSES: Record = { + Sepolia: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + 'Polygon Mumbai': '0x9999f7fea5938fd3b1e26a12c3f2fb024e194f97', + Goerli: '0x07865c6e87b9f70255377e024ace6630c1eaa37f' +} + +// RPC URLs +export const RPC_URLS: Record = { + Sepolia: 'https://rpc.ankr.com/eth_sepolia', + 'Polygon Mumbai': 'https://mumbai.rpc.thirdweb.com', + Goerli: 'https://ethereum-goerli.publicnode.com' +} + +// Pimlico RPC names +export const PIMLICO_NETWORK_NAMES: Record = { + Sepolia: 'sepolia', + 'Polygon Mumbai': 'mumbai', + Goerli: 'goerli' +} + +export const VITALIK_ADDRESS = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' as Hex + +export const publicRPCUrl = ({ chain }: UrlConfig) => RPC_URLS[chain.name] +export const paymasterUrl = ({ chain }: UrlConfig) => + `https://api.pimlico.io/v2/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}` +export const bundlerUrl = ({ chain }: UrlConfig) => + `https://api.pimlico.io/v1/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}` + + +const publicClient = ({ chain }: UrlConfig) => createPublicClient({ + transport: http(publicRPCUrl({ chain })), +}) + +export const approvePaymasterUSDCSpend = (chain: Chain) => { + // Approve paymaster to spend USDC on our behalf + const approveData = approveUSDCSpendCallData({ + to: PAYMASTER_ADDRESSES[chain.name], + amount: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn + }) + + // GENERATE THE CALLDATA FOR USEROP TO SEND TO THE SMART ACCOUNT + const dest = USDC_ADDRESSES[chain.name] // Execute tx in USDC contract + const value = 0n + const data = approveData // Execute approve call + + return generateUserOperationExecuteCallData({ dest, value, data }) +} + +export const approveUSDCSpendCallData = ({ to, amount }: { to: Hex, amount: bigint }) => { + return encodeFunctionData({ + abi: [ + { + inputs: [ + { name: "_spender", type: "address" }, + { name: "_value", type: "uint256" } + ], + name: "approve", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function" + } + ], + args: [to, amount] + }) +} + +// Wraps the call data in the execute function in order to send via UserOperation +export const generateUserOperationExecuteCallData = ({ dest, data, value }: { dest: Hex, data: Hex, value: bigint }) => { + return encodeFunctionData({ + abi: [ + { + inputs: [ + { name: "dest", type: "address" }, + { name: "value", type: "uint256" }, + { name: "func", type: "bytes" } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + args: [dest, value, data] + }) +} + + +export const getUSDCBalance = async ({ address, chain }: { address: Hex, chain: Chain }) => { + return publicClient({ chain }).readContract({ + abi: [ + { + inputs: [{ name: "_owner", type: "address" }], + name: "balanceOf", + outputs: [{ name: "balance", type: "uint256" }], + type: "function", + stateMutability: "view" + } + ], + address: USDC_ADDRESSES[chain.name], + functionName: "balanceOf", + args: [address] + }) +} \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts index 24f1105f9..b2fea49b5 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts @@ -40,11 +40,11 @@ export async function updateSignClientChainId(chainId: string, address: string) [namespace]: { ...session.namespaces[namespace], chains: [ - ...new Set([chainId].concat(Array.from(session.namespaces[namespace].chains || []))) + ...new Set([chainId].concat(Array.from(session?.namespaces?.[namespace]?.chains || []))) ], accounts: [ ...new Set( - [`${chainId}:${address}`].concat(Array.from(session.namespaces[namespace].accounts)) + [`${chainId}:${address}`].concat(Array.from(session?.namespaces?.[namespace]?.accounts || [])) ) ] } diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx index ec12fb6ed..99d017ef7 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx @@ -36,6 +36,7 @@ import { Hex } from 'viem' import ChainSmartAddressMini from '@/components/ChainSmartAddressMini' import { useSnapshot } from 'valtio' import SettingsStore from '@/store/SettingsStore' +import { allowedChains } from '@/utils/SmartAccountUtils' const StyledText = styled(Text, { fontWeight: 400 @@ -236,9 +237,10 @@ export default function SessionProposalModal() { // TODO: improve for multi network console.log('checking if SA is deployed', eip155Wallets[eip155Addresses[0]]) + const chainId = namespaces['eip155'].chains?.[0] const smartAccountClient = new SmartAccountLib({ privateKey: eip155Wallets[eip155Addresses[0]].getPrivateKey() as Hex, - chain: 'goerli', + chain: allowedChains.find(chain => chain.id.toString() === chainId)!, sponsored: smartAccountSponsorshipEnabled, }) const isDeployed = await smartAccountClient.checkIfSmartAccountDeployed()