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))
A user buys a ticket worth 100 $BLAST, but after 15% fees, only 85 $BLAST is “entered” into the lottery pool.
Thus, if the jackpot is 100000 $BLAST, we want to show that a ticket has 1 in 1176 odds of winning rather than 1 in 1000. This is how we calculate this:
We recommend showing a loading indicator for both the approval and ticket purchase. You should prompt the user twice since these are two transactions. Here is how we do it:
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 BLAST (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 builds it into each user's ticket history card. Note, we check if this is the BLAST or ETH lottery by using the check token == "Blast"
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: