Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add safe userOp builder #640

Merged
merged 12 commits into from
Aug 20, 2024
20 changes: 20 additions & 0 deletions advanced/wallets/react-wallet-v2/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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')
]
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FillUserOpResponse> {
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<SendUserOpWithSigantureResponse> {
throw new Error('Method not implemented.')
}

private async getVersion(): Promise<string> {
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<boolean> {
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<Address> {
const value = await this.publicClient.getStorageAt({
address: this.accountAddress,
slot: SAFE_FALLBACK_HANDLER_STORAGE_SLOT
})
return trim(value as Hex)
}
}
Original file line number Diff line number Diff line change
@@ -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<FillUserOpResponse>
sendUserOpWithSignature(
params: SendUserOpWithSigantureParams
): Promise<SendUserOpWithSigantureResponse>
}
28 changes: 28 additions & 0 deletions advanced/wallets/react-wallet-v2/src/pages/api/build.ts
Original file line number Diff line number Diff line change
@@ -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<FillUserOpResponse | ErrorResponse>
) {
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
})
}
}
12 changes: 12 additions & 0 deletions advanced/wallets/react-wallet-v2/src/utils/ChainUtil.ts
Original file line number Diff line number Diff line change
@@ -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`)
}
43 changes: 36 additions & 7 deletions advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
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'
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<Chain['name'], Hex> = {
Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
Expand All @@ -34,14 +30,14 @@ export const USDC_ADDRESSES: Record<Chain['name'], Hex> = {
}

// RPC URLs
export const RPC_URLS: Record<Chain['name'], string> = {
export const RPC_URLS: Record<ViemChain['name'], string> = {
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<Chain['name'], string> = {
export const PIMLICO_NETWORK_NAMES: Record<ViemChain['name'], string> = {
Sepolia: 'sepolia',
'Polygon Mumbai': 'mumbai',
Goerli: 'goerli'
Expand Down Expand Up @@ -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}`
}
Loading