268 lines
7.2 KiB
TypeScript
268 lines
7.2 KiB
TypeScript
|
import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||
|
import {
|
||
|
RawSigner,
|
||
|
SUI_CLOCK_OBJECT_ID,
|
||
|
TransactionBlock,
|
||
|
fromB64,
|
||
|
normalizeSuiObjectId,
|
||
|
JsonRpcProvider,
|
||
|
Ed25519Keypair,
|
||
|
testnetConnection,
|
||
|
} from "@mysten/sui.js";
|
||
|
import { execSync } from "child_process";
|
||
|
import { resolve } from "path";
|
||
|
import * as fs from "fs";
|
||
|
|
||
|
const GOVERNANCE_EMITTER =
|
||
|
"0000000000000000000000000000000000000000000000000000000000000004";
|
||
|
|
||
|
const WORMHOLE_STATE_ID =
|
||
|
"0x69ae41bdef4770895eb4e7aaefee5e4673acc08f6917b4856cf55549c4573ca8";
|
||
|
|
||
|
async function main() {
|
||
|
const guardianPrivateKey = process.env.TESTNET_GUARDIAN_PRIVATE_KEY;
|
||
|
if (guardianPrivateKey === undefined) {
|
||
|
throw new Error("TESTNET_GUARDIAN_PRIVATE_KEY unset in environment");
|
||
|
}
|
||
|
|
||
|
const walletPrivateKey = process.env.TESTNET_WALLET_PRIVATE_KEY;
|
||
|
if (walletPrivateKey === undefined) {
|
||
|
throw new Error("TESTNET_WALLET_PRIVATE_KEY unset in environment");
|
||
|
}
|
||
|
|
||
|
const provider = new JsonRpcProvider(testnetConnection);
|
||
|
const wallet = new RawSigner(
|
||
|
Ed25519Keypair.fromSecretKey(
|
||
|
Buffer.from(walletPrivateKey, "base64").subarray(1)
|
||
|
),
|
||
|
provider
|
||
|
);
|
||
|
|
||
|
const srcWormholePath = resolve(`${__dirname}/../../wormhole`);
|
||
|
const dstWormholePath = resolve(`${__dirname}/wormhole`);
|
||
|
|
||
|
// Stage build(s).
|
||
|
setUpWormholeDirectory(srcWormholePath, dstWormholePath);
|
||
|
|
||
|
// Build for digest.
|
||
|
const { modules, dependencies, digest } =
|
||
|
buildForBytecodeAndDigest(dstWormholePath);
|
||
|
|
||
|
// We will use the signed VAA when we execute the upgrade.
|
||
|
const guardians = new mock.MockGuardians(0, [guardianPrivateKey]);
|
||
|
|
||
|
const timestamp = 12345678;
|
||
|
const governance = new mock.GovernanceEmitter(GOVERNANCE_EMITTER);
|
||
|
const published = governance.publishWormholeUpgradeContract(
|
||
|
timestamp,
|
||
|
2,
|
||
|
"0x" + digest.toString("hex")
|
||
|
);
|
||
|
published.writeUInt16BE(21, published.length - 34);
|
||
|
|
||
|
const signedVaa = guardians.addSignatures(published, [0]);
|
||
|
console.log("Upgrade VAA:", signedVaa.toString("hex"));
|
||
|
|
||
|
// And execute upgrade with governance VAA.
|
||
|
const upgradeResults = await buildAndUpgradeWormhole(
|
||
|
wallet,
|
||
|
WORMHOLE_STATE_ID,
|
||
|
modules,
|
||
|
dependencies,
|
||
|
signedVaa
|
||
|
);
|
||
|
|
||
|
console.log("tx digest", upgradeResults.digest);
|
||
|
console.log("tx effects", JSON.stringify(upgradeResults.effects!));
|
||
|
console.log("tx events", JSON.stringify(upgradeResults.events!));
|
||
|
|
||
|
// TODO: grab new package ID from the events above. Do not rely on the RPC
|
||
|
// call because it may give you a stale package ID after the upgrade.
|
||
|
|
||
|
// const migrateResults = await migrateWormhole(
|
||
|
// wallet,
|
||
|
// WORMHOLE_STATE_ID,
|
||
|
// signedVaa
|
||
|
// );
|
||
|
// console.log("tx digest", migrateResults.digest);
|
||
|
// console.log("tx effects", JSON.stringify(migrateResults.effects!));
|
||
|
// console.log("tx events", JSON.stringify(migrateResults.events!));
|
||
|
|
||
|
// Clean up.
|
||
|
cleanUpPackageDirectory(dstWormholePath);
|
||
|
}
|
||
|
|
||
|
main();
|
||
|
|
||
|
// Yeah buddy.
|
||
|
|
||
|
function buildForBytecodeAndDigest(packagePath: string) {
|
||
|
const buildOutput: {
|
||
|
modules: string[];
|
||
|
dependencies: string[];
|
||
|
digest: number[];
|
||
|
} = JSON.parse(
|
||
|
execSync(
|
||
|
`sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
|
||
|
{ encoding: "utf-8" }
|
||
|
)
|
||
|
);
|
||
|
return {
|
||
|
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
|
||
|
dependencies: buildOutput.dependencies.map((d: string) =>
|
||
|
normalizeSuiObjectId(d)
|
||
|
),
|
||
|
digest: Buffer.from(buildOutput.digest),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
async function getPackageId(
|
||
|
provider: JsonRpcProvider,
|
||
|
stateId: string
|
||
|
): Promise<string> {
|
||
|
const state = await provider
|
||
|
.getObject({
|
||
|
id: stateId,
|
||
|
options: {
|
||
|
showContent: true,
|
||
|
},
|
||
|
})
|
||
|
.then((result) => {
|
||
|
if (result.data?.content?.dataType == "moveObject") {
|
||
|
return result.data.content.fields;
|
||
|
}
|
||
|
|
||
|
throw new Error("not move object");
|
||
|
});
|
||
|
|
||
|
if ("upgrade_cap" in state) {
|
||
|
return state.upgrade_cap.fields.package;
|
||
|
}
|
||
|
|
||
|
throw new Error("upgrade_cap not found");
|
||
|
}
|
||
|
|
||
|
async function buildAndUpgradeWormhole(
|
||
|
signer: RawSigner,
|
||
|
wormholeStateId: string,
|
||
|
modules: number[][],
|
||
|
dependencies: string[],
|
||
|
signedVaa: Buffer
|
||
|
) {
|
||
|
const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
||
|
|
||
|
const tx = new TransactionBlock();
|
||
|
|
||
|
const [verifiedVaa] = tx.moveCall({
|
||
|
target: `${wormholePackage}::vaa::parse_and_verify`,
|
||
|
arguments: [
|
||
|
tx.object(wormholeStateId),
|
||
|
tx.pure(Array.from(signedVaa)),
|
||
|
tx.object(SUI_CLOCK_OBJECT_ID),
|
||
|
],
|
||
|
});
|
||
|
const [decreeTicket] = tx.moveCall({
|
||
|
target: `${wormholePackage}::upgrade_contract::authorize_governance`,
|
||
|
arguments: [tx.object(wormholeStateId)],
|
||
|
});
|
||
|
const [decreeReceipt] = tx.moveCall({
|
||
|
target: `${wormholePackage}::governance_message::verify_vaa`,
|
||
|
arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
||
|
typeArguments: [`${wormholePackage}::upgrade_contract::GovernanceWitness`],
|
||
|
});
|
||
|
|
||
|
// Authorize upgrade.
|
||
|
const [upgradeTicket] = tx.moveCall({
|
||
|
target: `${wormholePackage}::upgrade_contract::authorize_upgrade`,
|
||
|
arguments: [tx.object(wormholeStateId), decreeReceipt],
|
||
|
});
|
||
|
|
||
|
// Build and generate modules and dependencies for upgrade.
|
||
|
const [upgradeReceipt] = tx.upgrade({
|
||
|
modules,
|
||
|
dependencies,
|
||
|
packageId: wormholePackage,
|
||
|
ticket: upgradeTicket,
|
||
|
});
|
||
|
|
||
|
// Commit upgrade.
|
||
|
tx.moveCall({
|
||
|
target: `${wormholePackage}::upgrade_contract::commit_upgrade`,
|
||
|
arguments: [tx.object(wormholeStateId), upgradeReceipt],
|
||
|
});
|
||
|
|
||
|
// Cannot auto compute gas budget, so we need to configure it manually.
|
||
|
// Gas ~215m.
|
||
|
//tx.setGasBudget(1_000_000_000n);
|
||
|
|
||
|
return signer.signAndExecuteTransactionBlock({
|
||
|
transactionBlock: tx,
|
||
|
options: {
|
||
|
showEffects: true,
|
||
|
showEvents: true,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async function migrateWormhole(
|
||
|
signer: RawSigner,
|
||
|
wormholeStateId: string,
|
||
|
signedUpgradeVaa: Buffer
|
||
|
) {
|
||
|
const contractPackage = await getPackageId(signer.provider, wormholeStateId);
|
||
|
|
||
|
const tx = new TransactionBlock();
|
||
|
tx.moveCall({
|
||
|
target: `${contractPackage}::migrate::migrate`,
|
||
|
arguments: [
|
||
|
tx.object(wormholeStateId),
|
||
|
tx.pure(Array.from(signedUpgradeVaa)),
|
||
|
tx.object(SUI_CLOCK_OBJECT_ID),
|
||
|
],
|
||
|
});
|
||
|
|
||
|
return signer.signAndExecuteTransactionBlock({
|
||
|
transactionBlock: tx,
|
||
|
options: {
|
||
|
showEffects: true,
|
||
|
showEvents: true,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function setUpWormholeDirectory(
|
||
|
srcWormholePath: string,
|
||
|
dstWormholePath: string
|
||
|
) {
|
||
|
fs.cpSync(srcWormholePath, dstWormholePath, { recursive: true });
|
||
|
|
||
|
// Remove irrelevant files. This part is not necessary, but is helpful
|
||
|
// for debugging a clean package directory.
|
||
|
const removeThese = [
|
||
|
"Move.devnet.toml",
|
||
|
"Move.lock",
|
||
|
"Makefile",
|
||
|
"README.md",
|
||
|
"build",
|
||
|
];
|
||
|
for (const basename of removeThese) {
|
||
|
fs.rmSync(`${dstWormholePath}/${basename}`, {
|
||
|
recursive: true,
|
||
|
force: true,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Fix Move.toml file.
|
||
|
const moveTomlPath = `${dstWormholePath}/Move.toml`;
|
||
|
const moveToml = fs.readFileSync(moveTomlPath, "utf-8");
|
||
|
fs.writeFileSync(
|
||
|
moveTomlPath,
|
||
|
moveToml.replace(`wormhole = "_"`, `wormhole = "0x0"`),
|
||
|
"utf-8"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function cleanUpPackageDirectory(packagePath: string) {
|
||
|
fs.rmSync(packagePath, { recursive: true, force: true });
|
||
|
}
|