Kinetiq

Integration guide

This guide provides practical examples for integrating with the Kinetiq liquid staking protocol using both Solidity smart contracts and TypeScript with Viem.

Contract addresses (Hyperliquid Mainnet)

// Core contract addresses
address constant STAKING_MANAGER = 0x393D0B87Ed38fc779FD9611144aE649BA6082109;
address constant KHYPE_TOKEN = 0xfD739d4e423301CE9385c1fb8850539D657C296D;
address constant STAKING_ACCOUNTANT = 0x9209648Ec9D448EF57116B73A2f081835643dc7A;
address constant VALIDATOR_MANAGER = 0x4b797A93DfC3D18Cf98B7322a2b142FA8007508f;

Interface imports

import "./interfaces/IStakingManager.sol";
import "./interfaces/IKHYPEToken.sol";
import "./interfaces/IStakingAccountant.sol";

1. Depositing (staking) HYPE tokens

Basic deposit example

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);
}

Advanced deposit with validation

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
    );
}

2. Withdrawals (unstaking)

Basic withdrawal flow

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);
}

Advanced withdrawal with batching

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);
}

3. Complete integration example

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()
        );
    }
}

Error handling and best practices

Common errors and solutions

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");
        }
    }

    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);
}

Integration best practices

  1. Always check protocol limits before staking
  2. Calculate expected returns and set minimum slippage protection
  3. Handle errors gracefully with try-catch blocks
  4. Verify withdrawal readiness before attempting confirmation
  5. Track withdrawal IDs for users in your contract
  6. Consider gas optimization for batch operations
  7. Monitor protocol events for state changes
  8. Test thoroughly on testnet before mainnet deployment

Gas optimization tips

  • Batch multiple operations when possible
  • Cache frequently accessed values
  • Use view functions to estimate gas before transactions
  • Consider using multicall patterns for complex operations

TypeScript integration with viem

This section provides examples for integrating with Kinetiq protocol using TypeScript and Viem for frontend applications.

Setup and dependencies

npm install viem wagmi
# or
yarn add viem wagmi
# or
pnpm i viem wagmi

Contract configuration

import {
  createPublicClient,
  createWalletClient,
  formatEther,
  http,
  parseEther,
} from 'viem'
import { hyperliquid } from 'viem/chains'

// Contract addresses
export const CONTRACTS = {
  KHYPE_TOKEN: '0xfD739d4e423301CE9385c1fb8850539D657C296D' as const,
  STAKING_ACCOUNTANT: '0x9209648Ec9D448EF57116B73A2f081835643dc7A' as const,
  STAKING_MANAGER: '0x393D0B87Ed38fc779FD9611144aE649BA6082109' as const,
  VALIDATOR_MANAGER: '0x4b797A93DfC3D18Cf98B7322a2b142FA8007508f' as const,
} as const

