| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- import { KeyValueConfig, PrivateKey, Storable, TxResult } from "./base";
- import {
- ChainName,
- SetFee,
- CosmosUpgradeContract,
- EvmUpgradeContract,
- SuiAuthorizeUpgradeContract,
- AptosAuthorizeUpgradeContract,
- toChainId,
- SetDataSources,
- SetValidPeriod,
- DataSource,
- EvmSetWormholeAddress,
- } from "xc_admin_common";
- import { AptosClient, AptosAccount, CoinClient, TxnBuilderTypes } from "aptos";
- import Web3 from "web3";
- import {
- CosmwasmExecutor,
- CosmwasmQuerier,
- InjectiveExecutor,
- } from "@pythnetwork/cosmwasm-deploy-tools";
- import { Network } from "@injectivelabs/networks";
- import { SuiClient } from "@mysten/sui.js/client";
- import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
- import { TransactionObject } from "web3/eth/types";
- export type ChainConfig = Record<string, string> & {
- mainnet: boolean;
- id: string;
- };
- export abstract class Chain extends Storable {
- public wormholeChainName: ChainName;
- /**
- * Creates a new Chain object
- * @param id unique id representing this chain
- * @param mainnet whether this chain is mainnet or testnet/devnet
- * @param wormholeChainName the name of the wormhole chain that this chain is associated with.
- * Note that pyth has included additional chain names and ids to the wormhole spec.
- * @protected
- */
- protected constructor(
- protected id: string,
- protected mainnet: boolean,
- wormholeChainName: string
- ) {
- super();
- this.wormholeChainName = wormholeChainName as ChainName;
- if (toChainId(this.wormholeChainName) === undefined)
- throw new Error(
- `Invalid chain name ${wormholeChainName}.
- Try rebuilding xc_admin_common: npx lerna run build --scope xc_admin_common`
- );
- }
- public getWormholeChainId(): number {
- return toChainId(this.wormholeChainName);
- }
- getId(): string {
- return this.id;
- }
- isMainnet(): boolean {
- return this.mainnet;
- }
- /**
- * Returns the payload for a governance SetFee instruction for contracts deployed on this chain
- * @param fee the new fee to set
- * @param exponent the new fee exponent to set
- */
- generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
- return new SetFee(
- this.wormholeChainName,
- BigInt(fee),
- BigInt(exponent)
- ).encode();
- }
- /**
- * Returns the payload for a governance SetDataSources instruction for contracts deployed on this chain
- * @param datasources the new datasources
- */
- generateGovernanceSetDataSources(datasources: DataSource[]): Buffer {
- return new SetDataSources(this.wormholeChainName, datasources).encode();
- }
- /**
- * Returns the payload for a governance SetStalePriceThreshold instruction for contracts deployed on this chain
- * @param newValidStalePriceThreshold the new stale price threshold in seconds
- */
- generateGovernanceSetStalePriceThreshold(
- newValidStalePriceThreshold: bigint
- ): Buffer {
- return new SetValidPeriod(
- this.wormholeChainName,
- newValidStalePriceThreshold
- ).encode();
- }
- /**
- * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
- * @param upgradeInfo based on the contract type, this can be a contract address, codeId, package digest, etc.
- */
- abstract generateGovernanceUpgradePayload(upgradeInfo: unknown): Buffer;
- /**
- * Returns the account address associated with the given private key.
- * @param privateKey the account private key
- */
- abstract getAccountAddress(privateKey: PrivateKey): Promise<string>;
- /**
- * Returns the balance of the account associated with the given private key.
- * @param privateKey the account private key
- */
- abstract getAccountBalance(privateKey: PrivateKey): Promise<number>;
- }
- /**
- * A Chain object that represents all chains. This is used for governance instructions that apply to all chains.
- * For example, governance instructions to upgrade Pyth data sources.
- */
- export class GlobalChain extends Chain {
- static type = "GlobalChain";
- constructor() {
- super("global", true, "unset");
- }
- generateGovernanceUpgradePayload(): Buffer {
- throw new Error(
- "Can not create a governance message for upgrading contracts on all chains!"
- );
- }
- async getAccountAddress(_privateKey: PrivateKey): Promise<string> {
- throw new Error("Can not get account for GlobalChain.");
- }
- async getAccountBalance(_privateKey: PrivateKey): Promise<number> {
- throw new Error("Can not get account balance for GlobalChain.");
- }
- getType(): string {
- return GlobalChain.type;
- }
- toJson(): KeyValueConfig {
- return {
- id: this.id,
- wormholeChainName: this.wormholeChainName,
- mainnet: this.mainnet,
- type: GlobalChain.type,
- };
- }
- }
- export class CosmWasmChain extends Chain {
- static type = "CosmWasmChain";
- constructor(
- id: string,
- mainnet: boolean,
- wormholeChainName: string,
- public endpoint: string,
- public gasPrice: string,
- public prefix: string,
- public feeDenom: string
- ) {
- super(id, mainnet, wormholeChainName);
- }
- static fromJson(parsed: ChainConfig): CosmWasmChain {
- if (parsed.type !== CosmWasmChain.type) throw new Error("Invalid type");
- return new CosmWasmChain(
- parsed.id,
- parsed.mainnet,
- parsed.wormholeChainName,
- parsed.endpoint,
- parsed.gasPrice,
- parsed.prefix,
- parsed.feeDenom
- );
- }
- toJson(): KeyValueConfig {
- return {
- endpoint: this.endpoint,
- id: this.id,
- wormholeChainName: this.wormholeChainName,
- mainnet: this.mainnet,
- gasPrice: this.gasPrice,
- prefix: this.prefix,
- feeDenom: this.feeDenom,
- type: CosmWasmChain.type,
- };
- }
- getType(): string {
- return CosmWasmChain.type;
- }
- async getCode(codeId: number): Promise<Buffer> {
- const chainQuerier = await CosmwasmQuerier.connect(this.endpoint);
- return await chainQuerier.getCode({ codeId });
- }
- generateGovernanceUpgradePayload(codeId: bigint): Buffer {
- return new CosmosUpgradeContract(this.wormholeChainName, codeId).encode();
- }
- async getExecutor(
- privateKey: PrivateKey
- ): Promise<CosmwasmExecutor | InjectiveExecutor> {
- if (this.getId().indexOf("injective") > -1) {
- return InjectiveExecutor.fromPrivateKey(
- this.isMainnet() ? Network.Mainnet : Network.Testnet,
- privateKey
- );
- }
- return new CosmwasmExecutor(
- this.endpoint,
- await CosmwasmExecutor.getSignerFromPrivateKey(privateKey, this.prefix),
- this.gasPrice + this.feeDenom
- );
- }
- async getAccountAddress(privateKey: PrivateKey): Promise<string> {
- const executor = await this.getExecutor(privateKey);
- if (executor instanceof InjectiveExecutor) {
- return executor.getAddress();
- } else {
- return await executor.getAddress();
- }
- }
- async getAccountBalance(privateKey: PrivateKey): Promise<number> {
- const executor = await this.getExecutor(privateKey);
- return await executor.getBalance();
- }
- }
- export class SuiChain extends Chain {
- static type = "SuiChain";
- constructor(
- id: string,
- mainnet: boolean,
- wormholeChainName: string,
- public rpcUrl: string
- ) {
- super(id, mainnet, wormholeChainName);
- }
- static fromJson(parsed: ChainConfig): SuiChain {
- if (parsed.type !== SuiChain.type) throw new Error("Invalid type");
- return new SuiChain(
- parsed.id,
- parsed.mainnet,
- parsed.wormholeChainName,
- parsed.rpcUrl
- );
- }
- toJson(): KeyValueConfig {
- return {
- id: this.id,
- wormholeChainName: this.wormholeChainName,
- mainnet: this.mainnet,
- rpcUrl: this.rpcUrl,
- type: SuiChain.type,
- };
- }
- getType(): string {
- return SuiChain.type;
- }
- /**
- * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
- * @param digest hex string of the 32 byte digest for the new package without the 0x prefix
- */
- generateGovernanceUpgradePayload(digest: string): Buffer {
- return new SuiAuthorizeUpgradeContract(
- this.wormholeChainName,
- digest
- ).encode();
- }
- getProvider(): SuiClient {
- return new SuiClient({ url: this.rpcUrl });
- }
- async getAccountAddress(privateKey: PrivateKey): Promise<string> {
- const keypair = Ed25519Keypair.fromSecretKey(
- Buffer.from(privateKey, "hex")
- );
- return keypair.toSuiAddress();
- }
- async getAccountBalance(privateKey: PrivateKey): Promise<number> {
- const provider = this.getProvider();
- const balance = await provider.getBalance({
- owner: await this.getAccountAddress(privateKey),
- });
- return Number(balance.totalBalance) / 10 ** 9;
- }
- }
- export class EvmChain extends Chain {
- static type = "EvmChain";
- constructor(
- id: string,
- mainnet: boolean,
- private rpcUrl: string,
- private networkId: number
- ) {
- // On EVM networks we use the chain id as the wormhole chain name
- super(id, mainnet, id);
- }
- static fromJson(parsed: ChainConfig & { networkId: number }): EvmChain {
- if (parsed.type !== EvmChain.type) throw new Error("Invalid type");
- return new EvmChain(
- parsed.id,
- parsed.mainnet,
- parsed.rpcUrl,
- parsed.networkId
- );
- }
- /**
- * Returns the chain rpc url with any environment variables replaced or throws an error if any are missing
- */
- getRpcUrl(): string {
- const envMatches = this.rpcUrl.match(/\$ENV_\w+/);
- if (envMatches) {
- for (const envMatch of envMatches) {
- const envName = envMatch.replace("$ENV_", "");
- const envValue = process.env[envName];
- if (!envValue) {
- throw new Error(
- `Missing env variable ${envName} required for chain ${this.id} rpc: ${this.rpcUrl}`
- );
- }
- this.rpcUrl = this.rpcUrl.replace(envMatch, envValue);
- }
- }
- return this.rpcUrl;
- }
- /**
- * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
- * @param address hex string of the 20 byte address of the contract to upgrade to without the 0x prefix
- */
- generateGovernanceUpgradePayload(address: string): Buffer {
- return new EvmUpgradeContract(this.wormholeChainName, address).encode();
- }
- generateGovernanceSetWormholeAddressPayload(address: string): Buffer {
- return new EvmSetWormholeAddress(this.wormholeChainName, address).encode();
- }
- toJson(): KeyValueConfig {
- return {
- id: this.id,
- mainnet: this.mainnet,
- rpcUrl: this.rpcUrl,
- networkId: this.networkId,
- type: EvmChain.type,
- };
- }
- getType(): string {
- return EvmChain.type;
- }
- async getGasPrice() {
- const web3 = new Web3(this.getRpcUrl());
- let gasPrice = await web3.eth.getGasPrice();
- // some testnets have inaccuarte gas prices that leads to transactions not being mined, we double it since it's free!
- if (!this.isMainnet()) {
- gasPrice = (BigInt(gasPrice) * 2n).toString();
- }
- return gasPrice;
- }
- async estiamteAndSendTransaction(
- transactionObject: TransactionObject<any>,
- txParams: { from?: string; value?: string }
- ) {
- const GAS_ESTIMATE_MULTIPLIER = 2;
- const gasEstimate = await transactionObject.estimateGas({
- gas: 15000000,
- ...txParams,
- });
- // Some networks like Filecoin do not support the normal transaction type and need a type 2 transaction.
- // To send a type 2 transaction, remove the ``gasPrice`` field and add the `type` field with the value
- // `0x2` to the transaction configuration parameters.
- return transactionObject.send({
- gas: gasEstimate * GAS_ESTIMATE_MULTIPLIER,
- gasPrice: await this.getGasPrice(),
- ...txParams,
- });
- }
- /**
- * Deploys a contract on this chain
- * @param privateKey hex string of the 32 byte private key without the 0x prefix
- * @param abi the abi of the contract, can be obtained from the compiled contract json file
- * @param bytecode bytecode of the contract, can be obtained from the compiled contract json file
- * @param deployArgs arguments to pass to the constructor. Each argument must begin with 0x if it's a hex string
- * @returns the address of the deployed contract
- */
- async deploy(
- privateKey: PrivateKey,
- abi: any, // eslint-disable-line @typescript-eslint/no-explicit-any
- bytecode: string,
- deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
- gasMultiplier = 1,
- gasPriceMultiplier = 1
- ): Promise<string> {
- const web3 = new Web3(this.getRpcUrl());
- const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
- web3.eth.accounts.wallet.add(signer);
- const contract = new web3.eth.Contract(abi);
- const deployTx = contract.deploy({ data: bytecode, arguments: deployArgs });
- const gas = (await deployTx.estimateGas()) * gasMultiplier;
- const gasPrice = Number(await this.getGasPrice()) * gasPriceMultiplier;
- const deployerBalance = await web3.eth.getBalance(signer.address);
- const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
- if (gasDiff > 0n) {
- throw new Error(
- `Insufficient funds to deploy contract. Need ${gas} (gas) * ${gasPrice} (gasPrice)= ${
- BigInt(gas) * BigInt(gasPrice)
- } wei, but only have ${deployerBalance} wei. We need ${
- Number(gasDiff) / 10 ** 18
- } ETH more.`
- );
- }
- const deployedContract = await deployTx.send({
- from: signer.address,
- gas,
- gasPrice: gasPrice.toString(),
- });
- return deployedContract.options.address;
- }
- async getAccountAddress(privateKey: PrivateKey): Promise<string> {
- const web3 = new Web3(this.getRpcUrl());
- const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
- return signer.address;
- }
- async getAccountBalance(privateKey: PrivateKey): Promise<number> {
- const web3 = new Web3(this.getRpcUrl());
- const balance = await web3.eth.getBalance(
- await this.getAccountAddress(privateKey)
- );
- return Number(balance) / 10 ** 18;
- }
- }
- export class AptosChain extends Chain {
- static type = "AptosChain";
- constructor(
- id: string,
- mainnet: boolean,
- wormholeChainName: string,
- public rpcUrl: string
- ) {
- super(id, mainnet, wormholeChainName);
- }
- getClient(): AptosClient {
- return new AptosClient(this.rpcUrl);
- }
- /**
- * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
- * @param digest hex string of the 32 byte digest for the new package without the 0x prefix
- */
- generateGovernanceUpgradePayload(digest: string): Buffer {
- return new AptosAuthorizeUpgradeContract(
- this.wormholeChainName,
- digest
- ).encode();
- }
- getType(): string {
- return AptosChain.type;
- }
- toJson(): KeyValueConfig {
- return {
- id: this.id,
- wormholeChainName: this.wormholeChainName,
- mainnet: this.mainnet,
- rpcUrl: this.rpcUrl,
- type: AptosChain.type,
- };
- }
- static fromJson(parsed: ChainConfig): AptosChain {
- if (parsed.type !== AptosChain.type) throw new Error("Invalid type");
- return new AptosChain(
- parsed.id,
- parsed.mainnet,
- parsed.wormholeChainName,
- parsed.rpcUrl
- );
- }
- async getAccountAddress(privateKey: PrivateKey): Promise<string> {
- const account = new AptosAccount(
- new Uint8Array(Buffer.from(privateKey, "hex"))
- );
- return account.address().toString();
- }
- async getAccountBalance(privateKey: PrivateKey): Promise<number> {
- const client = this.getClient();
- const account = new AptosAccount(
- new Uint8Array(Buffer.from(privateKey, "hex"))
- );
- const coinClient = new CoinClient(client);
- return Number(await coinClient.checkBalance(account)) / 10 ** 8;
- }
- async sendTransaction(
- senderPrivateKey: PrivateKey,
- txPayload: TxnBuilderTypes.TransactionPayloadEntryFunction
- ): Promise<TxResult> {
- const client = this.getClient();
- const sender = new AptosAccount(
- new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
- );
- const result = await client.generateSignSubmitWaitForTransaction(
- sender,
- txPayload,
- {
- maxGasAmount: BigInt(30000),
- }
- );
- return { id: result.hash, info: result };
- }
- }
|