Răsfoiți Sursa

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
Mohammad Amin Khashkhashi Moghaddam 2 ani în urmă
părinte
comite
66e5f186b2
39 a modificat fișierele cu 1645 adăugiri și 17 ștergeri
  1. 28 0
      contract_manager/package.json
  2. 37 0
      contract_manager/src/base.ts
  3. 107 0
      contract_manager/src/chains.ts
  4. 344 0
      contract_manager/src/cosmwasm.ts
  5. 273 0
      contract_manager/src/entities.ts
  6. 134 0
      contract_manager/src/evm.ts
  7. 25 0
      contract_manager/src/shell.ts
  8. 105 0
      contract_manager/src/store.ts
  9. 381 0
      contract_manager/src/sui.ts
  10. 48 0
      contract_manager/src/test.ts
  11. 9 0
      contract_manager/store/chains/CosmWasmChain/juno_testnet.json
  12. 9 0
      contract_manager/store/chains/CosmWasmChain/neutron.json
  13. 9 0
      contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json
  14. 9 0
      contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json
  15. 9 0
      contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json
  16. 9 0
      contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json
  17. 5 0
      contract_manager/store/chains/EVMChain/arbitrum_testnet.json
  18. 5 0
      contract_manager/store/chains/EVMChain/cronos.json
  19. 5 0
      contract_manager/store/chains/EVMChain/cronos_testnet.json
  20. 5 0
      contract_manager/store/chains/SuiChain/sui_devnet.json
  21. 5 0
      contract_manager/store/chains/SuiChain/sui_mainnet.json
  22. 5 0
      contract_manager/store/chains/SuiChain/sui_testnet.json
  23. 5 0
      contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json
  24. 5 0
      contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json
  25. 5 0
      contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json
  26. 5 0
      contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json
  27. 5 0
      contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json
  28. 5 0
      contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json
  29. 5 0
      contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json
  30. 6 0
      contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json
  31. 6 0
      contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json
  32. 6 0
      contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json
  33. 9 0
      contract_manager/tsconfig.json
  34. 1 0
      governance/xc_governance_sdk_js/src/index.ts
  35. 10 0
      governance/xc_governance_sdk_js/src/instructions.ts
  36. 1 1
      target_chains/cosmwasm/tools/package.json
  37. 2 0
      target_chains/cosmwasm/tools/src/chains-manager/index.ts
  38. 3 0
      target_chains/cosmwasm/tools/src/index.ts
  39. 0 16
      target_chains/ethereum/contracts/networks/338.json

+ 28 - 0
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"
+  }
+}

+ 37 - 0
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<number>;
+
+  /**
+   * Returns an array of data sources that this contract accepts price feed messages from
+   */
+  abstract getDataSources(): Promise<DataSource[]>;
+
+  /**
+   * Returns the single data source that this contract accepts governance messages from
+   */
+  abstract getGovernanceDataSource(): Promise<DataSource>;
+}

+ 107 - 0
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<string, Chain> = {};

