common.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import {
  2. DefaultStore,
  3. EvmChain,
  4. EvmEntropyContract,
  5. EvmWormholeContract,
  6. getDefaultDeploymentConfig,
  7. PrivateKey,
  8. } from "../src";
  9. import { existsSync, readFileSync, writeFileSync } from "fs";
  10. import { join } from "path";
  11. import Web3 from "web3";
  12. import { Contract } from "web3-eth-contract";
  13. import { InferredOptionType } from "yargs";
  14. export interface BaseDeployConfig {
  15. gasMultiplier: number;
  16. gasPriceMultiplier: number;
  17. jsonOutputDir: string;
  18. privateKey: PrivateKey;
  19. }
  20. // Deploys a contract if it was not deployed before.
  21. // It will check for the past deployments in file `cacheFile` against a key
  22. // If `cacheKey` is provided it will be used as the key, else it will use
  23. // a key - `${chain.getId()}-${artifactName}`
  24. export async function deployIfNotCached(
  25. cacheFile: string,
  26. chain: EvmChain,
  27. config: BaseDeployConfig,
  28. artifactName: string,
  29. deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
  30. cacheKey?: string,
  31. ): Promise<string> {
  32. const runIfNotCached = makeCacheFunction(cacheFile);
  33. const key = cacheKey ?? `${chain.getId()}-${artifactName}`;
  34. return runIfNotCached(key, async () => {
  35. const artifact = JSON.parse(
  36. readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8"),
  37. );
  38. // Handle bytecode which can be either a string or an object with an 'object' property
  39. let bytecode = artifact["bytecode"];
  40. if (
  41. typeof bytecode === "object" &&
  42. bytecode !== null &&
  43. "object" in bytecode
  44. ) {
  45. bytecode = bytecode.object;
  46. }
  47. // Ensure bytecode starts with 0x
  48. if (!bytecode.startsWith("0x")) {
  49. bytecode = `0x${bytecode}`;
  50. }
  51. console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
  52. const addr = await chain.deploy(
  53. config.privateKey,
  54. artifact["abi"],
  55. bytecode,
  56. deployArgs,
  57. config.gasMultiplier,
  58. config.gasPriceMultiplier,
  59. );
  60. console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
  61. return addr;
  62. });
  63. }
  64. export function getWeb3Contract(
  65. jsonOutputDir: string,
  66. artifactName: string,
  67. address: string,
  68. ): Contract {
  69. const artifact = JSON.parse(
  70. readFileSync(join(jsonOutputDir, `${artifactName}.json`), "utf8"),
  71. );
  72. const web3 = new Web3();
  73. return new web3.eth.Contract(artifact["abi"], address);
  74. }
  75. export const COMMON_DEPLOY_OPTIONS = {
  76. "std-output-dir": {
  77. type: "string",
  78. demandOption: true,
  79. desc: "Path to the standard JSON output of the contracts (build artifact) directory",
  80. },
  81. "private-key": {
  82. type: "string",
  83. demandOption: true,
  84. desc: "Private key to sign the transactions with",
  85. },
  86. chain: {
  87. type: "array",
  88. string: true,
  89. demandOption: true,
  90. desc: "Chains to upload the contract on. Must be one of the chains available in the store",
  91. },
  92. "deployment-type": {
  93. type: "string",
  94. demandOption: false,
  95. default: "stable",
  96. desc: "Deployment type to use. Can be 'stable' or 'beta'",
  97. },
  98. "gas-multiplier": {
  99. type: "number",
  100. demandOption: false,
  101. // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
  102. default: 2,
  103. desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
  104. },
  105. "gas-price-multiplier": {
  106. type: "number",
  107. demandOption: false,
  108. default: 1,
  109. desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
  110. },
  111. "save-contract": {
  112. type: "boolean",
  113. demandOption: false,
  114. default: true,
  115. desc: "Save the contract to the store",
  116. },
  117. } as const;
  118. export const CHAIN_SELECTION_OPTIONS = {
  119. testnet: {
  120. type: "boolean",
  121. default: false,
  122. desc: "Upgrade testnet contracts instead of mainnet",
  123. },
  124. "all-chains": {
  125. type: "boolean",
  126. default: false,
  127. desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
  128. },
  129. chain: {
  130. type: "array",
  131. string: true,
  132. desc: "Chains to upgrade the contract on",
  133. },
  134. } as const;
  135. export const COMMON_UPGRADE_OPTIONS = {
  136. ...CHAIN_SELECTION_OPTIONS,
  137. "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
  138. "ops-key-path": {
  139. type: "string",
  140. demandOption: true,
  141. desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
  142. },
  143. "std-output": {
  144. type: "string",
  145. demandOption: true,
  146. desc: "Path to the standard JSON output of the pyth contract (build artifact)",
  147. },
  148. } as const;
  149. export function makeCacheFunction(
  150. cacheFile: string,
  151. ): (cacheKey: string, fn: () => Promise<string>) => Promise<string> {
  152. async function runIfNotCached(
  153. cacheKey: string,
  154. fn: () => Promise<string>,
  155. ): Promise<string> {
  156. const cache = existsSync(cacheFile)
  157. ? JSON.parse(readFileSync(cacheFile, "utf8"))
  158. : {};
  159. if (cache[cacheKey]) {
  160. return cache[cacheKey];
  161. }
  162. const result = await fn();
  163. cache[cacheKey] = result;
  164. writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
  165. return result;
  166. }
  167. return runIfNotCached;
  168. }
  169. export function getSelectedChains(argv: {
  170. chain: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["chain"]>;
  171. testnet: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["testnet"]>;
  172. allChains: InferredOptionType<(typeof COMMON_UPGRADE_OPTIONS)["all-chains"]>;
  173. }) {
  174. const selectedChains: EvmChain[] = [];
  175. if (argv.allChains && argv.chain)
  176. throw new Error("Cannot use both --all-chains and --chain");
  177. if (!argv.allChains && !argv.chain)
  178. throw new Error("Must use either --all-chains or --chain");
  179. for (const chain of Object.values(DefaultStore.chains)) {
  180. if (!(chain instanceof EvmChain)) continue;
  181. if (
  182. (argv.allChains && chain.isMainnet() !== argv.testnet) ||
  183. argv.chain?.includes(chain.getId())
  184. )
  185. selectedChains.push(chain);
  186. }
  187. if (argv.chain && selectedChains.length !== argv.chain.length)
  188. throw new Error(
  189. `Some chains were not found ${selectedChains
  190. .map((chain) => chain.getId())
  191. .toString()}`,
  192. );
  193. for (const chain of selectedChains) {
  194. if (chain.isMainnet() != selectedChains[0].isMainnet())
  195. throw new Error("All chains must be either mainnet or testnet");
  196. }
  197. return selectedChains;
  198. }
  199. /**
  200. * Finds the entropy contract for a given EVM chain.
  201. * @param {EvmChain} chain The EVM chain to find the entropy contract for.
  202. * @returns The entropy contract for the given EVM chain.
  203. * @throws {Error} an error if the entropy contract is not found for the given EVM chain.
  204. */
  205. export function findEntropyContract(chain: EvmChain): EvmEntropyContract {
  206. for (const contract of Object.values(DefaultStore.entropy_contracts)) {
  207. if (contract.getChain().getId() === chain.getId()) {
  208. return contract;
  209. }
  210. }
  211. throw new Error(`Entropy contract not found for chain ${chain.getId()}`);
  212. }
  213. /**
  214. * Finds the wormhole contract for a given EVM chain.
  215. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  216. * @returns If found, the wormhole contract for the given EVM chain. Else, undefined
  217. */
  218. export function findWormholeContract(
  219. chain: EvmChain,
  220. ): EvmWormholeContract | undefined {
  221. for (const contract of Object.values(DefaultStore.wormhole_contracts)) {
  222. if (
  223. contract instanceof EvmWormholeContract &&
  224. contract.getChain().getId() === chain.getId()
  225. ) {
  226. return contract;
  227. }
  228. }
  229. }
  230. export interface DeployWormholeReceiverContractsConfig
  231. extends BaseDeployConfig {
  232. saveContract: boolean;
  233. type: "stable" | "beta";
  234. }
  235. /**
  236. * Deploys the wormhole receiver contract for a given EVM chain.
  237. * @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
  238. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  239. * @param {string} cacheFile The path to the cache file.
  240. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  241. */
  242. export async function deployWormholeContract(
  243. chain: EvmChain,
  244. config: DeployWormholeReceiverContractsConfig,
  245. cacheFile: string,
  246. ): Promise<EvmWormholeContract> {
  247. const receiverSetupAddr = await deployIfNotCached(
  248. cacheFile,
  249. chain,
  250. config,
  251. "ReceiverSetup",
  252. [],
  253. );
  254. const receiverImplAddr = await deployIfNotCached(
  255. cacheFile,
  256. chain,
  257. config,
  258. "ReceiverImplementation",
  259. [],
  260. );
  261. // Craft the init data for the proxy contract
  262. const setupContract = getWeb3Contract(
  263. config.jsonOutputDir,
  264. "ReceiverSetup",
  265. receiverSetupAddr,
  266. );
  267. const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
  268. const initData = setupContract.methods
  269. .setup(
  270. receiverImplAddr,
  271. wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
  272. chain.getWormholeChainId(),
  273. wormholeConfig.governanceChainId,
  274. "0x" + wormholeConfig.governanceContract,
  275. )
  276. .encodeABI();
  277. const wormholeReceiverAddr = await deployIfNotCached(
  278. cacheFile,
  279. chain,
  280. config,
  281. "WormholeReceiver",
  282. [receiverSetupAddr, initData],
  283. );
  284. const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
  285. if (config.type === "stable") {
  286. console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
  287. // TODO: Add a way to pass gas configs to this
  288. await wormholeContract.syncMainnetGuardianSets(config.privateKey);
  289. console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
  290. }
  291. if (config.saveContract) {
  292. DefaultStore.wormhole_contracts[wormholeContract.getId()] =
  293. wormholeContract;
  294. DefaultStore.saveAllContracts();
  295. }
  296. return wormholeContract;
  297. }
  298. /**
  299. * Returns the wormhole contract for a given EVM chain.
  300. * If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
  301. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  302. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  303. * @param {string} cacheFile The path to the cache file.
  304. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  305. */
  306. export async function getOrDeployWormholeContract(
  307. chain: EvmChain,
  308. config: DeployWormholeReceiverContractsConfig,
  309. cacheFile: string,
  310. ): Promise<EvmWormholeContract> {
  311. return (
  312. findWormholeContract(chain) ??
  313. (await deployWormholeContract(chain, config, cacheFile))
  314. );
  315. }
  316. export interface DefaultAddresses {
  317. mainnet: string;
  318. testnet: string;
  319. }
  320. export async function topupAccountsIfNecessary(
  321. chain: EvmChain,
  322. deploymentConfig: BaseDeployConfig,
  323. accounts: Array<[string, DefaultAddresses]>,
  324. minBalance = 0.01,
  325. ) {
  326. for (const [accountName, defaultAddresses] of accounts) {
  327. const accountAddress = chain.isMainnet()
  328. ? defaultAddresses.mainnet
  329. : defaultAddresses.testnet;
  330. const web3 = chain.getWeb3();
  331. const balance = Number(
  332. web3.utils.fromWei(await web3.eth.getBalance(accountAddress), "ether"),
  333. );
  334. console.log(`${accountName} balance: ${balance} ETH`);
  335. if (balance < minBalance) {
  336. console.log(
  337. `Balance is less than ${minBalance}. Topping up the ${accountName} address...`,
  338. );
  339. const signer = web3.eth.accounts.privateKeyToAccount(
  340. deploymentConfig.privateKey,
  341. );
  342. web3.eth.accounts.wallet.add(signer);
  343. const estimatedGas = await web3.eth.estimateGas({
  344. from: signer.address,
  345. to: accountAddress,
  346. value: web3.utils.toWei(`${minBalance}`, "ether"),
  347. });
  348. const tx = await web3.eth.sendTransaction({
  349. from: signer.address,
  350. to: accountAddress,
  351. gas: estimatedGas * deploymentConfig.gasMultiplier,
  352. value: web3.utils.toWei(`${minBalance}`, "ether"),
  353. });
  354. console.log(
  355. `Topped up the ${accountName} address. Tx: `,
  356. tx.transactionHash,
  357. );
  358. }
  359. }
  360. }