Skip to content

Commit 317fbb6

Browse files
committed
cli: implement ntt cli
1 parent 1c0c171 commit 317fbb6

13 files changed

+2494
-64
lines changed

Dockerfile.cli

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
FROM ubuntu:latest as base
2+
3+
RUN apt update
4+
5+
RUN apt install -y python3
6+
RUN apt install -y build-essential
7+
RUN apt install -y git
8+
RUN apt install -y curl
9+
RUN apt install -y unzip
10+
11+
RUN curl -fsSL https://bun.sh/install | bash
12+
13+
RUN curl -L https://foundry.paradigm.xyz | bash
14+
RUN bash -ci "foundryup"
15+
16+
RUN apt install -y jq
17+
18+
FROM base as cli-remote
19+
# NOTE: when invoking the installer outside of the source tree, it clones the
20+
# repo and installs that way.
21+
# This build stage tests that path.
22+
COPY cli/install.sh cli/install.sh
23+
RUN bash -ci "./cli/install.sh"
24+
RUN bash -ci "which ntt"
25+
26+
FROM base as cli-local
27+
# NOTE: when invoking the installer inside of the source tree, it installs from
28+
# the local source tree.
29+
# This build stage tests that path.
30+
WORKDIR /app
31+
COPY tsconfig.json tsconfig.json
32+
COPY package.json package.json
33+
COPY package-lock.json package-lock.json
34+
COPY sdk sdk
35+
COPY solana/package.json solana/package.json
36+
COPY solana/ts solana/ts
37+
COPY solana/tsconfig.*.json solana/
38+
COPY cli/package.json cli/package.json
39+
COPY cli/package-lock.json cli/package-lock.json
40+
COPY cli/src cli/src
41+
COPY cli/install.sh cli/install.sh
42+
# COPY package.json .
43+
# COPY cli/package.json .
44+
RUN bash -ci "./cli/install.sh"
45+
RUN bash -ci "which ntt"
46+
47+
FROM cli-local as cli-local-test
48+
COPY cli/test cli/test
49+
COPY evm evm
50+
RUN bash -ci "./cli/test/sepolia-bsc.sh"

cli/example-overrides.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"chains": {
3+
"Bsc": {
4+
"rpc": "http://127.0.0.1:8545"
5+
},
6+
"Sepolia": {
7+
"rpc": "http://127.0.0.1:8546"
8+
},
9+
"Solana": {
10+
"rpc": "http://127.0.0.1:8899"
11+
}
12+
}
13+
}

cli/install.sh

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# check that 'bun' is installed
6+
7+
if ! command -v bun > /dev/null; then
8+
echo "bun is not installed. Follow the instructions at https://bun.sh/docs/installation"
9+
exit 1
10+
fi
11+
12+
function main {
13+
path=""
14+
15+
# check if there's a package.json in the parent directory, with "name": "@wormhole-foundation/ntt-cli"
16+
if [ -f "$(dirname $0)/package.json" ] && grep -q '"name": "@wormhole-foundation/ntt-cli"' "$(dirname $0)/package.json"; then
17+
path="$(dirname $0)/.."
18+
else
19+
# clone to $HOME/.ntt-cli if it doesn't exist, otherwise update it
20+
repo_ref="$(select_repo)"
21+
repo="$(echo "$repo_ref" | awk '{print $1}')"
22+
ref="$(echo "$repo_ref" | awk '{print $2}')"
23+
echo "Cloning $repo $ref"
24+
25+
mkdir -p "$HOME/.ntt-cli"
26+
path="$HOME/.ntt-cli/.checkout"
27+
28+
if [ ! -d "$path" ]; then
29+
git clone --branch "$ref" "$repo" "$path"
30+
else
31+
pushd "$path"
32+
git fetch origin
33+
# reset hard
34+
git reset --hard "origin/$ref"
35+
popd
36+
fi
37+
38+
fi
39+
40+
install_cli "$path"
41+
}
42+
43+
# function that determines which repo to clone
44+
function select_repo {
45+
foundation_repo="https://github.com/wormhole-foundation/example-native-token-transfers.git"
46+
labs_repo="https://github.com/wormholelabs-xyz/example-native-token-transfers.git"
47+
# if the foundation repo has a tag of the form "vX.Y.Z+cli", use that (the latest one)
48+
# otherwise we'll use the 'cli' branch from the labs repo
49+
ref=""
50+
repo=""
51+
regex="refs/tags/v[0-9]*\.[0-9]*\.[0-9]*+cli"
52+
if git ls-remote --tags "$foundation_repo" | grep -q "$regex"; then
53+
repo="$foundation_repo"
54+
ref="$(git ls-remote --tags "$foundation_repo" | grep "$regex" | sort -V | tail -n 1 | awk '{print $2}')"
55+
else
56+
repo="$labs_repo"
57+
ref="cli"
58+
fi
59+
60+
echo "$repo $ref"
61+
}
62+
63+
# the above but as a function. takes a single argument: the path to the package.json file
64+
# TODO: should just take the path to the repo root as an argument...
65+
function install_cli {
66+
cd "$1"
67+
68+
# if 'ntt' is already installed, uninstall it
69+
# just check with 'which'
70+
if which ntt > /dev/null; then
71+
echo "Removing existing ntt CLI"
72+
rm $(which ntt)
73+
fi
74+
75+
# swallow the output of the first install
76+
# TODO: figure out why it fails the first time.
77+
bun install > /dev/null 2>&1 || true
78+
bun install
79+
80+
# make a temporary directory
81+
82+
tmpdir="$(mktemp -d)"
83+
84+
# create a temporary symlink 'npm' to 'bun'
85+
86+
ln -s "$(command -v bun)" "$tmpdir/npm"
87+
88+
# add the temporary directory to the PATH
89+
90+
export PATH="$tmpdir:$PATH"
91+
92+
# swallow the output of the first build
93+
# TODO: figure out why it fails the first time.
94+
bun --bun run --filter '*' build > /dev/null 2>&1 || true
95+
bun --bun run --filter '*' build
96+
97+
# remove the temporary directory
98+
99+
rm -r "$tmpdir"
100+
101+
# now link the CLI
102+
103+
cd cli
104+
105+
bun link
106+
107+
bun link @wormhole-foundation/ntt-cli
108+
}
109+
110+
main

