common.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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. console.log(`Deploying ${artifactName} on ${chain.getId()}...`);
  39. const addr = await chain.deploy(
  40. config.privateKey,
  41. artifact["abi"],
  42. artifact["bytecode"],
  43. deployArgs,
  44. config.gasMultiplier,
  45. config.gasPriceMultiplier
  46. );
  47. console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);
  48. return addr;
  49. });
  50. }
  51. export function getWeb3Contract(
  52. jsonOutputDir: string,
  53. artifactName: string,
  54. address: string
  55. ): Contract {
  56. const artifact = JSON.parse(
  57. readFileSync(join(jsonOutputDir, `${artifactName}.json`), "utf8")
  58. );
  59. const web3 = new Web3();
  60. return new web3.eth.Contract(artifact["abi"], address);
  61. }
  62. export const COMMON_DEPLOY_OPTIONS = {
  63. "std-output-dir": {
  64. type: "string",
  65. demandOption: true,
  66. desc: "Path to the standard JSON output of the contracts (build artifact) directory",
  67. },
  68. "private-key": {
  69. type: "string",
  70. demandOption: true,
  71. desc: "Private key to sign the transactions with",
  72. },
  73. chain: {
  74. type: "array",
  75. demandOption: true,
  76. desc: "Chains to upload the contract on. Must be one of the chains available in the store",
  77. },
  78. "deployment-type": {
  79. type: "string",
  80. demandOption: false,
  81. default: "stable",
  82. desc: "Deployment type to use. Can be 'stable' or 'beta'",
  83. },
  84. "gas-multiplier": {
  85. type: "number",
  86. demandOption: false,
  87. // Proxy (ERC1967) contract gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
  88. default: 2,
  89. desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
  90. },
  91. "gas-price-multiplier": {
  92. type: "number",
  93. demandOption: false,
  94. default: 1,
  95. desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
  96. },
  97. "save-contract": {
  98. type: "boolean",
  99. demandOption: false,
  100. default: true,
  101. desc: "Save the contract to the store",
  102. },
  103. } as const;
  104. export const CHAIN_SELECTION_OPTIONS = {
  105. testnet: {
  106. type: "boolean",
  107. default: false,
  108. desc: "Upgrade testnet contracts instead of mainnet",
  109. },
  110. "all-chains": {
  111. type: "boolean",
  112. default: false,
  113. desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
  114. },
  115. chain: {
  116. type: "array",
  117. string: true,
  118. desc: "Chains to upgrade the contract on",
  119. },
  120. } as const;
  121. export const COMMON_UPGRADE_OPTIONS = {
  122. ...CHAIN_SELECTION_OPTIONS,
  123. "private-key": COMMON_DEPLOY_OPTIONS["private-key"],
  124. "ops-key-path": {
  125. type: "string",
  126. demandOption: true,
  127. desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
  128. },
  129. "std-output": {
  130. type: "string",
  131. demandOption: true,
  132. desc: "Path to the standard JSON output of the pyth contract (build artifact)",
  133. },
  134. } as const;
  135. export function makeCacheFunction(
  136. cacheFile: string
  137. ): (cacheKey: string, fn: () => Promise<string>) => Promise<string> {
  138. async function runIfNotCached(
  139. cacheKey: string,
  140. fn: () => Promise<string>
  141. ): Promise<string> {
  142. const cache = existsSync(cacheFile)
  143. ? JSON.parse(readFileSync(cacheFile, "utf8"))
  144. : {};
  145. if (cache[cacheKey]) {
  146. return cache[cacheKey];
  147. }
  148. const result = await fn();
  149. cache[cacheKey] = result;
  150. writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
  151. return result;
  152. }
  153. return runIfNotCached;
  154. }
  155. export function getSelectedChains(argv: {
  156. chain: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["chain"]>;
  157. testnet: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["testnet"]>;
  158. allChains: InferredOptionType<typeof COMMON_UPGRADE_OPTIONS["all-chains"]>;
  159. }) {
  160. const selectedChains: EvmChain[] = [];
  161. if (argv.allChains && argv.chain)
  162. throw new Error("Cannot use both --all-chains and --chain");
  163. if (!argv.allChains && !argv.chain)
  164. throw new Error("Must use either --all-chains or --chain");
  165. for (const chain of Object.values(DefaultStore.chains)) {
  166. if (!(chain instanceof EvmChain)) continue;
  167. if (
  168. (argv.allChains && chain.isMainnet() !== argv.testnet) ||
  169. argv.chain?.includes(chain.getId())
  170. )
  171. selectedChains.push(chain);
  172. }
  173. if (argv.chain && selectedChains.length !== argv.chain.length)
  174. throw new Error(
  175. `Some chains were not found ${selectedChains
  176. .map((chain) => chain.getId())
  177. .toString()}`
  178. );
  179. for (const chain of selectedChains) {
  180. if (chain.isMainnet() != selectedChains[0].isMainnet())
  181. throw new Error("All chains must be either mainnet or testnet");
  182. }
  183. return selectedChains;
  184. }
  185. /**
  186. * Finds the entropy contract for a given EVM chain.
  187. * @param {EvmChain} chain The EVM chain to find the entropy contract for.
  188. * @returns The entropy contract for the given EVM chain.
  189. * @throws {Error} an error if the entropy contract is not found for the given EVM chain.
  190. */
  191. export function findEntropyContract(chain: EvmChain): EvmEntropyContract {
  192. for (const contract of Object.values(DefaultStore.entropy_contracts)) {
  193. if (contract.getChain().getId() === chain.getId()) {
  194. return contract;
  195. }
  196. }
  197. throw new Error(`Entropy contract not found for chain ${chain.getId()}`);
  198. }
  199. /**
  200. * Finds an EVM chain by its name.
  201. * @param {string} chainName The name of the chain to find.
  202. * @returns The EVM chain instance.
  203. * @throws {Error} an error if the chain is not found or is not an EVM chain.
  204. */
  205. export function findEvmChain(chainName: string): EvmChain {
  206. const chain = DefaultStore.chains[chainName];
  207. if (!chain) {
  208. throw new Error(`Chain ${chainName} not found`);
  209. } else if (!(chain instanceof EvmChain)) {
  210. throw new Error(`Chain ${chainName} is not an EVM chain`);
  211. }
  212. return chain;
  213. }
  214. /**
  215. * Finds the wormhole contract for a given EVM chain.
  216. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  217. * @returns If found, the wormhole contract for the given EVM chain. Else, undefined
  218. */
  219. export function findWormholeContract(
  220. chain: EvmChain
  221. ): EvmWormholeContract | undefined {
  222. for (const contract of Object.values(DefaultStore.wormhole_contracts)) {
  223. if (
  224. contract instanceof EvmWormholeContract &&
  225. contract.getChain().getId() === chain.getId()
  226. ) {
  227. return contract;
  228. }
  229. }
  230. }
  231. export interface DeployWormholeReceiverContractsConfig
  232. extends BaseDeployConfig {
  233. saveContract: boolean;
  234. type: "stable" | "beta";
  235. }
  236. /**
  237. * Deploys the wormhole receiver contract for a given EVM chain.
  238. * @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
  239. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  240. * @param {string} cacheFile The path to the cache file.
  241. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  242. */
  243. export async function deployWormholeContract(
  244. chain: EvmChain,
  245. config: DeployWormholeReceiverContractsConfig,
  246. cacheFile: string
  247. ): Promise<EvmWormholeContract> {
  248. const receiverSetupAddr = await deployIfNotCached(
  249. cacheFile,
  250. chain,
  251. config,
  252. "ReceiverSetup",
  253. []
  254. );
  255. const receiverImplAddr = await deployIfNotCached(
  256. cacheFile,
  257. chain,
  258. config,
  259. "ReceiverImplementation",
  260. []
  261. );
  262. // Craft the init data for the proxy contract
  263. const setupContract = getWeb3Contract(
  264. config.jsonOutputDir,
  265. "ReceiverSetup",
  266. receiverSetupAddr
  267. );
  268. const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
  269. const initData = setupContract.methods
  270. .setup(
  271. receiverImplAddr,
  272. wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
  273. chain.getWormholeChainId(),
  274. wormholeConfig.governanceChainId,
  275. "0x" + wormholeConfig.governanceContract
  276. )
  277. .encodeABI();
  278. const wormholeReceiverAddr = await deployIfNotCached(
  279. cacheFile,
  280. chain,
  281. config,
  282. "WormholeReceiver",
  283. [receiverSetupAddr, initData]
  284. );
  285. const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
  286. if (config.type === "stable") {
  287. console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
  288. // TODO: Add a way to pass gas configs to this
  289. await wormholeContract.syncMainnetGuardianSets(config.privateKey);
  290. console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
  291. }
  292. if (config.saveContract) {
  293. DefaultStore.wormhole_contracts[wormholeContract.getId()] =
  294. wormholeContract;
  295. DefaultStore.saveAllContracts();
  296. }
  297. return wormholeContract;
  298. }
  299. /**
  300. * Returns the wormhole contract for a given EVM chain.
  301. * If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
  302. * @param {EvmChain} chain The EVM chain to find the wormhole contract for.
  303. * @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
  304. * @param {string} cacheFile The path to the cache file.
  305. * @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
  306. */
  307. export async function getOrDeployWormholeContract(
  308. chain: EvmChain,
  309. config: DeployWormholeReceiverContractsConfig,
  310. cacheFile: string
  311. ): Promise<EvmWormholeContract> {
  312. return (
  313. findWormholeContract(chain) ??
  314. (await deployWormholeContract(chain, config, cacheFile))
  315. );
  316. }