Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3b702ec
add token-transfer main logic
Nov 3, 2025
22e03a1
add new command into index
Nov 3, 2025
4a5f1a7
update error handling for duplicated flags
Nov 3, 2025
9908286
update readability
Nov 3, 2025
cfb34c3
add warning log for RPCs
Nov 3, 2025
8734b5a
add desciptive comments to functions
Nov 3, 2025
2df42bd
add dependency for token-transfer
Nov 3, 2025
ef3f1e9
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 4, 2025
c4bf625
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 4, 2025
83cbc3d
add deployment types
Nov 10, 2025
5ac335e
add error handling
Nov 10, 2025
9ebc966
import logic
Nov 10, 2025
ef5dad2
adapt to ntt transfers
Nov 10, 2025
68ca8dd
update script
Nov 10, 2025
7ff37fa
add error handling
Nov 10, 2025
afb1ced
update error handling
Nov 10, 2025
0b4449e
catch errors
Nov 10, 2025
caa7d5f
removed any types
Nov 10, 2025
cde8296
update clone
Nov 10, 2025
a895eab
add comments
Nov 10, 2025
e7a5a0c
Merge branch 'main' into cli-add-token-transfer
Nov 10, 2025
a142bb9
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 11, 2025
de87d36
add link to readme
Nov 12, 2025
d4aa7a6
update function naming
Nov 12, 2025
d970a07
validate executor quote fields
Nov 12, 2025
12449c0
update comment
Nov 12, 2025
37ac96e
mainnet confirmation prompt
Nov 12, 2025
cd6f12c
update spinner
Nov 12, 2025
5e800bf
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 13, 2025
e48845f
consolidate rpc handling
Nov 13, 2025
dbb3347
update dependencies
Nov 13, 2025
dd8bb81
update error
Nov 14, 2025
d466447
update error
Nov 17, 2025
2973bac
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 17, 2025
643405a
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 19, 2025
504753f
Merge branch 'main' into cli-add-token-transfer
evgeniko Nov 19, 2025
f6cafd4
Merge branch 'main' into cli-add-token-transfer
martin0995 Nov 25, 2025
3e98eec
Merge branch 'main' into cli-add-token-transfer
evgeniko Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NTT cli