// ABI fragments for the functions we need
export const KHYPE_ABI = [
  {
    inputs: [{ name: 'account', type: 'address' }],
    name: 'balanceOf',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'totalSupply',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    name: 'approve',
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'nonpayable',
    type: 'function',
  },
] as const
export const STAKING_ACCOUNTANT_ABI = [
  {
    inputs: [{ name: 'HYPEAmount', type: 'uint256' }],
    name: 'HYPEToKHYPE',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [{ name: 'kHYPEAmount', type: 'uint256' }],
    name: 'kHYPEToHYPE',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
] as const
export const STAKING_MANAGER_ABI = [
  {
    inputs: [],
    name: 'stake',
    outputs: [],
    stateMutability: 'payable',
    type: 'function',
  },
  {
    inputs: [{ name: 'amount', type: 'uint256' }],
    name: 'queueWithdrawal',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [{ name: 'withdrawalId', type: 'uint256' }],
    name: 'confirmWithdrawal',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      { name: 'user', type: 'address' },
      { name: 'id', type: 'uint256' },
    ],
    name: 'withdrawalRequests',
    outputs: [
      {
        components: [
          { name: 'hypeAmount', type: 'uint256' },
          { name: 'kHYPEAmount', type: 'uint256' },
          { name: 'kHYPEFee', type: 'uint256' },
          { name: 'bufferUsed', type: 'uint256' },
          { name: 'timestamp', type: 'uint256' },
        ],
        type: 'tuple',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [{ name: 'user', type: 'address' }],
    name: 'nextWithdrawalId',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'minStakeAmount',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'maxStakeAmount',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'withdrawalDelay',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'unstakeFeeRate',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'totalStaked',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      { indexed: true, name: 'staking', type: 'address' },
      { indexed: true, name: 'staker', type: 'address' },
      { indexed: false, name: 'amount', type: 'uint256' },
    ],
    name: 'StakeReceived',
    type: 'event',
  },
  {
    inputs: [
      { indexed: true, name: 'staking', type: 'address' },
      { indexed: true, name: 'user', type: 'address' },
      { indexed: true, name: 'withdrawalId', type: 'uint256' },
      { indexed: false, name: 'kHYPEAmount', type: 'uint256' },
      { indexed: false, name: 'hypeAmount', type: 'uint256' },
      { indexed: false, name: 'feeAmount', type: 'uint256' },
    ],
    name: 'WithdrawalQueued',
    type: 'event',
  },
] as const

// Client setup
export const publicClient = createPublicClient({
  chain: hyperliquid,
  transport: http(),
})

1. Depositing (staking) HYPE

import {
  type Address,
  formatEther,
  parseEther,
  type PublicClient,
  type WalletClient,
} from 'viem'

/**
 * Get current exchange rate for HYPE to kHYPE
 */
export const getKHYPERate = async ({
  amount,
  publicClient,
}: {
  amount: bigint
  publicClient: PublicClient
}): Promise<bigint> => {
  const result = await publicClient.readContract({
    abi: STAKING_ACCOUNTANT_ABI,
    address: CONTRACTS.STAKING_ACCOUNTANT,
    args: [amount],
    functionName: 'HYPEToKHYPE',
  })
  return result
}

/**
 * Get protocol limits and information
 */
export const getProtocolInfo = async ({
  publicClient,
}: {
  publicClient: PublicClient
}) => {
  const [minStake, maxStake, totalStaked, withdrawalDelay, unstakeFeeRate] =
    await Promise.all([
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        functionName: 'minStakeAmount',
      }),
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        functionName: 'maxStakeAmount',
      }),
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        functionName: 'totalStaked',
      }),
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        functionName: 'withdrawalDelay',
      }),
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        functionName: 'unstakeFeeRate',
      }),
    ])

  return {
    maxStake,
    minStake,
    totalStaked,
    unstakeFeeRate,
    withdrawalDelay,
  }
}

/**
 * Validate stake amount against protocol limits
 */
export const validateStakeAmount = async ({
  amount,
  publicClient,
}: {
  amount: bigint
  publicClient: PublicClient
}): Promise<{ error?: string; valid: boolean }> => {
  try {
    const { maxStake, minStake } = await getProtocolInfo({ publicClient })

    if (amount < minStake) {
      return {
        error: `Amount below minimum stake of ${formatEther(minStake)} HYPE`,
        valid: false,
      }
    }

    if (amount > maxStake) {
      return {
        error: `Amount above maximum stake of ${formatEther(maxStake)} HYPE`,
        valid: false,
      }
    }

    return { valid: true }
  } catch (error) {
    return { error: 'Failed to validate stake amount', valid: false }
  }
}

/**
 * Stake HYPE tokens
 */
export const stakeHYPE = async ({
  amountInHYPE,
  publicClient,
  walletClient,
}: {
  amountInHYPE: string
  publicClient: PublicClient
  walletClient: WalletClient
}): Promise<{ expectedKHYPE: bigint; hash: string }> => {
  const amount = parseEther(amountInHYPE)

  // Validate amount
  const validation = await validateStakeAmount({ amount, publicClient })
  if (!validation.valid) {
    throw new Error(validation.error)
  }

  // Get expected kHYPE amount
  const expectedKHYPE = await getKHYPERate({ amount, publicClient })

  // Execute stake transaction
  const hash = await walletClient.writeContract({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    functionName: 'stake',
    value: amount,
  })

  return { expectedKHYPE, hash }
}

/**
 * Estimate gas for staking
 */
export const estimateStakeGas = async ({
  amountInHYPE,
  publicClient,
  walletClient,
}: {
  amountInHYPE: string
  publicClient: PublicClient
  walletClient: WalletClient
}): Promise<bigint> => {
  const amount = parseEther(amountInHYPE)
  const [account] = await walletClient.getAddresses()

  return await publicClient.estimateContractGas({
    abi: STAKING_MANAGER_ABI,
    account,
    address: CONTRACTS.STAKING_MANAGER,
    functionName: 'stake',
    value: amount,
  })
}

2. Withdrawals (unstaking)

export interface WithdrawalRequest {
  bufferUsed: bigint
  hypeAmount: bigint
  kHYPEAmount: bigint
  kHYPEFee: bigint
  timestamp: bigint
}

