Skip to content

Commit 5825941

Browse files
feat: add safe userOp builder (#640)
* 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
1 parent 1bfd393 commit 5825941

File tree

10 files changed

+361
-9
lines changed

10 files changed

+361
-9
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
anvil:
3+
image: ghcr.io/foundry-rs/foundry:nightly-f6208d8db68f9acbe4ff8cd76958309efb61ea0b
4+
ports: ["8545:8545"]
5+
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",]
6+
platform: linux/amd64
7+
8+
mock-paymaster:
9+
image: ghcr.io/pimlicolabs/mock-verifying-paymaster:main
10+
ports: ["3000:3000"]
11+
environment:
12+
- ALTO_RPC=http://alto:4337
13+
- ANVIL_RPC=http://anvil:8545
14+
15+
alto:
16+
image: ghcr.io/pimlicolabs/mock-alto-bundler:main
17+
ports: ["4337:4337"]
18+
environment:
19+
- ANVIL_RPC=http://anvil:8545
20+
- SKIP_DEPLOYMENTS=true

advanced/wallets/react-wallet-v2/src/consts/smartAccounts.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { KernelSmartAccountLib } from '@/lib/smart-accounts/KernelSmartAccountLib'
22
import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib'
3+
import { getAddress } from 'viem'
34
import { goerli, polygonMumbai, sepolia } from 'viem/chains'
45

56
// Types
@@ -15,3 +16,12 @@ export const availableSmartAccounts = {
1516
safe: SafeSmartAccountLib,
1617
kernel: KernelSmartAccountLib
1718
}
19+
20+
export const SAFE_FALLBACK_HANDLER_STORAGE_SLOT =
21+
'0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5'
22+
23+
export const SAFE_4337_MODULE_ADDRESSES = [
24+
getAddress('0xa581c4A4DB7175302464fF3C06380BC3270b4037'),
25+
getAddress('0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'),
26+
getAddress('0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2')
27+
]

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class SafeSmartAccountLib extends SmartAccountLib {
3434
safeVersion: '1.4.1',
3535
entryPoint: ENTRYPOINT_ADDRESS_V07,
3636
safe4337ModuleAddress: this.SAFE_4337_MODULE_ADDRESS,
37+
//@ts-ignore
3738
erc7579LaunchpadAddress: this.ERC_7579_LAUNCHPAD_ADDRESS,
3839
signer: this.signer
3940
})

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
createSmartAccountClient
2727
} from 'permissionless'
2828
import { PimlicoBundlerActions, pimlicoBundlerActions } from 'permissionless/actions/pimlico'
29-
import { PIMLICO_NETWORK_NAMES, UrlConfig, publicRPCUrl } from '@/utils/SmartAccountUtil'
29+
import { PIMLICO_NETWORK_NAMES, publicClientUrl, publicRPCUrl, UrlConfig } from '@/utils/SmartAccountUtil'
3030
import { Chain } from '@/consts/smartAccounts'
3131
import { EntryPoint } from 'permissionless/types/entrypoint'
3232
import { Erc7579Actions, erc7579Actions } from 'permissionless/actions/erc7579'
@@ -115,7 +115,7 @@ export abstract class SmartAccountLib implements EIP155Wallet {
115115
})
116116

117117
this.publicClient = createPublicClient({
118-
transport: http(publicClientRPCUrl)
118+
transport: http(publicClientUrl({ chain: this.chain }))
119119
}).extend(bundlerActions(this.entryPoint))
120120

121121
this.paymasterClient = createPimlicoPaymasterClient({
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
2+
import {
3+
FillUserOpParams,
4+
FillUserOpResponse,
5+
SendUserOpWithSigantureParams,
6+
SendUserOpWithSigantureResponse,
7+
UserOpBuilder
8+
} from './UserOpBuilder'
9+
import {
10+
Address,
11+
Chain,
12+
createPublicClient,
13+
GetStorageAtReturnType,
14+
Hex,
15+
http,
16+
parseAbi,
17+
PublicClient,
18+
trim
19+
} from 'viem'
20+
import { signerToSafeSmartAccount } from 'permissionless/accounts'
21+
import {
22+
createSmartAccountClient,
23+
ENTRYPOINT_ADDRESS_V07,
24+
getUserOperationHash
25+
} from 'permissionless'
26+
import {
27+
createPimlicoBundlerClient,
28+
createPimlicoPaymasterClient
29+
} from 'permissionless/clients/pimlico'
30+
import { bundlerUrl, paymasterUrl, publicClientUrl } from '@/utils/SmartAccountUtil'
31+
32+
import { getChainById } from '@/utils/ChainUtil'
33+
import { SAFE_FALLBACK_HANDLER_STORAGE_SLOT } from '@/consts/smartAccounts'
34+
35+
const ERC_7579_LAUNCHPAD_ADDRESS: Address = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE'
36+
37+
export class SafeUserOpBuilder implements UserOpBuilder {
38+
protected chain: Chain
39+
protected publicClient: PublicClient
40+
protected accountAddress: Address
41+
42+
constructor(accountAddress: Address, chainId: number) {
43+
this.chain = getChainById(chainId)
44+
this.publicClient = createPublicClient({
45+
transport: http(publicClientUrl({ chain: this.chain }))
46+
})
47+
this.accountAddress = accountAddress
48+
}
49+
50+
async fillUserOp(params: FillUserOpParams): Promise<FillUserOpResponse> {
51+
const privateKey = generatePrivateKey()
52+
const signer = privateKeyToAccount(privateKey)
53+
54+
let erc7579LaunchpadAddress: Address
55+
const safe4337ModuleAddress = await this.getFallbackHandlerAddress()
56+
const is7579Safe = await this.is7579Safe()
57+
58+
if (is7579Safe) {
59+
erc7579LaunchpadAddress = ERC_7579_LAUNCHPAD_ADDRESS
60+
}
61+
62+
const version = await this.getVersion()
63+
64+
const paymasterClient = createPimlicoPaymasterClient({
65+
transport: http(paymasterUrl({ chain: this.chain }), {
66+
timeout: 30000
67+
}),
68+
entryPoint: ENTRYPOINT_ADDRESS_V07
69+
})
70+
71+
const bundlerTransport = http(bundlerUrl({ chain: this.chain }), {
72+
timeout: 30000
73+
})
74+
const pimlicoBundlerClient = createPimlicoBundlerClient({
75+
transport: bundlerTransport,
76+
entryPoint: ENTRYPOINT_ADDRESS_V07
77+
})
78+
79+
const safeAccount = await signerToSafeSmartAccount(this.publicClient, {
80+
entryPoint: ENTRYPOINT_ADDRESS_V07,
81+
signer: signer,
82+
//@ts-ignore
83+
safeVersion: version,
84+
address: this.accountAddress,
85+
safe4337ModuleAddress,
86+
//@ts-ignore
87+
erc7579LaunchpadAddress
88+
})
89+
90+
const smartAccountClient = createSmartAccountClient({
91+
account: safeAccount,
92+
entryPoint: ENTRYPOINT_ADDRESS_V07,
93+
chain: this.chain,
94+
bundlerTransport,
95+
middleware: {
96+
sponsorUserOperation: paymasterClient.sponsorUserOperation, // optional
97+
gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast // if using pimlico bundler
98+
}
99+
})
100+
const account = smartAccountClient.account
101+
102+
const userOp = await smartAccountClient.prepareUserOperationRequest({
103+
userOperation: {
104+
callData: await account.encodeCallData(params.calls)
105+
},
106+
account: account
107+
})
108+
const hash = getUserOperationHash({
109+
userOperation: userOp,
110+
chainId: this.chain.id,
111+
entryPoint: ENTRYPOINT_ADDRESS_V07
112+
})
113+
return {
114+
userOp,
115+
hash
116+
}
117+
}
118+
sendUserOpWithSignature(
119+
params: SendUserOpWithSigantureParams
120+
): Promise<SendUserOpWithSigantureResponse> {
121+
throw new Error('Method not implemented.')
122+
}
123+
124+
private async getVersion(): Promise<string> {
125+
const version = await this.publicClient.readContract({
126+
address: this.accountAddress,
127+
abi: parseAbi(['function VERSION() view returns (string)']),
128+
functionName: 'VERSION',
129+
args: []
130+
})
131+
return version
132+
}
133+
134+
private async is7579Safe(): Promise<boolean> {
135+
const accountId = await this.publicClient.readContract({
136+
address: this.accountAddress,
137+
abi: parseAbi([
138+
'function accountId() external view returns (string memory accountImplementationId)'
139+
]),
140+
functionName: 'accountId',
141+
args: []
142+
})
143+
if (accountId.includes('7579') && accountId.includes('safe')) {
144+
return true
145+
}
146+
return false
147+
}
148+
149+
private async getFallbackHandlerAddress(): Promise<Address> {
150+
const value = await this.publicClient.getStorageAt({
151+
address: this.accountAddress,
152+
slot: SAFE_FALLBACK_HANDLER_STORAGE_SLOT
153+
})
154+
return trim(value as Hex)
155+
}
156+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { UserOperation } from 'permissionless'
2+
import { Address, Hex } from 'viem'
3+
4+
type Call = { to: Address; value: bigint; data: Hex }
5+
6+
type UserOp = UserOperation<'v0.7'>
7+
8+
export type FillUserOpParams = {
9+
chainId: number
10+
account: Address
11+
calls: Call[]
12+
capabilities: {
13+
paymasterService?: { url: string }
14+
permissions?: { context: Hex }
15+
}
16+
}
17+
export type FillUserOpResponse = {
18+
userOp: UserOp
19+
hash: Hex
20+
}
21+
22+
export type ErrorResponse = {
23+
message: string
24+
error: string
25+
}
26+
27+
export type SendUserOpWithSigantureParams = {
28+
chainId: Hex
29+
userOp: UserOp
30+
signature: Hex
31+
permissionsContext?: Hex
32+
}
33+
export type SendUserOpWithSigantureResponse = {
34+
receipt: Hex
35+
}
36+
37+
export interface UserOpBuilder {
38+
fillUserOp(params: FillUserOpParams): Promise<FillUserOpResponse>
39+
sendUserOpWithSignature(
40+
params: SendUserOpWithSigantureParams
41+
): Promise<SendUserOpWithSigantureResponse>
42+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ErrorResponse, FillUserOpResponse } from '@/lib/smart-accounts/builders/UserOpBuilder'
2+
import { getChainById } from '@/utils/ChainUtil'
3+
import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil'
4+
import { NextApiRequest, NextApiResponse } from 'next'
5+
6+
export default async function handler(
7+
req: NextApiRequest,
8+
res: NextApiResponse<FillUserOpResponse | ErrorResponse>
9+
) {
10+
const chainId = req.body.chainId
11+
const account = req.body.account
12+
const chain = getChainById(chainId)
13+
try {
14+
const builder = await getUserOpBuilder({
15+
account,
16+
chain
17+
})
18+
19+
const response = await builder.fillUserOp(req.body)
20+
21+
res.status(200).json(response)
22+
} catch (error: any) {
23+
return res.status(200).json({
24+
message: 'Unable to build userOp',
25+
error: error.message
26+
})
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as chains from 'viem/chains'
2+
import { Chain } from 'viem/chains'
3+
4+
export function getChainById(chainId: number): Chain {
5+
for (const chain of Object.values(chains)) {
6+
if (chain.id === chainId) {
7+
return chain
8+
}
9+
}
10+
11+
throw new Error(`Chain with id ${chainId} not found`)
12+
}

advanced/wallets/react-wallet-v2/src/utils/SmartAccountUtil.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import { BiconomySmartAccountLib } from './../lib/smart-accounts/BiconomySmartAccountLib'
2-
import { Hex } from 'viem'
2+
import { Hex, Chain as ViemChain } from 'viem'
33
import { SessionTypes } from '@walletconnect/types'
44
import { Chain, allowedChains } from '@/consts/smartAccounts'
55
import { KernelSmartAccountLib } from '@/lib/smart-accounts/KernelSmartAccountLib'
66
import { sepolia } from 'viem/chains'
77
import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib'
88
import { SmartAccountLib } from '@/lib/smart-accounts/SmartAccountLib'
99

10-
export type UrlConfig = {
11-
chain: Chain
12-
}
13-
1410
// Entrypoints [I think this is constant but JIC]
1511
export const ENTRYPOINT_ADDRESSES: Record<Chain['name'], Hex> = {
1612
Sepolia: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
@@ -34,14 +30,14 @@ export const USDC_ADDRESSES: Record<Chain['name'], Hex> = {
3430
}
3531

3632
// RPC URLs
37-
export const RPC_URLS: Record<Chain['name'], string> = {
33+
export const RPC_URLS: Record<ViemChain['name'], string> = {
3834
Sepolia: 'https://rpc.ankr.com/eth_sepolia',
3935
'Polygon Mumbai': 'https://mumbai.rpc.thirdweb.com',
4036
Goerli: 'https://ethereum-goerli.publicnode.com'
4137
}
4238

4339
// Pimlico RPC names
44-
export const PIMLICO_NETWORK_NAMES: Record<Chain['name'], string> = {
40+
export const PIMLICO_NETWORK_NAMES: Record<ViemChain['name'], string> = {
4541
Sepolia: 'sepolia',
4642
'Polygon Mumbai': 'mumbai',
4743
Goerli: 'goerli'
@@ -144,3 +140,36 @@ export async function createOrRestoreBiconomySmartAccount(privateKey: string) {
144140
biconomySmartAccountAddress: address
145141
}
146142
}
143+
144+
export type UrlConfig = {
145+
chain: Chain | ViemChain
146+
}
147+
148+
export const publicClientUrl = ({ chain }: UrlConfig) => {
149+
return process.env.NEXT_PUBLIC_LOCAL_CLIENT_URL || publicRPCUrl({ chain })
150+
}
151+
152+
export const paymasterUrl = ({ chain }: UrlConfig) => {
153+
const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
154+
if (apiKey == null) {
155+
throw new Error('Pimlico API Key not set')
156+
}
157+
158+
const localPaymasterUrl = process.env.NEXT_PUBLIC_LOCAL_PAYMASTER_URL
159+
if (localPaymasterUrl) {
160+
return localPaymasterUrl
161+
}
162+
return `https://api.pimlico.io/v2/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}`
163+
}
164+
165+
export const bundlerUrl = ({ chain }: UrlConfig) => {
166+
const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY
167+
if (apiKey == null) {
168+
throw new Error('Pimlico API Key not set')
169+
}
170+
const localBundlerUrl = process.env.NEXT_PUBLIC_LOCAL_BUNDLER_URL
171+
if (localBundlerUrl) {
172+
return localBundlerUrl
173+
}
174+
return `https://api.pimlico.io/v1/${PIMLICO_NETWORK_NAMES[chain.name]}/rpc?apikey=${apiKey}`
175+
}

0 commit comments

Comments
 (0)