As of January 2nd, 2025, Megapot will update the ETH testnet & mainnet jackpot deployments. Please be sure to update your contract usage on this date.
Blast Testnet ETH
Blast Mainnet ETH
OLD Testnet ETH
OLD Mainnet ETH
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))
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";
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 =
"d72c70202ab87b3549553b1d4ceb2a632c83cb96fa2dfe65c30282862fe11ade";
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.
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