mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Contracts upgrade command with simple version tracking This PR aims to take the most minimal changes from #438 to make the upgrade command available. So it adds the `bun cli contracts upgrade` command for deploying a new `DataHavenServiceManager` implementation and upgrading the proxy, and includes a simple version tracking via a `contracts/VERSION` file. ### Contracts **`DataHavenServiceManager.sol`** - Added `_version` storage variable - Added `DATAHAVEN_VERSION()` view function, - Added `updateVersion(string)` function gated by `onlyProxyAdmin` - Added `VersionUpdated` event - The version is set at initialization and updated atomically with proxy upgrades via `upgradeAndCall`. ### CLI **`bun cli contracts upgrade`** works in two modes: _dry-run_ or _execute_. **Dry-run (default)** Deploys the new implementation on-chain (signed by the deployer key), then prints a ready-to-submit JSON payload for the multisig to execute the proxy upgrade. No AVS owner key required. ```bash # Uses version from contracts/VERSION (standard workflow) bun cli contracts upgrade --chain hoodi # Override version for this upgrade only (warns if it differs from contracts/VERSION) bun cli contracts upgrade --chain hoodi --target x.y.z ``` Example output: ```json { "to": "0xProxyAdmin...", "value": "0", "data": "0x...", "description": "Upgrade ServiceManager proxy to 0xNewImpl... and set version to 1.1.0" } ``` **Execute mode (`--execute`)** Deploys the implementation and broadcasts the proxy upgrade + version update in a single atomic `upgradeAndCall` transaction. Requires `AVS_OWNER_PRIVATE_KEY`. Used mostly for testing. ```bash bun cli contracts upgrade --chain anvil --execute ``` --- ### Expected flow - Bump mannually contracts/VERSION (e.g., 1.1.0) - Run bun cli contracts upgrade --chain anvil|hoodi|mainnet
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
// Update validator set on DataHaven substrate chain
|
|
import { $ } from "bun";
|
|
import invariant from "tiny-invariant";
|
|
import { logger } from "../utils/index";
|
|
|
|
interface UpdateValidatorSetOptions {
|
|
rpcUrl: string;
|
|
targetEra?: bigint;
|
|
}
|
|
|
|
/**
|
|
* Sends the validator set to the DataHaven chain through Snowbridge
|
|
*
|
|
* @param options - Configuration options for update
|
|
* @param options.rpcUrl - The RPC URL to connect to
|
|
* @returns Promise resolving to true if validator set was sent successfully, false if skipped
|
|
*/
|
|
export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Promise<boolean> => {
|
|
const { rpcUrl } = options;
|
|
|
|
// Validate RPC URL
|
|
invariant(rpcUrl, "❌ RPC URL is required");
|
|
|
|
// Get cast path for transactions
|
|
const { stdout: castPath } = await $`which cast`.quiet();
|
|
const castExecutable = castPath.toString().trim();
|
|
|
|
// Get the owner's private key for transaction signing from the .env
|
|
const ownerPrivateKey =
|
|
process.env.AVS_OWNER_PRIVATE_KEY ||
|
|
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e"; // Sixth pre-funded account from Anvil
|
|
|
|
// Get deployed contract addresses from the deployments file
|
|
const deploymentPath = path.resolve("../contracts/deployments/anvil.json");
|
|
|
|
if (!fs.existsSync(deploymentPath)) {
|
|
logger.error(`Deployment file not found: ${deploymentPath}`);
|
|
return false;
|
|
}
|
|
|
|
const deployments = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
|
|
|
|
// Prepare command to send validator set
|
|
const serviceManagerAddress = deployments.ServiceManager;
|
|
invariant(serviceManagerAddress, "ServiceManager address not found in deployments");
|
|
|
|
// Security Note: Private key is passed via PRIVATE_KEY env var (not in argv) to avoid exposure in process lists.
|
|
// Using cast to send the transaction
|
|
const executionFee = "100000000000000000"; // 0.1 ETH
|
|
const relayerFee = "200000000000000000"; // 0.2 ETH
|
|
const value = "300000000000000000"; // 0.3 ETH (sum of fees)
|
|
const targetEra = options.targetEra ?? 1n;
|
|
|
|
if (options.targetEra === undefined) {
|
|
logger.warn(
|
|
"No target era specified; defaulting to era 1. Use --target-era for already-running networks."
|
|
);
|
|
}
|
|
|
|
const sendCommand = `${castExecutable} send --private-key $PRIVATE_KEY --value ${value} ${serviceManagerAddress} "sendNewValidatorSetForEra(uint64,uint128,uint128)" ${targetEra} ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
|
|
|
|
logger.debug(`Running command: ${sendCommand}`);
|
|
|
|
const { exitCode, stderr } = await $`sh -c ${sendCommand}`
|
|
.env({ ...process.env, PRIVATE_KEY: ownerPrivateKey })
|
|
.nothrow()
|
|
.quiet();
|
|
|
|
if (exitCode !== 0) {
|
|
logger.error(`Failed to send validator set: ${stderr.toString()}`);
|
|
return false;
|
|
}
|
|
|
|
logger.success("Validator set sent to Snowbridge Gateway");
|
|
|
|
// Check if the validator set has been queued on the substrate side (placeholder)
|
|
logger.debug("Checking validator set on substrate chain (not implemented)");
|
|
/*
|
|
// PLACEHOLDER: Code to check if validator set has been queued on substrate
|
|
// This requires a connection to the DataHaven substrate node which is not available yet
|
|
|
|
// Example of what this might look like:
|
|
const substrateApi = await ApiPromise.create({ provider: new WsProvider('ws://localhost:9944') });
|
|
const validatorSetModule = substrateApi.query.validatorSet;
|
|
const queuedValidators = await validatorSetModule.queuedValidators();
|
|
|
|
if (queuedValidators.length === validators.length) {
|
|
logger.success('Validator set successfully queued on substrate chain');
|
|
} else {
|
|
logger.warn('Validator set not properly queued on substrate chain');
|
|
}
|
|
*/
|
|
|
|
return true;
|
|
};
|
|
|
|
// Allow script to be run directly with CLI arguments
|
|
if (import.meta.main) {
|
|
const args = process.argv.slice(2);
|
|
const options: {
|
|
rpcUrl?: string;
|
|
targetEra?: bigint;
|
|
} = {};
|
|
|
|
// Extract RPC URL
|
|
const rpcUrlIndex = args.indexOf("--rpc-url");
|
|
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
|
|
options.rpcUrl = args[rpcUrlIndex + 1];
|
|
}
|
|
|
|
// Extract target era
|
|
const targetEraIndex = args.indexOf("--target-era");
|
|
if (targetEraIndex !== -1 && targetEraIndex + 1 < args.length) {
|
|
options.targetEra = BigInt(args[targetEraIndex + 1]);
|
|
}
|
|
|
|
// Check required parameters
|
|
if (!options.rpcUrl) {
|
|
console.error("Error: --rpc-url parameter is required");
|
|
process.exit(1);
|
|
}
|
|
|
|
// Run update
|
|
updateValidatorSet({
|
|
rpcUrl: options.rpcUrl,
|
|
targetEra: options.targetEra
|
|
}).catch((error) => {
|
|
console.error("Validator set update failed:", error);
|
|
process.exit(1);
|
|
});
|
|
}
|