@openzeppelin/hardhat-upgrades
Version:
[](https://docs.openzeppelin.com/upgrades-plugins/hardhat-upgrades) [](https://www.npmjs.org/package/@openzeppelin/h
126 lines • 6.93 kB
JavaScript
import { encodeAbiParameters, encodeDeployData, encodeFunctionData as viemEncodeFunctionData, getAddress, getContractAddress, toFunctionSignature, } from 'viem';
import { UpgradesError } from '@openzeppelin/upgrades-core';
import { waitForReceipt } from '../engine/receipt.js';
/**
* Builds the client-neutral engine binding for the viem API. All transactions are signed and
* broadcast through the given wallet client, so viem local accounts (private key, mnemonic, KMS,
* hardware) are supported natively — viem's own `sendTransaction` branches on the account type.
*/
export function makeViemBinding(hre, connection, walletClient, exec) {
const provider = connection.provider;
const overrides = {
...(exec.value !== undefined ? { value: exec.value } : {}),
...(exec.gas !== undefined ? { gas: exec.gas } : {}),
...(exec.gasPrice !== undefined ? { gasPrice: exec.gasPrice } : {}),
...(exec.maxFeePerGas !== undefined ? { maxFeePerGas: exec.maxFeePerGas } : {}),
...(exec.maxPriorityFeePerGas !== undefined ? { maxPriorityFeePerGas: exec.maxPriorityFeePerGas } : {}),
};
const waitOpts = { timeout: exec.timeout, pollingInterval: exec.pollingInterval };
const send = (request) => {
if (walletClient === undefined) {
// Read-only bindings (validation, force-import) never reach here.
throw new UpgradesError('Broken invariant: a transaction was sent without a wallet client');
}
return walletClient.sendTransaction({
account: walletClient.account,
...request,
...overrides,
});
};
const broadcastDeploy = (info, args, nonce) => send({
data: encodeDeployData({
abi: info.abi,
bytecode: info.bytecode,
...(args.length > 0 ? { args: args } : {}),
}),
...(nonce !== undefined ? { nonce } : {}),
});
return {
hre,
provider,
encodeConstructorArgs(info, args) {
// Validate the constructor argument count up front and ABI-encode just the arguments (no
// bytecode) for the version hash. viem's `encodeDeployData` silently returns the bytecode
// unchanged when no args are passed, so a contract with required constructor parameters
// deployed without `constructorArgs` would otherwise be encoded with no arguments instead of
// failing — `encodeAbiParameters` throws on a length mismatch, matching the ethers binding.
const inputs = constructorInputs(info.abi);
if (inputs.length !== args.length) {
throw new UpgradesError(`Expected ${inputs.length} constructor argument(s) but got ${args.length}`, () => "Provide the implementation contract's constructor arguments with the `constructorArgs` option.");
}
if (inputs.length === 0) {
return '0x';
}
return encodeAbiParameters(inputs, args);
},
encodeFunctionData(abi, fn, args) {
if (fn.includes('(')) {
const item = findBySignature(abi, fn);
if (item === undefined) {
throw new UpgradesError(`The contract has no function matching the signature: ${fn}`);
}
return viemEncodeFunctionData({ abi: [item], functionName: item.name, args: args });
}
return viemEncodeFunctionData({ abi: abi, functionName: fn, args: args });
},
hasFunction(abi, fn) {
if (fn.includes('(')) {
return findBySignature(abi, fn) !== undefined;
}
return abi.some(item => item.type === 'function' && item.name === fn);
},
formatManifestAbi(abi) {
return abi;
},
async getSignerAddress() {
return walletClient?.account?.address;
},
async deployUnconfirmed(info, args) {
// Determine the eventual address before broadcasting: read the sender's pending nonce, send the
// deployment with that explicit nonce, and compute the CREATE address from `(from, nonce)`
// locally — the address depends only on the sender and nonce, not on the bytecode or arguments.
// This mirrors how the ethers binding deploys, and unlike querying the chain for the transaction
// after broadcasting it leaves no step that can fail between the (irreversible) broadcast and
// returning the record, so the engine always records the deployment before it is mined.
// `@openzeppelin/upgrades-core` then confirms it outside the manifest lock, so the lock is not
// held while the contract mines.
const account = walletClient?.account;
if (account === undefined) {
// Read-only bindings (validation, force-import) never reach here.
throw new UpgradesError('Broken invariant: a transaction was sent without a wallet client');
}
const publicClient = await connection.viem.getPublicClient();
const nonce = await publicClient.getTransactionCount({ address: account.address, blockTag: 'pending' });
const txHash = await broadcastDeploy(info, args, nonce);
return { address: getContractAddress({ from: account.address, nonce: BigInt(nonce) }), txHash };
},
async deploy(info, args) {
// Wait for the receipt so the returned address is immediately usable. Used for deployments the
// engine does not confirm itself (the beacon contract); proxies go through `deployProxy`.
const txHash = await broadcastDeploy(info, args);
const receipt = await waitForReceipt(provider, txHash, waitOpts);
if (receipt.contractAddress === undefined || receipt.contractAddress === null) {
throw new UpgradesError(`Deployment transaction ${txHash} did not create a contract`);
}
return { address: getAddress(receipt.contractAddress), txHash };
},
deployProxy(info, args) {
// The viem API has no `proxyFactory`/`deployFunction` escape hatches, so proxies deploy the
// same way as any other contract.
return this.deploy(info, args);
},
async sendTransaction(tx) {
const txHash = await send({ to: tx.to, data: tx.data });
await waitForReceipt(provider, txHash, waitOpts);
return { txHash };
},
};
}
function findBySignature(abi, signature) {
return abi.find((item) => item.type === 'function' && toFunctionSignature(item) === signature);
}
function constructorInputs(abi) {
const constructor = abi.find((item) => item.type === 'constructor');
return constructor?.inputs ?? [];
}
//# sourceMappingURL=viem-binding.js.map