From b9d266f07d7cc0788b1251e4d144ecd010ed03c4 Mon Sep 17 00:00:00 2001 From: Karandeep Singh <90941366+KannuSingh@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:41:27 -0400 Subject: [PATCH] Feat: Enable sendUserOp for userOpBuilder service (#680) * chores:run prettier * add EncodeLib for SmartSession formatSignature * formatSignature implementation * add sendUserOp implementation * remove signature field from sendUserOp body * chores: refactor code use viem methods for encoding/decoding --- advanced/wallets/react-wallet-v2/package.json | 1 + .../src/lib/smart-accounts/SmartAccountLib.ts | 7 +- .../lib/smart-accounts/builders/EncodeLib.ts | 40 +++ .../builders/SafeUserOpBuilder.ts | 80 +++++- .../builders/SmartSessionUserOpBuilder.ts | 232 ++++++++++++++++++ .../smart-accounts/builders/UserOpBuilder.ts | 9 +- .../src/pages/api/sendUserOp.ts | 30 +++ advanced/wallets/react-wallet-v2/yarn.lock | 5 + 8 files changed, 388 insertions(+), 16 deletions(-) create mode 100644 advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/EncodeLib.ts create mode 100644 advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SmartSessionUserOpBuilder.ts create mode 100644 advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts diff --git a/advanced/wallets/react-wallet-v2/package.json b/advanced/wallets/react-wallet-v2/package.json index 238058115..cb140ce6e 100644 --- a/advanced/wallets/react-wallet-v2/package.json +++ b/advanced/wallets/react-wallet-v2/package.json @@ -54,6 +54,7 @@ "react-dom": "17.0.2", "react-hot-toast": "^2.4.1", "react-qr-reader-es6": "2.2.1-2", + "solady": "^0.0.234", "tronweb": "^4.4.0", "valtio": "1.13.2", "viem": "2.17.8", 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 562910958..f44db06f0 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,12 @@ import { createSmartAccountClient } from 'permissionless' import { PimlicoBundlerActions, pimlicoBundlerActions } from 'permissionless/actions/pimlico' -import { PIMLICO_NETWORK_NAMES, publicClientUrl, publicRPCUrl, UrlConfig } 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' diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/EncodeLib.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/EncodeLib.ts new file mode 100644 index 000000000..52aab3751 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/EncodeLib.ts @@ -0,0 +1,40 @@ +import { SmartSessionMode } from '@biconomy/permission-context-builder' +import { LibZip } from 'solady' +import { Address, encodeAbiParameters, encodePacked, Hex } from 'viem' +import { enableSessionsStructAbi } from './SmartSessionUserOpBuilder' + +type EnableSessions = { + isigner: Address + isignerInitData: Hex + userOpPolicies: readonly { policy: Address; initData: Hex }[] + erc1271Policies: readonly { policy: Address; initData: Hex }[] + actions: readonly { + actionId: Hex + actionPolicies: readonly { policy: Address; initData: Hex }[] + }[] + permissionEnableSig: Hex +} + +export function packMode(data: Hex, mode: SmartSessionMode, signerId: Hex): Hex { + return encodePacked(['uint8', 'bytes32', 'bytes'], [mode, signerId, data]) +} + +export function encodeUse(signerId: Hex, sig: Hex) { + const data = encodeAbiParameters([{ type: 'bytes' }], [sig]) + const compressedData = LibZip.flzCompress(data) as Hex + return packMode(compressedData, SmartSessionMode.USE, signerId) +} + +export function encodeEnable(signerId: Hex, sig: Hex, enableData: EnableSessions) { + const data = encodeAbiParameters( + [enableSessionsStructAbi[0], { type: 'bytes' }], + [ + { + ...enableData + }, + sig + ] + ) + const compressedData = LibZip.flzCompress(data) as Hex + return packMode(compressedData, SmartSessionMode.UNSAFE_ENABLE, signerId) +} 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 index 7fc64e884..6349eb108 100644 --- 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 @@ -2,25 +2,28 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { FillUserOpParams, FillUserOpResponse, - SendUserOpWithSigantureParams, - SendUserOpWithSigantureResponse, + SendUserOpWithSignatureParams, + SendUserOpWithSignatureResponse, UserOpBuilder } from './UserOpBuilder' import { Address, Chain, createPublicClient, - GetStorageAtReturnType, Hex, http, + pad, parseAbi, PublicClient, - trim + trim, + zeroAddress } from 'viem' import { signerToSafeSmartAccount } from 'permissionless/accounts' import { createSmartAccountClient, ENTRYPOINT_ADDRESS_V07, + getAccountNonce, + getPackedUserOperation, getUserOperationHash } from 'permissionless' import { @@ -31,6 +34,7 @@ import { bundlerUrl, paymasterUrl, publicClientUrl } from '@/utils/SmartAccountU import { getChainById } from '@/utils/ChainUtil' import { SAFE_FALLBACK_HANDLER_STORAGE_SLOT } from '@/consts/smartAccounts' +import { formatSignature } from './SmartSessionUserOpBuilder' const ERC_7579_LAUNCHPAD_ADDRESS: Address = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE' @@ -93,15 +97,33 @@ export class SafeUserOpBuilder implements UserOpBuilder { chain: this.chain, bundlerTransport, middleware: { - sponsorUserOperation: paymasterClient.sponsorUserOperation, // optional + sponsorUserOperation: + params.capabilities.paymasterService && paymasterClient.sponsorUserOperation, // optional gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast // if using pimlico bundler } }) const account = smartAccountClient.account + const validatorAddress = (params.capabilities.permissions?.context.slice(0, 42) || + zeroAddress) as Address + let nonce: bigint = await getAccountNonce(this.publicClient, { + sender: this.accountAddress, + entryPoint: ENTRYPOINT_ADDRESS_V07, + key: BigInt( + pad(validatorAddress, { + dir: 'right', + size: 24 + }) || 0 + ) + }) + const userOp = await smartAccountClient.prepareUserOperationRequest({ userOperation: { - callData: await account.encodeCallData(params.calls) + nonce: nonce, + callData: await account.encodeCallData(params.calls), + callGasLimit: BigInt('0x1E8480'), + verificationGasLimit: BigInt('0x1E8480'), + preVerificationGas: BigInt('0x1E8480') }, account: account }) @@ -115,10 +137,48 @@ export class SafeUserOpBuilder implements UserOpBuilder { hash } } - sendUserOpWithSignature( - params: SendUserOpWithSigantureParams - ): Promise { - throw new Error('Method not implemented.') + async sendUserOpWithSignature( + params: SendUserOpWithSignatureParams + ): Promise { + const { userOp, permissionsContext } = params + if (permissionsContext) { + const formattedSignature = await formatSignature(this.publicClient, { + signature: userOp.signature, + permissionsContext, + accountAddress: userOp.sender + }) + userOp.signature = formattedSignature + } + const bundlerTransport = http(bundlerUrl({ chain: this.chain }), { + timeout: 30000 + }) + const pimlicoBundlerClient = createPimlicoBundlerClient({ + chain: this.chain, + transport: bundlerTransport, + entryPoint: ENTRYPOINT_ADDRESS_V07 + }) + + const userOpHash = await pimlicoBundlerClient.sendUserOperation({ + userOperation: { + ...userOp, + callData: userOp.callData, + callGasLimit: BigInt(userOp.callGasLimit), + nonce: BigInt(userOp.nonce), + preVerificationGas: BigInt(userOp.preVerificationGas), + verificationGasLimit: BigInt(userOp.verificationGasLimit), + sender: userOp.sender, + signature: userOp.signature, + maxFeePerGas: BigInt(userOp.maxFeePerGas), + maxPriorityFeePerGas: BigInt(userOp.maxPriorityFeePerGas) + } + }) + const receipt = await pimlicoBundlerClient.waitForUserOperationReceipt({ + hash: userOpHash + }) + + return { + receipt: receipt.receipt.transactionHash + } } private async getVersion(): Promise { diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SmartSessionUserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SmartSessionUserOpBuilder.ts new file mode 100644 index 000000000..59be819e7 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SmartSessionUserOpBuilder.ts @@ -0,0 +1,232 @@ +import { Address, decodeAbiParameters, getAddress, Hex, PublicClient } from 'viem' +import { encodeEnable, encodeUse } from './EncodeLib' +import { smartSessionAddress } from '@biconomy/permission-context-builder' +import { readContract } from 'viem/actions' + +export const enableSessionsStructAbi = [ + { + components: [ + { + name: 'isigner', + type: 'address' + }, + { + name: 'isignerInitData', + type: 'bytes' + }, + { + name: 'userOpPolicies', + type: 'tuple[]', + components: [ + { + name: 'policy', + type: 'address' + }, + { + name: 'initData', + type: 'bytes' + } + ] + }, + { + name: 'erc1271Policies', + type: 'tuple[]', + components: [ + { + name: 'policy', + type: 'address' + }, + { + name: 'initData', + type: 'bytes' + } + ] + }, + { + name: 'actions', + type: 'tuple[]', + components: [ + { + name: 'actionId', + type: 'bytes32' + }, + { + name: 'actionPolicies', + type: 'tuple[]', + components: [ + { + name: 'policy', + type: 'address' + }, + { + name: 'initData', + type: 'bytes' + } + ] + } + ] + }, + { + name: 'permissionEnableSig', + type: 'bytes' + } + ], + name: 'EnableSessions', + type: 'tuple' + } +] as const + +export const isPermissionsEnabledAbi = [ + { + inputs: [ + { + internalType: 'SignerId', + name: 'signerId', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'account', + type: 'address' + }, + { + components: [ + { + internalType: 'contract ISigner', + name: 'isigner', + type: 'address' + }, + { + internalType: 'bytes', + name: 'isignerInitData', + type: 'bytes' + }, + { + components: [ + { + internalType: 'address', + name: 'policy', + type: 'address' + }, + { + internalType: 'bytes', + name: 'initData', + type: 'bytes' + } + ], + internalType: 'struct PolicyData[]', + name: 'userOpPolicies', + type: 'tuple[]' + }, + { + components: [ + { + internalType: 'address', + name: 'policy', + type: 'address' + }, + { + internalType: 'bytes', + name: 'initData', + type: 'bytes' + } + ], + internalType: 'struct PolicyData[]', + name: 'erc1271Policies', + type: 'tuple[]' + }, + { + components: [ + { + internalType: 'ActionId', + name: 'actionId', + type: 'bytes32' + }, + { + components: [ + { + internalType: 'address', + name: 'policy', + type: 'address' + }, + { + internalType: 'bytes', + name: 'initData', + type: 'bytes' + } + ], + internalType: 'struct PolicyData[]', + name: 'actionPolicies', + type: 'tuple[]' + } + ], + internalType: 'struct ActionData[]', + name: 'actions', + type: 'tuple[]' + }, + { + internalType: 'bytes', + name: 'permissionEnableSig', + type: 'bytes' + } + ], + internalType: 'struct EnableSessions', + name: 'enableData', + type: 'tuple' + } + ], + name: 'isPermissionEnabled', + outputs: [ + { + internalType: 'bool', + name: 'isEnabled', + type: 'bool' + } + ], + stateMutability: 'view', + type: 'function' + } +] as const + +export async function formatSignature( + publicClient: PublicClient, + params: { + signature: Hex + permissionsContext: Hex + accountAddress: Address + } +) { + const { signature, permissionsContext, accountAddress } = params + if (!permissionsContext || permissionsContext.length < 178) { + throw new Error('Permissions context too short') + } + const signerId = `0x${permissionsContext.slice(114, 178)}` as Hex + + if (permissionsContext.length == 178) { + return encodeUse(signerId, signature) + } + + const validatorAddress = permissionsContext.slice(0, 42) as Address + + if (getAddress(validatorAddress) !== getAddress(smartSessionAddress)) { + throw new Error( + `Validator ${validatorAddress} is not the smart session validator ${smartSessionAddress}` + ) + } + + const enableData = `0x${permissionsContext.slice(178)}` as Hex + const enableSession = Array.from(decodeAbiParameters(enableSessionsStructAbi, enableData)) + + const isPermissionsEnabled = await readContract(publicClient, { + address: smartSessionAddress, + abi: isPermissionsEnabledAbi, + functionName: 'isPermissionEnabled', + args: [signerId, accountAddress, enableSession[0]] + }) + + if (isPermissionsEnabled) { + return encodeUse(signerId, signature) + } else { + return encodeEnable(signerId, signature, enableSession[0]) + } +} 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 index 01de2b840..8fccfae7d 100644 --- 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 @@ -24,19 +24,18 @@ export type ErrorResponse = { error: string } -export type SendUserOpWithSigantureParams = { +export type SendUserOpWithSignatureParams = { chainId: Hex userOp: UserOp - signature: Hex permissionsContext?: Hex } -export type SendUserOpWithSigantureResponse = { +export type SendUserOpWithSignatureResponse = { receipt: Hex } export interface UserOpBuilder { fillUserOp(params: FillUserOpParams): Promise sendUserOpWithSignature( - params: SendUserOpWithSigantureParams - ): Promise + params: SendUserOpWithSignatureParams + ): Promise } diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts b/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts new file mode 100644 index 000000000..3a61cee0f --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts @@ -0,0 +1,30 @@ +import { + ErrorResponse, + SendUserOpWithSignatureResponse +} 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.userOp.sender + const chain = getChainById(chainId) + try { + const builder = await getUserOpBuilder({ + account, + chain + }) + const response = await builder.sendUserOpWithSignature(req.body) + + res.status(200).json(response) + } catch (error: any) { + return res.status(200).json({ + message: 'Unable to send userOp', + error: error.message + }) + } +} diff --git a/advanced/wallets/react-wallet-v2/yarn.lock b/advanced/wallets/react-wallet-v2/yarn.lock index 0a372e21f..963c1ad85 100644 --- a/advanced/wallets/react-wallet-v2/yarn.lock +++ b/advanced/wallets/react-wallet-v2/yarn.lock @@ -6610,6 +6610,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +solady@^0.0.234: + version "0.0.234" + resolved "https://registry.yarnpkg.com/solady/-/solady-0.0.234.tgz#99ddd59e38f1987683b465e5f97d6808c2c50a32" + integrity sha512-twY/0NtBbOZ7fGFIqp7Ebuvfgw2yD6A44HKbEpS05RK03VH+bv81/Mg6kYI/7EK/NuATlvrACDdLYfp/c1+d4A== + sonic-boom@^2.2.1: version "2.8.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611"