From 9439c9af54d5f363ac58cbf81e5a5e39da3dce2a Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 1 Feb 2024 16:23:32 -0600 Subject: [PATCH] feat: Smart Account relay connection + paymaster base (#351) * feat: added Goerli smart accounts * chore: remove viem version modifier. Split useSmartAccount usage into multiliune * feat: fix error handling * feat: added smart wallets to proposal modal * feat: added paymasters to be able to freely transact on testnet with Pimlico sponsorship. Serialize SA addresses in relay response * feat: adapted SmartWalletLib interface to be able to use same methods as EIP155Lib. Hooked up all operations on EIP155RequestHandler to smart accounts. * chore: remove logs * feat: added spinner to modal footer. Use it when handling eip155 requests * feat: added sponsorship toggle in settings * feat: upgraded to permissionless@0.0.16 * fix: conflicts after merge * chore: unify loaders * chore: kristoph refactor + multi chain support * fix: chain Id issue. remove unused logs --- .../src/components/ChainSmartAddressMini.tsx | 31 ++ .../src/components/ModalFooter.tsx | 7 +- .../src/components/SmartAccountCard.tsx | 12 +- .../react-wallet-v2/src/data/EIP155Data.ts | 10 +- .../src/hooks/useInitialization.ts | 2 +- .../src/hooks/useSmartAccount.ts | 33 +- .../hooks/useWalletConnectEventsManager.ts | 2 +- .../react-wallet-v2/src/lib/EIP155Lib.ts | 2 +- .../src/lib/SmartAccountLib.ts | 309 +++++++++++++----- .../react-wallet-v2/src/pages/_app.tsx | 4 +- .../react-wallet-v2/src/pages/settings.tsx | 15 + .../src/store/SettingsStore.ts | 11 +- .../src/utils/EIP155RequestHandlerUtil.ts | 45 ++- .../src/utils/SmartAccountUtils.ts | 140 ++++++++ .../src/utils/WalletConnectUtil.ts | 4 +- .../src/views/RequestModal.tsx | 4 - .../src/views/SessionProposalModal.tsx | 42 ++- .../src/views/SessionSignTypedDataModal.tsx | 2 + 18 files changed, 544 insertions(+), 131 deletions(-) create mode 100644 advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx 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 new file mode 100644 index 0000000..51f0f6b --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/components/ChainSmartAddressMini.tsx @@ -0,0 +1,31 @@ +import useSmartAccount from '@/hooks/useSmartAccount' +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 +} + +const getKey = (namespace?: string) => { + switch (namespace) { + case 'eip155': + createOrRestoreEIP155Wallet() + const key = Object.values(eip155Wallets)[0]?.getPrivateKey() as Hex + return key + } +} + +export default function ChainSmartAddressMini({ namespace }: Props) { + 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/ModalFooter.tsx b/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx index 5983bfd..fbaf378 100644 --- a/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx +++ b/advanced/wallets/react-wallet-v2/src/components/ModalFooter.tsx @@ -11,7 +11,6 @@ interface Props { onReject: () => void infoBoxCondition?: boolean infoBoxText?: string - disabledApprove?: boolean approveLoader?: LoaderProps rejectLoader?: LoaderProps } @@ -23,7 +22,6 @@ export default function ModalFooter({ rejectLoader, infoBoxCondition, infoBoxText, - disabledApprove }: Props) { const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state) const validation = currentRequestVerifyContext?.verified.validation @@ -46,13 +44,14 @@ export default function ModalFooter({ {infoBoxText || ''} )} - + diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts index 8b79daa..0c16d6b 100644 --- a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts +++ b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts @@ -8,7 +8,7 @@ */ export type TEIP155Chain = keyof typeof EIP155_CHAINS -export type EIP155TestChain = { +export type EIP155Chain = { chainId: number name: string logo: string @@ -21,7 +21,7 @@ export type EIP155TestChain = { /** * Chains */ -export const EIP155_MAINNET_CHAINS = { +export const EIP155_MAINNET_CHAINS: Record = { 'eip155:1': { chainId: 1, name: 'Ethereum', @@ -64,7 +64,7 @@ export const EIP155_MAINNET_CHAINS = { } } -export const EIP155_TEST_CHAINS: Record = { +export const EIP155_TEST_CHAINS: Record = { 'eip155:5': { chainId: 5, name: 'Ethereum Goerli', @@ -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 d96955f..9f378ab 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 a3f3074..38fce46 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useSmartAccount.ts @@ -1,18 +1,24 @@ import { SmartAccountLib } from "@/lib/SmartAccountLib"; -import { styledToast } from "@/utils/HelperUtil"; +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"; +import { styledToast } from "@/utils/HelperUtil"; import { TransactionExecutionError } from "viem"; -export default function useSmartAccount(signerPrivateKey: `0x${string}`) { +export default function useSmartAccount(signerPrivateKey: Hex, chain: Chain) { const [loading, setLoading] = useState(false) const [client, setClient] = useState(); const [isDeployed, setIsDeployed] = useState(false) - const [address, setAddress] = useState<`0x${string}`>() + const [address, setAddress] = useState() + const { smartAccountSponsorshipEnabled } = useSnapshot(SettingsStore.state); const execute = useCallback(async (callback: () => void) => { try { setLoading(true) - await callback() + const res = await callback() + console.log('result:', res) setLoading(false) } catch (e) { @@ -34,13 +40,22 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) { const sendTestTransaction = useCallback(async () => { if (!client) return - execute(client?.sendTestTransaction) + execute(() => client?.sendTransaction({ + to: VITALIK_ADDRESS, + value: 0n, + data: '0x', + })) }, [client, execute]) useEffect(() => { - const smartAccountClient = new SmartAccountLib(signerPrivateKey, 'goerli') - setClient(smartAccountClient) - }, [signerPrivateKey]) + if (!signerPrivateKey || !chain) return + const smartAccountClient = new SmartAccountLib({ + chain, + privateKey: signerPrivateKey, + sponsored: smartAccountSponsorshipEnabled, + }) + setClient(smartAccountClient) + }, [signerPrivateKey, smartAccountSponsorshipEnabled, chain]) useEffect(() => { client?.checkIfSmartAccountDeployed() @@ -48,7 +63,7 @@ export default function useSmartAccount(signerPrivateKey: `0x${string}`) { 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 af4f6c2..a860ba2 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/EIP155Lib.ts b/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts index 2be3436..87989a0 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/EIP155Lib.ts @@ -39,7 +39,7 @@ export default class EIP155Lib { return this.wallet.signMessage(message) } - _signTypedData(domain: any, types: any, data: any) { + _signTypedData(domain: any, types: any, data: any, _primaryType?: string) { return this.wallet._signTypedData(domain, types, data) } diff --git a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts index 7d13432..960f3a9 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/SmartAccountLib.ts @@ -1,134 +1,267 @@ -import { createSmartAccountClient } 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 } from 'viem' -import { createPimlicoBundlerClient } 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 { 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 type SmartAccountEnabledChains = 'sepolia' | 'goerli' +type SmartAccountLibOptions = { + privateKey: `0x${string}` + chain: Chain + sponsored?: boolean +}; // -- Helpers ----------------------------------------------------------------- -const bundlerApiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY +const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY const projectId = process.env.NEXT_PUBLIC_PROJECT_ID // -- Sdk --------------------------------------------------------------------- export class SmartAccountLib { public chain: Chain - private bundlerApiKey: 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 - public constructor(privateKey: `0x${string}`, chain: SmartAccountEnabledChains = 'goerli') { - if (!bundlerApiKey) { - throw new Error('Missing required data in SmartAccountSdk') + #signerPrivateKey: `0x${string}`; + + + public constructor({ privateKey, chain, sponsored = true }: SmartAccountLibOptions) { + if (!pimlicoApiKey) { + throw new Error('A Pimlico API Key is required') } - this.bundlerApiKey = bundlerApiKey - this.chain = chains[chain] as Chain - this.#signerPrivateKey = privateKey - } - // -- Public ------------------------------------------------------------------ + this.chain = chain + this.sponsored = sponsored + this.#signerPrivateKey = privateKey + this.publicClient = createPublicClient({ + transport: http(publicRPCUrl({ chain: this.chain })) + }) + + this.paymasterClient = createPimlicoPaymasterClient({ + transport: http(paymasterUrl({ chain: this.chain })) + }) + + this.bundlerClient = createClient({ + transport: http(bundlerUrl({ chain: this.chain })), + chain: this.chain + }) + .extend(bundlerActions) + .extend(pimlicoBundlerActions) + + this.signerClient = createWalletClient({ + account: privateKeyToAccount(this.#signerPrivateKey), + chain: this.chain, + transport: http(publicRPCUrl({ chain: this.chain })) + }) + + } // -- Private ----------------------------------------------------------------- - private getWalletConnectTransport = () => http( - `https://rpc.walletconnect.com/v1/?chainId=EIP155:${this.chain.id}&projectId=${projectId}`, - { retryDelay: 1000 } - ); - - private getBundlerTransport = () => http( - `https://api.pimlico.io/v1/${this.chain.name.toLowerCase()}/rpc?apikey=${this.bundlerApiKey}`, - { retryDelay: 1000 } - ); - - - private getBundlerClient = () => createPimlicoBundlerClient({ - chain: this.chain, - transport: this.getBundlerTransport() - }) - - private getPublicClient = () => createPublicClient({ - chain: this.chain, - transport: this.getWalletConnectTransport() - }) - - private getSignerClient = () => { - const signerAccount = privateKeyToAccount(this.#signerPrivateKey) - return createWalletClient({ - account: signerAccount, + private getSmartAccountClient = async ( + sponsorUserOperation?: (args: { + userOperation: UserOperation + entryPoint: Address + }) => Promise + ) => { + const account = await this.getAccount() + return createSmartAccountClient({ + account, chain: this.chain, - transport: this.getWalletConnectTransport() - }) + transport: http(bundlerUrl({ chain: this.chain })), + sponsorUserOperation: sponsorUserOperation + ? sponsorUserOperation + : this.sponsored ? this.paymasterClient.sponsorUserOperation : undefined + }).extend(pimlicoBundlerActions) } - private getSmartAccountClient = async () => { - const smartAccount = await privateKeyToSafeSmartAccount(this.getPublicClient(), { - privateKey: this.#signerPrivateKey, - safeVersion: '1.4.1', - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' - }) - - return createSmartAccountClient({ - account: smartAccount, - chain: this.chain, - transport: this.getBundlerTransport() + 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.getSignerClient(); - const publicClient = this.getPublicClient(); - const bundlerClient = this.getBundlerClient(); - 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` ) } } - // By default first transaction will deploy the smart contract if it hasn't been deployed yet - public sendTestTransaction = async () => { - const publicClient = this.getPublicClient(); - const bundlerClient = this.getBundlerClient(); - 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, - }) - - console.log(`Sending Test Transaction With Hash: ${testHash}`) - - await publicClient.waitForTransactionReceipt({ hash: testHash }) - console.log(`Test Transaction Success`) + private getSmartAccountUSDCBalance = async () => { + const params = { + abi: [ + { + inputs: [{ name: "_owner", type: "address" }], + name: "balanceOf", + outputs: [{ name: "balance", type: "uint256" }], + type: "function", + } + ], + address: USDC_ADDRESSES[this.chain.name] as Hex, + functionName: "balanceOf", + args: [this.address!] + } + const usdcBalance = await this.publicClient.readContract(params) as bigint + 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 ------------------------------------------------------------------ + 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, + transport: http( + `https://rpc.walletconnect.com/v1/?chainId=EIP155:${chains.goerli.id}&projectId=${projectId}`, + { retryDelay: 1000 } + ) + }) + const bytecode = await client.getBytecode({ address }) + return Boolean(bytecode) + } + + 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 + }) + } + + public signMessage = async (message: string) => { + const client = await this.getSmartAccountClient() + return client.signMessage({ message }) + } + public _signTypedData = async (domain: any, types: any, data: any, primaryType: any) => { + const client = await this.getSmartAccountClient() + return client.signTypedData({ account: client.account, domain, types, primaryType, message: data }) + } + + public connect = async (_provider: providers.JsonRpcProvider) => { + return this.getSmartAccountClient() + } + + public signTransaction = async (transaction: any) => { + const smartAccountClient = await this.getSmartAccountClient() + return smartAccountClient.account.signTransaction(transaction) + } + + public sendUSDCSponsoredTransaction = async ({ + to, + value, + data + }: { to: Address; value: bigint; data: Hex }) => { + // 1. Check USDC Balance on smart account + const usdcBalance = await this.getSmartAccountUSDCBalance() + + if (usdcBalance < 1_000_000n) { + throw new Error( + `insufficient USDC balance for counterfactual wallet address ${this.address}: ${ + Number(usdcBalance) / 1000000 + } USDC, required at least 1 USDC` + ) + } + + const smartAccountClient = await this.getSmartAccountClient(this.sponsorUserOperation) + const gasPrices = await smartAccountClient.getUserOperationGasPrice() + + return smartAccountClient.sendTransaction({ + to, + value, + data, + maxFeePerGas: gasPrices.fast.maxFeePerGas, + maxPriorityFeePerGas: gasPrices.fast.maxPriorityFeePerGas + }) + } + + public checkIfSmartAccountDeployed = async () => { - console.log('checking if deployed') const smartAccountClient = await this.getSmartAccountClient(); - const publicClient = this.getPublicClient(); - const bytecode = await publicClient.getBytecode({ address: 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) + console.log(`Smart Account Deployed: ${this.isDeployed}`) if (this.isDeployed) { this.address = smartAccountClient.account.address @@ -145,7 +278,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 8de06ad..8cf3976 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/pages/settings.tsx b/advanced/wallets/react-wallet-v2/src/pages/settings.tsx index 3c2eaaf..2ade434 100644 --- a/advanced/wallets/react-wallet-v2/src/pages/settings.tsx +++ b/advanced/wallets/react-wallet-v2/src/pages/settings.tsx @@ -16,6 +16,7 @@ import { tezosWallets } from '@/utils/TezosWalletUtil' export default function SettingsPage() { const { testNets, + smartAccountSponsorshipEnabled, eip155Address, cosmosAddress, solanaAddress, @@ -53,6 +54,20 @@ export default function SettingsPage() { + + Smart Account Sponsorship (Pimlico) + + + + {smartAccountSponsorshipEnabled ? 'Enabled' : 'Disabled'} + + + + Relayer Region diff --git a/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts b/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts index 56edf6a..264654a 100644 --- a/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts +++ b/advanced/wallets/react-wallet-v2/src/store/SettingsStore.ts @@ -20,6 +20,7 @@ interface State { activeChainId: string currentRequestVerifyContext?: Verify.Context sessions: SessionTypes.Struct[] + smartAccountSponsorshipEnabled: boolean } /** @@ -39,7 +40,8 @@ const state = proxy({ tezosAddress: '', kadenaAddress: '', relayerRegionURL: '', - sessions: [] + sessions: [], + smartAccountSponsorshipEnabled: false, }) /** @@ -103,10 +105,17 @@ const SettingsStore = { toggleTestNets() { state.testNets = !state.testNets if (state.testNets) { + state.smartAccountSponsorshipEnabled = true localStorage.setItem('TEST_NETS', 'YES') } else { + state.smartAccountSponsorshipEnabled = false localStorage.removeItem('TEST_NETS') } + }, + + toggleSmartAccountSponsorship() { + if (!state.testNets) return + state.smartAccountSponsorshipEnabled = !state.smartAccountSponsorshipEnabled } } diff --git a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts index 36b8d61..6cc20b5 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/EIP155RequestHandlerUtil.ts @@ -1,4 +1,6 @@ import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data' +import EIP155Lib from '@/lib/EIP155Lib' +import { SmartAccountLib } from '@/lib/SmartAccountLib' import { eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil' import { getSignParamsMessage, @@ -9,11 +11,41 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils' 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 + + +const getWallet = async (params: any) => { + const requestParams: Array = params?.request?.params || [] + const eoaWallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)] + if (eoaWallet) { + return eoaWallet + } + + const deployedSmartAccounts = await Promise.all(Object.values(eip155Wallets).map(async (wallet) => { + const smartAccount = new SmartAccountLib({ + privateKey: wallet.getPrivateKey() as Hex, + 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) { + return smartAccount + } + return null + })); + 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 +} + + export async function approveEIP155Request(requestEvent: RequestEventArgs) { const { params, id } = requestEvent const { chainId, request } = params - const wallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)] + const wallet = await getWallet(params) switch (request.method) { case EIP155_SIGNING_METHODS.PERSONAL_SIGN: @@ -32,10 +64,10 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) { case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3: case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4: try { - const { domain, types, message: data } = getSignTypedDataParamsData(request.params) + const { domain, types, message: data, primaryType } = getSignTypedDataParamsData(request.params) // https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471 delete types.EIP712Domain - const signedData = await wallet._signTypedData(domain, types, data) + const signedData = await wallet._signTypedData(domain, types, data, primaryType) return formatJsonRpcResult(id, signedData) } catch (error: any) { console.error(error) @@ -47,9 +79,10 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) { try { const provider = new providers.JsonRpcProvider(EIP155_CHAINS[chainId as TEIP155Chain].rpc) const sendTransaction = request.params[0] - const connectedWallet = wallet.connect(provider) - const { hash } = await connectedWallet.sendTransaction(sendTransaction) - return formatJsonRpcResult(id, hash) + const connectedWallet = await wallet.connect(provider) + const hash = await connectedWallet.sendTransaction(sendTransaction) + const receipt = typeof hash === 'string' ? hash : hash?.hash // TODO improve interface + return formatJsonRpcResult(id, receipt) } catch (error: any) { console.error(error) alert(error.message) 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 0000000..4ea710e --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtils.ts @@ -0,0 +1,140 @@ +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 FAUCET_URLS: Record = { + Sepolia: 'https://sepoliafaucet.com', + 'Polygon Mumbai': 'https://faucet.polygon.technology', + Goerli: 'https://goerlifaucet.com' +} + +export const USDC_FAUCET_URL = 'https://faucet.circle.com/' + +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 24f1105..b2fea49 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/RequestModal.tsx b/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx index cd879ed..64c1251 100644 --- a/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/RequestModal.tsx @@ -18,7 +18,6 @@ interface IProps { intention?: string infoBoxCondition?: boolean infoBoxText?: string - disabledApprove?: boolean approveLoader?: LoaderProps rejectLoader?: LoaderProps } @@ -32,7 +31,6 @@ export default function RequestModal({ intention, infoBoxCondition, infoBoxText, - disabledApprove }: IProps) { const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state) const isScam = currentRequestVerifyContext?.verified.isScam @@ -65,14 +63,12 @@ export default function RequestModal({ rejectLoader={rejectLoader} infoBoxCondition={infoBoxCondition} infoBoxText={infoBoxText} - disabledApprove={disabledApprove} /> ) }, [ approveLoader, children, - disabledApprove, infoBoxCondition, infoBoxText, intention, diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx index 472763f..e29b6ea 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close' import ModalStore from '@/store/ModalStore' import { cosmosAddresses } from '@/utils/CosmosWalletUtil' -import { eip155Addresses } from '@/utils/EIP155WalletUtil' +import { createOrRestoreEIP155Wallet, eip155Addresses, eip155Wallets } from '@/utils/EIP155WalletUtil' import { polkadotAddresses } from '@/utils/PolkadotWalletUtil' import { multiversxAddresses } from '@/utils/MultiversxWalletUtil' import { tronAddresses } from '@/utils/TronWalletUtil' @@ -18,7 +18,7 @@ import { nearAddresses } from '@/utils/NearWalletUtil' import { kadenaAddresses } from '@/utils/KadenaWalletUtil' import { styledToast } from '@/utils/HelperUtil' import { web3wallet } from '@/utils/WalletConnectUtil' -import { EIP155_CHAINS, EIP155_SIGNING_METHODS } from '@/data/EIP155Data' +import { EIP155Chain, EIP155_CHAINS, EIP155_SIGNING_METHODS } from '@/data/EIP155Data' import { COSMOS_MAINNET_CHAINS, COSMOS_SIGNING_METHODS } from '@/data/COSMOSData' import { KADENA_CHAINS, KADENA_SIGNING_METHODS } from '@/data/KadenaData' import { MULTIVERSX_CHAINS, MULTIVERSX_SIGNING_METHODS } from '@/data/MultiversxData' @@ -31,8 +31,12 @@ import ChainDataMini from '@/components/ChainDataMini' import ChainAddressMini from '@/components/ChainAddressMini' import { getChainData } from '@/data/chainsUtil' import RequestModal from './RequestModal' +import { SmartAccountLib } from '@/lib/SmartAccountLib' +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 @@ -43,6 +47,7 @@ const StyledSpan = styled('span', { } as any) export default function SessionProposalModal() { + const { smartAccountSponsorshipEnabled } = useSnapshot(SettingsStore.state) // Get proposal data and wallet address from store const data = useSnapshot(ModalStore.state) const proposal = data?.data?.proposal as SignClientTypes.EventArguments['session_proposal'] @@ -174,6 +179,11 @@ export default function SessionProposalModal() { [requestedChains] ) + const smartAccountChains = useMemo( + () => supportedChains.filter(chain =>(chain as any)?.smartAccountEnabled), + [supportedChains] + ) + // get required chains that are not supported by the wallet const notSupportedChains = useMemo(() => { if (!proposal) return [] @@ -225,6 +235,21 @@ export default function SessionProposalModal() { supportedNamespaces }) + // 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: allowedChains.find(chain => chain.id.toString() === chainId)!, + sponsored: smartAccountSponsorshipEnabled, + }) + const isDeployed = await smartAccountClient.checkIfSmartAccountDeployed() + console.log('isDeployed', isDeployed) + + if (isDeployed) { + namespaces.eip155.accounts = [...namespaces.eip155.accounts, `eip155:5:${smartAccountClient.address}`] + } + console.log('approving namespaces:', namespaces) try { @@ -241,7 +266,7 @@ export default function SessionProposalModal() { } setIsLoadingApprove(false) ModalStore.close() - }, [proposal, supportedNamespaces]) + }, [proposal, supportedNamespaces, smartAccountSponsorshipEnabled]) // Hanlde reject action // eslint-disable-next-line react-hooks/rules-of-hooks @@ -273,7 +298,6 @@ export default function SessionProposalModal() { rejectLoader={{ active: isLoadingReject }} infoBoxCondition={notSupportedChains.length > 0} infoBoxText={`The following required chains are not supported by your wallet - ${notSupportedChains.toString()}`} - disabledApprove={notSupportedChains.length > 0} > @@ -309,6 +333,16 @@ export default function SessionProposalModal() { ) })} + + Smart Accounts + {smartAccountChains.length && + smartAccountChains.map((chain, i) => { + return ( + + + + ) + })} diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx index 0fada9f..ec9c01c 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTypedDataModal.tsx @@ -75,6 +75,8 @@ export default function SessionSignTypedDataModal() { metadata={requestSession.peer.metadata} onApprove={onApprove} onReject={onReject} + approveLoader={{ active: isLoadingApprove }} + rejectLoader={{ active: isLoadingReject }} >