Our testnet lottery is not automatically run every 24h - ping us to run it for you. To do it yourself:
Fetch the lottery fee at Blastscan > Read Proxy Contract > getLotteryFee. Convert this from WEI to ETH, thus, divide by 10^18
Go to Sepolia Blastscan > Write Proxy Contract > RunLottery
For runLottery - payableAmount (ether), enter in the lottery fee in WEI.
For userRandomNumber - bytes32, enter in 0x55fb29339a98ca25bedb7b5aa225041f669ca1407e926a95ce4a9b080ac66907 (note: reusing this is fine, Pyth generates randomness, this seed further maximizes randomness - generate your own with Web3.utils.randomHex(32))
Call userInfo with their address, it returns these three fields
struct User {// Total tickets purchased by the user for current lottery, multiplied by 10000, resets each lottery uint256 ticketsPurchasedTotalBps;// Tracks the total win amount in ETH (how much the user can withdraw) uint256 winningsClaimable;// Whether or not the user is participating in the current lottery bool active; }
This is how we calculate # of tickets purchased per user, as well as if they have any winnings claimable
// Purchased tickets are in BPS, so we need to divide by 10,0000 to get the post-fee number// and then divide by (1 - fee percentage) to get the pre-fee number ticketsPurchased =toDecimal(userInfoData[0]).div(10000).toNumber() / ((100-TICKET_PURCHASE_FEE_PERCENTAGE) /100); winningsClaimable =toDecimal(userInfoData[1],18).toNumber();
Here is the API call we make to Blastscan to fetch lottery results and all users' ticket purchases, then add it into each user's ticket history card.
import { type Address } from"viem";import { blast, blastSepolia } from"viem/chains";import { TICKET_PURCHASE_FEE_PERCENTAGE } from"@/utils/constants";constLOTTERY_CONTRACT_ADDRESS=process.env.NEXT_PUBLIC_CONTRACT_ADDRESSasAddress;constBLAST_LOTTERY_ADDRESS=process.env.NEXT_PUBLIC_BLAST_LOTTERY_ADDRESSasAddress;constCHAIN=process.env.NEXT_PUBLIC_CHAIN||"testnet";constAPI_URL=CHAIN=="mainnet"?blast.blockExplorers.default.apiUrl:blastSepolia.blockExplorers.default.apiUrl;constETH_USER_TICKET_PURCHASE_TOPIC="0xedc823804e4431d900483237575344cd2db13eaacc836fb481c86a0f51d2c182";constBLAST_USER_TICKET_PURCHASE_TOPIC="0xb0cf329104ca766d6830968f6a926d915e398d06541a13106c6b372bb6834df3";constLOTTERY_RUN_TOPIC="0x4718ede503b98b63750fe9c8b5b8410db1b336e47ba2172e8c04abe4a6c8a1fb";constBLASTSCAN_API_KEY=process.env.BLASTSCAN_API_KEYasstring;// fetch ticket purchases for all userstypeTicketPurchaseInfo= { blockNumber:number; ticketsPurchased:number };constfetchTicketPurchases=async (token:string) => {constbatchSize=1000; // Maximum allowed by the APIlet allEvents:any[] = [];let page =1;let hasMoreEvents =true;while (hasMoreEvents) {consturlParams=newURLSearchParams({ module:"logs", action:"getLogs", address: token ==="Blast"?BLAST_LOTTERY_ADDRESS:LOTTERY_CONTRACT_ADDRESS, topic0: token ==="Blast"?BLAST_USER_TICKET_PURCHASE_TOPIC:ETH_USER_TICKET_PURCHASE_TOPIC, fromBlock:"0", toBlock:"latest", page:page.toString(), offset:batchSize.toString(), apikey:BLASTSCAN_API_KEY, });constrequest=`${API_URL}?${urlParams}`;constresponse=awaitfetch(request);constbody=awaitresponse?.json();if (body.status ==="1"&&body.result &&body.result.length>0) { allEvents =allEvents.concat(body.result);if (body.result.length< batchSize) { hasMoreEvents =false; } else { page++;// Add a small delay to avoid rate limitingawaitnewPromise((resolve) =>setTimeout(resolve,200)); } } else { hasMoreEvents =false; } }constticketPurchases=newMap<string,TicketPurchaseInfo[]>();allEvents.forEach((event:any) => {constuserAddress=`0x${event.topics[1].substring(26,66)}`;let ticketsPurchased;if (token ==="Blast") {// For Blast, the tickets purchased is the first 32 bytes of the data ticketsPurchased =parseInt(event.data.substring(0,66),16) /10000/ ((100-TICKET_PURCHASE_FEE_PERCENTAGE) /100);// The referrer address is the next 32 bytes, if you need to use it// const referrerAddress = `0x${event.data.substring(66, 130)}`; } else {// For ETH, the entire data field is the tickets purchased ticketsPurchased =parseInt(event.data,16) /10000/ ((100-TICKET_PURCHASE_FEE_PERCENTAGE) /100); }constblockNumber=parseInt(event.blockNumber,16);if (!ticketPurchases.has(userAddress)) {ticketPurchases.set(userAddress, []); }ticketPurchases.get(userAddress)?.push({ blockNumber, ticketsPurchased }); });return ticketPurchases;};// Add this function to get the current block numberconstgetCurrentBlockNumber=async () => {consturlParams=newURLSearchParams({ module:"block", action:"getblocknobytime", timestamp:Math.floor(Date.now() /1000).toString(), closest:"before", apikey:BLASTSCAN_API_KEY, });constrequest=`${API_URL}?${urlParams}`;constresponse=awaitfetch(request);constbody=awaitresponse?.json();returnparseInt(body.result);};// fetch lottery info (winnerAddress, winAmount, ticketsPurchasedByWinner, blockNumber, transactionHash) for all lotteries
typeLotteryInfo= { winnerAddress:string; winAmount:number; ticketsPurchasedByWinner:number; blockNumber:number; transactionHash:string; estimatedTimestamp:number;};constfetchLotteryInfo=async (token:string) => {constTICKET_PURCHASE_FEE_PERCENTAGE=10;consturlParams=newURLSearchParams({ module:"logs", action:"getLogs", address: token =="Blast"?BLAST_LOTTERY_ADDRESS:LOTTERY_CONTRACT_ADDRESS, topic0:LOTTERY_RUN_TOPIC, apiKey:BLASTSCAN_API_KEY, });constrequest=`${API_URL}?${urlParams}`;constresponse=awaitfetch(request);constbody=awaitresponse?.json();constevents=body.result;constcurrentBlockNumber=awaitgetCurrentBlockNumber();constcurrentTimestamp=Math.floor(Date.now() /1000);constlotteryInfos:LotteryInfo[] = [];events.forEach((event:any) => {// field 1 [66:130]constwinner=event.data.substring(66,130);constwinnerAddress=parseInt(winner,16) ===0?"LP_WINNER":`0x${winner.substring(24,64)}`;// field 3 [194:258]constwinAmount=parseInt(event.data.substring(194,258),16) /10**18;// field 4 [258:322]constticketsPurchasedByWinner=parseInt(event.data.substring(258,322),16) /10000/ ((100-TICKET_PURCHASE_FEE_PERCENTAGE) /100);constblockNumber=parseInt(event.blockNumber,16);consttransactionHash=event.transactionHash;constestimatedTimestamp= currentTimestamp - (currentBlockNumber - blockNumber) *2; // 2 seconds per blockconstlotteryInfo= { winnerAddress, winAmount, ticketsPurchasedByWinner, blockNumber, transactionHash, estimatedTimestamp, };lotteryInfos.push(lotteryInfo); });return lotteryInfos;};// build lottery history for a user address by fetching lottery info and merging with user's ticket purchase counttypeUserLotteryInfo= { winnerAddress:string; winAmount:number; ticketsPurchasedByWinner:number; blockNumber:number; transactionHash:string; ticketsPurchasedByUser:number; estimatedTimestamp:number;};constbuildUserLotteryHistory=async (userAddress:string|undefined, token:string) => {constlotteryInfos=awaitfetchLotteryInfo(token);if (!userAddress) {returnlotteryInfos.map((info) => ({...info, ticketsPurchasedByUser:0, })); }constticketPurchases=awaitfetchTicketPurchases(token);constuserTicketPurchases=ticketPurchases.get(userAddress.toLowerCase()) || [];constuserLotteryHistory:UserLotteryInfo[] = [];let prevBlock =0;lotteryInfos.forEach((lotteryInfo) => {let ticketsPurchasedByUser =0;userTicketPurchases.forEach((userTicketPurchase) => {if ( prevBlock <=userTicketPurchase.blockNumber &&userTicketPurchase.blockNumber <lotteryInfo.blockNumber ) { ticketsPurchasedByUser +=userTicketPurchase.ticketsPurchased; } }); prevBlock =lotteryInfo.blockNumber;userLotteryHistory.push({...lotteryInfo, ticketsPurchasedByUser, }); });return userLotteryHistory;};export { buildUserLotteryHistory,type UserLotteryInfo };
We highlight recent lottery wins and only show lottery results when a user actually entered. This filters out lottery history that's irrelevant for a user and demoralizing since LPs win most of the time: