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>
This commit is contained in:
parent
892274ffa4
commit
f6f93bf35e
12
Tiltfile
12
Tiltfile
|
@ -85,7 +85,7 @@ ci_tests = cfg.get("ci_tests", ci)
|
||||||
guardiand_debug = cfg.get("guardiand_debug", False)
|
guardiand_debug = cfg.get("guardiand_debug", False)
|
||||||
node_metrics = cfg.get("node_metrics", False)
|
node_metrics = cfg.get("node_metrics", False)
|
||||||
guardiand_governor = cfg.get("guardiand_governor", 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)
|
btc = cfg.get("btc", False)
|
||||||
|
|
||||||
if cfg.get("manual", False):
|
if cfg.get("manual", False):
|
||||||
|
@ -287,7 +287,13 @@ def build_node_yaml():
|
||||||
"--accountantContract",
|
"--accountantContract",
|
||||||
"wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465",
|
"wormhole14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9srrg465",
|
||||||
"--accountantCheckEnabled",
|
"--accountantCheckEnabled",
|
||||||
"true"
|
"true",
|
||||||
|
"--ibcWS",
|
||||||
|
"ws://wormchain:26657/websocket",
|
||||||
|
"--ibcLCD",
|
||||||
|
"http://wormchain:1317",
|
||||||
|
"--ibcContract",
|
||||||
|
"wormhole1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq0kdhcj"
|
||||||
]
|
]
|
||||||
|
|
||||||
return encode_yaml_stream(node_yaml_with_replicas)
|
return encode_yaml_stream(node_yaml_with_replicas)
|
||||||
|
@ -854,7 +860,7 @@ if ibc_relayer:
|
||||||
port_forwards = [
|
port_forwards = [
|
||||||
port_forward(7597, name = "HTTPDEBUG [:7597]", host = webHost),
|
port_forward(7597, name = "HTTPDEBUG [:7597]", host = webHost),
|
||||||
],
|
],
|
||||||
resource_deps = ["wormchain", "terra2-terrad"],
|
resource_deps = ["wormchain-deploy", "terra2-terrad"],
|
||||||
labels = ["ibc-relayer"],
|
labels = ["ibc-relayer"],
|
||||||
trigger_mode = trigger_mode,
|
trigger_mode = trigger_mode,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.message",
|
||||||
"message.sender",
|
"message.sender",
|
||||||
"message.chain_id",
|
"message.chain_id",
|
||||||
"message.nonce",
|
"message.nonce",
|
||||||
"message.sequence",
|
"message.sequence",
|
||||||
"message.block_time",
|
"message.block_time",
|
||||||
"message.tx_index",
|
|
||||||
"message.block_height",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fn receive_publish(
|
fn receive_publish(
|
||||||
|
|
|
@ -148,22 +148,8 @@ fn post_message_ibc(
|
||||||
// compute the packet timeout
|
// compute the packet timeout
|
||||||
let packet_timeout = env.block.time.plus_seconds(PACKET_LIFETIME).into();
|
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
|
// actually execute the postMessage call on the core contract
|
||||||
let mut res = core_execute(deps, env, info, msg).context("wormhole core execution failed")?;
|
let 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);
|
|
||||||
|
|
||||||
// Send the result attributes over IBC on this channel
|
// Send the result attributes over IBC on this channel
|
||||||
let packet = WormholeIbcPacketMsg::Publish {
|
let packet = WormholeIbcPacketMsg::Publish {
|
||||||
|
|
|
@ -9,6 +9,12 @@ import { readFileSync, readdirSync } from "fs";
|
||||||
import { Bech32, toHex } from "@cosmjs/encoding";
|
import { Bech32, toHex } from "@cosmjs/encoding";
|
||||||
import { zeroPad } from "ethers/lib/utils.js";
|
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
|
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
|
contracts must be imported in a deterministic order so their addresses remain
|
||||||
|
@ -19,11 +25,6 @@ const artifacts = [
|
||||||
"cw_token_bridge.wasm",
|
"cw_token_bridge.wasm",
|
||||||
"cw20_wrapped_2.wasm",
|
"cw20_wrapped_2.wasm",
|
||||||
"cw20_base.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",
|
"wormhole_ibc.wasm",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -50,18 +51,6 @@ if (missing_artifacts.length) {
|
||||||
process.exit(1);
|
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 */
|
/* Set up terra client & wallet */
|
||||||
|
|
||||||
const terra = new LCDClient({
|
const terra = new LCDClient({
|
||||||
|
@ -208,6 +197,28 @@ addresses["mock.wasm"] = await instantiate(
|
||||||
"mock"
|
"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 */
|
/* Registrations: tell the bridge contracts to know about each other */
|
||||||
|
|
||||||
const contract_registrations = {
|
const contract_registrations = {
|
||||||
|
@ -253,10 +264,49 @@ for (const [contract, registrations] of Object.entries(
|
||||||
memo: "",
|
memo: "",
|
||||||
})
|
})
|
||||||
.then((tx) => terra.tx.broadcast(tx))
|
.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
|
// Terra addresses are "human-readable", but for cross-chain registrations, we
|
||||||
// want the "canonical" version
|
// want the "canonical" version
|
||||||
function convert_terra_address_to_hex(human_addr) {
|
function convert_terra_address_to_hex(human_addr) {
|
||||||
|
|
|
@ -37,7 +37,14 @@ spec:
|
||||||
- link-then-start
|
- link-then-start
|
||||||
- terra-wormchain
|
- terra-wormchain
|
||||||
- --debug-addr
|
- --debug-addr
|
||||||
- localhost:7597
|
- 0.0.0.0:7597
|
||||||
|
- --src-port
|
||||||
|
- wasm.terra1436kxs0w2es6xlqpp9rd35e3d0cjnw4sv8j3a7483sgks29jqwgsnyey7t
|
||||||
|
- --dst-port
|
||||||
|
- wasm.wormhole1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq0kdhcj
|
||||||
|
- --version
|
||||||
|
- ibc-wormhole-v1
|
||||||
|
- --override
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 7597
|
- containerPort: 7597
|
||||||
name: rest
|
name: rest
|
||||||
|
@ -48,4 +55,4 @@ spec:
|
||||||
path: /
|
path: /
|
||||||
periodSeconds: 1
|
periodSeconds: 1
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceName: ibc-relayer
|
serviceName: ibc-relayer
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,4 +2,5 @@
|
||||||
set -e
|
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}'' 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}'' 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
|
CI=true npm --prefix ../sdk/js run test-ci
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import {
|
import {
|
||||||
CHAIN_ID_WORMCHAIN,
|
CHAIN_ID_WORMCHAIN,
|
||||||
hexToUint8Array,
|
hexToUint8Array,
|
||||||
Other,
|
Other,
|
||||||
Payload,
|
Payload,
|
||||||
serialiseVAA,
|
serialiseVAA,
|
||||||
sign,
|
sign,
|
||||||
VAA,
|
VAA,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { toBinary } from "@cosmjs/cosmwasm-stargate";
|
import { toBinary } from "@cosmjs/cosmwasm-stargate";
|
||||||
import { fromBase64, toUtf8 } from "@cosmjs/encoding";
|
import { fromBase64, toUtf8, fromBech32 } from "@cosmjs/encoding";
|
||||||
import {
|
import {
|
||||||
getWallet,
|
getWallet,
|
||||||
getWormchainSigningClient,
|
getWormchainSigningClient,
|
||||||
} from "@wormhole-foundation/wormchain-sdk";
|
} from "@wormhole-foundation/wormchain-sdk";
|
||||||
import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts";
|
import { ZERO_FEE } from "@wormhole-foundation/wormchain-sdk/lib/core/consts";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
@ -23,9 +23,9 @@ import * as util from "util";
|
||||||
import * as devnetConsts from "./devnet-consts.json";
|
import * as devnetConsts from "./devnet-consts.json";
|
||||||
|
|
||||||
if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") {
|
if (process.env.INIT_SIGNERS_KEYS_CSV === "undefined") {
|
||||||
let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`;
|
let msg = `.env is missing. run "make contracts-tools-deps" to fetch.`;
|
||||||
console.error(msg);
|
console.error(msg);
|
||||||
throw msg;
|
throw msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(",");
|
const VAA_SIGNERS = process.env.INIT_SIGNERS_KEYS_CSV.split(",");
|
||||||
|
@ -40,236 +40,306 @@ const readFileAsync = util.promisify(fs.readFile);
|
||||||
deterministic.
|
deterministic.
|
||||||
*/
|
*/
|
||||||
type ContractName = string;
|
type ContractName = string;
|
||||||
const artifacts: ContractName[] = ["global_accountant.wasm"];
|
const artifacts: ContractName[] = [
|
||||||
|
"global_accountant.wasm",
|
||||||
|
"wormchain_ibc_receiver.wasm",
|
||||||
|
];
|
||||||
|
|
||||||
const ARTIFACTS_PATH = "../artifacts/";
|
const ARTIFACTS_PATH = "../artifacts/";
|
||||||
/* Check that the artifact folder contains all the wasm files we expect and nothing else */
|
/* Check that the artifact folder contains all the wasm files we expect and nothing else */
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) =>
|
const actual_artifacts = readdirSync(ARTIFACTS_PATH).filter((a) =>
|
||||||
a.endsWith(".wasm")
|
a.endsWith(".wasm")
|
||||||
);
|
);
|
||||||
|
|
||||||
const missing_artifacts = artifacts.filter(
|
const missing_artifacts = artifacts.filter(
|
||||||
(a) => !actual_artifacts.includes(a)
|
(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) {
|
missing_artifacts.forEach((file) => console.log(` - ${file}`));
|
||||||
console.log(
|
console.log(
|
||||||
"Error during wormchain deployment. The following files are expected to be in the artifacts folder:"
|
"Hint: the deploy script needs to run after the contracts have been built."
|
||||||
);
|
);
|
||||||
missing_artifacts.forEach((file) => console.log(` - ${file}`));
|
console.log(
|
||||||
console.log(
|
"External binary blobs need to be manually added in tools/Dockerfile."
|
||||||
"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);
|
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() {
|
async function main() {
|
||||||
/* Set up cosmos client & wallet */
|
/* Set up cosmos client & wallet */
|
||||||
|
|
||||||
let host = devnetConsts.chains[3104].tendermintUrlLocal;
|
let host = devnetConsts.chains[3104].tendermintUrlLocal;
|
||||||
if (os.hostname().includes("wormchain-deploy")) {
|
if (os.hostname().includes("wormchain-deploy")) {
|
||||||
// running in tilt devnet
|
// running in tilt devnet
|
||||||
host = devnetConsts.chains[3104].tendermintUrlTilt;
|
host = devnetConsts.chains[3104].tendermintUrlTilt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mnemonic =
|
const mnemonic =
|
||||||
devnetConsts.chains[3104].accounts.wormchainNodeOfGuardian0.mnemonic;
|
devnetConsts.chains[3104].accounts.wormchainNodeOfGuardian0.mnemonic;
|
||||||
|
|
||||||
const wallet = await getWallet(mnemonic);
|
const wallet = await getWallet(mnemonic);
|
||||||
const client = await getWormchainSigningClient(host, wallet);
|
const client = await getWormchainSigningClient(host, wallet);
|
||||||
|
|
||||||
// there are several Cosmos chains in devnet, so check the config is as expected
|
// there are several Cosmos chains in devnet, so check the config is as expected
|
||||||
let id = await client.getChainId();
|
let id = await client.getChainId();
|
||||||
if (id !== "wormchain") {
|
if (id !== "wormchain") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`
|
`Wormchain CosmWasmClient connection produced an unexpected chainID: ${id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signers = await wallet.getAccounts();
|
const signers = await wallet.getAccounts();
|
||||||
const signer = signers[0].address;
|
const signer = signers[0].address;
|
||||||
console.log("wormchain contract deployer is: ", signer);
|
console.log("wormchain contract deployer is: ", signer);
|
||||||
|
|
||||||
/* Deploy artifacts */
|
/* Deploy artifacts */
|
||||||
|
|
||||||
const codeIds: { [name: ContractName]: number } = await artifacts.reduce(
|
const codeIds: { [name: ContractName]: number } = await artifacts.reduce(
|
||||||
async (prev, file) => {
|
async (prev, file) => {
|
||||||
// wait for the previous to finish, to avoid the race condition of wallet sequence mismatch.
|
// wait for the previous to finish, to avoid the race condition of wallet sequence mismatch.
|
||||||
const accum = await prev;
|
const accum = await prev;
|
||||||
|
|
||||||
const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`);
|
const contract_bytes = await readFileAsync(`${ARTIFACTS_PATH}${file}`);
|
||||||
|
|
||||||
const payload = keccak256(contract_bytes);
|
const payload = keccak256(contract_bytes);
|
||||||
let vaa: VAA<Other> = {
|
let vaa: VAA<Other> = {
|
||||||
version: 1,
|
version: 1,
|
||||||
guardianSetIndex: 0,
|
guardianSetIndex: 0,
|
||||||
signatures: [],
|
signatures: [],
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
emitterChain: GOVERNANCE_CHAIN,
|
emitterChain: GOVERNANCE_CHAIN,
|
||||||
emitterAddress: GOVERNANCE_EMITTER,
|
emitterAddress: GOVERNANCE_EMITTER,
|
||||||
sequence: BigInt(Math.floor(Math.random() * 100000000)),
|
sequence: BigInt(Math.floor(Math.random() * 100000000)),
|
||||||
consistencyLevel: 0,
|
consistencyLevel: 0,
|
||||||
payload: {
|
payload: {
|
||||||
type: "Other",
|
type: "Other",
|
||||||
hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString(
|
hex: `0000000000000000000000000000000000000000005761736D644D6F64756C65010${CHAIN_ID_WORMCHAIN.toString(
|
||||||
16
|
16
|
||||||
)}${payload}`,
|
)}${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;
|
|
||||||
},
|
},
|
||||||
Object()
|
};
|
||||||
);
|
vaa.signatures = sign(VAA_SIGNERS, vaa as unknown as VAA<Payload>);
|
||||||
|
console.log("uploading", file);
|
||||||
// Instantiate contracts.
|
const msg = client.core.msgStoreCode({
|
||||||
|
signer,
|
||||||
async function instantiate(code_id: number, inst_msg: any, label: string) {
|
wasm_byte_code: new Uint8Array(contract_bytes),
|
||||||
const instMsgBinary = toBinary(inst_msg);
|
vaa: hexToUint8Array(serialiseVAA(vaa as unknown as VAA<Payload>)),
|
||||||
const instMsgBytes = fromBase64(instMsgBinary);
|
});
|
||||||
|
const result = await client.signAndBroadcast(signer, [msg], {
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
...ZERO_FEE,
|
||||||
gas: "10000000",
|
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;
|
||||||
|
},
|
||||||
|
Object()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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>)),
|
||||||
});
|
});
|
||||||
console.log(`sent accounting chain registrations, tx: `, res.transactionHash);
|
const result = await client.signAndBroadcast(signer, [msg], {
|
||||||
|
...ZERO_FEE,
|
||||||
|
gas: "10000000",
|
||||||
|
});
|
||||||
|
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 {
|
try {
|
||||||
main();
|
main();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.message) {
|
if (e?.message) {
|
||||||
console.error(e.message);
|
console.error(e.message);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "wormhole-chain-sdk",
|
"name": "@wormhole-foundation/wormchain-sdk",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wormhole-chain-sdk",
|
"name": "@wormhole-foundation/wormchain-sdk",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certusone/wormhole-sdk": "^0.2.0",
|
"@certusone/wormhole-sdk": "^0.2.0",
|
||||||
|
|
Loading…
Reference in New Issue