This guide provides comprehensive documentation for integrating with the HyperPool protocol, including smart contract interfaces, frontend integration patterns, and external API usage.
- Smart Contract API
- Frontend Integration
- Event Monitoring
- Drand VRF Integration
- HyperLend Integration
- Error Handling
- Gas Optimization
- Best Practices
Deposits wHYPE tokens and receives lottery tickets.
Parameters:
amount(uint256): Amount of wHYPE tokens to deposit (must be multiple of 0.1 wHYPE)
Requirements:
- Amount must be > 0
- Amount must be multiple of
TICKET_UNIT(1e17 wei) - Caller must have sufficient wHYPE balance
- Caller must have approved contract to spend tokens
Events Emitted:
Deposited(address indexed user, uint256 amount, uint256 newTickets)
Example:
// Deposit 1 wHYPE (receives 10 tickets)
const amount = ethers.parseEther("1.0");
await lotteryContract.depositWHYPE(amount);Withdraws deposited tokens and burns corresponding tickets.
Parameters:
amount(uint256): Amount of wHYPE tokens to withdraw
Requirements:
- Amount must be > 0
- User must have sufficient deposit balance
- Amount must not exceed user's total deposits
Events Emitted:
Withdrawn(address indexed user, uint256 amount, uint256 burnedTickets)
Example:
// Withdraw 0.5 wHYPE
const amount = ethers.parseEther("0.5");
await lotteryContract.withdraw(amount);Harvests accumulated yield from HyperLend into the prize pool.
Requirements:
- Must wait
HARVEST_INTERVALsince last harvest - Must have accrued yield available
Events Emitted:
YieldHarvested(uint256 yieldAmount, uint256 prizePoolIncrease, address indexed caller, uint256 incentive)IncentivePaid(address indexed caller, uint256 amount, string action)
Returns:
- Incentive payment to caller (1% of harvested yield)
Example:
// Anyone can call this to earn incentives
const tx = await lotteryContract.harvestYield();
const receipt = await tx.wait();Closes the current round and requests VRF randomness.
Requirements:
- Current round must be active
- Round must have exceeded
LOTTERY_INTERVALduration - Must have participants in the round
Events Emitted:
RoundClosed(uint256 indexed round, uint256 drawBlock, uint256 totalTickets, uint256 prizeAmount)RoundVRFRequested(uint256 indexed round, uint256 requestId)
Example:
// Close current round (anyone can call)
await lotteryContract.closeRound();Uses VRF randomness to select winner and distribute prize.
Requirements:
- Round must be in "Closed" state
- VRF randomness must be available
- Must be called after VRF fulfillment
Events Emitted:
RoundFinalized(uint256 indexed round, address indexed winner, uint256 prize)IncentivePaid(address indexed caller, uint256 amount, string action)
Example:
// Finalize round after VRF callback
await lotteryContract.finalizeRound();Returns comprehensive user information.
Parameters:
user(address): User address to query
Returns:
deposits(uint256): Total deposited amounttickets(uint256): Current ticket countallocationBps(uint256): Allocation in basis points (out of 10000)
Example:
const [deposits, tickets, allocation] = await lotteryContract.getUserInfo(userAddress);
console.log(`User has ${ethers.formatEther(deposits)} wHYPE deposited`);
console.log(`User has ${tickets} tickets`);
console.log(`User allocation: ${allocation / 100}%`);Returns detailed information about a specific round.
Parameters:
roundId(uint256): Round number to query
Returns:
startTime(uint256): Round start timestampendTime(uint256): Round end timestampstate(uint8): Round state (0=Active, 1=Closed, 2=Finalized)totalTickets(uint256): Tickets when round closedprizeAmount(uint256): Prize pool amountwinner(address): Winner address (if finalized)participantCount(uint256): Number of participants
Example:
const roundInfo = await lotteryContract.getRoundInfo(1);
console.log(`Round 1 winner: ${roundInfo.winner}`);
console.log(`Prize: ${ethers.formatEther(roundInfo.prizeAmount)} wHYPE`);Returns information about the current active round.
Returns:
roundId(uint256): Current round numbertimeLeft(uint256): Seconds until round can be closedcloseable(bool): Whether round can be closed nowfinalizable(bool): Whether round can be finalizedtotalTickets(uint256): Current total ticketsprizePool(uint256): Current prize pool amount
Example:
const [roundId, timeLeft, closeable, finalizable, tickets, prize] =
await lotteryContract.getCurrentRoundInfo();
if (closeable) {
console.log("Round can be closed!");
}
if (finalizable) {
console.log("Round can be finalized!");
}Checks if yield harvesting is currently available.
Returns:
canHarvest(bool): Whether harvest can be calledyieldAvailable(uint256): Estimated yield amounttimeUntilNext(uint256): Seconds until next harvest allowed
Example:
const [canHarvest, yieldAmount, timeLeft] = await lotteryContract.canHarvest();
if (canHarvest) {
console.log(`Can harvest ${ethers.formatEther(yieldAmount)} wHYPE yield`);
}Returns recent winner history.
Parameters:
count(uint256): Number of recent winners to fetch
Returns:
- Array of winner objects with round info, winner address, and prize amount
Example:
const recentWinners = await lotteryContract.getRecentWinners(5);
recentWinners.forEach((winner, i) => {
console.log(`Round ${winner.round}: ${winner.winner} won ${ethers.formatEther(winner.prize)}`);
});Returns protocol-wide statistics.
Returns:
totalValueLocked(uint256): Total deposits across all userstotalPrizesPaid(uint256): Cumulative prizes distributedtotalParticipants(uint256): Number of unique participantscurrentPrizePool(uint256): Current prize pool balanceavgPrizeSize(uint256): Average prize amounttotalRounds(uint256): Total rounds completed
Example:
const stats = await lotteryContract.getStats();
console.log(`TVL: $${ethers.formatEther(stats.totalValueLocked)}`);
console.log(`Total participants: ${stats.totalParticipants}`);// Contract constants
const TICKET_UNIT = await lotteryContract.TICKET_UNIT(); // 1e17 (0.1 wHYPE)
const LOTTERY_INTERVAL = await lotteryContract.LOTTERY_INTERVAL(); // 600 seconds
const HARVEST_INTERVAL = await lotteryContract.HARVEST_INTERVAL(); // 600 seconds
const INCENTIVE_BPS = await lotteryContract.INCENTIVE_BPS(); // 100 (1%)import { ethers } from 'ethers';
import LotteryABI from './abis/LotteryVRF.json';
// Contract configuration
const LOTTERY_ADDRESS = "0xYourContractAddress";
const provider = new ethers.JsonRpcProvider("https://api.hyperliquid.xyz/evm");
// Read-only contract instance
const lotteryContract = new ethers.Contract(
LOTTERY_ADDRESS,
LotteryABI,
provider
);
// For transactions, use signer
const lotteryContractWithSigner = lotteryContract.connect(signer);import { usePrivy } from '@privy-io/react-auth';
function WalletConnection() {
const { login, logout, authenticated, user } = usePrivy();
if (!authenticated) {
return <button onClick={login}>Connect Wallet</button>;
}
return (
<div>
<span>Connected: {user.wallet?.address}</span>
<button onClick={logout}>Disconnect</button>
</div>
);
}import { parseEther, formatEther } from 'ethers';
async function handleDeposit(amount: string) {
try {
const depositAmount = parseEther(amount);
// Check allowance
const allowance = await tokenContract.allowance(userAddress, LOTTERY_ADDRESS);
// Approve if needed
if (allowance < depositAmount) {
const approveTx = await tokenContract.approve(LOTTERY_ADDRESS, depositAmount);
await approveTx.wait();
}
// Deposit
const depositTx = await lotteryContractWithSigner.depositWHYPE(depositAmount);
const receipt = await depositTx.wait();
console.log("Deposit successful:", receipt);
} catch (error) {
console.error("Deposit failed:", error);
}
}import { useEffect, useState } from 'react';
function useLotteryData(userAddress: string) {
const [userData, setUserData] = useState(null);
const [currentRound, setCurrentRound] = useState(null);
const [stats, setStats] = useState(null);
useEffect(() => {
async function fetchData() {
try {
// Fetch user data
const [deposits, tickets, allocation] = await lotteryContract.getUserInfo(userAddress);
setUserData({ deposits, tickets, allocation });
// Fetch current round
const roundInfo = await lotteryContract.getCurrentRoundInfo();
setCurrentRound(roundInfo);
// Fetch stats
const protocolStats = await lotteryContract.getStats();
setStats(protocolStats);
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
fetchData();
const interval = setInterval(fetchData, 10000); // Update every 10s
return () => clearInterval(interval);
}, [userAddress]);
return { userData, currentRound, stats };
}// Listen for deposits
lotteryContract.on('Deposited', (user, amount, tickets, event) => {
console.log(`${user} deposited ${formatEther(amount)} wHYPE for ${tickets} tickets`);
// Update UI
if (user.toLowerCase() === userAddress.toLowerCase()) {
refreshUserData();
}
refreshStats();
});
// Listen for round finalization
lotteryContract.on('RoundFinalized', (round, winner, prize, event) => {
console.log(`Round ${round} winner: ${winner} won ${formatEther(prize)} wHYPE`);
// Show celebration if user won
if (winner.toLowerCase() === userAddress.toLowerCase()) {
showWinnerModal(round, prize);
}
refreshCurrentRound();
});
// Listen for yield harvests
lotteryContract.on('YieldHarvested', (yieldAmount, prizeIncrease, caller, incentive, event) => {
console.log(`Yield harvested: ${formatEther(yieldAmount)} wHYPE`);
console.log(`Caller ${caller} earned ${formatEther(incentive)} incentive`);
refreshStats();
});async function getHistoricalWinners(fromBlock = 0, toBlock = 'latest') {
const filter = lotteryContract.filters.RoundFinalized();
const events = await lotteryContract.queryFilter(filter, fromBlock, toBlock);
return events.map(event => ({
round: event.args.round,
winner: event.args.winner,
prize: event.args.prize,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash
}));
}
async function getUserDeposits(userAddress: string) {
const filter = lotteryContract.filters.Deposited(userAddress);
const events = await lotteryContract.queryFilter(filter);
return events.map(event => ({
amount: event.args.amount,
tickets: event.args.newTickets,
timestamp: event.blockNumber, // Convert to actual timestamp if needed
txHash: event.transactionHash
}));
}// Monitor VRF requests
lotteryContract.on('RoundVRFRequested', async (round, requestId, event) => {
console.log(`VRF requested for round ${round}, ID: ${requestId}`);
// Start monitoring Drand for fulfillment
monitorVRFFulfillment(requestId);
});
async function monitorVRFFulfillment(requestId: bigint) {
const drandContract = new ethers.Contract(DRAND_VRF_ADDRESS, DrandVRFABI, provider);
// Listen for fulfillment
const filter = drandContract.filters.RandomnessFulfilled(requestId);
drandContract.once(filter, (reqId, randomness, event) => {
console.log(`VRF fulfilled for request ${reqId}: ${randomness}`);
// Check if round can now be finalized
checkRoundFinalizeable();
});
}// Fetch current Drand round
async function getCurrentDrandRound() {
const response = await fetch('https://api.drand.sh/public/latest');
const data = await response.json();
return {
round: data.round,
randomness: data.randomness,
signature: data.signature,
previous_signature: data.previous_signature
};
}
// Calculate future Drand round for deadline
function calculateDrandRound(deadline: number) {
const DRAND_GENESIS_TIME = 1595431050; // Drand genesis timestamp
const DRAND_PERIOD = 30; // 30 seconds per round
const roundNumber = Math.floor((deadline - DRAND_GENESIS_TIME) / DRAND_PERIOD);
return roundNumber;
}import HyperLendPoolABI from './abis/HyperLendPool.json';
const hyperLendPool = new ethers.Contract(
HYPERLEND_POOL_ADDRESS,
HyperLendPoolABI,
provider
);
// Check lending pool reserves
async function getPoolReserveData(assetAddress: string) {
const reserveData = await hyperLendPool.getReserveData(assetAddress);
return {
liquidityRate: reserveData.currentLiquidityRate,
utilizationRate: reserveData.currentUtilizationRate,
totalSupply: reserveData.totalSupply,
totalBorrow: reserveData.totalStableDebt.add(reserveData.totalVariableDebt)
};
}// Calculate expected APY from HyperLend
async function calculateExpectedAPY() {
const reserveData = await getPoolReserveData(WHYPE_TOKEN_ADDRESS);
// HyperLend uses ray math (1e27 precision)
const RAY = BigInt(1e27);
const SECONDS_PER_YEAR = BigInt(365 * 24 * 60 * 60);
// Convert liquidityRate to APY
const liquidityRate = reserveData.liquidityRate;
const apy = (liquidityRate * SECONDS_PER_YEAR) / RAY;
return Number(apy) / 1e27 * 100; // Convert to percentage
}// Define custom error types from contract
const LOTTERY_ERRORS = {
AmountMustBePositive: "Amount must be greater than zero",
InsufficientBalance: "Insufficient token balance",
InvalidTicketAmount: "Amount must be multiple of 0.1 wHYPE",
InsufficientDeposit: "Insufficient deposit for withdrawal",
RoundNotActive: "No active round available",
RoundNotEnded: "Round has not ended yet",
NoTickets: "No tickets in this round",
NoPrize: "No prize pool available"
};
// Error parsing helper
function parseContractError(error: any): string {
const errorData = error.data || error.error?.data;
if (errorData) {
// Try to decode custom error
const errorSelector = errorData.slice(0, 10);
// Match against known selectors
for (const [errorName, message] of Object.entries(LOTTERY_ERRORS)) {
if (errorSelector === getErrorSelector(errorName)) {
return message;
}
}
}
// Fallback to generic error message
return error.reason || error.message || "Transaction failed";
}
function getErrorSelector(errorName: string): string {
return ethers.id(`${errorName}()`).slice(0, 10);
}async function safeContractCall(contractFunction: () => Promise<any>) {
try {
const tx = await contractFunction();
const receipt = await tx.wait();
if (receipt.status === 0) {
throw new Error("Transaction reverted");
}
return receipt;
} catch (error: any) {
console.error("Contract call failed:", error);
// Parse and display user-friendly error
const userMessage = parseContractError(error);
// Handle specific error types
if (error.code === 'INSUFFICIENT_FUNDS') {
throw new Error("Insufficient balance for gas fees");
}
if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
throw new Error("Transaction would fail - check your inputs");
}
throw new Error(userMessage);
}
}// Batch approval and deposit
async function depositWithApproval(amount: bigint) {
const calls = [];
// Check current allowance
const allowance = await tokenContract.allowance(userAddress, LOTTERY_ADDRESS);
if (allowance < amount) {
calls.push({
target: TOKEN_ADDRESS,
data: tokenContract.interface.encodeFunctionData("approve", [LOTTERY_ADDRESS, amount])
});
}
calls.push({
target: LOTTERY_ADDRESS,
data: lotteryContract.interface.encodeFunctionData("depositWHYPE", [amount])
});
// Execute batch transaction (requires multicall contract)
await multicallContract.aggregate(calls);
}async function estimateTransactionCost(functionName: string, args: any[]) {
try {
const gasEstimate = await lotteryContract[functionName].estimateGas(...args);
const gasPrice = await provider.getFeeData();
const cost = gasEstimate * (gasPrice.gasPrice || gasPrice.maxFeePerGas || 0n);
return {
gasLimit: gasEstimate,
gasPrice: gasPrice.gasPrice,
cost: cost,
costFormatted: formatEther(cost)
};
} catch (error) {
console.error("Gas estimation failed:", error);
return null;
}
}- Always validate user inputs
function validateDepositAmount(amount: string): boolean {
const depositAmount = parseEther(amount);
const ticketUnit = parseEther("0.1");
return depositAmount > 0 && depositAmount % ticketUnit === 0n;
}- Use safe arithmetic
// Use BigInt for all calculations
const tickets = deposits / TICKET_UNIT;
const allocation = (tickets * 10000n) / totalTickets;- Handle edge cases
async function safeGetUserInfo(address: string) {
if (!ethers.isAddress(address)) {
throw new Error("Invalid address");
}
try {
return await lotteryContract.getUserInfo(address);
} catch (error) {
return { deposits: 0n, tickets: 0n, allocation: 0n };
}
}- Cache contract instances
const contractCache = new Map();
function getContract(address: string, abi: any) {
const key = `${address}-${JSON.stringify(abi)}`;
if (!contractCache.has(key)) {
contractCache.set(key, new ethers.Contract(address, abi, provider));
}
return contractCache.get(key);
}- Batch multiple queries
async function fetchAllUserData(userAddress: string) {
const [userInfo, balance, allowance] = await Promise.all([
lotteryContract.getUserInfo(userAddress),
tokenContract.balanceOf(userAddress),
tokenContract.allowance(userAddress, LOTTERY_ADDRESS)
]);
return { userInfo, balance, allowance };
}- Use multicall for complex queries
import { Multicall } from '@ethersproject/providers';
async function batchQuery(calls: Array<{contract: Contract, method: string, params: any[]}>) {
const multicall = new Multicall(provider);
const results = await multicall.all(
calls.map(call => call.contract[call.method](...call.params))
);
return results;
}This API reference provides comprehensive guidance for integrating with the HyperPool protocol across all supported interfaces and use cases.