Our testnet jackpot is not automatically run every 24h - ping us to run it for you. To do it yourself:
Fetch the jackpot 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 jackpot 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 jackpot, 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 jackpot 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 jackpot 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="d72c70202ab87b3549553b1d4ceb2a632c83cb96fa2dfe65c30282862fe11ade";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 lotteriestypeLotteryInfo= { 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 jackpot wins and only show jackpot results when a user actually entered. This filters out jackpot history that's irrelevant for a user and demoralizing since LPs win most of the time:
Your users won't see how many Megapoints they've earned so far. Megapoints are how we distribute Blast Points and Gold at the end of the month. We calculate this with an onchain snapshot on the 28th of every month, so you and your users will get your rightful Blast Points and Gold allocation.
Your users earn 50% more Megapoints than if they were on megapot.io, since the address you pass in as the referrer for purchaseTickets is an ambassador code!
Your users are eligible for Streak bonuses - all of this is calculated onchain