From b354e68d14301ef8128820f592c71df45987fc79 Mon Sep 17 00:00:00 2001 From: ByeongSu Hong Date: Fri, 8 Mar 2024 13:02:29 +0900 Subject: [PATCH] feat(script): add commands & entrypoint (#105) * feat: add shared utilities * fix: correct representation of `instant finality` * fix: also actual value * Squashed commit of the following: commit fad8f9e59a9de9b25028fab89c6c851d50d0dd7d Merge: 1639e2f eb77beb Author: byeongsu-hong Date: Fri Mar 1 23:21:15 2024 +0900 Merge branch 'eddy/pick-docs' into eddy/pick-cw-hpl commit 1639e2fd0983c9bdc3b9814b7c8b314bd803a7d7 Author: byeongsu-hong Date: Fri Mar 1 23:17:08 2024 +0900 remove ts/sdk commit d73647b4b073e1f142b13fd44da723cf7dcba153 Author: byeongsu-hong Date: Fri Mar 1 23:15:19 2024 +0900 Revert "Merge branch 'main' into eddy/pick-cw-hpl" This reverts commit 82fc10fc57ccf868ae172944c34974fa8917a48c, reversing changes made to 6566e7ad0ee50198c46a463369d7dfececb9865f. commit 82fc10fc57ccf868ae172944c34974fa8917a48c Merge: 6566e7a be64967 Author: byeongsu-hong Date: Fri Mar 1 23:14:05 2024 +0900 Merge branch 'main' into eddy/pick-cw-hpl commit be64967876be5e8b0de21d07bd3544ef48afdf41 Author: ByeongSu Hong Date: Fri Mar 1 23:08:13 2024 +0900 doc: documentation (#95) * wip * project structure & overview * toc * swap * fix(schema): reflect missing contracts * refactor(ts): migrate sdk - 1 * refactor(script): pull back project setup to root * reorg script / cli * fix(script): make igp deployable * build: redeploy * build: stride hyperlane deployment * fix(hook): default gas denom * feat(script): add migrate command * chore: detailed ignore policy * feat(script): supprot mnemonic * chore(script): cleanup * chore: trailing comma * feat: add grpc endpoint as config * feat: generate agent config * feat(script): add test-dispatch * chore: redundant args * docs: owner -> * build: yarn berry * env: yarn * env: add cw-hpl command * feat: mailbox null-check * fix: prune imports * docs: wip guide & example * fix: rename compose file * fix: use localosmosis key * refactor: restruct guide * fix: remove DATA_PATH * fix: add `yarn install` * fix: handle rest endpoint not working * fix: use localwasmd * fix: hrp * feat: add wallet command * feat: wallet generator * fix: use osmosis testnet * line spacing * fix: xor * fix: split length * feat: more info to replace * fix: osmosis testnet network name * chore: ignore example * feat: add test recipient * fix: trouble shooting * docs: done * docs: update README.md * fix: line break * chore: remove testnet contexts * wip: neutron deployment * feat: context -> agent config * feat: apply review changes (#97) * docs: add context / example * fix: real instant finality * fix: typo commit e8716c39d1ef82f8b641736acb73b62c74bd2990 Author: ByeongSu Hong Date: Fri Mar 1 19:57:30 2024 +0900 review: docs (#98) * wip * project structure & overview * toc * swap * docs: owner -> * build: yarn berry * env: yarn * env: add cw-hpl command * docs: wip guide & example * fix: line break * fix: rename compose file * fix: use localosmosis key * refactor: restruct guide * fix: remove DATA_PATH * fix: add `yarn install` * fix: use localwasmd * fix: hrp * fix: use osmosis testnet * line spacing * feat: more info to replace * fix: osmosis testnet network name * chore: ignore example * docs: done * docs: update README.md * merge: remove conflicts * docs: move to root * docs: remove outdated docs commit eb77bebd6b33d514be379f40af8cffedf0e1369b Author: byeongsu-hong Date: Thu Feb 29 17:55:52 2024 +0900 docs: remove outdated docs commit 6a600c52dde686493a1c64e6c7b6d34ba4e382c6 Author: byeongsu-hong Date: Thu Feb 29 17:54:38 2024 +0900 docs: move to root commit 68074f013565e92d4e596f93cb2eb52eac5551db Author: ByeongSu Hong Date: Thu Feb 29 02:20:50 2024 +0900 refactor: ts schema (#99) * fix(schema): reflect missing contracts * refactor(ts): migrate sdk - 1 commit 5896800b212821e16411e25d0c618146f62b7d4c Author: byeongsu-hong Date: Wed Feb 28 17:16:32 2024 +0900 merge: remove conflicts commit adb18525cf60addcb6760d6950ed4e138d410408 Author: ByeongSu Hong Date: Tue Feb 27 02:59:10 2024 +0900 docs: update README.md commit 19c6c60101b89e5be194a12bd3f007fddbd3a6e0 Author: byeongsu-hong Date: Tue Feb 27 02:57:59 2024 +0900 docs: done commit 5cdecaa6ae19ee1e80579774180e4b2fdd1a9f94 Author: byeongsu-hong Date: Tue Feb 27 02:32:56 2024 +0900 chore: ignore example commit 28a5e6917cb63f7b896fc254cf35a3651e5d63d6 Author: byeongsu-hong Date: Tue Feb 27 02:21:21 2024 +0900 fix: osmosis testnet network name commit 57e78ea2e0c6bffd27f7d971304056a7db98f232 Author: byeongsu-hong Date: Tue Feb 27 02:18:23 2024 +0900 feat: more info to replace commit 56e2700914607485f23cf7105dd6b6aa5ba73369 Author: byeongsu-hong Date: Tue Feb 27 01:35:03 2024 +0900 line spacing commit b707b6748bdfa7ac5c7443e90d6e84f40504b987 Author: byeongsu-hong Date: Tue Feb 27 01:32:35 2024 +0900 fix: use osmosis testnet commit aa53d70dbf7a82cf88105aa2721b7e66713f8bf4 Author: byeongsu-hong Date: Tue Feb 27 00:38:22 2024 +0900 fix: hrp commit bc1b84f78f4078c3cf515d455d1ebe38459680aa Author: byeongsu-hong Date: Tue Feb 27 00:36:03 2024 +0900 fix: use localwasmd commit 7aaa751f71c6042c25d1ecc536e85995ac39a0d0 Author: byeongsu-hong Date: Tue Feb 27 00:04:08 2024 +0900 fix: add `yarn install` commit ca25459e023c95eb20a2f76d83ce0d510bd88b0c Author: byeongsu-hong Date: Mon Feb 26 23:56:19 2024 +0900 fix: remove DATA_PATH commit 82b5f59f46054381c0331b5c074a49de8f07d254 Author: byeongsu-hong Date: Mon Feb 26 23:49:25 2024 +0900 refactor: restruct guide commit df1a67cc1a68609254f299b0883149f27f623cff Author: byeongsu-hong Date: Mon Feb 26 22:32:48 2024 +0900 fix: use localosmosis key commit ef171651e991faabb5b1312c12e747049cf79189 Author: byeongsu-hong Date: Mon Feb 26 22:26:51 2024 +0900 fix: rename compose file commit 1289770ac6960a9d6db41ad12426a93565cdd9e1 Author: byeongsu-hong Date: Tue Feb 27 03:00:17 2024 +0900 fix: line break commit 2604486e096413bd846dd11e006c490c7f4e0b08 Author: byeongsu-hong Date: Mon Feb 26 22:22:49 2024 +0900 docs: wip guide & example commit 2b4c5a1a5072841c2cc1b0eed30e0f6a9e8e9024 Author: byeongsu-hong Date: Mon Feb 26 21:21:24 2024 +0900 env: add cw-hpl command commit e43cfb33cb19b1a5956111d3e5bbe662b019b8f0 Author: byeongsu-hong Date: Mon Feb 26 21:21:14 2024 +0900 env: yarn commit 76313e89fdca069d274d094d9fa4a32a6b1344a4 Author: byeongsu-hong Date: Mon Feb 26 20:49:06 2024 +0900 build: yarn berry commit 9dba0a4d93f447092326fa0f0db6f62b702d8ec5 Author: byeongsu-hong Date: Mon Feb 26 20:44:20 2024 +0900 docs: owner -> commit 17200dc65d2f18c43dd98afba53c9dfb4de20782 Author: byeongsu-hong Date: Wed Feb 14 18:50:16 2024 +0900 swap commit 50b01d70caf45028cde7c80a2f46e0e0884c69e1 Author: byeongsu-hong Date: Wed Feb 14 18:42:09 2024 +0900 toc commit 6cde4c8da4bf937cf4ad15d0138cb9ce6c1f3c2e Author: byeongsu-hong Date: Wed Feb 14 18:29:22 2024 +0900 project structure & overview commit cab7199fb97355bdc2f333e56d0c7e47ba635cc1 Author: byeongsu-hong Date: Wed Feb 14 18:22:41 2024 +0900 wip * merge: cw-hpl (#110) * refactor: ts schema (#99) * fix(schema): reflect missing contracts * refactor(ts): migrate sdk - 1 * review: docs (#98) * wip * project structure & overview * toc * swap * docs: owner -> * build: yarn berry * env: yarn * env: add cw-hpl command * docs: wip guide & example * fix: line break * fix: rename compose file * fix: use localosmosis key * refactor: restruct guide * fix: remove DATA_PATH * fix: add `yarn install` * fix: use localwasmd * fix: hrp * fix: use osmosis testnet * line spacing * feat: more info to replace * fix: osmosis testnet network name * chore: ignore example * docs: done * docs: update README.md * merge: remove conflicts * docs: move to root * docs: remove outdated docs * doc: documentation (#95) * wip * project structure & overview * toc * swap * fix(schema): reflect missing contracts * refactor(ts): migrate sdk - 1 * refactor(script): pull back project setup to root * reorg script / cli * fix(script): make igp deployable * build: redeploy * build: stride hyperlane deployment * fix(hook): default gas denom * feat(script): add migrate command * chore: detailed ignore policy * feat(script): supprot mnemonic * chore(script): cleanup * chore: trailing comma * feat: add grpc endpoint as config * feat: generate agent config * feat(script): add test-dispatch * chore: redundant args * docs: owner -> * build: yarn berry * env: yarn * env: add cw-hpl command * feat: mailbox null-check * fix: prune imports * docs: wip guide & example * fix: rename compose file * fix: use localosmosis key * refactor: restruct guide * fix: remove DATA_PATH * fix: add `yarn install` * fix: handle rest endpoint not working * fix: use localwasmd * fix: hrp * feat: add wallet command * feat: wallet generator * fix: use osmosis testnet * line spacing * fix: xor * fix: split length * feat: more info to replace * fix: osmosis testnet network name * chore: ignore example * feat: add test recipient * fix: trouble shooting * docs: done * docs: update README.md * fix: line break * chore: remove testnet contexts * wip: neutron deployment * feat: context -> agent config * feat: apply review changes (#97) * docs: add context / example * fix: real instant finality * fix: typo * Revert "Merge branch 'main' into eddy/pick-cw-hpl" This reverts commit 82fc10fc57ccf868ae172944c34974fa8917a48c, reversing changes made to 6566e7ad0ee50198c46a463369d7dfececb9865f. * remove ts/sdk * merge: schema refactoring (#106) * fix(schema): reflect missing contracts * refactor(ts): migrate sdk - 1 * feat(script): initial config setup (#102) feat: initial config setup * merge: docs improvements (#108) * wip * project structure & overview * toc * swap * docs: owner -> * build: yarn berry * env: yarn * env: add cw-hpl command * docs: wip guide & example * fix: line break * fix: rename compose file * fix: use localosmosis key * refactor: restruct guide * fix: remove DATA_PATH * fix: add `yarn install` * fix: use localwasmd * fix: hrp * fix: use osmosis testnet * line spacing * feat: more info to replace * fix: osmosis testnet network name * chore: ignore example * docs: done * docs: update README.md * merge: remove conflicts * docs: move to root * docs: remove outdated docs * fix: apply code review * feat: add deploy script for hook * feat: add ism deployer * feat: add igp deployer * feat: export everything * feat: add commands * feat: main entrypoint * chore: prevent logic duplication * fix: apply review changes --- script/commands/context.ts | 25 +++++ script/commands/contract.ts | 44 ++++++++ script/commands/deploy.ts | 175 +++++++++++++++++++++++++++++ script/commands/index.ts | 4 + script/commands/migrate.ts | 168 ++++++++++++++++++++++++++++ script/commands/upload.ts | 216 ++++++++++++++++++++++++++++++++++++ script/commands/wallet.ts | 72 ++++++++++++ script/index.ts | 47 ++++++++ 8 files changed, 751 insertions(+) create mode 100644 script/commands/context.ts create mode 100644 script/commands/contract.ts create mode 100644 script/commands/deploy.ts create mode 100644 script/commands/index.ts create mode 100644 script/commands/migrate.ts create mode 100644 script/commands/upload.ts create mode 100644 script/commands/wallet.ts create mode 100644 script/index.ts diff --git a/script/commands/context.ts b/script/commands/context.ts new file mode 100644 index 00000000..0c7386e5 --- /dev/null +++ b/script/commands/context.ts @@ -0,0 +1,25 @@ +import { Command } from "commander"; + +import { CONTAINER, Dependencies } from "../shared/ioc"; +import { saveAgentConfig } from "../shared/agent"; +import { getNetwork } from "../shared/config"; + +const contextCmd = new Command("context"); + +contextCmd + .command("make-agent-config") + .description("Make an agent config") + .option("-o --output ", "The output directory") + .action(async (_, cmd) => { + const opts = cmd.optsWithGlobals(); + const { ctx } = CONTAINER.get(Dependencies); + const network = getNetwork(opts.networkId); + + await saveAgentConfig( + network, + ctx, + opts.output && { contextPath: opts.output } + ); + }); + +export { contextCmd }; diff --git a/script/commands/contract.ts b/script/commands/contract.ts new file mode 100644 index 00000000..8aeb4547 --- /dev/null +++ b/script/commands/contract.ts @@ -0,0 +1,44 @@ +import { Command } from "commander"; + +import { contractNames } from "../shared/constants"; +import { executeContract } from "../shared/contract"; +import { CONTAINER, Dependencies } from "../shared/ioc"; +import { addPad } from "../shared/utils"; +import { getNetwork } from "../shared/config"; + +export const contractCmd = new Command("contract").configureHelp({ + showGlobalOptions: true, +}); + +contractCmd.command("list").action(() => { + console.log("Available contracts:".green); + contractNames.forEach((v) => console.log("-", v)); +}); + +contractCmd + .command("test-dispatch") + .argument("dest-domian") + .argument("recipient-addr") + .argument("msg-body") + .action(async (destDomain, recipientAddr, msgBody, _, cmd) => { + const opts = cmd.optsWithGlobals(); + const { ctx, client } = CONTAINER.get(Dependencies); + const network = getNetwork(opts.networkId); + + const mailbox = ctx.deployments.core?.mailbox!; + + const res = await executeContract( + client, + mailbox, + { + dispatch: { + dest_domain: parseInt(destDomain), + recipient_addr: addPad(recipientAddr), + msg_body: Buffer.from(msgBody, "utf-8").toString("hex"), + }, + }, + [{ amount: "500", denom: network.gas.denom }] + ); + + console.log(res.hash); + }); diff --git a/script/commands/deploy.ts b/script/commands/deploy.ts new file mode 100644 index 00000000..ae5b061a --- /dev/null +++ b/script/commands/deploy.ts @@ -0,0 +1,175 @@ +import { Command } from "commander"; + +import { deployIsm, deployHook } from "../deploy"; +import { CONTAINER, Dependencies } from "../shared/ioc"; +import { Client, config, getNetwork } from "../shared/config"; +import { Context, ContextDeployments, saveContext } from "../shared/context"; +import { deployContract, executeMultiMsg } from "../shared/contract"; +import { saveAgentConfig } from "../shared/agent"; + +export const deployCmd = new Command("deploy") + .description("Deploy contracts") + .configureHelp({ showGlobalOptions: true }) + .action(handleDeploy); + +async function handleDeploy(_: any, cmd: any) { + const opts = cmd.optsWithGlobals(); + const { ctx, client } = CONTAINER.get(Dependencies); + + ctx.deployments = ctx.deployments || {}; + ctx.deployments.core = await deployCore(opts, ctx, client); + ctx.deployments.isms = await deployIsms(ctx, client); + ctx.deployments.hooks = await deployHooks(opts, ctx, client); + // TODO: deploy warp + ctx.deployments.test = await deployTest(opts, ctx, client); + + await executeMultiMsg(client, [ + { + contract: ctx.deployments.core?.mailbox!, + msg: { + set_default_ism: { + ism: ctx.deployments.isms?.address!, + }, + }, + }, + { + contract: ctx.deployments.core?.mailbox!, + msg: { + set_default_hook: { + hook: ctx.deployments.hooks?.default!.address, + }, + }, + }, + { + contract: ctx.deployments.core?.mailbox!, + msg: { + set_required_hook: { + hook: ctx.deployments.hooks?.required!.address, + }, + }, + }, + ]); + + saveContext(opts.networkId, ctx); + await saveAgentConfig(getNetwork(opts.networkId), ctx); +} + +const deployCore = async ( + { networkId }: { networkId: string }, + ctx: Context, + client: Client +): Promise => { + const { hrp, domain } = getNetwork(networkId); + + const log = (v: string) => console.log("[core]".green, v); + const preload = ctx.deployments.core; + const deployment = preload || {}; + + if (preload?.mailbox) { + log(`${preload.mailbox.type} already deployed`); + deployment.mailbox = preload.mailbox; + } else { + deployment.mailbox = await deployContract(ctx, client, "hpl_mailbox", { + hrp, + domain, + owner: client.signer, + }); + } + + deployment.validator_announce = + preload?.validator_announce || + (await deployContract(ctx, client, "hpl_validator_announce", { + hrp, + mailbox: deployment.mailbox.address, + })); + if (preload?.validator_announce) + log(`${deployment.validator_announce.type} already deployed`); + + return deployment; +}; + +const deployIsms = async ( + ctx: Context, + client: Client +): Promise => { + if (!config.deploy.ism) { + throw new Error("ISM deployment config not found"); + } + + const log = (v: string) => console.log("[ism]".green, v); + const preload = ctx.deployments.isms; + if (preload) { + log(`ism ${preload.type} already deployed`); + return preload; + } + + return deployIsm(ctx, client, config.deploy.ism); +}; + +const deployHooks = async ( + { networkId }: { networkId: string }, + ctx: Context, + client: Client, +): Promise => { + if (!config.deploy.hooks) { + throw new Error("Hook deployment config not found"); + } + + const log = (v: string) => console.log("[hooks]".green, v); + const preload = ctx.deployments?.hooks; + const deployment = preload || {}; + + if (preload?.default) { + log(`default hook ${preload.default.type} already deployed`); + deployment.default = preload.default; + } else { + if (!config.deploy.hooks.default) + throw Error("Default hook deployment config not found"); + + deployment.default = await deployHook( + networkId, + ctx, + client, + config.deploy.hooks.default, + ); + } + + if (preload?.required) { + log(`required hook ${preload.required.type} already deployed`); + deployment.required = preload.required; + } else { + if (!config.deploy.hooks.required) + throw Error("Required hook deployment config not found"); + + deployment.required = await deployHook( + networkId, + ctx, + client, + config.deploy.hooks.required, + ); + } + + return deployment; +}; + +const deployTest = async ( + { networkId }: { networkId: string }, + ctx: Context, + client: Client +): Promise => { + const { hrp } = getNetwork(networkId); + + const log = (v: string) => console.log("[test]".green, v); + const preload = ctx.deployments.test; + const deployment = preload || {}; + + deployment.msg_receiver = + preload?.msg_receiver || + (await deployContract(ctx, client, "hpl_test_mock_msg_receiver", { + hrp, + })); + if (preload?.msg_receiver) + log(`${deployment.msg_receiver.type} already deployed`); + + return deployment; +}; diff --git a/script/commands/index.ts b/script/commands/index.ts new file mode 100644 index 00000000..7afb70d3 --- /dev/null +++ b/script/commands/index.ts @@ -0,0 +1,4 @@ +export { deployCmd } from "./deploy"; +export { uploadCmd } from "./upload"; +export { contractCmd } from "./contract"; +export { migrateCmd } from "./migrate"; diff --git a/script/commands/migrate.ts b/script/commands/migrate.ts new file mode 100644 index 00000000..1facd9e4 --- /dev/null +++ b/script/commands/migrate.ts @@ -0,0 +1,168 @@ +import { Command, Option } from "commander"; +import { CodeDetails } from "@cosmjs/cosmwasm-stargate"; + +import { CONTAINER, Dependencies } from "../shared/ioc"; +import { ContractNames } from "../shared/contract"; +import { ContextHook, ContextIsm } from "../shared/context"; +import { askQuestion, waitTx } from "../shared/utils"; +import { contractNames } from "../shared/constants"; + +export const migrateCmd = new Command("migrate") + .description("Migrate contracts") + .configureHelp({ showGlobalOptions: true }) + .addOption( + new Option( + "-c --contracts ", + "specify contract types to migrate", + ).choices(contractNames), + ) + .action(handleMigrate); + +async function handleMigrate(_: object, cmd: Command) { + const { contracts } = cmd.optsWithGlobals() as { + networkId: string; + contracts: ContractNames[]; + }; + + const { ctx, client } = CONTAINER.get(Dependencies); + + const flattenedContracts = [ + ...flattenIsm(ctx.deployments.isms), + ...flattenHook(ctx.deployments.hooks?.default), + ...flattenHook(ctx.deployments.hooks?.required), + ctx.deployments.core?.mailbox, + ctx.deployments.core?.validator_announce, + ctx.deployments.test?.msg_receiver, + ...(ctx.deployments.warp?.cw20 || []), + ...(ctx.deployments.warp?.native || []), + ].filter((v, i, arr) => arr.indexOf(v) === i) as { + type: ContractNames; + address: string; + }[]; + + const withContractInfo = await Promise.all( + (contracts + ? flattenedContracts.filter((v) => contracts.includes(v.type)) + : flattenedContracts + ).map(async (v) => { + const contractInfo = await client.wasm.getContract(v.address); + const codeInfo = await client.wasm.getCodeDetails(contractInfo.codeId); + return { ...v, contractInfo, codeInfo }; + }), + ); + + const artifacts = Object.fromEntries( + await Promise.all( + Object.entries(ctx.artifacts).map(async ([contractName, codeId]) => { + const codeInfo = await client.wasm.getCodeDetails(codeId); + return [contractName, codeInfo]; + }), + ), + ) as Record; + + const toMigrate = withContractInfo.filter( + (v) => + v.codeInfo.id !== ctx.artifacts[v.type] && + v.codeInfo.checksum !== artifacts[v.type].checksum, + ); + + if (toMigrate.length === 0) { + console.log("No changes detected."); + return; + } + + for (const migrate of toMigrate) { + console.log( + `${migrate.type} needs migration from`, + `${migrate.codeInfo.id} to ${artifacts[migrate.type].id}`, + `(contract: ${migrate.address})`, + ); + } + + if (!(await askQuestion("Do you want to proceed? (y/n)"))) { + console.log("Aborted."); + return; + } + console.log("Proceeding to migrate..."); + + for (const migrate of toMigrate) { + console.log(`Migrating ${migrate.type}...`); + + const res = await client.wasm.migrate( + client.signer, + migrate.address, + artifacts[migrate.type].id, + {}, + "auto", + ); + await waitTx(res.transactionHash, client.stargate); + console.log(`${migrate.type} migrated successfully`); + } +} + +const flattenIsm = ( + ism: ContextIsm | undefined +): { type: ContractNames; address: string }[] => { + if (!ism) return []; + + switch (ism.type) { + case "hpl_ism_aggregate": + return [ + { type: ism.type, address: ism.address }, + ...ism.isms.flatMap(flattenIsm), + ]; + case "hpl_ism_routing": + return [ + { type: ism.type, address: ism.address }, + ...Object.values(ism.isms).flatMap(flattenIsm), + ]; + case "hpl_ism_multisig": + return [ism]; + case "hpl_ism_pausable": + return [ism]; + case "hpl_test_mock_ism": + return [ism]; + } +}; + +const flattenHook = ( + hook: ContextHook | undefined +): { type: ContractNames; address: string }[] => { + if (!hook) return []; + + switch (hook.type) { + case "hpl_hook_aggregate": + return [ + { type: hook.type, address: hook.address }, + ...hook.hooks.flatMap(flattenHook), + ]; + case "hpl_hook_routing": + return [ + { type: hook.type, address: hook.address }, + ...Object.values(hook.hooks).flatMap(flattenHook), + ]; + case "hpl_hook_routing_custom": + return [ + { type: hook.type, address: hook.address }, + ...Object.values(hook.hooks).flatMap((v) => + Object.values(v).flatMap(flattenHook) + ), + ]; + case "hpl_hook_routing_fallback": + return [ + { type: hook.type, address: hook.address }, + ...Object.values(hook.hooks).flatMap(flattenHook), + ]; + + case "hpl_igp": + return [{ type: hook.type, address: hook.address }, hook.oracle]; + case "hpl_hook_fee": + return [hook]; + case "hpl_hook_merkle": + return [hook]; + case "hpl_hook_pausable": + return [hook]; + case "hpl_test_mock_hook": + return [hook]; + } +}; diff --git a/script/commands/upload.ts b/script/commands/upload.ts new file mode 100644 index 00000000..526f6903 --- /dev/null +++ b/script/commands/upload.ts @@ -0,0 +1,216 @@ +/** + * Upload command for contract codes + * 1. local + * - upload local artifacts + * 2. remote + * - upload remote artifacts + * 3. remote-list + * - list available releases from github (check `../common/github.ts` to see how it works) + */ + +import * as fs from "fs"; +import { Command } from "commander"; +import { CodeDetails } from "@cosmjs/cosmwasm-stargate"; + +import { getWasmPath, loadWasmFileDigest } from "../shared/wasm"; +import { CONTAINER, Dependencies } from "../shared/ioc"; +import { + MIN_RELEASE_VERSION, + downloadReleases, + getReleases, +} from "../shared/github"; +import { + contractNames, + defaultArtifactPath, + defaultTmpDir, +} from "../shared/constants"; +import { askQuestion, sleep, waitTx } from "../shared/utils"; +import { saveContext } from "../shared/context"; +import { ContractNames } from "../shared/contract"; + +// ============ Command Definitions + +const uploadCmd = new Command("upload") + .description("Upload contract codes") + .option("-c --contracts ", "specify contracts to upload") + .configureHelp({ showGlobalOptions: true }); + +uploadCmd + .command("local") + .description("upload artifacts from local") + .option("-a --artifacts ", "artifacts", defaultArtifactPath) + .action(async (_, cmd) => upload(cmd.optsWithGlobals())); + +uploadCmd + .command("remote") + .description("upload artifacts from remote") + .argument("", `name of release tag. min: ${MIN_RELEASE_VERSION}`) + .option("-o --out ", "artifact output directory", defaultTmpDir) + .action(handleRemote); + +uploadCmd + .command("remote-list") + .description("list all available public release of cw-hyperlane") + .action(handleRemoteList); + +export { uploadCmd }; + +// ============ Handler Functions + +async function handleRemote(tagName: string, _: any, cmd: any): Promise { + const opts = cmd.optsWithGlobals(); + + if (tagName < MIN_RELEASE_VERSION) + throw new Error(`${tagName} < ${MIN_RELEASE_VERSION}`); + + const releases = await getReleases(); + if (!releases[tagName]) + throw new Error( + `release ${tagName} not found in remote.` + + "try 'upload remote-list' to see available releases." + ); + + // make directory if not exists + if (!fs.existsSync(opts.out)) fs.mkdirSync(opts.out, { recursive: true }); + + const artifactPath = await downloadReleases(releases[tagName], opts.out); + + console.log("Downloaded artifacts to", artifactPath.green); + + return upload({ ...opts, artifacts: artifactPath }); +} + +async function handleRemoteList() { + const releases = await getReleases(); + + console.log("Available releases:".green); + for (const [tagName, codes] of Object.entries(releases)) { + console.log("-", `[${tagName}]`.blue); + console.log("ㄴ codes:".grey, `(${codes})`); + } +} + +// ============ Business Logic + +type UploadArgs = { + artifacts: string; + contracts?: ContractNames[]; + networkId: string; +}; + +async function upload({ + artifacts: artifactPath, + contracts: uploadTargets, + networkId, +}: UploadArgs) { + (uploadTargets || []).forEach((v) => { + if (!contractNames.includes(v)) + throw new Error( + `invalid contract name ${v}.` + + "try 'contract list' to see available contracts." + ); + }); + + const digest = await loadWasmFileDigest({ artifactPath }); + const { ctx, client }: Dependencies = CONTAINER.get(Dependencies); + + // query code details of context artifacts + const codeIds = Object.fromEntries( + await Promise.all( + (Object.values(contractNames) as ContractNames[]) + .filter((k) => (uploadTargets ? uploadTargets.includes(k) : true)) + .map(async (k) => [ + k, + ctx.artifacts[k] && + (await client.wasm.getCodeDetails(ctx.artifacts[k])), + ]) + ) + ) as Record; + + // checking code changes + console.log("Checking code changes...".green); + + const listDiff = Object.entries(codeIds) + .map(([v, codeId]) => { + const oldCodeChecksum = codeId?.checksum; + const newCodeChecksum = digest[getWasmPath(v, { artifactPath })]; + + if (oldCodeChecksum && oldCodeChecksum === newCodeChecksum) { + console.log("[NO-CHANGE]".green.padEnd(12, " "), v.padEnd(30, " ")); + return undefined; + } + + if (!oldCodeChecksum) { + console.log( + "[NEW]".yellow.padEnd(12, " "), + v.padEnd(30, " "), + newCodeChecksum + ); + } else { + console.log( + "[REPLACE]".yellow.padEnd(12, " "), + v.padEnd(30, " "), + oldCodeChecksum, + "!=", + newCodeChecksum + ); + } + + return v; + }) + .filter((v) => v !== undefined) as ContractNames[]; + + if (listDiff.length === 0) { + console.log("No changes detected."); + return; + } + + if (!(await askQuestion("Do you want to proceed? (y/n)"))) { + console.log("Aborted."); + return; + } + console.log("Proceeding to upload..."); + + let okCount = 0; + for (const diff of listDiff) { + const upload = await client.wasm.upload( + client.signer, + fs.readFileSync(getWasmPath(diff, { artifactPath })), + "auto" + ); + + const receipt = await waitTx(upload.transactionHash, client.stargate); + + if (receipt.code > 0) { + console.error( + "[FAILURE]".red.padEnd(10, " "), + `${diff.padEnd(30, " ")}`, + `tx: ${upload.transactionHash}` + ); + continue; + } + + console.log( + "[SUCCESS]".green.padEnd(10, " "), + `${diff.padEnd(30, " ")}`, + `codeId: ${upload.codeId}, tx: ${upload.transactionHash}` + ); + + ctx.artifacts[diff] = upload.codeId; + okCount++; + } + + if (okCount === 0) { + console.error( + "[FAILURE]".red.padEnd(10, " "), + "every uploads have failed." + ); + return; + } + + console.log( + "[SUCCESS]".green.padEnd(10, " "), + `uploaded ${okCount} contracts.` + ); + saveContext(networkId, ctx); +} diff --git a/script/commands/wallet.ts b/script/commands/wallet.ts new file mode 100644 index 00000000..7c3fe406 --- /dev/null +++ b/script/commands/wallet.ts @@ -0,0 +1,72 @@ +import { + DirectSecp256k1HdWallet, + DirectSecp256k1Wallet, + makeCosmoshubPath, +} from "@cosmjs/proto-signing"; +import { Command } from "commander"; + +import { getNetwork } from "../shared/config"; +import { getKeyPair } from "../shared/crypto"; + +const walletCmd = new Command("wallet") + .description("Wallet commands") + .configureHelp({ showGlobalOptions: true }); + +walletCmd + .command("new") + .description("Create a new wallet") + .action(async (_, cmd) => { + const opts = cmd.optsWithGlobals(); + const network = getNetwork(opts.networkId); + + const prefix = { prefix: network.hrp }; + const wallet = await DirectSecp256k1HdWallet.generate(12, prefix); + + const [account] = await wallet.getAccounts(); + const { privkey } = await getKeyPair(wallet.mnemonic, makeCosmoshubPath(0)); + + console.log({ + address: account.address, + mnemonic: wallet.mnemonic, + privateKey: Buffer.from(privkey).toString("hex"), + }); + }); + +walletCmd + .command("address") + .description("Get the address of the wallet") + .option("--private-key ", "The private key of the wallet") + .option("--mnemonic ", "The mnemonic of the wallet") + .action(async (_, cmd) => { + const opts = cmd.optsWithGlobals(); + const network = getNetwork(opts.networkId); + + if ( + (opts.privateKey && opts.mnemonic) || + (!opts.privateKey && !opts.mnemonic) + ) { + throw new Error( + "Only one of --private-key and --mnemonic can be specified" + ); + } + + const wallet = opts.privateKey + ? await DirectSecp256k1Wallet.fromKey( + Buffer.from( + opts.privateKey.startsWith("0x") + ? opts.privateKey.slice(2) + : opts.privateKey, + "hex" + ), + network.hrp + ) + : await DirectSecp256k1HdWallet.fromMnemonic(opts.mnemonic, { + prefix: network.hrp, + }); + + const [account] = await wallet.getAccounts(); + + console.log(account.address); + }); + +export { walletCmd }; diff --git a/script/index.ts b/script/index.ts new file mode 100644 index 00000000..ee0432bf --- /dev/null +++ b/script/index.ts @@ -0,0 +1,47 @@ +import "reflect-metadata"; +import colors from "colors"; +import { Command, Option } from "commander"; + +import { uploadCmd, deployCmd, contractCmd, migrateCmd } from "./commands"; +import { config, getSigningClient } from "./shared/config"; +import { loadContext } from "./shared/context"; +import { CONTAINER, Dependencies } from "./shared/ioc"; + +import { version } from "../package.json"; + +colors.enable(); + +const optNetworkId = new Option( + "-n, --network-id ", + "specify network id" +) + .choices(config.networks.map((v) => v.id)) + .makeOptionMandatory(); + +const cli = new Command(); + +cli + .name("cw-hpl") + .version(version) + .description("CLI toolkit for CosmWasm Hyperlane") + .addOption(optNetworkId) + .hook("preAction", injectDependencies); + +cli.addCommand(uploadCmd); +cli.addCommand(deployCmd); +cli.addCommand(contractCmd); +cli.addCommand(migrateCmd); + +cli.parseAsync(process.argv).catch(console.error); + +async function injectDependencies(cmd: Command): Promise { + const { networkId } = cmd.optsWithGlobals(); + + const client = await getSigningClient(networkId, config); + const ctx = loadContext(networkId); + + CONTAINER.bind(Dependencies).toConstantValue({ + ctx, + client, + }); +}