/**
 * Get HYPE amount for kHYPE tokens
 */
export const getHYPERate = async ({
  amount,
  publicClient,
}: {
  amount: bigint
  publicClient: PublicClient
}): Promise<bigint> => {
  const result = await publicClient.readContract({
    abi: STAKING_ACCOUNTANT_ABI,
    address: CONTRACTS.STAKING_ACCOUNTANT,
    args: [amount],
    functionName: 'kHYPEToHYPE',
  })
  return result
}

/**
 * Calculate withdrawal fee
 */
export const calculateWithdrawalFee = async ({
  amount,
  publicClient,
}: {
  amount: bigint
  publicClient: PublicClient
}): Promise<bigint> => {
  const feeRate = await publicClient.readContract({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    functionName: 'unstakeFeeRate',
  })

  return (amount * feeRate) / 10000n // feeRate is in basis points
}

/**
 * Get user's kHYPE balance
 */
export const getKHYPEBalance = async ({
  publicClient,
  userAddress,
}: {
  publicClient: PublicClient
  userAddress: Address
}): Promise<bigint> =>
  await publicClient.readContract({
    abi: KHYPE_ABI,
    address: CONTRACTS.KHYPE_TOKEN,
    args: [userAddress],
    functionName: 'balanceOf',
  })

/**
 * Get next withdrawal ID for user
 */
export const getNextWithdrawalId = async ({
  publicClient,
  userAddress,
}: {
  publicClient: PublicClient
  userAddress: Address
}): Promise<bigint> =>
  await publicClient.readContract({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    args: [userAddress],
    functionName: 'nextWithdrawalId',
  })

/**
 * Get withdrawal request details
 */
export const getWithdrawalRequest = async ({
  publicClient,
  userAddress,
  withdrawalId,
}: {
  publicClient: PublicClient
  userAddress: Address
  withdrawalId: bigint
}): Promise<WithdrawalRequest> => {
  const result = await publicClient.readContract({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    args: [userAddress, withdrawalId],
    functionName: 'withdrawalRequests',
  })

  return {
    bufferUsed: result[3],
    hypeAmount: result[0],
    kHYPEAmount: result[1],
    kHYPEFee: result[2],
    timestamp: result[4],
  }
}

/**
 * Check if withdrawal is ready to confirm
 */
export const isWithdrawalReady = async ({
  publicClient,
  userAddress,
  withdrawalId,
}: {
  publicClient: PublicClient
  userAddress: Address
  withdrawalId: bigint
}): Promise<{ ready: boolean; timeRemaining: number }> => {
  try {
    const [request, withdrawalDelay] = await Promise.all([
      getWithdrawalRequest(publicClient, userAddress, withdrawalId),
      publicClient.readContract({
        abi: STAKING_MANAGER_ABI,
        address: CONTRACTS.STAKING_MANAGER,
        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 const queueWithdrawal = async ({
  kHYPEAmountStr,
  publicClient,
  walletClient,
}: {
  kHYPEAmountStr: string
  publicClient: PublicClient
  walletClient: WalletClient
}): Promise<{
  expectedHYPE: bigint
  fee: bigint
  hash: string
  withdrawalId: 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({ amount: kHYPEAmount, publicClient }),
    calculateWithdrawalFee({ amount: kHYPEAmount, publicClient }),
    getNextWithdrawalId({ publicClient, userAddress }),
  ])

  // Execute withdrawal queue transaction
  const hash = await walletClient.writeContract({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    args: [kHYPEAmount],
    functionName: 'queueWithdrawal',
  })

  return { expectedHYPE, fee, hash, withdrawalId }
}

/**
 * Confirm a withdrawal request
 */
export const confirmWithdrawal = async (
  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({
    abi: STAKING_MANAGER_ABI,
    address: CONTRACTS.STAKING_MANAGER,
    args: [withdrawalId],
    functionName: 'confirmWithdrawal',
  })

  return hash
}

/**
 * Get all pending withdrawals for user
 */
export const getPendingWithdrawals = async (
  publicClient: PublicClient,
  userAddress: Address,
): Promise<
  Array<{
    id: bigint
    ready: boolean
    request: WithdrawalRequest
    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 += 1) {
    try {
      const request = await getWithdrawalRequest({
        publicClient,
        userAddress,
        withdrawalId: id,
      })
      if (request.timestamp > 0n) {
        const { ready, timeRemaining } = await isWithdrawalReady({
          publicClient,
          userAddress,
          withdrawalId: id,
        })
        pendingWithdrawals.push({ id, ready, request, timeRemaining })
      }
    } catch {
      // Skip invalid withdrawal IDs
      continue
    }
  }

  return pendingWithdrawals
}

3. Complete integration example

import { createWalletClient, custom } from 'viem'
import { hyperliquid } from 'viem/chains'

/**
 * Get comprehensive user dashboard data
 */
export const getUserDashboard = async ({
  publicClient,
  userAddress,
}: {
  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({ amount: kHYPEBalance, publicClient })
      : 0n

  return {
    balances: {
      hypeEquivalent: formatEther(hypeEquivalent),
      kHYPE: formatEther(kHYPEBalance),
    },
    protocol: {
      maxStake: formatEther(protocolInfo.maxStake),
      minStake: formatEther(protocolInfo.minStake),
      totalStaked: formatEther(protocolInfo.totalStaked),
      unstakeFeePercent: Number(protocolInfo.unstakeFeeRate) / 100,
      withdrawalDelayHours: Number(protocolInfo.withdrawalDelay) / 3600,
    },
    withdrawals: pendingWithdrawals.map(
      ({ id, ready, request, timeRemaining }) => ({
        fee: formatEther(request.kHYPEFee),
        hypeAmount: formatEther(request.hypeAmount),
        id: id.toString(),
        kHYPEAmount: formatEther(request.kHYPEAmount),
        ready: ready,
        timeRemainingHours: timeRemaining / 3600,
      }),
    ),
  }
}

/**
 * Stake with transaction monitoring
 */
export const stakeWithMonitoring = async ({
  amountInHYPE,
  publicClient,
  walletClient,
}: {
  amountInHYPE: string
  publicClient: PublicClient
  walletClient: WalletClient
}) => {
  try {
    // Estimate gas first
    const gasEstimate = await estimateStakeGas({
      amountInHYPE,
      publicClient,
      walletClient,
    })
    console.log(`Estimated gas: ${gasEstimate}`)

    // Execute stake
    const { expectedKHYPE, hash } = await stakeHYPE({
      amountInHYPE,
      publicClient,
      walletClient,
    })
    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 { expectedKHYPE, hash, receipt }
  } catch (error) {
    console.error('Stake failed:', error)
    throw error
  }
}

/**
 * Complete withdrawal flow with monitoring
 */
export const withdrawWithMonitoring = async ({
  kHYPEAmount,
  publicClient,
  walletClient,
}: {
  kHYPEAmount: string
  publicClient: PublicClient
  walletClient: WalletClient
}) => {
  try {
    // Queue withdrawal
    const queueResult = await queueWithdrawal({
      kHYPEAmountStr: kHYPEAmount,
      publicClient,
      walletClient,
    })
    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 const autoConfirmReadyWithdrawals = async (
  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,
        withdrawalId: withdrawal.id,
      })
      console.log(`Confirmed withdrawal ${withdrawal.id}: ${hash}`)
      return { hash, id: withdrawal.id, success: true }
    } catch (error) {
      console.error(`Failed to confirm withdrawal ${withdrawal.id}:`, error)
      return { error, id: withdrawal.id, success: false }
    }
  })

  return Promise.all(confirmPromises)
}

// Usage example
export const initializeKinetiq = async () => {
  // 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 { dashboard, publicClient, userAddress, walletClient }
}

Error handling and best practices

/**
 * Handle contract errors with user-friendly messages
 */
export const handleContractError = ({ error }: { 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'
  }

  return error.message || 'Transaction failed'
}

/**
 * Retry operation with exponential backoff
 */
export const retryWithBackoff = async <T>({
  baseDelay = 1000,
  maxRetries = 3,
  operation,
}: {
  baseDelay: number
  maxRetries: number
  operation: () => Promise<T>
}): Promise<T> => {
  for (let tryCount = 0; tryCount < maxRetries; tryCount += 1) {
    try {
      return await operation()
    } catch (error) {
      if (tryCount === maxRetries - 1) {
        throw error
      }

      const delay = baseDelay * Math.pow(2, tryCount)
      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }
  throw new Error('Max retries exceeded')
}

// Usage with error handling
export const safeStake = async ({
  amount,
  publicClient,
  walletClient,
}: {
  amount: string
  publicClient: PublicClient
  walletClient: WalletClient
}) => {
  try {
    const result = await retryWithBackoff({
      operation: () =>
        stakeWithMonitoring({
          amountInHYPE: amount,
          publicClient,
          walletClient,
        }),
    })
    return { result, success: true }
  } catch (error) {
    const message = handleContractError({ error })
    return { error: message, success: false }
  }
}
Integration guide | Docs | Kinetiq