Przeglądaj źródła

tilt: devnet deployment for ibc generic messaging (#2593)

* Tilt devnet deployment for ibc generic messaging

* Address review comments from kcsongor and hendrikhofstadt

* Add IBC channel whitelist updates to wormchain and terra devnet deploy scripts

* VAAs had guardian set index three instead of zero

* ci: update addresses

* Remove message.block_height and message.tx_index from attributes

* Remove unnecessary contracts from terra2 devnet deployment

* Update wormhole-ibc address on terra2

* Update wormhole-ibc guardian set on terra2 devnet deployment

* IBC relayer testnet deployment fixes

* Wormchain update whitelist fix

---------

Co-authored-by: Bruce Riley <briley@jumptrading.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
Nikhil Suri 2 lat temu
rodzic
commit
f6f93bf35e

+ 9 - 3
Tiltfile

@@ -85,7 +85,7 @@ ci_tests = cfg.get("ci_tests", ci)
 guardiand_debug = cfg.get("guardiand_debug", False)
 node_metrics = cfg.get("node_metrics", False)
 guardiand_governor = cfg.get("guardiand_governor", False)
-ibc_relayer = cfg.get("ibc_relayer", False)
+ibc_relayer = cfg.get("ibc_relayer", ci)
 btc = cfg.get("btc", False)
 
 if cfg.get("manual", False):
@@ -287,7 +287,13 @@ def build_node_yaml():
                     "--accountantContract",
                     "wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465",
                     "--accountantCheckEnabled",
-                    "true"
+                    "true",
+                    "--ibcWS",
+                    "ws://wormchain:26657/websocket",
+                    "--ibcLCD",
+                    "http://wormchain:1317",
+                    "--ibcContract",
+                    "wormhole1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq0kdhcj"
                 ]
 
     return encode_yaml_stream(node_yaml_with_replicas)
@@ -854,7 +860,7 @@ if ibc_relayer:
         port_forwards = [
             port_forward(7597, name = "HTTPDEBUG [:7597]", host = webHost),
         ],
-        resource_deps = ["wormchain", "terra2-terrad"],
+        resource_deps = ["wormchain-deploy", "terra2-terrad"],
         labels = ["ibc-relayer"],
         trigger_mode = trigger_mode,
     )

+ 1 - 3
cosmwasm/contracts/wormchain-ibc-receiver/src/ibc.rs

@@ -101,15 +101,13 @@ fn handle_packet_receive(msg: IbcPacketReceiveMsg) -> Result<IbcReceiveResponse,
     }
 }
 
