UNPKG

@abstract-foundation/agw-client

Version:
201 lines 7.75 kB
import { BaseError, encodeFunctionData, ExecutionRevertedError, formatGwei, keccak256, RpcRequestError, toBytes, } from 'viem'; import { estimateGas, getBalance, getChainId as getChainId_, getGasPrice, getTransactionCount, } from 'viem/actions'; import { assertRequest, getAction, parseAccount, } from 'viem/utils'; import { EOA_VALIDATOR_ADDRESS, INSUFFICIENT_BALANCE_SELECTOR, SMART_ACCOUNT_FACTORY_ADDRESS, } from '../constants.js'; import { InsufficientBalanceError } from '../errors/insufficientBalance.js'; import { AccountFactoryAbi } from '../exports/constants.js'; import { isSmartAccountDeployed, transformHexValues } from '../utils.js'; import { getInitializerCalldata } from '../utils.js'; export const defaultParameters = [ 'blobVersionedHashes', 'chainId', 'fees', 'gas', 'nonce', 'type', ]; export class MaxFeePerGasTooLowError extends BaseError { constructor({ maxPriorityFeePerGas }) { super(`\`maxFeePerGas\` cannot be less than the \`maxPriorityFeePerGas\` (${formatGwei(maxPriorityFeePerGas)} gwei).`, { name: 'MaxFeePerGasTooLowError' }); } } /** * Prepares a transaction request for signing. * * - Docs: https://viem.sh/docs/actions/wallet/prepareTransactionRequest * * @param args - {@link PrepareTransactionRequestParameters} * @returns The transaction request. {@link PrepareTransactionRequestReturnType} * * @example * import { createWalletClient, custom } from 'viem' * import { mainnet } from 'viem/chains' * import { prepareTransactionRequest } from 'viem/actions' * * const client = createWalletClient({ * chain: mainnet, * transport: custom(window.ethereum), * }) * const request = await prepareTransactionRequest(client, { * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', * to: '0x0000000000000000000000000000000000000000', * value: 1n, * }) * * @example * // Account Hoisting * import { createWalletClient, http } from 'viem' * import { privateKeyToAccount } from 'viem/accounts' * import { mainnet } from 'viem/chains' * import { prepareTransactionRequest } from 'viem/actions' * * const client = createWalletClient({ * account: privateKeyToAccount('0x…'), * chain: mainnet, * transport: custom(window.ethereum), * }) * const request = await prepareTransactionRequest(client, { * to: '0x0000000000000000000000000000000000000000', * value: 1n, * }) */ export async function prepareTransactionRequest(client, signerClient, publicClient, args) { // transform values in case any are provided in hex format (from rpc) transformHexValues(args, [ 'value', 'nonce', 'maxFeePerGas', 'maxPriorityFeePerGas', 'gas', 'chainId', 'gasPerPubdata', ]); const isSponsored = 'paymaster' in args && 'paymasterInput' in args && args.paymaster !== undefined && args.paymasterInput !== undefined; const { gas, nonce, chain, nonceManager, parameters: parameterNames = defaultParameters, } = args; const isDeployed = await isSmartAccountDeployed(publicClient, client.account.address); if (!isDeployed) { const initialCall = { target: args.to, allowFailure: false, value: args.value ?? 0, callData: args.data ?? '0x', }; // Create calldata for initializing the proxy account const initializerCallData = getInitializerCalldata(signerClient.account.address, EOA_VALIDATOR_ADDRESS, initialCall); const addressBytes = toBytes(signerClient.account.address); const salt = keccak256(addressBytes); const deploymentCalldata = encodeFunctionData({ abi: AccountFactoryAbi, functionName: 'deployAccount', args: [salt, initializerCallData], }); // Override transaction fields args.to = SMART_ACCOUNT_FACTORY_ADDRESS; args.data = deploymentCalldata; } const initiatorAccount = parseAccount(isDeployed ? client.account : signerClient.account); const request = { ...args, from: initiatorAccount.address, }; // Prepare all async operations that can run in parallel const asyncOperations = []; let userBalance; // Get balance if the transaction is not sponsored or has a value if (!isSponsored || (request.value !== undefined && request.value > 0n)) { asyncOperations.push(getBalance(publicClient, { address: initiatorAccount.address, }).then((balance) => { userBalance = balance; })); } let chainId; async function getChainId() { if (chainId) return chainId; if (chain) return chain.id; if (typeof args.chainId !== 'undefined') return args.chainId; const chainId_ = await getAction(client, getChainId_, 'getChainId')({}); chainId = chainId_; return chainId; } // Get nonce if needed if (parameterNames.includes('nonce') && typeof nonce === 'undefined' && initiatorAccount) { if (nonceManager) { asyncOperations.push((async () => { const chainId = await getChainId(); request.nonce = await nonceManager.consume({ address: initiatorAccount.address, chainId, client: publicClient, }); })()); } else { asyncOperations.push(getAction(publicClient, getTransactionCount, 'getTransactionCount')({ address: initiatorAccount.address, blockTag: 'pending', }).then((nonce) => { request.nonce = nonce; })); } } // Estimate fees if needed if (parameterNames.includes('fees')) { if (typeof request.maxFeePerGas === 'undefined') { asyncOperations.push((async () => { request.maxFeePerGas = await getGasPrice(publicClient); request.maxPriorityFeePerGas = 0n; })()); } } // Estimate gas limit if needed if (parameterNames.includes('gas') && typeof gas === 'undefined') { asyncOperations.push((async () => { try { request.gas = await getAction(client, estimateGas, 'estimateGas')({ ...request, account: initiatorAccount ? { address: initiatorAccount.address, type: 'json-rpc' } : undefined, }); } catch (error) { if (error instanceof Error && error.message.includes(INSUFFICIENT_BALANCE_SELECTOR)) { throw new InsufficientBalanceError(); } else if (error instanceof RpcRequestError && error.details.includes('execution reverted')) { throw new ExecutionRevertedError({ message: `${error.data}`, }); } throw error; } })()); } // Wait for all async operations to complete await Promise.all(asyncOperations); // Check if user has enough balance const gasCost = isSponsored || !request.gas || !request.maxFeePerGas ? 0n : request.gas * request.maxFeePerGas; if (userBalance !== undefined && userBalance < (request.value ?? 0n) + gasCost) { throw new InsufficientBalanceError(); } assertRequest(request); delete request.parameters; delete request.isInitialTransaction; delete request.nonceManager; return request; } //# sourceMappingURL=prepareTransaction.js.map