feat(cli): add ntt hype commands for HyperCore integration#827
feat(cli): add ntt hype commands for HyperCore integration#827
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
cli/package.jsoncli/src/deployments.tscli/src/hype/__tests__/commands.test.tscli/src/hype/__tests__/hyperliquid.test.tscli/src/hype/__tests__/validation.test.tscli/src/hype/validation.tscli/src/hyperliquid.tscli/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
cli/src/index.ts (2)
2922-2931:⚠️ Potential issue | 🟠 MajorHandle deploy-nonce auto-derivation failures as controlled CLI errors
computeDeployNonceFromHyperEvm(...)can fail (RPC errors, rate limiting, etc.). Without a localtry/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
linkcommand’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 | 🟠 MajorPersist
hypercore.tokenIndeximmediately after a successfullink-requestRight 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 byspotSendmatches Hyperliquid’s specThe 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 andres.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) addressesRight now
parseEvmAddressvalidates but returns the original string verbatim. Returning a canonical checksummed form (viaethers.getAddress()) and trimming input tends to reduce downstream casing/whitespace issues and makes logs/config outputs consistent. This aligns with the pattern already used inparsePositiveDecimalAmountin 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 withisAddress()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
📒 Files selected for processing (5)
.github/workflows/cli.ymlcli/src/hype/__tests__/commands.test.tscli/src/hype/validation.tscli/src/hyperliquid.tscli/src/index.ts
There was a problem hiding this comment.
♻️ Duplicate comments (2)
cli/src/index.ts (2)
2755-2758:⚠️ Potential issue | 🔴 CriticalGuard unsupported deployment networks before deriving Hyperliquid environment.
isTestnetcurrently 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 | 🟠 MajorHandle nonce auto-derivation errors in
hype linkwith formatted CLI failure.Unlike
link-finalize, the combinedlinkflow does not catch errors fromcomputeDeployNonceFromHyperEvm, 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: Persisthypercore.tokenIndeximmediately after successful step 1 in combinedlink.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.
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>
68d351b to
65e976a
Compare
There was a problem hiding this comment.
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 | 🟡 MinorPrettier 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
📒 Files selected for processing (7)
cli/src/__tests__/cli-help.test.tscli/src/commands/hype.tscli/src/deployments.tscli/src/evm/hyperliquid.tscli/src/hype/__tests__/hyperliquid.test.tscli/src/hype/__tests__/validation.test.tscli/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
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>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
cli/src/commands/hype.ts (1)
159-190:⚠️ Potential issue | 🟠 MajorAllow
--only-finalize --deploy-noncewithout 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 | 🔴 CriticalUse 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 breaksignTypedDatain 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.
Summary
Adds a new
ntt hypecommand 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
link— the single command for linking a HyperCore spot token to an ERC-20 contract. By default runs both steps in sequence:requestEvmContract— sends aspotDeploy/requestEvmContractL1 action to HyperCore. The address is force-lowercased before msgpack encoding (required for correct signer recovery). PersiststokenIndextodeployment.jsonon success.finalizeEvmContract— sends afinalizeEvmContractL1 action (top-level, not nested underspotDeploy). Automatically resolves the ERC-20 deploy nonce by querying the deployer's tx count from HyperEVM and iterating CREATE addresses.Pass
--only-finalizeto skip the request step and run only the finalize step (e.g. if the request was already submitted).--token-indexfalls back todeployment.hypercore.tokenIndexwhen--only-finalizeis used.bridge-in— callstransfer(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 aspotSenduser-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 ID421614/42161, withtimeas nonce.status— displays the HyperCore token index, asset bridge address, szDecimals, weiDecimals, and token string for this deployment.Deployment config changes
Added optional
hypercorefield to theConfigtype:tokenIndexis automatically written todeployment.jsonbylinkon success. Existing deployment files without this field continue to work unchanged.Input validation
New
cli/src/hype/validation.tswith helpers used throughout allhypecommands:parseIntegerInRange— validates integer ranges (token index must be a uint16, i.e. ≤ 65535)parsePositiveDecimalAmount— rejects zero and non-numeric amountsparseEvmAddress— validates0x...address formatReliability
All HyperLiquid API
fetch()calls are wrapped in afetchWithTimeouthelper (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 helpershyperliquid.test.ts: unit tests forcomputeAssetBridge,computeDeployNonce, andcomputeDeployNonceFromHyperEvm(mocksgetTransactionCountat 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 WSVbridge-out 1.0→spotSendaccepted; balance returned to 0.0 WSVstatus→ correct asset bridge address and token index displayedlink→ reached HyperLiquid API (returned expected domain error "Address is already used as EVM contract", confirming correct signing and payload shape)Test plan
bun test srcincli/— all tests should passntt hype status --path <deployment>with a deployment containinghypercore.tokenIndexntt hype bridge-in 0 --path <deployment>— should fail with "must be greater than zero"ntt hype link --token-index 70000 --path <deployment>— should fail with "must be <= 65535"ntt hype link --only-finalize --path <deployment>— should read token index from deployment.json and run only the finalize stepSummary by CodeRabbit
New Features
Bug Fixes / Reliability
Tests