-const EXPECTED_WORMHOLE_IBC_EVENT_ATTRS: [&str; 8] = [
+const EXPECTED_WORMHOLE_IBC_EVENT_ATTRS: [&str; 6] = [
     "message.message",
     "message.sender",
     "message.chain_id",
     "message.nonce",
     "message.sequence",
     "message.block_time",
-    "message.tx_index",
-    "message.block_height",
 ];
 
 fn receive_publish(

+ 1 - 15
cosmwasm/contracts/wormhole-ibc/src/contract.rs

@@ -148,22 +148,8 @@ fn post_message_ibc(
     // compute the packet timeout
     let packet_timeout = env.block.time.plus_seconds(PACKET_LIFETIME).into();
 
-    // compute the block height
-    let block_height = env.block.height.to_string();
-
-    // compute the transaction index
-    // (this is an optional since not all messages are executed as part of txns)
-    // (they may be executed part of the pre/post block handlers)
-    let tx_index = env.transaction.as_ref().map(|tx_info| tx_info.index);
-
     // actually execute the postMessage call on the core contract
-    let mut res = core_execute(deps, env, info, msg).context("wormhole core execution failed")?;
-
-    res = match tx_index {
-        Some(index) => res.add_attribute("message.tx_index", index.to_string()),
-        None => res,
-    };
-    res = res.add_attribute("message.block_height", block_height);
+    let res = core_execute(deps, env, info, msg).context("wormhole core execution failed")?;
 
     // Send the result attributes over IBC on this channel
     let packet = WormholeIbcPacketMsg::Publish {

+ 68 - 18
cosmwasm/deployment/terra2/tools/deploy.js

@@ -9,6 +9,12 @@ import { readFileSync, readdirSync } from "fs";
 import { Bech32, toHex } from "@cosmjs/encoding";
 import { zeroPad } from "ethers/lib/utils.js";
 
+// Generated using
+// `guardiand template ibc-receiver-update-channel-chain --channel-id channel-0 --chain-id 3104 --target-chain-id 32 > terra2.prototxt`
+// `guardiand admin governance-vaa-verify terra2.prototxt`
+const WORMHOLE_IBC_WHITELIST_VAA =
+  "0100000000010025e55ab23c8d0a7fddd4686f41801792cdce1ff7335a2b9436192bd552fa0f9b5c18016057b0d4b3f24c759eafe3e5fedd7fce76fe6f21cec815ffbaf4ec3ad801000000009b9a6b2d0001000000000000000000000000000000000000000000000000000000000000000460efd4405060ac0c200000000000000000000000000000000000000000004962635265636569766572010020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006368616e6e656c2d300c20";
+
 /*
   NOTE: Only append to this array: keeping the ordering is crucial, as the
   contracts must be imported in a deterministic order so their addresses remain
@@ -19,11 +25,6 @@ const artifacts = [
   "cw_token_bridge.wasm",
   "cw20_wrapped_2.wasm",
   "cw20_base.wasm",
-  "mock_bridge_integration_2.wasm",
-  "shutdown_core_bridge_cosmwasm.wasm",
-  "shutdown_token_bridge_cosmwasm.wasm",
-  "global_accountant.wasm",
-  "wormchain_ibc_receiver.wasm",
   "wormhole_ibc.wasm",
 ];
 
@@ -50,18 +51,6 @@ if (missing_artifacts.length) {
   process.exit(1);
 }
 
-const unexpected_artifacts = actual_artifacts.filter(
-  (a) => !artifacts.includes(a)
-);
-if (unexpected_artifacts.length) {
-  console.log(
-    "Error during terra deployment. The following files are not expected to be in the artifacts folder:"
-  );
-  unexpected_artifacts.forEach((file) => console.log(`  - ${file}`));
-  console.log("Hint: you might need to modify tools/deploy.js");
-  process.exit(1);
-}
-
 /* Set up terra client & wallet */
 
 const terra = new LCDClient({
@@ -208,6 +197,28 @@ addresses["mock.wasm"] = await instantiate(
   "mock"
 );
 
+addresses["wormhole_ibc.wasm"] = await instantiate(
+  "wormhole_ibc.wasm",
+  {
+    gov_chain: govChain,
+    gov_address: Buffer.from(govAddress, "hex").toString("base64"),
+    guardian_set_expirity: 86400,
+    initial_guardian_set: {
+      // This is using one guardian so the above registration can be hard-coded
+      // TODO: instantiate with the correct guardian set and dynamically generate the registration
+      addresses: [
+        {
+          bytes: Buffer.from(init_guardians[0], "hex").toString("base64"),
+        },
+      ],
+      expiration_time: 0,
+    },
+    chain_id: 32,
+    fee_denom: "uluna",
+  },
+  "wormholeIbc"
+);
+
 /* Registrations: tell the bridge contracts to know about each other */
 
 const contract_registrations = {
@@ -253,10 +264,49 @@ for (const [contract, registrations] of Object.entries(
         memo: "",
       })
       .then((tx) => terra.tx.broadcast(tx))
-      .then((rs) => console.log(rs));
+      .then((rs) => console.log(rs))
+      .catch((error) => {
+        if (error.response) {
+          // Request made and server responded
+          console.error(
+            error.response.data,
+            error.response.status,
+            error.response.headers
+          );
+        } else if (error.request) {
+          // The request was made but no response was received
+          console.error(error.request);
+        } else {
+          // Something happened in setting up the request that triggered an Error
+          console.error("Error", error.message);
+        }
+
+        throw new Error(`Registering chain failed: ${registration}`);
+      });
   }
 }
 
+// submit wormchain channel ID whitelist to the wormhole_ibc contract
+const ibc_whitelist_tx = await wallet.createAndSignTx({
+  msgs: [
+    new MsgExecuteContract(
+      wallet.key.accAddress,
+      addresses["wormhole_ibc.wasm"],
+      {
+        submit_update_channel_chain: {
+          vaa: Buffer.from(WORMHOLE_IBC_WHITELIST_VAA, "hex").toString(
+            "base64"
+          ),
+        },
+      },
+      { uluna: 1000 }
+    ),
+  ],
+  memo: "",
+});
+const ibc_whitelist_res = await terra.tx.broadcast(ibc_whitelist_tx);
+console.log("updated wormhole_ibc channel whitelist", ibc_whitelist_res.txhash);
+
 // Terra addresses are "human-readable", but for cross-chain registrations, we
 // want the "canonical" version
 function convert_terra_address_to_hex(human_addr) {

+ 9 - 2
devnet/ibc-relayer.yaml

@@ -37,7 +37,14 @@ spec:
             - link-then-start
             - terra-wormchain
             - --debug-addr
-            - localhost:7597
+            - 0.0.0.0:7597
+            - --src-port
+            - wasm.terra1436kxs0w2es6xlqpp9rd35e3d0cjnw4sv8j3a7483sgks29jqwgsnyey7t
+            - --dst-port
+            - wasm.wormhole1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq0kdhcj
+            - --version
+            - ibc-wormhole-v1
+            - --override
           ports:
             - containerPort: 7597
               name: rest
@@ -48,4 +55,4 @@ spec:
               path: /
             periodSeconds: 1
       restartPolicy: Always
-  serviceName: ibc-relayer
+  serviceName: ibc-relayer

+ 84 - 0
sdk/js/src/bridge/__tests__/wormhole_ibc_e2e.ts

@@ -0,0 +1,84 @@
+import { describe, test } from "@jest/globals";
+import {
+  LCDClient,
+  MnemonicKey,
+  Msg,
+  MsgExecuteContract,
+  Wallet,
+  isTxError,
+} from "@terra-money/terra.js";
+import { getEmitterAddressTerra, parseSequenceFromLogTerra } from "../..";
+import {
+  TERRA2_NODE_URL,
+  TERRA_CHAIN_ID,
+} from "../../token_bridge/__tests__/utils/consts";
+import {
+  getSignedVAABySequence,
+  waitForTerraExecution,
+} from "../../token_bridge/__tests__/utils/helpers";
+import { CHAIN_ID_SEI, CHAIN_ID_TERRA2 } from "../../utils/consts";
+
+const TERRA2_PRIVATE_KEY_4 =
+  "bounce success option birth apple portion aunt rural episode solution hockey pencil lend session cause hedgehog slender journey system canvas decorate razor catch empty";
+
+const lcd = new LCDClient({
+  URL: TERRA2_NODE_URL,
+  chainID: TERRA_CHAIN_ID,
+});
+const terraWallet = lcd.wallet(
+  new MnemonicKey({ mnemonic: TERRA2_PRIVATE_KEY_4 })
+);
+const terraWalletAddress = terraWallet.key.accAddress;
+
+const terraBroadcastAndWaitForExecution = async (
+  msgs: Msg[],
+  wallet: Wallet
+) => {
+  const tx = await wallet.createAndSignTx({
+    msgs,
+  });
+  const txResult = await lcd.tx.broadcast(tx);
+  if (isTxError(txResult)) {
+    throw new Error("tx error");
+  }
+  const txInfo = await waitForTerraExecution(txResult.txhash, lcd);
+  if (!txInfo) {
+    throw new Error("tx info not found");
+  }
+  return txInfo;
+};
+
+const terraBroadcastTxAndGetSignedVaa = async (
+  msgs: Msg[],
+  wallet: Wallet,
+  emitter: string
+) => {
+  const txInfo = await terraBroadcastAndWaitForExecution(msgs, wallet);
+  const txSequence = parseSequenceFromLogTerra(txInfo);
+  if (!txSequence) {
+    throw new Error("tx sequence not found");
+  }
+  console.log(`${CHAIN_ID_SEI}/${emitter}/${txSequence}`);
+  return await getSignedVAABySequence(CHAIN_ID_SEI, txSequence, emitter);
+};
+
+describe("IBC Watcher Integration Tests", () => {
+  test('Send a message from "Sei" (Terra2) via IBC', async () => {
+    const postMsg = new MsgExecuteContract(
+      terraWalletAddress,
+      "terra1436kxs0w2es6xlqpp9rd35e3d0cjnw4sv8j3a7483sgks29jqwgsnyey7t",
+      {
+        post_message: {
+          message: Buffer.from("Hello World").toString("base64"),
+          nonce: 1,
+        },
+      }
+    );
+    const postedVaa = await terraBroadcastTxAndGetSignedVaa(
+      [postMsg],
+      terraWallet,
+      await getEmitterAddressTerra(terraWalletAddress)
+    );
+    console.log(postedVaa);
+  });
+});

+ 1 - 0
testing/sdk.sh

@@ -2,4 +2,5 @@
 set -e
 while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' guardian:6060/readyz)" != "200" ]]; do sleep 5; done
 while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' spy:6060/metrics)" != "200" ]]; do sleep 5; done
+while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ibc-relayer:7597/debug/pprof/)" != "200" ]]; do sleep 5; done
 CI=true npm --prefix ../sdk/js run test-ci

+ 286 - 216
wormchain/contracts/tools/deploy_wormchain.ts

@@ -1,17 +1,17 @@
 import {
-    CHAIN_ID_WORMCHAIN,
-    hexToUint8Array,
-    Other,
-    Payload,
-    serialiseVAA,
-    sign,
-    VAA,
+  CHAIN_ID_WORMCHAIN,
+  hexToUint8Array,
+  Other,
+  Payload,
+  serialiseVAA,
+  sign,
+  VAA,
 } from "@certusone/wormhole-sdk";
 import { toBinary } from "@cosmjs/cosmwasm-stargate";
-import { fromBase64, toUtf8 } from "@cosmjs/encoding";
+import { fromBase64, toUtf8, fromBech32 } from "@cosmjs/encoding";
 import {
-    getWallet,
-    getWormchainSigningClient,
+  getWallet,
+  getWormchainSigningClient,
 } from "@wormhole-foundation/wormchain-sdk";
 import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts";
 import "dotenv/config";
@@ -23,9 +23,9 @@ import * as util from "util";
 import * as devnetConsts from "./devnet-consts.json";
 
 if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") {
-    let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`;
-    console.error(msg);
-    throw msg;
+  let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`;
+  console.error(msg);
+  throw msg;
 }
 
 const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(",");
@@ -40,236 +40,306 @@ const readFileAsync = util.promisify(fs.readFile);
   deterministic.
 */
 type ContractName = string;
-const artifacts: ContractName[] = ["global_accountant.wasm"];
+const artifacts: ContractName[] = [
+  "global_accountant.wasm",
+  "wormchain_ibc_receiver.wasm",
+];
 
 const ARTIFACTS_PATH = "../artifacts/";
 /* Check that the artifact folder contains all the wasm files we expect and nothing else */
 
 try {
-    const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) =>
-        a.endsWith(".wasm")
-    );
+  const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) =>
+    a.endsWith(".wasm")
+  );
 
-    const missing_artifacts = artifacts.filter(
-        (a) => !actual_artifacts.includes(a)
+  const missing_artifacts = artifacts.filter(
+    (a) => !actual_artifacts.includes(a)
+  );
+  if (missing_artifacts.length) {
+    console.log(
+      "Error during wormchain deployment. The following files are expected to be in the artifacts folder:"
     );
-    if (missing_artifacts.length) {
-        console.log(
-            "Error during wormchain deployment. The following files are expected to be in the artifacts folder:"
-        );
-        missing_artifacts.forEach((file) => console.log(`  - ${file}`));
-        console.log(
-            "Hint: the deploy script needs to run after the contracts have been built."
-        );
-        console.log(
-            "External binary blobs need to be manually added in tools/Dockerfile."
-        );
-        process.exit(1);
-    }
-} catch (err) {
-    console.error(
-        `${ARTIFACTS_PATH} cannot be read. Do you need to run "make contracts-deploy-setup"?`
+    missing_artifacts.forEach((file) => console.log(`  - ${file}`));
+    console.log(
+      "Hint: the deploy script needs to run after the contracts have been built."
+    );
+    console.log(
+      "External binary blobs need to be manually added in tools/Dockerfile."
     );
     process.exit(1);
+  }
+} catch (err) {
+  console.error(
+    `${ARTIFACTS_PATH} cannot be read. Do you need to run "make contracts-deploy-setup"?`
+  );
+  process.exit(1);
 }
 
 async function main() {
-    /* Set up cosmos client & wallet */
+  /* Set up cosmos client & wallet */
 
-    let host = devnetConsts.chains[3104].tendermintUrlLocal;
-    if (os.hostname().includes("wormchain-deploy")) {
-        // running in tilt devnet
-        host = devnetConsts.chains[3104].tendermintUrlTilt;
-    }
+  let host = devnetConsts.chains[3104].tendermintUrlLocal;
+  if (os.hostname().includes("wormchain-deploy")) {
+    // running in tilt devnet
+    host = devnetConsts.chains[3104].tendermintUrlTilt;
+  }
 
-    const mnemonic =
-        devnetConsts.chains[3104].accounts.wormchainNodeOfGuardian0.mnemonic;
+  const mnemonic =
+    devnetConsts.chains[3104].accounts.wormchainNodeOfGuardian0.mnemonic;
 
-    const wallet = await getWallet(mnemonic);
-    const client = await getWormchainSigningClient(host, wallet);
+  const wallet = await getWallet(mnemonic);
+  const client = await getWormchainSigningClient(host, wallet);
 
-    // there are several Cosmos chains in devnet, so check the config is as expected
-    let id = await client.getChainId();
-    if (id !== "wormchain") {
-        throw new Error(
-            `Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`
-        );
-    }
+  // there are several Cosmos chains in devnet, so check the config is as expected
+  let id = await client.getChainId();
+  if (id !== "wormchain") {
+    throw new Error(
+      `Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`
+    );
+  }
+
+  const signers = await wallet.getAccounts();
+  const signer = signers[0].address;
+  console.log("wormchain contract deployer is: ", signer);
 
-    const signers = await wallet.getAccounts();
-    const signer = signers[0].address;
-    console.log("wormchain contract deployer is: ", signer);
-
-    /* Deploy artifacts */
-
-    const codeIds: { [name: ContractName]: number } = await artifacts.reduce(
-        async (prev, file) => {
-            // wait for the previous to finish, to avoid the race condition of wallet sequence mismatch.
-            const accum = await prev;
-
-            const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`);
-
-            const payload = keccak256(contract_bytes);
-            let vaa: VAA<Other> = {
-                version: 1,
-                guardianSetIndex: 0,
-                signatures: [],
-                timestamp: 0,
-                nonce: 0,
-                emitterChain: GOVERNANCE_CHAIN,
-                emitterAddress: GOVERNANCE_EMITTER,
-                sequence: BigInt(Math.floor(Math.random() * 100000000)),
-                consistencyLevel: 0,
-                payload: {
-                    type: "Other",
-                    hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString(
-                        16
-                    )}${payload}`,
-                },
-            };
-            vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA<Payload>);
-            console.log("uploading", file);
-            const msg = client.core.msgStoreCode({
-                signer,
-                wasm_byte_code: new Uint8Array(contract_bytes),
-                vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA<Payload>)),
-            });
-            const result = await client.signAndBroadcast(signer, [msg], {
-                ...ZERO_FEE,
-                gas: "10000000",
-            });
-            const codeId = Number(
-                JSON.parse(result.rawLog)[0]
-                    .events.find(({ type }) => type === "store_code")
-                    .attributes.find(({ key }) => key === "code_id").value
-            );
-            console.log(
-                `uploaded ${file}, codeID: ${codeId}, tx: ${result.transactionHash}`
-            );
-
-            accum[file] = codeId;
-            return accum;
+  /* Deploy artifacts */
+
+  const codeIds: { [name: ContractName]: number } = await artifacts.reduce(
+    async (prev, file) => {
+      // wait for the previous to finish, to avoid the race condition of wallet sequence mismatch.
+      const accum = await prev;
+
+      const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`);
+
+      const payload = keccak256(contract_bytes);
+      let vaa: VAA<Other> = {
+        version: 1,
+        guardianSetIndex: 0,
+        signatures: [],
+        timestamp: 0,
+        nonce: 0,
+        emitterChain: GOVERNANCE_CHAIN,
+        emitterAddress: GOVERNANCE_EMITTER,
+        sequence: BigInt(Math.floor(Math.random() * 100000000)),
+        consistencyLevel: 0,
+        payload: {
+          type: "Other",
+          hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString(
+            16
+          )}${payload}`,
         },
-        Object()
-    );
+      };
+      vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA<Payload>);
+      console.log("uploading", file);
+      const msg = client.core.msgStoreCode({
+        signer,
+        wasm_byte_code: new Uint8Array(contract_bytes),
+        vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA<Payload>)),
+      });
+      const result = await client.signAndBroadcast(signer, [msg], {
+        ...ZERO_FEE,
+        gas: "10000000",
+      });
+      const codeId = Number(
+        JSON.parse(result.rawLog)[0]
+          .events.find(({ type }) => type === "store_code")
+          .attributes.find(({ key }) => key === "code_id").value
+      );
+      console.log(
+        `uploaded ${file}, codeID: ${codeId}, tx: ${result.transactionHash}`
+      );
 
