diff --git a/bun.lock b/bun.lock index 9b54d7628..5a43f6f4c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "ntt", diff --git a/evm/ts-scripts/.env.sample b/evm/ts-scripts/.env.sample new file mode 100644 index 000000000..b200e26c1 --- /dev/null +++ b/evm/ts-scripts/.env.sample @@ -0,0 +1,3 @@ +WALLET_KEY= +LEDGER_BIP32_PATH= +GUARDIAN_KEY= \ No newline at end of file diff --git a/evm/ts-scripts/.gitignore b/evm/ts-scripts/.gitignore new file mode 100644 index 000000000..22ac52776 --- /dev/null +++ b/evm/ts-scripts/.gitignore @@ -0,0 +1,5 @@ +output +contract-bindings +.env +.env.testnet +.env.mainnet \ No newline at end of file diff --git a/evm/ts-scripts/config/testnet/governance-vaas.json b/evm/ts-scripts/config/testnet/governance-vaas.json new file mode 100644 index 000000000..446c3734a --- /dev/null +++ b/evm/ts-scripts/config/testnet/governance-vaas.json @@ -0,0 +1,18 @@ +[ + { + "chainId": 10002, + "vaa": "AQAAAAABAO9GFw0p7M1k1+dApnFYahEtKd6We4dPSZa+FirgR0BNWAcESgcQvNguSV66noi7/0dY+/Vg4NvyjC3WSPsiTBcAAAAAALXylaUAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExxsVgsLk1ikgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxKVF/AWTB0ImtcuZp5XuQiHkJZtvXOBQe/2WWJfLq1P7s382UFVxn8YAATO/BQp" + }, + { + "chainId": 10003, + "vaa": "AQAAAAABABRt1qnWaWa2nRm22+NDCbmX1SIt6uck4CFZf5OFhrHVP8HhCMWOfobvSmnrAp0aBCyg1Wq/smyZybczTthXnmMAAAAAAL931RgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE0TeyxhuLDpwgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxOBtlpI3KzLoErKPAVcQRKwcVuQwDldPHQjLRKRbsqJUro1K00ngYA1AATO/BQp" + }, + { + "chainId": 10004, + "vaa": "AQAAAAABAH345FjR31wRQxliMQKFNmGmerZZ3VCzfJxnW4LYABHPCGlnicrUHmXSXfx6hMvdr560prlSoJqO8ard2a+dhJwAAAAAAEB58FMAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE19jKl8J3pksgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxRyClkSi5btpuwpQMeJlAbk3FbQ3B0w54t8f7vO+Hrm6XtTibLkcMpKAATO/BQp" + }, + { + "chainId": 10005, + "vaa": "AQAAAAABAGHthpHgkBKYghkiZ9wlsjq/yzfOrqtNQI84ZmrQcQX+NFQG2o1XhL2Gm+/iYZjFpmAX3Z9vVs+iMjstsbNwpZsBAAAAAK7WazAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMpcRWPILd/UgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UBJxXOHeHqSwQNMkoHcZBDpiNMlP0LXd3+q8zyBjzWb1OhIY4jxoG6bnliAATO/BQp" + } +] diff --git a/evm/ts-scripts/package.json b/evm/ts-scripts/package.json new file mode 100644 index 000000000..172ec2232 --- /dev/null +++ b/evm/ts-scripts/package.json @@ -0,0 +1,26 @@ +{ + "name": "ts-scripts", + "version": "1.0.0", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "build": "npx typechain --target=ethers-v5 --out-dir=./contract-bindings ../out/*/*.json" + }, + "author": "", + "license": "ISC", + "overrides": { + "elliptic": "6.6.1" + }, + "devDependencies": { + "@certusone/wormhole-sdk": "^0.10.11", + "@typechain/ethers-v5": "^11.1.2", + "@types/node": "^20.11.22", + "@xlabs-xyz/ledger-signer": "^0.0.3", + "dotenv": "^16.4.5", + "ethers": "^5.8.0", + "typechain": "^8.3.2", + "tsx": "^4.7.2", + "typescript": "^5.4.5" + } +} diff --git a/evm/ts-scripts/src/env.ts b/evm/ts-scripts/src/env.ts new file mode 100644 index 000000000..21a873046 --- /dev/null +++ b/evm/ts-scripts/src/env.ts @@ -0,0 +1,214 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { ethers } from "ethers"; +import fs from "fs"; + +export type ChainInfo = { + evmNetworkId: number; + chainId: ChainId; + rpc: string; + wormholeAddress: string; +}; + +export type Deployment = { + chainId: ChainId; + address: string; +}; + +export type ContractsJson = { + GeneralPurposeGovernances: Deployment[]; +}; + +const DEFAULT_ENV = "testnet"; + +export let env = ""; +let lastRunOverride: boolean | undefined; + +export function init(overrides: { lastRunOverride?: boolean } = {}): string { + env = get_env_var("ENV"); + if (!env) { + console.log( + "No environment was specified, using default environment files" + ); + env = DEFAULT_ENV; + } + lastRunOverride = overrides?.lastRunOverride; + + require("dotenv").config({ + path: `./ts-scripts/.env${env != DEFAULT_ENV ? "." + env : ""}`, + }); + return env; +} + +function get_env_var(env: string): string { + const v = process.env[env]; + return v || ""; +} + +export function loadScriptConfig(filename: string): any { + const configFile = fs.readFileSync( + `./ts-scripts/config/${env}/${filename}.json` + ); + const config = JSON.parse(configFile.toString()); + if (!config) { + throw Error("Failed to pull config file!"); + } + return config; +} + +type ChainConfig = { + chainId: ChainId; +}; + +export async function getChainConfig( + filename: string, + chainId: ChainId +): Promise { + const scriptConfig: T[] = await loadScriptConfig(filename); + + const chainConfig = scriptConfig.find((x) => x.chainId == chainId); + + if (!chainConfig) { + throw Error(`Failed to find chain config for chain ${chainId}`); + } + + return chainConfig; +} + +export function loadOperatingChains(): ChainInfo[] { + const allChains = loadChains(); + let operatingChains: number[] | null = null; + + const chainFile = fs.readFileSync(`./ts-scripts/config/${env}/chains.json`); + const chains = JSON.parse(chainFile.toString()); + if (chains.operatingChains) { + operatingChains = chains.operatingChains; + } + if (!operatingChains) { + return allChains; + } + + const output: ChainInfo[] = []; + operatingChains.forEach((x: number) => { + const item = allChains.find((y) => { + return x == y.chainId; + }); + if (item) { + output.push(item); + } + }); + + return output; +} + +export function loadChains(): ChainInfo[] { + const chainFile = fs.readFileSync(`./ts-scripts/config/${env}/chains.json`); + const chains = JSON.parse(chainFile.toString()); + if (!chains.chains) { + throw Error("Failed to pull chain config file!"); + } + return chains.chains; +} + +export function getChain(chain: ChainId): ChainInfo { + const chains = loadChains(); + const output = chains.find((x) => x.chainId == chain); + if (!output) { + throw Error("bad chain ID"); + } + + return output; +} + +export function loadPrivateKey(): string { + const privateKey = get_env_var("WALLET_KEY"); + if (!privateKey) { + throw Error("Failed to find private key for this process!"); + } + return privateKey; +} + +export function writeOutputFiles(output: any, processName: string) { + fs.mkdirSync(`./ts-scripts/output/${env}/${processName}`, { + recursive: true, + }); + fs.writeFileSync( + `./ts-scripts/output/${env}/${processName}/lastrun.json`, + JSON.stringify(output), + { flag: "w" } + ); + fs.writeFileSync( + `./ts-scripts/output/${env}/${processName}/${Date.now()}.json`, + JSON.stringify(output), + { flag: "w" } + ); +} + +export async function getSigner(chain: ChainInfo): Promise { + const provider = getProvider(chain); + const privateKey = loadPrivateKey(); + + if (privateKey === "ledger") { + if (process.env.LEDGER_BIP32_PATH === undefined) { + throw new Error(`Missing BIP32 derivation path. +With ledger devices the path needs to be specified in env var 'LEDGER_BIP32_PATH'.`); + } + const { LedgerSigner } = await import("@xlabs-xyz/ledger-signer"); + return LedgerSigner.create(provider, process.env.LEDGER_BIP32_PATH); + } + + const signer = new ethers.Wallet(privateKey, provider); + return signer; +} + +export function getProvider( + chain: ChainInfo +): ethers.providers.StaticJsonRpcProvider { + const providerRpc = + loadChains().find((x: any) => x.chainId == chain.chainId)?.rpc || ""; + + if (!providerRpc) { + throw new Error("Failed to find a provider RPC for chain " + chain.chainId); + } + + return new ethers.providers.StaticJsonRpcProvider(providerRpc); +} + +let contracts: ContractsJson; +export function loadContracts() { + if (contracts) { + return contracts; + } + + const contractsFile = fs.readFileSync( + `./ts-scripts/config/${env}/contracts.json` + ); + + if (!contractsFile) { + throw Error("Failed to find contracts file for this process!"); + } + + // NOTE: We assume that the contracts.json file is correctly formed... + contracts = JSON.parse(contractsFile.toString()) as ContractsJson; + + return loadContracts(); +} +type ContractTypes = keyof ContractsJson; + +export async function getAllContracts(contractName: ContractTypes) { + return (await loadContracts())[contractName]; +} + +export async function getContractAddress( + contractName: ContractTypes, + chainId: ChainId +): Promise { + const contract = (await getAllContracts(contractName))?.find( + (c) => c.chainId === chainId + )?.address; + + if (!contract) { + throw new Error(`No ${contractName} contract found for chain ${chainId}`); + } + + return contract; +} diff --git a/evm/ts-scripts/src/executeGovernanceVaa.ts b/evm/ts-scripts/src/executeGovernanceVaa.ts new file mode 100644 index 000000000..72bce9794 --- /dev/null +++ b/evm/ts-scripts/src/executeGovernanceVaa.ts @@ -0,0 +1,88 @@ +import { inspect } from "util"; +import { ChainId } from "@certusone/wormhole-sdk"; + +import { + loadOperatingChains, + init, + ChainInfo, + getChainConfig, + getSigner, + getContractAddress, +} from "./env"; +import { Governance__factory } from "../contract-bindings"; + +const processName = "executeGovernanceVaas"; + +type GovernanceConfig = { + chainId: ChainId; + vaa: string; +}; + +init(); +const chains = loadOperatingChains(); +async function run() { + console.log(`Start ${processName}!`); + + const results = await Promise.all( + chains.map(async (chain) => { + let result; + try { + result = await executeGovernance(chain); + } catch (error: unknown) { + return { chainId: chain.chainId, peerUpdateTxs: [] as string[], error }; + } + + return result; + }) + ); + + for (const result of results) { + if (!result) { + continue; + } + + if ("error" in result) { + console.error( + `${processName} failed for chain ${result.chainId}: ${inspect(result.error)}` + ); + continue; + } + + console.log(`${processName} succeeded for chain ${result.chainId}`); + } +} + +async function executeGovernance(chain: ChainInfo) { + const log = (...args: any[]) => console.log(`[${chain.chainId}]`, ...args); + const { vaa } = await getChainConfig( + "governance-vaas", + chain.chainId + ); + + const governanceContract = await getGovernanceContract(chain); + + const vaaHex = Buffer.from(vaa, "base64").toString("hex"); + log("Executing governance with VAA: ", vaaHex); + + const tx = await governanceContract.performGovernance( + `0x${vaaHex}`, + {} // overrides + ); + + log(`Submitted governance transaction: ${tx.hash}`); + + await tx.wait(); + + log("success."); +} + +run().then(() => console.log("Done!")); + +async function getGovernanceContract(chain: ChainInfo) { + const signer = await getSigner(chain); + const governanceAddress = await getContractAddress( + "GeneralPurposeGovernances", + chain.chainId + ); + return Governance__factory.connect(governanceAddress, signer); +} diff --git a/solana/ts-scripts/.env.sample b/solana/ts-scripts/.env.sample new file mode 100644 index 000000000..5b7b83e95 --- /dev/null +++ b/solana/ts-scripts/.env.sample @@ -0,0 +1,14 @@ +# Required: "devnet" or "mainnet" +ENV=devnet + +# Solana RPC URL +SOLANA_RPC_URL=https://api.devnet.solana.com + +# Commitment level (optional, defaults to "confirmed") +SOLANA_COMMITMENT=confirmed + +# Ledger derivation path (required for env.ts / Ledger scripts) +LEDGER_DERIVATION_PATH= + +# Guardian key (required for governance message creation) +GUARDIAN_KEY= diff --git a/solana/ts-scripts/.gitignore b/solana/ts-scripts/.gitignore new file mode 100644 index 000000000..ecff56526 --- /dev/null +++ b/solana/ts-scripts/.gitignore @@ -0,0 +1,3 @@ +.env +node_modules +dist diff --git a/solana/ts-scripts/config/devnet/governance-vaa.json b/solana/ts-scripts/config/devnet/governance-vaa.json new file mode 100644 index 000000000..30f39e319 --- /dev/null +++ b/solana/ts-scripts/config/devnet/governance-vaa.json @@ -0,0 +1,3 @@ +{ + "vaa": "01000000000100403baa70cb8c22af68f1f17ce4790024ef4073a7d380f936e418da36a3ee9e8134515679b2feaff806584edf5a8b1976d11a77b1c1b9323d3596c425652957870100000000a3c9167e00010000000000000000000000000000000000000000000000000000000000000004c815dfbc88fdf79420000000000000000047656e6572616c507572706f7365476f7665726e616e636502000105730eefefc3605ca196155a3ca5594706690d45d01848b8bedfb7d99980f7ec057f97be1c39478e57974f6cc9dbfbeebb0e5ce340c2efd52b8295e889a9ede400050885ee38373250ab21bafa2ea2b6c037b511243608b750fcd6c24bdf31710b1f00018a98013dc14753bff4aa9ba82a9bb6f95835fdc6f97561e4d9eaee76cae462ee00006f776e65720000000000000000000000000000000000000000000000000000000100963bdfa10970171c86e222a176f6f831222031172f2c2de3e511ed95ace40018000102a8f6914e88a1b0e210153ef763ae2b00c2b93d16c124d2c0537a100480000000000008eca6efde0e2d8ffe" +} diff --git a/solana/ts/scripts/config/devnet/programs.json b/solana/ts-scripts/config/devnet/programs.json similarity index 100% rename from solana/ts/scripts/config/devnet/programs.json rename to solana/ts-scripts/config/devnet/programs.json diff --git a/solana/ts-scripts/config/mainnet/governance-vaas-june-05.json b/solana/ts-scripts/config/mainnet/governance-vaas-june-05.json new file mode 100644 index 000000000..c5283b92a --- /dev/null +++ b/solana/ts-scripts/config/mainnet/governance-vaas-june-05.json @@ -0,0 +1,10 @@ +[ + { + "id": "1/0000000000000000000000000000000000000000000000000000000000000004/17267778771311161006", + "vaa": "AQAAAAQNAI65VaLhYFpsKDt5UzezT5jFBhRxbGHhBWSuDPIVNyH9HQOlTHSyBDpXhOAR8hy0Wjj1AlPG67AnuGRJNSbIhvYAA03z3U0upcHC0JOnMnmPNSPE+lCKuYF/vHU/FtQ+rg8UIOggAn9/meZLhhlCh7sJN/xIe5Mhnm19psjKz9FfGnIBBUgKKGChLIlRUgb2ZWLGS4OjByi+j6YSExb+5R3UTgJDLdU5K5F54m//HLP4hagpMb314+UWJv7EJfdYeS8Qx94ABvPI4Ae+H3wwH5I/Y65DRYQDtpMGDH6EogkYNN/DyiShB9Pm5gGN0JM7DgNmCHODlV6O7UB3zI2+W9WsnZnCOdkABz4HOBelUsaeBXvLeEgIuDYrE6/4sUWMqlOjB1FY4V9PM+ZG6a5Ts4K9zRcMpT3t1NM4Wjf+CffvBK/C+Wu4MSwACsXQ5JvMpXTe7QAAKaTmtfrH0EYKCwtNkbqJULyLMbC3AQXh677QeaSqZ8/snJqHADClVkfTsuF8u4vm46MEQAcBCxq4Dbif1rNBXVxFeF0JrvFD7dWCPF207pAQzTv43ZRNP+vt+h0tfB1m1xDWMjms4EsEoTB/gGmhZI/pRRW32wUBDHS/dvPBezSpPVaOZx3jhuOBj6XNkAI7aXGyYR5PyrYvNjxOlf3YaMeud9bt4MUjuZY1PdKYqTwNJULRO6g3eBUBDQbuIAf3N/WcmmIIGHXTVMxMPEPI9YHlyxTwIRqzA1NBZrVYnRcc7NSmdLr3bJ4QXwxNi2/fwvdsQSC1YVTmAskBDp3ylnFFrXZGOw+NGUzRl+GbVxMG/PPJe4vCaijmENuwRQkQl6e9/gVboWEk0doI5xV+cNQpzzyMQvtcoMuB6TMBEA49JvrmJ9Tys/OO/l0O8kuPTnBiEQ0MNnOWVJ8JwVTZHNYMxx5xjqEGT+aGZRZ5AtGyaX4/wW1a/A+0UsRGxUQAEf6/+jJtuPGxUHvlJzgUAAMs+/LMsew8VRG/tpU9uH00DZitLKDXogfEyPI0laGix8Pgk81YiF+HkuUZCtKi6WgBEvj+rOA48qWczx1m9wdxFFqtawbG4We+qx0U1ot4ExvhEVJoP8OMPArQnJd78okWn2ekZK+yWuQ8e307OoBl2pkAAAAAAAaA4fgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE76N5TcwIbq4gAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UCAAEFcw7v78NgXKGWFVo8pVlHBmkNRdAYSLi+37fZmYD37AV/l74cOUeOV5dPbMnb++67DlzjQMLv1SuCleiJqe3kAAMIhe44NzJQqyG6+i6itsA3tREkNgi3UPzWwkvfMXELHwAAb3duZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABADlm1IlN79/aRsLMXswc43tUQKJlINi1e69RyzvOkpJhAAEAENoIAcyn6QqeAADBb/KGIwA=" + }, + { + "id": "1/0000000000000000000000000000000000000000000000000000000000000004/5665574265595141132", + "vaa": "AQAAAAQNALq8JasAs0jexvcmn9DUk729hB/Hn+Nb6fiJlgGpkJUvbfCNUF6c/miNTGBdTWiRvZpUugBfOCha1/rZ0qYvCRsBA+ov5I985O9AQgBYllJGuiLjzxQRF7Zu7LL9vbKyuqICMvkHXNpJYe+XWjjrA0/9dzbEeqbCq2aEibFd4UeHIqUBBfeObPyG+EFKwVhjp14aQ3j1RBSBE2LVr2asP3Gh2E2xVgAIqOis+q0pABMF3ES1oC1gVPTDq3hwZYIncP9bQkIBBsvKjIUf6dzQc1nsla2nzO/7hy+3K+zrnI73RVz6ntyCKFxXLJIFqTxbmwSOEn+X+CpR/1/dZ+///MVcHALo8iUAB6mEOYDzEKbOpwqzHBTKetvLtuytUbDjXLfT2JmIGpDPRBnSOjrdWVeV/xEVs/KT6TmSNATemBerVpUgNRWjln0ACFABS7L/OytWxOvEd415GYO1S8Ome7O9OtcdkBqW7qVCf0kKc4y0WCsKKizS/Zl1JB8MtQTRNeQ4M40wg2HCcK4ACTREFSREEj2w1pXIBoj8FStDn75bSWlgVoumYyvmB7JJVuOwB7ao4yreGRFzKrOTyxjkbHMIVKe1MoI/nekjlnkACs1KektGrLvVTZjB/jKT3Ncr0epxAgX7mUoupH8MZZiHHoT/jcUQX5wKajAaAAQIBS0fQDXQc6ndlHw+/1kNpK0ADdiRpv0GABSe4fPPjJUnchYFlXmkh9mR1XBPetoZcvqiCZEdJgtPNqkg9qpK7t9kna247nsdwzTXmvy7ROsSPfsBDmOH9g4AWxmuViggcYPHN9b4KvM+4YOwUU54hWYGOxGMboS+QzVx+l8m975J8V98xsFklAy+ZdPEwg8oR90rorIBEJgM4lkC+jg77/atGLm9KjGZaefyav89MqcBDjBCBVZjQ5Wn6xhljUlemTXwhnsakaZUyjJe2ovCO0dtPylwCxEAEbth2Ejuso+rAhZGkdvTjVVlzC3EqstMCU/X+Re4FQrIGTjNJfpjn1ZeQ/v0Mwuu8TC9gwJJssju0h+A4/t/k/IAEujBi2W5AGxiDXA22u9q3fQdJ3L7W3pXtGRPtuVHh6FUD0pm5n4jaPuWbcnH2F+EwZU/bSacUAZyiJkWxHaGYqwBAAAAAPB6e/4AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAETqApxu1knAwgAAAAAAAAAABHZW5lcmFsUHVycG9zZUdvdmVybmFuY2UCAAEFcw7v78NgXKGWFVo8pVlHBmkNRdAYSLi+37fZmYD37AV/l74cOUeOV5dPbMnb++67DlzjQMLv1SuCleiJqe3kAAMIhe44NzJQqyG6+i6itsA3tREkNgi3UPzWwkvfMXELHwAAb3duZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABADlm1IlN79/aRsLMXswc43tUQKJlINi1e69RyzvOkpJhAAEAENoIAcyn6QqeAEB6EPNaAAA=" + } +] diff --git a/solana/ts/scripts/config/mainnet/programs.json b/solana/ts-scripts/config/mainnet/programs.json similarity index 100% rename from solana/ts/scripts/config/mainnet/programs.json rename to solana/ts-scripts/config/mainnet/programs.json diff --git a/solana/ts-scripts/package.json b/solana/ts-scripts/package.json new file mode 100644 index 000000000..77ee0d890 --- /dev/null +++ b/solana/ts-scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "solana-ts-scripts", + "version": "1.0.0", + "private": true, + "description": "Governance scripts for Solana NTT", + "type": "module", + "scripts": {}, + "devDependencies": { + "@certusone/wormhole-sdk": "^0.10.11", + "@coral-xyz/anchor": "0.29.0", + "@solana/web3.js": "^1.95.8", + "@types/node": "^20.11.22", + "@wormhole-foundation/sdk-base": "^4.12.2", + "@xlabs-xyz/ledger-signer-solana": "^0.0.1", + "dotenv": "^16.4.5", + "tsx": "^4.7.2", + "typescript": "^5.4.5" + } +} diff --git a/solana/ts-scripts/src/env.ts b/solana/ts-scripts/src/env.ts new file mode 100644 index 000000000..e8238031a --- /dev/null +++ b/solana/ts-scripts/src/env.ts @@ -0,0 +1,73 @@ +import fs from "fs"; +import { Connection, Commitment } from "@solana/web3.js"; +import { SolanaLedgerSigner } from "@xlabs-xyz/ledger-signer-solana"; + +if (!process.env.LEDGER_DERIVATION_PATH) { + throw new Error("LEDGER_DERIVATION_PATH is not set"); +} + +if (!process.env.ENV) { + throw new Error("ENV not set"); +} + +const env = process.env.ENV; + +const derivationPath = process.env.LEDGER_DERIVATION_PATH! as string; + +let signer; +export async function getSigner(): Promise { + if (!signer) { + signer = await SolanaLedgerSigner.create(derivationPath); + } + + return signer; +} + +export function getEnv(key: string): string { + if (!process.env[key]) { + throw new Error(`${key} not found on environment`); + } + + return process.env[key]!; +} + +export const rpcUrl = + process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com"; + +export const connectionCommitmentLevel = (process.env.SOLANA_COMMITMENT || + "confirmed") as Commitment; + +export const connection = new Connection(rpcUrl, connectionCommitmentLevel); + +export type Programs = { + mintProgramId: string; + nttProgramId: string; + wormholeProgramId: string; + quoterProgramId: string; + governanceProgramId: string; +}; + +export type GovernanceVaa = { + vaa: string; +}; + +export function getProgramAddresses(): Programs { + return loadScriptConfig("programs"); +} + +export function getGovernanceVaa(): GovernanceVaa { + return loadScriptConfig("governance-vaa"); +} + +export function loadScriptConfig(filename: string): any { + const configFile = fs.readFileSync(`./config/${env}/${filename}.json`); + const config = JSON.parse(configFile.toString()); + if (!config) { + throw Error("Failed to pull config file!"); + } + return config; +} + +export function getGuardianKey(): string { + return getEnv("GUARDIAN_KEY"); +} diff --git a/solana/ts-scripts/src/env_short.ts b/solana/ts-scripts/src/env_short.ts new file mode 100644 index 000000000..2e8f62fbb --- /dev/null +++ b/solana/ts-scripts/src/env_short.ts @@ -0,0 +1,48 @@ +import { Commitment, Connection } from "@solana/web3.js"; +import fs from "fs"; + +const env = "mainnet"; // Or devnet + +export function getEnv(key: string): string { + if (!process.env[key]) { + throw new Error(`${key} not found on environment`); + } + + return process.env[key]!; +} + +export const rpcUrl = process.env.SOLANA_RPC_URL || "INSERT_RPC_URL"; + +export const connectionCommitmentLevel = (process.env.SOLANA_COMMITMENT || + "confirmed") as Commitment; + +export const connection = new Connection(rpcUrl, connectionCommitmentLevel); + +export type Programs = { + mintProgramId: string; + nttProgramId: string; + wormholeProgramId: string; + quoterProgramId: string; + governanceProgramId: string; +}; + +export type GovernanceVaa = { + vaa: string; +}; + +export function getProgramAddresses(): Programs { + return loadScriptConfig("programs"); +} + +export function getGovernanceVaa(): GovernanceVaa { + return loadScriptConfig("governance-vaa"); +} + +export function loadScriptConfig(filename: string): any { + const configFile = fs.readFileSync(`./config/${env}/${filename}.json`); + const config = JSON.parse(configFile.toString()); + if (!config) { + throw Error("Failed to pull config file!"); + } + return config; +} diff --git a/solana/ts-scripts/src/executeGovernanceVaa.ts b/solana/ts-scripts/src/executeGovernanceVaa.ts new file mode 100644 index 000000000..374cbbdf9 --- /dev/null +++ b/solana/ts-scripts/src/executeGovernanceVaa.ts @@ -0,0 +1,63 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { parseVaa } from "@certusone/wormhole-sdk"; +import { + connection, + getSigner, + getProgramAddresses, + getGovernanceVaa, +} from "./env"; +import { addLedgerSignature, ledgerSignAndSend } from "./helpers"; +import { postVaaSolana } from "@certusone/wormhole-sdk"; +import { NTTGovernance } from "./governance"; + +(async () => { + const { vaa } = getGovernanceVaa(); + + const { nttProgramId, wormholeProgramId, governanceProgramId } = + getProgramAddresses(); + + const governance = new NTTGovernance(connection, { + programId: governanceProgramId as any, + }); + + const signer = await getSigner(); + const signerPk = new PublicKey(await signer.getAddress()); + + const vaaBuff = Buffer.from(vaa, "hex"); + async function sign(tx: Transaction) { + await addLedgerSignature(tx, signer, signerPk); + return tx; + } + + console.log("Posting VAA to Solana..."); + + await postVaaSolana( + connection, + sign, + new PublicKey(wormholeProgramId), + signerPk, + vaaBuff + ); + + console.log("VAA posted to Solana."); + + const parsedVaa = parseVaa(vaaBuff); + + const governanceIx = await governance.createGovernanceVaaInstruction({ + payer: signerPk, + vaa: parsedVaa, + wormholeId: new PublicKey(wormholeProgramId), + }); + + console.log( + `Account ${signerPk.toBase58()} is claiming ownership of NTT Program ${nttProgramId}.` + ); + + const tx = await ledgerSignAndSend([governanceIx], []); + + console.log("Waiting for confirmation... Signature: ", tx.signature); + + await connection.confirmTransaction(tx); + + console.log("success."); +})(); diff --git a/solana/ts-scripts/src/executeGovernanceVaa_short.ts b/solana/ts-scripts/src/executeGovernanceVaa_short.ts new file mode 100644 index 000000000..9ee2204bd --- /dev/null +++ b/solana/ts-scripts/src/executeGovernanceVaa_short.ts @@ -0,0 +1,80 @@ +import { + ComputeBudgetProgram, + Keypair, + PublicKey, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { parseVaa } from "@certusone/wormhole-sdk"; +import { connection, getProgramAddresses, getGovernanceVaa } from "./env_short"; +import { postVaaSolana } from "@certusone/wormhole-sdk"; +import { NTTGovernance } from "./governance"; +import fs from "fs"; + +(async () => { + const { vaa } = getGovernanceVaa(); + + const { nttProgramId, wormholeProgramId, governanceProgramId } = + getProgramAddresses(); + + const governance = new NTTGovernance(connection, { + programId: governanceProgramId as any, + }); + + const payerSecretKey = Uint8Array.from( + JSON.parse( + fs.readFileSync(`YOUR_SIGNER_FILE`, { + encoding: "utf-8", + }) + ) + ); + const signer = Keypair.fromSecretKey(payerSecretKey); + // const signer: Keypair = "INSERT_KEYPAIR"; // Derive from file/secret key etc. + const signerPk = signer.publicKey; + + const vaaBuff = Buffer.from(vaa, "base64"); // replace with "hex" if VAA is in hex + async function sign(tx: Transaction) { + tx.partialSign(signer); // partial sign using signer instead of adding ledger signature + return tx; + } + + console.log("Posting VAA to Solana..."); + + await postVaaSolana( + connection, + sign, + new PublicKey(wormholeProgramId).toBase58(), + signerPk.toBase58(), + vaaBuff + ); + + console.log("VAA posted to Solana."); + + const parsedVaa = parseVaa(vaaBuff); + + const governanceIx = await governance.createGovernanceVaaInstruction({ + payer: signerPk, + vaa: parsedVaa, + wormholeId: new PublicKey(wormholeProgramId), + }); + + console.log( + `Governance PDA is claiming ownership of NTT Program ${nttProgramId}.` + ); + + const tx = new Transaction().add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1_000_000, + }), + governanceIx + ); + const recentBlockHash = await connection.getLatestBlockhash(); + + tx.recentBlockhash = recentBlockHash.blockhash; + tx.feePayer = signerPk; + const txSig = await sendAndConfirmTransaction(connection, tx, [signer], { + skipPreflight: true, + }); + + console.log("success:", txSig); +})(); diff --git a/solana/ts-scripts/src/executeGovernanceVaas.ts b/solana/ts-scripts/src/executeGovernanceVaas.ts new file mode 100644 index 000000000..87bd2920e --- /dev/null +++ b/solana/ts-scripts/src/executeGovernanceVaas.ts @@ -0,0 +1,78 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { parseVaa } from "@certusone/wormhole-sdk"; +import { + connection, + getSigner, + getProgramAddresses, + loadScriptConfig, +} from "./env"; +import { addLedgerSignature, ledgerSignAndSend } from "./helpers"; +import { postVaaSolana } from "@certusone/wormhole-sdk"; +import { NTTGovernance } from "./governance"; + +const governanceVaasFileName = process.env.GOVERNANCE_VAAS_FILE_PATH; +if (!governanceVaasFileName) { + throw new Error("GOVERNANCE_VAAS_FILE_PATH is required."); +} + +(async () => { + const vaas: { id: string; vaa: string }[] = loadScriptConfig( + governanceVaasFileName + ); + const { wormholeProgramId, governanceProgramId } = getProgramAddresses(); + + const governance = new NTTGovernance(connection, { + programId: governanceProgramId as any, + }); + + const signer = await getSigner(); + const signerPk = new PublicKey(await signer.getAddress()); + + async function sign(tx: Transaction) { + await addLedgerSignature(tx, signer, signerPk); + return tx; + } + + const instructions: TransactionInstruction[] = []; + for (const vaa of vaas) { + const vaaBuff = Buffer.from(vaa.vaa, "base64"); + + console.log(`Posting VAA ${vaa.id} to Solana...`); + console.log(`Vaa: ${vaa.vaa}`); + + await postVaaSolana( + connection, + sign, + new PublicKey(wormholeProgramId), + signerPk, + vaaBuff + ); + + console.log("VAA post succeeded"); + + const parsedVaa = parseVaa(vaaBuff); + + const governanceIx = await governance.createGovernanceVaaInstruction({ + payer: signerPk, + vaa: parsedVaa, + wormholeId: new PublicKey(wormholeProgramId), + }); + + console.log("Parsed VAA Payload", parsedVaa.payload.toString("hex")); + instructions.push(governanceIx); + } + + console.log(`Executing governance instructions.`, instructions); + + const tx = await ledgerSignAndSend(instructions, []); + + console.log("Waiting for confirmation... Signature: ", tx.signature); + + await connection.confirmTransaction(tx); + + console.log("success."); +})(); diff --git a/solana/ts-scripts/src/governance.ts b/solana/ts-scripts/src/governance.ts new file mode 100644 index 000000000..9db813b9c --- /dev/null +++ b/solana/ts-scripts/src/governance.ts @@ -0,0 +1,145 @@ +import { Program } from "@coral-xyz/anchor"; +import { + PublicKey, + type Connection, + SystemProgram, + AccountMeta, + TransactionInstruction, +} from "@solana/web3.js"; +import { ParsedVaa } from "@certusone/wormhole-sdk"; +import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; + +import { encoding } from "@wormhole-foundation/sdk-base"; +import { type WormholeGovernance as RawWormholeGovernance } from "../../target/types/wormhole_governance"; +import IDL from "../../target/idl/wormhole_governance.json"; + +type Seed = Uint8Array | string; +function derivePda(seeds: Seed | readonly Seed[], programId: PublicKey) { + const toBytes = (s: string | Uint8Array) => + typeof s === "string" ? encoding.bytes.encode(s) : s; + return PublicKey.findProgramAddressSync( + Array.isArray(seeds) ? seeds.map(toBytes) : [toBytes(seeds as Seed)], + programId + )[0]; +} + +// This is a workaround for the fact that the anchor idl doesn't support generics +// yet. This type is used to remove the generics from the idl types. +type OmitGenerics = { + [P in keyof T]: T[P] extends Record<"generics", any> + ? never + : T[P] extends object + ? OmitGenerics + : T[P]; +}; + +export type WormholeGovernance = OmitGenerics; + +export const GOV_PROGRAM_IDS = [ + "NTTManager111111111111111111111111111111111", + "NGoD1yTeq5KaURrZo7MnCTFzTA4g62ygakJCnzMLCfm", + "NGoD1yTeq5KaURrZo7MnCTFzTA4g62ygakJCnzMLCfm", +] as const; + +export type GovProgramId = (typeof GOV_PROGRAM_IDS)[number]; + +export class NTTGovernance { + readonly program: Program; + readonly wormholeId: PublicKey; + + constructor(connection: Connection, args: { programId: GovProgramId }) { + // TODO: initialise a new Program here with a passed in Connection + this.program = new Program(IDL as any, new PublicKey(args.programId), { + connection, + }); + } + + governanceAccountAddress() { + return derivePda("governance", this.program.programId); + } + + async createGovernanceVaaInstruction(args: { + payer: PublicKey; + wormholeId: PublicKey; + vaa: ParsedVaa; + }) { + const vaaKey = derivePostedVaaKey(args.wormholeId, args.vaa.hash); + const emitterChain = Buffer.alloc(2); + emitterChain.writeUInt16BE(args.vaa.emitterChain); + const sequence = Buffer.alloc(8); + sequence.writeBigUInt64BE(BigInt(args.vaa.sequence)); + const [governanceProgramId, ixData] = verifyGovernanceHeader( + args.vaa.payload + ); + const ix = deserializeInstruction(ixData); + + const governanceIx = await this.program.methods + .governance() + .accountsStrict({ + payer: args.payer, + governance: derivePda("governance", this.program.programId), + vaa: vaaKey, + program: ix.programId, + replay: derivePda( + ["replay", emitterChain, args.vaa.emitterAddress, sequence], + this.program.programId + ), + systemProgram: SystemProgram.programId, + }) + .instruction(); + + // add extra instructions + governanceIx.keys = governanceIx.keys.concat( + ix.keys.map((k) => { + return { ...k, isSigner: false }; + }) + ); + return governanceIx; + } +} + +function deserializeInstruction(data: Buffer): TransactionInstruction { + let offset = 0; + const programId = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + const accountsLen = data.readUInt16BE(offset); + offset += 2; + const keys: Array = []; + for (let i = 0; i < accountsLen; i++) { + const pubkey = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + const isSigner = data.readUInt8(offset) === 1; + offset += 1; + const isWritable = data.readUInt8(offset) === 1; + offset += 1; + keys.push({ pubkey, isSigner, isWritable }); + } + const dataLen = data.readUInt16BE(offset); + offset += 2; + const instructionData = data.subarray(offset, offset + dataLen); + return new TransactionInstruction({ keys, programId, data: instructionData }); +} + +function verifyGovernanceHeader(data: Buffer): [PublicKey, Buffer] { + let offset = 0; + const module = data.subarray(offset, offset + 32); + offset += 32; + const action = data.readUInt8(offset); + offset += 1; + const chainId = data.readUInt16BE(offset); + offset += 2; + const governanceProgramId = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + if ( + !module.equals(Buffer.from("GeneralPurposeGovernance".padStart(32, "\0"))) + ) { + throw new Error("Invalid module"); + } + if (action !== 2) { + throw new Error("Invalid action"); + } + if (chainId !== 1) { + throw new Error("Invalid chainId"); + } + return [governanceProgramId, data.subarray(offset)]; +} diff --git a/solana/ts-scripts/src/helpers.ts b/solana/ts-scripts/src/helpers.ts new file mode 100644 index 000000000..6b7c5a246 --- /dev/null +++ b/solana/ts-scripts/src/helpers.ts @@ -0,0 +1,57 @@ +import { + Transaction, + TransactionInstruction, + Keypair, + PublicKey, + ComputeBudgetProgram, +} from "@solana/web3.js"; + +import { SolanaLedgerSigner } from "@xlabs-xyz/ledger-signer-solana"; +import { connection, getSigner } from "./env"; + +export async function ledgerSignAndSend( + instructions: TransactionInstruction[], + signers: Keypair[] +) { + const deployerSigner = await getSigner(); + const deployerPk = new PublicKey(await deployerSigner.getAddress()); + + const tx = new Transaction(); + + const setComputePriceIx = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1_000_000, + }); + + tx.add(setComputePriceIx); + + tx.add(...instructions); + + const recentBlockHash = await connection.getLatestBlockhash(); + + tx.recentBlockhash = recentBlockHash.blockhash; + tx.feePayer = deployerPk; + + signers.forEach((signer) => tx.partialSign(signer)); + + await addLedgerSignature(tx, deployerSigner, deployerPk); + + const txSignature = await connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + + return { + signature: txSignature, + ...recentBlockHash, + }; +} + +export async function addLedgerSignature( + tx: Transaction, + signer: SolanaLedgerSigner, + signerPk: PublicKey +) { + const signedByPayer = await signer.signTransaction( + tx.compileMessage().serialize() + ); + tx.addSignature(signerPk, signedByPayer); +} diff --git a/solana/ts/scripts/setUpTestnet.ts b/solana/ts-scripts/src/setUpTestnet.ts similarity index 96% rename from solana/ts/scripts/setUpTestnet.ts rename to solana/ts-scripts/src/setUpTestnet.ts index 45a89da89..ae1c49c54 100644 --- a/solana/ts/scripts/setUpTestnet.ts +++ b/solana/ts-scripts/src/setUpTestnet.ts @@ -1,6 +1,6 @@ import { Connection, Keypair, PublicKey } from "@solana/web3.js"; import "dotenv/config"; -import { NTT } from "../lib"; +import { NTT } from "../../ts/lib"; import { BN } from "@coral-xyz/anchor"; main(); diff --git a/solana/ts-scripts/tsconfig.json b/solana/ts-scripts/tsconfig.json new file mode 100644 index 000000000..ae9df7397 --- /dev/null +++ b/solana/ts-scripts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +}