ISSUE-DASH-003: Automatic Lottery State Validation on Ticket Purchase
✨ Issue Request
Implement automatic lottery state validation both in the contract (BuyTicket function) and in the frontend (buy-tickets page) to detect and handle lottery closure when remaining blocks reach zero during or after a purchase.
📌 Description
Currently, when a user buys tickets, there's no automatic validation if the lottery reached its time limit (remaining blocks = 0) during the transaction. This issue requires implementing two complementary validations:
1. Contract Validation (Cairo)
Modify the BuyTicket function in the Lottery.cairo contract so that, upon successfully completing a purchase, it automatically checks if remaining blocks reached zero and, if so, changes the lottery state to inactive within the same transaction.
2. Frontend Validation (TypeScript/React)
Modify the buy-tickets page so that after a successful purchase it refreshes the lottery state and, if it detects the lottery closed, updates the UI showing an informative message and disabling purchase options.
This approach ensures the contract always maintains the correct state and the UI reflects that state in real-time.
🛠️ Steps to Reproduce (if applicable)
- Navigate to buy tickets page (
/dapp/buy-tickets)
- Wait for a lottery to be near its end (few remaining blocks)
- Buy tickets right when remaining blocks reach 0
- Observe that the lottery doesn't automatically change to "inactive" state
- The UI doesn't reflect that the lottery has closed
🖼️ Screenshots (if applicable)
Not applicable - business logic improvement.
🎯 Expected Behavior
The implementation must meet the following criteria:
Part 1: Cairo Contract Modification
Function to Modify: BuyTicket
Location: packages/snfoundry/contracts/src/Lottery.cairo
Logic to implement:
- 🔹 Upon successfully completing ticket purchase (after all records and events)
- 🔹 Call internal function
GetBlocksRemaining(drawId) to check remaining blocks
- 🔹 If
GetBlocksRemaining(drawId) == 0:
- Execute
SetDrawInactive(drawId) to close the lottery
- This changes the draw's
isActive flag to false
- 🔹 Everything must happen in the same purchase transaction
- 🔹 Should not affect normal flow if blocks remain
Contract functions to use:
GetBlocksRemaining(drawId: u64): Calculates and returns remaining blocks
- If
current_block >= endBlock: returns 0
- Otherwise: returns
endBlock - current_block
SetDrawInactive(drawId: u64): Changes draw's isActive state to false
- Validates draw exists
- Updates
draws.entry(drawId).isActive = false
- Emits
DrawClosed event
Considerations:
- Validation must be done AFTER successfully completing the purchase
- Should not revert transaction if blocks reached 0 (purchase is valid)
- Must be efficient to not significantly increase gas
- Must work for both individual and multiple purchases
Security Validations
- 🔹 Verify draw exists before modifying state
- 🔹 Ensure
SetDrawInactive only called if draw is active
- 🔹 Maintain integrity of all ticket records
- 🔹 Existing events (TicketPurchased, BulkTicketPurchase) should not be affected
Part 2: Frontend Modification
Component to Modify: BuyTickets Page
Location: packages/nextjs/app/dapp/buy-tickets/page.tsx
Logic to implement:
- 🔹 After
handleConfirmPurchase completes successfully
- 🔹 Call
refetchDrawActiveBlocks() to update state from contract
- 🔹 Check new value of
isDrawActive or isDrawActiveBlocks
- 🔹 If lottery changed to inactive (
isDrawActive === false):
- Show informative message/toast to user
- Update component visual state
- Disable purchase button
- Optionally, show "Closed" status next to countdown
Available hooks and functions:
useDrawInfo({ drawId }): Hook that provides:
isDrawActiveBlocks: Boolean of current state
refetchDrawActiveBlocks(): Function to refresh state
blocksRemaining: Updated remaining blocks
useBuyTickets({ drawId }): Hook that handles purchase, provides:
isDrawActive: Lottery state
refetchBalance(): Refresh user balance
Suggested UI flow:
// Pseudocode - DO NOT copy literally
async function handleConfirmPurchase() {
// 1. Execute purchase
const result = await buyTickets(...)
if (result) {
// 2. Refresh draw state
await refetchDrawActiveBlocks()
await refetchBalance()
// 3. Check if lottery closed
// (updated value will be available after refetch)
// Use updated state to condition UI
}
}
UI states:
- 🔹 Active lottery: Show countdown, enable purchases
- 🔹 Closed lottery:
- Show message: "The lottery has closed. Your tickets will participate in the next draw."
- Disable purchase button
- Change countdown banner color/style
- Optionally, offer button to "View next lottery"
Suggested message:
- Spanish: "¡La lotería ha cerrado! Tus tickets han sido registrados exitosamente y participarán en el sorteo. Revisa 'Reclamar' para saber si tus billetes son ganadores y retirar el premio."
- English: "The lottery has closed! Your tickets have been successfully registered and will participate in the draw. Check 'Claim' to see if your tickets are winners and withdraw your prize."
🚀 Suggested Solution / Feature Request
Solution Part 1: Cairo Contract
Modification location
Modification should be made in the BuyTicket function of the Lottery module:
// File: packages/snfoundry/contracts/src/Lottery.cairo
// Function: BuyTicket
// Pseudocode - DO NOT copy, only conceptual reference:
fn BuyTicket(ref self: ContractState, drawId: u64, numbers_array: Array<Array<u16>>, quantity: u8) {
// ... all existing purchase logic ...
// ... validations ...
// ... ticket creation ...
// ... event emission ...
// NEW LOGIC AT THE END:
// Check if blocks reached zero
// If so, close lottery automatically
}
Functions to query internally
The BuyTicket function should use these existing internal functions:
-
GetBlocksRemaining(drawId: u64) -> u64
- Returns amount of remaining blocks
- Calculates:
endBlock - currentBlock (if currentBlock < endBlock)
- Returns 0 if time expired
-
SetDrawInactive(drawId: u64)
- Changes draw state to inactive
- Updates
draws.entry(drawId).write(draw_with_inactive_status)
- Emits
DrawClosed event
Cairo implementation considerations
- Use
get_block_number() to get current block
- Comparison should be:
if blocks_remaining == 0 { ... }
- Call
SetDrawInactive only if draw is still active (to avoid redundant calls)
- Don't revert transaction, just update state
- Maintain efficiency: validation should be simple and fast
Solution Part 2: Frontend React/TypeScript
File to modify
packages/nextjs/app/dapp/buy-tickets/page.tsx
Function to modify
handleConfirmPurchase - add post-purchase verification logic
Hooks to use
Hook useDrawInfo:
const {
isDrawActiveBlocks,
refetchDrawActiveBlocks,
blocksRemaining,
} = useDrawInfo({ drawId: currentDrawId });
Hook useBuyTickets:
const {
buyTickets,
isDrawActive,
refetchBalance,
} = useBuyTickets({ drawId: currentDrawId });
Post-purchase verification logic
After successfully executing buyTickets():
- Call
refetchDrawActiveBlocks() to get updated state
- Wait for refetch to complete
- New value of
isDrawActiveBlocks will reflect if lottery closed
- Use that value to update UI
UI update
Component already has isDrawActive variable controlling several elements:
- Line 504-508: Shows message if draw is not active
- Line 566: Disables purchase button
Ensure that after refetch, these elements update automatically.
Additional message (optional)
Use react-hot-toast to show notification:
import toast from "react-hot-toast";
// If lottery closed:
toast.info("The lottery has closed! Your tickets will participate in the draw.");
Contract functions (complete reference)
GetBlocksRemaining(drawId: u64) -> u64
- Description: Calculates remaining blocks until closure
- Internal logic: Compares
get_block_number() with draw.endBlock
- Returns: Number of remaining blocks (0 if already expired)
SetDrawInactive(drawId: u64)
- Description: Marks a draw as inactive
- Effects: Changes
draw.isActive to false
- Event: Emits
DrawClosed { drawId, timestamp }
- Permissions: Currently owner only, may need adjustment to be called by BuyTicket
IsDrawActive(drawId: u64) -> bool
- Description: Verifies if a draw is active
- Returns:
true if active, false if closed
- Used by: Frontend to validate state
📌 Additional Notes
Contract Notes
- The
SetDrawInactive function may currently have only_owner restriction. If so, logic must be adjusted to allow internal calls from BuyTicket
- Alternatively, duplicate closure logic within
BuyTicket without calling SetDrawInactive
- Consider additional gas cost of this validation (should be minimal: one read + one conditional write)
- Emitting
DrawClosed event is important for traceability
Frontend Notes
- Scaffold-Stark hooks automatically handle cache and revalidation
refetch may take a few milliseconds, consider showing brief loading
- Message should be informative but not alarming (user completed their purchase successfully)
- Consider adding link/button to go to dashboard or view upcoming lotteries
Testing
- Contract: Create test simulating purchase when
blocksRemaining == 0
- Frontend: Test flow with lottery about to close
- Verify
DrawClosed event emits correctly
- Confirm UI updates without needing manual refresh
⚠️ Before Applying
Please read this guide: Contributor Guidelines
ISSUE-DASH-003: Automatic Lottery State Validation on Ticket Purchase
✨ Issue Request
Implement automatic lottery state validation both in the contract (
BuyTicketfunction) and in the frontend (buy-tickets page) to detect and handle lottery closure when remaining blocks reach zero during or after a purchase.📌 Description
Currently, when a user buys tickets, there's no automatic validation if the lottery reached its time limit (remaining blocks = 0) during the transaction. This issue requires implementing two complementary validations:
1. Contract Validation (Cairo)
Modify the
BuyTicketfunction in theLottery.cairocontract so that, upon successfully completing a purchase, it automatically checks if remaining blocks reached zero and, if so, changes the lottery state to inactive within the same transaction.2. Frontend Validation (TypeScript/React)
Modify the
buy-ticketspage so that after a successful purchase it refreshes the lottery state and, if it detects the lottery closed, updates the UI showing an informative message and disabling purchase options.This approach ensures the contract always maintains the correct state and the UI reflects that state in real-time.
🛠️ Steps to Reproduce (if applicable)
/dapp/buy-tickets)🖼️ Screenshots (if applicable)
Not applicable - business logic improvement.
🎯 Expected Behavior
The implementation must meet the following criteria:
Part 1: Cairo Contract Modification
Function to Modify:
BuyTicketLocation:
packages/snfoundry/contracts/src/Lottery.cairoLogic to implement:
GetBlocksRemaining(drawId)to check remaining blocksGetBlocksRemaining(drawId) == 0:SetDrawInactive(drawId)to close the lotteryisActiveflag tofalseContract functions to use:
GetBlocksRemaining(drawId: u64): Calculates and returns remaining blockscurrent_block >= endBlock: returns 0endBlock - current_blockSetDrawInactive(drawId: u64): Changes draw'sisActivestate tofalsedraws.entry(drawId).isActive = falseDrawClosedeventConsiderations:
Security Validations
SetDrawInactiveonly called if draw is activePart 2: Frontend Modification
Component to Modify: BuyTickets Page
Location:
packages/nextjs/app/dapp/buy-tickets/page.tsxLogic to implement:
handleConfirmPurchasecompletes successfullyrefetchDrawActiveBlocks()to update state from contractisDrawActiveorisDrawActiveBlocksisDrawActive === false):Available hooks and functions:
useDrawInfo({ drawId }): Hook that provides:isDrawActiveBlocks: Boolean of current staterefetchDrawActiveBlocks(): Function to refresh stateblocksRemaining: Updated remaining blocksuseBuyTickets({ drawId }): Hook that handles purchase, provides:isDrawActive: Lottery staterefetchBalance(): Refresh user balanceSuggested UI flow:
UI states:
Suggested message:
🚀 Suggested Solution / Feature Request
Solution Part 1: Cairo Contract
Modification location
Modification should be made in the
BuyTicketfunction of theLotterymodule:Functions to query internally
The
BuyTicketfunction should use these existing internal functions:GetBlocksRemaining(drawId: u64) -> u64endBlock - currentBlock(if currentBlock < endBlock)SetDrawInactive(drawId: u64)draws.entry(drawId).write(draw_with_inactive_status)DrawClosedeventCairo implementation considerations
get_block_number()to get current blockif blocks_remaining == 0 { ... }SetDrawInactiveonly if draw is still active (to avoid redundant calls)Solution Part 2: Frontend React/TypeScript
File to modify
packages/nextjs/app/dapp/buy-tickets/page.tsxFunction to modify
handleConfirmPurchase- add post-purchase verification logicHooks to use
Hook
useDrawInfo:Hook
useBuyTickets:Post-purchase verification logic
After successfully executing
buyTickets():refetchDrawActiveBlocks()to get updated stateisDrawActiveBlockswill reflect if lottery closedUI update
Component already has
isDrawActivevariable controlling several elements:Ensure that after refetch, these elements update automatically.
Additional message (optional)
Use
react-hot-toastto show notification:Contract functions (complete reference)
GetBlocksRemaining(drawId: u64) -> u64get_block_number()withdraw.endBlockSetDrawInactive(drawId: u64)draw.isActivetofalseDrawClosed { drawId, timestamp }IsDrawActive(drawId: u64) -> booltrueif active,falseif closed📌 Additional Notes
Contract Notes
SetDrawInactivefunction may currently haveonly_ownerrestriction. If so, logic must be adjusted to allow internal calls fromBuyTicketBuyTicketwithout callingSetDrawInactiveDrawClosedevent is important for traceabilityFrontend Notes
refetchmay take a few milliseconds, consider showing brief loadingTesting
blocksRemaining == 0DrawClosedevent emits correctlyPlease read this guide: Contributor Guidelines