import { Address, concat, encodeAbiParameters, encodeFunctionData, Hex, isAddressEqual, } from 'viem'; import { SMART_WALLET_FACTORY_WRAPPER, UserOperation, } from '@prex0/prex-structs'; import { PrexUser } from '../types'; import { PrexSigner } from '../core/sign'; import { PrexApiService } from '../api'; import { PrexClient } from '../prex-client'; import { normalizeErrorFn, PrexSDKError } from '../errors'; import { SmartWalletFactoryAbi } from '../abis/SmartWalletFactory'; import { getIsSmartWallet } from '../evm-api/is-smart-wallet'; export class ExecuteOperationAction { constructor( private client: PrexClient, private user: PrexUser, private signer: PrexSigner, private apiService: PrexApiService ) {} async executeOperation( target: Address, callData: Hex, options: { from?: Address; } ) { const sender = options.from || this.user.address; const callGasLimit = await this.client.evmChainClient.estimateGas({ account: sender, to: target, data: callData, }); const config = await this.client.loadConfig(this.apiService.chainId); this.client.logger.debug('callGasLimit', callGasLimit); const userOperation = UserOperation.createUserOperation( sender, await this.client.getNonce(sender), target, await this.getInitCode(sender), // initCode callData, BigInt(this.apiService.chainId), { maxFeePerGas: config.maxFeePerGas, maxPriorityFeePerGas: config.maxPriorityFeePerGas, callGasLimit: (callGasLimit * 101n) / 100n, } ); return this.signUserOperation(userOperation, sender); } async executeWithCreateSharedWallet( target: Address, callData: Hex, owners: Address[], nonce: number, sharedWalletAddress: Address ) { const callGasLimit = await this.client.evmChainClient.estimateGas({ account: sharedWalletAddress, to: target, data: callData, }); const config = await this.client.loadConfig(this.apiService.chainId); this.client.logger.debug( 'executeWithCreateSharedWallet.callGasLimit', callGasLimit ); const userOperation = UserOperation.createUserOperation( sharedWalletAddress, await this.client.getNonce(sharedWalletAddress), target, await this.createInitCodeWithAddress(owners, nonce), // initCode callData, BigInt(this.apiService.chainId), { maxFeePerGas: config.maxFeePerGas, maxPriorityFeePerGas: config.maxPriorityFeePerGas, callGasLimit: (callGasLimit * 101n) / 100n, } ); return this.signUserOperation(userOperation, sharedWalletAddress); } async executeOperationWithoutChainIdValidation( callData: Hex, options: { from?: Address; } ) { const sender = options.from || this.user.address; const callGasLimit = await this.client.evmChainClient.estimateGas({ account: sender, to: sender, data: callData, }); const config = await this.client.loadConfig(this.apiService.chainId); this.client.logger.debug('callGasLimit', callGasLimit); const userOperation = UserOperation.createUserOperationWithoutChainIdValidation( sender, await this.client.getNonce(sender, true), await this.getInitCode(sender), // initCode callData, BigInt(this.apiService.chainId), { maxFeePerGas: config.maxFeePerGas, maxPriorityFeePerGas: config.maxPriorityFeePerGas, callGasLimit: (callGasLimit * 101n) / 100n, } ); return this.signUserOperation(userOperation, sender, true); } async signUserOperation( userOperation: UserOperation, sender: Address, withoutChainIdValidation: boolean = false ) { const paymasterAndData = await this.apiService.signPaymasterAndData( userOperation ); const newUserOperation = UserOperation.deserialize( userOperation.serialize(paymasterAndData), BigInt(this.apiService.chainId) ); this.client.logger.debug('create user operation', newUserOperation); const opHash = withoutChainIdValidation ? await newUserOperation.hashWithoutChainIdValidation() : await newUserOperation.hash(); this.client.logger.debug('create user operation hash', opHash); const signature = this.user.backupMode && this.user.privateKey ? await this.signer.signHashByPrivateKey(opHash, this.user.privateKey) : await this.signer.signHash(opHash, sender); this.client.logger.debug('signed user operation hash'); try { const { hash } = await this.apiService.execute( newUserOperation, signature ); return { hash }; } catch (e) { const normalizedError = normalizeErrorFn('Failed to execute operation'); const error = normalizedError(e); this.client.logger.error(`Failed to execute operation: ${error}`); throw PrexSDKError.fromError(error); } } async getInitCode(sender: Address) { const isSmartWallet = await getIsSmartWallet( this.client.evmChainClient, sender ); this.client.logger.debug('isSmartWallet', isSmartWallet); if (isSmartWallet) { return '0x'; } const isMainAddress = isAddressEqual(sender, this.user.address); this.client.logger.debug('isMainAddress', isMainAddress); if (!isMainAddress) { return '0x'; } // if sender is main address, use passkey public key, otherwise use main address as public key const publicKey = this.user.passkeys[0].publicKey; if (!publicKey) { return '0x'; } return await this.createInitCode(publicKey); } async createInitCode(passkeyPublicKey: Hex) { // TODO: どのナンスでsenderが実現できるかチェックする? const callData = encodeFunctionData({ abi: SmartWalletFactoryAbi, functionName: 'createAccount', args: [[passkeyPublicKey], 0n], }); return concat([SMART_WALLET_FACTORY_WRAPPER, callData]); } async createInitCodeWithAddress(addressList: Address[], nonce: number) { const owners = addressList.map((address) => encodeAbiParameters([{ type: 'address' }], [address]) ); const callData = encodeFunctionData({ abi: SmartWalletFactoryAbi, functionName: 'createAccount', args: [owners, BigInt(nonce)], }); return concat([SMART_WALLET_FACTORY_WRAPPER, callData]); } }