Skip to content

feat(cli): add ntt hype commands for HyperCore integration#827

Merged
evgeniko merged 3 commits intomainfrom
feat/hype-hypercore-commands
Feb 26, 2026
Merged

feat(cli): add ntt hype commands for HyperCore integration#827
evgeniko merged 3 commits intomainfrom
feat/hype-hypercore-commands

Conversation

@evgeniko
Copy link
Copy Markdown
Contributor

@evgeniko evgeniko commented Feb 25, 2026

Summary

Adds a new ntt hype command group to the NTT CLI covering the full lifecycle of a HyperCore-linked EVM token: linking an ERC-20 contract to a HyperCore spot token, bridging tokens in and out, and querying status.

New commands

ntt hype link        --token-index <n> [--evm-extra-wei-decimals <n>] [--deploy-nonce <n>] [--only-finalize] --path <deployment>
ntt hype bridge-in   <amount> --path <deployment>
ntt hype bridge-out  <amount> --path <deployment>
ntt hype status      --path <deployment>

link — the single command for linking a HyperCore spot token to an ERC-20 contract. By default runs both steps in sequence:

  1. requestEvmContract — sends a spotDeploy/requestEvmContract L1 action to HyperCore. The address is force-lowercased before msgpack encoding (required for correct signer recovery). Persists tokenIndex to deployment.json on success.
  2. finalizeEvmContract — sends a finalizeEvmContract L1 action (top-level, not nested under spotDeploy). Automatically resolves the ERC-20 deploy nonce by querying the deployer's tx count from HyperEVM and iterating CREATE addresses.

Pass --only-finalize to skip the request step and run only the finalize step (e.g. if the request was already submitted). --token-index falls back to deployment.hypercore.tokenIndex when --only-finalize is used.

bridge-in — calls transfer(assetBridge, amount) on the ERC-20 from HyperEVM. The asset bridge address is derived deterministically from the token index (0x200000000000000000000000000000000000{index_hex_4digits}). <amount> is a human-readable decimal (e.g. "1.0"); decimals() is queried on-chain for conversion.

bridge-out — sends a spotSend user-signed action to move tokens from HyperCore back to the signing wallet's HyperEVM address via the asset bridge. Uses a distinct EIP-712 signing scheme from other L1 actions: domain "HyperliquidSignTransaction", chain ID 421614/42161, with time as nonce.

status — displays the HyperCore token index, asset bridge address, szDecimals, weiDecimals, and token string for this deployment.


Deployment config changes

Added optional hypercore field to the Config type:

export type HyperCoreConfig = {
  tokenIndex: number;
  szDecimals?: number;   // informational only
  weiDecimals?: number;  // informational only
};

tokenIndex is automatically written to deployment.json by link on success. Existing deployment files without this field continue to work unchanged.


Input validation

New cli/src/hype/validation.ts with helpers used throughout all hype commands:

  • parseIntegerInRange — validates integer ranges (token index must be a uint16, i.e. ≤ 65535)
  • parsePositiveDecimalAmount — rejects zero and non-numeric amounts
  • parseEvmAddress — validates 0x... address format

Reliability

All HyperLiquid API fetch() calls are wrapped in a fetchWithTimeout helper (30s, AbortController-based) so they cannot hang indefinitely.


Tests

Two new test files under cli/src/hype/__tests__/:

  • validation.test.ts: unit tests for all three validation helpers
  • hyperliquid.test.ts: unit tests for computeAssetBridge, computeDeployNonce, and computeDeployNonceFromHyperEvm (mocks getTransactionCount at the prototype level to avoid real RPC calls)

E2E verification (testnet)

All commands were tested against the HyperLiquid testnet with a real WSV token deployment:

  • bridge-in 1.0 → confirmed on-chain; HyperCore balance showed 1.0 WSV
  • bridge-out 1.0spotSend accepted; balance returned to 0.0 WSV
  • status → correct asset bridge address and token index displayed
  • link → reached HyperLiquid API (returned expected domain error "Address is already used as EVM contract", confirming correct signing and payload shape)

Test plan

  • Run bun test src in cli/ — all tests should pass
  • Run ntt hype status --path <deployment> with a deployment containing hypercore.tokenIndex
  • Run ntt hype bridge-in 0 --path <deployment> — should fail with "must be greater than zero"
  • Run ntt hype link --token-index 70000 --path <deployment> — should fail with "must be <= 65535"
  • Run ntt hype link --only-finalize --path <deployment> — should read token index from deployment.json and run only the finalize step

Summary by CodeRabbit

  • New Features

    • Added HyperCore configuration support in deployments.
    • New hype CLI commands: link, bridge-in, bridge-out, and status for linking tokens, bridging assets, and checking state.
    • Improved command output with clearer step/status messages and persistent tokenIndex storage after linking.
  • Bug Fixes / Reliability

    • More robust nonce/address handling and API request timeouts for HyperEVM/HyperCore flows.
  • Tests

    • Added comprehensive tests for HyperLiquid integration and input validation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds HyperCore/HyperEVM integration to the CLI: new hype subcommands (link, bridge-in, bridge-out, status), HyperLiquid L1 action helpers and signing, nonce/address utilities, validation helpers, type additions for HyperCore config, and associated tests.