-    // Instantiate contracts.
-
-    async function instantiate(code_id: number, inst_msg: any, label: string) {
-        const instMsgBinary = toBinary(inst_msg);
-        const instMsgBytes = fromBase64(instMsgBinary);
-
-        // see /sdk/vaa/governance.go
-        const codeIdBuf = Buffer.alloc(8);
-        codeIdBuf.writeBigInt64BE(BigInt(code_id));
-        const codeIdHash = keccak256(codeIdBuf);
-        const codeIdLabelHash = keccak256(
-            Buffer.concat([
-                Buffer.from(codeIdHash, "hex"),
-                Buffer.from(label, "utf8"),
-            ])
-        );
-        const fullHash = keccak256(
-            Buffer.concat([Buffer.from(codeIdLabelHash, "hex"), instMsgBytes])
-        );
-
-        console.log(fullHash);
-
-        let vaa: VAA<Other> = {
-            version: 1,
-            guardianSetIndex: 0,
-            signatures: [],
-            timestamp: 0,
-            nonce: 0,
-            emitterChain: GOVERNANCE_CHAIN,
-            emitterAddress: GOVERNANCE_EMITTER,
-            sequence: BigInt(Math.floor(Math.random() * 100000000)),
-            consistencyLevel: 0,
-            payload: {
-                type: "Other",
-                hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65020${CHAIN_ID_WORMCHAIN.toString(
-                    16
-                )}${fullHash}`,
-            },
-        };
-        // TODO: check for number of guardians in set and use the corresponding keys
-        vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA<Payload>);
-        const msg = client.core.msgInstantiateContract({
-            signer,
-            code_id,
-            label,
-            msg: instMsgBytes,
-            vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA<Payload>)),
-        });
-        const result = await client.signAndBroadcast(signer, [msg], {
-            ...ZERO_FEE,
-            gas: "10000000",
-        });
-        const addr = JSON.parse(result.rawLog)[0]
-            .events.find(({ type }) => type === "instantiate")
-            .attributes.find(({ key }) => key === "_contract_address").value;
-        console.log(
-            `deployed contract ${label}, codeID: ${code_id}, address: ${addr}, txHash: ${result.transactionHash}`
-        );
-
-        return addr;
-    }
+      accum[file] = codeId;
+      return accum;
+    },
+    Object()
+  );
 
-    // Instantiate contracts.
-    // NOTE: Only append at the end, the ordering must be deterministic.
-
-    const addresses: {
-        [contractName: string]: string;
-    } = {};
-
-    const registrations: { [chainName: string]: string } = {
-        // keys are only used for logging success/failure
-        solana: String(process.env.REGISTER_SOL_TOKEN_BRIDGE_VAA),
-        ethereum: String(process.env.REGISTER_ETH_TOKEN_BRIDGE_VAA),
-        bsc: String(process.env.REGISTER_BSC_TOKEN_BRIDGE_VAA),
-        algo: String(process.env.REGISTER_ALGO_TOKEN_BRIDGE_VAA),
-        terra: String(process.env.REGISTER_TERRA_TOKEN_BRIDGE_VAA),
-        near: String(process.env.REGISTER_NEAR_TOKEN_BRIDGE_VAA),
-        terra2: String(process.env.REGISTER_TERRA2_TOKEN_BRIDGE_VAA),
-        aptos: String(process.env.REGISTER_APTOS_TOKEN_BRIDGE_VAA),
-        sui: String(process.env.REGISTER_SUI_TOKEN_BRIDGE_VAA),
-    };
+  // Instantiate contracts.
 
-    const instantiateMsg = {};
-    addresses["global_accountant.wasm"] = await instantiate(
-        codeIds["global_accountant.wasm"],
-        instantiateMsg,
-        "wormchainAccounting"
-    );
-    console.log("instantiated accounting: ", addresses["global_accountant.wasm"]);
+  async function instantiate(code_id: number, inst_msg: any, label: string) {
+    const instMsgBinary = toBinary(inst_msg);
+    const instMsgBytes = fromBase64(instMsgBinary);
 
-    const accountingRegistrations = Object.values(registrations).map((r) =>
-        Buffer.from(r, "hex").toString("base64")
+    // see /sdk/vaa/governance.go
+    const codeIdBuf = Buffer.alloc(8);
+    codeIdBuf.writeBigInt64BE(BigInt(code_id));
+    const codeIdHash = keccak256(codeIdBuf);
+    const codeIdLabelHash = keccak256(
+      Buffer.concat([
+        Buffer.from(codeIdHash, "hex"),
+        Buffer.from(label, "utf8"),
+      ])
     );
-    const msg = client.wasm.msgExecuteContract({
-        sender: signer,
-        contract: addresses["global_accountant.wasm"],
-        msg: toUtf8(
-            JSON.stringify({
-                submit_vaas: {
-                    vaas: accountingRegistrations,
-                },
-            })
-        ),
-        funds: [],
+    const fullHash = keccak256(
+      Buffer.concat([Buffer.from(codeIdLabelHash, "hex"), instMsgBytes])
+    );
+
+    console.log(fullHash);
+
+    let vaa: VAA<Other> = {
+      version: 1,
+      guardianSetIndex: 0,
+      signatures: [],
+      timestamp: 0,
+      nonce: 0,
+      emitterChain: GOVERNANCE_CHAIN,
+      emitterAddress: GOVERNANCE_EMITTER,
+      sequence: BigInt(Math.floor(Math.random() * 100000000)),
+      consistencyLevel: 0,
+      payload: {
+        type: "Other",
+        hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65020${CHAIN_ID_WORMCHAIN.toString(
+          16
+        )}${fullHash}`,
+      },
+    };
+    // TODO: check for number of guardians in set and use the corresponding keys
+    vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA<Payload>);
+    const msg = client.core.msgInstantiateContract({
+      signer,
+      code_id,
+      label,
+      msg: instMsgBytes,
+      vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA<Payload>)),
     });
