Build with
DrawEngine.
Add provably fair, on-chain draws to your protocol in minutes. No VRF infrastructure, no ticketing logic — just one SDK and a Stellar wallet.
Overview
DrawEngine is the draw primitive powering LuckyPool's weekly jackpot. It handles VRF randomness, ticket weighting, winner selection, and on-chain recording — so your team doesn't have to build any of that from scratch.
VRF-backed randomness, verifiable by anyone on Stellar.
Equal odds, proportional tickets, tiered — configure it your way.
Winner and proof written on-chain the moment the draw fires.
Quickstart
npm install @luckypool/draw-engine
import { DrawEngine } from "@luckypool/draw-engine";
const engine = new DrawEngine({
network: "mainnet", // "mainnet" | "testnet"
contractId: "YOUR_CONTRACT", // your deployed DrawEngine contract
signer: freighterSigner, // any Stellar signer
});await engine.addEntrants([
{ address: "GABC...1234", tickets: 100 },
{ address: "GXYZ...5678", tickets: 250 },
]);const result = await engine.draw(); console.log(result.winner); // Stellar address of winner console.log(result.vrfProof); // on-chain verifiable proof console.log(result.txHash); // Stellar transaction hash
SDK Reference
new DrawEngine(config)→ DrawEngineCreates a new DrawEngine instance connected to your contract.
network"mainnet" | "testnet"Stellar network to connect to.contractIdstringAddress of your deployed DrawEngine contract.signerStellarSignerWallet signer for submitting transactions.engine.addEntrants(entrants[])→ Promise<void>Register participants for the next draw. Overwrites existing entries for the same address.
addressstringStellar address of the participant.ticketsnumberWeight / ticket count for this entrant. Must be > 0.engine.draw()→ Promise<DrawResult>Requests VRF randomness, selects a winner proportionally, and records the result on-chain.
engine.getResult(txHash)→ Promise<DrawResult>Fetches a past draw result by Stellar transaction hash.
txHashstringStellar transaction hash from a previous draw.engine.getEntrants()→ Promise<Entrant[]>Returns the current list of registered entrants and their ticket counts.
engine.clearEntrants()→ Promise<void>Removes all registered entrants. Typically called after a draw to reset for the next round.
Draw Types
DrawEngine supports any weighting model. Pass ticket counts to control odds — or give everyone equal weight for a pure raffle.
// Every entrant gets 1 ticket
{ address: "G...", tickets: 1 }// Tickets = USDC deposited
{ address: "G...", tickets: 500 }// Custom tier weights
{ address: "G...", tickets: 3 } // Gold
{ address: "G...", tickets: 1 } // Bronze// 1 ticket per NFT held
{ address: "G...", tickets: nftCount }VRF & Fairness
Every draw uses a Verifiable Random Function (VRF) seeded from the Stellar ledger hash. The VRF proof is published on-chain alongside the draw result — anyone can independently verify the winner selection was fair and not manipulated.
DrawEngine uses Stellar's native randomness beacon. No external oracle dependency, no off-chain trust assumption, no admin key that could influence the outcome.
Stellar ledger hash — deterministic and tamper-proof.
VRF proof stored with every draw. Verify anytime.
No privileged actor can influence the draw outcome.
// Independent verification — no trust required const proof = await engine.getResult(txHash); // 1. Check VRF output is authentic const valid = await oracle.verify(proof.vrfProof, proof.roundSeed, proof.vrfOutput); // 2. Check winner arithmetic const winningTicket = BigInt(proof.vrfOutput.slice(0, 8)) % proof.totalTickets; assert(winningTicket === proof.winningTicket); // 3. Check ticket maps to winner address assert(proof.ticketOwner[winningTicket] === proof.winner);
On-chain Records
Each draw writes a permanent record to the DrawEngine contract on Stellar containing the winner address, VRF proof, timestamp, and total entrant count.
const past = await engine.getResult("TX_HASH_HERE");
// past.winner — winning Stellar address
// past.vrfProof — verifiable proof bytes
// past.vrfOutput — 32-byte random seed used
// past.winningTicket — which ticket number won
// past.totalTickets — total tickets in draw
// past.timestamp — Unix timestamp of draw
// past.entrants — total number of entrants
// past.prize — USDC amount paid to winner (if LuckyPool draw)Contracts
CDRAW...TESTNETCore draw primitiveCLUCKY...TESTNETPrize-linked savingsError Codes
DrawEngine and LuckyPool contracts return typed errors. Catch them via the SDK's DrawEngineError class.
AlreadyInitializedContract has already been initialized.NotInitializedContract must be initialized before use.PausedThe pool is paused. Deposits and draws are blocked.UnauthorizedCaller does not have permission for this action.InsufficientBalanceWithdrawal amount exceeds deposited balance.InsufficientPrizePrize pool is empty — fund it before running a draw.InvalidAmountAmount must be greater than zero.NoDepositorsDraw cannot run with zero depositors.FeeTooHighProtocol fee exceeds the maximum allowed (50%).import { DrawEngineError } from "@luckypool/draw-engine";
try {
await engine.draw();
} catch (err) {
if (err instanceof DrawEngineError) {
switch (err.code) {
case 6: console.error("Prize pool is empty — fund it first"); break;
case 8: console.error("No depositors registered for this draw"); break;
default: console.error("Contract error:", err.code, err.name);
}
}
}Events
All state-changing actions emit typed events on Stellar. Subscribe to them via the SDK or query Stellar Horizon directly.
Depositeduser: Address, amount: i128, round: u64Withdrawnuser: Address, amount: i128PrizeFundedfrom: Address, amount: i128DrawCompletedround: u64, winner: Address, prize: i128// Subscribe to draw completions
engine.on("DrawCompleted", (event) => {
console.log(`Round ${event.round}: ${event.winner} won ${event.prize} USDC`);
});
// Or query past events from Horizon
const events = await engine.getEvents({
type: "DrawCompleted",
fromLedger: 50000000,
});