diff --git a/.changeset/twenty-chairs-sparkle.md b/.changeset/twenty-chairs-sparkle.md new file mode 100644 index 0000000000..077e8eddca --- /dev/null +++ b/.changeset/twenty-chairs-sparkle.md @@ -0,0 +1,6 @@ +--- +"@layerzerolabs/hyperliquid-composer": patch +"@layerzerolabs/oft-hyperliquid-example": patch +--- + +sdk updates for quote asset improvement diff --git a/examples/oft-hyperliquid/HYPERLIQUID.README.md b/examples/oft-hyperliquid/HYPERLIQUID.README.md index bddb8e49e1..3073569bcb 100644 --- a/examples/oft-hyperliquid/HYPERLIQUID.README.md +++ b/examples/oft-hyperliquid/HYPERLIQUID.README.md @@ -123,7 +123,16 @@ npx @layerzerolabs/hyperliquid-composer trading-fee \ ### 7. Enable Quote Token Capability (Optional) -Enables your token to be used as a quote asset for trading pairs. **Dependency:** Requires specific trading fee share value (see Step 6 above). See: [Permissionless Spot Quote Assets](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) +Enables your token to be used as a quote asset for trading pairs. + +> ⚠️ **Important**: Review the complete [Quote Assets (Fee Tokens)](https://github.com/LayerZero-Labs/devtools/tree/main/packages/hyperliquid-composer/HYPERLIQUID.README.md#quote-assets-fee-tokens) section in the main documentation for: +> +> - Mainnet requirements (technical and liquidity) +> - Testnet requirements (50 HYPE stake + active order book) +> - Order book maintenance for `HYPE/YOUR_ASSET` pair +> - Composer selection guidance (use `FeeToken` variant for quote assets) + +**Dependency:** Requires trading fee share configuration (see Step 6 above). ```bash npx @layerzerolabs/hyperliquid-composer enable-quote-token \ @@ -161,6 +170,8 @@ npx @layerzerolabs/hyperliquid-composer request-evm-contract \ ### 2. Finalize EVM Contract Link +#### Hypercore action method + ```bash npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ --token-index \ @@ -169,6 +180,20 @@ npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ [--log-level {info | verbose}] ``` +#### CoreWriter Method + +Alternative method using direct CoreWriter interaction. This is useful if you prefer to use Foundry's `cast` command. + +```bash +npx @layerzerolabs/hyperliquid-composer finalize-evm-contract-corewriter \ + --token-index \ + --nonce \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] +``` + +The command will output the calldata and a ready-to-use `cast send` command for finalizing the EVM contract link via the CoreWriter precompile at `0x3333333333333333333333333333333333333333`. + ## Post-Launch Management ### Freeze/Unfreeze Users @@ -264,6 +289,25 @@ npx @layerzerolabs/hyperliquid-composer spot-auction-status \ [--log-level {info | verbose}] ``` +### Check if Token is Quote Asset + +Check if a specific token is a quote asset, or list all quote assets when no token index is provided. Quote assets are automatically paired with HYPE when promoted by the Hyperliquid protocol. + +```bash +# List all quote assets +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] + +# Check if specific token is a quote asset +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --filter-token-index \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] +``` + +The command returns `yes` or `no` when checking a specific token, or lists all quote assets when no token index is provided. + ## Utilities ### Convert Token Index to Bridge Address @@ -490,11 +534,11 @@ npx @layerzerolabs/hyperliquid-composer trading-fee \ This step enables the token to be used as a quote asset for trading pairs. This allows other tokens to form trading pairs against your token (e.g., TOKEN/YOUR_TOKEN instead of only YOUR_TOKEN/USDC). -> ⚠️ **Requirements**: +> ⚠️ **Important**: Review the complete [Quote Assets (Fee Tokens)](../../packages/hyperliquid-composer/HYPERLIQUID.README.md#quote-assets-fee-tokens) section in the main documentation for detailed requirements, including: > -> - Requires specific trading fee share value (see Step 6/7 above) -> - Review all requirements at: [Permissionless Spot Quote Assets](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) -> - Contact the Hyperliquid team for the most up-to-date information +> - Mainnet: Complete technical and liquidity requirements +> - Testnet: Simplified requirements (50 HYPE stake + active order book) +> - Order book maintenance for `HYPE/YOUR_ASSET` pair after enablement > > 📝 **Note**: This can be executed after the trading fee share is set and even after deployment and linking are complete. @@ -506,6 +550,18 @@ npx @layerzerolabs/hyperliquid-composer enable-quote-token \ [--log-level {info | verbose}] ``` +**Prerequisites:** + +- Trading fee share must be set (see Step 6/7 above) +- **Testnet**: 50 HYPE staked + active BUY and SELL limit orders on your token's order book +- **Mainnet**: All requirements per [Hyperliquid's documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) + +**After Execution:** + +- A `HYPE/YOUR_ASSET` trading pair is automatically created +- You must maintain order book requirements for the new `HYPE/ASSET` pair +- Verify quote asset status with the `list-quote-asset` command + ### Step 6.7/7 `enableAlignedQuoteToken` (Optional) This step enables the token to be used as an aligned quote asset for trading pairs. Aligned quote tokens have special properties and requirements different from regular quote tokens. @@ -559,6 +615,19 @@ npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ --private-key $PRIVATE_KEY_HYPERLIQUID ``` +**Alternative: Using CoreWriter directly with Foundry** + +If you prefer to use Foundry's `cast` command, you can generate the calldata and send the transaction directly: + +```bash +npx @layerzerolabs/hyperliquid-composer finalize-evm-contract-corewriter \ + --token-index \ + --nonce \ + --network {testnet | mainnet} +``` + +This will output the calldata and a ready-to-use `cast send` command that you can execute directly. + ## Deploy the Composer While the composer could have been deployed at any point in time due to its statelessness, it is technically the final step of the deployment process. The following script automatically handles the block switching for you. diff --git a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeAbstraction.ts b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeAbstraction.ts index c02aa206ec..4e2b3faa22 100644 --- a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeAbstraction.ts +++ b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeAbstraction.ts @@ -7,6 +7,7 @@ import { CHAIN_IDS, EthersSigner, getCoreSpotDeployment, + isQuoteAsset, useBigBlock, useSmallBlock, } from '@layerzerolabs/hyperliquid-composer' @@ -73,6 +74,30 @@ const deploy: DeployFunction = async (hre) => { // Validates and returns the native spot const hip1Token = getCoreSpotDeployment(coreSpotIndex, isTestnet) + // Check if the token is a quote asset - warn if it is + console.log(`\nChecking if token ${coreSpotIndex} is a quote asset...`) + const { isQuoteAsset: isQuote, tokenName } = await isQuoteAsset(isTestnet, parseInt(coreSpotIndex), loglevel) + + if (isQuote) { + console.warn(`\n[WARNING] Token ${coreSpotIndex}${tokenName ? ` (${tokenName})` : ''} IS a quote asset!`) + console.warn(`For quote assets, you should use MyHyperLiquidComposer_FeeToken instead.`) + console.warn(`FeeToken composer provides automatic user activation using the token itself.\n`) + + const { continueAnyway } = await inquirer.prompt([ + { + type: 'confirm', + name: 'continueAnyway', + message: 'Do you want to continue with FeeAbstraction anyway?', + default: false, + }, + ]) + + if (!continueAnyway) { + console.log('Deployment cancelled.') + process.exit(0) + } + } + const { address: address_oft } = await hre.deployments.get(contractName_oft).catch(async () => { console.log(`Deployment file for ${contractName_oft}.json in deployments/${networkName} not found`) const { proceedWithOFTAddress } = await inquirer.prompt([ diff --git a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeToken.ts b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeToken.ts index 8bee1b6686..18874749dc 100644 --- a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeToken.ts +++ b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_FeeToken.ts @@ -7,6 +7,7 @@ import { CHAIN_IDS, EthersSigner, getCoreSpotDeployment, + isQuoteAsset, useBigBlock, useSmallBlock, } from '@layerzerolabs/hyperliquid-composer' @@ -52,6 +53,23 @@ const deploy: DeployFunction = async (hre) => { // Validates and returns the native spot const hip1Token = getCoreSpotDeployment(coreSpotIndex, isTestnet) + // Check if the token is a quote asset - required for FeeToken composer + console.log(`\nChecking if token ${coreSpotIndex} is a quote asset...`) + const { isQuoteAsset: isQuote, tokenName } = await isQuoteAsset(isTestnet, parseInt(coreSpotIndex), loglevel) + + if (!isQuote) { + console.error(`\n[ERROR] Token ${coreSpotIndex} is NOT a quote asset!`) + console.error(`The FeeToken composer can ONLY be used with quote assets (tokens paired with HYPE).`) + console.error(`\nThis token is not a quote asset. Please use one of the following composers instead:`) + console.error(` - MyHyperliquidComposer (regular)`) + console.error(` - MyHyperLiquidComposer_FeeAbstraction`) + console.error(` - MyHyperLiquidComposer_Recoverable`) + throw new Error(`Token ${coreSpotIndex} is not a quote asset. FeeToken composer requires a quote asset.`) + } + + console.log(`[OK] Confirmed: Token ${coreSpotIndex}${tokenName ? ` (${tokenName})` : ''} is a quote asset`) + console.log(` FeeToken composer can be used for automatic user activation.\n`) + const { address: address_oft } = await hre.deployments.get(contractName_oft).catch(async () => { console.log(`Deployment file for ${contractName_oft}.json in deployments/${networkName} not found`) const { proceedWithOFTAddress } = await inquirer.prompt([ diff --git a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_Recoverable.ts b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_Recoverable.ts index 6a7805994d..66195d169d 100644 --- a/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_Recoverable.ts +++ b/examples/oft-hyperliquid/deploy/MyHyperLiquidComposer_Recoverable.ts @@ -7,6 +7,7 @@ import { CHAIN_IDS, EthersSigner, getCoreSpotDeployment, + isQuoteAsset, useBigBlock, useSmallBlock, } from '@layerzerolabs/hyperliquid-composer' @@ -54,6 +55,30 @@ const deploy: DeployFunction = async (hre) => { // Validates and returns the native spot const hip1Token = getCoreSpotDeployment(coreSpotIndex, isTestnet) + // Check if the token is a quote asset - warn if it is + console.log(`\nChecking if token ${coreSpotIndex} is a quote asset...`) + const { isQuoteAsset: isQuote, tokenName } = await isQuoteAsset(isTestnet, parseInt(coreSpotIndex), loglevel) + + if (isQuote) { + console.warn(`\n[WARNING] Token ${coreSpotIndex}${tokenName ? ` (${tokenName})` : ''} IS a quote asset!`) + console.warn(`For quote assets, you should use MyHyperLiquidComposer_FeeToken instead.`) + console.warn(`FeeToken composer provides automatic user activation using the token itself.\n`) + + const { continueAnyway } = await inquirer.prompt([ + { + type: 'confirm', + name: 'continueAnyway', + message: 'Do you want to continue with recoverable composer anyway?', + default: false, + }, + ]) + + if (!continueAnyway) { + console.log('Deployment cancelled.') + process.exit(0) + } + } + const { address: address_oft } = await hre.deployments.get(contractName_oft).catch(async () => { console.log(`Deployment file for ${contractName_oft}.json in deployments/${networkName} not found`) const { proceedWithOFTAddress } = await inquirer.prompt([ diff --git a/examples/oft-hyperliquid/deploy/MyHyperliquidComposer.ts b/examples/oft-hyperliquid/deploy/MyHyperliquidComposer.ts index 03a6372792..e0b3ccd843 100644 --- a/examples/oft-hyperliquid/deploy/MyHyperliquidComposer.ts +++ b/examples/oft-hyperliquid/deploy/MyHyperliquidComposer.ts @@ -7,6 +7,7 @@ import { CHAIN_IDS, EthersSigner, getCoreSpotDeployment, + isQuoteAsset, useBigBlock, useSmallBlock, } from '@layerzerolabs/hyperliquid-composer' @@ -52,6 +53,30 @@ const deploy: DeployFunction = async (hre) => { // Validates and returns the native spot const hip1Token = getCoreSpotDeployment(coreSpotIndex, isTestnet) + // Check if the token is a quote asset - warn if it is + console.log(`\nChecking if token ${coreSpotIndex} is a quote asset...`) + const { isQuoteAsset: isQuote, tokenName } = await isQuoteAsset(isTestnet, parseInt(coreSpotIndex), loglevel) + + if (isQuote) { + console.warn(`\n[WARNING] Token ${coreSpotIndex}${tokenName ? ` (${tokenName})` : ''} IS a quote asset!`) + console.warn(`For quote assets, you should use MyHyperLiquidComposer_FeeToken instead.`) + console.warn(`FeeToken composer provides automatic user activation using the token itself.\n`) + + const { continueAnyway } = await inquirer.prompt([ + { + type: 'confirm', + name: 'continueAnyway', + message: 'Do you want to continue with regular composer anyway?', + default: false, + }, + ]) + + if (!continueAnyway) { + console.log('Deployment cancelled.') + process.exit(0) + } + } + const { address: address_oft } = await hre.deployments.get(contractName_oft).catch(async () => { console.log(`Deployment file for ${contractName_oft}.json in deployments/${networkName} not found`) const { proceedWithOFTAddress } = await inquirer.prompt([ diff --git a/packages/hyperliquid-composer/HYPERLIQUID.README.md b/packages/hyperliquid-composer/HYPERLIQUID.README.md index ecdc94d517..72512dceae 100644 --- a/packages/hyperliquid-composer/HYPERLIQUID.README.md +++ b/packages/hyperliquid-composer/HYPERLIQUID.README.md @@ -159,6 +159,65 @@ This creates the asset bridge precompile `0x2000...abcd` (where `abcd` is the `c > > ⚠️ **Setup Guidance**: CoreDecimals - EVMDecimals must be within [-2,18] is a requirement for the hyperliquid protocol +## Quote Assets (Fee Tokens) + +A **quote asset** (or fee token) is a token that can be used as the quote currency in trading pairs on HyperCore. When a token becomes a quote asset, Hyperliquid automatically creates a `HYPE/QUOTE_ASSET` spot market. Thus every quote asset for a `HYPE` spot market is a quote asset. + +It is permissionless to deploy spot markets for OTHER tokens as well. + +### Requirements Overview + +**Mainnet:** +- Follow the complete requirements outlined in [Hyperliquid's Permissionless Spot Quote Assets documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) +- Requires specific trading fee share configuration +- Additional technical and liquidity requirements apply +- Contact the Hyperliquid team for the most up-to-date requirements + +**Testnet (lighter requirements):** +The requirements are more relaxed to facilitate testing: +1. **Stake 50 HYPE tokens** (refer to [Hyperliquid's Permissionless Spot Quote Assets documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) for more details) +2. **Create active limit orders** on both sides of your token's order book: + - Place at least one BUY limit order + - Place at least one SELL limit order + - Orders must be active before executing the `enable-quote-token` command + - You can place these orders via the [Hyperliquid Explorer](https://app.hyperliquid.xyz/) + + Example order book requirement: + ``` + Price Size Total + 1.0015 1,000.00 1,000.00 <- Active SELL order + -------- Spread: 0.0025 (0.250%) -------- + 0.9990 1,000.00 1,000.00 <- Active BUY order + ``` + +3. After executing `enable-quote-token`: + - A `HYPE/YOUR_ASSET` trading pair is automatically created + - You must then maintain order book requirements for the `HYPE/YOUR_ASSET` pair (not required for testnet) + - Follow Hyperliquid's documentation for maintaining the `HYPE/ASSET` order book + +### Checking Quote Asset Status + +To verify if a token is a quote asset: + +```bash +# Check specific token +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --filter-token-index \ + --network {testnet | mainnet} + +# List all quote assets +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --network {testnet | mainnet} +``` + +### Composer Requirements for Quote Assets + +If you're deploying a quote asset token (or plan to make it one): +- **Use `MyHyperLiquidComposer_FeeToken`** - This composer variant provides automatic user activation using the token itself as the fee token +- Regular composers (`MyHyperliquidComposer`, `FeeAbstraction`, `Recoverable`) will work but require users to have HYPE for gas fees + +The deployment scripts automatically check if your token is a quote asset and guide you to use the appropriate composer type. + ## The Asset Bridge Transactions can be sent to the asset bridge address `0x2000...abcd` (where `abcd` is the `coreIndexId` of the HIP-1 in hex) to send tokens between HyperEVM and HyperCore. This bridge is formed after the token linking step, until then the bridge does not exist. @@ -260,18 +319,90 @@ contract HyperLiquidComposer is IHyperLiquidComposer { } ``` -### There are 2 extensions for Hyperliquid Composers +### There are 3 extensions for Hyperliquid Composers #### Recovery Extension -This gives you the ability to pull tokens our of the composer on hypercore and into the composer's address on hyperevm. -The priviledged address can also send those tokens to itself on HyperEVM, giving you the ability to recover locked tokens. +This gives you the ability to pull tokens out of the composer on HyperCore and into the composer's address on HyperEVM. +The privileged address can also send those tokens to itself on HyperEVM, giving you the ability to recover locked tokens. + +**Use Case:** Useful for any token deployment where you want the ability to recover tokens that may become stuck in the composer contract. + +**Constructor Arguments:** +- `oft`: OFT address +- `coreIndex`: Core spot index +- `weiDiff`: Decimal difference between EVM and Core +- `recoveryAddress`: Address with recovery privileges #### FeeToken Extension -This extension is for tokens that are a `FeeToken` - can be used to activate users on hypercore. Should a composer deployed with this extension notice that a user's address has not been activated then it would send across bridge across the whole amount of tokens to HyperCore and then send across `amt - activationFee` to the user. This consumes `activationFee` from the composer's address. +This extension is for tokens that are a **quote asset** (fee token) - tokens that can be used to activate users on HyperCore. + +**How it Works:** +When the composer detects that a user's address has not been activated on HyperCore, it: +1. Sends the full amount of tokens across the bridge to HyperCore +2. Transfers `amt - activationFee` to the user +The transfer automatically consumes `activationFee` from the composer's address to activate the user + +**Example:** User sends `1.5 USDT0` to a new address. The composer sends over `1.5 USDT0` to itself on HyperCore, then makes a core transfer of `0.5 USDT0` to the user. The `1.0 USDT0` activation fee is automatically consumed. + +**Requirements:** +- Token **must be a quote asset** (see [Quote Assets section](#quote-assets-fee-tokens)) +- Deployment scripts automatically verify this requirement +- If not a quote asset, deployment will fail with guidance to use alternative composers + +**Constructor Arguments:** +- `oft`: OFT address +- `coreIndex`: Core spot index +- `weiDiff`: Decimal difference between EVM and Core + +On-chain deployments: +USDT0 : [0x80123Ab57c9bc0C452d6c18F92A653a4ee2e7585](https://hyperevmscan.io/address/0x80123Ab57c9bc0C452d6c18F92A653a4ee2e7585) -Ex: User sends `1.5 USDT0` to an new address. The composer sends over `1.5 USDT0` to itself and then makes a core transfer of `0.5 USDT0`. The `1 USDT0` is consumed as Fee. +#### FeeAbstraction Extension + +This extension provides automatic user activation using a **different token** for fees, combined with price oracle integration for dynamic fee calculation. + +**How it Works:** +1. Checks if a user's address is activated on HyperCore +2. Uses the hyperliquid's spot pair oracle to convert between your token and the fee token value +3. Can charge an overhead fee (set on deployment) in addition to the base activation cost +4. If there is insufficient quote asset balance, the composer will revert the transaction and user gets tokens on HyperEVM + +**Key Features:** +- **Price Oracle Integration**: Queries real-time prices via `spotId` (e.g., 107 for HYPE/USDC) +- **Overhead Fee**: Configurable additional fee in cents (e.g., 100 = $1.00 overhead on top of $1.00 base activation) +- **Recovery Capability**: Includes recovery address functionality for fee management +- **Flexible Fee Token**: Can work with any token, not limited to quote assets + +**Example Configuration:** +- SpotId: `107` (HYPE/USDC pair for price queries) +- Activation Overhead Fee: `100` cents (adds $1.00 overhead) +- Total User Fee: $2.00 (Base $1.00 + Overhead $1.00) + +**Use Case:** Ideal for non-quote-asset tokens where you want to provide seamless user activation without requiring users to hold quote assets. + +**Constructor Arguments:** +- `oft`: OFT address +- `coreIndex`: Core spot index +- `weiDiff`: Decimal difference between EVM and Core +- `spotId`: Spot pair ID for price queries (e.g., 107 for HYPE/USDC) +- `activationOverheadFee`: Overhead fee in cents +- `recoveryAddress`: Address with recovery privileges for fee management + +On-chain deployments: +ENA : [0x5879d9821909A41cd3A382A990A4A5A6Ca77F2f0](https://hyperevmscan.io/address/0x5879d9821909A41cd3A382A990A4A5A6Ca77F2f0) + +### Choosing the Right Composer + +| Composer Type | Best For | Key Feature | +|--------------|----------|-------------| +| **Regular** | Standard tokens | Basic functionality, no extensions | +| **Recoverable** | Any token | Token recovery capability | +| **FeeToken** | **Quote assets only** | Automatic activation using your token | +| **FeeAbstraction** | Non-quote assets | Automatic activation using oracle-priced fees | + +> ⚠️ **Important**: The deployment scripts automatically check if your token is a quote asset and guide you to use the appropriate composer type. See [Quote Assets (Fee Tokens)](#quote-assets-fee-tokens) for more details. ## LayerZero Transaction on HyperEVM @@ -430,6 +561,7 @@ npx @layerzerolabs/hyperliquid-composer request-evm-contract \ ### 2. Finalize EVM Contract Link +#### Hypercore action method ```bash npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ --token-index \ @@ -438,6 +570,20 @@ npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ [--log-level {info | verbose}] ``` +#### CoreWriter Method + +Alternative method using direct CoreWriter interaction. This is useful if you prefer to use Foundry's `cast` command. + +```bash +npx @layerzerolabs/hyperliquid-composer finalize-evm-contract-corewriter \ + --token-index \ + --nonce \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] +``` + +The command will output the calldata and a ready-to-use `cast send` command for finalizing the EVM contract link via the CoreWriter precompile at `0x3333333333333333333333333333333333333333`. + ## Post-Launch Management ### Freeze/Unfreeze Users @@ -533,6 +679,25 @@ npx @layerzerolabs/hyperliquid-composer spot-auction-status \ [--log-level {info | verbose}] ``` +### Check if Token is Quote Asset + +Check if a specific token is a quote asset, or list all quote assets when no token index is provided. Quote assets are automatically paired with HYPE when promoted by the Hyperliquid protocol. + +```bash +# List all quote assets +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] + +# Check if specific token is a quote asset +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --filter-token-index \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] +``` + +The command returns `yes` or `no` when checking a specific token, or lists all quote assets when no token index is provided. + ## Utilities ### Convert Token Index to Bridge Address @@ -805,10 +970,10 @@ npx @layerzerolabs/hyperliquid-composer trading-fee \ This step enables the token to be used as a quote asset for trading pairs. This allows other tokens to form trading pairs against your token (e.g., TOKEN/YOUR_TOKEN instead of only YOUR_TOKEN/USDC). -> ⚠️ **Requirements**: -> - Requires specific trading fee share value (see Step 6/7 above) -> - Review all requirements at: [Permissionless Spot Quote Assets](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) -> - Contact the Hyperliquid team for the most up-to-date information +> ⚠️ **Important**: Review the complete [Quote Assets (Fee Tokens)](#quote-assets-fee-tokens) section above for detailed requirements, including: +> - Mainnet: Complete technical and liquidity requirements +> - Testnet: Simplified requirements (50 HYPE stake + active order book) +> - Order book maintenance for `HYPE/YOUR_ASSET` pair after enablement > > 📝 **Note**: This can be executed after the trading fee share is set and even after deployment and linking are complete. @@ -820,6 +985,17 @@ npx @layerzerolabs/hyperliquid-composer enable-quote-token \ [--log-level {info | verbose}] ``` +**Prerequisites:** +- Trading fee share must be set (see Step 6/7 above) +- **Testnet**: 50 HYPE staked + active BUY and SELL limit orders on your token's order book +- **Mainnet**: All requirements per [Hyperliquid's documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) + +**After Execution:** +- A `HYPE/YOUR_ASSET` trading pair is automatically created +- You must maintain order book requirements for the new `HYPE/ASSET` pair +- Verify quote asset status with the `list-quote-asset` command + + ### Step 6.7/7 `enableAlignedQuoteToken` (Optional) This step enables the token to be used as an aligned quote asset for trading pairs. Aligned quote tokens have special properties and requirements different from regular quote tokens. @@ -872,6 +1048,19 @@ npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ --private-key $PRIVATE_KEY_HYPERLIQUID ``` +**Alternative: Using CoreWriter directly with Foundry** + +If you prefer to use Foundry's `cast` command, you can generate the calldata and send the transaction directly: + +```bash +npx @layerzerolabs/hyperliquid-composer finalize-evm-contract-corewriter \ + --token-index \ + --nonce \ + --network {testnet | mainnet} +``` + +This will output the calldata and a ready-to-use `cast send` command that you can execute directly. + ## Deploy the Composer While the composer could have been deployed at any point in time due to its statelessness, it is technically the final step of the deployment process. The following script automatically handles the block switching for you. diff --git a/packages/hyperliquid-composer/README.md b/packages/hyperliquid-composer/README.md index cdd0f0135c..ad93e19334 100644 --- a/packages/hyperliquid-composer/README.md +++ b/packages/hyperliquid-composer/README.md @@ -131,7 +131,15 @@ npx @layerzerolabs/hyperliquid-composer trading-fee \ ### 7. Enable Quote Token Capability (Optional) -Enables your token to be used as a quote asset for trading pairs. **Dependency:** Requires specific trading fee share value (see Step 6 above). See: [Permissionless Spot Quote Assets](https://hyperliquid.gitbook.io/hyperliquid-docs/hypercore/permissionless-spot-quote-assets) +Enables your token to be used as a quote asset for trading pairs. + +> ⚠️ **Important**: Review the complete [Quote Assets (Fee Tokens)](./HYPERLIQUID.README.md#quote-assets-fee-tokens) section for: +> - Mainnet requirements (technical and liquidity) +> - Testnet requirements (50 HYPE stake + active order book) +> - Order book maintenance for `HYPE/YOUR_ASSET` pair +> - Composer selection guidance (use `FeeToken` variant for quote assets) + +**Dependency:** Requires trading fee share configuration (see Step 6 above). ```bash npx @layerzerolabs/hyperliquid-composer enable-quote-token \ @@ -179,6 +187,17 @@ npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \ [--log-level {info | verbose}] ``` +**Alternative: Using CoreWriter directly with Foundry** + +If you prefer to use Foundry's `cast` command, you can generate the calldata and send the transaction directly: + +```bash +npx @layerzerolabs/hyperliquid-composer finalize-evm-contract-corewriter \ + --token-index \ + --nonce \ + --network {testnet | mainnet} +``` + ## Post-Launch Management ### Freeze/Unfreeze Users @@ -274,6 +293,25 @@ npx @layerzerolabs/hyperliquid-composer spot-auction-status \ [--log-level {info | verbose}] ``` +### Check if Token is Quote Asset + +Check if a specific token is a quote asset, or list all quote assets when no token index is provided. + +```bash +# List all quote assets +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] + +# Check if specific token is a quote asset +npx @layerzerolabs/hyperliquid-composer list-quote-asset \ + --filter-token-index \ + --network {testnet | mainnet} \ + [--log-level {info | verbose}] +``` + +The command returns `yes` or `no` when checking a specific token, or lists all quote assets when no token index is provided. + ## Utilities ### Convert Token Index to Bridge Address diff --git a/packages/hyperliquid-composer/package.json b/packages/hyperliquid-composer/package.json index d350b37ad5..89690bd7b4 100644 --- a/packages/hyperliquid-composer/package.json +++ b/packages/hyperliquid-composer/package.json @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "git+https://github.com/LayerZero-Labs/devtools.git", - "directory": "packages/oft-hyperliquid-evm" + "directory": "packages/hyperliquid-composer" }, "license": "MIT", "exports": { @@ -33,7 +33,7 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "bin": { - "oft-hyperliquid-evm": "./cli.js" + "hyperliquid-composer": "./cli.js" }, "files": [ "artifacts/HyperLiquid*.sol", diff --git a/packages/hyperliquid-composer/src/cli.ts b/packages/hyperliquid-composer/src/cli.ts index a64b923300..3397b28e54 100644 --- a/packages/hyperliquid-composer/src/cli.ts +++ b/packages/hyperliquid-composer/src/cli.ts @@ -20,6 +20,7 @@ import { // EVM-HyperCore Linking requestEvmContract, finalizeEvmContract, + finalizeEvmContractCorewriter, // Post-Launch Management freezeTokenUser, @@ -32,6 +33,7 @@ import { getCoreBalances, listSpotPairs, spotAuctionStatus, + listQuoteAsset, // Utilities intoAssetBridgeAddress, @@ -223,6 +225,19 @@ optionGroups ) .action(withNormalizedNetwork(finalizeEvmContract)) +optionGroups + .base( + program + .command(CLI_COMMANDS.FINALIZE_EVM_CONTRACT_COREWRITER) + .description( + 'Linking 2a. Generate CoreWriter calldata for finalizing EVM contract link (for Foundry usage)' + ) + .requiredOption(...commonOptions.tokenIndex()) + .requiredOption('-n, --nonce ', 'EVM contract deployment nonce') + .option('--only-calldata', 'Only output calldata without usage instructions', false) + ) + .action(withNormalizedNetwork(finalizeEvmContractCorewriter)) + // === Post-Launch Management === optionGroups .deployment( @@ -299,6 +314,15 @@ optionGroups ) .action(withNormalizedNetwork(listSpotPairs)) +optionGroups + .base( + program + .command(CLI_COMMANDS.LIST_QUOTE_ASSET) + .description('List all quote assets (lists all if no token-index provided)') + .option('-idx, --filter-token-index ', 'Filter on token index') + ) + .action(withNormalizedNetwork(listQuoteAsset)) + optionGroups .base( program diff --git a/packages/hyperliquid-composer/src/commands/core-spot-deployment.ts b/packages/hyperliquid-composer/src/commands/core-spot-deployment.ts index b9c905a4d9..ece02c21d2 100644 --- a/packages/hyperliquid-composer/src/commands/core-spot-deployment.ts +++ b/packages/hyperliquid-composer/src/commands/core-spot-deployment.ts @@ -19,6 +19,38 @@ import { RPC_URLS } from '@/types' import { LOGGER_MODULES } from '@/types/cli-constants' import inquirer from 'inquirer' +import type { Logger } from '@layerzerolabs/io-devtools' + +/** + * Helper function to fetch spot metadata and token info with error handling + * @param tokenIndex - The token index to fetch information for + * @param isTestnet - Whether to use testnet or mainnet + * @param logLevel - The log level to use + * @param logger - Logger instance for error reporting + * @returns Object containing coreSpot and coreSpotInfo, or exits on error + */ +async function fetchTokenMetadata( + tokenIndex: string, + isTestnet: boolean, + logLevel: string, + logger: Logger +): Promise<{ coreSpot: CoreSpotMetaData; coreSpotInfo: SpotInfo }> { + let coreSpot: CoreSpotMetaData + let coreSpotInfo: SpotInfo + + try { + coreSpot = await getSpotMeta(null, isTestnet, logLevel, tokenIndex) + coreSpotInfo = await getHipTokenInfo(null, isTestnet, logLevel, coreSpot.tokenId) + } catch (error) { + logger.error( + `Failed to fetch token information for token ${tokenIndex}. The token's deployment may not have started.` + ) + logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`) + process.exit(1) + } + + return { coreSpot, coreSpotInfo } +} export async function coreSpotDeployment(args: CoreSpotDeploymentArgs): Promise { setDefaultLogLevel(args.logLevel) @@ -148,10 +180,9 @@ export async function hipTokenInfo(args: TokenIndexArgs): Promise { const tokenIndex = args.tokenIndex const network = args.network - const isTestnet = network === 'testnet' - const coreSpot: CoreSpotMetaData = await getSpotMeta(null, isTestnet, args.logLevel, tokenIndex) - const coreSpotInfo: SpotInfo = await getHipTokenInfo(null, isTestnet, args.logLevel, coreSpot.tokenId) + + const { coreSpotInfo } = await fetchTokenMetadata(tokenIndex, isTestnet, args.logLevel, logger) logger.info(JSON.stringify(coreSpotInfo, null, 2)) } @@ -162,21 +193,30 @@ export async function spotDeployState(args: SpotDeployStateArgs): Promise const tokenIndex = args.tokenIndex const network = args.network - const isTestnet = network === 'testnet' + let deployerAddress: string if (args.deployerAddress) { deployerAddress = args.deployerAddress } else { - const coreSpot: CoreSpotMetaData = await getSpotMeta(null, isTestnet, args.logLevel, tokenIndex) - const coreSpotInfo: SpotInfo = await getHipTokenInfo(null, isTestnet, args.logLevel, coreSpot.tokenId) + const { coreSpotInfo } = await fetchTokenMetadata(tokenIndex, isTestnet, args.logLevel, logger) deployerAddress = coreSpotInfo.deployer logger.info( `Using deployer address: ${deployerAddress} for token ${coreSpotInfo.name} with index ${tokenIndex}` ) } - const deployState = (await getSpotDeployState(deployerAddress, isTestnet, args.logLevel)) as SpotDeployStates + let deployState: SpotDeployStates + try { + deployState = await getSpotDeployState(deployerAddress, isTestnet, args.logLevel) + } catch (error) { + logger.error( + `Failed to fetch deployment state for token ${tokenIndex}. The token's deployment hasn't started yet.` + ) + logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`) + process.exit(1) + } + logger.verbose(`All deployment states for ${deployerAddress}: ${JSON.stringify(deployState, null, 2)}`) // iterate through deployState and print out the one with the same "token" as tokenIndex diff --git a/packages/hyperliquid-composer/src/commands/index.ts b/packages/hyperliquid-composer/src/commands/index.ts index 7e8e386a6f..8f8da09e1b 100644 --- a/packages/hyperliquid-composer/src/commands/index.ts +++ b/packages/hyperliquid-composer/src/commands/index.ts @@ -9,12 +9,13 @@ export * from './core-spot-deployment' export * from './spot-deploy' // === EVM-HyperCore Linking === -export * from './register-token' +export * from './link-evm-core' // === Info & Queries === export * from './account-state' export * from './list-spot-pairs' export * from './spot-auction-status' +export * from './list-quote-asset' // === Utilities === export * from './type-conversion' diff --git a/packages/hyperliquid-composer/src/commands/register-token.ts b/packages/hyperliquid-composer/src/commands/link-evm-core.ts similarity index 71% rename from packages/hyperliquid-composer/src/commands/register-token.ts rename to packages/hyperliquid-composer/src/commands/link-evm-core.ts index 3b8c472d0e..99800498c6 100644 --- a/packages/hyperliquid-composer/src/commands/register-token.ts +++ b/packages/hyperliquid-composer/src/commands/link-evm-core.ts @@ -5,9 +5,14 @@ import { getCoreSpotDeployment, writeUpdatedCoreSpotDeployment } from '@/io/pars import { getHyperliquidSigner } from '@/signer' import { setRequestEvmContract, setFinalizeEvmContract } from '@/operations' import { LOGGER_MODULES } from '@/types/cli-constants' -import { RequestEvmContractArgs, FinalizeEvmContractArgs } from '@/types' +import { RequestEvmContractArgs, FinalizeEvmContractArgs, FinalizeEvmContractCorewriterArgs } from '@/types' +import { RPC_URLS } from '@/types/constants' import { ethers } from 'ethers' +const COREWRITER_ADDRESS = '0x3333333333333333333333333333333333333333' +const FINALIZE_EVM_CONTRACT_ACTION_TYPE_PREFIX = '0x01000008' +const ACTION_TYPE_FINALIZE = 1 + export async function requestEvmContract(args: RequestEvmContractArgs): Promise { setDefaultLogLevel(args.logLevel) const logger = createModuleLogger(LOGGER_MODULES.REGISTER_TOKEN, args.logLevel) @@ -145,3 +150,46 @@ export async function finalizeEvmContract(args: FinalizeEvmContractArgs): Promis process.exit(1) } } + +/** + * Generates the calldata for finalizing an EVM contract link using the CoreWriter precompile + * This allows users to send the transaction using Foundry's cast command + */ +export async function finalizeEvmContractCorewriter(args: FinalizeEvmContractCorewriterArgs): Promise { + setDefaultLogLevel(args.logLevel) + const logger = createModuleLogger(LOGGER_MODULES.FINALIZE_EVM_CONTRACT_COREWRITER, args.logLevel) + logger.verbose(JSON.stringify(args, null, 2)) + + const tokenIndex = parseInt(args.tokenIndex) + const nonce = parseInt(args.nonce) + const network = args.network + const isTestnet = network === 'testnet' + + // Step 1: Encode the action parameters (tokenIndex, actionType, nonce) + const encodedParams = ethers.utils.defaultAbiCoder.encode( + ['uint64', 'uint8', 'uint64'], + [tokenIndex, ACTION_TYPE_FINALIZE, nonce] + ) + + // Step 2: Pack the action type prefix with the encoded parameters + const packedData = ethers.utils.solidityPack( + ['bytes', 'bytes'], + [FINALIZE_EVM_CONTRACT_ACTION_TYPE_PREFIX, encodedParams] + ) + + // Step 3: Create the calldata for sendRawAction(bytes) + const iface = new ethers.utils.Interface(['function sendRawAction(bytes)']) + const calldata = iface.encodeFunctionData('sendRawAction', [packedData]) + + const rpcUrl = isTestnet ? RPC_URLS.TESTNET : RPC_URLS.MAINNET + + // Print full usage instructions + logger.info(`\n=== Finalize EVM Contract CoreWriter Calldata ===\n`) + logger.info(`Token Index: ${tokenIndex}`) + logger.info(`Nonce: ${nonce}`) + logger.info(`Calldata:\n${calldata}\n`) + logger.info(`Usage:\n`) + logger.info( + `cast send ${COREWRITER_ADDRESS} \\\n ${calldata} \\\n --rpc-url ${rpcUrl} \\\n --private-key $EVM_TOKEN_DEPLOYER\n` + ) +} diff --git a/packages/hyperliquid-composer/src/commands/list-quote-asset.ts b/packages/hyperliquid-composer/src/commands/list-quote-asset.ts new file mode 100644 index 0000000000..c2149d5a44 --- /dev/null +++ b/packages/hyperliquid-composer/src/commands/list-quote-asset.ts @@ -0,0 +1,42 @@ +import { createModuleLogger, setDefaultLogLevel } from '@layerzerolabs/io-devtools' + +import { isQuoteAsset as isQuoteAssetOperation } from '@/operations' +import { LOGGER_MODULES } from '@/types/cli-constants' +import { ListQuoteAssetArgs } from '@/types' + +export async function listQuoteAsset(args: ListQuoteAssetArgs): Promise { + setDefaultLogLevel(args.logLevel) + const logger = createModuleLogger(LOGGER_MODULES.LIST_QUOTE_ASSET, args.logLevel) + + const filterTokenIndex = args.filterTokenIndex ? parseInt(args.filterTokenIndex) : null + const isTestnet = args.network === 'testnet' + + try { + const result = await isQuoteAssetOperation(isTestnet, filterTokenIndex, args.logLevel) + + if (filterTokenIndex === null) { + // Print all quote assets + logger.info(`\nAll Quote Assets on ${args.network}:\n`) + if (result.allQuoteAssets && result.allQuoteAssets.length > 0) { + result.allQuoteAssets.forEach((asset) => { + logger.info(` ${asset.name} (Index: ${asset.index})`) + }) + logger.info(`\nTotal quote assets: ${result.allQuoteAssets.length}`) + } else { + logger.info('No quote assets found.') + } + } else { + // Check specific token + if (result.isQuoteAsset) { + logger.verbose(`Token ${filterTokenIndex} (${result.tokenName}) is a quote asset`) + logger.info('true\n') + } else { + logger.verbose(`Token ${filterTokenIndex} is not a quote asset`) + logger.info('false\n') + } + } + } catch (error) { + logger.error(`Failed to check quote asset status: ${error}`) + process.exit(1) + } +} diff --git a/packages/hyperliquid-composer/src/io/parser.ts b/packages/hyperliquid-composer/src/io/parser.ts index cbdc2787f2..b012138a25 100644 --- a/packages/hyperliquid-composer/src/io/parser.ts +++ b/packages/hyperliquid-composer/src/io/parser.ts @@ -14,12 +14,39 @@ function getFullPath(index: string, isTestnet: boolean): string { export function getCoreSpotDeployment(index: string | number, isTestnet: boolean, logger?: Logger): CoreSpotDeployment { const fullPath = getFullPath(index.toString(), isTestnet) - const nativeSpot = fs.readFileSync(fullPath, 'utf8') - if (!nativeSpot) { - const errMsg = `Native spot ${index} not found - make sure the native spot for the token ${index} is found at ${fullPath}` + + // Check if file exists, if not provide helpful error message + if (!fs.existsSync(fullPath)) { + const deploymentDir = path.join(process.cwd(), 'deployments', `hypercore-${isTestnet ? 'testnet' : 'mainnet'}`) + + // List available deployment files + let availableTokens: string[] = [] + try { + if (fs.existsSync(deploymentDir)) { + const files = fs.readdirSync(deploymentDir) + availableTokens = files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', '')) + } + } catch (e) { + // Ignore error when listing files + } + + const network = isTestnet ? 'testnet' : 'mainnet' + let errMsg = `Core spot deployment file not found for token index ${index} on ${network}.\n` + errMsg += `Expected location: ${fullPath}\n` + + if (availableTokens.length > 0) { + errMsg += `\nAvailable token indices on ${network}:\n` + errMsg += availableTokens.map((t) => ` - ${t}`).join('\n') + } else { + errMsg += `\nNo deployment files found in ${deploymentDir}` + errMsg += `\nYou need to run the core-spot deployment commands first.` + } + logger?.error(errMsg) throw new Error(errMsg) } + + const nativeSpot = fs.readFileSync(fullPath, 'utf8') return JSON.parse(nativeSpot) as CoreSpotDeployment } diff --git a/packages/hyperliquid-composer/src/operations/index.ts b/packages/hyperliquid-composer/src/operations/index.ts index 64d436e3ca..cb5d14ccb2 100644 --- a/packages/hyperliquid-composer/src/operations/index.ts +++ b/packages/hyperliquid-composer/src/operations/index.ts @@ -8,7 +8,7 @@ export * from './spotMeta' export * from './spotDeploy' // === EVM-HyperCore Linking === -export * from './registerEvmContract' +export * from './linkEvmCore' // === Info & Queries === export * from './infoMisc' diff --git a/packages/hyperliquid-composer/src/operations/registerEvmContract.ts b/packages/hyperliquid-composer/src/operations/linkEvmCore.ts similarity index 100% rename from packages/hyperliquid-composer/src/operations/registerEvmContract.ts rename to packages/hyperliquid-composer/src/operations/linkEvmCore.ts diff --git a/packages/hyperliquid-composer/src/operations/spotDeploy.ts b/packages/hyperliquid-composer/src/operations/spotDeploy.ts index b65b20bfca..7f18ba162e 100644 --- a/packages/hyperliquid-composer/src/operations/spotDeploy.ts +++ b/packages/hyperliquid-composer/src/operations/spotDeploy.ts @@ -9,7 +9,7 @@ import { } from '../io' import { HyperliquidClient, IHyperliquidSigner } from '../signer' import { MAX_HYPERCORE_SUPPLY, QUOTE_TOKENS } from '../types' -import { getSpotDeployState, getExistingQuoteTokens, getSpotPairDeployAuctionStatus } from './spotMeta' +import { getSpotDeployState, getExistingQuoteTokens, getSpotPairDeployAuctionStatus, isQuoteAsset } from './spotMeta' import type { SpotDeployAction, SpotDeployStates } from '../types' import { RegisterHyperliquidity } from '@/types/spotDeploy' import { LOGGER_MODULES } from '@/types/cli-constants' @@ -300,7 +300,7 @@ export async function registerSpot( const choices: Array<{ name: string; value: number }> = [] // Add all network quote tokens that haven't been deployed yet - networkQuoteTokens.forEach((quoteToken) => { + networkQuoteTokens.forEach((quoteToken: { tokenId: number; name: string }) => { if (!existingQuoteTokens.includes(quoteToken.tokenId)) { choices.push({ name: `${quoteToken.name} (Token ${quoteToken.tokenId})`, @@ -355,15 +355,32 @@ export async function registerSpot( type: 'input', name: 'customTokenId', message: 'Enter the core spot token ID to use as quote token:', - validate: (input: string) => { + validate: async (input: string) => { + // Allow user to quit + if (input.toLowerCase() === 'q') { + console.log('\nOperation cancelled by user.\n') + process.exit(0) + } + const num = parseInt(input) if (isNaN(num) || num < 0) { - return 'Please enter a valid positive number' + return 'Please enter a valid positive number (or "q" as the token ID to quit)' } if (existingQuoteTokens.includes(num)) { return `Token ${num} is already deployed as a quote token for this asset` } - return true + + // Check if the token is a quote asset + try { + const { isQuoteAsset: isQuote } = await isQuoteAsset(isTestnet, num, logLevel) + if (!isQuote) { + return `Token ${num} is not a recognized quote asset on the Hyperliquid protocol. Only quote assets (tokens paired with HYPE) can be used. Enter "q" as the token ID to quit.` + } + return true + } catch (error) { + // If check fails, don't allow + return `Unable to verify if token ${num} is a quote asset. Enter "q" as the token ID to quit.` + } }, }, ]) diff --git a/packages/hyperliquid-composer/src/operations/spotMeta.ts b/packages/hyperliquid-composer/src/operations/spotMeta.ts index 13a95f6acb..158f815e1a 100644 --- a/packages/hyperliquid-composer/src/operations/spotMeta.ts +++ b/packages/hyperliquid-composer/src/operations/spotMeta.ts @@ -9,6 +9,7 @@ import { SpotPairsWithMetadata, SpotPairDeployAuctionStatus, } from '../types' +import { HYPE_INDEX } from '../types/constants' export async function getSpotMeta( signer: IHyperliquidSigner | null, @@ -163,3 +164,60 @@ export async function getExistingQuoteTokens( return [...new Set(quoteTokens)] // Remove duplicates } + +/** + * Checks if a token is a quote asset by looking at HYPE trading pairs. + * When any core spot is promoted to a quote asset (fee token), the Hyperliquid protocol + * automatically deploys a new spot market for HYPE/QUOTE_ASSET. + * + * @param isTestnet Whether to query testnet or mainnet + * @param tokenIndex The token index to check (optional - if not provided, returns all quote assets) + * @param logLevel Logging level for the client + * @returns Object containing isQuoteAsset boolean and tokenName string + */ +export async function isQuoteAsset( + isTestnet: boolean, + tokenIndex: number | null, + logLevel: string +): Promise<{ isQuoteAsset: boolean; tokenName: string; allQuoteAssets?: Array<{ index: number; name: string }> }> { + // Get HYPE token index based on network + const hypeTokenIndex = isTestnet ? HYPE_INDEX.TESTNET : HYPE_INDEX.MAINNET + + // Get all HYPE trading pairs with metadata + const { pairs, tokens } = await getSpotPairsWithMetadata(isTestnet, hypeTokenIndex, logLevel) + + // Extract all quote assets paired with HYPE + const quoteAssets = pairs + .map((pair) => { + // Find the token that's NOT HYPE + const quoteTokenIndex = pair.tokens.find((token) => token !== hypeTokenIndex) + if (quoteTokenIndex === undefined) { + return null + } + + // Find the token metadata + const tokenMetadata = tokens.find((token) => token.index === quoteTokenIndex) + return { + index: quoteTokenIndex, + name: tokenMetadata?.name || `Token-${quoteTokenIndex}`, + } + }) + .filter((asset): asset is { index: number; name: string } => asset !== null) + + // If no tokenIndex provided, return all quote assets + if (tokenIndex === null) { + return { + isQuoteAsset: false, + tokenName: '', + allQuoteAssets: quoteAssets, + } + } + + // Check if the provided tokenIndex is in the list of quote assets + const matchedAsset = quoteAssets.find((asset) => asset.index === tokenIndex) + + return { + isQuoteAsset: matchedAsset !== undefined, + tokenName: matchedAsset?.name || '', + } +} diff --git a/packages/hyperliquid-composer/src/types/cli-args.ts b/packages/hyperliquid-composer/src/types/cli-args.ts index bfa231b821..c39465aa82 100644 --- a/packages/hyperliquid-composer/src/types/cli-args.ts +++ b/packages/hyperliquid-composer/src/types/cli-args.ts @@ -61,6 +61,10 @@ export interface GetCoreBalancesArgs extends UserArgs { showZero: boolean } +export interface ListQuoteAssetArgs extends BaseArgs { + filterTokenIndex?: string +} + // Simple command args - using concrete interfaces instead of empty extends export interface GenesisArgs extends TokenIndexArgs, PrivateKeyArgs {} export interface CreateSpotDeploymentArgs extends TokenIndexArgs, PrivateKeyArgs {} @@ -71,3 +75,6 @@ export interface EnableTokenQuoteAssetArgs extends TokenIndexArgs, PrivateKeyArg export interface EnableTokenAlignedQuoteAssetArgs extends TokenIndexArgs, PrivateKeyArgs {} export interface RequestEvmContractArgs extends TokenIndexArgs, PrivateKeyArgs {} export interface FinalizeEvmContractArgs extends TokenIndexArgs, PrivateKeyArgs {} +export interface FinalizeEvmContractCorewriterArgs extends TokenIndexArgs { + nonce: string +} diff --git a/packages/hyperliquid-composer/src/types/cli-constants.ts b/packages/hyperliquid-composer/src/types/cli-constants.ts index 8d2f93c1c3..0f277c226b 100644 --- a/packages/hyperliquid-composer/src/types/cli-constants.ts +++ b/packages/hyperliquid-composer/src/types/cli-constants.ts @@ -26,6 +26,7 @@ const STRINGS = { // EVM-HyperCore Linking REQUEST_EVM_CONTRACT: 'request-evm-contract', FINALIZE_EVM_CONTRACT: 'finalize-evm-contract', + FINALIZE_EVM_CONTRACT_COREWRITER: 'finalize-evm-contract-corewriter', // Post-Launch Management FREEZE_USER: 'freeze-user', @@ -38,6 +39,7 @@ const STRINGS = { GET_CORE_BALANCES: 'get-core-balances', LIST_SPOT_PAIRS: 'list-spot-pairs', SPOT_AUCTION_STATUS: 'spot-auction-status', + LIST_QUOTE_ASSET: 'list-quote-asset', // Utilities TO_BRIDGE: 'to-bridge', @@ -81,6 +83,7 @@ export const CLI_COMMANDS = { // EVM-HyperCore Linking REQUEST_EVM_CONTRACT: STRINGS.REQUEST_EVM_CONTRACT, FINALIZE_EVM_CONTRACT: STRINGS.FINALIZE_EVM_CONTRACT, + FINALIZE_EVM_CONTRACT_COREWRITER: STRINGS.FINALIZE_EVM_CONTRACT_COREWRITER, // Post-Launch Management FREEZE_USER: STRINGS.FREEZE_USER, @@ -93,6 +96,7 @@ export const CLI_COMMANDS = { GET_CORE_BALANCES: STRINGS.GET_CORE_BALANCES, LIST_SPOT_PAIRS: STRINGS.LIST_SPOT_PAIRS, SPOT_AUCTION_STATUS: STRINGS.SPOT_AUCTION_STATUS, + LIST_QUOTE_ASSET: STRINGS.LIST_QUOTE_ASSET, // Utilities TO_BRIDGE: STRINGS.TO_BRIDGE, @@ -137,6 +141,7 @@ export const LOGGER_MODULES = { REQUEST_EVM_CONTRACT: STRINGS.REQUEST_EVM_CONTRACT, FINALIZE_EVM_CONTRACT: STRINGS.FINALIZE_EVM_CONTRACT, REGISTER_TOKEN: STRINGS.REGISTER_TOKEN, + FINALIZE_EVM_CONTRACT_COREWRITER: STRINGS.FINALIZE_EVM_CONTRACT_COREWRITER, // Post-Launch Management FREEZE_USER: STRINGS.FREEZE_USER, @@ -145,6 +150,7 @@ export const LOGGER_MODULES = { // Info & Queries LIST_SPOT_PAIRS: STRINGS.LIST_SPOT_PAIRS, SPOT_AUCTION_STATUS: STRINGS.SPOT_AUCTION_STATUS, + LIST_QUOTE_ASSET: STRINGS.LIST_QUOTE_ASSET, // Utilities INTO_ASSET_BRIDGE_ADDRESS: STRINGS.INTO_ASSET_BRIDGE_ADDRESS, diff --git a/packages/hyperliquid-composer/src/types/constants.ts b/packages/hyperliquid-composer/src/types/constants.ts index 3417e49692..728389f072 100644 --- a/packages/hyperliquid-composer/src/types/constants.ts +++ b/packages/hyperliquid-composer/src/types/constants.ts @@ -18,6 +18,11 @@ export const ENDPOINTS = { EXCHANGE: '/exchange', } +export const HYPE_INDEX = { + MAINNET: 150, + TESTNET: 1105, +} as const + export const MAX_HYPERCORE_SUPPLY = BigInt(2) ** BigInt(64) - BigInt(1) // 18446744073709551615n /** @@ -28,6 +33,7 @@ export const MAX_HYPERCORE_SUPPLY = BigInt(2) ** BigInt(64) - BigInt(1) // 18446 export const QUOTE_TOKENS = { MAINNET: [ { tokenId: 0, name: 'USDC' }, + { tokenId: 235, name: 'USDe' }, { tokenId: 268, name: 'USDT0' }, { tokenId: 360, name: 'USDH' }, ],