cli/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"name": "cli",
2+
"name": "@wormhole-foundation/ntt-cli",
3+
"version": "1.0.0-beta",
34
"module": "src/index.ts",
45
"type": "module",
56
"devDependencies": {
@@ -13,7 +14,7 @@
1314
"ntt": "src/index.ts"
1415
},
1516
"dependencies": {
17+
"chalk": "^5.3.0",
1618
"yargs": "^17.7.2"
17-
},
18-
"version": "0.1.0-beta.0"
19-
}
19+
}
20+
}

cli/src/configuration.ts

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { assertChain, chains, type Chain } from "@wormhole-foundation/sdk";
2+
import * as yargs from "yargs";
3+
import fs from "fs";
4+
import { ensureNttRoot } from ".";
5+
import chalk from "chalk";
6+
7+
// We support project-local and global configuration.
8+
// The configuration is stored in JSON files in $HOME/.ntt-cli/config.json (global) and .ntt-cli/config.json (local).
9+
// These can further be overridden by environment variables of the form CHAIN_KEY=value.
10+
type Scope = "global" | "local";
11+
12+
type Config = {
13+
chains: Partial<{
14+
[C in Chain]: ChainConfig;
15+
}>
16+
}
17+
18+
type ChainConfig = Partial<typeof configTemplate>;
19+
20+
// TODO: per-network configuration? (i.e. mainnet, testnet, etc)
21+
const configTemplate = {
22+
scan_api_key: "",
23+
};
24+
25+
function assertChainConfigKey(key: string): asserts key is keyof ChainConfig {
26+
const validKeys = Object.keys(configTemplate);
27+
if (!validKeys.includes(key)) {
28+
throw new Error(`Invalid key: ${key}`);
29+
}
30+
}
31+
32+
const options = {
33+
chain: {
34+
describe: "Chain",
35+
type: "string",
36+
choices: chains,
37+
demandOption: true,
38+
},
39+
key: {
40+
describe: "Key",
41+
type: "string",
42+
choices: Object.keys(configTemplate),
43+
demandOption: true,
44+
},
45+
value: {
46+
describe: "Value",
47+
type: "string",
48+
demandOption: true,
49+
},
50+
local: {
51+
describe: "Use local configuration",
52+
type: "boolean",
53+
default: false,
54+
},
55+
global: {
56+
describe: "Use global configuration",
57+
type: "boolean",
58+
default: true,
59+
}
60+
} as const;
61+
export const command = (args: yargs.Argv<{}>) => args
62+
.command("set-chain <chain> <key> <value>",
63+
"set a configuration value for a chain",
64+
(yargs) => yargs
65+
.positional("chain", options.chain)
66+
.positional("key", options.key)
67+
.positional("value", options.value)
68+
.option("local", options.local)
69+
.option("global", options.global),
70+
(argv) => {
71+
const scope = resolveScope(argv.local, argv.global);
72+
assertChain(argv.chain);
73+
assertChainConfigKey(argv.key);
74+
setChainConfig(scope, argv.chain, argv.key, argv.value);
75+
})
76+
.command("unset-chain <chain> <key>",
77+
"unset a configuration value for a chain",
78+
(yargs) => yargs
79+
.positional("chain", options.chain)
80+
.positional("key", options.key)
81+
.option("local", options.local)
82+
.option("global", options.global),
83+
(argv) => {
84+
const scope = resolveScope(argv.local, argv.global);
85+
assertChainConfigKey(argv.key);
86+
assertChain(argv.chain);
87+
setChainConfig(scope, argv.chain, argv.key, undefined);
88+
})
89+
.command("get-chain <chain> <key>",
90+
"get a configuration value",
91+
(yargs) => yargs
92+
.positional("chain", options.chain)
93+
.positional("key", options.key)
94+
.option("local", options.local)
95+
.option("global", options.global),
96+
(argv) => {
97+
const scope = resolveScope(argv.local, argv.global);
98+
assertChainConfigKey(argv.key);
99+
assertChain(argv.chain);
100+
const val = getChainConfig(argv.scope as Scope, argv.chain, argv.key);
101+
if (!val) {
102+
console.error("undefined");
103+
} else {
104+
console.log(val);
105+
}
106+
})
107+
.demandCommand()
108+
109+
function findOrCreateConfigFile(scope: Scope): string {
110+
// if scope is global, touch $HOME/.ntt-cli/config.json
111+
// if scope is local, touch .ntt-cli/config.json. In the latter case, make sure we're in an ntt project (call ensureNttRoot())
112+
113+
// if the file doesn't exist, write an empty object
114+
let configDir;
115+
116+
switch (scope) {
117+
case "global":
118+
if (!process.env.HOME) {
119+
throw new Error("Could not determine home directory");
120+
}
121+
configDir = `${process.env.HOME}/.ntt-cli`;
122+
break;
123+
case "local":
124+
ensureNttRoot();
125+
configDir = ".ntt-cli";
126+
break;
127+
}
128+
129+
const emptyConfig: Config = {
130+
chains: {},
131+
};
132+
133+
if (!fs.existsSync(configDir)) {
134+
fs.mkdirSync(configDir);
135+
}
136+
const configFile = `${configDir}/config.json`;
137+
if (!fs.existsSync(configFile)) {
138+
fs.writeFileSync(configFile, JSON.stringify(emptyConfig, null, 2));
139+
}
140+
return configFile;
141+
}
142+
143+
function setChainConfig(scope: Scope, chain: Chain, key: keyof ChainConfig, value: string | undefined) {
144+
const configFile = findOrCreateConfigFile(scope);
145+
const config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Config;
146+
if (!config.chains[chain]) {
147+
config.chains[chain] = {};
148+
}
149+
config.chains[chain]![key] = value;
150+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
151+
}
152+
153+
function getChainConfig(scope: Scope, chain: Chain, key: keyof ChainConfig): string | undefined {
154+
const configFile = findOrCreateConfigFile(scope);
155+
const config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Config;
156+
return config.chains[chain]?.[key];
157+
}
158+
159+
function envVarName(chain: Chain, key: keyof ChainConfig): string {
160+
return `${chain.toUpperCase()}_${key.toUpperCase()}`;
161+
}
162+
163+
export function get(
164+
chain: Chain,
165+
key: keyof ChainConfig,
166+
{ reportError = false }
167+
): string | undefined {
168+
const varName = envVarName(chain, key);
169+
const env = process.env[varName];
170+
if (env) {
171+
console.info(chalk.yellow(`Using ${varName} for ${chain} ${key}`));
172+
return env;
173+
}
174+
const local = getChainConfig("local", chain, key);
175+
if (local) {
176+
console.info(chalk.yellow(`Using local configuration for ${chain} ${key} (in .ntt-cli/config.json)`));
177+
return local;
178+
}
179+
const global = getChainConfig("global", chain, key);
180+
if (global) {
181+
console.info(chalk.yellow(`Using global configuration for ${chain} ${key} (in $HOME/.ntt-cli/config.json)`));
182+
return global;
183+
}
184+
if (reportError) {
185+
console.error(`Could not find configuration for ${chain} ${key}`);
186+
console.error(`Please set it using 'ntt config set-chain ${chain} ${key} <value>' or by setting the environment variable ${varName}`);
187+
}
188+
}
189+
function resolveScope(local: boolean, global: boolean) {
190+
if (local && global) {
191+
throw new Error("Cannot specify both --local and --global");
192+
}
193+
if (local) {
194+
return "local";
195+
}
196+
if (global) {
197+
return "global";
198+
}
199+
throw new Error("Must specify either --local or --global");
200+
}

0 commit comments

Comments
 (0)