Skip to content

ISSUE-DASH-003: Automatic Lottery State Validation on Ticket Purchase #553

Description

@davidmelendez

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)

  1. Navigate to buy tickets page (/dapp/buy-tickets)
  2. Wait for a lottery to be near its end (few remaining blocks)
  3. Buy tickets right when remaining blocks reach 0
  4. Observe that the lottery doesn't automatically change to "inactive" state
  5. 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:

  1. 🔹 Upon successfully completing ticket purchase (after all records and events)
  2. 🔹 Call internal function GetBlocksRemaining(drawId) to check remaining blocks
  3. 🔹 If GetBlocksRemaining(drawId) == 0:
    • Execute SetDrawInactive(drawId) to close the lottery
    • This changes the draw's isActive flag to false
  4. 🔹 Everything must happen in the same purchase transaction
  5. 🔹 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:

  1. 🔹 After handleConfirmPurchase completes successfully
  2. 🔹 Call refetchDrawActiveBlocks() to update state from contract
  3. 🔹 Check new value of isDrawActive or isDrawActiveBlocks
  4. 🔹 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:

  1. GetBlocksRemaining(drawId: u64) -> u64

    • Returns amount of remaining blocks
    • Calculates: endBlock - currentBlock (if currentBlock < endBlock)
    • Returns 0 if time expired
  2. 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():

  1. Call refetchDrawActiveBlocks() to get updated state
  2. Wait for refetch to complete
  3. New value of isDrawActiveBlocks will reflect if lottery closed
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions