Преглед изворни кода

Contract manager upgrades (#952)

* Reuse xc_governance logic as much as possible in contract manager

Some functions in xc_governance were moved and refactored in order to become usable in
contract manager

* Add getBaseUpdateFee function for contract manager

* Add method for executeGovernanceInstructions

* Move up SetFee method to base class

* Add governance upgrade instruction

* Move governance payload generators out of contract classes into chain classes

* Switch from json to yaml for storage

* Remove test script for ci

* Add minimal aptos implementation

* Remove global Chains and Contracts variable and put them in DefaultStore

* Move aptos getClient function to Chain class

* Make denom field in baseUpdateFee optional and remove it from non-cosmwasm chains

* More documentation and minor fixes

* Add vaults storage

Although the set of vaults used in testing/production is just 2 it's a good idea to
not set them as predefined constants. So that for development purposes, we can create
new vaults and test them on the fly without changing too many places.
Mohammad Amin Khashkhashi Moghaddam пре 2 година
родитељ
комит
6c52eb6606
67 измењених фајлова са 1996 додато и 534 уклоњено
  1. 48 0
      contract_manager/examples/deploy_cosmwasm.ts
  2. 2 2
      contract_manager/package.json
  3. 103 0
      contract_manager/src/aptos.ts
  4. 25 1
      contract_manager/src/base.ts
  5. 122 2
      contract_manager/src/chains.ts
  6. 14 18
      contract_manager/src/cosmwasm.ts
  7. 141 104
      contract_manager/src/entities.ts
  8. 111 62
      contract_manager/src/evm.ts
  9. 4 2
      contract_manager/src/shell.ts
  10. 48 22
      contract_manager/src/store.ts
  11. 20 57
      contract_manager/src/sui.ts
  12. 3 0
      contract_manager/src/test.ts
  13. 3 0
      contract_manager/store/chains/AptosChain/aptos_testnet.yaml
  14. 0 9
      contract_manager/store/chains/CosmWasmChain/juno_testnet.json
  15. 7 0
      contract_manager/store/chains/CosmWasmChain/juno_testnet.yaml
  16. 0 9
      contract_manager/store/chains/CosmWasmChain/neutron.json
  17. 7 0
      contract_manager/store/chains/CosmWasmChain/neutron.yaml
  18. 0 9
      contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json
  19. 7 0
      contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.yaml
  20. 0 9
      contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json
  21. 7 0
      contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.yaml
  22. 0 9
      contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json
  23. 7 0
      contract_manager/store/chains/CosmWasmChain/sei_pacific_1.yaml
  24. 0 9
      contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json
  25. 7 0
      contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.yaml
  26. 0 5
      contract_manager/store/chains/EVMChain/arbitrum_testnet.json
  27. 3 0
      contract_manager/store/chains/EVMChain/arbitrum_testnet.yaml
  28. 0 5
      contract_manager/store/chains/EVMChain/cronos.json
  29. 3 0
      contract_manager/store/chains/EVMChain/cronos.yaml
  30. 0 5
      contract_manager/store/chains/EVMChain/cronos_testnet.json
  31. 3 0
      contract_manager/store/chains/EVMChain/cronos_testnet.yaml
  32. 0 5
      contract_manager/store/chains/SuiChain/sui_devnet.json
  33. 3 0
      contract_manager/store/chains/SuiChain/sui_devnet.yaml
  34. 0 5
      contract_manager/store/chains/SuiChain/sui_mainnet.json
  35. 3 0
      contract_manager/store/chains/SuiChain/sui_mainnet.yaml
  36. 0 5
      contract_manager/store/chains/SuiChain/sui_testnet.json
  37. 3 0
      contract_manager/store/chains/SuiChain/sui_testnet.yaml
  38. 4 0
      contract_manager/store/contracts/AptosContract/aptos_testnet_0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387.yaml
  39. 0 5
      contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.json
  40. 3 0
      contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.yaml
  41. 0 5
      contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.json
  42. 3 0
      contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.yaml
  43. 0 5
      contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.json
  44. 3 0
      contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.yaml
  45. 0 5
      contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.json
  46. 3 0
      contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.yaml
  47. 3 0
      contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1q3pzdelxnh2yk66vux4s6ewrw59sx0uu2q6xd7navlsvc3vry92sk3pe7g.yaml
  48. 0 5
      contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.json
  49. 3 0
      contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.yaml
  50. 0 5
      contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.json
  51. 3 0
      contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.yaml
  52. 0 5
      contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.json
  53. 3 0
      contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.yaml
  54. 0 6
      contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.json
  55. 4 0
      contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.yaml
  56. 0 6
      contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.json
  57. 4 0
      contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.yaml
  58. 0 6
      contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.json
  59. 4 0
      contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.yaml
  60. 3 0
      contract_manager/store/vaults/devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3.yaml
  61. 3 0
      contract_manager/store/vaults/mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj.yaml
  62. 4 116
      governance/xc_admin/packages/crank_executor/src/index.ts
  63. 149 0
      governance/xc_admin/packages/xc_admin_common/src/executor.ts
  64. 1 0
      governance/xc_admin/packages/xc_admin_common/src/index.ts
  65. 23 5
      governance/xc_admin/packages/xc_admin_common/src/propose.ts
  66. 1067 5
      package-lock.json
  67. 2 1
      package.json

+ 48 - 0
contract_manager/examples/deploy_cosmwasm.ts

@@ -0,0 +1,48 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import { CosmWasmChain } from "../src/chains";
+import { CosmWasmContract } from "../src/cosmwasm";
+import { DefaultStore } from "../src/store";
+
+const parser = yargs(hideBin(process.argv))
+  .scriptName("deploy_cosmwasm.ts")
+  .usage(
+    "Usage: $0 --code <path/to/artifact.wasm> --mnemonic <mnemonic> --chain <chain>"
+  )
+  .options({
+    code: {
+      type: "string",
+      demandOption: true,
+      desc: "Path to the artifact .wasm file",
+    },
+    mnemonic: {
+      type: "string",
+      demandOption: true,
+      desc: "Mnemonic to use for the deployment",
+    },
+    chain: {
+      type: "string",
+      demandOption: true,
+      desc: "Chain to upload the code on. Can be one of the chains available in the store",
+    },
+    wormholeContract: {
+      type: "string",
+      demandOption: true,
+      desc: "Wormhole contract address deployed on this chain",
+    },
+  });
+
+async function main() {
+  const argv = await parser.argv;
+  const { code, wormholeContract } = argv;
+  console.log(
+    await CosmWasmContract.deploy(
+      DefaultStore.chains[argv.chain] as CosmWasmChain,
+      wormholeContract,
+      argv.mnemonic,
+      code
+    )
+  );
+}
+
+main();

+ 2 - 2
contract_manager/package.json

@@ -5,7 +5,6 @@
   "private": true,
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
     "shell": "ts-node ./src/shell.ts"
   },
   "author": "",
@@ -15,10 +14,11 @@
     "url": "git+https://github.com/pyth-network/pyth-crosschain.git"
   },
   "dependencies": {
+    "@certusone/wormhole-sdk": "^0.9.8",
     "@pythnetwork/cosmwasm-deploy-tools": "*",
     "@pythnetwork/price-service-client": "*",
     "@pythnetwork/xc-governance-sdk": "*",
-    "@certusone/wormhole-sdk": "^0.9.8",
+    "bs58": "^5.0.0",
     "ts-node": "^10.9.1",
     "typescript": "^4.9.3"
   },

+ 103 - 0
contract_manager/src/aptos.ts

@@ -0,0 +1,103 @@
+import { Contract } from "./base";
+import { AptosChain, Chain } from "./chains";
+import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
+import { AptosClient } from "aptos";
+
+export class AptosContract extends Contract {
+  static type: string = "AptosContract";
+
+  /**
+   * Given the ids of the pyth state and wormhole state, create a new AptosContract
+   * 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: AptosChain,
+    public stateId: string,
+    public wormholeStateId: string
+  ) {
+    super();
+  }
+
+  static fromJson(chain: Chain, parsed: any): AptosContract {
+    if (parsed.type !== AptosContract.type) throw new Error("Invalid type");
+    if (!(chain instanceof AptosChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new AptosContract(chain, parsed.stateId, parsed.wormholeStateId);
+  }
+
+  executeGovernanceInstruction(sender: any, vaa: Buffer): Promise<any> {
+    throw new Error("Method not implemented.");
+  }
+
+  getStateResources() {
+    const client = this.chain.getClient();
+    return client.getAccountResources(this.stateId);
+  }
+
+  /**
+   * Returns the first occurrence of a resource with the given type in the pyth package state
+   * @param type
+   */
+  async findResource(type: string) {
+    const resources = await this.getStateResources();
+    for (const resource of resources) {
+      if (resource.type === `${this.stateId}::state::${type}`) {
+        return resource.data;
+      }
+    }
+    throw new Error(`${type} resource not found in state ${this.stateId}`);
+  }
+
+  async getBaseUpdateFee() {
+    const data = (await this.findResource("BaseUpdateFee")) as any;
+    return { amount: data.fee };
+  }
+
+  getChain(): AptosChain {
+    return this.chain;
+  }
+
+  async getDataSources(): Promise<DataSource[]> {
+    const data = (await this.findResource("DataSources")) as any;
+    return data.sources.keys.map((source: any) => {
+      return new DataSource(
+        Number(source.emitter_chain),
+        new HexString32Bytes(source.emitter_address.external_address)
+      );
+    });
+  }
+
+  async getGovernanceDataSource(): Promise<DataSource> {
+    const data = (await this.findResource("GovernanceDataSource")) as any;
+    return new DataSource(
+      Number(data.source.emitter_chain),
+      new HexString32Bytes(data.source.emitter_address.external_address)
+    );
+  }
+
+  getId(): string {
+    return `${this.chain.getId()}_${this.stateId}`;
+  }
+
+  getType(): string {
+    return AptosContract.type;
+  }
+
+  async getValidTimePeriod() {
+    const data = (await this.findResource("StalePriceThreshold")) as any;
+    return Number(data.threshold_secs);
+  }
+
+  toJson() {
+    return {
+      chain: this.chain.id,
+      stateId: this.stateId,
+      wormholeStateId: this.wormholeStateId,
+      type: AptosContract.type,
+    };
+  }
+}

+ 25 - 1
contract_manager/src/base.ts

