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;import "./interfaces/IStakingManager.sol";
import "./interfaces/IKHYPEToken.sol";
import "./interfaces/IStakingAccountant.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);
}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");
}
}
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);
}- 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,
} 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 },
],
},
] 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
// 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
}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'
}
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])
return {
clients,
dashboard,
loading,
error,
loadDashboard,
stake,
queueWithdrawal,
confirmWithdrawal: confirmWithdrawalById,
}
}