diff --git a/advanced/wallets/react-wallet-v2/docker-compose.yaml b/advanced/wallets/react-wallet-v2/docker-compose.yaml new file mode 100644 index 000000000..8fcde8bf3 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/docker-compose.yaml @@ -0,0 +1,20 @@ +services: + anvil: + image: ghcr.io/foundry-rs/foundry:nightly-f6208d8db68f9acbe4ff8cd76958309efb61ea0b + ports: ["8545:8545"] + entrypoint: [ "anvil","--chain-id", "31337", "--fork-url", "https://gateway.tenderly.co/public/sepolia", "--host", "0.0.0.0", "--block-time", "0.1", "--gas-price", "1", "--silent",] + platform: linux/amd64 + + mock-paymaster: + image: ghcr.io/pimlicolabs/mock-verifying-paymaster:main + ports: ["3000:3000"] + environment: + - ALTO_RPC=http://alto:4337 + - ANVIL_RPC=http://anvil:8545 + + alto: + image: ghcr.io/pimlicolabs/mock-alto-bundler:main + ports: ["4337:4337"] + environment: + - ANVIL_RPC=http://anvil:8545 + - SKIP_DEPLOYMENTS=true \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts b/advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts index 89e2c73b6..13da3514d 100644 --- a/advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts +++ b/advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts @@ -1,5 +1,6 @@ import { KernelSmartAccountLib } from '@/lib/smart-accounts/KernelSmartAccountLib' import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib' +import { getAddress } from 'viem' import { goerli, polygonMumbai, sepolia } from 'viem/chains' // Types @@ -15,3 +16,12 @@ export const availableSmartAccounts = { safe: SafeSmartAccountLib, kernel: KernelSmartAccountLib } + +export const SAFE_FALLBACK_HANDLER_STORAGE_SLOT = + '0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5' + +export const SAFE_4337_MODULE_ADDRESSES = [ + getAddress('0xa581c4A4DB7175302464fF3C06380BC3270b4037'), + getAddress('0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'), + getAddress('0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2') +] diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts index d3f0f8b4e..57a902b04 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts @@ -34,6 +34,7 @@ export class SafeSmartAccountLib extends SmartAccountLib { safeVersion: '1.4.1', entryPoint: ENTRYPOINT_ADDRESS_V07, safe4337ModuleAddress: this.SAFE_4337_MODULE_ADDRESS, + //@ts-ignore erc7579LaunchpadAddress: this.ERC_7579_LAUNCHPAD_ADDRESS, signer: this.signer }) diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts index 16145de98..562910958 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts @@ -26,7 +26,7 @@ import { createSmartAccountClient } from 'permissionless' import { PimlicoBundlerActions, pimlicoBundlerActions } from 'permissionless/actions/pimlico' -import { PIMLICO_NETWORK_NAMES, UrlConfig, publicRPCUrl } from '@/utils/SmartAccountUtil' +import { PIMLICO_NETWORK_NAMES, publicClientUrl, publicRPCUrl, UrlConfig } from '@/utils/SmartAccountUtil' import { Chain } from '@/consts/smartAccounts' import { EntryPoint } from 'permissionless/types/entrypoint' import { Erc7579Actions, erc7579Actions } from 'permissionless/actions/erc7579' @@ -115,7 +115,7 @@ export abstract class SmartAccountLib implements EIP155Wallet { }) this.publicClient = createPublicClient({ - transport: http(publicClientRPCUrl) + transport: http(publicClientUrl({ chain: this.chain })) }).extend(bundlerActions(this.entryPoint)) this.paymasterClient = createPimlicoPaymasterClient({ diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts new file mode 100644 index 000000000..7fc64e884 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts @@ -0,0 +1,156 @@ +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' +import { + FillUserOpParams, + FillUserOpResponse, + SendUserOpWithSigantureParams, + SendUserOpWithSigantureResponse, + UserOpBuilder +} from './UserOpBuilder' +import { + Address, + Chain, + createPublicClient, + GetStorageAtReturnType, + Hex, + http, + parseAbi, + PublicClient, + trim +} from 'viem' +import { signerToSafeSmartAccount } from 'permissionless/accounts' +import { + createSmartAccountClient, + ENTRYPOINT_ADDRESS_V07, + getUserOperationHash +} from 'permissionless' +import { + createPimlicoBundlerClient, + createPimlicoPaymasterClient +} from 'permissionless/clients/pimlico' +import { bundlerUrl, paymasterUrl, publicClientUrl } from '@/utils/SmartAccountUtil' + +import { getChainById } from '@/utils/ChainUtil' +import { SAFE_FALLBACK_HANDLER_STORAGE_SLOT } from '@/consts/smartAccounts' + +const ERC_7579_LAUNCHPAD_ADDRESS: Address = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE' + +export class SafeUserOpBuilder implements UserOpBuilder { + protected chain: Chain + protected publicClient: PublicClient + protected accountAddress: Address + + constructor(accountAddress: Address, chainId: number) { + this.chain = getChainById(chainId) + this.publicClient = createPublicClient({ + transport: http(publicClientUrl({ chain: this.chain })) + }) + this.accountAddress = accountAddress + } + + async fillUserOp(params: FillUserOpParams): Promise { + const privateKey = generatePrivateKey() + const signer = privateKeyToAccount(privateKey) + + let erc7579LaunchpadAddress: Address + const safe4337ModuleAddress = await this.getFallbackHandlerAddress() + const is7579Safe = await this.is7579Safe() + + if (is7579Safe) { + erc7579LaunchpadAddress = ERC_7579_LAUNCHPAD_ADDRESS + } + + const version = await this.getVersion() + + const paymasterClient = createPimlicoPaymasterClient({ + transport: http(paymasterUrl({ chain: this.chain }), { + timeout: 30000 + }), + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + const bundlerTransport = http(bundlerUrl({ chain: this.chain }), { + timeout: 30000 + }) + const pimlicoBundlerClient = createPimlicoBundlerClient({ + transport: bundlerTransport, + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + const safeAccount = await signerToSafeSmartAccount(this.publicClient, { + entryPoint: ENTRYPOINT_ADDRESS_V07, + signer: signer, + //@ts-ignore + safeVersion: version, + address: this.accountAddress, + safe4337ModuleAddress, + //@ts-ignore + erc7579LaunchpadAddress + }) + + const smartAccountClient = createSmartAccountClient({ + account: safeAccount, + entryPoint: ENTRYPOINT_ADDRESS_V07, + chain: this.chain, + bundlerTransport, + middleware: { + sponsorUserOperation: paymasterClient.sponsorUserOperation, // optional + gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast // if using pimlico bundler + } + }) + const account = smartAccountClient.account + + const userOp = await smartAccountClient.prepareUserOperationRequest({ + userOperation: { + callData: await account.encodeCallData(params.calls) + }, + account: account + }) + const hash = getUserOperationHash({ + userOperation: userOp, + chainId: this.chain.id, + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + return { + userOp, + hash + } + } + sendUserOpWithSignature( + params: SendUserOpWithSigantureParams + ): Promise { + throw new Error('Method not implemented.') + } + + private async getVersion(): Promise { + const version = await this.publicClient.readContract({ + address: this.accountAddress, + abi: parseAbi(['function VERSION() view returns (string)']), + functionName: 'VERSION', + args: [] + }) + return version + } + + private async is7579Safe(): Promise { + const accountId = await this.publicClient.readContract({ + address: this.accountAddress, + abi: parseAbi([ + 'function accountId() external view returns (string memory accountImplementationId)' + ]), + functionName: 'accountId', + args: [] + }) + if (accountId.includes('7579') && accountId.includes('safe')) { + return true + } + return false + } + + private async getFallbackHandlerAddress(): Promise
{ + const value = await this.publicClient.getStorageAt({ + address: this.accountAddress, + slot: SAFE_FALLBACK_HANDLER_STORAGE_SLOT + }) + return trim(value as Hex) + } +} diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts new file mode 100644 index 000000000..01de2b840 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts @@ -0,0 +1,42 @@ +import { UserOperation } from 'permissionless' +import { Address, Hex } from 'viem' + +type Call = { to: Address; value: bigint; data: Hex } + +type UserOp = UserOperation<'v0.7'> + +export type FillUserOpParams = { + chainId: number + account: Address + calls: Call[] + capabilities: { + paymasterService?: { url: string } + permissions?: { context: Hex } + } +} +export type FillUserOpResponse = { + userOp: UserOp + hash: Hex +} + +export type ErrorResponse = { + message: string + error: string +} + +export type SendUserOpWithSigantureParams = { + chainId: Hex + userOp: UserOp + signature: Hex + permissionsContext?: Hex +} +export type SendUserOpWithSigantureResponse = { + receipt: Hex +} + +export interface UserOpBuilder { + fillUserOp(params: FillUserOpParams): Promise + sendUserOpWithSignature( + params: SendUserOpWithSigantureParams + ): Promise +} diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/build.ts b/advanced/wallets/react-wallet-v2/src/pages/api/build.ts new file mode 100644 index 000000000..9456b5fc3 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/pages/api/build.ts @@ -0,0 +1,28 @@ +import { ErrorResponse, FillUserOpResponse } from '@/lib/smart-accounts/builders/UserOpBuilder' +import { getChainById } from '@/utils/ChainUtil' +import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil' +import { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const chainId = req.body.chainId + const account = req.body.account + const chain = getChainById(chainId) + try { + const builder = await getUserOpBuilder({ + account, + chain + }) + + const response = await builder.fillUserOp(req.body) + + res.status(200).json(response) + } catch (error: any) { + return res.status(200).json({ + message: 'Unable to build userOp', + error: error.message + }) + } +} diff --git a/advanced/wallets/react-wallet-v2/src/utils/ChainUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/ChainUtil.ts new file mode 100644 index 000000000..7934acc6c --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/utils/ChainUtil.ts @@ -0,0 +1,12 @@ +import * as chains from 'viem/chains' +import { Chain } from 'viem/chains' + +export function getChainById(chainId: number): Chain { + for (const chain of Object.values(chains)) { + if (chain.id === chainId) { + return chain + } + } + + throw new Error(`Chain with id ${chainId} not found`) +} diff --git a/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts index 265ceb78d..a5a6aa14c 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts @@ -1,5 +1,5 @@ import { BiconomySmartAccountLib } from './../lib/smart-accounts/BiconomySmartAccountLib' -import { Hex } from 'viem' +import { Hex, Chain as ViemChain } from 'viem' import { SessionTypes } from '@walletconnect/types' import { Chain, allowedChains } from '@/consts/smartAccounts' import { KernelSmartAccountLib } from '@/lib/smart-accounts/KernelSmartAccountLib' @@ -7,10 +7,6 @@ import { sepolia } from 'viem/chains' import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib' import { SmartAccountLib } from '@/lib/smart-accounts/SmartAccountLib' -export type UrlConfig = { - chain: Chain -} - // Entrypoints [I think this is constant but JIC] export const ENTRYPOINT_ADDRESSES: Record = { Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', @@ -34,14 +30,14 @@ export const USDC_ADDRESSES: Record = { } // RPC URLs -export const RPC_URLS: Record = { +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 = { +export const PIMLICO_NETWORK_NAMES: Record = { Sepolia: 'sepolia', 'Polygon Mumbai': 'mumbai', Goerli: 'goerli' @@ -144,3 +140,36 @@ export async function createOrRestoreBiconomySmartAccount(privateKey: string) { biconomySmartAccountAddress: address } } + +export type UrlConfig = { + chain: Chain | ViemChain +} + +export const publicClientUrl = ({ chain }: UrlConfig) => { + return process.env.NEXT_PUBLIC_LOCAL_CLIENT_URL || publicRPCUrl({ chain }) +} + +export const paymasterUrl = ({ chain }: UrlConfig) => { + const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY + if (apiKey == null) { + throw new Error('Pimlico API Key not set') + } + + const localPaymasterUrl = process.env.NEXT_PUBLIC_LOCAL_PAYMASTER_URL + if (localPaymasterUrl) { + return localPaymasterUrl + } + return `https://api.pimlico.io/v2/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}` +} + +export const bundlerUrl = ({ chain }: UrlConfig) => { + const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY + if (apiKey == null) { + throw new Error('Pimlico API Key not set') + } + const localBundlerUrl = process.env.NEXT_PUBLIC_LOCAL_BUNDLER_URL + if (localBundlerUrl) { + return localBundlerUrl + } + return `https://api.pimlico.io/v1/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}` +} diff --git a/advanced/wallets/react-wallet-v2/src/utils/UserOpBuilderUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/UserOpBuilderUtil.ts new file mode 100644 index 000000000..3d10143c8 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/utils/UserOpBuilderUtil.ts @@ -0,0 +1,54 @@ +import { SafeUserOpBuilder } from '@/lib/smart-accounts/builders/SafeUserOpBuilder' +import { UserOpBuilder } from '@/lib/smart-accounts/builders/UserOpBuilder' +import { + Address, + Chain, + createPublicClient, + getAddress, + http, + PublicClient, + size, + slice +} from 'viem' +import { publicClientUrl } from './SmartAccountUtil' +import { + SAFE_4337_MODULE_ADDRESSES, + SAFE_FALLBACK_HANDLER_STORAGE_SLOT +} from '@/consts/smartAccounts' + +type GetUserOpBuilderParams = { + account: Address + chain: Chain + publicClient?: PublicClient +} + +export async function getUserOpBuilder(params: GetUserOpBuilderParams): Promise { + let publicClient = params.publicClient + if (!publicClient) { + publicClient = createPublicClient({ + transport: http(publicClientUrl({ chain: params.chain })) + }) + } + if (await isSafeAccount(publicClient, params.account)) { + return new SafeUserOpBuilder(params.account, params.chain.id) + } + + throw new Error('Unsupported implementation type') +} + +async function isSafeAccount(publicClient: PublicClient, address: Address): Promise { + try { + const storageValue = await publicClient.getStorageAt({ + address, + slot: SAFE_FALLBACK_HANDLER_STORAGE_SLOT + }) + if (!storageValue) { + return false + } + const safe4337ModuleAddress = getAddress(slice(storageValue, size(storageValue) - 20)) + return SAFE_4337_MODULE_ADDRESSES.includes(safe4337ModuleAddress) + } catch (error) { + console.log('Unable to check if account is Safe', error) + return false + } +}