@abstract-foundation/agw-client
Version:
Abstract Global Wallet Client SDK
215 lines • 8.86 kB
JavaScript
import { BaseError, encodeFunctionData, ExecutionRevertedError, formatGwei, keccak256, RpcRequestError, toBytes, } from 'viem';
import {} from 'viem/accounts';
import { estimateGas, getBalance, getTransactionCount, } from 'viem/actions';
import { assertRequest, getAction, parseAccount, } from 'viem/utils';
import { estimateFee, } from 'viem/zksync';
import { CONTRACT_DEPLOYER_ADDRESS, 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, 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;
}));
}
// Get nonce if needed
if (parameterNames.includes('nonce') &&
typeof nonce === 'undefined' &&
initiatorAccount) {
asyncOperations.push(getAction(publicClient, getTransactionCount, 'getTransactionCount')({
address: initiatorAccount.address,
blockTag: 'pending',
}).then((nonce) => {
request.nonce = nonce;
}));
}
let gasLimitFromFeeEstimation;
// Estimate fees if needed
if (parameterNames.includes('fees')) {
if (typeof request.maxFeePerGas === 'undefined' ||
typeof request.maxPriorityFeePerGas === 'undefined') {
asyncOperations.push((async () => {
let maxFeePerGas;
let maxPriorityFeePerGas;
// Skip fee estimation for contract deployments
if (request.to === CONTRACT_DEPLOYER_ADDRESS) {
maxFeePerGas = 25000000n;
maxPriorityFeePerGas = 0n;
}
else {
const estimateFeeRequest = {
account: initiatorAccount,
to: request.to,
value: request.value,
data: request.data,
gas: request.gas,
nonce: request.nonce,
chainId: request.chainId,
authorizationList: [],
};
let feeEstimation;
try {
feeEstimation = await estimateFee(publicClient, estimateFeeRequest);
}
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;
}
maxFeePerGas = feeEstimation.maxFeePerGas;
maxPriorityFeePerGas = feeEstimation.maxPriorityFeePerGas;
gasLimitFromFeeEstimation = feeEstimation.gasLimit;
}
if (typeof args.maxPriorityFeePerGas === 'undefined' &&
args.maxFeePerGas &&
args.maxFeePerGas < maxPriorityFeePerGas)
throw new MaxFeePerGasTooLowError({
maxPriorityFeePerGas,
});
request.maxPriorityFeePerGas = maxPriorityFeePerGas;
request.maxFeePerGas = maxFeePerGas;
// set gas to gasFromFeeEstimation if gas is not already set
if (typeof gas === 'undefined') {
request.gas = gasLimitFromFeeEstimation;
}
})());
}
}
// 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();
}
// Estimate gas limit if needed
if (parameterNames.includes('gas') &&
typeof gas === 'undefined' &&
gasLimitFromFeeEstimation === undefined // if gas was set by fee estimation, don't estimate again
) {
request.gas = await getAction(client, estimateGas, 'estimateGas')({
...request,
account: initiatorAccount
? { address: initiatorAccount.address, type: 'json-rpc' }
: undefined,
});
}
assertRequest(request);
delete request.parameters;
delete request.isInitialTransaction;
return request;
}
//# sourceMappingURL=prepareTransaction.js.map