Our testnet jackpot is not automatically run every 24h. It's easy to run 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))
A user buys a ticket worth 100 $BLAST, but after 15% fees, only 85 $BLAST is “entered” into the jackpot 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:
This is how the grace period looks in the app, otherwise, it'll show 0h 0m:
You should do the same for jackpot history as well:
Buy tickets
Before a user is buy a ticket, they need to approve spending BLAST from this contract. You need to prompt a user to do this. Here is the function to call. Set spender as this contract address 0xEa358b86AF8763cc6996Df87063BFf1144401371
Call purchaseTickets with a value that’s a multiple of ticketPrice and set your wallet address as the referrer.
This wallet will need to:
Call a function to withdraw fees
Sign up with Blast to earn Blast Points and Gold
We recommend checking if a user's account owns BLAST. Here's a simple link to send them to that prefills the token to be swapped to as BLAST.
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 jackpot, 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 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 builds it into each user's ticket history card. Note, we check if this is the BLAST or ETH jackpot 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";
const LOTTERY_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as Address;
const BLAST_LOTTERY_ADDRESS = process.env.NEXT_PUBLIC_BLAST_LOTTERY_ADDRESS as Address;
const CHAIN = process.env.NEXT_PUBLIC_CHAIN || "testnet";
const API_URL =
CHAIN == "mainnet"
? blast.blockExplorers.default.apiUrl
: blastSepolia.blockExplorers.default.apiUrl;
const ETH_USER_TICKET_PURCHASE_TOPIC =
"0xedc823804e4431d900483237575344cd2db13eaacc836fb481c86a0f51d2c182";
const BLAST_USER_TICKET_PURCHASE_TOPIC =
"0xb0cf329104ca766d6830968f6a926d915e398d06541a13106c6b372bb6834df3";
const LOTTERY_RUN_TOPIC = "0x4718ede503b98b63750fe9c8b5b8410db1b336e47ba2172e8c04abe4a6c8a1fb";
const BLASTSCAN_API_KEY = process.env.BLASTSCAN_API_KEY as string;
// fetch ticket purchases for all users
type TicketPurchaseInfo = { blockNumber: number; ticketsPurchased: number };
const fetchTicketPurchases = async (token: string) => {
const batchSize = 1000; // Maximum allowed by the API
let allEvents: any[] = [];
let page = 1;
let hasMoreEvents = true;
while (hasMoreEvents) {
const urlParams = new URLSearchParams({
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,
});
const request = `${API_URL}?${urlParams}`;
const response = await fetch(request);
const body = await response?.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 limiting
await new Promise((resolve) => setTimeout(resolve, 200));
}
} else {
hasMoreEvents = false;
}
}
const ticketPurchases = new Map<string, TicketPurchaseInfo[]>();
allEvents.forEach((event: any) => {
const userAddress = `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);
}
const blockNumber = 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 number
const getCurrentBlockNumber = async () => {
const urlParams = new URLSearchParams({
module: "block",
action: "getblocknobytime",
timestamp: Math.floor(Date.now() / 1000).toString(),
closest: "before",
apikey: BLASTSCAN_API_KEY,
});
const request = `${API_URL}?${urlParams}`;
const response = await fetch(request);
const body = await response?.json();
return parseInt(body.result);
};
// fetch lottery info (winnerAddress, winAmount, ticketsPurchasedByWinner, blockNumber, transactionHash) for all lotteries
type LotteryInfo = {
winnerAddress: string;
winAmount: number;
ticketsPurchasedByWinner: number;
blockNumber: number;
transactionHash: string;
estimatedTimestamp: number;
};
const fetchLotteryInfo = async (token: string) => {
const TICKET_PURCHASE_FEE_PERCENTAGE = 10;
const urlParams = new URLSearchParams({
module: "logs",
action: "getLogs",
address: token == "Blast" ? BLAST_LOTTERY_ADDRESS : LOTTERY_CONTRACT_ADDRESS,
topic0: LOTTERY_RUN_TOPIC,
apiKey: BLASTSCAN_API_KEY,
});
const request = `${API_URL}?${urlParams}`;
const response = await fetch(request);
const body = await response?.json();
const events = body.result;
const currentBlockNumber = await getCurrentBlockNumber();
const currentTimestamp = Math.floor(Date.now() / 1000);
const lotteryInfos: LotteryInfo[] = [];
events.forEach((event: any) => {
// field 1 [66:130]
const winner = event.data.substring(66, 130);
const winnerAddress =
parseInt(winner, 16) === 0 ? "LP_WINNER" : `0x${winner.substring(24, 64)}`;
// field 3 [194:258]
const winAmount = parseInt(event.data.substring(194, 258), 16) / 10 ** 18;
// field 4 [258:322]
const ticketsPurchasedByWinner =
parseInt(event.data.substring(258, 322), 16) /
10000 /
((100 - TICKET_PURCHASE_FEE_PERCENTAGE) / 100);
const blockNumber = parseInt(event.blockNumber, 16);
const transactionHash = event.transactionHash;
const estimatedTimestamp = currentTimestamp - (currentBlockNumber - blockNumber) * 2; // 2 seconds per block
const lotteryInfo = {
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 count
type UserLotteryInfo = {
winnerAddress: string;
winAmount: number;
ticketsPurchasedByWinner: number;
blockNumber: number;
transactionHash: string;
ticketsPurchasedByUser: number;
estimatedTimestamp: number;
};
const buildUserLotteryHistory = async (userAddress: string | undefined, token: string) => {
const lotteryInfos = await fetchLotteryInfo(token);
if (!userAddress) {
return lotteryInfos.map((info) => ({
...info,
ticketsPurchasedByUser: 0,
}));
}
const ticketPurchases = await fetchTicketPurchases(token);
const userTicketPurchases = ticketPurchases.get(userAddress.toLowerCase()) || [];
const userLotteryHistory: 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:
const filteredLotteryHistory = lotteryHistory
.filter((info) => {
const date = new Date(info.estimatedTimestamp * 1000);
return (
date.getTime() >= new Date("2024-07-07").getTime() &&
(info.winnerAddress !== "LP_WINNER" || info.ticketsPurchasedByUser > 0)
);
})
.sort((a, b) => b.estimatedTimestamp - a.estimatedTimestamp);
This is how that looks like:
Let a user claim their winnings
Have them call withdrawWinnings from their wallet. They can claim at any time.
Here is an SVG for the Blast logo on a yellow circle that you can use as a currency icon.
On Megapoints
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