-    const res = await client.signAndBroadcast(signer, [msg], {
-        ...ZERO_FEE,
-        gas: "10000000",
+    const result = await client.signAndBroadcast(signer, [msg], {
+      ...ZERO_FEE,
+      gas: "10000000",
     });
-    console.log(`sent accounting chain registrations, tx: `, res.transactionHash);
+    console.log("contract instantiation msg: ", msg);
+    console.log("contract instantiation result: ", result);
+    const addr = JSON.parse(result.rawLog)[0]
+      .events.find(({ type }) => type === "instantiate")
+      .attributes.find(({ key }) => key === "_contract_address").value;
+    console.log(
+      `deployed contract ${label}, codeID: ${code_id}, address: ${addr}, txHash: ${result.transactionHash}`
+    );
+
+    return addr;
+  }
+
+  // Instantiate contracts.
+  // NOTE: Only append at the end, the ordering must be deterministic.
+
+  const addresses: {
+    [contractName: string]: string;
+  } = {};
+
+  const registrations: { [chainName: string]: string } = {
+    // keys are only used for logging success/failure
+    solana: String(process.env.REGISTER_SOL_TOKEN_BRIDGE_VAA),
+    ethereum: String(process.env.REGISTER_ETH_TOKEN_BRIDGE_VAA),
+    bsc: String(process.env.REGISTER_BSC_TOKEN_BRIDGE_VAA),
+    algo: String(process.env.REGISTER_ALGO_TOKEN_BRIDGE_VAA),
+    terra: String(process.env.REGISTER_TERRA_TOKEN_BRIDGE_VAA),
+    near: String(process.env.REGISTER_NEAR_TOKEN_BRIDGE_VAA),
+    terra2: String(process.env.REGISTER_TERRA2_TOKEN_BRIDGE_VAA),
+    aptos: String(process.env.REGISTER_APTOS_TOKEN_BRIDGE_VAA),
+    sui: String(process.env.REGISTER_SUI_TOKEN_BRIDGE_VAA),
+  };
+
+  const instantiateMsg = {};
+  addresses["global_accountant.wasm"] = await instantiate(
+    codeIds["global_accountant.wasm"],
+    instantiateMsg,
+    "wormchainAccounting"
+  );
+  console.log("instantiated accounting: ", addresses["global_accountant.wasm"]);
+
+  const accountingRegistrations = Object.values(registrations).map((r) =>
+    Buffer.from(r, "hex").toString("base64")
+  );
+  const msg = client.wasm.msgExecuteContract({
+    sender: signer,
+    contract: addresses["global_accountant.wasm"],
+    msg: toUtf8(
+      JSON.stringify({
+        submit_vaas: {
+          vaas: accountingRegistrations,
+        },
+      })
+    ),
+    funds: [],
+  });
+  const res = await client.signAndBroadcast(signer, [msg], {
+    ...ZERO_FEE,
+    gas: "10000000",
+  });
+  console.log(`sent accounting chain registrations, tx: `, res.transactionHash);
+
+  const wormchainIbcReceiverInstantiateMsg = {};
+  addresses["wormchain_ibc_receiver.wasm"] = await instantiate(
+    codeIds["wormchain_ibc_receiver.wasm"],
+    wormchainIbcReceiverInstantiateMsg,
+    "wormchainIbcReceiver"
+  );
+  console.log(
+    "instantiated wormchain ibc receiver contract: ",
+    addresses["wormchain_ibc_receiver.wasm"]
+  );
+
+  // Generated VAA using
+  // `guardiand template ibc-receiver-update-channel-chain --channel-id channel-0 --chain-id 32 --target-chain-id 3104 > wormchain.prototxt`
+  // `guardiand admin governance-vaa-verify wormchain.prototxt`
+  let wormchainIbcReceiverWhitelistVaa: VAA<Other> = {
+    version: 1,
+    guardianSetIndex: 0,
+    signatures: [],
+    timestamp: 0,
+    nonce: 0,
+    emitterChain: GOVERNANCE_CHAIN,
+    emitterAddress: GOVERNANCE_EMITTER,
+    sequence: BigInt(Math.floor(Math.random() * 100000000)),
+    consistencyLevel: 0,
+    payload: {
+      type: "Other",
+      hex: `0000000000000000000000000000000000000000004962635265636569766572010c20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006368616e6e656c2d300020`,
+    },
+  };
+  wormchainIbcReceiverWhitelistVaa.signatures = sign(
+    VAA_SIGNERS,
+    wormchainIbcReceiverWhitelistVaa as unknown as VAA<Payload>
+  );
+  const wormchainIbcReceiverUpdateWhitelistMsg = {
+    submit_update_channel_chain: {
+      vaas: [
+        Buffer.from(
+          serialiseVAA(
+            wormchainIbcReceiverWhitelistVaa as unknown as VAA<Payload>
+          ),
+          "hex"
+        ).toString("base64"),
+      ],
+    },
+  };
+  const executeMsg = client.wasm.msgExecuteContract({
+    sender: signer,
+    contract: addresses["wormchain_ibc_receiver.wasm"],
+    msg: toUtf8(JSON.stringify(wormchainIbcReceiverUpdateWhitelistMsg)),
+    funds: [],
+  });
+  const updateIbcWhitelistRes = await client.signAndBroadcast(
+    signer,
+    [executeMsg],
+    {
+      ...ZERO_FEE,
+      gas: "10000000",
+    }
+  );
+  console.log(
+    "updated wormchain_ibc_receiver whitelist: ",
+    updateIbcWhitelistRes.transactionHash,
+    updateIbcWhitelistRes.code
+  );
 }
 
 try {
-    main();
+  main();
 } catch (e: any) {
-    if (e?.message) {
-        console.error(e.message);
-    }
-    throw e;
+  if (e?.message) {
+    console.error(e.message);
+  }
+  throw e;
 }

+ 4 - 4
wormchain/ts-sdk/package-lock.json

@@ -1,12 +1,12 @@
 {
-  "name": "wormhole-chain-sdk",
-  "version": "0.0.0",
+  "name": "@wormhole-foundation/wormchain-sdk",
+  "version": "0.0.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "name": "wormhole-chain-sdk",
-      "version": "0.0.0",
+      "name": "@wormhole-foundation/wormchain-sdk",
+      "version": "0.0.1",
       "license": "ISC",
       "dependencies": {
         "@certusone/wormhole-sdk": "^0.2.0",