Comprehensive setup instructions, walkthroughs, and deployment guides live in the Native Token Transfers documentation. Start with [Get Started with NTT](https://wormhole.com/docs/products/token-transfers/native-token-transfers/get-started/).

## Prerequisites

- [bun](https://bun.sh/docs/installation)
Expand Down
5 changes: 3 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"ntt": "src/index.ts"
},
"dependencies": {
"chalk": "^5.3.0",
"yargs": "^17.7.2"
"chalk": "^5.6.0",
"ora": "8.2.0",
"yargs": "18.0.0"
},
"overrides": {
"@wormhole-foundation/sdk-definitions-ntt": {
Expand Down
41 changes: 41 additions & 0 deletions cli/src/deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Chain, Network } from "@wormhole-foundation/sdk";
import type { Ntt } from "@wormhole-foundation/sdk-definitions-ntt";
import fs from "fs";

export type ChainConfig = {
version: string;
mode: Ntt.Mode;
paused: boolean;
owner: string;
pauser?: string;
manager: string;
token: string;
transceivers: {
threshold: number;
wormhole: { address: string; pauser?: string; executor?: boolean };
};
limits: {
outbound: string;
inbound: Partial<{ [C in Chain]: string }>;
};
};

export type Config = {
network: Network;
chains: Partial<{
[C in Chain]: ChainConfig;
}>;
defaultLimits?: {
outbound: string;
};
};

export function loadConfig(path: string): Config {
if (!fs.existsSync(path)) {
console.error(`File not found: ${path}`);
console.error(`Create with 'ntt init' or specify another file with --path`);
process.exit(1);
}
const deployments: Config = JSON.parse(fs.readFileSync(path).toString());
return deployments;
}
159 changes: 104 additions & 55 deletions cli/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@ import chalk from "chalk";
import type { Chain, Network } from "@wormhole-foundation/sdk";
import { chainToPlatform } from "@wormhole-foundation/sdk-base";

const RPC_ERROR_KEYWORDS = [
"jsonrpc",
"network error",
"rpc",
"connection",
"unable to connect",
"404 not found",
"status code: 404",
"status code",
"not found",
"could not connect",
"failed to fetch",
"connect to",
] as const;

function extractErrorDetails(
error: unknown
): { message: string; stack: string } {
if (error instanceof Error) {
return {
message: error.message ?? "",
stack: error.stack ?? "",
};
}
if (typeof error === "object" && error !== null) {
const message =
"message" in error ? String((error as { message?: unknown }).message ?? "") : "";
const stack =
"stack" in error ? String((error as { stack?: unknown }).stack ?? "") : "";
return { message, stack };
}
return { message: String(error ?? ""), stack: "" };
}

export function isRpcConnectionError(error: unknown): boolean {
const { message, stack } = extractErrorDetails(error);
const haystack = `${message} ${stack}`.toLowerCase();
if (!haystack.trim()) {
return false;
}
return RPC_ERROR_KEYWORDS.some((needle) => haystack.includes(needle));
}

/**
* @param error - The error that occurred (typically from execSync)
* @param rpc - The RPC endpoint URL
Expand Down Expand Up @@ -57,41 +100,33 @@ function handleRpcConnectionError(
network: Network,
rpc: string
): boolean {
const errorMessage = error?.message || String(error);
const errorStack = error?.stack || "";
if (!isRpcConnectionError(error)) {
return false;
}

// Check if this is an RPC-related error by looking for common RPC error indicators
const isRpcError =
errorMessage.toLowerCase().includes("jsonrpc") ||
errorStack.toLowerCase().includes("jsonrpc") ||
errorMessage.toLowerCase().includes("rpc") ||
errorMessage.toLowerCase().includes("connection") ||
errorMessage.toLowerCase().includes("network error");
const errorMessage = error?.message || String(error);

if (isRpcError) {
console.error(
chalk.red(`RPC connection error for ${chain} on ${network}\n`)
);
console.error(chalk.yellow("RPC endpoint:"), chalk.white(rpc));
console.error(chalk.yellow("Error:"), errorMessage);
console.error();
console.error(
chalk.yellow(
"This error usually means the RPC endpoint is missing, invalid, or unreachable."
)
);
console.error(
chalk.yellow(
"You can specify a private RPC endpoint by creating an overrides.json file.\n"
)
);
console.error(
chalk.cyan("Create a file named ") +
chalk.white("overrides.json") +
chalk.cyan(" in your project root:")
);
console.error(
chalk.white(`
console.error(chalk.red(`RPC connection error for ${chain} on ${network}\n`));
console.error(chalk.yellow("RPC endpoint:"), chalk.white(rpc));
console.error(chalk.yellow("Error:"), errorMessage);
console.error();
console.error(
chalk.yellow(
"This error usually means the RPC endpoint is missing, invalid, or unreachable."
)
);
console.error(
chalk.yellow(
"You can specify a private RPC endpoint by creating an overrides.json file.\n"
)
);
console.error(
chalk.cyan("Create a file named ") +
chalk.white("overrides.json") +
chalk.cyan(" in your project root:")
);
console.error(
chalk.white(`
{
"chains": {
"${chain}": {
Expand All @@ -100,31 +135,28 @@ function handleRpcConnectionError(
}
}
`)
);
);

// Show chainlist.org only for EVM chains
try {
const platform = chainToPlatform(chain as any);
if (platform === "Evm") {
console.error(
chalk.cyan(`Find RPC endpoints for ${chain}: https://chainlist.org`)
);
}
} catch (e) {
// If chainToPlatform fails, just skip the platform-specific message
// Show chainlist.org only for EVM chains
try {
const platform = chainToPlatform(chain as any);
if (platform === "Evm") {
console.error(
chalk.cyan(`Find RPC endpoints for ${chain}: https://chainlist.org`)
);
}

console.error(
chalk.cyan(
`For more information about overrides.json:\n` +
` • https://wormhole.com/docs/products/token-transfers/native-token-transfers/faqs/#how-can-i-specify-a-custom-rpc-for-ntt`
)
);

return true;
} catch (e) {
// If chainToPlatform fails, just skip the platform-specific message
}

return false;

console.error(
chalk.cyan(
`For more information about overrides.json:\n` +
` • https://wormhole.com/docs/products/token-transfers/native-token-transfers/faqs/#how-can-i-specify-a-custom-rpc-for-ntt`
)
);

return true;
}

/**
Expand Down Expand Up @@ -177,3 +209,20 @@ export function handleDeploymentError(

handleGenericError(error);
}

/**
* Log a concise RPC failure when connection-specific guidance wasn’t already printed.
*/
export function logRpcError(
error: any,
chain: Chain,
network: Network,
rpc?: string
): void {
if (rpc && handleRpcConnectionError(error, chain, network, rpc)) {
return;
}
const message = error instanceof Error ? error.message : String(error);
console.error(chalk.red(`RPC error for ${chain} on ${network}`));
console.error(message);
}
43 changes: 4 additions & 39 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import { registerSolanaTransceiver } from "./solanaHelpers";
import { colorizeDiff, diffObjects } from "./diff";
import { forgeSignerArgs, getSigner, type SignerType } from "./getSigner";
import { handleDeploymentError } from "./error";
import { loadConfig, type ChainConfig, type Config } from "./deployments";
export type { ChainConfig, Config } from "./deployments";

// Configuration fields that should be excluded from diff operations
// These are local-only configurations that don't have on-chain representations
Expand Down Expand Up @@ -105,6 +107,7 @@ import type {
} from "@wormhole-foundation/sdk-evm";
import { getAvailableVersions, getGitTagName } from "./tag";
import * as configuration from "./configuration";
import { createTokenTransferCommand } from "./tokenTransfer";
import { AbiCoder, ethers, Interface } from "ethers";
import { newSignSendWaiter, signSendWaitWithOverride } from "./signSendWait.js";

Expand Down Expand Up @@ -262,35 +265,6 @@ export type SuiDeploymentResult<C extends Chain> = ChainAddress<C> & {
};
};

// TODO: rename
export type ChainConfig = {
version: string;
mode: Ntt.Mode;
paused: boolean;
owner: string;
pauser?: string;
manager: string;
token: string;
transceivers: {
threshold: number;
wormhole: { address: string; pauser?: string; executor?: boolean };
};
limits: {
outbound: string;
inbound: Partial<{ [C in Chain]: string }>;
};
};

export type Config = {
network: Network;
chains: Partial<{
[C in Chain]: ChainConfig;
}>;
defaultLimits?: {
outbound: string;
};
};

const options = {
network: {
alias: "n",
Expand Down Expand Up @@ -2010,6 +1984,7 @@ yargs(hideBin(process.argv))
}
}
)
.command(createTokenTransferCommand(overrides))
.command("solana", "Solana commands", (yargs) => {
yargs
.command(
Expand Down Expand Up @@ -5256,16 +5231,6 @@ function checkAnchorVersion(pwd: string) {
}
}

function loadConfig(path: string): Config {
if (!fs.existsSync(path)) {
console.error(`File not found: ${path}`);
console.error(`Create with 'ntt init' or specify another file with --path`);
process.exit(1);
}
const deployments: Config = JSON.parse(fs.readFileSync(path).toString());
return deployments;
}

function resolveVersion(
latest: boolean,
ver: string | undefined,
Expand Down
Loading
Loading