@coinbase/onchaintestkit
Version:
End-to-end testing toolkit for blockchain applications, powered by Playwright
256 lines (255 loc) • 9.98 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SmartContractManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const viem_1 = require("viem");
const accounts_1 = require("viem/accounts");
const ProxyDeployer_1 = require("./ProxyDeployer");
/**
* SmartContractManager handles deploying contracts using CREATE2 and executing contract calls
* using viem instead of ethers for improved performance and reliability.
*/
class SmartContractManager {
constructor(projectRoot) {
this.deployedContracts = new Map(); // Store deployed contract ABIs
this.projectRoot = projectRoot;
}
/**
* Initialize the manager by setting up viem clients
*/
async initialize(node) {
const rpcUrl = node.rpcUrl;
// Create a custom chain configuration based on the node's chain ID
const nodeChainId = await this.getChainId(rpcUrl);
this.chain = (0, viem_1.defineChain)({
id: nodeChainId,
network: "localhost",
name: "Local Test Network",
nativeCurrency: {
decimals: 18,
name: "Ether",
symbol: "ETH",
},
rpcUrls: {
default: { http: [rpcUrl] },
public: { http: [rpcUrl] },
},
});
this.publicClient = (0, viem_1.createPublicClient)({
chain: this.chain,
transport: (0, viem_1.http)(rpcUrl),
});
// Use the first account from Anvil (the default test account)
// default mnemonic: test test test test test test test test test test test junk
const account = (0, accounts_1.privateKeyToAccount)("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
this.walletClient = (0, viem_1.createWalletClient)({
account,
chain: this.chain,
transport: (0, viem_1.http)(rpcUrl),
});
// Initialize proxy deployer
this.proxyDeployer = new ProxyDeployer_1.ProxyDeployer(node);
await this.proxyDeployer.ensureProxyDeployed();
}
/**
* Get the chain ID from the RPC endpoint
*/
async getChainId(rpcUrl) {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_chainId",
params: [],
id: 1,
}),
});
const data = await response.json();
return parseInt(data.result, 16);
}
/**
* Deploy a contract using CREATE2 for deterministic addresses
*/
async deployContract(deployment) {
if (!this.walletClient || !this.publicClient || !this.proxyDeployer) {
throw new Error("SmartContractManager not initialized. Call initialize() first.");
}
const artifact = this.loadArtifact(deployment.name);
// Predict the deployment address
const predictedAddress = this.predictContractAddress(deployment.salt, artifact.bytecode, deployment.args);
// Check if contract is already deployed
const existingCode = await this.publicClient.getBytecode({
address: predictedAddress,
});
if (existingCode && existingCode !== "0x") {
console.log(`Contract ${deployment.name} already deployed at ${predictedAddress}`);
this.deployedContracts.set(predictedAddress, artifact.abi);
return predictedAddress;
}
// Encode deployment data
const deploymentData = (0, viem_1.encodeDeployData)({
abi: artifact.abi,
bytecode: artifact.bytecode,
args: deployment.args,
});
// Prepare CREATE2 transaction data
const proxyAddress = this.proxyDeployer.getProxyAddress();
const create2Data = (0, viem_1.concat)([deployment.salt, deploymentData]);
// Deploy the contract
if (!this.walletClient.account) {
throw new Error("Wallet client account not configured");
}
const hash = await this.walletClient.sendTransaction({
to: proxyAddress,
data: create2Data,
account: this.walletClient.account,
chain: this.chain,
});
// Wait for deployment
await this.publicClient.waitForTransactionReceipt({ hash });
// Store the ABI for later use
this.deployedContracts.set(predictedAddress, artifact.abi);
console.log(`Deployed ${deployment.name} to ${predictedAddress}`);
return predictedAddress;
}
/**
* Execute a contract function call
*/
async executeCall(call) {
if (!this.walletClient || !this.publicClient) {
throw new Error("SmartContractManager not initialized. Call initialize() first.");
}
// Get the ABI for the contract
const abi = this.deployedContracts.get(call.target);
if (!abi) {
throw new Error(`ABI not found for contract at ${call.target}. Deploy the contract first or provide the ABI.`);
}
// Execute the call
if (!this.walletClient.account) {
throw new Error("Wallet client account not configured");
}
const hash = await this.walletClient.writeContract({
address: call.target,
abi,
functionName: call.functionName,
args: call.args,
account: this.walletClient.account,
chain: this.chain,
...(call.value !== undefined && { value: call.value }),
});
console.log(`Executed ${call.functionName} on ${call.target}, tx: ${hash}`);
return hash;
}
/**
* Set the complete contract state (deploy contracts and execute calls)
*/
async setContractState(config, node) {
if (!this.walletClient || !this.publicClient) {
await this.initialize(node);
}
this.validateConfig(config);
// Deploy contracts first
if (config.deployments) {
for (const deployment of config.deployments) {
await this.deployContract(deployment);
}
}
// Then execute calls
if (config.calls) {
for (const call of config.calls) {
await this.executeCall(call);
}
}
}
/**
* Predict the address where a contract will be deployed using CREATE2
*/
predictContractAddress(salt, bytecode, args) {
if (!this.proxyDeployer) {
throw new Error("ProxyDeployer not initialized");
}
const deploymentData = (0, viem_1.encodeDeployData)({
abi: [], // We don't need ABI for address prediction
bytecode,
args,
});
return (0, viem_1.getContractAddress)({
opcode: "CREATE2",
from: this.proxyDeployer.getProxyAddress(),
salt,
bytecode: deploymentData,
});
}
/**
* Load contract artifact from the compiled contracts
*/
loadArtifact(contractName) {
const artifactPath = path.join(this.projectRoot, "out", `${contractName}.sol`, `${contractName}.json`);
if (!fs.existsSync(artifactPath)) {
throw new Error(`Artifact not found: ${artifactPath}. Make sure to compile contracts with 'forge build'.`);
}
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
return {
abi: artifact.abi,
bytecode: artifact.bytecode.object,
};
}
/**
* Validate the setup configuration
*/
validateConfig(config) {
if (!config.deployments && !config.calls) {
throw new Error("Setup config must contain at least deployments or calls");
}
if (config.deployments) {
for (const deployment of config.deployments) {
if (!deployment.name || !deployment.salt || !deployment.deployer) {
throw new Error("Each deployment must have name, salt, and deployer");
}
}
}
if (config.calls) {
for (const call of config.calls) {
if (!call.target || !call.functionName || !call.account) {
throw new Error("Each call must have target, functionName, and account");
}
}
}
}
}
exports.SmartContractManager = SmartContractManager;