@@ -1,4 +1,10 @@
-import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
+import {
+  CHAINS,
+  DataSource,
+  HexString32Bytes,
+  SetFeeInstruction,
+} from "@pythnetwork/xc-governance-sdk";
+import { Chain } from "./chains";
 
 export abstract class Storable {
   /**
@@ -25,11 +31,29 @@ export abstract class Contract extends Storable {
    */
   abstract getValidTimePeriod(): Promise<number>;
 
+  /**
+   * Returns the chain that this contract is deployed on
+   */
+  abstract getChain(): Chain;
+
   /**
    * Returns an array of data sources that this contract accepts price feed messages from
    */
   abstract getDataSources(): Promise<DataSource[]>;
 
+  /**
+   * Returns the base update fee for this contract
+   * This is the required fee for updating the price feeds in the contract
+   */
+  abstract getBaseUpdateFee(): Promise<{ amount: string; denom?: string }>;
+
+  /**
+   * Executes the governance instruction contained in the VAA using the sender credentials
+   * @param sender based on the contract type, this can be a private key, a mnemonic, a wallet, etc.
+   * @param vaa the VAA to execute
+   */
+  abstract executeGovernanceInstruction(sender: any, vaa: Buffer): Promise<any>;
+
   /**
    * Returns the single data source that this contract accepts governance messages from
    */

+ 122 - 2
contract_manager/src/chains.ts

@@ -1,5 +1,16 @@
 import { readdirSync, readFileSync, writeFileSync } from "fs";
 import { Storable } from "./base";
+import {
+  CHAINS,
+  CosmwasmUpgradeContractInstruction,
+  EthereumUpgradeContractInstruction,
+  HexString20Bytes,
+  HexString32Bytes,
+  SetFeeInstruction,
+  SuiAuthorizeUpgradeContractInstruction,
+} from "@pythnetwork/xc-governance-sdk";
+import { BufferBuilder } from "@pythnetwork/xc-governance-sdk/lib/serialize";
+import { AptosClient } from "aptos";
 
 export abstract class Chain extends Storable {
   protected constructor(public id: string) {
@@ -9,6 +20,25 @@ export abstract class Chain extends Storable {
   getId(): string {
     return this.id;
   }
+
+  /**
+   * Returns the payload for a governance SetFee instruction for contracts deployed on this chain
+   * @param fee the new fee to set
+   * @param exponent the new fee exponent to set
+   */
+  generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
+    return new SetFeeInstruction(
+      CHAINS[this.getId() as keyof typeof CHAINS],
+      BigInt(fee),
+      BigInt(exponent)
+    ).serialize();
+  }
+
+  /**
+   * Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
+   * @param upgradeInfo based on the contract type, this can be a contract address, codeId, package digest, etc.
+   */
+  abstract generateGovernanceUpgradePayload(upgradeInfo: any): Buffer;
 }
 
 export class CosmWasmChain extends Chain {
@@ -52,6 +82,13 @@ export class CosmWasmChain extends Chain {
   getType(): string {
     return CosmWasmChain.type;
   }
+
+  generateGovernanceUpgradePayload(codeId: bigint): Buffer {
+    return new CosmwasmUpgradeContractInstruction(
+      CHAINS[this.getId() as keyof typeof CHAINS],
+      codeId
+    ).serialize();
+  }
 }
 
 export class SuiChain extends Chain {
@@ -77,6 +114,40 @@ export class SuiChain extends Chain {
   getType(): string {
     return SuiChain.type;
   }
+
+  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();
+  }
+
+  generateGovernanceUpgradePayload(digest: string): Buffer {
+    let setFee = new SuiAuthorizeUpgradeContractInstruction(
+      CHAINS["sui"],
+      new HexString32Bytes(digest)
+    ).serialize();
+    return this.wrapWithWormholeGovernancePayload(0, setFee);
+  }
+
+  generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
+    let setFee = new SetFeeInstruction(
+      CHAINS["sui"],
+      BigInt(fee),
+      BigInt(exponent)
+    ).serialize();
+    return this.wrapWithWormholeGovernancePayload(3, setFee);
+  }
 }
 
 export class EVMChain extends Chain {
@@ -86,11 +157,18 @@ export class EVMChain extends Chain {
     super(id);
   }
 
-  static fromJson(parsed: any): SuiChain {
+  static fromJson(parsed: any): EVMChain {
     if (parsed.type !== EVMChain.type) throw new Error("Invalid type");
     return new EVMChain(parsed.id, parsed.rpcUrl);
   }
 
+  generateGovernanceUpgradePayload(address: HexString20Bytes): Buffer {
+    return new EthereumUpgradeContractInstruction(
+      CHAINS[this.getId() as keyof typeof CHAINS],
+      address
+    ).serialize();
+  }
+
   toJson(): any {
     return {
       id: this.id,
@@ -104,4 +182,46 @@ export class EVMChain extends Chain {
   }
 }
 
-export const Chains: Record<string, Chain> = {};
+export class AptosChain extends Chain {
+  static type = "AptosChain";
+
+  constructor(id: string, public rpcUrl: string) {
+    super(id);
+  }
+
+  getClient(): AptosClient {
+    return new AptosClient(this.rpcUrl);
+  }
+
+  generateGovernanceUpgradePayload(digest: string): Buffer {
+    return new SuiAuthorizeUpgradeContractInstruction(
+      CHAINS["aptos"],
+      new HexString32Bytes(digest)
+    ).serialize();
+  }
+
+  generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
+    return new SetFeeInstruction(
+      CHAINS["aptos"], // should always be aptos (22) no matter devnet or testnet or mainnet
+      BigInt(fee),
+      BigInt(exponent)
+    ).serialize();
+  }
+
+  getType(): string {
+    return AptosChain.type;
+  }
+
+  toJson(): any {
+    return {
+      id: this.id,
+      rpcUrl: this.rpcUrl,
+      type: AptosChain.type,
+    };
+  }
+
+  static fromJson(parsed: any): AptosChain {
+    if (parsed.type !== AptosChain.type) throw new Error("Invalid type");
+    return new AptosChain(parsed.id, parsed.rpcUrl);
+  }
+}

+ 14 - 18
contract_manager/src/cosmwasm.ts

@@ -1,11 +1,10 @@
-import { Chains, CosmWasmChain } from "./chains";
+import { Chain, 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 {
@@ -71,14 +70,11 @@ export class CosmWasmContract extends Contract {
     super();
   }
 
-  static fromJson(parsed: any): CosmWasmContract {
+  static fromJson(chain: Chain, 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
-    );
+    if (!(chain instanceof CosmWasmChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new CosmWasmContract(chain, parsed.address);
   }
 
   getType(): string {
@@ -220,7 +216,6 @@ export class CosmWasmContract extends Contract {
     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
 
@@ -306,7 +301,7 @@ export class CosmWasmContract extends Contract {
     });
   }
 
-  async executeGovernanceInstruction(mnemonic: string, vaa: string) {
+  async executeGovernanceInstruction(mnemonic: string, vaa: Buffer) {
     let executor = new CosmwasmExecutor(
       this.chain.executorEndpoint,
       mnemonic,
@@ -316,7 +311,7 @@ export class CosmWasmContract extends Contract {
     let pythExecutor = new PythWrapperExecutor(executor);
     return pythExecutor.executeGovernanceInstruction({
       contractAddr: this.address,
-      vaa,
+      vaa: vaa.toString("base64"),
     });
   }
 
@@ -325,12 +320,13 @@ export class CosmWasmContract extends Contract {
     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 getBaseUpdateFee(): Promise<any> {
+    const config = await this.getConfig();
+    return config.config_v1.fee;
+  }
+
+  getChain(): CosmWasmChain {
+    return this.chain;
   }
 
   async getValidTimePeriod() {

+ 141 - 104
contract_manager/src/entities.ts

@@ -3,29 +3,42 @@ import { readFileSync } from "fs";
 import {
   Connection,
   Keypair,
+  ParsedInstruction,
+  PartiallyDecodedInstruction,
   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 * as bs58 from "bs58";
+import {
+  getPythClusterApiUrl,
+  PythCluster,
+} from "@pythnetwork/client/lib/cluster";
+import SquadsMesh 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 {
+  executeProposal,
+  MultisigVault,
+  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";
+import { Storable } from "./base";
 
-export const Contracts: Record<string, Contract> = {};
+class InvalidTransactionError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "InvalidTransactionError";
+  }
+}
 
 export class SubmittedWormholeMessage {
   constructor(
@@ -34,6 +47,57 @@ export class SubmittedWormholeMessage {
     public cluster: string
   ) {}
 
+  /**
+   * Attempts to find the emitter and sequence number of a wormhole message from a transaction
+   * Parses the transaction and looks for the wormhole postMessage instruction to find the emitter
+   * Inspects the transaction logs to find the sequence number
+   * @param signature signature of the transaction to inspect
+   * @param cluster the cluster the transaction was submitted to
+   */
+  static async fromTransactionSignature(
+    signature: string,
+    cluster: PythCluster
+  ): Promise<SubmittedWormholeMessage> {
+    const connection = new Connection(
+      getPythClusterApiUrl(cluster),
+      "confirmed"
+    );
+
+    const txDetails = await connection.getParsedTransaction(signature);
+    const sequenceLogPrefix = "Sequence: ";
+    const txLog = txDetails?.meta?.logMessages?.find((s) =>
+      s.includes(sequenceLogPrefix)
+    );
+
+    const sequenceNumber = Number(
+      txLog?.substring(
+        txLog.indexOf(sequenceLogPrefix) + sequenceLogPrefix.length
+      )
+    );
+
+    const wormholeAddress =
+      WORMHOLE_ADDRESS[cluster as keyof typeof WORMHOLE_ADDRESS]!;
+    let emitter: PublicKey | undefined = undefined;
+
+    let allInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[] =
+      txDetails?.transaction?.message?.instructions || [];
+    txDetails?.meta?.innerInstructions?.forEach((instruction) => {
+      allInstructions = allInstructions.concat(instruction.instructions);
+    });
+    allInstructions.forEach((instruction) => {
+      if (!instruction.programId.equals(wormholeAddress)) return;
+      // we assume RPC can not parse wormhole instructions and the type is not ParsedInstruction
+      const wormholeInstruction = instruction as PartiallyDecodedInstruction;
+      if (bs58.decode(wormholeInstruction.data)[0] !== 1) return; // 1 is wormhole postMessage Instruction discriminator
+      emitter = wormholeInstruction.accounts[2];
+    });
+    if (!emitter)
+      throw new InvalidTransactionError(
+        "Could not find wormhole postMessage instruction"
+      );
+    return new SubmittedWormholeMessage(emitter, sequenceNumber, cluster);
+  }
+
   /**
    * Tries to fetch the VAA from the wormhole bridge API waiting for a certain amount of time
    * before giving up and throwing an error
@@ -61,6 +125,12 @@ export class SubmittedWormholeMessage {
   }
 }
 
+function asPythCluster(cluster: string): PythCluster {
+  const pythCluster = cluster as PythCluster;
+  getPythClusterApiUrl(pythCluster); // throws if cluster is invalid
+  return pythCluster;
+}
+
 /**
  * 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
@@ -68,17 +138,17 @@ export class SubmittedWormholeMessage {
  * You need to set your pyth contract data source / governance source address to this emitter
  */
 export class WormholeEmitter {
-  cluster: string;
+  cluster: PythCluster;
   wallet: Wallet;
 
   constructor(cluster: string, wallet: Wallet) {
-    this.cluster = cluster;
     this.wallet = wallet;
+    this.cluster = asPythCluster(cluster);
   }
 
   async sendMessage(payload: Buffer) {
     const provider = new AnchorProvider(
-      new Connection(getPythClusterApiUrl(this.cluster as any), "confirmed"),
+      new Connection(getPythClusterApiUrl(this.cluster), "confirmed"),
       this.wallet,
       {
         commitment: "confirmed",
@@ -119,19 +189,44 @@ export class WormholeEmitter {
         .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 signature = await provider.sendAndConfirm(transaction, [kp]);
+    return SubmittedWormholeMessage.fromTransactionSignature(
+      signature,
+      this.cluster
     );
+  }
+}
 
-    const sequenceNumber = Number(
-      txLog?.substring(
-        txLog.indexOf(sequenceLogPrefix) + sequenceLogPrefix.length
-      )
+export class WormholeMultiSigTransaction {
+  constructor(
+    public address: PublicKey,
+    public squad: SquadsMesh,
+    public cluster: PythCluster
+  ) {}
+
+  async getState() {
+    const proposal = await this.squad.getTransaction(this.address);
+    return proposal.status; //TODO: make it typed and parse the status to a more readable format
+  }
+
+  async execute(): Promise<SubmittedWormholeMessage> {
+    const proposal = await this.squad.getTransaction(this.address);
+    const signatures = await executeProposal(
+      proposal,
+      this.squad,
+      this.cluster
     );
-    return new SubmittedWormholeMessage(emitter, sequenceNumber, this.cluster);
+    for (const signature of signatures) {
+      try {
+        return SubmittedWormholeMessage.fromTransactionSignature(
+          signature,
+          this.cluster
+        );
+      } catch (e: any) {
+        if (!(e instanceof InvalidTransactionError)) throw e;
+      }
+    }
+    throw new Error("No transactions with wormhole messages found");
   }
 }
 
@@ -139,20 +234,19 @@ export class Vault extends Storable {
   static type: string = "vault";
   key: PublicKey;
   squad?: SquadsMesh;
-  cluster: string;
+  cluster: PythCluster;
 
   constructor(key: string, cluster: string) {
     super();
     this.key = new PublicKey(key);
-    this.cluster = cluster;
+    this.cluster = asPythCluster(cluster);
   }
 
   getType(): string {
     return Vault.type;
   }
 
-  static from(path: string): Vault {
-    let parsed = JSON.parse(readFileSync(path, "utf-8"));
+  static fromJson(parsed: any): Vault {
     if (parsed.type !== Vault.type) throw new Error("Invalid type");
     return new Vault(parsed.key, parsed.cluster);
   }
@@ -171,99 +265,42 @@ export class Vault extends Storable {
 
   public connect(wallet: Wallet): void {
     this.squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(this.cluster as any), // TODO Fix any
+      getPythClusterApiUrl(this.cluster),
       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
+  public async getEmitter() {
+    const squad = SquadsMesh.endpoint(
+      getPythClusterApiUrl(this.cluster),
+      new NodeWallet(Keypair.generate()) // dummy wallet
     );
+    return squad.getAuthorityPDA(this.key, 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;
+  public async proposeWormholeMessage(
+    payload: Buffer
+  ): Promise<WormholeMultiSigTransaction> {
+    const squad = this.getSquadOrThrow();
+    const multisigVault = new MultisigVault(
+      squad.wallet,
+      this.cluster,
+      squad,
+      this.key
+    );
+    const txAccount = await multisigVault.proposeWormholeMessageWithPayer(
+      payload,
+      squad.wallet.publicKey
+    );
+    return new WormholeMultiSigTransaction(txAccount, squad, this.cluster);
   }
 }
 
-export const Vaults: Record<string, Vault> = {};
-
 export async function loadHotWallet(wallet: string): Promise<Wallet> {
   return new NodeWallet(
     Keypair.fromSecretKey(

+ 111 - 62
contract_manager/src/evm.ts

@@ -1,9 +1,90 @@
 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 { Chain, EVMChain } from "./chains";
 import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
 
+const EXTENDED_PYTH_ABI = [
+  {
+    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: [
+      {
+        internalType: "bytes",
+        name: "encodedVM",
+        type: "bytes",
+      },
+    ],
+    name: "executeGovernanceInstruction",
+    outputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+  },
+  {
+    inputs: [],
+    name: "singleUpdateFeeInWei",
+    outputs: [
+      {
+        internalType: "uint256",
+        name: "",
+        type: "uint256",
+      },
+    ],
+    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;
+
 export class EVMContract extends Contract {
   static type = "EVMContract";
 
@@ -11,11 +92,11 @@ export class EVMContract extends Contract {
     super();
   }
 
-  static fromJson(parsed: any): EVMContract {
+  static fromJson(chain: Chain, 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);
+    if (!(chain instanceof EVMChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new EVMContract(chain, parsed.address);
   }
 
   getId(): string {
@@ -28,63 +109,7 @@ export class EVMContract extends Contract {
 
   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
-    );
+    const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);
     return pythContract;
   }
 
@@ -102,6 +127,12 @@ export class EVMContract extends Contract {
     return Number(result);
   }
 
+  async getBaseUpdateFee() {
+    const pythContract = this.getContract();
+    const result = await pythContract.methods.singleUpdateFeeInWei().call();
+    return { amount: result };
+  }
+
   async getDataSources(): Promise<DataSource[]> {
     const pythContract = this.getContract();
     const result = await pythContract.methods.validDataSources().call();
@@ -124,6 +155,24 @@ export class EVMContract extends Contract {
     );
   }
 
+  async executeGovernanceInstruction(privateKey: string, vaa: Buffer) {
+    const web3 = new Web3(this.chain.rpcUrl);
+    const { address } = web3.eth.accounts.wallet.add(privateKey);
+    const pythContract = new web3.eth.Contract(EXTENDED_PYTH_ABI, this.address);
+    const transactionObject = pythContract.methods.executeGovernanceInstruction(
+      "0x" + vaa.toString("hex")
+    );
+    const gasEstiamte = await transactionObject.estimateGas({
+      from: address,
+      gas: 100000000,
+    });
+    return transactionObject.send({ from: address, gas: gasEstiamte * 2 });
+  }
+
+  getChain(): EVMChain {
+    return this.chain;
+  }
+
   toJson() {
     return {
       chain: this.chain.id,

+ 4 - 2
contract_manager/src/shell.ts

@@ -5,10 +5,12 @@ 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 { loadHotWallet, Vault } from './src/entities';" +
+    "import { SuiChain, CosmWasmChain, AptosChain, EVMChain } from './src/chains';" +
     "import { SuiContract } from './src/sui';" +
     "import { CosmWasmContract } from './src/cosmwasm';" +
+    "import { EVMContract } from './src/evm';" +
+    "import { AptosContract } from './src/aptos';" +
     "import { DefaultStore } from './src/store';" +
     "DefaultStore"
 );

+ 48 - 22
contract_manager/src/store.ts

@@ -1,25 +1,29 @@
-import { Chain, CosmWasmChain, SuiChain, Chains, EVMChain } from "./chains";
+import { AptosChain, Chain, CosmWasmChain, EVMChain, SuiChain } from "./chains";
 import { CosmWasmContract } from "./cosmwasm";
 import { SuiContract } from "./sui";
 import { Contract } from "./base";
+import { parse, stringify } from "yaml";
 import {
+  existsSync,
+  mkdirSync,
   readdirSync,
   readFileSync,
-  writeFileSync,
-  mkdirSync,
-  existsSync,
   statSync,
+  writeFileSync,
 } from "fs";
-import { Contracts } from "./entities";
 import { EVMContract } from "./evm";
+import { AptosContract } from "./aptos";
+import { Vault } from "./entities";
 
 class Store {
-  static Chains: Record<string, Chain> = {};
-  static Contracts: Record<string, CosmWasmContract | SuiContract> = {};
+  public chains: Record<string, Chain> = {};
+  public contracts: Record<string, Contract> = {};
+  public vaults: Record<string, Vault> = {};
 
   constructor(public path: string) {
     this.loadAllChains();
     this.loadAllContracts();
+    this.loadAllVaults();
   }
 
   save(obj: any) {
@@ -34,19 +38,21 @@ class Store {
       dir = `${this.path}/chains/${chain.getType()}`;
       file = chain.getId();
       content = chain.toJson();
+    } else if (obj instanceof Vault) {
+      let vault = obj;
+      dir = `${this.path}/vaults`;
+      file = vault.getId();
+      content = vault.toJson();
     } else {
       throw new Error("Invalid type");
     }
     if (!existsSync(dir)) {
       mkdirSync(dir, { recursive: true });
     }
-    writeFileSync(
-      `${dir}/${file}.json`,
-      JSON.stringify(content, undefined, 2) + "\n"
-    );
+    writeFileSync(`${dir}/${file}.yaml`, stringify(content));
   }
 
-  getJSONFiles(path: string) {
+  getYamlFiles(path: string) {
     const walk = function (dir: string) {
       let results: string[] = [];
       const list = readdirSync(dir);
@@ -63,7 +69,7 @@ class Store {
       });
       return results;
     };
-    return walk(path).filter((file) => file.endsWith(".json"));
+    return walk(path).filter((file) => file.endsWith(".yaml"));
   }
 
   loadAllChains() {
@@ -71,15 +77,16 @@ class Store {
       [CosmWasmChain.type]: CosmWasmChain,
       [SuiChain.type]: SuiChain,
       [EVMChain.type]: EVMChain,
+      [AptosChain.type]: AptosChain,
     };
 
-    this.getJSONFiles(`${this.path}/chains/`).forEach((jsonFile) => {
-      let parsed = JSON.parse(readFileSync(jsonFile, "utf-8"));
+    this.getYamlFiles(`${this.path}/chains/`).forEach((yamlFile) => {
+      let parsed = parse(readFileSync(yamlFile, "utf-8"));
       if (allChainClasses[parsed.type] === undefined) return;
       let chain = allChainClasses[parsed.type].fromJson(parsed);
-      if (Chains[chain.getId()])
+      if (this.chains[chain.getId()])
         throw new Error(`Multiple chains with id ${chain.getId()} found`);
-      Chains[chain.getId()] = chain;
+      this.chains[chain.getId()] = chain;
     });
   }
 
@@ -88,16 +95,35 @@ class Store {
       [CosmWasmContract.type]: CosmWasmContract,
       [SuiContract.type]: SuiContract,
       [EVMContract.type]: EVMContract,
+      [AptosContract.type]: AptosContract,
     };
-    this.getJSONFiles(`${this.path}/contracts/`).forEach((jsonFile) => {
-      let parsed = JSON.parse(readFileSync(jsonFile, "utf-8"));
+    this.getYamlFiles(`${this.path}/contracts/`).forEach((yamlFile) => {
+      let parsed = parse(readFileSync(yamlFile, "utf-8"));
       if (allContractClasses[parsed.type] === undefined) return;
-      let chainContract = allContractClasses[parsed.type].fromJson(parsed);
-      if (Contracts[chainContract.getId()])
+      if (!this.chains[parsed.chain])
+        throw new Error(`Chain ${parsed.chain} not found`);
+      const chain = this.chains[parsed.chain];
+      let chainContract = allContractClasses[parsed.type].fromJson(
+        chain,
+        parsed
+      );
+      if (this.contracts[chainContract.getId()])
         throw new Error(
           `Multiple contracts with id ${chainContract.getId()} found`
         );
-      Contracts[chainContract.getId()] = chainContract;
+      this.contracts[chainContract.getId()] = chainContract;
+    });
+  }
+
+  loadAllVaults() {
+    this.getYamlFiles(`${this.path}/vaults/`).forEach((yamlFile) => {
+      let parsed = parse(readFileSync(yamlFile, "utf-8"));
+      if (parsed.type !== Vault.type) return;
+
+      const vault = Vault.fromJson(parsed);
+      if (this.vaults[vault.getId()])
+        throw new Error(`Multiple vaults with id ${vault.getId()} found`);
+      this.vaults[vault.getId()] = vault;
     });
   }
 }

+ 20 - 57
contract_manager/src/sui.ts

@@ -1,22 +1,14 @@
 import {
+  Connection,
+  Ed25519Keypair,
+  JsonRpcProvider,
+  ObjectId,
   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 { Chain, SuiChain } from "./chains";
+import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
 import { Contract } from "./base";
 
 export class SuiContract extends Contract {
@@ -38,21 +30,21 @@ export class SuiContract extends Contract {
     super();
   }
 
-  static fromJson(parsed: any): SuiContract {
+  static fromJson(chain: Chain, 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
-    );
+    if (!(chain instanceof SuiChain))
+      throw new Error(`Wrong chain type ${chain}`);
+    return new SuiContract(chain, parsed.stateId, parsed.wormholeStateId);
   }
 
   getType(): string {
     return SuiContract.type;
   }
 
+  getChain(): SuiChain {
+    return this.chain;
+  }
+
   toJson() {
     return {
       chain: this.chain.id,
@@ -175,24 +167,7 @@ export class SuiContract extends Contract {
     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) {
+  async executeGovernanceInstruction(keypair: Ed25519Keypair, vaa: Buffer) {
     const tx = new TransactionBlock();
     const packageId = await this.getPythPackageId();
     let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa);
@@ -234,23 +209,6 @@ export class SuiContract extends Contract {
     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.
@@ -360,6 +318,11 @@ export class SuiContract extends Contract {
     );
   }
 
+  async getBaseUpdateFee() {
+    const fields = await this.getStateFields();
+    return { amount: fields.base_update_fee };
+  }
+
   private getProvider() {
     return new JsonRpcProvider(new Connection({ fullnode: this.chain.rpcUrl }));
   }

+ 3 - 0
contract_manager/src/test.ts

@@ -4,12 +4,15 @@ import {
   Vaults,
   loadHotWallet,
   WormholeEmitter,
+  SubmittedWormholeMessage,
 } 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";
+import { executeProposal } from "xc_admin_common";
+import { EVMContract } from "./evm";
 
 async function test() {
   // Deploy the same cosmwasm code with different config

+ 3 - 0
contract_manager/store/chains/AptosChain/aptos_testnet.yaml

@@ -0,0 +1,3 @@
+id: aptos_testnet
+rpcUrl: https://fullnode.testnet.aptoslabs.com/v1
+type: AptosChain

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/juno_testnet.json

@@ -1,9 +0,0 @@
-{
-  "querierEndpoint": "https://rpc.uni.junonetwork.io/",
-  "executorEndpoint": "https://rpc.uni.junonetwork.io/",
-  "id": "juno_testnet",
-  "gasPrice": "0.025",
-  "prefix": "juno",
-  "feeDenom": "ujunox",
-  "type": "CosmWasmChain"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/juno_testnet.yaml

@@ -0,0 +1,7 @@
+querierEndpoint: https://rpc.uni.junonetwork.io/
+executorEndpoint: https://rpc.uni.junonetwork.io/
+id: juno_testnet
+gasPrice: "0.025"
+prefix: juno
+feeDenom: ujunox
+type: CosmWasmChain

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/neutron.json

@@ -1,9 +0,0 @@
-{
-  "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"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/neutron.yaml

@@ -0,0 +1,7 @@
+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

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.json

@@ -1,9 +0,0 @@
-{
-  "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"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/neutron_testnet_pion_1.yaml

@@ -0,0 +1,7 @@
+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

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.json

@@ -1,9 +0,0 @@
-{
-  "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"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/osmosis_testnet_5.yaml

@@ -0,0 +1,7 @@
+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

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/sei_pacific_1.json

@@ -1,9 +0,0 @@
-{
-  "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"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/sei_pacific_1.yaml

@@ -0,0 +1,7 @@
+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

+ 0 - 9
contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.json

@@ -1,9 +0,0 @@
-{
-  "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"
-}

+ 7 - 0
contract_manager/store/chains/CosmWasmChain/sei_testnet_atlantic_2.yaml

@@ -0,0 +1,7 @@
+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

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

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

+ 3 - 0
contract_manager/store/chains/EVMChain/arbitrum_testnet.yaml

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

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

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

+ 3 - 0
contract_manager/store/chains/EVMChain/cronos.yaml

@@ -0,0 +1,3 @@
+id: cronos
+rpcUrl: https://cronosrpc-1.xstaking.sg
+type: EVMChain

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

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

+ 3 - 0
contract_manager/store/chains/EVMChain/cronos_testnet.yaml

@@ -0,0 +1,3 @@
+id: cronos_testnet
+rpcUrl: https://evm-t3.cronos.org
+type: EVMChain

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

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

+ 3 - 0
contract_manager/store/chains/SuiChain/sui_devnet.yaml

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

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

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

+ 3 - 0
contract_manager/store/chains/SuiChain/sui_mainnet.yaml

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

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

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

+ 3 - 0
contract_manager/store/chains/SuiChain/sui_testnet.yaml

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

+ 4 - 0
contract_manager/store/contracts/AptosContract/aptos_testnet_0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387.yaml

@@ -0,0 +1,4 @@
+chain: aptos_testnet
+stateId: "0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387"
+wormholeStateId: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625"
+type: AptosContract

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

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

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/juno_testnet_juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs.yaml

@@ -0,0 +1,3 @@
+chain: juno_testnet
+address: juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs
+type: CosmWasmContract

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

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

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/neutron_neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv.yaml

@@ -0,0 +1,3 @@
+chain: neutron
+address: neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv
+type: CosmWasmContract

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

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

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/neutron_testnet_pion_1_neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp.yaml

@@ -0,0 +1,3 @@
+chain: neutron_testnet_pion_1
+address: neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp
+type: CosmWasmContract

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

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

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3.yaml

@@ -0,0 +1,3 @@
+chain: osmosis_testnet_5
+address: osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3
+type: CosmWasmContract

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/osmosis_testnet_5_osmo1q3pzdelxnh2yk66vux4s6ewrw59sx0uu2q6xd7navlsvc3vry92sk3pe7g.yaml

@@ -0,0 +1,3 @@
+chain: osmosis_testnet_5
+address: osmo1q3pzdelxnh2yk66vux4s6ewrw59sx0uu2q6xd7navlsvc3vry92sk3pe7g
+type: CosmWasmContract

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

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

+ 3 - 0
contract_manager/store/contracts/CosmWasmContract/sei_testnet_atlantic_2_sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy.yaml

@@ -0,0 +1,3 @@
+chain: sei_testnet_atlantic_2
+address: sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy
+type: CosmWasmContract

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

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

+ 3 - 0
contract_manager/store/contracts/EVMContract/cronos_0xe0d0e68297772dd5a1f1d99897c581e2082dba5b.yaml

@@ -0,0 +1,3 @@
+chain: cronos
+address: "0xe0d0e68297772dd5a1f1d99897c581e2082dba5b"
+type: EVMContract

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

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

+ 3 - 0
contract_manager/store/contracts/EVMContract/cronos_testnet_0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7.yaml

@@ -0,0 +1,3 @@
+chain: cronos_testnet
+address: "0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7"
+type: EVMContract

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

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

+ 4 - 0
contract_manager/store/contracts/SuiContract/sui_mainnet_0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f.yaml

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

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

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

+ 4 - 0
contract_manager/store/contracts/SuiContract/sui_testnet_0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3.yaml

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

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

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

+ 4 - 0
contract_manager/store/contracts/SuiContract/sui_testnet_0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88.yaml

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

+ 3 - 0
contract_manager/store/vaults/devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3.yaml

@@ -0,0 +1,3 @@
+key: 6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3
+cluster: devnet
+type: vault

+ 3 - 0
contract_manager/store/vaults/mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj.yaml

@@ -0,0 +1,3 @@
+key: FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj
+cluster: mainnet-beta
+type: vault

+ 4 - 116
governance/xc_admin/packages/crank_executor/src/index.ts

@@ -1,34 +1,12 @@
-import {
-  AccountMeta,
-  Commitment,
-  Connection,
-  Keypair,
-  PublicKey,
-  SystemProgram,
-  Transaction,
-} from "@solana/web3.js";
-import SquadsMesh, { DEFAULT_MULTISIG_PROGRAM_ID, getIxPDA } from "@sqds/mesh";
+import { Commitment, Connection, Keypair, PublicKey } from "@solana/web3.js";
+import SquadsMesh, { DEFAULT_MULTISIG_PROGRAM_ID } from "@sqds/mesh";
 import * as fs from "fs";
 import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
-import {
-  envOrErr,
-  getCreateAccountWithSeedInstruction,
-  getProposals,
-  MultisigParser,
-  PythMultisigInstruction,
-  WormholeMultisigInstruction,
-} from "xc_admin_common";
-import BN from "bn.js";
-import { AnchorProvider } from "@project-serum/anchor";
+import { envOrErr, executeProposal, getProposals } from "xc_admin_common";
 import {
   getPythClusterApiUrl,
   PythCluster,
 } from "@pythnetwork/client/lib/cluster";
-import {
-  deriveFeeCollectorKey,
-  getWormholeBridgeData,
-} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
-import { AccountType, parseProductData } from "@pythnetwork/client";
 
 const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster;
 const VAULT: PublicKey = new PublicKey(envOrErr("VAULT"));
@@ -44,17 +22,6 @@ async function run() {
     wallet: new NodeWallet(KEYPAIR),
     multisigProgramId: DEFAULT_MULTISIG_PROGRAM_ID,
   });
-  const multisigParser = MultisigParser.fromCluster(CLUSTER);
-
-  const wormholeFee = multisigParser.wormholeBridgeAddress
-    ? (
-        await getWormholeBridgeData(
-          squad.connection,
-          multisigParser.wormholeBridgeAddress!,
-          COMMITMENT
-        )
-      ).config.fee
-    : 0;
 
   const proposals = await getProposals(squad, VAULT, undefined, "executeReady");
 
@@ -62,86 +29,7 @@ async function run() {
     console.log("Trying to execute: ", proposal.publicKey.toBase58());
     // If we have previously cancelled because the proposal was failing, don't attempt
     if (proposal.cancelled.length == 0) {
-      for (
-        let i = proposal.executedIndex + 1;
-        i <= proposal.instructionIndex;
-        i++
-      ) {
-        const instructionPda = getIxPDA(
-          proposal.publicKey,
-          new BN(i),
-          squad.multisigProgramId
-        )[0];
-        const instruction = await squad.getInstruction(instructionPda);
-        const parsedInstruction = multisigParser.parseInstruction({
-          programId: instruction.programId,
-          data: instruction.data as Buffer,
-          keys: instruction.keys as AccountMeta[],
-        });
-        const transaction = new Transaction();
-
-        if (
-          parsedInstruction instanceof WormholeMultisigInstruction &&
-          parsedInstruction.name == "postMessage"
-        ) {
-          transaction.add(
-            SystemProgram.transfer({
-              lamports: wormholeFee,
-              toPubkey: deriveFeeCollectorKey(
-                multisigParser.wormholeBridgeAddress!
-              ),
-              fromPubkey: squad.wallet.publicKey,
-            })
-          );
-        } else if (
-          parsedInstruction instanceof PythMultisigInstruction &&
-          parsedInstruction.name == "addProduct"
-        ) {
-          /// Add product, fetch the symbol from the instruction
-          transaction.add(
-            await getCreateAccountWithSeedInstruction(
-              squad.connection,
-              CLUSTER,
-              squad.wallet.publicKey,
-              parsedInstruction.args.symbol,
-              AccountType.Product
-            )
-          );
-        } else if (
-          parsedInstruction instanceof PythMultisigInstruction &&
-          parsedInstruction.name == "addPrice"
-        ) {
-          /// Add price, fetch the symbol from the product account
-          const productAccount = await squad.connection.getAccountInfo(
-            parsedInstruction.accounts.named.productAccount.pubkey
-          );
-          if (productAccount) {
-            transaction.add(
-              await getCreateAccountWithSeedInstruction(
-                squad.connection,
-                CLUSTER,
-                squad.wallet.publicKey,
-                parseProductData(productAccount.data).product.symbol,
-                AccountType.Price
-              )
-            );
-          } else {
-            throw Error("Product account not found");
-          }
-        }
-
-        transaction.add(
-          await squad.buildExecuteInstruction(
-            proposal.publicKey,
-            getIxPDA(proposal.publicKey, new BN(i), squad.multisigProgramId)[0]
-          )
-        );
-
-        await new AnchorProvider(squad.connection, squad.wallet, {
-          commitment: COMMITMENT,
-          preflightCommitment: COMMITMENT,
-        }).sendAndConfirm(transaction, [], { skipPreflight: true });
-      }
+      await executeProposal(proposal, squad, CLUSTER, COMMITMENT);
     } else {
       console.log("Skipping: ", proposal.publicKey.toBase58());
     }

+ 149 - 0
governance/xc_admin/packages/xc_admin_common/src/executor.ts

@@ -0,0 +1,149 @@
+import { TransactionAccount } from "@sqds/mesh/lib/types";
+import SquadsMesh, { getIxPDA } from "@sqds/mesh";
+import { PythCluster } from "@pythnetwork/client/lib/cluster";
+import {
+  AccountMeta,
+  Commitment,
+  PublicKey,
+  SystemProgram,
+  Transaction,
+} from "@solana/web3.js";
+import {
+  MultisigParser,
+  PythMultisigInstruction,
+  WormholeMultisigInstruction,
+} from "./multisig_transaction";
+import BN from "bn.js";
+import {
+  deriveFeeCollectorKey,
+  getWormholeBridgeData,
+} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
+import { getCreateAccountWithSeedInstruction } from "./deterministic_oracle_accounts";
+import { AccountType, parseProductData } from "@pythnetwork/client";
+import { AnchorProvider } from "@project-serum/anchor";
+
+/**
+ * Returns the instruction to pay the fee for a wormhole postMessage instruction
+ * This instruction is usually not included in the proposals to save space and shorten the tx size
+ * @param wormholeBridgeAddress address of the wormhole bridge program deployed on the cluster
+ * @param squad
+ */
+export async function getPostMessageFeeInstruction(
+  wormholeBridgeAddress: PublicKey,
+  squad: SquadsMesh
+) {
+  const wormholeFee = wormholeBridgeAddress
+    ? (
+        await getWormholeBridgeData(
+          squad.connection,
+          wormholeBridgeAddress,
+          "confirmed"
+        )
+      ).config.fee
+    : 0;
+
+  return SystemProgram.transfer({
+    lamports: wormholeFee,
+    toPubkey: deriveFeeCollectorKey(wormholeBridgeAddress),
+    fromPubkey: squad.wallet.publicKey,
+  });
+}
+
+/**
+ * Continues executing a multisig proposal from the last executed instruction
+ * Each instruction is executed in a separate transaction
+ * Returns the signatures of the executed transactions
+ * @param proposal the account which stores the proposed transaction
+ * @param squad squad owning the proposal
+ * @param cluster cluster the proposal and squad are on
+ * @param commitment commitment level to use for RPC calls
+ */
+export async function executeProposal(
+  proposal: TransactionAccount,
+  squad: SquadsMesh,
+  cluster: PythCluster,
+  commitment: Commitment = "confirmed"
+) {
+  const multisigParser = MultisigParser.fromCluster(cluster);
+  const signatures: string[] = [];
+  for (
+    let i = proposal.executedIndex + 1;
+    i <= proposal.instructionIndex;
+    i++
+  ) {
+    const instructionPda = getIxPDA(
+      proposal.publicKey,
+      new BN(i),
+      squad.multisigProgramId
+    )[0];
+    const instruction = await squad.getInstruction(instructionPda);
+    const parsedInstruction = multisigParser.parseInstruction({
+      programId: instruction.programId,
+      data: instruction.data as Buffer,
+      keys: instruction.keys as AccountMeta[],
+    });
+    const transaction = new Transaction();
+
+    if (
+      parsedInstruction instanceof WormholeMultisigInstruction &&
+      parsedInstruction.name == "postMessage"
+    ) {
+      transaction.add(
+        await getPostMessageFeeInstruction(
+          multisigParser.wormholeBridgeAddress!,
+          squad
+        )
+      );
+    } else if (
+      parsedInstruction instanceof PythMultisigInstruction &&
+      parsedInstruction.name == "addProduct"
+    ) {
+      /// Add product, fetch the symbol from the instruction
+      transaction.add(
+        await getCreateAccountWithSeedInstruction(
+          squad.connection,
+          cluster,
+          squad.wallet.publicKey,
+          parsedInstruction.args.symbol,
+          AccountType.Product
+        )
+      );
+    } else if (
+      parsedInstruction instanceof PythMultisigInstruction &&
+      parsedInstruction.name == "addPrice"
+    ) {
+      /// Add price, fetch the symbol from the product account
+      const productAccount = await squad.connection.getAccountInfo(
+        parsedInstruction.accounts.named.productAccount.pubkey
+      );
+      if (productAccount) {
+        transaction.add(
+          await getCreateAccountWithSeedInstruction(
+            squad.connection,
+            cluster,
+            squad.wallet.publicKey,
+            parseProductData(productAccount.data).product.symbol,
+            AccountType.Price
+          )
+        );
+      } else {
+        throw Error("Product account not found");
+      }
+    }
+
+    transaction.add(
+      await squad.buildExecuteInstruction(
+        proposal.publicKey,
+        getIxPDA(proposal.publicKey, new BN(i), squad.multisigProgramId)[0]
+      )
+    );
+
+    signatures.push(
+      await new AnchorProvider(squad.connection, squad.wallet, {
+        commitment: commitment,
+        preflightCommitment: commitment,
+      }).sendAndConfirm(transaction, [], { skipPreflight: true })
+    );
+  }
+  return signatures;
+}

+ 1 - 0
governance/xc_admin/packages/xc_admin_common/src/index.ts

@@ -10,3 +10,4 @@ export * from "./deterministic_oracle_accounts";
 export * from "./cranks";
 export * from "./message_buffer";
 export * from "./contracts";
+export * from "./executor";

+ 23 - 5
governance/xc_admin/packages/xc_admin_common/src/propose.ts

@@ -171,13 +171,27 @@ export class MultisigVault {
 
   // Propose instructions
 
+  /**
+   * Same as proposeWormholeMessageWithPayer, but uses the predefined ops key for the vault
+   * If the vault is not configured with an ops key, this will throw an error.
+   * @param payload the bytes to send as the wormhole message's payload.
+   * @returns the newly created proposal's public key
+   */
+  public async proposeWormholeMessage(payload: Buffer): Promise<PublicKey> {
+    return this.proposeWormholeMessageWithPayer(payload, getOpsKey(this.vault));
+  }
+
   /**
    * Propose submitting `payload` as a wormhole message. If the proposal is approved, the sent message
    * will have `this.getVaultAuthorityPda()` as its emitter address.
    * @param payload the bytes to send as the wormhole message's payload.
+   * @param messagePayer key used as the payer for the wormhole message instruction
    * @returns the newly created proposal's public key
    */
-  public async proposeWormholeMessage(payload: Buffer): Promise<PublicKey> {
+  public async proposeWormholeMessageWithPayer(
+    payload: Buffer,
+    messagePayer: PublicKey
+  ): Promise<PublicKey> {
     const msAccount = await this.getMultisigAccount();
 
     let ixToSend: TransactionInstruction[] = [];
@@ -194,7 +208,8 @@ export class MultisigVault {
       newProposalAddress,
       1,
       this.wormholeAddress()!,
-      payload
+      payload,
+      messagePayer
     );
     ixToSend.push(
       await this.squad.buildAddInstruction(
@@ -473,7 +488,8 @@ export async function wrapAsRemoteInstruction(
     proposalAddress,
     instructionIndex,
     wormholeAddress,
-    buffer
+    buffer,
+    getOpsKey(vault)
   );
 }
 
@@ -485,6 +501,7 @@ export async function wrapAsRemoteInstruction(
  * @param instructionIndex index of the instruction within the proposal
  * @param wormholeAddress address of the Wormhole bridge
  * @param payload the payload to be posted
+ * @param messagePayer key used as the payer for the wormhole message instruction
  */
 async function getPostMessageInstruction(
   squad: SquadsMesh,
@@ -492,7 +509,8 @@ async function getPostMessageInstruction(
   proposalAddress: PublicKey,
   instructionIndex: number,
   wormholeAddress: PublicKey,
-  payload: Buffer
+  payload: Buffer,
+  messagePayer: PublicKey
 ): Promise<SquadInstruction> {
   const [messagePDA, messagePdaBump] = getIxAuthorityPDA(
     proposalAddress,
@@ -514,7 +532,7 @@ async function getPostMessageInstruction(
   const accounts = getPostMessageAccounts(
     wormholeAddress,
     emitter,
-    getOpsKey(vault),
+    messagePayer,
     messagePDA
   );
 

Разлика између датотеке није приказан због своје велике величине
+ 1067 - 5
package-lock.json


+ 2 - 1
package.json

@@ -17,7 +17,8 @@
     "target_chains/ethereum/sdk/solidity",
     "target_chains/ethereum/examples/oracle_swap/app",
     "third_party/pyth/p2w-relay",
-    "wormhole_attester/sdk/js"
+    "wormhole_attester/sdk/js",
+    "contract_manager"
   ],
   "dependencies": {
     "pre-commit": "^1.2.2",

Неке датотеке нису приказане због велике количине промена