+ 344 - 0
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<DataSource[]> {
+    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<DataSource> {
+    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<CosmWasmContract> {
+    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<CosmWasmContract> {
+    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<PythWrapperQuerier> {
+    const chainQuerier = await CosmwasmQuerier.connect(
+      this.chain.querierEndpoint
+    );
+    const pythQuerier = new PythWrapperQuerier(chainQuerier);
+    return pythQuerier;
+  }
+
+  async getCodeId(): Promise<number> {
+    let result = await this.getWasmContractInfo();
+    return result.codeId;
+  }
+
+  async getWasmContractInfo(): Promise<ContractInfoResponse> {
+    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<any> {
+    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<string> {
+    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<any> {
+    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);
+  }
+}

+ 273 - 0
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<string, Contract> = {};
+
+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<Buffer> {
+    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<TransactionInstruction> {
+    const squad = this.getSquadOrThrow();
+    return await squad.buildActivateTransaction(this.key, proposalAddress);
+  }
+
+  public async approveProposalIx(
+    proposalAddress: PublicKey
+  ): Promise<TransactionInstruction> {
+    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<any> {
+    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<string, Vault> = {};
+
+export async function loadHotWallet(wallet: string): Promise<Wallet> {
+  return new NodeWallet(
+    Keypair.fromSecretKey(
+      Uint8Array.from(JSON.parse(readFileSync(wallet, "ascii")))
+    )
+  );
+}

+ 134 - 0
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<DataSource[]> {
+    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<DataSource> {
+    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,
+    };
+  }
+}

+ 25 - 0
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;

+ 105 - 0
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<string, Chain> = {};
+  static Contracts: Record<string, CosmWasmContract | SuiContract> = {};
+
+  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`);

+ 381 - 0
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<ObjectId> {
+    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<ObjectId> {
+    return await this.getPackageId(this.stateId);
+  }
+
+  async getWormholePackageId(): Promise<ObjectId> {
+    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<ObjectId> {
+    const provider = this.getProvider();
+    let result = await provider.getDynamicFieldObject({
+      parentId: this.stateId,
+      name: {
+        type: "vector<u8>",
+        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<DataSource[]> {
+    const provider = this.getProvider();
+    let result = await provider.getDynamicFieldObject({
+      parentId: this.stateId,
+      name: {
+        type: "vector<u8>",
+        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<DataSource> {
+    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;
+  }
+}

+ 48 - 0
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();

+ 9 - 0
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"
+}

+ 9 - 0
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"
+}

+ 9 - 0
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"
+}

+ 9 - 0
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"
+}

+ 9 - 0
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"
+}

+ 9 - 0
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"
+}

+ 5 - 0
contract_manager/store/chains/EVMChain/arbitrum_testnet.json

@@ -0,0 +1,5 @@
+{
+  "id": "arbitrum_testnet",
+  "rpcUrl": "https://goerli-rollup.arbitrum.io/rpc",
+  "type": "EVMChain"
+}

+ 5 - 0
contract_manager/store/chains/EVMChain/cronos.json

@@ -0,0 +1,5 @@
+{
+  "id": "cronos",
+  "rpcUrl": "https://cronosrpc-1.xstaking.sg",
+  "type": "EVMChain"
+}

+ 5 - 0
contract_manager/store/chains/EVMChain/cronos_testnet.json

@@ -0,0 +1,5 @@
+{
+  "id": "cronos_testnet",
+  "rpcUrl": "https://evm-t3.cronos.org",
+  "type": "EVMChain"
+}

+ 5 - 0
contract_manager/store/chains/SuiChain/sui_devnet.json

@@ -0,0 +1,5 @@
+{
+  "id": "sui_devnet",
+  "rpcUrl": "https://fullnode.devnet.sui.io:443",
+  "type": "SuiChain"
+}

+ 5 - 0
contract_manager/store/chains/SuiChain/sui_mainnet.json

@@ -0,0 +1,5 @@
+{
+  "id": "sui_mainnet",
+  "rpcUrl": "https://fullnode.mainnet.sui.io:443",
+  "type": "SuiChain"
+}

+ 5 - 0
contract_manager/store/chains/SuiChain/sui_testnet.json

@@ -0,0 +1,5 @@
+{
+  "id": "sui_testnet",
+  "rpcUrl": "https://fullnode.testnet.sui.io:443",
+  "type": "SuiChain"
+}

+ 5 - 0
contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json

@@ -0,0 +1,5 @@
+{
+  "chain": "juno_testnet",
+  "address": "juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs",
+  "type": "CosmWasmContract"
+}

+ 5 - 0
contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json

@@ -0,0 +1,5 @@
+{
+  "chain": "neutron",
+  "address": "neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv",
+  "type": "CosmWasmContract"
+}

+ 5 - 0
contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json

@@ -0,0 +1,5 @@
+{
+  "chain": "neutron_testnet_pion_1",
+  "address": "neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp",
+  "type": "CosmWasmContract"
+}

+ 5 - 0
contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json

@@ -0,0 +1,5 @@
+{
+  "chain": "osmosis_testnet_5",
+  "address": "osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3",
+  "type": "CosmWasmContract"
+}

+ 5 - 0
contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json

@@ -0,0 +1,5 @@
+{
+  "chain": "sei_testnet_atlantic_2",
+  "address": "sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy",
+  "type": "CosmWasmContract"
+}

+ 5 - 0
contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json

@@ -0,0 +1,5 @@
+{
+  "chain": "cronos",
+  "address": "0xe0d0e68297772dd5a1f1d99897c581e2082dba5b",
+  "type": "EVMContract"
+}

+ 5 - 0
contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json

@@ -0,0 +1,5 @@
+{
+  "chain": "cronos_testnet",
+  "address": "0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7",
+  "type": "EVMContract"
+}

+ 6 - 0
contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json

@@ -0,0 +1,6 @@
+{
+  "chain": "sui_mainnet",
+  "stateId": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f",
+  "wormholeStateId": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
+  "type": "SuiContract"
+}

+ 6 - 0
contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json

@@ -0,0 +1,6 @@
+{
+  "chain": "sui_testnet",
+  "stateId": "0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3",
+  "wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
+  "type": "SuiContract"
+}

+ 6 - 0
contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json

@@ -0,0 +1,6 @@
+{
+  "chain": "sui_testnet",
+  "stateId": "0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88",
+  "wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
+  "type": "SuiContract"
+}

+ 9 - 0
contract_manager/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../tsconfig.base.json",
+  "include": ["src"],
+  "exclude": ["node_modules", "**/__tests__/*"],
+  "compilerOptions": {
+    "rootDir": "src/",
+    "outDir": "./lib"
+  }
+}

+ 1 - 0
governance/xc_governance_sdk_js/src/index.ts

@@ -1,6 +1,7 @@
 export {
   DataSource,
   AptosAuthorizeUpgradeContractInstruction,
+  SuiAuthorizeUpgradeContractInstruction,
   EthereumUpgradeContractInstruction,
   EthereumSetWormholeAddress,
   HexString20Bytes,

+ 10 - 0
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);

+ 1 - 1
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",

+ 2 - 0
target_chains/cosmwasm/tools/src/chains-manager/index.ts

@@ -0,0 +1,2 @@
+export * from "./cosmwasm";
+export * from "./injective";

+ 3 - 0
target_chains/cosmwasm/tools/src/index.ts

@@ -0,0 +1,3 @@
+export * from "./deployer";
+export * from "./chains-manager";
+export * from "./pyth-wrapper";

+ 0 - 16
target_chains/ethereum/contracts/networks/338.json

@@ -1,16 +0,0 @@
-[
-  {
-    "contractName": "Migrations",
-    "address": "0x1c6Cd107fB71768FBc46F8B6180Eec155C03eEb5"
-  },
-  {
-    "contractName": "WormholeReceiver",
-    "address": "0x15D35b8985e350f783fe3d95401401E194ff1E6f",
-    "transactionHash": "0x1c33d9b6971f7337e0e2ea390affe18fe90709dcb803712f6d8bb4a008705fb7"
-  },
-  {
-    "contractName": "PythUpgradable",
-    "address": "0xBAEA4A1A2Eaa4E9bb78f2303C213Da152933170E",
-    "transactionHash": "0x507d747b3c978794cc880a201c009d37367f66925b930c0cebc30c493c9d31eb"
-  }
-]