Skip to content

Commit

Permalink
feat: add safe userOp builder (#640)
Browse files Browse the repository at this point in the history
* feat: add safe userOp builder

* chore: use already existing transport builders to support local urls

* fix: lint

* chore: add docker-compose

* fix: provide module and launchpad address

* chore: use the onchain data to query account details

* fix: format

* chore: change accountImplementation to accountId

* chore: refactor userOp builder to use generic implementation

* chore: add one log for unexpected behaviour
  • Loading branch information
lukaisailovic authored Aug 20, 2024
1 parent 1bfd393 commit 5825941
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 9 deletions.
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

0 comments on commit 5825941

Please sign in to comment.