chains.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. import { KeyValueConfig, PrivateKey, Storable, TxResult } from "./base";
  2. import {
  3. ChainName,
  4. SetFee,
  5. CosmosUpgradeContract,
  6. EvmUpgradeContract,
  7. SuiAuthorizeUpgradeContract,
  8. AptosAuthorizeUpgradeContract,
  9. toChainId,
  10. SetDataSources,
  11. SetValidPeriod,
  12. DataSource,
  13. EvmSetWormholeAddress,
  14. } from "xc_admin_common";
  15. import { AptosClient, AptosAccount, CoinClient, TxnBuilderTypes } from "aptos";
  16. import Web3 from "web3";
  17. import {
  18. CosmwasmExecutor,
  19. CosmwasmQuerier,
  20. InjectiveExecutor,
  21. } from "@pythnetwork/cosmwasm-deploy-tools";
  22. import { Network } from "@injectivelabs/networks";
  23. import { SuiClient } from "@mysten/sui.js/client";
  24. import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
  25. import { TransactionObject } from "web3/eth/types";
  26. export type ChainConfig = Record<string, string> & {
  27. mainnet: boolean;
  28. id: string;
  29. };
  30. export abstract class Chain extends Storable {
  31. public wormholeChainName: ChainName;
  32. /**
  33. * Creates a new Chain object
  34. * @param id unique id representing this chain
  35. * @param mainnet whether this chain is mainnet or testnet/devnet
  36. * @param wormholeChainName the name of the wormhole chain that this chain is associated with.
  37. * Note that pyth has included additional chain names and ids to the wormhole spec.
  38. * @protected
  39. */
  40. protected constructor(
  41. protected id: string,
  42. protected mainnet: boolean,
  43. wormholeChainName: string
  44. ) {
  45. super();
  46. this.wormholeChainName = wormholeChainName as ChainName;
  47. if (toChainId(this.wormholeChainName) === undefined)
  48. throw new Error(
  49. `Invalid chain name ${wormholeChainName}.
  50. Try rebuilding xc_admin_common: npx lerna run build --scope xc_admin_common`
  51. );
  52. }
  53. public getWormholeChainId(): number {
  54. return toChainId(this.wormholeChainName);
  55. }
  56. getId(): string {
  57. return this.id;
  58. }
  59. isMainnet(): boolean {
  60. return this.mainnet;
  61. }
  62. /**
  63. * Returns the payload for a governance SetFee instruction for contracts deployed on this chain
  64. * @param fee the new fee to set
  65. * @param exponent the new fee exponent to set
  66. */
  67. generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
  68. return new SetFee(
  69. this.wormholeChainName,
  70. BigInt(fee),
  71. BigInt(exponent)
  72. ).encode();
  73. }
  74. /**
  75. * Returns the payload for a governance SetDataSources instruction for contracts deployed on this chain
  76. * @param datasources the new datasources
  77. */
  78. generateGovernanceSetDataSources(datasources: DataSource[]): Buffer {
  79. return new SetDataSources(this.wormholeChainName, datasources).encode();
  80. }
  81. /**
  82. * Returns the payload for a governance SetStalePriceThreshold instruction for contracts deployed on this chain
  83. * @param newValidStalePriceThreshold the new stale price threshold in seconds
  84. */
  85. generateGovernanceSetStalePriceThreshold(
  86. newValidStalePriceThreshold: bigint
  87. ): Buffer {
  88. return new SetValidPeriod(
  89. this.wormholeChainName,
  90. newValidStalePriceThreshold
  91. ).encode();
  92. }
  93. /**
  94. * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
  95. * @param upgradeInfo based on the contract type, this can be a contract address, codeId, package digest, etc.
  96. */
  97. abstract generateGovernanceUpgradePayload(upgradeInfo: unknown): Buffer;
  98. /**
  99. * Returns the account address associated with the given private key.
  100. * @param privateKey the account private key
  101. */
  102. abstract getAccountAddress(privateKey: PrivateKey): Promise<string>;
  103. /**
  104. * Returns the balance of the account associated with the given private key.
  105. * @param privateKey the account private key
  106. */
  107. abstract getAccountBalance(privateKey: PrivateKey): Promise<number>;
  108. }
  109. /**
  110. * A Chain object that represents all chains. This is used for governance instructions that apply to all chains.
  111. * For example, governance instructions to upgrade Pyth data sources.
  112. */
  113. export class GlobalChain extends Chain {
  114. static type = "GlobalChain";
  115. constructor() {
  116. super("global", true, "unset");
  117. }
  118. generateGovernanceUpgradePayload(): Buffer {
  119. throw new Error(
  120. "Can not create a governance message for upgrading contracts on all chains!"
  121. );
  122. }
  123. async getAccountAddress(_privateKey: PrivateKey): Promise<string> {
  124. throw new Error("Can not get account for GlobalChain.");
  125. }
  126. async getAccountBalance(_privateKey: PrivateKey): Promise<number> {
  127. throw new Error("Can not get account balance for GlobalChain.");
  128. }
  129. getType(): string {
  130. return GlobalChain.type;
  131. }
  132. toJson(): KeyValueConfig {
  133. return {
  134. id: this.id,
  135. wormholeChainName: this.wormholeChainName,
  136. mainnet: this.mainnet,
  137. type: GlobalChain.type,
  138. };
  139. }
  140. }
  141. export class CosmWasmChain extends Chain {
  142. static type = "CosmWasmChain";
  143. constructor(
  144. id: string,
  145. mainnet: boolean,
  146. wormholeChainName: string,
  147. public endpoint: string,
  148. public gasPrice: string,
  149. public prefix: string,
  150. public feeDenom: string
  151. ) {
  152. super(id, mainnet, wormholeChainName);
  153. }
  154. static fromJson(parsed: ChainConfig): CosmWasmChain {
  155. if (parsed.type !== CosmWasmChain.type) throw new Error("Invalid type");
  156. return new CosmWasmChain(
  157. parsed.id,
  158. parsed.mainnet,
  159. parsed.wormholeChainName,
  160. parsed.endpoint,
  161. parsed.gasPrice,
  162. parsed.prefix,
  163. parsed.feeDenom
  164. );
  165. }
  166. toJson(): KeyValueConfig {
  167. return {
  168. endpoint: this.endpoint,
  169. id: this.id,
  170. wormholeChainName: this.wormholeChainName,
  171. mainnet: this.mainnet,
  172. gasPrice: this.gasPrice,
  173. prefix: this.prefix,
  174. feeDenom: this.feeDenom,
  175. type: CosmWasmChain.type,
  176. };
  177. }
  178. getType(): string {
  179. return CosmWasmChain.type;
  180. }
  181. async getCode(codeId: number): Promise<Buffer> {
  182. const chainQuerier = await CosmwasmQuerier.connect(this.endpoint);
  183. return await chainQuerier.getCode({ codeId });
  184. }
  185. generateGovernanceUpgradePayload(codeId: bigint): Buffer {
  186. return new CosmosUpgradeContract(this.wormholeChainName, codeId).encode();
  187. }
  188. async getExecutor(
  189. privateKey: PrivateKey
  190. ): Promise<CosmwasmExecutor | InjectiveExecutor> {
  191. if (this.getId().indexOf("injective") > -1) {
  192. return InjectiveExecutor.fromPrivateKey(
  193. this.isMainnet() ? Network.Mainnet : Network.Testnet,
  194. privateKey
  195. );
  196. }
  197. return new CosmwasmExecutor(
  198. this.endpoint,
  199. await CosmwasmExecutor.getSignerFromPrivateKey(privateKey, this.prefix),
  200. this.gasPrice + this.feeDenom
  201. );
  202. }
  203. async getAccountAddress(privateKey: PrivateKey): Promise<string> {
  204. const executor = await this.getExecutor(privateKey);
  205. if (executor instanceof InjectiveExecutor) {
  206. return executor.getAddress();
  207. } else {
  208. return await executor.getAddress();
  209. }
  210. }
  211. async getAccountBalance(privateKey: PrivateKey): Promise<number> {
  212. const executor = await this.getExecutor(privateKey);
  213. return await executor.getBalance();
  214. }
  215. }
  216. export class SuiChain extends Chain {
  217. static type = "SuiChain";
  218. constructor(
  219. id: string,
  220. mainnet: boolean,
  221. wormholeChainName: string,
  222. public rpcUrl: string
  223. ) {
  224. super(id, mainnet, wormholeChainName);
  225. }
  226. static fromJson(parsed: ChainConfig): SuiChain {
  227. if (parsed.type !== SuiChain.type) throw new Error("Invalid type");
  228. return new SuiChain(
  229. parsed.id,
  230. parsed.mainnet,
  231. parsed.wormholeChainName,
  232. parsed.rpcUrl
  233. );
  234. }
  235. toJson(): KeyValueConfig {
  236. return {
  237. id: this.id,
  238. wormholeChainName: this.wormholeChainName,
  239. mainnet: this.mainnet,
  240. rpcUrl: this.rpcUrl,
  241. type: SuiChain.type,
  242. };
  243. }
  244. getType(): string {
  245. return SuiChain.type;
  246. }
  247. /**
  248. * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
  249. * @param digest hex string of the 32 byte digest for the new package without the 0x prefix
  250. */
  251. generateGovernanceUpgradePayload(digest: string): Buffer {
  252. return new SuiAuthorizeUpgradeContract(
  253. this.wormholeChainName,
  254. digest
  255. ).encode();
  256. }
  257. getProvider(): SuiClient {
  258. return new SuiClient({ url: this.rpcUrl });
  259. }
  260. async getAccountAddress(privateKey: PrivateKey): Promise<string> {
  261. const keypair = Ed25519Keypair.fromSecretKey(
  262. Buffer.from(privateKey, "hex")
  263. );
  264. return keypair.toSuiAddress();
  265. }
  266. async getAccountBalance(privateKey: PrivateKey): Promise<number> {
  267. const provider = this.getProvider();
  268. const balance = await provider.getBalance({
  269. owner: await this.getAccountAddress(privateKey),
  270. });
  271. return Number(balance.totalBalance) / 10 ** 9;
  272. }
  273. }
  274. export class EvmChain extends Chain {
  275. static type = "EvmChain";
  276. constructor(
  277. id: string,
  278. mainnet: boolean,
  279. private rpcUrl: string,
  280. private networkId: number
  281. ) {
  282. // On EVM networks we use the chain id as the wormhole chain name
  283. super(id, mainnet, id);
  284. }
  285. static fromJson(parsed: ChainConfig & { networkId: number }): EvmChain {
  286. if (parsed.type !== EvmChain.type) throw new Error("Invalid type");
  287. return new EvmChain(
  288. parsed.id,
  289. parsed.mainnet,
  290. parsed.rpcUrl,
  291. parsed.networkId
  292. );
  293. }
  294. /**
  295. * Returns the chain rpc url with any environment variables replaced or throws an error if any are missing
  296. */
  297. getRpcUrl(): string {
  298. const envMatches = this.rpcUrl.match(/\$ENV_\w+/);
  299. if (envMatches) {
  300. for (const envMatch of envMatches) {
  301. const envName = envMatch.replace("$ENV_", "");
  302. const envValue = process.env[envName];
  303. if (!envValue) {
  304. throw new Error(
  305. `Missing env variable ${envName} required for chain ${this.id} rpc: ${this.rpcUrl}`
  306. );
  307. }
  308. this.rpcUrl = this.rpcUrl.replace(envMatch, envValue);
  309. }
  310. }
  311. return this.rpcUrl;
  312. }
  313. /**
  314. * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
  315. * @param address hex string of the 20 byte address of the contract to upgrade to without the 0x prefix
  316. */
  317. generateGovernanceUpgradePayload(address: string): Buffer {
  318. return new EvmUpgradeContract(this.wormholeChainName, address).encode();
  319. }
  320. generateGovernanceSetWormholeAddressPayload(address: string): Buffer {
  321. return new EvmSetWormholeAddress(this.wormholeChainName, address).encode();
  322. }
  323. toJson(): KeyValueConfig {
  324. return {
  325. id: this.id,
  326. mainnet: this.mainnet,
  327. rpcUrl: this.rpcUrl,
  328. networkId: this.networkId,
  329. type: EvmChain.type,
  330. };
  331. }
  332. getType(): string {
  333. return EvmChain.type;
  334. }
  335. async getGasPrice() {
  336. const web3 = new Web3(this.getRpcUrl());
  337. let gasPrice = await web3.eth.getGasPrice();
  338. // some testnets have inaccuarte gas prices that leads to transactions not being mined, we double it since it's free!
  339. if (!this.isMainnet()) {
  340. gasPrice = (BigInt(gasPrice) * 2n).toString();
  341. }
  342. return gasPrice;
  343. }
  344. async estiamteAndSendTransaction(
  345. transactionObject: TransactionObject<any>,
  346. txParams: { from?: string; value?: string }
  347. ) {
  348. const GAS_ESTIMATE_MULTIPLIER = 2;
  349. const gasEstimate = await transactionObject.estimateGas({
  350. gas: 15000000,
  351. ...txParams,
  352. });
  353. // Some networks like Filecoin do not support the normal transaction type and need a type 2 transaction.
  354. // To send a type 2 transaction, remove the ``gasPrice`` field and add the `type` field with the value
  355. // `0x2` to the transaction configuration parameters.
  356. return transactionObject.send({
  357. gas: gasEstimate * GAS_ESTIMATE_MULTIPLIER,
  358. gasPrice: await this.getGasPrice(),
  359. ...txParams,
  360. });
  361. }
  362. /**
  363. * Deploys a contract on this chain
  364. * @param privateKey hex string of the 32 byte private key without the 0x prefix
  365. * @param abi the abi of the contract, can be obtained from the compiled contract json file
  366. * @param bytecode bytecode of the contract, can be obtained from the compiled contract json file
  367. * @param deployArgs arguments to pass to the constructor. Each argument must begin with 0x if it's a hex string
  368. * @returns the address of the deployed contract
  369. */
  370. async deploy(
  371. privateKey: PrivateKey,
  372. abi: any, // eslint-disable-line @typescript-eslint/no-explicit-any
  373. bytecode: string,
  374. deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
  375. gasMultiplier = 1,
  376. gasPriceMultiplier = 1
  377. ): Promise<string> {
  378. const web3 = new Web3(this.getRpcUrl());
  379. const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
  380. web3.eth.accounts.wallet.add(signer);
  381. const contract = new web3.eth.Contract(abi);
  382. const deployTx = contract.deploy({ data: bytecode, arguments: deployArgs });
  383. const gas = (await deployTx.estimateGas()) * gasMultiplier;
  384. const gasPrice = Number(await this.getGasPrice()) * gasPriceMultiplier;
  385. const deployerBalance = await web3.eth.getBalance(signer.address);
  386. const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
  387. if (gasDiff > 0n) {
  388. throw new Error(
  389. `Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
  390. BigInt(gas) * BigInt(gasPrice)
  391. } wei, but only have ${deployerBalance} wei. We need ${
  392. Number(gasDiff) / 10 ** 18
  393. } ETH more.`
  394. );
  395. }
  396. const deployedContract = await deployTx.send({
  397. from: signer.address,
  398. gas,
  399. gasPrice: gasPrice.toString(),
  400. });
  401. return deployedContract.options.address;
  402. }
  403. async getAccountAddress(privateKey: PrivateKey): Promise<string> {
  404. const web3 = new Web3(this.getRpcUrl());
  405. const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
  406. return signer.address;
  407. }
  408. async getAccountBalance(privateKey: PrivateKey): Promise<number> {
  409. const web3 = new Web3(this.getRpcUrl());
  410. const balance = await web3.eth.getBalance(
  411. await this.getAccountAddress(privateKey)
  412. );
  413. return Number(balance) / 10 ** 18;
  414. }
  415. }
  416. export class AptosChain extends Chain {
  417. static type = "AptosChain";
  418. constructor(
  419. id: string,
  420. mainnet: boolean,
  421. wormholeChainName: string,
  422. public rpcUrl: string
  423. ) {
  424. super(id, mainnet, wormholeChainName);
  425. }
  426. getClient(): AptosClient {
  427. return new AptosClient(this.rpcUrl);
  428. }
  429. /**
  430. * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
  431. * @param digest hex string of the 32 byte digest for the new package without the 0x prefix
  432. */
  433. generateGovernanceUpgradePayload(digest: string): Buffer {
  434. return new AptosAuthorizeUpgradeContract(
  435. this.wormholeChainName,
  436. digest
  437. ).encode();
  438. }
  439. getType(): string {
  440. return AptosChain.type;
  441. }
  442. toJson(): KeyValueConfig {
  443. return {
  444. id: this.id,
  445. wormholeChainName: this.wormholeChainName,
  446. mainnet: this.mainnet,
  447. rpcUrl: this.rpcUrl,
  448. type: AptosChain.type,
  449. };
  450. }
  451. static fromJson(parsed: ChainConfig): AptosChain {
  452. if (parsed.type !== AptosChain.type) throw new Error("Invalid type");
  453. return new AptosChain(
  454. parsed.id,
  455. parsed.mainnet,
  456. parsed.wormholeChainName,
  457. parsed.rpcUrl
  458. );
  459. }
  460. async getAccountAddress(privateKey: PrivateKey): Promise<string> {
  461. const account = new AptosAccount(
  462. new Uint8Array(Buffer.from(privateKey, "hex"))
  463. );
  464. return account.address().toString();
  465. }
  466. async getAccountBalance(privateKey: PrivateKey): Promise<number> {
  467. const client = this.getClient();
  468. const account = new AptosAccount(
  469. new Uint8Array(Buffer.from(privateKey, "hex"))
  470. );
  471. const coinClient = new CoinClient(client);
  472. return Number(await coinClient.checkBalance(account)) / 10 ** 8;
  473. }
  474. async sendTransaction(
  475. senderPrivateKey: PrivateKey,
  476. txPayload: TxnBuilderTypes.TransactionPayloadEntryFunction
  477. ): Promise<TxResult> {
  478. const client = this.getClient();
  479. const sender = new AptosAccount(
  480. new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
  481. );
  482. const result = await client.generateSignSubmitWaitForTransaction(
  483. sender,
  484. txPayload,
  485. {
  486. maxGasAmount: BigInt(30000),
  487. }
  488. );
  489. return { id: result.hash, info: result };
  490. }
  491. }