From 66e5f186b298df276c2eb6aa55838e3c495dae25 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Khashkhashi Moghaddam Date: Wed, 12 Jul 2023 15:50:24 +0200 Subject: [PATCH] Initial version of contract manager sdk (#943) * Initial version of governance sdk * Add more functionality to Sui contract manager and migrate variable naming to camelCase * Refactor sui functions * Add prettier * Add SuiAuthorizeUpgradeContractInstruction for governance * Update cosmwasm deploy tools entry point and expose some classes * Remove console.logs from CosmWasm * Refactor storage logic and add sui docs * Use relative path for default path of store * More documentation and minor fixes * Rename package * Add EVM classes * Implement getters for data sources * Use Google naming convention for abbreviations More info here: https://google.github.io/styleguide/tsguide.html#identifiers-abbreviations * Change package license * More comments and documentation * Store code proxy function in CosmWasm --- contract_manager/package.json | 28 ++ contract_manager/src/base.ts | 37 ++ contract_manager/src/chains.ts | 107 +++++ contract_manager/src/cosmwasm.ts | 344 ++++++++++++++++ contract_manager/src/entities.ts | 273 +++++++++++++ contract_manager/src/evm.ts | 134 ++++++ contract_manager/src/shell.ts | 25 ++ contract_manager/src/store.ts | 105 +++++ contract_manager/src/sui.ts | 381 ++++++++++++++++++ contract_manager/src/test.ts | 48 +++ .../chains/CosmWasmChain/juno_testnet.json | 9 + .../store/chains/CosmWasmChain/neutron.json | 9 + .../CosmWasmChain/neutron_testnet_pion_1.json | 9 + .../CosmWasmChain/osmosis_testnet_5.json | 9 + .../chains/CosmWasmChain/sei_pacific_1.json | 9 + .../CosmWasmChain/sei_testnet_atlantic_2.json | 9 + .../chains/EVMChain/arbitrum_testnet.json | 5 + .../store/chains/EVMChain/cronos.json | 5 + .../store/chains/EVMChain/cronos_testnet.json | 5 + .../store/chains/SuiChain/sui_devnet.json | 5 + .../store/chains/SuiChain/sui_mainnet.json | 5 + .../store/chains/SuiChain/sui_testnet.json | 5 + ...al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json | 5 + ...v9xvgqh654630v7dfrhrkmr5slly53spg85wv.json | 5 + ...k9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json | 5 + ...r4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json | 5 + ...mq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json | 5 + ...0e68297772dd5a1f1d99897c581e2082dba5b.json | 5 + ...25F377F9F7631a05f4B01CeD32a6A2ab843C7.json | 5 + ...754cebeb63346e29114a535ea6f41315e5a3f.json | 6 + ...fe38f09f3684e360b56d8d90fc574e71e75f3.json | 6 + ...8f0f6fd9315f810ae61fa4001858851f21c88.json | 6 + contract_manager/tsconfig.json | 9 + governance/xc_governance_sdk_js/src/index.ts | 1 + .../xc_governance_sdk_js/src/instructions.ts | 10 + target_chains/cosmwasm/tools/package.json | 2 +- .../tools/src/chains-manager/index.ts | 2 + target_chains/cosmwasm/tools/src/index.ts | 3 + .../ethereum/contracts/networks/338.json | 16 - 39 files changed, 1645 insertions(+), 17 deletions(-) create mode 100644 contract_manager/package.json create mode 100644 contract_manager/src/base.ts create mode 100644 contract_manager/src/chains.ts create mode 100644 contract_manager/src/cosmwasm.ts create mode 100644 contract_manager/src/entities.ts create mode 100644 contract_manager/src/evm.ts create mode 100644 contract_manager/src/shell.ts create mode 100644 contract_manager/src/store.ts create mode 100644 contract_manager/src/sui.ts create mode 100644 contract_manager/src/test.ts create mode 100644 contract_manager/store/chains/CosmWasmChain/juno_testnet.json create mode 100644 contract_manager/store/chains/CosmWasmChain/neutron.json create mode 100644 contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json create mode 100644 contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json create mode 100644 contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json create mode 100644 contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json create mode 100644 contract_manager/store/chains/EVMChain/arbitrum_testnet.json create mode 100644 contract_manager/store/chains/EVMChain/cronos.json create mode 100644 contract_manager/store/chains/EVMChain/cronos_testnet.json create mode 100644 contract_manager/store/chains/SuiChain/sui_devnet.json create mode 100644 contract_manager/store/chains/SuiChain/sui_mainnet.json create mode 100644 contract_manager/store/chains/SuiChain/sui_testnet.json create mode 100644 contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json create mode 100644 contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json create mode 100644 contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json create mode 100644 contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json create mode 100644 contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json create mode 100644 contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json create mode 100644 contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json create mode 100644 contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json create mode 100644 contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json create mode 100644 contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json create mode 100644 contract_manager/tsconfig.json create mode 100644 target_chains/cosmwasm/tools/src/chains-manager/index.ts create mode 100644 target_chains/cosmwasm/tools/src/index.ts delete mode 100644 target_chains/ethereum/contracts/networks/338.json diff --git a/contract_manager/package.json b/contract_manager/package.json new file mode 100644 index 00000000..82843ec3 --- /dev/null +++ b/contract_manager/package.json @@ -0,0 +1,28 @@ +{ + "name": "@pythnetwork/pyth-contract-manager", + "version": "1.0.0", + "description": "Set of tools to manage pyth contracts", + "private": true, + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "shell": "ts-node ./src/shell.ts" + }, + "author": "", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/pyth-network/pyth-crosschain.git" + }, + "dependencies": { + "@pythnetwork/cosmwasm-deploy-tools": "*", + "@pythnetwork/price-service-client": "*", + "@pythnetwork/xc-governance-sdk": "*", + "@certusone/wormhole-sdk": "^0.9.8", + "ts-node": "^10.9.1", + "typescript": "^4.9.3" + }, + "devDependencies": { + "prettier": "^2.6.2" + } +} diff --git a/contract_manager/src/base.ts b/contract_manager/src/base.ts new file mode 100644 index 00000000..015db279 --- /dev/null +++ b/contract_manager/src/base.ts @@ -0,0 +1,37 @@ +import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk"; + +export abstract class Storable { + /** + * Returns the unique identifier for this object + */ + abstract getId(): string; + + /** + * Returns the type of this object. This is used to reconstruct the object and should match + * the static field type in the class responsible for constructing this object. + */ + abstract getType(): string; + + /** + * Returns a JSON representation of this object. It should be possible to + * reconstruct the object from the JSON using the fromJson method. + */ + abstract toJson(): any; +} + +export abstract class Contract extends Storable { + /** + * Returns the time period in seconds that stale data is considered valid for. + */ + abstract getValidTimePeriod(): Promise; + + /** + * Returns an array of data sources that this contract accepts price feed messages from + */ + abstract getDataSources(): Promise; + + /** + * Returns the single data source that this contract accepts governance messages from + */ + abstract getGovernanceDataSource(): Promise; +} diff --git a/contract_manager/src/chains.ts b/contract_manager/src/chains.ts new file mode 100644 index 00000000..d901accb --- /dev/null +++ b/contract_manager/src/chains.ts @@ -0,0 +1,107 @@ +import { readdirSync, readFileSync, writeFileSync } from "fs"; +import { Storable } from "./base"; + +export abstract class Chain extends Storable { + protected constructor(public id: string) { + super(); + } + + getId(): string { + return this.id; + } +} + +export class CosmWasmChain extends Chain { + static type: string = "CosmWasmChain"; + + constructor( + id: string, + public querierEndpoint: string, + public executorEndpoint: string, + public gasPrice: string, + public prefix: string, + public feeDenom: string + ) { + super(id); + } + + static fromJson(parsed: any): CosmWasmChain { + if (parsed.type !== CosmWasmChain.type) throw new Error("Invalid type"); + return new CosmWasmChain( + parsed.id, + parsed.querierEndpoint, + parsed.executorEndpoint, + parsed.gasPrice, + parsed.prefix, + parsed.feeDenom + ); + } + + toJson(): any { + return { + querierEndpoint: this.querierEndpoint, + executorEndpoint: this.executorEndpoint, + id: this.id, + gasPrice: this.gasPrice, + prefix: this.prefix, + feeDenom: this.feeDenom, + type: CosmWasmChain.type, + }; + } + + getType(): string { + return CosmWasmChain.type; + } +} + +export class SuiChain extends Chain { + static type: string = "SuiChain"; + + constructor(id: string, public rpcUrl: string) { + super(id); + } + + static fromJson(parsed: any): SuiChain { + if (parsed.type !== SuiChain.type) throw new Error("Invalid type"); + return new SuiChain(parsed.id, parsed.rpcUrl); + } + + toJson(): any { + return { + id: this.id, + rpcUrl: this.rpcUrl, + type: SuiChain.type, + }; + } + + getType(): string { + return SuiChain.type; + } +} + +export class EVMChain extends Chain { + static type: string = "EVMChain"; + + constructor(id: string, public rpcUrl: string) { + super(id); + } + + static fromJson(parsed: any): SuiChain { + if (parsed.type !== EVMChain.type) throw new Error("Invalid type"); + return new EVMChain(parsed.id, parsed.rpcUrl); + } + + toJson(): any { + return { + id: this.id, + rpcUrl: this.rpcUrl, + type: EVMChain.type, + }; + } + + getType(): string { + return EVMChain.type; + } +} + +export const Chains: Record = {}; diff --git a/contract_manager/src/cosmwasm.ts b/contract_manager/src/cosmwasm.ts new file mode 100644 index 00000000..996dd11a --- /dev/null +++ b/contract_manager/src/cosmwasm.ts @@ -0,0 +1,344 @@ +import { Chains, CosmWasmChain } from "./chains"; +import { readFileSync } from "fs"; +import { getPythConfig } from "@pythnetwork/cosmwasm-deploy-tools/lib/configs"; +import { + CHAINS, + DataSource, + HexString32Bytes, + SetFeeInstruction, +} from "@pythnetwork/xc-governance-sdk"; +import { DeploymentType } from "@pythnetwork/cosmwasm-deploy-tools/lib/helper"; +import { + CosmwasmExecutor, + PythWrapperExecutor, + PythWrapperQuerier, +} from "@pythnetwork/cosmwasm-deploy-tools"; +import { + ContractInfoResponse, + CosmwasmQuerier, +} from "@pythnetwork/cosmwasm-deploy-tools/lib/chains-manager/chain-querier"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { Contract } from "./base"; + +/** + * Variables here need to be snake case to match the on-chain contract configs + */ +namespace CosmWasmContract { + export interface WormholeSource { + emitter: string; + chain_id: number; + } + + export interface DeploymentConfig { + data_sources: WormholeSource[]; + governance_source: WormholeSource; + wormhole_contract: string; + governance_source_index: number; + governance_sequence_number: number; + chain_id: number; + valid_time_period_secs: number; + fee: { amount: string; denom: string }; + } +} + +export class CosmWasmContract extends Contract { + async getDataSources(): Promise { + const config = await this.getConfig(); + return config.config_v1.data_sources.map(({ emitter, chain_id }: any) => { + return new DataSource( + Number(chain_id), + new HexString32Bytes(Buffer.from(emitter, "base64").toString("hex")) + ); + }); + } + + async getGovernanceDataSource(): Promise { + const config = await this.getConfig(); + const { emitter: emitterAddress, chain_id: chainId } = + config.config_v1.governance_source; + return new DataSource( + Number(chainId), + new HexString32Bytes( + Buffer.from(emitterAddress, "base64").toString("hex") + ) + ); + } + + static type = "CosmWasmContract"; + + constructor(public chain: CosmWasmChain, public address: string) { + super(); + } + + static fromJson(parsed: any): CosmWasmContract { + if (parsed.type !== CosmWasmContract.type) throw new Error("Invalid type"); + if (!Chains[parsed.chain]) + throw new Error(`Chain ${parsed.chain} not found`); + return new CosmWasmContract( + Chains[parsed.chain] as CosmWasmChain, + parsed.address + ); + } + + getType(): string { + return CosmWasmContract.type; + } + + //TODO : make deploymentType enum stable | edge + static getDeploymentConfig( + chain: CosmWasmChain, + deploymentType: string, + wormholeContract: string + ): CosmWasmContract.DeploymentConfig { + return getPythConfig({ + feeDenom: chain.feeDenom, + wormholeChainId: CHAINS[chain.getId() as keyof typeof CHAINS], + wormholeContract, + deploymentType: deploymentType as DeploymentType, + }); + } + + /** + * Stores the wasm code on the specified chain using the provided mnemonic as the signer + * You can find the wasm artifacts from the repo releases + * @param chain chain to store the code on + * @param mnemonic mnemonic to use for signing the transaction + * @param wasmPath path in your local filesystem to the wasm artifact + */ + static async storeCode( + chain: CosmWasmChain, + mnemonic: string, + wasmPath: string + ) { + const contractBytes = readFileSync(wasmPath); + let executor = this.getExecutor(chain, mnemonic); + return executor.storeCode({ contractBytes }); + } + + /** + * Deploys a new contract to the specified chain using the uploaded wasm code codeId + * @param chain chain to deploy to + * @param codeId codeId of the uploaded wasm code. You can get this from the storeCode result + * @param config deployment config for initializing the contract (data sources, governance source, etc) + * @param mnemonic mnemonic to use for signing the transaction + */ + static async initialize( + chain: CosmWasmChain, + codeId: number, + config: CosmWasmContract.DeploymentConfig, + mnemonic: string + ): Promise { + let executor = this.getExecutor(chain, mnemonic); + let result = await executor.instantiateContract({ + codeId: codeId, + instMsg: config, + label: "pyth", + }); + await executor.updateContractAdmin({ + newAdminAddr: result.contractAddr, + contractAddr: result.contractAddr, + }); + return new CosmWasmContract(chain, result.contractAddr); + } + + /** + * Uploads the wasm code and initializes a new contract to the specified chain. + * Use this method if you are deploying to a new chain, or you want a fresh contract in + * a testnet environment. Uses the default deployment configurations for governance, data sources, + * valid time period, etc. You can manually run the storeCode and initialize methods if you want + * more control over the deployment process. + * @param chain + * @param wormholeContract + * @param mnemonic + * @param wasmPath + */ + static async deploy( + chain: CosmWasmChain, + wormholeContract: string, + mnemonic: string, + wasmPath: string + ): Promise { + let config = this.getDeploymentConfig(chain, "edge", wormholeContract); + const { codeId } = await this.storeCode(chain, mnemonic, wasmPath); + return this.initialize(chain, codeId, config, mnemonic); + } + + private static getExecutor(chain: CosmWasmChain, mnemonic: string) { + // TODO: logic for injective + return new CosmwasmExecutor( + chain.executorEndpoint, + mnemonic, + chain.prefix, + chain.gasPrice + chain.feeDenom + ); + } + + getId(): string { + return `${this.chain.getId()}_${this.address}`; + } + + toJson() { + return { + chain: this.chain.id, + address: this.address, + type: CosmWasmContract.type, + }; + } + + async getQuerier(): Promise { + const chainQuerier = await CosmwasmQuerier.connect( + this.chain.querierEndpoint + ); + const pythQuerier = new PythWrapperQuerier(chainQuerier); + return pythQuerier; + } + + async getCodeId(): Promise { + let result = await this.getWasmContractInfo(); + return result.codeId; + } + + async getWasmContractInfo(): Promise { + const chainQuerier = await CosmwasmQuerier.connect( + this.chain.querierEndpoint + ); + return chainQuerier.getContractInfo({ contractAddr: this.address }); + } + + async getConfig() { + const chainQuerier = await CosmwasmQuerier.connect( + this.chain.querierEndpoint + ); + let allStates = (await chainQuerier.getAllContractState({ + contractAddr: this.address, + })) as any; + let config = { + config_v1: JSON.parse(allStates["\x00\tconfig_v1"]), + contract_version: JSON.parse(allStates["\x00\x10contract_version"]), + }; + return config; + } + + // TODO: function for uploading the code and getting the code id + // TODO: function for upgrading the contract + // TODO: Cleanup and more strict linter to convert let to const + + async getPriceFeed(feedId: string): Promise { + let querier = await this.getQuerier(); + return querier.getPriceFeed(this.address, feedId); + } + + equalDataSources( + dataSources1: CosmWasmContract.WormholeSource[], + dataSources2: CosmWasmContract.WormholeSource[] + ): boolean { + if (dataSources1.length !== dataSources2.length) return false; + for (let i = 0; i < dataSources1.length; i++) { + let found = false; + for (let j = 0; j < dataSources2.length; j++) { + if ( + dataSources1[i].emitter === dataSources2[j].emitter && + dataSources1[i].chain_id === dataSources2[j].chain_id + ) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } + + async getDeploymentType(): Promise { + let config = await this.getConfig(); + let wormholeContract = config.config_v1.wormhole_contract; + let stableConfig = getPythConfig({ + feeDenom: this.chain.feeDenom, + wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS], + wormholeContract, + deploymentType: "stable", + }); + let edgeConfig = getPythConfig({ + feeDenom: this.chain.feeDenom, + wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS], + wormholeContract, + deploymentType: "edge", + }); + if ( + this.equalDataSources( + config.config_v1.data_sources, + stableConfig.data_sources + ) + ) + return "stable"; + else if ( + this.equalDataSources( + config.config_v1.data_sources, + edgeConfig.data_sources + ) + ) + return "edge"; + else return "unknown"; + } + + async executeUpdatePriceFeed(feedId: string, mnemonic: string) { + const deploymentType = await this.getDeploymentType(); + const priceServiceConnection = new PriceServiceConnection( + deploymentType === "stable" + ? "https://xc-mainnet.pyth.network" + : "https://xc-testnet.pyth.network" + ); + + const vaas = await priceServiceConnection.getLatestVaas([feedId]); + const fund = await this.getUpdateFee(vaas); + let executor = new CosmwasmExecutor( + this.chain.executorEndpoint, + mnemonic, + this.chain.prefix, + this.chain.gasPrice + this.chain.feeDenom + ); + let pythExecutor = new PythWrapperExecutor(executor); + return pythExecutor.executeUpdatePriceFeeds({ + contractAddr: this.address, + vaas, + fund, + }); + } + + async executeGovernanceInstruction(mnemonic: string, vaa: string) { + let executor = new CosmwasmExecutor( + this.chain.executorEndpoint, + mnemonic, + this.chain.prefix, + this.chain.gasPrice + this.chain.feeDenom + ); + let pythExecutor = new PythWrapperExecutor(executor); + return pythExecutor.executeGovernanceInstruction({ + contractAddr: this.address, + vaa, + }); + } + + async getUpdateFee(msgs: string[]): Promise { + let querier = await this.getQuerier(); + return querier.getUpdateFee(this.address, msgs); + } + + getSetUpdateFeePayload(fee: number): Buffer { + return new SetFeeInstruction( + CHAINS[this.chain.getId() as keyof typeof CHAINS], + BigInt(fee), + BigInt(0) + ).serialize(); + } + + async getValidTimePeriod() { + let client = await CosmWasmClient.connect(this.chain.querierEndpoint); + let result = await client.queryContractSmart( + this.address, + "get_valid_time_period" + ); + return Number(result.secs + result.nanos * 1e-9); + } +} diff --git a/contract_manager/src/entities.ts b/contract_manager/src/entities.ts new file mode 100644 index 00000000..5d2218bb --- /dev/null +++ b/contract_manager/src/entities.ts @@ -0,0 +1,273 @@ +import { readFileSync } from "fs"; + +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; + +import { BN } from "bn.js"; +import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster"; +import SquadsMesh, { getTxPDA } from "@sqds/mesh"; +import { AnchorProvider, Wallet } from "@coral-xyz/anchor/dist/cjs/provider"; +import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; +import { WORMHOLE_ADDRESS, WORMHOLE_API_ENDPOINT } from "xc_admin_common"; +import { + createWormholeProgramInterface, + deriveEmitterSequenceKey, + deriveFeeCollectorKey, + deriveWormholeBridgeDataKey, +} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { Contract, Storable } from "./base"; + +export const Contracts: Record = {}; + +export class SubmittedWormholeMessage { + constructor( + public emitter: PublicKey, + public sequenceNumber: number, + public cluster: string + ) {} + + /** + * Tries to fetch the VAA from the wormhole bridge API waiting for a certain amount of time + * before giving up and throwing an error + * @param waitingSeconds how long to wait before giving up + */ + async fetchVaa(waitingSeconds: number = 1): Promise { + let rpcUrl = + WORMHOLE_API_ENDPOINT[this.cluster as keyof typeof WORMHOLE_API_ENDPOINT]; + + let startTime = Date.now(); + while (Date.now() - startTime < waitingSeconds * 1000) { + const response = await fetch( + `${rpcUrl}/v1/signed_vaa/1/${this.emitter.toBuffer().toString("hex")}/${ + this.sequenceNumber + }` + ); + if (response.status === 404) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + continue; + } + const { vaaBytes } = await response.json(); + return Buffer.from(vaaBytes, "base64"); + } + throw new Error("VAA not found, maybe too soon to fetch?"); + } +} + +/** + * A simple emitter that can send messages to the wormhole bridge + * This can be used instead of multisig as a simple way to send messages + * and debug contracts deployed on testing networks + * You need to set your pyth contract data source / governance source address to this emitter + */ +export class WormholeEmitter { + cluster: string; + wallet: Wallet; + + constructor(cluster: string, wallet: Wallet) { + this.cluster = cluster; + this.wallet = wallet; + } + + async sendMessage(payload: Buffer) { + const provider = new AnchorProvider( + new Connection(getPythClusterApiUrl(this.cluster as any), "confirmed"), + this.wallet, + { + commitment: "confirmed", + preflightCommitment: "confirmed", + } + ); + let wormholeAddress = + WORMHOLE_ADDRESS[this.cluster as keyof typeof WORMHOLE_ADDRESS]!; + let kp = Keypair.generate(); + let feeCollector = deriveFeeCollectorKey(wormholeAddress); + let emitter = this.wallet.publicKey; + let accounts = { + bridge: deriveWormholeBridgeDataKey(wormholeAddress), + message: kp.publicKey, + emitter: emitter, + sequence: deriveEmitterSequenceKey(emitter, wormholeAddress), + payer: emitter, + feeCollector, + clock: SYSVAR_CLOCK_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }; + const wormholeProgram = createWormholeProgramInterface( + wormholeAddress, + provider + ); + const transaction = new Transaction(); + transaction.add( + SystemProgram.transfer({ + fromPubkey: emitter, + toPubkey: feeCollector, + lamports: 1000, + }) + ); + transaction.add( + await wormholeProgram.methods + .postMessage(0, payload, 0) + .accounts(accounts) + .instruction() + ); + const txSig = await provider.sendAndConfirm(transaction, [kp]); + const txDetails = await provider.connection.getParsedTransaction(txSig); + const sequenceLogPrefix = "Sequence: "; + const txLog = txDetails?.meta?.logMessages?.find((s) => + s.includes(sequenceLogPrefix) + ); + + const sequenceNumber = Number( + txLog?.substring( + txLog.indexOf(sequenceLogPrefix) + sequenceLogPrefix.length + ) + ); + return new SubmittedWormholeMessage(emitter, sequenceNumber, this.cluster); + } +} + +export class Vault extends Storable { + static type: string = "vault"; + key: PublicKey; + squad?: SquadsMesh; + cluster: string; + + constructor(key: string, cluster: string) { + super(); + this.key = new PublicKey(key); + this.cluster = cluster; + } + + getType(): string { + return Vault.type; + } + + static from(path: string): Vault { + let parsed = JSON.parse(readFileSync(path, "utf-8")); + if (parsed.type !== Vault.type) throw new Error("Invalid type"); + return new Vault(parsed.key, parsed.cluster); + } + + getId(): string { + return `${this.cluster}_${this.key.toString()}`; + } + + toJson(): any { + return { + key: this.key.toString(), + cluster: this.cluster, + type: Vault.type, + }; + } + + public connect(wallet: Wallet): void { + this.squad = SquadsMesh.endpoint( + getPythClusterApiUrl(this.cluster as any), // TODO Fix any + wallet + ); + } + + public async createProposalIx( + proposalIndex: number + ): Promise<[TransactionInstruction, PublicKey]> { + const squad = this.getSquadOrThrow(); + const msAccount = await squad.getMultisig(this.key); + + const ix = await squad.buildCreateTransaction( + msAccount.publicKey, + msAccount.authorityIndex, + proposalIndex + ); + + const newProposalAddress = getTxPDA( + this.key, + new BN(proposalIndex), + squad.multisigProgramId + )[0]; + + return [ix, newProposalAddress]; + } + + public async activateProposalIx( + proposalAddress: PublicKey + ): Promise { + const squad = this.getSquadOrThrow(); + return await squad.buildActivateTransaction(this.key, proposalAddress); + } + + public async approveProposalIx( + proposalAddress: PublicKey + ): Promise { + const squad = this.getSquadOrThrow(); + return await squad.buildApproveTransaction(this.key, proposalAddress); + } + + getSquadOrThrow(): SquadsMesh { + if (!this.squad) throw new Error("Please connect a wallet to the vault"); + return this.squad; + } + + public async proposeWormholeMessage(payload: Buffer): Promise { + const squad = this.getSquadOrThrow(); + const msAccount = await squad.getMultisig(this.key); + + let ixToSend: TransactionInstruction[] = []; + const [proposalIx, newProposalAddress] = await this.createProposalIx( + msAccount.transactionIndex + 1 + ); + + const proposalIndex = msAccount.transactionIndex + 1; + ixToSend.push(proposalIx); + return ixToSend; + // const instructionToPropose = await getPostMessageInstruction( + // squad, + // this.key, + // newProposalAddress, + // 1, + // this.wormholeAddress()!, + // payload + // ); + // ixToSend.push( + // await squad.buildAddInstruction( + // this.key, + // newProposalAddress, + // instructionToPropose.instruction, + // 1, + // instructionToPropose.authorityIndex, + // instructionToPropose.authorityBump, + // instructionToPropose.authorityType + // ) + // ); + // ixToSend.push(await this.activateProposalIx(newProposalAddress)); + // ixToSend.push(await this.approveProposalIx(newProposalAddress)); + + // const txToSend = batchIntoTransactions(ixToSend); + // for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) { + // await this.getAnchorProvider().sendAll( + // txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => { + // return { tx, signers: [] }; + // }) + // ); + // } + // return newProposalAddress; + } +} + +export const Vaults: Record = {}; + +export async function loadHotWallet(wallet: string): Promise { + return new NodeWallet( + Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(readFileSync(wallet, "ascii"))) + ) + ); +} diff --git a/contract_manager/src/evm.ts b/contract_manager/src/evm.ts new file mode 100644 index 00000000..934ff9a9 --- /dev/null +++ b/contract_manager/src/evm.ts @@ -0,0 +1,134 @@ +import Web3 from "web3"; //TODO: decide on using web3 or ethers.js +import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json"; +import { Contract } from "./base"; +import { Chains, EVMChain } from "./chains"; +import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk"; + +export class EVMContract extends Contract { + static type = "EVMContract"; + + constructor(public chain: EVMChain, public address: string) { + super(); + } + + static fromJson(parsed: any): EVMContract { + if (parsed.type !== EVMContract.type) throw new Error("Invalid type"); + if (!Chains[parsed.chain]) + throw new Error(`Chain ${parsed.chain} not found`); + return new EVMContract(Chains[parsed.chain] as EVMChain, parsed.address); + } + + getId(): string { + return `${this.chain.getId()}_${this.address}`; + } + + getType(): string { + return EVMContract.type; + } + + getContract() { + const web3 = new Web3(this.chain.rpcUrl); + const pythContract = new web3.eth.Contract( + [ + { + inputs: [], + name: "governanceDataSource", + outputs: [ + { + components: [ + { + internalType: "uint16", + name: "chainId", + type: "uint16", + }, + { + internalType: "bytes32", + name: "emitterAddress", + type: "bytes32", + }, + ], + internalType: "struct PythInternalStructs.DataSource", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "validDataSources", + outputs: [ + { + components: [ + { + internalType: "uint16", + name: "chainId", + type: "uint16", + }, + { + internalType: "bytes32", + name: "emitterAddress", + type: "bytes32", + }, + ], + internalType: "struct PythInternalStructs.DataSource[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + constant: true, + }, + ...PythInterfaceAbi, + ] as any, + this.address + ); + return pythContract; + } + + async getPriceFeed(feedId: string) { + const pythContract = this.getContract(); + const [price, conf, expo, publishTime] = await pythContract.methods + .getPriceUnsafe(feedId) + .call(); + return { price, conf, expo, publishTime }; + } + + async getValidTimePeriod() { + const pythContract = this.getContract(); + const result = await pythContract.methods.getValidTimePeriod().call(); + return Number(result); + } + + async getDataSources(): Promise { + const pythContract = this.getContract(); + const result = await pythContract.methods.validDataSources().call(); + return result.map(({ chainId, emitterAddress }: any) => { + return new DataSource( + Number(chainId), + new HexString32Bytes(emitterAddress) + ); + }); + } + + async getGovernanceDataSource(): Promise { + const pythContract = this.getContract(); + const [chainId, emitterAddress] = await pythContract.methods + .governanceDataSource() + .call(); + return new DataSource( + Number(chainId), + new HexString32Bytes(emitterAddress) + ); + } + + toJson() { + return { + chain: this.chain.id, + address: this.address, + type: EVMContract.type, + }; + } +} diff --git a/contract_manager/src/shell.ts b/contract_manager/src/shell.ts new file mode 100644 index 00000000..50ffcb86 --- /dev/null +++ b/contract_manager/src/shell.ts @@ -0,0 +1,25 @@ +import * as tsNode from "ts-node"; + +const repl = tsNode.createRepl(); +const service = tsNode.create({ ...repl.evalAwarePartialHost }); +repl.setService(service); +repl.start(); +repl.evalCode( + "import { Contracts, Vaults, loadHotWallet } from './src/entities';" + + "import { Chains,SuiChain,CosmWasmChain } from './src/chains';" + + "import { SuiContract } from './src/sui';" + + "import { CosmWasmContract } from './src/cosmwasm';" + + "import { DefaultStore } from './src/store';" + + "DefaultStore" +); + +// import * as repl from 'node:repl'; +// import { CosmWasmChain, Chains, ChainContracts } from './entities'; +// // import { CHAINS_NETWORK_CONFIG } from './chains-manager/chains'; + +// const replServer = repl.start('Pyth shell> ') +// // const mnemonic = "salon myth guide analyst umbrella load arm first roast pelican stuff satoshi"; + +// replServer.context.CosmWasmChain = CosmWasmChain; +// replServer.context.Chains = Chains; +// replServer.context.ChainContracts = ChainContracts; diff --git a/contract_manager/src/store.ts b/contract_manager/src/store.ts new file mode 100644 index 00000000..60c6b499 --- /dev/null +++ b/contract_manager/src/store.ts @@ -0,0 +1,105 @@ +import { Chain, CosmWasmChain, SuiChain, Chains, EVMChain } from "./chains"; +import { CosmWasmContract } from "./cosmwasm"; +import { SuiContract } from "./sui"; +import { Contract } from "./base"; +import { + readdirSync, + readFileSync, + writeFileSync, + mkdirSync, + existsSync, + statSync, +} from "fs"; +import { Contracts } from "./entities"; +import { EVMContract } from "./evm"; + +class Store { + static Chains: Record = {}; + static Contracts: Record = {}; + + constructor(public path: string) { + this.loadAllChains(); + this.loadAllContracts(); + } + + save(obj: any) { + let dir, file, content; + if (obj instanceof Contract) { + let contract = obj; + dir = `${this.path}/contracts/${contract.getType()}`; + file = contract.getId(); + content = contract.toJson(); + } else if (obj instanceof Chain) { + let chain = obj; + dir = `${this.path}/chains/${chain.getType()}`; + file = chain.getId(); + content = chain.toJson(); + } else { + throw new Error("Invalid type"); + } + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync( + `${dir}/${file}.json`, + JSON.stringify(content, undefined, 2) + "\n" + ); + } + + getJSONFiles(path: string) { + const walk = function (dir: string) { + let results: string[] = []; + const list = readdirSync(dir); + list.forEach(function (file) { + file = dir + "/" + file; + const stat = statSync(file); + if (stat && stat.isDirectory()) { + // Recurse into a subdirectory + results = results.concat(walk(file)); + } else { + // Is a file + results.push(file); + } + }); + return results; + }; + return walk(path).filter((file) => file.endsWith(".json")); + } + + loadAllChains() { + let allChainClasses = { + [CosmWasmChain.type]: CosmWasmChain, + [SuiChain.type]: SuiChain, + [EVMChain.type]: EVMChain, + }; + + this.getJSONFiles(`${this.path}/chains/`).forEach((jsonFile) => { + let parsed = JSON.parse(readFileSync(jsonFile, "utf-8")); + if (allChainClasses[parsed.type] === undefined) return; + let chain = allChainClasses[parsed.type].fromJson(parsed); + if (Chains[chain.getId()]) + throw new Error(`Multiple chains with id ${chain.getId()} found`); + Chains[chain.getId()] = chain; + }); + } + + loadAllContracts() { + let allContractClasses = { + [CosmWasmContract.type]: CosmWasmContract, + [SuiContract.type]: SuiContract, + [EVMContract.type]: EVMContract, + }; + this.getJSONFiles(`${this.path}/contracts/`).forEach((jsonFile) => { + let parsed = JSON.parse(readFileSync(jsonFile, "utf-8")); + if (allContractClasses[parsed.type] === undefined) return; + let chainContract = allContractClasses[parsed.type].fromJson(parsed); + if (Contracts[chainContract.getId()]) + throw new Error( + `Multiple contracts with id ${chainContract.getId()} found` + ); + Contracts[chainContract.getId()] = chainContract; + }); + } +} + +export const DefaultStore = new Store(`${__dirname}/../store`); diff --git a/contract_manager/src/sui.ts b/contract_manager/src/sui.ts new file mode 100644 index 00000000..65fc0c93 --- /dev/null +++ b/contract_manager/src/sui.ts @@ -0,0 +1,381 @@ +import { + RawSigner, + SUI_CLOCK_OBJECT_ID, + TransactionBlock, + JsonRpcProvider, + Ed25519Keypair, + Connection, + ObjectId, +} from "@mysten/sui.js"; +import { readFileSync, writeFileSync } from "fs"; +import { Chains, SuiChain } from "./chains"; +import { + CHAINS, + DataSource, + HexString32Bytes, + SetFeeInstruction, + SuiAuthorizeUpgradeContractInstruction, +} from "@pythnetwork/xc-governance-sdk"; +import { BufferBuilder } from "@pythnetwork/xc-governance-sdk/lib/serialize"; +import { Contract } from "./base"; + +export class SuiContract extends Contract { + static type = "SuiContract"; + + /** + * Given the ids of the pyth state and wormhole state, create a new SuiContract + * The package ids are derived based on the state ids + * + * @param chain the chain which this contract is deployed on + * @param stateId id of the pyth state for the deployed contract + * @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to + */ + constructor( + public chain: SuiChain, + public stateId: string, + public wormholeStateId: string + ) { + super(); + } + + static fromJson(parsed: any): SuiContract { + if (parsed.type !== SuiContract.type) throw new Error("Invalid type"); + if (!Chains[parsed.chain]) + throw new Error(`Chain ${parsed.chain} not found`); + return new SuiContract( + Chains[parsed.chain] as SuiChain, + parsed.stateId, + parsed.wormholeStateId + ); + } + + getType(): string { + return SuiContract.type; + } + + toJson() { + return { + chain: this.chain.id, + stateId: this.stateId, + wormholeStateId: this.wormholeStateId, + type: SuiContract.type, + }; + } + + /** + * Given a objectId, returns the id for the package that the object belongs to. + * @param objectId + */ + async getPackageId(objectId: ObjectId): Promise { + const provider = this.getProvider(); + const state = await provider + .getObject({ + id: objectId, + options: { + showContent: true, + }, + }) + .then((result) => { + if (result.data?.content?.dataType == "moveObject") { + return result.data.content.fields; + } + + throw new Error("not move object"); + }); + + if ("upgrade_cap" in state) { + return state.upgrade_cap.fields.package; + } + + throw new Error("upgrade_cap not found"); + } + + async getPythPackageId(): Promise { + return await this.getPackageId(this.stateId); + } + + async getWormholePackageId(): Promise { + return await this.getPackageId(this.wormholeStateId); + } + + getId(): string { + return `${this.chain.getId()}_${this.stateId}`; + } + + /** + * Fetches the price table object id for the current state id + */ + async getPriceTableId(): Promise { + const provider = this.getProvider(); + let result = await provider.getDynamicFieldObject({ + parentId: this.stateId, + name: { + type: "vector", + value: "price_info", + }, + }); + if (!result.data) { + throw new Error("Price Table not found, contract may not be initialized"); + } + return result.data.objectId; + } + + async getPriceFeed(feedId: string) { + const tableId = await this.getPriceTableId(); + const provider = this.getProvider(); + let result = await provider.getDynamicFieldObject({ + parentId: tableId, + name: { + type: `${await this.getPythPackageId()}::price_identifier::PriceIdentifier`, + value: { + bytes: Array.from(Buffer.from(feedId, "hex")), + }, + }, + }); + if (!result.data || !result.data.content) { + throw new Error("Price feed not found"); + } + if (result.data.content.dataType !== "moveObject") { + throw new Error("Price feed type mismatch"); + } + let priceInfoObjectId = result.data.content.fields.value; + let priceInfo = await provider.getObject({ + id: priceInfoObjectId, + options: { showContent: true }, + }); + if (!priceInfo.data || !priceInfo.data.content) { + throw new Error( + `Price feed ID ${priceInfoObjectId} in price table but object not found!!` + ); + } + if (priceInfo.data.content.dataType !== "moveObject") { + throw new Error( + `Expected ${priceInfoObjectId} to be a moveObject (PriceInfoObject)` + ); + } + return priceInfo.data.content.fields; + } + + /** + * Given a signed VAA, execute the migration instruction on the pyth contract. + * The payload of the VAA can be obtained from the `getUpgradePackagePayload` method. + * @param vaa + * @param keypair used to sign the transaction + */ + async executeMigrateInstruction(vaa: Buffer, keypair: Ed25519Keypair) { + const tx = new TransactionBlock(); + const packageId = await this.getPythPackageId(); + let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa); + + tx.moveCall({ + target: `${packageId}::migrate::migrate`, + arguments: [tx.object(this.stateId), decreeReceipt], + }); + + return this.executeTransaction(tx, keypair); + } + + getUpgradePackagePayload(digest: string): Buffer { + let setFee = new SuiAuthorizeUpgradeContractInstruction( + CHAINS["sui"], + new HexString32Bytes(digest) + ).serialize(); + return this.wrapWithWormholeGovernancePayload(0, setFee); + } + + getSetUpdateFeePayload(fee: number): Buffer { + let setFee = new SetFeeInstruction( + CHAINS["sui"], + BigInt(fee), + BigInt(0) + ).serialize(); + return this.wrapWithWormholeGovernancePayload(3, setFee); + } + + async executeGovernanceInstruction(vaa: Buffer, keypair: Ed25519Keypair) { + const tx = new TransactionBlock(); + const packageId = await this.getPythPackageId(); + let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa); + + tx.moveCall({ + target: `${packageId}::governance::execute_governance_instruction`, + arguments: [tx.object(this.stateId), decreeReceipt], + }); + + return this.executeTransaction(tx, keypair); + } + + async executeUpgradeInstruction( + vaa: Buffer, + keypair: Ed25519Keypair, + modules: number[][], + dependencies: string[] + ) { + const tx = new TransactionBlock(); + const packageId = await this.getPythPackageId(); + let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa); + + const [upgradeTicket] = tx.moveCall({ + target: `${packageId}::contract_upgrade::authorize_upgrade`, + arguments: [tx.object(this.stateId), decreeReceipt], + }); + + const [upgradeReceipt] = tx.upgrade({ + modules, + dependencies, + packageId: packageId, + ticket: upgradeTicket, + }); + + tx.moveCall({ + target: `${packageId}::contract_upgrade::commit_upgrade`, + arguments: [tx.object(this.stateId), upgradeReceipt], + }); + return this.executeTransaction(tx, keypair); + } + + private wrapWithWormholeGovernancePayload( + actionVariant: number, + payload: Buffer + ): Buffer { + const builder = new BufferBuilder(); + builder.addBuffer( + Buffer.from( + "0000000000000000000000000000000000000000000000000000000000000001", + "hex" + ) + ); + builder.addUint8(actionVariant); + builder.addUint16(CHAINS["sui"]); // should always be sui (21) no matter devnet or testnet + builder.addBuffer(payload); + return builder.build(); + } + + /** + * Utility function to get the decree receipt object for a VAA that can be + * used to authorize a governance instruction. + * @param tx + * @param packageId pyth package id + * @param vaa + * @private + */ + private async getVaaDecreeReceipt( + tx: TransactionBlock, + packageId: string, + vaa: Buffer + ) { + const wormholePackageId = await this.getWormholePackageId(); + let [decreeTicket] = tx.moveCall({ + target: `${packageId}::set_update_fee::authorize_governance`, + arguments: [tx.object(this.stateId), tx.pure(false)], + }); + + let [verifiedVAA] = tx.moveCall({ + target: `${wormholePackageId}::vaa::parse_and_verify`, + arguments: [ + tx.object(this.wormholeStateId), + tx.pure(Array.from(vaa)), + tx.object(SUI_CLOCK_OBJECT_ID), + ], + }); + + let [decreeReceipt] = tx.moveCall({ + target: `${wormholePackageId}::governance_message::verify_vaa`, + arguments: [tx.object(this.wormholeStateId), verifiedVAA, decreeTicket], + typeArguments: [`${packageId}::governance_witness::GovernanceWitness`], + }); + return decreeReceipt; + } + + /** + * Given a transaction block and a keypair, sign and execute it + * Sets the gas budget to 2x the estimated gas cost + * @param tx + * @param keypair + * @private + */ + private async executeTransaction( + tx: TransactionBlock, + keypair: Ed25519Keypair + ) { + const provider = this.getProvider(); + let txBlock = { + transactionBlock: tx, + options: { + showEffects: true, + showEvents: true, + }, + }; + const wallet = new RawSigner(keypair, provider); + let gasCost = await wallet.getGasCostEstimation(txBlock); + tx.setGasBudget(gasCost * BigInt(2)); + return wallet.signAndExecuteTransactionBlock(txBlock); + } + + async getValidTimePeriod() { + const fields = await this.getStateFields(); + return Number(fields.stale_price_threshold); + } + + async getDataSources(): Promise { + const provider = this.getProvider(); + let result = await provider.getDynamicFieldObject({ + parentId: this.stateId, + name: { + type: "vector", + value: "data_sources", + }, + }); + if (!result.data || !result.data.content) { + throw new Error( + "Data Sources not found, contract may not be initialized" + ); + } + if (result.data.content.dataType !== "moveObject") { + throw new Error("Data Sources type mismatch"); + } + return result.data.content.fields.value.fields.keys.map( + ({ fields }: any) => { + return new DataSource( + Number(fields.emitter_chain), + new HexString32Bytes( + Buffer.from( + fields.emitter_address.fields.value.fields.data + ).toString("hex") + ) + ); + } + ); + } + + async getGovernanceDataSource(): Promise { + const fields = await this.getStateFields(); + const governanceFields = fields.governance_data_source.fields; + const chainId = governanceFields.emitter_chain; + const emitterAddress = + governanceFields.emitter_address.fields.value.fields.data; + return new DataSource( + Number(chainId), + new HexString32Bytes(Buffer.from(emitterAddress).toString("hex")) + ); + } + + private getProvider() { + return new JsonRpcProvider(new Connection({ fullnode: this.chain.rpcUrl })); + } + + private async getStateFields() { + const provider = this.getProvider(); + const result = await provider.getObject({ + id: this.stateId, + options: { showContent: true }, + }); + if ( + !result.data || + !result.data.content || + result.data.content.dataType !== "moveObject" + ) + throw new Error("Unable to fetch pyth state object"); + return result.data.content.fields; + } +} diff --git a/contract_manager/src/test.ts b/contract_manager/src/test.ts new file mode 100644 index 00000000..54deed0d --- /dev/null +++ b/contract_manager/src/test.ts @@ -0,0 +1,48 @@ +import { + Vault, + Contracts, + Vaults, + loadHotWallet, + WormholeEmitter, +} from "./entities"; +import { SuiContract } from "./sui"; +import { CosmWasmContract } from "./cosmwasm"; +import { Ed25519Keypair, RawSigner } from "@mysten/sui.js"; +import { DefaultStore } from "./store"; +import { Chains } from "./chains"; + +async function test() { + // Deploy the same cosmwasm code with different config + + // let c = Contracts.osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3 as CosmWasmContract; + // let old_conf = await c.getConfig(); + // let config = CosmWasmContract.getDeploymentConfig(c.chain, 'edge', old_conf.config_v1.wormhole_contract); + // console.log(config); + // config.governance_source.emitter = wallet.publicKey.toBuffer().toString('base64'); + // let mnemonic = 'FILLME' + // console.log(await CosmWasmContract.deploy(c.chain, await c.getCodeId(), config, mnemonic)); + + let s = DefaultStore; + Object.values(Contracts).forEach((c) => { + console.log(c); + s.save(c); + }); + + Object.values(Chains).forEach((c) => { + console.log(c); + s.save(c); + }); + + // Execute some governance instruction on sui contract + + // let c = Contracts.sui_testnet_0x651dcb84d579fcdf51f15d79eb28f7e10b416c9202b6a156495bb1a4aecd55ea as SuiContract + // let wallet = await loadHotWallet('/tmp/priv.json'); + // let emitter = new WormholeEmitter("devnet", wallet); + // let proposal = c.setUpdateFee(200); + // let submittedWormholeMessage = await emitter.sendMessage(proposal); + // let vaa = await submittedWormholeMessage.fetchVAA(10); + // const keypair = Ed25519Keypair.fromSecretKey(Buffer.from('FILLME', "hex")); + // await c.executeGovernanceInstruction(vaa); +} + +test(); diff --git a/contract_manager/store/chains/CosmWasmChain/juno_testnet.json b/contract_manager/store/chains/CosmWasmChain/juno_testnet.json new file mode 100644 index 00000000..aaa30e8b --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/juno_testnet.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://rpc.uni.junonetwork.io/", + "executorEndpoint": "https://rpc.uni.junonetwork.io/", + "id": "juno_testnet", + "gasPrice": "0.025", + "prefix": "juno", + "feeDenom": "ujunox", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/CosmWasmChain/neutron.json b/contract_manager/store/chains/CosmWasmChain/neutron.json new file mode 100644 index 00000000..18bee3ab --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/neutron.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://rpc-kralum.neutron-1.neutron.org", + "executorEndpoint": "https://rpc-kralum.neutron-1.neutron.org", + "id": "neutron", + "gasPrice": "0.025", + "prefix": "neutron", + "feeDenom": "untrn", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json b/contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json new file mode 100644 index 00000000..dcb23ad3 --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://rpc.pion.rs-testnet.polypore.xyz/", + "executorEndpoint": "https://rpc.pion.rs-testnet.polypore.xyz/", + "id": "neutron_testnet_pion_1", + "gasPrice": "0.05", + "prefix": "neutron", + "feeDenom": "untrn", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json b/contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json new file mode 100644 index 00000000..36c165e1 --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://rpc.osmotest5.osmosis.zone/", + "executorEndpoint": "https://rpc.osmotest5.osmosis.zone/", + "id": "osmosis_testnet_5", + "gasPrice": "0.025", + "prefix": "osmo", + "feeDenom": "uosmo", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json b/contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json new file mode 100644 index 00000000..e591ed7f --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://sei-rpc.polkachu.com", + "executorEndpoint": "https://sei-rpc.polkachu.com", + "id": "sei_pacific_1", + "gasPrice": "0.025", + "prefix": "sei", + "feeDenom": "usei", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json b/contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json new file mode 100644 index 00000000..8d39d5ae --- /dev/null +++ b/contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json @@ -0,0 +1,9 @@ +{ + "querierEndpoint": "https://rpc.atlantic-2.seinetwork.io/", + "executorEndpoint": "https://rpc.atlantic-2.seinetwork.io/", + "id": "sei_testnet_atlantic_2", + "gasPrice": "0.01", + "prefix": "sei", + "feeDenom": "usei", + "type": "CosmWasmChain" +} diff --git a/contract_manager/store/chains/EVMChain/arbitrum_testnet.json b/contract_manager/store/chains/EVMChain/arbitrum_testnet.json new file mode 100644 index 00000000..7a6c3d74 --- /dev/null +++ b/contract_manager/store/chains/EVMChain/arbitrum_testnet.json @@ -0,0 +1,5 @@ +{ + "id": "arbitrum_testnet", + "rpcUrl": "https://goerli-rollup.arbitrum.io/rpc", + "type": "EVMChain" +} diff --git a/contract_manager/store/chains/EVMChain/cronos.json b/contract_manager/store/chains/EVMChain/cronos.json new file mode 100644 index 00000000..01b2833e --- /dev/null +++ b/contract_manager/store/chains/EVMChain/cronos.json @@ -0,0 +1,5 @@ +{ + "id": "cronos", + "rpcUrl": "https://cronosrpc-1.xstaking.sg", + "type": "EVMChain" +} diff --git a/contract_manager/store/chains/EVMChain/cronos_testnet.json b/contract_manager/store/chains/EVMChain/cronos_testnet.json new file mode 100644 index 00000000..e819dce3 --- /dev/null +++ b/contract_manager/store/chains/EVMChain/cronos_testnet.json @@ -0,0 +1,5 @@ +{ + "id": "cronos_testnet", + "rpcUrl": "https://evm-t3.cronos.org", + "type": "EVMChain" +} diff --git a/contract_manager/store/chains/SuiChain/sui_devnet.json b/contract_manager/store/chains/SuiChain/sui_devnet.json new file mode 100644 index 00000000..7f2fc76b --- /dev/null +++ b/contract_manager/store/chains/SuiChain/sui_devnet.json @@ -0,0 +1,5 @@ +{ + "id": "sui_devnet", + "rpcUrl": "https://fullnode.devnet.sui.io:443", + "type": "SuiChain" +} diff --git a/contract_manager/store/chains/SuiChain/sui_mainnet.json b/contract_manager/store/chains/SuiChain/sui_mainnet.json new file mode 100644 index 00000000..3f2fff56 --- /dev/null +++ b/contract_manager/store/chains/SuiChain/sui_mainnet.json @@ -0,0 +1,5 @@ +{ + "id": "sui_mainnet", + "rpcUrl": "https://fullnode.mainnet.sui.io:443", + "type": "SuiChain" +} diff --git a/contract_manager/store/chains/SuiChain/sui_testnet.json b/contract_manager/store/chains/SuiChain/sui_testnet.json new file mode 100644 index 00000000..143c94c8 --- /dev/null +++ b/contract_manager/store/chains/SuiChain/sui_testnet.json @@ -0,0 +1,5 @@ +{ + "id": "sui_testnet", + "rpcUrl": "https://fullnode.testnet.sui.io:443", + "type": "SuiChain" +} diff --git a/contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json b/contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json new file mode 100644 index 00000000..e96e67d8 --- /dev/null +++ b/contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json @@ -0,0 +1,5 @@ +{ + "chain": "juno_testnet", + "address": "juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs", + "type": "CosmWasmContract" +} diff --git a/contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json b/contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json new file mode 100644 index 00000000..bfe17980 --- /dev/null +++ b/contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json @@ -0,0 +1,5 @@ +{ + "chain": "neutron", + "address": "neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv", + "type": "CosmWasmContract" +} diff --git a/contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json b/contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json new file mode 100644 index 00000000..410b91ae --- /dev/null +++ b/contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json @@ -0,0 +1,5 @@ +{ + "chain": "neutron_testnet_pion_1", + "address": "neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp", + "type": "CosmWasmContract" +} diff --git a/contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json b/contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json new file mode 100644 index 00000000..5c455799 --- /dev/null +++ b/contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json @@ -0,0 +1,5 @@ +{ + "chain": "osmosis_testnet_5", + "address": "osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3", + "type": "CosmWasmContract" +} diff --git a/contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json b/contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json new file mode 100644 index 00000000..9c5039e2 --- /dev/null +++ b/contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json @@ -0,0 +1,5 @@ +{ + "chain": "sei_testnet_atlantic_2", + "address": "sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy", + "type": "CosmWasmContract" +} diff --git a/contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json b/contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json new file mode 100644 index 00000000..134b811d --- /dev/null +++ b/contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json @@ -0,0 +1,5 @@ +{ + "chain": "cronos", + "address": "0xe0d0e68297772dd5a1f1d99897c581e2082dba5b", + "type": "EVMContract" +} diff --git a/contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json b/contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json new file mode 100644 index 00000000..a2171ba2 --- /dev/null +++ b/contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json @@ -0,0 +1,5 @@ +{ + "chain": "cronos_testnet", + "address": "0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7", + "type": "EVMContract" +} diff --git a/contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json b/contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json new file mode 100644 index 00000000..41a80056 --- /dev/null +++ b/contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json @@ -0,0 +1,6 @@ +{ + "chain": "sui_mainnet", + "stateId": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f", + "wormholeStateId": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c", + "type": "SuiContract" +} diff --git a/contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json b/contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json new file mode 100644 index 00000000..b88f8882 --- /dev/null +++ b/contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json @@ -0,0 +1,6 @@ +{ + "chain": "sui_testnet", + "stateId": "0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3", + "wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02", + "type": "SuiContract" +} diff --git a/contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json b/contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json new file mode 100644 index 00000000..93788eb8 --- /dev/null +++ b/contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json @@ -0,0 +1,6 @@ +{ + "chain": "sui_testnet", + "stateId": "0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88", + "wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02", + "type": "SuiContract" +} diff --git a/contract_manager/tsconfig.json b/contract_manager/tsconfig.json new file mode 100644 index 00000000..0d1e0b87 --- /dev/null +++ b/contract_manager/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "exclude": ["node_modules", "**/__tests__/*"], + "compilerOptions": { + "rootDir": "src/", + "outDir": "./lib" + } +} diff --git a/governance/xc_governance_sdk_js/src/index.ts b/governance/xc_governance_sdk_js/src/index.ts index 57a01901..c5f43df1 100644 --- a/governance/xc_governance_sdk_js/src/index.ts +++ b/governance/xc_governance_sdk_js/src/index.ts @@ -1,6 +1,7 @@ export { DataSource, AptosAuthorizeUpgradeContractInstruction, + SuiAuthorizeUpgradeContractInstruction, EthereumUpgradeContractInstruction, EthereumSetWormholeAddress, HexString20Bytes, diff --git a/governance/xc_governance_sdk_js/src/instructions.ts b/governance/xc_governance_sdk_js/src/instructions.ts index fbac46ec..a2afe97c 100644 --- a/governance/xc_governance_sdk_js/src/instructions.ts +++ b/governance/xc_governance_sdk_js/src/instructions.ts @@ -111,6 +111,16 @@ export class AptosAuthorizeUpgradeContractInstruction extends TargetInstruction } } +export class SuiAuthorizeUpgradeContractInstruction extends TargetInstruction { + constructor(targetChainId: ChainId, private digest: HexString32Bytes) { + super(TargetAction.UpgradeContract, targetChainId); + } + + protected serializePayload(): Buffer { + return this.digest.serialize(); + } +} + export class EthereumUpgradeContractInstruction extends TargetInstruction { constructor(targetChainId: ChainId, private address: HexString20Bytes) { super(TargetAction.UpgradeContract, targetChainId); diff --git a/target_chains/cosmwasm/tools/package.json b/target_chains/cosmwasm/tools/package.json index 75a8fe28..f2aa8d7a 100644 --- a/target_chains/cosmwasm/tools/package.json +++ b/target_chains/cosmwasm/tools/package.json @@ -2,7 +2,7 @@ "name": "@pythnetwork/cosmwasm-deploy-tools", "version": "1.1.0", "description": "", - "main": "deploy-pyth-bridge.ts", + "main": "./src/index.ts", "private": "true", "scripts": { "build": "tsc", diff --git a/target_chains/cosmwasm/tools/src/chains-manager/index.ts b/target_chains/cosmwasm/tools/src/chains-manager/index.ts new file mode 100644 index 00000000..070d9c60 --- /dev/null +++ b/target_chains/cosmwasm/tools/src/chains-manager/index.ts @@ -0,0 +1,2 @@ +export * from "./cosmwasm"; +export * from "./injective"; diff --git a/target_chains/cosmwasm/tools/src/index.ts b/target_chains/cosmwasm/tools/src/index.ts new file mode 100644 index 00000000..4d6a8e4a --- /dev/null +++ b/target_chains/cosmwasm/tools/src/index.ts @@ -0,0 +1,3 @@ +export * from "./deployer"; +export * from "./chains-manager"; +export * from "./pyth-wrapper"; diff --git a/target_chains/ethereum/contracts/networks/338.json b/target_chains/ethereum/contracts/networks/338.json deleted file mode 100644 index b9765a64..00000000 --- a/target_chains/ethereum/contracts/networks/338.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "contractName": "Migrations", - "address": "0x1c6Cd107fB71768FBc46F8B6180Eec155C03eEb5" - }, - { - "contractName": "WormholeReceiver", - "address": "0x15D35b8985e350f783fe3d95401401E194ff1E6f", - "transactionHash": "0x1c33d9b6971f7337e0e2ea390affe18fe90709dcb803712f6d8bb4a008705fb7" - }, - { - "contractName": "PythUpgradable", - "address": "0xBAEA4A1A2Eaa4E9bb78f2303C213Da152933170E", - "transactionHash": "0x507d747b3c978794cc880a201c009d37367f66925b930c0cebc30c493c9d31eb" - } -]