Changes

Cohort / File(s) Summary
Type and Config Definitions
cli/src/deployments.ts
Add HyperCoreConfig type (tokenIndex, optional szDecimals, weiDecimals) and optional hypercore property on exported Config.
Validation Utilities
cli/src/hype/validation.ts
Add parseIntegerInRange, parsePositiveDecimalAmount, and parseEvmAddress exported validators using ethers for address validation.
Validation Tests
cli/src/hype/__tests__/validation.test.ts
Unit tests covering valid/invalid cases for integer range, positive decimal parsing, and EVM address parsing.
HyperLiquid Core Helpers
cli/src/evm/hyperliquid.ts
Implement L1 action signing/sending, fetchWithTimeout, computeL1ActionHash, spot flows (spotRequestEvmContract, spotFinalizeEvmContract, spotSend), bridging (bridgeIn), asset bridge derivation (computeAssetBridge), deployer/address/nonce utilities (getDeployerAddress, computeDeployNonce, computeDeployNonceFromHyperEvm), and metadata fetch (getSpotTokenString).
HyperLiquid Tests
cli/src/hype/__tests__/hyperliquid.test.ts
Tests for asset bridge derivation, token-index validation, computeDeployNonce behavior, and computeDeployNonceFromHyperEvm floor/hard-cap scenarios with provider overrides.
CLI Commands (hype)
cli/src/commands/hype.ts
Add subcommands link, bridge-in, bridge-out, status; wire validation helpers; handle deploy nonce auto-derive and persistence to deployment.json; require ETH_PRIVATE_KEY where appropriate; enhanced logging and error paths.
CLI Help Tests
cli/src/__tests__/cli-help.test.ts
Extend help assertions to include new hype subcommands (link, bridge-in, bridge-out, status).

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI (hype cmd)
    participant Validator as Validation
    participant HyperEVM as HyperEVM RPC
    participant HyperLiquid as HyperLiquid API
    participant DeployJSON as Deployment JSON

    User->>CLI: run "hype link --token-index ..."
    CLI->>Validator: validate token index, evm address, decimals
    Validator-->>CLI: validated inputs
    CLI->>HyperLiquid: spotRequestEvmContract (signed EIP-712 request)
    HyperLiquid-->>CLI: request accepted / response
    rect rgba(100,150,200,0.5)
      Note over CLI,HyperEVM: derive deploy nonce
      CLI->>HyperEVM: getTransactionCount (bound search)
      HyperEVM-->>CLI: tx count
      CLI->>CLI: computeDeployNonce / brute-force
    end
    CLI->>HyperLiquid: spotFinalizeEvmContract (signed finalize)
    HyperLiquid-->>CLI: confirmation
    CLI->>DeployJSON: write hypercore.tokenIndex
    DeployJSON-->>CLI: persisted
    CLI-->>User: success / status output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • nik-suri
  • nvsriram
  • dvgui

Poem

🐰 I hopped through code and signed with flair,

Linking chains and counting nonces with care,
Validators steady, bridges shining bright,
Tokens cross over in the moonlit night,
A tiny rabbit cheers the new Hype flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding HyperCore integration commands to the ntt hype CLI tool.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/hype-hypercore-commands

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/src/hyperliquid.ts`:
- Around line 260-268: The EIP-712 type key "HyperliquidTransaction:SpotSend"
(found in the types object) uses a colon which is invalid per EIP-712/Solidity
identifiers; rename the type key to a valid identifier (e.g.,
"HyperliquidTransactionSpotSend" or "HyperliquidTransaction_SpotSend") in the
types declaration and update any other occurrences (such as the primaryType or
any references around the secondary types at the other location mentioned) to
use the new name so signing uses a colon-free type name. Ensure the symbol names
referenced (types, primaryType) are updated consistently.

In `@cli/src/index.ts`:
- Around line 2922-2933: Wrap the computeDeployNonceFromHyperEvm call in a
try/catch so RPC/lookup failures are handled as a controlled CLI error: call
computeDeployNonceFromHyperEvm(deployerAddress, hyperEvmChain.token, isTestnet)
inside try, assign to deployNonce on success and keep the existing success log,
and in catch emit a formatted CLI error (e.g. console.error(colors.red(`Failed
to auto-derive deploy nonce: ${err.message}`))) and terminate the command (throw
a new Error with that message or call process.exit(1)) so failures no longer
escape unformatted.
- Around line 2839-2843: The link-finalize CLI should not require --token-index;
remove demandOption for option("token-index") in the link-finalize command
registration and instead, when handling the command in its handler (the
link-finalize flow), if argv.tokenIndex is undefined/null, read the previously
saved token index from the deployment state/config (use the same saved field you
persist for deployments, e.g., savedTokenIndex or deployment.tokenIndex) and use
that; if neither CLI arg nor saved value exists, then error with a clear
message. Apply the same change to the other occurrence (the option block at the
second location) so both places fallback to the saved deployment token index and
only error when no value is available from either source.
- Around line 2811-2823: After a successful spotRequestEvmContract(...) call,
persist the token index into the deployment state by assigning
hypercore.tokenIndex = tokenIndex on the deployment/state object used in this
module and immediately calling the existing state persistence helper (e.g.,
saveDeploymentState/writeDeployment/persistDeployment — whichever this project
uses) to write the change; update the block after spotRequestEvmContract in
cli/src/index.ts so the code sets and persists hypercore.tokenIndex (and handle
any persistence errors) before printing the success message so follow-up
commands can rely on the saved token index.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75f77f4 and 99bbfa5.

📒 Files selected for processing (8)
  • cli/package.json
  • cli/src/deployments.ts
  • cli/src/hype/__tests__/commands.test.ts
  • cli/src/hype/__tests__/hyperliquid.test.ts
  • cli/src/hype/__tests__/validation.test.ts
  • cli/src/hype/validation.ts
  • cli/src/hyperliquid.ts
  • cli/src/index.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
cli/src/index.ts (2)

2922-2931: ⚠️ Potential issue | 🟠 Major

Handle deploy-nonce auto-derivation failures as controlled CLI errors

computeDeployNonceFromHyperEvm(...) can fail (RPC errors, rate limiting, etc.). Without a local try/catch, the command may exit with an unformatted exception/stack, breaking the otherwise consistent UX.

Proposed change (wrap RPC derivation)
           } else {
             console.log(
               colors.cyan("Auto-deriving deploy nonce from deployer address…")
             );
-            deployNonce = await computeDeployNonceFromHyperEvm(
-              deployerAddress,
-              hyperEvmChain.token,
-              isTestnet
-            );
+            try {
+              deployNonce = await computeDeployNonceFromHyperEvm(
+                deployerAddress,
+                hyperEvmChain.token,
+                isTestnet
+              );
+            } catch (error) {
+              console.error(
+                colors.red(
+                  `Failed to auto-derive deploy nonce: ${
+                    error instanceof Error ? error.message : String(error)
+                  }`
+                )
+              );
+              process.exit(1);
+              return;
+            }
             console.log(colors.cyan(`  Derived deploy nonce: ${deployNonce}`));
           }

Apply the same pattern to the link command’s auto-derivation block.

Also applies to: 3099-3108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/index.ts` around lines 2922 - 2931, The auto-derivation call to
computeDeployNonceFromHyperEvm (used to set deployNonce from deployerAddress,
hyperEvmChain.token and isTestnet) must be wrapped in a local try/catch so RPC
failures become controlled CLI errors instead of unformatted crashes; catch any
error from computeDeployNonceFromHyperEvm, log or rethrow a user-friendly error
(consistent with other commands' CLI error handling), and ensure the same
try/catch pattern is applied to the duplicate block in the link command as well.

2811-2823: ⚠️ Potential issue | 🟠 Major

Persist hypercore.tokenIndex immediately after a successful link-request

Right now step 1 succeeds but doesn’t persist state; step 2 and status/bridge-* then rely on the user re-providing context. Saving the token index right after the request makes the two-step flow more robust.

Proposed change (persist after request)
           try {
             await spotRequestEvmContract(
               privateKey,
               tokenIndex,
               address,
               evmExtraWeiDecimals,
               isTestnet
             );
+            deployments.hypercore = {
+              ...(deployments.hypercore ?? {}),
+              tokenIndex,
+            };
+            fs.writeFileSync(
+              deploymentPath,
+              JSON.stringify(deployments, null, 2) + "\n"
+            );
             console.log(
               colors.green(
                 `link-request submitted for token ${tokenIndex}. Now run 'ntt hype link-finalize --token-index ${tokenIndex}' to complete.`
               )
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/index.ts` around lines 2811 - 2823, After a successful
spotRequestEvmContract call, immediately persist the chosen token index to the
shared hypercore state (set hypercore.tokenIndex = tokenIndex) and then call an
async persistence helper (e.g., await saveHypercore() or await
persistHypercoreState()) before the console.log; if no persistence helper
exists, add one that writes the hypercore object to disk/config and await it to
ensure the two-step flow can read the tokenIndex later.
cli/src/hyperliquid.ts (1)

229-276: Verify the typed-data schema used by spotSend matches Hyperliquid’s spec

The primary type key "HyperliquidTransaction:SpotSend" is non-standard per strict EIP-712 identifier rules, but may still be what Hyperliquid expects. This is worth verifying against official docs / reference implementations, since changing it would change the signature hash.

Hyperliquid spotSend EIP-712 schema: does the primary type name include "HyperliquidTransaction:SpotSend" (with a colon), and is ethers v6 signTypedData expected to accept it?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/hyperliquid.ts` around lines 229 - 276, Confirm and align the EIP-712
primary type name used in spotSend with Hyperliquid’s official spec: check
whether the schema expects "HyperliquidTransaction:SpotSend" (including the
colon) or a standard identifier like "SpotSend"/"HyperliquidTransaction" and
update the types object accordingly; verify that ethers v6 wallet.signTypedData
accepts the chosen primary type format (colon-inclusive names) and if not,
change the primary key in the types constant and any consumers of the signature
(the Signature.from(...) call and code that sends the signed payload) so the
produced signature matches Hyperliquid's reference implementation.
🧹 Nitpick comments (2)
cli/src/hyperliquid.ts (1)

55-98: Add timeouts + safer non-JSON error handling around fetch calls

sendL1Action (and other fetches below) can hang indefinitely and res.json() can throw on non-JSON error bodies; both cases degrade CLI UX.

Sketch of a minimal hardening (timeout + robust parsing)
 async function sendL1Action(
   wallet: ethers.Wallet,
   action: object,
   isTestnet: boolean
 ): Promise<{ status?: string; response?: unknown }> {
   const apiUrl = isTestnet ? HYPERLIQUID_API.testnet : HYPERLIQUID_API.mainnet;
   const nonce = Date.now();
   const connectionId = computeL1ActionHash(action, nonce, null);

+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 30_000);
   const res = await fetch(`${apiUrl}/exchange`, {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify({
       action,
       nonce,
       signature: { r: sig.r, s: sig.s, v: sig.v },
     }),
+    signal: controller.signal,
   });
+  clearTimeout(timeout);

-  const json = (await res.json()) as { status?: string; response?: unknown };
+  // Consider: parse text then JSON.parse to keep a useful error message on non-JSON responses.
+  const json = (await res.json()) as { status?: string; response?: unknown };
   if (!res.ok || json.status === "err") {
     throw new Error(JSON.stringify(json.response ?? json));
   }
   return json;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/hyperliquid.ts` around lines 55 - 98, sendL1Action currently can hang
indefinitely and will throw if res.json() encounters non-JSON error bodies;
update sendL1Action to use an AbortController with a configurable timeout (e.g.,
5–10s) and pass its signal to fetch, ensure the controller is cleared on
completion, and change response handling to first check res.ok then attempt safe
JSON parsing (try/catch around res.json()) falling back to await res.text() for
non-JSON error bodies and include that text in the thrown Error; apply the same
pattern to the other fetch helpers in this file (identify by other functions
that call fetch) so all HTTP calls use an AbortController timeout and robust
JSON/text error handling.
cli/src/hype/validation.ts (1)

38-43: Consider trimming and returning canonical (checksummed) addresses

Right now parseEvmAddress validates but returns the original string verbatim. Returning a canonical checksummed form (via ethers.getAddress()) and trimming input tends to reduce downstream casing/whitespace issues and makes logs/config outputs consistent. This aligns with the pattern already used in parsePositiveDecimalAmount in the same file.

Proposed refactor
 export function parseEvmAddress(name: string, value: string): string {
-  if (!ethers.isAddress(value)) {
+  const trimmed = value.trim();
+  if (!ethers.isAddress(trimmed)) {
     throw new Error(`${name} must be a valid EVM address`);
   }
-  return value;
+  return ethers.getAddress(trimmed);
 }

In ethers v6, getAddress() safely normalizes and returns the EIP-55 checksummed address format. Since you validate with isAddress() first, getAddress() will not throw—it will only return the canonical checksummed form.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/hype/validation.ts` around lines 38 - 43, parseEvmAddress currently
only validates the input but returns it as-is; trim whitespace and return the
canonical EIP-55 checksummed address using ethers.getAddress(value.trim())
instead of the raw value. Keep the existing validation with ethers.isAddress (or
replace it by try/catch around getAddress), call .trim() on the incoming value
first, and return the result of ethers.getAddress so downstream code receives a
normalized checksummed address; update the parseEvmAddress function accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/src/hype/__tests__/commands.test.ts`:
- Around line 13-15: The Biome linter flags the ANSI ESC regex in stripAnsi as
containing control characters; update the stripAnsi implementation to either
call Node's built-in stripVTControlCharacters helper or suppress the Biome rule
for that specific regex line: locate the stripAnsi function and replace the
regex logic with a call to stripVTControlCharacters(input) (importing it from
'node:util' or the appropriate module), or add a single-line Biome suppression
comment immediately above the regex (e.g., a lint-disable for
lint/suspicious/noControlCharactersInRegex) while keeping the function name
stripAnsi and behavior identical.

In `@cli/src/hyperliquid.ts`:
- Around line 383-390: The code assumes decimals() returns a number but in
ethers v6 it returns a bigint; update the handling around token.decimals() (the
call in the hyperliquid flow creating the ethers.Contract named token) to
convert the returned bigint to a safe JS number before calling
ethers.parseUnits(amount, decimals): e.g. use ethers.toNumber(decimalsBigint) or
Number(decimalsBigint) with a safety check to ensure it's a safe integer, then
pass that numeric value into ethers.parseUnits; adjust the variable name (e.g.,
decimalsBigint -> decimals) and types accordingly.

In `@cli/src/index.ts`:
- Around line 2754-2759: The current isTestnet computation (using
argv["testnet"] ?: deployments.network === "Testnet") allows unknown networks
like "Devnet" to silently map to mainnet; change the logic to use the explicit
flag when provided, otherwise treat any non-"Mainnet" deployments.network as
testnet (e.g. isTestnet = argv["testnet"] !== undefined ?
Boolean(argv["testnet"]) : deployments.network !== "Mainnet"); update this
pattern wherever isTestnet is computed (referencing loadConfig, deployments,
argv["testnet"], and isTestnet) and for the bridge-in command either apply the
same guard or add a --testnet override for parity.

---

Duplicate comments:
In `@cli/src/hyperliquid.ts`:
- Around line 229-276: Confirm and align the EIP-712 primary type name used in
spotSend with Hyperliquid’s official spec: check whether the schema expects
"HyperliquidTransaction:SpotSend" (including the colon) or a standard identifier
like "SpotSend"/"HyperliquidTransaction" and update the types object
accordingly; verify that ethers v6 wallet.signTypedData accepts the chosen
primary type format (colon-inclusive names) and if not, change the primary key
in the types constant and any consumers of the signature (the
Signature.from(...) call and code that sends the signed payload) so the produced
signature matches Hyperliquid's reference implementation.

In `@cli/src/index.ts`:
- Around line 2922-2931: The auto-derivation call to
computeDeployNonceFromHyperEvm (used to set deployNonce from deployerAddress,
hyperEvmChain.token and isTestnet) must be wrapped in a local try/catch so RPC
failures become controlled CLI errors instead of unformatted crashes; catch any
error from computeDeployNonceFromHyperEvm, log or rethrow a user-friendly error
(consistent with other commands' CLI error handling), and ensure the same
try/catch pattern is applied to the duplicate block in the link command as well.
- Around line 2811-2823: After a successful spotRequestEvmContract call,
immediately persist the chosen token index to the shared hypercore state (set
hypercore.tokenIndex = tokenIndex) and then call an async persistence helper
(e.g., await saveHypercore() or await persistHypercoreState()) before the
console.log; if no persistence helper exists, add one that writes the hypercore
object to disk/config and await it to ensure the two-step flow can read the
tokenIndex later.

---

Nitpick comments:
In `@cli/src/hype/validation.ts`:
- Around line 38-43: parseEvmAddress currently only validates the input but
returns it as-is; trim whitespace and return the canonical EIP-55 checksummed
address using ethers.getAddress(value.trim()) instead of the raw value. Keep the
existing validation with ethers.isAddress (or replace it by try/catch around
getAddress), call .trim() on the incoming value first, and return the result of
ethers.getAddress so downstream code receives a normalized checksummed address;
update the parseEvmAddress function accordingly.

In `@cli/src/hyperliquid.ts`:
- Around line 55-98: sendL1Action currently can hang indefinitely and will throw
if res.json() encounters non-JSON error bodies; update sendL1Action to use an
AbortController with a configurable timeout (e.g., 5–10s) and pass its signal to
fetch, ensure the controller is cleared on completion, and change response
handling to first check res.ok then attempt safe JSON parsing (try/catch around
res.json()) falling back to await res.text() for non-JSON error bodies and
include that text in the thrown Error; apply the same pattern to the other fetch
helpers in this file (identify by other functions that call fetch) so all HTTP
calls use an AbortController timeout and robust JSON/text error handling.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99bbfa5 and 390f41f.

📒 Files selected for processing (5)
  • .github/workflows/cli.yml
  • cli/src/hype/__tests__/commands.test.ts
  • cli/src/hype/validation.ts
  • cli/src/hyperliquid.ts
  • cli/src/index.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
cli/src/index.ts (2)

2755-2758: ⚠️ Potential issue | 🔴 Critical

Guard unsupported deployment networks before deriving Hyperliquid environment.

isTestnet currently treats anything except "Testnet" as mainnet. On Line 3252 (and similarly in the listed ranges), this can silently route Devnet/unknown configs to mainnet.

Suggested fix
+function resolveHyperliquidTestnetFlag(
+  deploymentNetwork: Config["network"],
+  override?: boolean
+): boolean {
+  if (override !== undefined) return override;
+  if (deploymentNetwork === "Testnet") return true;
+  if (deploymentNetwork === "Mainnet") return false;
+  throw new Error(
+    `Unsupported deployment.network "${deploymentNetwork}" for Hyperliquid commands. Use --testnet/--testnet=false explicitly.`
+  );
+}
-const isTestnet =
-  argv["testnet"] !== undefined
-    ? argv["testnet"]
-    : deployments.network === "Testnet";
+let isTestnet: boolean;
+try {
+  isTestnet = resolveHyperliquidTestnetFlag(
+    deployments.network,
+    argv["testnet"] as boolean | undefined
+  );
+} catch (error) {
+  console.error(
+    colors.red(error instanceof Error ? error.message : String(error))
+  );
+  process.exit(1);
+  return;
+}

Also applies to: 2873-2876, 3036-3039, 3252-3252, 3301-3304, 3405-3408

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/index.ts` around lines 2755 - 2758, The isTestnet derivation
currently treats any non-"Testnet" value as mainnet; update the logic wherever
isTestnet is computed (e.g., the assignment using argv["testnet"] and
deployments.network) to explicitly accept only known networks (e.g., "Testnet"
-> testnet, "Mainnet" -> mainnet), handle "Devnet" or other supported values
explicitly, and throw or log an error for unknown/unsupported
deployments.network values instead of defaulting to mainnet; apply this change
to all occurrences (the isTestnet computation at the shown diff and the other
ranges mentioned) so Hyperliquid environment selection never silently routes
unknown configs to mainnet.

3130-3138: ⚠️ Potential issue | 🟠 Major

Handle nonce auto-derivation errors in hype link with formatted CLI failure.

Unlike link-finalize, the combined link flow does not catch errors from computeDeployNonceFromHyperEvm, so RPC/lookup failures can escape unformatted.

Suggested fix
           } else {
             console.log(
               colors.cyan("Auto-deriving deploy nonce from deployer address…")
             );
-            deployNonce = await computeDeployNonceFromHyperEvm(
-              deployerAddress,
-              address,
-              isTestnet
-            );
+            try {
+              deployNonce = await computeDeployNonceFromHyperEvm(
+                deployerAddress,
+                address,
+                isTestnet
+              );
+            } catch (err) {
+              console.error(
+                colors.red(
+                  `Failed to auto-derive deploy nonce: ${err instanceof Error ? err.message : String(err)}`
+                )
+              );
+              process.exit(1);
+              return;
+            }
             console.log(colors.cyan(`  Derived deploy nonce: ${deployNonce}`));
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/index.ts` around lines 3130 - 3138, Wrap the await
computeDeployNonceFromHyperEvm(deployerAddress, address, isTestnet) call in a
try/catch so RPC/lookup failures are caught; on error invoke the same formatted
CLI failure handler used by the link-finalize flow (reuse the existing error
handler/import) to print a consistent formatted failure and exit instead of
letting the error escape, and preserve the original error details in the handler
call while leaving deployNonce unchanged on failure.
🧹 Nitpick comments (1)
cli/src/index.ts (1)

3092-3100: Persist hypercore.tokenIndex immediately after successful step 1 in combined link.

If request succeeds but finalize fails, the token index is currently not saved until the end, making recovery less ergonomic than link-request + link-finalize.

Suggested refactor
           try {
             await spotRequestEvmContract(
               privateKey,
               tokenIndex,
               address,
               evmExtraWeiDecimals,
               isTestnet
             );
+            // Persist after step 1 so recovery commands can continue on partial success
+            const raw = JSON.parse(fs.readFileSync(deploymentPath).toString());
+            raw.hypercore = {
+              ...(raw.hypercore ?? {}),
+              tokenIndex,
+            };
+            fs.writeFileSync(
+              deploymentPath,
+              JSON.stringify(raw, null, 2) + "\n"
+            );
           } catch (err) {
             console.error(
               colors.red(
                 `link-request failed: ${err instanceof Error ? err.message : String(err)}`
               )
             );
             process.exit(1);
           }

Also applies to: 3162-3168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/index.ts` around lines 3092 - 3100, After a successful call to
spotRequestEvmContract in the combined link flow, persist the incremented
hypercore.tokenIndex immediately instead of waiting until finalize; update the
code in the try block that calls spotRequestEvmContract (and the analogous block
around the other spotRequest call at lines ~3162–3168) to write the new
tokenIndex to storage right after the call succeeds (e.g., call the existing
persist/save routine for hypercore.tokenIndex or invoke the same code path used
by link-request), so that if finalize fails the tokenIndex has already been
recorded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cli/src/index.ts`:
- Around line 2755-2758: The isTestnet derivation currently treats any
non-"Testnet" value as mainnet; update the logic wherever isTestnet is computed
(e.g., the assignment using argv["testnet"] and deployments.network) to
explicitly accept only known networks (e.g., "Testnet" -> testnet, "Mainnet" ->
mainnet), handle "Devnet" or other supported values explicitly, and throw or log
an error for unknown/unsupported deployments.network values instead of
defaulting to mainnet; apply this change to all occurrences (the isTestnet
computation at the shown diff and the other ranges mentioned) so Hyperliquid
environment selection never silently routes unknown configs to mainnet.
- Around line 3130-3138: Wrap the await
computeDeployNonceFromHyperEvm(deployerAddress, address, isTestnet) call in a
try/catch so RPC/lookup failures are caught; on error invoke the same formatted
CLI failure handler used by the link-finalize flow (reuse the existing error
handler/import) to print a consistent formatted failure and exit instead of
letting the error escape, and preserve the original error details in the handler
call while leaving deployNonce unchanged on failure.

---

Nitpick comments:
In `@cli/src/index.ts`:
- Around line 3092-3100: After a successful call to spotRequestEvmContract in
the combined link flow, persist the incremented hypercore.tokenIndex immediately
instead of waiting until finalize; update the code in the try block that calls
spotRequestEvmContract (and the analogous block around the other spotRequest
call at lines ~3162–3168) to write the new tokenIndex to storage right after the
call succeeds (e.g., call the existing persist/save routine for
hypercore.tokenIndex or invoke the same code path used by link-request), so that
if finalize fails the tokenIndex has already been recorded.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 390f41f and 056aaf3.

📒 Files selected for processing (1)
  • cli/src/index.ts

Rebased onto main's restructured CLI layout and ported all HyperCore
integration commands into the new commands/hype.ts + evm/hyperliquid.ts
module structure.

Commands added under `ntt hype`:
- link: links a HyperCore spot token to its HyperEVM ERC-20 contract.
  Runs request + finalize by default; --only-finalize skips the request
  step and reads tokenIndex from deployment.json if --token-index is omitted.
- bridge-in: bridges tokens from HyperEVM into HyperCore via asset bridge
- bridge-out: bridges tokens from HyperCore back to HyperEVM via spotSend
- status: shows HyperCore token index, asset bridge address, and token string

Also adds:
- HyperCoreConfig type + hypercore field to deployments.ts Config
- evm/hyperliquid.ts: spotRequestEvmContract, spotFinalizeEvmContract,
  spotSend, bridgeIn, computeAssetBridge, computeDeployNonce,
  computeDeployNonceFromHyperEvm, getDeployerAddress, getSpotTokenString
- hype/validation.ts: parseIntegerInRange, parsePositiveDecimalAmount,
  parseEvmAddress helpers
- Unit tests for hyperliquid helpers and validation helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@evgeniko evgeniko force-pushed the feat/hype-hypercore-commands branch from 68d351b to 65e976a Compare February 26, 2026 14:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/src/commands/hype.ts (1)

8-626: ⚠️ Potential issue | 🟡 Minor

Prettier check is currently failing for this file.

CI reports formatting issues in cli/src/commands/hype.ts; run Prettier before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/hype.ts` around lines 8 - 626, Prettier formatting errors in
cli/src/commands/hype.ts are failing CI; run Prettier (or your project's format
script) on the file and commit the changes so the file matches the repository
formatting. Locate the createHypeCommand function (and its nested command
builders like "set-big-blocks", "link", "bridge-in", "bridge-out", "status") to
ensure the entire exported command definition is formatted; running the
project's formatter will update spacing, imports, and trailing newlines
consistently and resolve the CI failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/src/commands/hype.ts`:
- Around line 160-190: The guard that errors when
deployments.chains["HyperEVM"]?.token is missing blocks valid flows when users
pass --only-finalize or --deploy-nonce; update the check in hype.ts so it only
enforces presence of hyperEvmChain.token when you actually need to parse the
HyperEVM token address (i.e., when not running with argv["only-finalize"] or
argv["deploy-nonce"]). Concretely, move or conditionalize the early-return that
references hyperEvmChain/token so that parseEvmAddress("HyperEVM token",
hyperEvmChain.token) and the token-index resolution are only executed when those
flags are false, and apply the same conditional change to the other occurrence
around the code referenced at lines 266-274 (the other HyperEVM token lookup).

In `@cli/src/evm/hyperliquid.ts`:
- Around line 83-91: The fetch calls in hyperliquid.ts (including the POST to
`${apiUrl}/exchange` inside the function that builds action/nonce/signature, and
the other fetches used by spotSend and getSpotTokenString) lack timeouts and can
hang; introduce and use a shared fetch-with-timeout helper (e.g., wrap fetch in
a helper like fetchWithTimeout) and replace raw fetch(...) invocations in the
functions spotSend, getSpotTokenString, and the exchange POST block with calls
to that helper, passing a sensible timeout value and ensuring errors/timeouts
are propagated consistently to callers.

---

Outside diff comments:
In `@cli/src/commands/hype.ts`:
- Around line 8-626: Prettier formatting errors in cli/src/commands/hype.ts are
failing CI; run Prettier (or your project's format script) on the file and
commit the changes so the file matches the repository formatting. Locate the
createHypeCommand function (and its nested command builders like
"set-big-blocks", "link", "bridge-in", "bridge-out", "status") to ensure the
entire exported command definition is formatted; running the project's formatter
will update spacing, imports, and trailing newlines consistently and resolve the
CI failure.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 056aaf3 and 65e976a.

📒 Files selected for processing (7)
  • cli/src/__tests__/cli-help.test.ts
  • cli/src/commands/hype.ts
  • cli/src/deployments.ts
  • cli/src/evm/hyperliquid.ts
  • cli/src/hype/__tests__/hyperliquid.test.ts
  • cli/src/hype/__tests__/validation.test.ts
  • cli/src/hype/validation.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • cli/src/deployments.ts
  • cli/src/hype/validation.ts
  • cli/src/hype/tests/hyperliquid.test.ts

evgeniko and others added 2 commits February 26, 2026 09:34
All raw fetch() calls in evm/hyperliquid.ts lacked timeouts and could
hang indefinitely. Introduce a shared fetchWithTimeout helper (30s default,
AbortController-based) and replace the four bare fetch() call sites:
sendL1Action, setHyperEvmBigBlocks, spotSend, and getSpotTokenString.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'manual and hype subcommands are listed' test was the only one in
cli-help.test.ts without an explicit timeout, relying on bun's default
5000ms. Adding imports to commands/hype.ts increased CLI startup time
enough to cause a flaky timeout in ts-sdk-ci. Apply the same
SUBPROCESS_TIMEOUT (30s) used by all other subprocess tests in the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
cli/src/commands/hype.ts (1)

159-190: ⚠️ Potential issue | 🟠 Major

Allow --only-finalize --deploy-nonce without requiring HyperEVM token config.

Line 160 and Line 189 currently force token-address presence/validation even when finalize can run with explicit nonce, which blocks a valid CLI flow.

🔧 Proposed fix
             const hyperEvmChain = deployments.chains["HyperEVM"];
-            if (!hyperEvmChain?.token) {
+            const needsTokenAddress =
+              !onlyFinalize || argv["deploy-nonce"] === undefined;
+            if (needsTokenAddress && !hyperEvmChain?.token) {
               console.error(
                 colors.red(
                   "No HyperEVM chain config found in deployment.json. Add HyperEVM chain first."
                 )
               );
               process.exit(1);
               return;
             }

             // Resolve and validate token index and EVM address
             let tokenIndex: number;
-            let address: string;
+            let address: string | undefined;
             try {
               const rawTokenIndex =
                 argv["token-index"] !== undefined
                   ? (argv["token-index"] as number)
                   : deployments.hypercore?.tokenIndex;
@@
               tokenIndex = parseIntegerInRange(
                 "--token-index",
                 rawTokenIndex,
                 0,
                 0xffff
               );
-              address = parseEvmAddress("HyperEVM token", hyperEvmChain.token);
+              if (needsTokenAddress) {
+                address = parseEvmAddress("HyperEVM token", hyperEvmChain!.token);
+              }
             } catch (error) {
@@
                 await spotRequestEvmContract(
                   privateKey,
                   tokenIndex,
-                  address,
+                  address!,
                   evmExtraWeiDecimals,
                   isTestnet
                 );
@@
                 deployNonce = await computeDeployNonceFromHyperEvm(
                   deployerAddress,
-                  address,
+                  address!,
                   isTestnet
                 );

Also applies to: 247-273

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/hype.ts` around lines 159 - 190, The code currently always
requires HyperEVM token config by reading hyperEvmChain.token and calling
parseEvmAddress; change the logic in the block around hyperEvmChain, tokenIndex
and address so that token resolution/validation (references: hyperEvmChain,
tokenIndex, address, parseEvmAddress, argv["token-index"],
deployments.hypercore?.tokenIndex) only runs when the command actually needs a
token — i.e., skip token lookup and parseEvmAddress when the user passed the
finalize-only flow with an explicit deploy nonce (check argv["only-finalize"]
and argv["deploy-nonce"] or equivalent flags) so the CLI can proceed without
HyperEVM token config; implement this by gating the token validation in an
if-statement (or early return) that ensures tokenIndex/address are only computed
when not (only-finalize && deploy-nonce provided).
cli/src/evm/hyperliquid.ts (1)

325-333: ⚠️ Potential issue | 🔴 Critical

Use a valid EIP-712 struct type identifier.

Line 326 uses "HyperliquidTransaction:SpotSend". The : makes this a non-identifier type name for EIP-712, which can break signTypedData in the bridge-out path.

🔧 Proposed fix
   const types = {
-    "HyperliquidTransaction:SpotSend": [
+    HyperliquidTransactionSpotSend: [
       { name: "hyperliquidChain", type: "string" },
       { name: "destination", type: "string" },
       { name: "token", type: "string" },
       { name: "amount", type: "string" },
       { name: "time", type: "uint64" },
     ],
   };
Does EIP-712 require struct type names to be valid identifiers, and is ":" invalid in a type name like "HyperliquidTransaction:SpotSend" when using ethers v6 signTypedData?

Also applies to: 343-343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/evm/hyperliquid.ts` around lines 325 - 333, The EIP-712 struct type
name "HyperliquidTransaction:SpotSend" is invalid because ":" is not a valid
identifier; update the types map to use a valid identifier (e.g.,
"HyperliquidTransactionSpotSend" or "HyperliquidTransaction_SpotSend") and
change every place that references the old name to the new identifier (look for
the types constant and any usages in signTypedData or message/type definitions
that currently reference "HyperliquidTransaction:SpotSend" — also update the
other occurrence mentioned around line 343). Ensure the new identifier is used
consistently in the types object and the typed data payload so ethers v6
signTypedData accepts it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cli/src/commands/hype.ts`:
- Around line 159-190: The code currently always requires HyperEVM token config
by reading hyperEvmChain.token and calling parseEvmAddress; change the logic in
the block around hyperEvmChain, tokenIndex and address so that token
resolution/validation (references: hyperEvmChain, tokenIndex, address,
parseEvmAddress, argv["token-index"], deployments.hypercore?.tokenIndex) only
runs when the command actually needs a token — i.e., skip token lookup and
parseEvmAddress when the user passed the finalize-only flow with an explicit
deploy nonce (check argv["only-finalize"] and argv["deploy-nonce"] or equivalent
flags) so the CLI can proceed without HyperEVM token config; implement this by
gating the token validation in an if-statement (or early return) that ensures
tokenIndex/address are only computed when not (only-finalize && deploy-nonce
provided).

In `@cli/src/evm/hyperliquid.ts`:
- Around line 325-333: The EIP-712 struct type name
"HyperliquidTransaction:SpotSend" is invalid because ":" is not a valid
identifier; update the types map to use a valid identifier (e.g.,
"HyperliquidTransactionSpotSend" or "HyperliquidTransaction_SpotSend") and
change every place that references the old name to the new identifier (look for
the types constant and any usages in signTypedData or message/type definitions
that currently reference "HyperliquidTransaction:SpotSend" — also update the
other occurrence mentioned around line 343). Ensure the new identifier is used
consistently in the types object and the typed data payload so ethers v6
signTypedData accepts it.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65e976a and 6211656.

📒 Files selected for processing (2)
  • cli/src/commands/hype.ts
  • cli/src/evm/hyperliquid.ts

Copy link
Copy Markdown
Contributor

@dvgui dvgui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@evgeniko evgeniko merged commit fee796a into main Feb 26, 2026
23 of 25 checks passed
@evgeniko evgeniko deleted the feat/hype-hypercore-commands branch February 26, 2026 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants