datahaven/test/scripts/set-datahaven-parameters.ts
Gonza Montiel 7097767021
feat: contracts upgrade command (#463)
## 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
2026-03-02 21:50:10 +01:00

102 lines
3 KiB
TypeScript

import { parseArgs } from "node:util";
import { datahaven } from "@polkadot-api/descriptors";
import { createClient } from "polkadot-api";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/node";
import { getEvmEcdsaSigner, logger, SUBSTRATE_FUNDED_ACCOUNTS } from "utils";
import { parseJsonToParameters } from "utils/types";
/**
* Sets DataHaven runtime parameters on the specified RPC URL from a JSON file.
*/
export const setDataHavenParameters = async (
rpcUrl: string,
parametersFilePath: string
): Promise<boolean> => {
const parametersJson = await Bun.file(parametersFilePath).json();
const parameters = parseJsonToParameters(parametersJson).filter((p) => p.value !== undefined);
if (parameters.length === 0) {
logger.warn("⚠️ No parameters to set.");
return false;
}
const client = createClient(withPolkadotSdkCompat(getWsProvider(rpcUrl)));
try {
const dhApi = client.getTypedApi(datahaven);
const signer = getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey);
// Log parameters being set
for (const p of parameters) {
logger.debug(`🔧 Setting ${p.name} = ${p.value!.asHex()}`);
}
// Build parameter calls
const calls = parameters.map(
(p) =>
dhApi.tx.Parameters.set_parameter({
key_value: {
type: "RuntimeConfig",
value: { type: p.name as any, value: [p.value] }
}
}).decodedCall
);
// Batch all calls and wrap in sudo
const tx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.Utility.batch_all({ calls }).decodedCall
});
const result = await tx.signAndSubmit(signer);
// sudo always returns Ok at the extrinsic level — check the Sudid event
// for the inner call result
const sudidEvent = result.events.find(
(e: any) => e.type === "Sudo" && e.value?.type === "Sudid"
);
if (!sudidEvent) {
logger.error("❌ Sudo.Sudid event not found in transaction events");
return false;
}
const sudoResult = (sudidEvent.value as any).value.sudo_result;
if (sudoResult.type === "Err") {
logger.error(`❌ Sudo inner call failed: ${JSON.stringify(sudoResult)}`);
return false;
}
logger.success("Runtime parameters set successfully");
return true;
} catch (error) {
logger.error(`${error instanceof Error ? error.message : error}`);
return false;
} finally {
client.destroy();
}
};
// CLI entry point
if (import.meta.main) {
const { values } = parseArgs({
args: process.argv,
options: {
rpcUrl: { type: "string", short: "r" },
parametersFile: { type: "string", short: "f" }
},
strict: true
});
if (!values.rpcUrl || !values.parametersFile) {
console.error("Usage: --rpc-url <url> --parameters-file <path>");
process.exit(1);
}
setDataHavenParameters(values.rpcUrl, values.parametersFile)
.then((ok) => process.exit(ok ? 0 : 1))
.catch((e) => {
console.error(e);
process.exit(1);
});
}