This guide provides practical examples for integrating with the Kinetiq liquid staking protocol using both Solidity smart contracts and TypeScript with Viem.
// Core contract addresses
address constant STAKING_MANAGER = 0x393D0B87Ed38fc779FD9611144aE649BA6082109;
address constant KHYPE_TOKEN = 0xfD739d4e423301CE9385c1fb8850539D657C296D;
address constant STAKING_ACCOUNTANT = 0x9209648Ec9D448EF57116B73A2f081835643dc7A;
address constant VALIDATOR_MANAGER = 0x4b797A93DfC3D18Cf98B7322a2b142FA8007508f;
address constant INSTANT_UNSTAKE_POOL = 0x...; // TBD - check latest deploymentimport "./interfaces/IStakingManager.sol";
import "./interfaces/IKHYPEToken.sol";
import "./interfaces/IStakingAccountant.sol";
import "./interfaces/IInstantUnstakePool.sol";pragma solidity ^0.8.20;
import "./interfaces/IStakingManager.sol";
import "./interfaces/IKHYPEToken.sol";
contract KinetiqDepositor {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
constructor(address _stakingManager, address _kHYPE) {
stakingManager = IStakingManager(_stakingManager);
kHYPE = IKHYPEToken(_kHYPE);
}
/**
* @notice Stake HYPE tokens and receive kHYPE
* @dev Sends native HYPE tokens to the staking manager
*/
function stakeHYPE() external payable {
require(msg.value > 0, "Must send HYPE to stake");
// Get user's kHYPE balance before staking
uint256 kHYPEBefore = kHYPE.balanceOf(msg.sender);
// Stake HYPE tokens (msg.value is automatically sent)
stakingManager.stake{value: msg.value}();
// Get user's kHYPE balance after staking
uint256 kHYPEAfter = kHYPE.balanceOf(msg.sender);
emit StakeCompleted(msg.sender, msg.value, kHYPEAfter - kHYPEBefore);
}
event StakeCompleted(address indexed user, uint256 hypeAmount, uint256 kHYPEReceived);
}contract AdvancedKinetiqDepositor {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
IStakingAccountant public immutable stakingAccountant;
constructor(address _stakingManager, address _kHYPE, address _stakingAccountant) {
stakingManager = IStakingManager(_stakingManager);
kHYPE = IKHYPEToken(_kHYPE);
stakingAccountant = IStakingAccountant(_stakingAccountant);
}
/**
* @notice Stake HYPE with validation and expected return calculation
* @param minKHYPEExpected Minimum kHYPE tokens expected to receive
*/
function stakeWithValidation(uint256 minKHYPEExpected) external payable {
require(msg.value > 0, "Must send HYPE to stake");
// Check protocol limits
require(msg.value >= stakingManager.minStakeAmount(), "Below minimum stake");
require(msg.value <= stakingManager.maxStakeAmount(), "Above maximum stake");
// Calculate expected kHYPE amount
uint256 expectedKHYPE = stakingAccountant.HYPEToKHYPE(msg.value);
require(expectedKHYPE >= minKHYPEExpected, "Expected kHYPE too low");
// Check if staking would exceed limit
uint256 currentTotal = stakingManager.totalStaked();
uint256 stakingLimit = stakingManager.stakingLimit();
require(currentTotal + msg.value <= stakingLimit, "Would exceed staking limit");
uint256 kHYPEBefore = kHYPE.balanceOf(msg.sender);
// Execute stake
stakingManager.stake{value: msg.value}();
uint256 kHYPEReceived = kHYPE.balanceOf(msg.sender) - kHYPEBefore;
require(kHYPEReceived >= minKHYPEExpected, "Insufficient kHYPE received");
emit ValidatedStakeCompleted(msg.sender, msg.value, kHYPEReceived, expectedKHYPE);
}
event ValidatedStakeCompleted(
address indexed user,
uint256 hypeAmount,
uint256 kHYPEReceived,
uint256 expectedKHYPE
);
}contract KinetiqWithdrawer {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
// Track user withdrawal requests
mapping(address => uint256[]) public userWithdrawalIds;
constructor(address _stakingManager, address _kHYPE) {
stakingManager = IStakingManager(_stakingManager);
kHYPE = IKHYPEToken(_kHYPE);
}
/**
* @notice Queue a withdrawal request
* @param kHYPEAmount Amount of kHYPE tokens to withdraw
*/
function queueWithdrawal(uint256 kHYPEAmount) external {
require(kHYPEAmount > 0, "Amount must be positive");
require(kHYPE.balanceOf(msg.sender) >= kHYPEAmount, "Insufficient kHYPE balance");
// Get next withdrawal ID for user
uint256 withdrawalId = stakingManager.nextWithdrawalId(msg.sender);
// Queue the withdrawal (this will burn kHYPE tokens)
stakingManager.queueWithdrawal(kHYPEAmount);
// Track withdrawal ID for user
userWithdrawalIds[msg.sender].push(withdrawalId);
emit WithdrawalQueued(msg.sender, withdrawalId, kHYPEAmount);
}
/**
* @notice Confirm a withdrawal request after delay period
* @param withdrawalId ID of the withdrawal request
*/
function confirmWithdrawal(uint256 withdrawalId) external {
// Get withdrawal request details
IStakingManager.WithdrawalRequest memory request =
stakingManager.withdrawalRequests(msg.sender, withdrawalId);
require(request.timestamp > 0, "Withdrawal request not found");
require(
block.timestamp >= request.timestamp + stakingManager.withdrawalDelay(),
"Withdrawal delay not met"
);
uint256 balanceBefore = address(msg.sender).balance;
// Confirm withdrawal (this will send HYPE to user)
stakingManager.confirmWithdrawal(withdrawalId);
uint256 hypeReceived = address(msg.sender).balance - balanceBefore;
emit WithdrawalConfirmed(msg.sender, withdrawalId, hypeReceived);
}
/**
* @notice Get all withdrawal IDs for a user
* @param user User address
* @return Array of withdrawal IDs
*/
function getUserWithdrawalIds(address user) external view returns (uint256[] memory) {
return userWithdrawalIds[user];
}
event WithdrawalQueued(address indexed user, uint256 indexed withdrawalId, uint256 kHYPEAmount);
event WithdrawalConfirmed(address indexed user, uint256 indexed withdrawalId, uint256 hypeReceived);
}contract BatchKinetiqWithdrawer {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
IStakingAccountant public immutable stakingAccountant;
constructor(address _stakingManager, address _kHYPE, address _stakingAccountant) {
stakingManager = IStakingManager(_stakingManager);
kHYPE = IKHYPEToken(_kHYPE);
stakingAccountant = IStakingAccountant(_stakingAccountant);
}
/**
* @notice Queue multiple withdrawal requests in a single transaction
* @param kHYPEAmounts Array of kHYPE amounts to withdraw
*/
function batchQueueWithdrawals(uint256[] calldata kHYPEAmounts) external {
require(kHYPEAmounts.length > 0, "No amounts provided");
uint256 totalKHYPE = 0;
for (uint256 i = 0; i < kHYPEAmounts.length; i++) {
require(kHYPEAmounts[i] > 0, "Amount must be positive");
totalKHYPE += kHYPEAmounts[i];
}
require(kHYPE.balanceOf(msg.sender) >= totalKHYPE, "Insufficient kHYPE balance");
uint256[] memory withdrawalIds = new uint256[](kHYPEAmounts.length);
for (uint256 i = 0; i < kHYPEAmounts.length; i++) {
withdrawalIds[i] = stakingManager.nextWithdrawalId(msg.sender);
stakingManager.queueWithdrawal(kHYPEAmounts[i]);
}
emit BatchWithdrawalQueued(msg.sender, withdrawalIds, kHYPEAmounts);
}
/**
* @notice Confirm multiple withdrawal requests
* @param withdrawalIds Array of withdrawal IDs to confirm
*/
function batchConfirmWithdrawals(uint256[] calldata withdrawalIds) external {
require(withdrawalIds.length > 0, "No withdrawal IDs provided");
uint256 totalHypeReceived = 0;
uint256 balanceBefore = address(msg.sender).balance;
for (uint256 i = 0; i < withdrawalIds.length; i++) {
// Verify withdrawal is ready
IStakingManager.WithdrawalRequest memory request =
stakingManager.withdrawalRequests(msg.sender, withdrawalIds[i]);
require(request.timestamp > 0, "Withdrawal request not found");
require(
block.timestamp >= request.timestamp + stakingManager.withdrawalDelay(),
"Withdrawal delay not met"
);
stakingManager.confirmWithdrawal(withdrawalIds[i]);
}
totalHypeReceived = address(msg.sender).balance - balanceBefore;
emit BatchWithdrawalConfirmed(msg.sender, withdrawalIds, totalHypeReceived);
}
event BatchWithdrawalQueued(address indexed user, uint256[] withdrawalIds, uint256[] kHYPEAmounts);
event BatchWithdrawalConfirmed(address indexed user, uint256[] withdrawalIds, uint256 totalHypeReceived);
}The Instant Exit feature allows users to bypass the standard 7-day withdrawal delay by withdrawing HYPE directly from a liquidity buffer. This provides immediate liquidity at the cost of a slightly higher fee.
| Aspect | Standard Withdrawal | Instant Exit |
|---|---|---|
| Wait Time | 7 days (or 36 hours if buffer-only) | Instant |
| Fee | 0.1% (to treasury) | 0.2% (70% burned, 30% to treasury) |
| Availability | Always available | Subject to buffer liquidity |
| Transactions | 2 (queue + confirm) | 1 |
pragma solidity ^0.8.20;
import "./interfaces/IStakingManager.sol";
import "./interfaces/IInstantUnstakePool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract KinetiqInstantExit {
IStakingManager public immutable stakingManager;
IInstantUnstakePool public immutable instantUnstakePool;
IERC20 public immutable kHYPE;
constructor(
address _stakingManager,
address _instantUnstakePool,
address _kHYPE
) {
stakingManager = IStakingManager(_stakingManager);
instantUnstakePool = IInstantUnstakePool(_instantUnstakePool);
kHYPE = IERC20(_kHYPE);
}
/**
* @notice Execute instant exit with slippage protection
* @param kHYPEAmount Amount of kHYPE to unstake
* @param slippageBps Maximum acceptable slippage in basis points (e.g., 50 = 0.5%)
*/
function instantExit(uint256 kHYPEAmount, uint256 slippageBps) external {
require(kHYPEAmount > 0, "Amount must be positive");
require(kHYPE.balanceOf(msg.sender) >= kHYPEAmount, "Insufficient kHYPE balance");
// Preview the unstake to get expected output and check availability
(uint256 expectedHYPE, uint256 fee, , bool bufferSufficient) =
instantUnstakePool.previewInstantUnstake(kHYPEAmount);
require(bufferSufficient, "Insufficient buffer liquidity");
// Calculate minimum output with slippage tolerance
uint256 minHYPEOut = expectedHYPE * (10000 - slippageBps) / 10000;
// Transfer kHYPE from user to this contract
kHYPE.transferFrom(msg.sender, address(this), kHYPEAmount);
// Approve StakingManager to spend kHYPE
kHYPE.approve(address(stakingManager), kHYPEAmount);
// Execute instant unstake
uint256 balanceBefore = address(this).balance;
stakingManager.instantUnstake(kHYPEAmount, minHYPEOut);
uint256 hypeReceived = address(this).balance - balanceBefore;
// Transfer HYPE to user
(bool success, ) = payable(msg.sender).call{value: hypeReceived}("");
require(success, "HYPE transfer failed");
emit InstantExitCompleted(msg.sender, kHYPEAmount, hypeReceived, fee);
}
/**
* @notice Check if instant exit is available for a given amount
* @param kHYPEAmount Amount of kHYPE to check
* @return available Whether the instant exit can be executed
* @return hypeOutput Expected HYPE output
* @return feeAmount Fee in kHYPE
*/
function checkInstantExitAvailability(uint256 kHYPEAmount)
external
view
returns (bool available, uint256 hypeOutput, uint256 feeAmount)
{
(hypeOutput, feeAmount, , available) =
instantUnstakePool.previewInstantUnstake(kHYPEAmount);
}
/**
* @notice Get current buffer status
* @return currentBuffer Current HYPE in buffer
* @return target Target buffer size
* @return utilizationPercent Buffer utilization percentage (basis points)
*/
function getBufferInfo()
external
view
returns (uint256 currentBuffer, uint256 target, uint256 utilizationPercent)
{
(currentBuffer, , target, utilizationPercent, ) = instantUnstakePool.getBufferStatus();
}
receive() external payable {}
event InstantExitCompleted(
address indexed user,
uint256 kHYPEAmount,
uint256 hypeReceived,
uint256 fee
);
}contract VaultWithInstantExit {
IStakingManager public immutable stakingManager;
IInstantUnstakePool public immutable instantUnstakePool;
IStakingAccountant public immutable stakingAccountant;
IERC20 public immutable kHYPE;
mapping(address => uint256) public shares;
constructor(
address _stakingManager,
address _instantUnstakePool,
address _stakingAccountant,
address _kHYPE
) {
stakingManager = IStakingManager(_stakingManager);
instantUnstakePool = IInstantUnstakePool(_instantUnstakePool);
stakingAccountant = IStakingAccountant(_stakingAccountant);
kHYPE = IERC20(_kHYPE);
}
/**
* @notice Instantly withdraw using instant exit (higher fee, no delay)
* @param shareAmount Amount of shares to redeem
* @param slippageBps Slippage tolerance in basis points
*/
function instantWithdraw(uint256 shareAmount, uint256 slippageBps) external {
require(shares[msg.sender] >= shareAmount, "Insufficient shares");
uint256 kHYPEAmount = shareAmount; // Simplified 1:1 for example
// Preview and validate
(uint256 expectedHYPE, , , bool bufferSufficient) =
instantUnstakePool.previewInstantUnstake(kHYPEAmount);
require(bufferSufficient, "Buffer insufficient - use standard withdrawal");
// Update state before external calls
shares[msg.sender] -= shareAmount;
// Calculate minimum output with slippage
uint256 minHYPEOut = expectedHYPE * (10000 - slippageBps) / 10000;
// Approve and execute
kHYPE.approve(address(stakingManager), kHYPEAmount);
uint256 balanceBefore = address(this).balance;
stakingManager.instantUnstake(kHYPEAmount, minHYPEOut);
uint256 hypeReceived = address(this).balance - balanceBefore;
// Transfer to user
(bool success, ) = payable(msg.sender).call{value: hypeReceived}("");
require(success, "Transfer failed");
emit InstantWithdrawal(msg.sender, shareAmount, hypeReceived);
}
/**
* @notice Check if instant withdrawal is available for shares
* @param shareAmount Amount of shares to check
* @return canInstant True if instant exit is available
* @return expectedHYPE Expected HYPE output
*/
function canInstantWithdraw(uint256 shareAmount)
external
view
returns (bool canInstant, uint256 expectedHYPE)
{
(expectedHYPE, , , canInstant) = instantUnstakePool.previewInstantUnstake(shareAmount);
}
/**
* @notice Get the HYPE value of a user's shares
*/
function getShareValue(address user) external view returns (uint256) {
return stakingAccountant.kHYPEToHYPE(shares[user]);
}
receive() external payable {}
event InstantWithdrawal(address indexed user, uint256 shares, uint256 hypeReceived);
}pragma solidity ^0.8.20;
/**
* @title KinetiqIntegrator
* @notice Complete integration example for Kinetiq protocol
*/
contract KinetiqIntegrator {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
IStakingAccountant public immutable stakingAccountant;
constructor(
address _stakingManager,
address _kHYPE,
address _stakingAccountant
) {
stakingManager = IStakingManager(_stakingManager);
kHYPE = IKHYPEToken(_kHYPE);
stakingAccountant = IStakingAccountant(_stakingAccountant);
}
/**
* @notice Get current exchange rate from HYPE to kHYPE
* @param hypeAmount Amount of HYPE tokens
* @return kHYPE amount that would be received
*/
function getKHYPEForHYPE(uint256 hypeAmount) external view returns (uint256) {
return stakingAccountant.HYPEToKHYPE(hypeAmount);
}
/**
* @notice Get current exchange rate from kHYPE to HYPE
* @param kHYPEAmount Amount of kHYPE tokens
* @return HYPE amount that would be received (before fees)
*/
function getHYPEForKHYPE(uint256 kHYPEAmount) external view returns (uint256) {
return stakingAccountant.kHYPEToHYPE(kHYPEAmount);
}
/**
* @notice Calculate withdrawal fee for unstaking
* @param kHYPEAmount Amount of kHYPE to unstake
* @return fee amount in kHYPE tokens
*/
function calculateWithdrawalFee(uint256 kHYPEAmount) external view returns (uint256) {
uint256 feeRate = stakingManager.unstakeFeeRate();
return (kHYPEAmount * feeRate) / 10000; // feeRate is in basis points
}
/**
* @notice Check if withdrawal request is ready to confirm
* @param user User address
* @param withdrawalId Withdrawal request ID
* @return isReady True if withdrawal can be confirmed
* @return timeRemaining Seconds remaining until confirmation is available
*/
function isWithdrawalReady(address user, uint256 withdrawalId)
external
view
returns (bool isReady, uint256 timeRemaining)
{
IStakingManager.WithdrawalRequest memory request =
stakingManager.withdrawalRequests(user, withdrawalId);
if (request.timestamp == 0) {
return (false, 0);
}
uint256 confirmTime = request.timestamp + stakingManager.withdrawalDelay();
if (block.timestamp >= confirmTime) {
return (true, 0);
} else {
return (false, confirmTime - block.timestamp);
}
}
/**
* @notice Get protocol status and limits
* @return minStake Minimum stake amount
* @return maxStake Maximum stake amount
* @return stakingLimit Total staking limit
* @return totalStaked Current total staked
* @return withdrawalDelay Withdrawal delay in seconds
* @return unstakeFeeRate Unstaking fee rate in basis points
*/
function getProtocolInfo()
external
view
returns (
uint256 minStake,
uint256 maxStake,
uint256 stakingLimit,
uint256 totalStaked,
uint256 withdrawalDelay,
uint256 unstakeFeeRate
)
{
return (
stakingManager.minStakeAmount(),
stakingManager.maxStakeAmount(),
stakingManager.stakingLimit(),
stakingManager.totalStaked(),
stakingManager.withdrawalDelay(),
stakingManager.unstakeFeeRate()
);
}
}contract KinetiqErrorHandler {
IStakingManager public immutable stakingManager;
IKHYPEToken public immutable kHYPE;
/**
* @notice Safe staking with comprehensive error handling
*/
function safeStake() external payable {
// Check basic requirements
require(msg.value > 0, "KinetiqErrorHandler: No HYPE sent");
// Check protocol limits
uint256 minStake = stakingManager.minStakeAmount();
require(msg.value >= minStake, "KinetiqErrorHandler: Below minimum stake");
uint256 maxStake = stakingManager.maxStakeAmount();
require(msg.value <= maxStake, "KinetiqErrorHandler: Above maximum stake");
// Check staking limit
uint256 totalStaked = stakingManager.totalStaked();
uint256 stakingLimit = stakingManager.stakingLimit();
require(
totalStaked + msg.value <= stakingLimit,
"KinetiqErrorHandler: Would exceed staking limit"
);
// Check if user is whitelisted (if whitelist is enabled)
if (stakingManager.whitelistLength() > 0) {
require(
stakingManager.isWhitelisted(msg.sender),
"KinetiqErrorHandler: Not whitelisted"
);
}
try stakingManager.stake{value: msg.value}() {
emit StakeSuccessful(msg.sender, msg.value);
} catch Error(string memory reason) {
emit StakeFailed(msg.sender, msg.value, reason);
// Return HYPE to user
payable(msg.sender).transfer(msg.value);
} catch {
emit StakeFailed(msg.sender, msg.value, "Unknown error");
// Return HYPE to user
payable(msg.sender).transfer(msg.value);
}
}
/**
* @notice Safe withdrawal queue with error handling
*/
function safeQueueWithdrawal(uint256 kHYPEAmount) external {
require(kHYPEAmount > 0, "KinetiqErrorHandler: Invalid amount");
require(
kHYPE.balanceOf(msg.sender) >= kHYPEAmount,
"KinetiqErrorHandler: Insufficient kHYPE balance"
);
try stakingManager.queueWithdrawal(kHYPEAmount) {
emit WithdrawalQueueSuccessful(msg.sender, kHYPEAmount);
} catch Error(string memory reason) {
emit WithdrawalQueueFailed(msg.sender, kHYPEAmount, reason);
} catch {
emit WithdrawalQueueFailed(msg.sender, kHYPEAmount, "Unknown error");
}
}
/**
* @notice Safe instant exit with error handling
*/
function safeInstantExit(uint256 kHYPEAmount, uint256 slippageBps) external {
require(kHYPEAmount > 0, "KinetiqErrorHandler: Invalid amount");
require(
kHYPE.balanceOf(msg.sender) >= kHYPEAmount,
"KinetiqErrorHandler: Insufficient kHYPE balance"
);
// Check if instant exit is enabled
address poolAddress = stakingManager.instantUnstakePool();
require(poolAddress != address(0), "KinetiqErrorHandler: Instant exit not enabled");
// Preview and check buffer
IInstantUnstakePool pool = IInstantUnstakePool(poolAddress);
(uint256 expectedHYPE, , , bool bufferSufficient) = pool.previewInstantUnstake(kHYPEAmount);
require(bufferSufficient, "KinetiqErrorHandler: Insufficient buffer liquidity");
uint256 minHYPEOut = expectedHYPE * (10000 - slippageBps) / 10000;
// Transfer and approve
kHYPE.transferFrom(msg.sender, address(this), kHYPEAmount);
kHYPE.approve(address(stakingManager), kHYPEAmount);
try stakingManager.instantUnstake(kHYPEAmount, minHYPEOut) {
uint256 hypeReceived = address(this).balance;
(bool success, ) = payable(msg.sender).call{value: hypeReceived}("");
require(success, "HYPE transfer failed");
emit InstantExitSuccessful(msg.sender, kHYPEAmount, hypeReceived);
} catch Error(string memory reason) {
// Return kHYPE to user on failure
kHYPE.transfer(msg.sender, kHYPEAmount);
emit InstantExitFailed(msg.sender, kHYPEAmount, reason);
} catch {
kHYPE.transfer(msg.sender, kHYPEAmount);
emit InstantExitFailed(msg.sender, kHYPEAmount, "Unknown error");
}
}
event StakeSuccessful(address indexed user, uint256 amount);
event StakeFailed(address indexed user, uint256 amount, string reason);
event WithdrawalQueueSuccessful(address indexed user, uint256 amount);
event WithdrawalQueueFailed(address indexed user, uint256 amount, string reason);
event InstantExitSuccessful(address indexed user, uint256 kHYPEAmount, uint256 hypeReceived);
event InstantExitFailed(address indexed user, uint256 kHYPEAmount, string reason);
}- Always check protocol limits before staking
- Calculate expected returns and set minimum slippage protection
- Handle errors gracefully with try-catch blocks
- Verify withdrawal readiness before attempting confirmation
- Track withdrawal IDs for users in your contract
- Consider gas optimization for batch operations
- Monitor protocol events for state changes
- Test thoroughly on testnet before mainnet deployment
- Batch multiple operations when possible
- Cache frequently accessed values
- Use
viewfunctions to estimate gas before transactions - Consider using multicall patterns for complex operations
This section provides examples for integrating with Kinetiq protocol using TypeScript and Viem for frontend applications.
npm install viem wagmi
# or
yarn add viem wagmiimport { createPublicClient, createWalletClient, http, parseEther, formatEther } from 'viem'
import { hyperliquid } from 'viem/chains'
// Contract addresses
export const CONTRACTS = {
STAKING_MANAGER: '0x393D0B87Ed38fc779FD9611144aE649BA6082109' as const,
KHYPE_TOKEN: '0xfD739d4e423301CE9385c1fb8850539D657C296D' as const,
STAKING_ACCOUNTANT: '0x9209648Ec9D448EF57116B73A2f081835643dc7A' as const,
VALIDATOR_MANAGER: '0x4b797A93DfC3D18Cf98B7322a2b142FA8007508f' as const,
INSTANT_UNSTAKE_POOL: '0x...' as const, // TBD - check latest deployment
} as const
// ABI fragments for the functions we need
export const STAKING_MANAGER_ABI = [
{
type: 'function',
name: 'stake',
inputs: [],
outputs: [],
stateMutability: 'payable',
},
{
type: 'function',
name: 'queueWithdrawal',
inputs: [{ name: 'amount', type: 'uint256' }],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'confirmWithdrawal',
inputs: [{ name: 'withdrawalId', type: 'uint256' }],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'withdrawalRequests',
inputs: [
{ name: 'user', type: 'address' },
{ name: 'id', type: 'uint256' },
],
outputs: [
{
type: 'tuple',
components: [
{ name: 'hypeAmount', type: 'uint256' },
{ name: 'kHYPEAmount', type: 'uint256' },
{ name: 'kHYPEFee', type: 'uint256' },
{ name: 'bufferUsed', type: 'uint256' },
{ name: 'timestamp', type: 'uint256' },
],
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'nextWithdrawalId',
inputs: [{ name: 'user', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'minStakeAmount',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'maxStakeAmount',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'withdrawalDelay',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'unstakeFeeRate',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'totalStaked',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'event',
name: 'StakeReceived',
inputs: [
{ name: 'staking', type: 'address', indexed: true },
{ name: 'staker', type: 'address', indexed: true },
{ name: 'amount', type: 'uint256', indexed: false },
],
},
{
type: 'event',
name: 'WithdrawalQueued',
inputs: [
{ name: 'staking', type: 'address', indexed: true },
{ name: 'user', type: 'address', indexed: true },
{ name: 'withdrawalId', type: 'uint256', indexed: true },
{ name: 'kHYPEAmount', type: 'uint256', indexed: false },
{ name: 'hypeAmount', type: 'uint256', indexed: false },
{ name: 'feeAmount', type: 'uint256', indexed: false },
],
},
// Instant Exit
{
type: 'function',
name: 'instantUnstake',
inputs: [
{ name: 'kHYPEAmount', type: 'uint256' },
{ name: 'minHYPEOut', type: 'uint256' },
],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'instantUnstakePool',
inputs: [],
outputs: [{ name: '', type: 'address' }],
stateMutability: 'view',
},
{
type: 'event',
name: 'InstantUnstakeExecuted',
inputs: [
{ name: 'user', type: 'address', indexed: true },
{ name: 'kHYPEAmount', type: 'uint256', indexed: false },
{ name: 'hypeReceived', type: 'uint256', indexed: false },
{ name: 'kHYPEFee', type: 'uint256', indexed: false },
{ name: 'feeRateBps', type: 'uint256', indexed: false },
{ name: 'kHYPEFeeBurned', type: 'uint256', indexed: false },
{ name: 'kHYPEFeeToTreasury', type: 'uint256', indexed: false },
],
},
] as const
export const KHYPE_ABI = [
{
type: 'function',
name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'totalSupply',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'approve',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'nonpayable',
},
] as const
export const STAKING_ACCOUNTANT_ABI = [
{
type: 'function',
name: 'HYPEToKHYPE',
inputs: [{ name: 'HYPEAmount', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'kHYPEToHYPE',
inputs: [{ name: 'kHYPEAmount', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
] as const
export const INSTANT_UNSTAKE_POOL_ABI = [
{
type: 'function',
name: 'previewInstantUnstake',
inputs: [{ name: 'kHYPEAmount', type: 'uint256' }],
outputs: [
{ name: 'hypeAmount', type: 'uint256' },
{ name: 'kHYPEFee', type: 'uint256' },
{ name: 'feeRateBps', type: 'uint256' },
{ name: 'bufferSufficient', type: 'bool' },
],
stateMutability: 'view',
},
{
type: 'function',
name: 'getBufferStatus',
inputs: [],
outputs: [
{ name: 'currentBuffer', type: 'uint256' },
{ name: 'effectiveBuffer', type: 'uint256' },
{ name: 'target', type: 'uint256' },
{ name: 'utilizationBps', type: 'uint256' },
{ name: 'pendingAmount', type: 'uint256' },
],
stateMutability: 'view',
},
{
type: 'function',
name: 'instantUnstakeBuffer',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'instantUnstakeFeeRate',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
] as const
// Client setup
export const publicClient = createPublicClient({
chain: hyperliquid,
transport: http(),
})import { parseEther, formatEther, Address, PublicClient, WalletClient } from 'viem'
/**
* Get current exchange rate for HYPE to kHYPE
*/
export async function getKHYPERate(
publicClient: PublicClient,
hypeAmount: bigint
): Promise<bigint> {
const result = await publicClient.readContract({
address: CONTRACTS.STAKING_ACCOUNTANT,
abi: STAKING_ACCOUNTANT_ABI,
functionName: 'HYPEToKHYPE',
args: [hypeAmount],
})
return result
}
/**
* Get protocol limits and information
*/
export async function getProtocolInfo(publicClient: PublicClient) {
const [minStake, maxStake, totalStaked, withdrawalDelay, unstakeFeeRate] = await Promise.all([
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'minStakeAmount',
}),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'maxStakeAmount',
}),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'totalStaked',
}),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'withdrawalDelay',
}),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'unstakeFeeRate',
}),
])
return {
minStake,
maxStake,
totalStaked,
withdrawalDelay,
unstakeFeeRate,
}
}
/**
* Validate stake amount against protocol limits
*/
export async function validateStakeAmount(
publicClient: PublicClient,
amount: bigint
): Promise<{ valid: boolean; error?: string }> {
try {
const { minStake, maxStake } = await getProtocolInfo(publicClient)
if (amount < minStake) {
return {
valid: false,
error: `Amount below minimum stake of ${formatEther(minStake)} HYPE`,
}
}
if (amount > maxStake) {
return {
valid: false,
error: `Amount above maximum stake of ${formatEther(maxStake)} HYPE`,
}
}
return { valid: true }
} catch (error) {
return { valid: false, error: 'Failed to validate stake amount' }
}
}
/**
* Stake HYPE tokens
*/
export async function stakeHYPE(
publicClient: PublicClient,
walletClient: WalletClient,
amountInHYPE: string
): Promise<{ hash: string; expectedKHYPE: bigint }> {
const amount = parseEther(amountInHYPE)
// Validate amount
const validation = await validateStakeAmount(publicClient, amount)
if (!validation.valid) {
throw new Error(validation.error)
}
// Get expected kHYPE amount
const expectedKHYPE = await getKHYPERate(publicClient, amount)
// Execute stake transaction
const hash = await walletClient.writeContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'stake',
value: amount,
})
return { hash, expectedKHYPE }
}
/**
* Estimate gas for staking
*/
export async function estimateStakeGas(
publicClient: PublicClient,
walletClient: WalletClient,
amountInHYPE: string
): Promise<bigint> {
const amount = parseEther(amountInHYPE)
const [account] = await walletClient.getAddresses()
return await publicClient.estimateContractGas({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'stake',
value: amount,
account,
})
}export interface WithdrawalRequest {
hypeAmount: bigint
kHYPEAmount: bigint
kHYPEFee: bigint
bufferUsed: bigint
timestamp: bigint
}
/**
* Get HYPE amount for kHYPE tokens
*/
export async function getHYPERate(
publicClient: PublicClient,
kHYPEAmount: bigint
): Promise<bigint> {
const result = await publicClient.readContract({
address: CONTRACTS.STAKING_ACCOUNTANT,
abi: STAKING_ACCOUNTANT_ABI,
functionName: 'kHYPEToHYPE',
args: [kHYPEAmount],
})
return result
}
/**
* Calculate withdrawal fee
*/
export async function calculateWithdrawalFee(
publicClient: PublicClient,
kHYPEAmount: bigint
): Promise<bigint> {
const feeRate = await publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'unstakeFeeRate',
})
return (kHYPEAmount * feeRate) / 10000n // feeRate is in basis points
}
/**
* Get user's kHYPE balance
*/
export async function getKHYPEBalance(
publicClient: PublicClient,
userAddress: Address
): Promise<bigint> {
return await publicClient.readContract({
address: CONTRACTS.KHYPE_TOKEN,
abi: KHYPE_ABI,
functionName: 'balanceOf',
args: [userAddress],
})
}
/**
* Get next withdrawal ID for user
*/
export async function getNextWithdrawalId(
publicClient: PublicClient,
userAddress: Address
): Promise<bigint> {
return await publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'nextWithdrawalId',
args: [userAddress],
})
}
/**
* Get withdrawal request details
*/
export async function getWithdrawalRequest(
publicClient: PublicClient,
userAddress: Address,
withdrawalId: bigint
): Promise<WithdrawalRequest> {
const result = await publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'withdrawalRequests',
args: [userAddress, withdrawalId],
})
return {
hypeAmount: result[0],
kHYPEAmount: result[1],
kHYPEFee: result[2],
bufferUsed: result[3],
timestamp: result[4],
}
}
/**
* Check if withdrawal is ready to confirm
*/
export async function isWithdrawalReady(
publicClient: PublicClient,
userAddress: Address,
withdrawalId: bigint
): Promise<{ ready: boolean; timeRemaining: number }> {
try {
const [request, withdrawalDelay] = await Promise.all([
getWithdrawalRequest(publicClient, userAddress, withdrawalId),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'withdrawalDelay',
}),
])
if (request.timestamp === 0n) {
return { ready: false, timeRemaining: 0 }
}
const currentTime = BigInt(Math.floor(Date.now() / 1000))
const confirmTime = request.timestamp + withdrawalDelay
if (currentTime >= confirmTime) {
return { ready: true, timeRemaining: 0 }
} else {
return {
ready: false,
timeRemaining: Number(confirmTime - currentTime),
}
}
} catch {
return { ready: false, timeRemaining: 0 }
}
}
/**
* Queue a withdrawal request
*/
export async function queueWithdrawal(
publicClient: PublicClient,
walletClient: WalletClient,
kHYPEAmountStr: string
): Promise<{
hash: string
withdrawalId: bigint
expectedHYPE: bigint
fee: bigint
}> {
const [userAddress] = await walletClient.getAddresses()
const kHYPEAmount = parseEther(kHYPEAmountStr)
// Validate balance
const balance = await getKHYPEBalance(publicClient, userAddress)
if (balance < kHYPEAmount) {
throw new Error('Insufficient kHYPE balance')
}
// Get expected values
const [expectedHYPE, fee, withdrawalId] = await Promise.all([
getHYPERate(publicClient, kHYPEAmount),
calculateWithdrawalFee(publicClient, kHYPEAmount),
getNextWithdrawalId(publicClient, userAddress),
])
// Execute withdrawal queue transaction
const hash = await walletClient.writeContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'queueWithdrawal',
args: [kHYPEAmount],
})
return { hash, withdrawalId, expectedHYPE, fee }
}
/**
* Confirm a withdrawal request
*/
export async function confirmWithdrawal(
publicClient: PublicClient,
walletClient: WalletClient,
withdrawalId: bigint
): Promise<string> {
const [userAddress] = await walletClient.getAddresses()
// Check if withdrawal is ready
const { ready, timeRemaining } = await isWithdrawalReady(publicClient, userAddress, withdrawalId)
if (!ready) {
throw new Error(`Withdrawal not ready. Time remaining: ${timeRemaining} seconds`)
}
// Execute confirmation transaction
const hash = await walletClient.writeContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'confirmWithdrawal',
args: [withdrawalId],
})
return hash
}
/**
* Get all pending withdrawals for user
*/
export async function getPendingWithdrawals(
publicClient: PublicClient,
userAddress: Address
): Promise<
Array<{
id: bigint
request: WithdrawalRequest
ready: boolean
timeRemaining: number
}>
> {
const nextId = await getNextWithdrawalId(publicClient, userAddress)
const pendingWithdrawals = []
// Check the last 10 withdrawal IDs (adjust as needed)
const startId = nextId > 10n ? nextId - 10n : 0n
for (let id = startId; id < nextId; id++) {
try {
const request = await getWithdrawalRequest(publicClient, userAddress, id)
if (request.timestamp > 0n) {
const { ready, timeRemaining } = await isWithdrawalReady(publicClient, userAddress, id)
pendingWithdrawals.push({ id, request, ready, timeRemaining })
}
} catch {
// Skip invalid withdrawal IDs
continue
}
}
return pendingWithdrawals
}The Instant Exit feature allows users to bypass the standard withdrawal delay by withdrawing HYPE directly from a liquidity buffer.
/**
* Preview instant exit to get expected output and check availability
*/
export async function previewInstantExit(
publicClient: PublicClient,
kHYPEAmount: bigint
): Promise<{
hypeOutput: bigint
fee: bigint
feeRateBps: number
isAvailable: boolean
}> {
const result = await publicClient.readContract({
address: CONTRACTS.INSTANT_UNSTAKE_POOL,
abi: INSTANT_UNSTAKE_POOL_ABI,
functionName: 'previewInstantUnstake',
args: [kHYPEAmount],
})
return {
hypeOutput: result[0],
fee: result[1],
feeRateBps: Number(result[2]),
isAvailable: result[3],
}
}
/**
* Get current buffer status for instant exit
*/
export async function getInstantExitBufferStatus(publicClient: PublicClient) {
const result = await publicClient.readContract({
address: CONTRACTS.INSTANT_UNSTAKE_POOL,
abi: INSTANT_UNSTAKE_POOL_ABI,
functionName: 'getBufferStatus',
})
return {
currentBuffer: result[0],
effectiveBuffer: result[1],
target: result[2],
utilizationBps: Number(result[3]),
pendingAmount: result[4],
}
}
/**
* Get instant exit fee rate
*/
export async function getInstantExitFeeRate(publicClient: PublicClient): Promise<number> {
const rate = await publicClient.readContract({
address: CONTRACTS.INSTANT_UNSTAKE_POOL,
abi: INSTANT_UNSTAKE_POOL_ABI,
functionName: 'instantUnstakeFeeRate',
})
return Number(rate)
}
/**
* Execute instant exit with slippage protection
*/
export async function executeInstantExit(
publicClient: PublicClient,
walletClient: WalletClient,
kHYPEAmountStr: string,
slippageBps: number = 50 // 0.5% default slippage
): Promise<{
hash: string
expectedHYPE: bigint
fee: bigint
}> {
const [userAddress] = await walletClient.getAddresses()
const kHYPEAmount = parseEther(kHYPEAmountStr)
// Check balance
const balance = await getKHYPEBalance(publicClient, userAddress)
if (balance < kHYPEAmount) {
throw new Error('Insufficient kHYPE balance')
}
// Preview to check availability and get expected output
const preview = await previewInstantExit(publicClient, kHYPEAmount)
if (!preview.isAvailable) {
throw new Error('Insufficient buffer liquidity - try smaller amount or use standard withdrawal')
}
// Calculate minimum output with slippage tolerance
const minHYPEOut = (preview.hypeOutput * BigInt(10000 - slippageBps)) / BigInt(10000)
// Check and set approval if needed
const allowance = await publicClient.readContract({
address: CONTRACTS.KHYPE_TOKEN,
abi: KHYPE_ABI,
functionName: 'allowance',
args: [userAddress, CONTRACTS.STAKING_MANAGER],
})
if (allowance < kHYPEAmount) {
const approveHash = await walletClient.writeContract({
address: CONTRACTS.KHYPE_TOKEN,
abi: KHYPE_ABI,
functionName: 'approve',
args: [CONTRACTS.STAKING_MANAGER, kHYPEAmount],
})
await publicClient.waitForTransactionReceipt({ hash: approveHash })
}
// Execute instant unstake
const hash = await walletClient.writeContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'instantUnstake',
args: [kHYPEAmount, minHYPEOut],
})
return {
hash,
expectedHYPE: preview.hypeOutput,
fee: preview.fee,
}
}
/**
* Check if instant exit is the better option vs standard withdrawal
*/
export async function compareExitOptions(
publicClient: PublicClient,
kHYPEAmount: bigint
): Promise<{
instantExit: {
available: boolean
hypeOutput: bigint
fee: bigint
feePercent: string
waitTime: string
}
standardWithdrawal: {
hypeOutput: bigint
fee: bigint
feePercent: string
waitTime: string
}
}> {
// Get instant exit preview
const instantPreview = await previewInstantExit(publicClient, kHYPEAmount)
// Get standard withdrawal info
const [hypeOutput, standardFee, withdrawalDelay] = await Promise.all([
getHYPERate(publicClient, kHYPEAmount),
calculateWithdrawalFee(publicClient, kHYPEAmount),
publicClient.readContract({
address: CONTRACTS.STAKING_MANAGER,
abi: STAKING_MANAGER_ABI,
functionName: 'withdrawalDelay',
}),
])
const instantFeePercent = Number((instantPreview.fee * 10000n) / kHYPEAmount) / 100
const standardFeePercent = Number((standardFee * 10000n) / kHYPEAmount) / 100
return {
instantExit: {
available: instantPreview.isAvailable,
hypeOutput: instantPreview.hypeOutput,
fee: instantPreview.fee,
feePercent: `${instantFeePercent.toFixed(2)}%`,
waitTime: 'Instant',
},
standardWithdrawal: {
hypeOutput,
fee: standardFee,
feePercent: `${standardFeePercent.toFixed(2)}%`,
waitTime: `${Number(withdrawalDelay) / 3600} hours`,
},
}
}
/**
* Execute instant exit with monitoring
*/
export async function instantExitWithMonitoring(
publicClient: PublicClient,
walletClient: WalletClient,
kHYPEAmount: string,
slippageBps: number = 50
) {
try {
console.log(`Previewing instant exit for ${kHYPEAmount} kHYPE...`)
const preview = await previewInstantExit(publicClient, parseEther(kHYPEAmount))
console.log(`Expected HYPE: ${formatEther(preview.hypeOutput)}`)
console.log(`Fee: ${formatEther(preview.fee)} kHYPE (${preview.feeRateBps / 100}%)`)
console.log(`Buffer available: ${preview.isAvailable ? 'Yes' : 'No'}`)
if (!preview.isAvailable) {
throw new Error('Buffer liquidity insufficient')
}
console.log('Executing instant exit...')
const result = await executeInstantExit(publicClient, walletClient, kHYPEAmount, slippageBps)
console.log(`Transaction submitted: ${result.hash}`)
const receipt = await publicClient.waitForTransactionReceipt({ hash: result.hash })
console.log(`Transaction confirmed in block: ${receipt.blockNumber}`)
return { ...result, receipt }
} catch (error) {
console.error('Instant exit failed:', error)
throw error
}
}import { createWalletClient, custom } from 'viem'
import { hyperliquid } from 'viem/chains'
/**
* Get comprehensive user dashboard data
*/
export async function getUserDashboard(publicClient: PublicClient, userAddress: Address) {
const [kHYPEBalance, protocolInfo, pendingWithdrawals] = await Promise.all([
getKHYPEBalance(publicClient, userAddress),
getProtocolInfo(publicClient),
getPendingWithdrawals(publicClient, userAddress),
])
// Calculate HYPE equivalent of kHYPE balance
const hypeEquivalent = kHYPEBalance > 0n ? await getHYPERate(publicClient, kHYPEBalance) : 0n
return {
balances: {
kHYPE: formatEther(kHYPEBalance),
hypeEquivalent: formatEther(hypeEquivalent),
},
protocol: {
minStake: formatEther(protocolInfo.minStake),
maxStake: formatEther(protocolInfo.maxStake),
totalStaked: formatEther(protocolInfo.totalStaked),
withdrawalDelayHours: Number(protocolInfo.withdrawalDelay) / 3600,
unstakeFeePercent: Number(protocolInfo.unstakeFeeRate) / 100,
},
withdrawals: pendingWithdrawals.map(w => ({
id: w.id.toString(),
kHYPEAmount: formatEther(w.request.kHYPEAmount),
hypeAmount: formatEther(w.request.hypeAmount),
fee: formatEther(w.request.kHYPEFee),
ready: w.ready,
timeRemainingHours: w.timeRemaining / 3600,
})),
}
}
/**
* Stake with transaction monitoring
*/
export async function stakeWithMonitoring(
publicClient: PublicClient,
walletClient: WalletClient,
amountInHYPE: string
) {
try {
// Estimate gas first
const gasEstimate = await estimateStakeGas(publicClient, walletClient, amountInHYPE)
console.log(`Estimated gas: ${gasEstimate}`)
// Execute stake
const { hash, expectedKHYPE } = await stakeHYPE(publicClient, walletClient, amountInHYPE)
console.log(`Transaction submitted: ${hash}`)
console.log(`Expected kHYPE: ${formatEther(expectedKHYPE)}`)
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(`Transaction confirmed in block: ${receipt.blockNumber}`)
return { hash, receipt, expectedKHYPE }
} catch (error) {
console.error('Stake failed:', error)
throw error
}
}
/**
* Complete withdrawal flow with monitoring
*/
export async function withdrawWithMonitoring(
publicClient: PublicClient,
walletClient: WalletClient,
kHYPEAmount: string
) {
try {
// Queue withdrawal
const queueResult = await queueWithdrawal(publicClient, walletClient, kHYPEAmount)
console.log(`Withdrawal queued: ${queueResult.hash}`)
console.log(`Withdrawal ID: ${queueResult.withdrawalId}`)
console.log(`Expected HYPE: ${formatEther(queueResult.expectedHYPE)}`)
console.log(`Fee: ${formatEther(queueResult.fee)}`)
// Wait for queue confirmation
await publicClient.waitForTransactionReceipt({ hash: queueResult.hash })
return queueResult
} catch (error) {
console.error('Withdrawal queue failed:', error)
throw error
}
}
/**
* Monitor and auto-confirm ready withdrawals
*/
export async function autoConfirmReadyWithdrawals(
publicClient: PublicClient,
walletClient: WalletClient,
userAddress: Address
) {
const pendingWithdrawals = await getPendingWithdrawals(publicClient, userAddress)
const readyWithdrawals = pendingWithdrawals.filter(w => w.ready)
const confirmPromises = readyWithdrawals.map(async withdrawal => {
try {
const hash = await confirmWithdrawal(publicClient, walletClient, withdrawal.id)
console.log(`Confirmed withdrawal ${withdrawal.id}: ${hash}`)
return { id: withdrawal.id, hash, success: true }
} catch (error) {
console.error(`Failed to confirm withdrawal ${withdrawal.id}:`, error)
return { id: withdrawal.id, error, success: false }
}
})
return Promise.all(confirmPromises)
}
// Usage example
export async function initializeKinetiq() {
// Setup wallet client (assuming window.ethereum is available)
const walletClient = createWalletClient({
chain: hyperliquid,
transport: custom(window.ethereum),
})
// Get user address (assuming wallet is connected)
const accounts = await window.ethereum.request({ method: 'eth_accounts' })
const userAddress = accounts[0] as Address
// Get dashboard data
const dashboard = await getUserDashboard(publicClient, userAddress)
console.log('User Dashboard:', dashboard)
return { publicClient, walletClient, userAddress, dashboard }
}/**
* Handle contract errors with user-friendly messages
*/
export function handleContractError(error: any): string {
if (error.message?.includes('insufficient funds')) {
return 'Insufficient HYPE balance for transaction'
}
if (error.message?.includes('Below minimum stake')) {
return 'Stake amount is below the minimum required'
}
if (error.message?.includes('Above maximum stake')) {
return 'Stake amount exceeds the maximum allowed'
}
if (error.message?.includes('Would exceed staking limit')) {
return 'This stake would exceed the protocol limit'
}
if (error.message?.includes('Withdrawal delay not met')) {
return 'Withdrawal is still in the delay period'
}
// Instant Exit errors
if (error.message?.includes('InstantUnstakeNotEnabled')) {
return 'Instant exit feature is not currently enabled'
}
if (error.message?.includes('InsufficientBufferLiquidity')) {
return 'Insufficient buffer liquidity - try a smaller amount or use standard withdrawal'
}
if (error.message?.includes('OutputBelowMinimum')) {
return 'Output would be below minimum - increase slippage tolerance'
}
if (error.message?.includes('BelowMinimum')) {
return 'Amount is below the minimum withdrawal amount'
}
return error.message || 'Transaction failed'
}
/**
* Retry operation with exponential backoff
*/
export async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation()
} catch (error) {
if (i === maxRetries - 1) throw error
const delay = baseDelay * Math.pow(2, i)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('Max retries exceeded')
}
// Usage with error handling
export async function safeStake(
publicClient: PublicClient,
walletClient: WalletClient,
amount: string
) {
try {
const result = await retryWithBackoff(() =>
stakeWithMonitoring(publicClient, walletClient, amount)
)
return { success: true, result }
} catch (error) {
const message = handleContractError(error)
return { success: false, error: message }
}
}import { useState, useCallback, useEffect } from 'react'
import { Address, PublicClient, WalletClient, createWalletClient, custom } from 'viem'
import { hyperliquid } from 'viem/chains'
interface KinetiqClients {
publicClient: PublicClient
walletClient: WalletClient
}
export function useKinetiq(userAddress?: Address) {
const [clients, setClients] = useState<KinetiqClients | null>(null)
const [dashboard, setDashboard] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Initialize clients
useEffect(() => {
const initClients = async () => {
try {
const walletClient = createWalletClient({
chain: hyperliquid,
transport: custom(window.ethereum),
})
setClients({ publicClient, walletClient })
} catch (err) {
setError('Failed to initialize Kinetiq clients')
}
}
initClients()
}, [])
// Load dashboard data
const loadDashboard = useCallback(async () => {
if (!clients || !userAddress) return
setLoading(true)
setError(null)
try {
const data = await getUserDashboard(clients.publicClient, userAddress)
setDashboard(data)
} catch (err) {
setError('Failed to load dashboard data')
} finally {
setLoading(false)
}
}, [clients, userAddress])
// Stake function
const stake = useCallback(
async (amount: string) => {
if (!clients) throw new Error('Clients not initialized')
setLoading(true)
setError(null)
try {
const result = await safeStake(clients.publicClient, clients.walletClient, amount)
if (result.success) {
await loadDashboard() // Refresh data
return result.result
} else {
throw new Error(result.error)
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Stake failed')
throw err
} finally {
setLoading(false)
}
},
[clients, loadDashboard]
)
// Queue withdrawal function
const queueWithdrawal = useCallback(
async (kHYPEAmount: string) => {
if (!clients) throw new Error('Clients not initialized')
setLoading(true)
setError(null)
try {
const result = await withdrawWithMonitoring(
clients.publicClient,
clients.walletClient,
kHYPEAmount
)
await loadDashboard() // Refresh data
return result
} catch (err) {
setError(err instanceof Error ? err.message : 'Withdrawal queue failed')
throw err
} finally {
setLoading(false)
}
},
[clients, loadDashboard]
)
// Confirm withdrawal function
const confirmWithdrawalById = useCallback(
async (withdrawalId: bigint) => {
if (!clients) throw new Error('Clients not initialized')
setLoading(true)
setError(null)
try {
const hash = await confirmWithdrawal(
clients.publicClient,
clients.walletClient,
withdrawalId
)
await loadDashboard() // Refresh data
return hash
} catch (err) {
setError(err instanceof Error ? err.message : 'Withdrawal confirmation failed')
throw err
} finally {
setLoading(false)
}
},
[clients, loadDashboard]
)
// Instant exit function
const instantExit = useCallback(
async (kHYPEAmount: string, slippageBps: number = 50) => {
if (!clients) throw new Error('Clients not initialized')
setLoading(true)
setError(null)
try {
const result = await instantExitWithMonitoring(
clients.publicClient,
clients.walletClient,
kHYPEAmount,
slippageBps
)
await loadDashboard() // Refresh data
return result
} catch (err) {
const message = handleContractError(err)
setError(message)
throw err
} finally {
setLoading(false)
}
},
[clients, loadDashboard]
)
// Preview instant exit
const previewInstantExitAmount = useCallback(
async (kHYPEAmount: string) => {
if (!clients) return null
try {
const preview = await previewInstantExit(clients.publicClient, parseEther(kHYPEAmount))
return {
hypeOutput: formatEther(preview.hypeOutput),
fee: formatEther(preview.fee),
feePercent: preview.feeRateBps / 100,
isAvailable: preview.isAvailable,
}
} catch {
return null
}
},
[clients]
)
return {
clients,
dashboard,
loading,
error,
loadDashboard,
stake,
queueWithdrawal,
confirmWithdrawal: confirmWithdrawalById,
instantExit,
previewInstantExit: previewInstantExitAmount,
}
}