-
Notifications
You must be signed in to change notification settings - Fork 362
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
1bfd393
commit 5825941
Showing
10 changed files
with
361 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.