diff --git a/src/common/blockchainWrapper.js b/src/common/blockchainWrapper.js index 515f304..c8b1649 100644 --- a/src/common/blockchainWrapper.js +++ b/src/common/blockchainWrapper.js @@ -133,7 +133,9 @@ async function getTokenSymbol() { const rouletteContractEvents = new ethers.Contract( ROULETTE_CONTRACT_ADDRESS, [ - 'event ExecutedWager(address indexed, uint256)', + 'event ExecutedWager(address indexed, uint256, uint256, uint256)', + 'event BetPlaced(address indexed, string, uint256)', + 'event BetCleared(address indexed)', ], provider ); diff --git a/src/components/roulette/ClearBetsButton.js b/src/components/roulette/ClearBetsButton.js new file mode 100644 index 0000000..a0cd228 --- /dev/null +++ b/src/components/roulette/ClearBetsButton.js @@ -0,0 +1,29 @@ +const CLASS_NAME = "ClearBetsButton-component"; + +export function ClearBetsButton(props) { + const hasBets = props.pendingBets && props.pendingBets.length > 0; + const isDisabled = props.wheelIsSpinning || !hasBets; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/roulette/MostRecentSpinResults.js b/src/components/roulette/MostRecentSpinResults.js index b037a6e..7600aad 100644 --- a/src/components/roulette/MostRecentSpinResults.js +++ b/src/components/roulette/MostRecentSpinResults.js @@ -8,17 +8,24 @@ const CLASS_NAME = "MostRecentSpinResults-component"; export function MostRecentSpinResults(props) { const [spinResults, setSpinResults] = useState([]); - rouletteContractEvents.on('ExecutedWager', (playerAddress, wheelNumber) => { - if (playerAddress === props.playerAddress) { - const copySpinResults = [...spinResults]; - copySpinResults.push(parseInt(wheelNumber, 10)); - setSpinResults(copySpinResults.slice(-20)); // Only keep the last 20 results - } - }); - useEffect(() => { - // render - }, [spinResults, props.playerAddress]); + const handleExecutedWager = (playerAddress, wheelNumber, totalWinnings, totalBetsReturned) => { + if (playerAddress === props.playerAddress) { + setSpinResults(prevResults => { + const copySpinResults = [...prevResults]; + copySpinResults.push(parseInt(wheelNumber, 10)); + return copySpinResults.slice(-20); // Only keep the last 20 results + }); + } + }; + + rouletteContractEvents.on('ExecutedWager', handleExecutedWager); + + // Cleanup event listener + return () => { + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + }; + }, [props.playerAddress]); return (
{ - setTimeout(async () => { - const currentNumbers = await getPlayerNumberCompletionSetCurrent(props.playerAddress); - setCurrentSet(new Set(currentNumbers)); - }, 1000); - }, [props.playerAddress, currentSet]); + const initializeSet = async () => { + try { + const currentNumbers = await getPlayerNumberCompletionSetCurrent(props.playerAddress); + setCurrentSet(new Set(currentNumbers)); + } catch (error) { + console.error('Error initializing numbers hit tracker:', error); + } + }; + + initializeSet(); + }, [props.playerAddress]); + + // Listen for ExecutedWager events to update the set immediately + useEffect(() => { + const handleExecutedWager = async (playerAddress, wheelNumber, totalWinnings, totalBetsReturned) => { + if (playerAddress === props.playerAddress) { + try { + // Get the updated completion set from the blockchain + const currentNumbers = await getPlayerNumberCompletionSetCurrent(props.playerAddress); + setCurrentSet(new Set(currentNumbers)); + } catch (error) { + console.error('Error updating numbers hit tracker:', error); + } + } + }; + + rouletteContractEvents.on('ExecutedWager', handleExecutedWager); + + // Cleanup event listener + return () => { + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + }; + }, [props.playerAddress]); return (
{ + const handleBetPlaced = (playerAddress, betName, betAmount) => { + if (playerAddress === props.playerAddress) { + // Refresh pending bets when a bet is placed + refreshPendingBets(); + refreshBalances(); + } + }; + + const handleBetCleared = (playerAddress) => { + if (playerAddress === props.playerAddress) { + // Refresh pending bets when bets are cleared + refreshPendingBets(); + refreshBalances(); + } + }; + + const handleExecutedWager = (playerAddress, wheelNumber, totalWinnings, totalBetsReturned) => { + if (playerAddress === props.playerAddress) { + // Convert wheel number to proper format for getWinningCriteria + let parsedWheelNumber; + const numericWheelNumber = parseInt(wheelNumber, 10); + if (numericWheelNumber === 37) { + // "00" is represented as 37 in the contract + parsedWheelNumber = "00"; + } else { + // Convert to string for other numbers + parsedWheelNumber = numericWheelNumber.toString(); + } + + // Calculate and store previous round results for display + const previousRoundResults = getCompleteResultsOfRound( + playerBalance || 0, + pendingBets, + parsedWheelNumber + ); + setPreviousRoundResultsForBetResultsInfo(previousRoundResults); + + // Refresh all data when wager is executed + refreshBalances(); + refreshPendingBets(); + } + }; + + // Set up event listeners + rouletteContractEvents.on('BetPlaced', handleBetPlaced); + rouletteContractEvents.on('BetCleared', handleBetCleared); + rouletteContractEvents.on('ExecutedWager', handleExecutedWager); + + // Cleanup event listeners + return () => { + rouletteContractEvents.off('BetPlaced', handleBetPlaced); + rouletteContractEvents.off('BetCleared', handleBetCleared); + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + }; + }, [props.playerAddress, refreshBalances, refreshPendingBets, playerBalance, pendingBets]); + function handleBettingSquareClick(bettingSquareName) { const availableBalance = (playerBalance !== undefined ? parseFloat(playerBalance) : 0) - calculateTotalBetAmount(pendingBets); @@ -157,39 +216,70 @@ export function Roulette(props) { setWheelIsSpinning(true); - getRandomWheelNumber(`${Date.now()}${playerAddress}`) - .then(randomWheelNumber => { - const resultsOfRound = getCompleteResultsOfRound(playerBalance, pendingBets, randomWheelNumber); - - setPreviousRoundResultsForBetResultsInfo(resultsOfRound); - - executeWager(playerAddress) - .then((response) => response.wait()) // wait for mining first - .then((receipt) => { - // Use mined block number - setLatestBlockNumber(receipt.blockNumber); - // Refresh balances after transaction is mined - refreshBalances(); - refreshPendingBets(); - }) - .then(() => { - rouletteContractEvents.on('ExecutedWager', (playerAddr, wheelNum, totalWinnings, totalBetsReturned) => { - if (playerAddr === props.playerAddress) { - setWheelNumber(parseInt(wheelNum, 10)); - setWheelIsSpinning(false); - } - }); - }) - .catch((error) => { - console.error('Error executing wager:', error); - setWheelIsSpinning(false); - alert('Failed to execute wager. Please try again.'); - }); + // Set up event listener BEFORE starting the transaction + const handleExecutedWager = (playerAddr, wheelNum, totalWinnings, totalBetsReturned) => { + if (playerAddr === props.playerAddress) { + // Keep wheel number as number for consistency with other components + const numericWheelNumber = parseInt(wheelNum, 10); + setWheelNumber(numericWheelNumber); + setWheelIsSpinning(false); + // Remove the listener after handling the event + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + } + }; + + rouletteContractEvents.on('ExecutedWager', handleExecutedWager); + + // Execute the wager transaction + executeWager(playerAddress) + .then((response) => response.wait()) // wait for mining + .then((receipt) => { + // Update block number and refresh data + setLatestBlockNumber(receipt.blockNumber); + refreshBalances(); + refreshPendingBets(); }) .catch((error) => { - console.error('Error getting random wheel number:', error); + console.error('Error executing wager:', error); setWheelIsSpinning(false); - alert('Failed to get random number. Please try again.'); + alert('Failed to execute wager. Please try again.'); + // Remove the listener on error + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + }); + } + + function handleClearBetsClick() { + if (pendingBets.length === 0) { + alert("No bets to clear!"); + return; + } + + if (wheelIsSpinning) { + alert("Cannot clear bets while wheel is spinning!"); + return; + } + + // Optimistically clear UI + setPendingBets([]); + + // Clear bets on the blockchain + clearBets() + .then((tx) => { + console.log('Bets cleared:', tx); + return tx.wait(); // wait for mining to ensure block number is available + }) + .then((receipt) => { + // Update latest block number from mined receipt + setLatestBlockNumber(receipt.blockNumber); + // Refresh balances after transaction is mined + refreshBalances(); + refreshPendingBets(); + }) + .catch((error) => { + console.error('Error clearing bets:', error); + alert('Failed to clear bets. Please try again.'); + // Restore pending bets on error + refreshPendingBets(); }); } @@ -210,6 +300,11 @@ export function Roulette(props) { hasABetBeenPlaced={hasABetBeenPlaced(pendingBets)} wheelIsSpinning={wheelIsSpinning} /> + handleClearBetsClick()} + pendingBets={pendingBets} + wheelIsSpinning={wheelIsSpinning} + /> { - if (playerAddress === props.playerAddress) { - setBgColor(getWheelNumberColor(parseInt(wheelNumber, 10))); - setMostRecentSpinResultText(parseInt(wheelNumber, 10) === 37 ? "00" : parseInt(wheelNumber, 10)); - } - }); } - }, [mostRecentSpinResultText, props.spinResult, props.playerAddress]); + }, [props.spinResult]); + + useEffect(() => { + const handleExecutedWager = (playerAddress, wheelNumber, totalWinnings, totalBetsReturned) => { + if (playerAddress === props.playerAddress) { + const wheelNum = parseInt(wheelNumber, 10); + setBgColor(getWheelNumberColor(wheelNum)); + setMostRecentSpinResultText(wheelNum === 37 ? "00" : wheelNum); + } + }; + + rouletteContractEvents.on('ExecutedWager', handleExecutedWager); + + // Cleanup event listener + return () => { + rouletteContractEvents.off('ExecutedWager', handleExecutedWager); + }; + }, [props.playerAddress]); return (
+
+ +
diff --git a/src/test/components/roulette/ClearBetsButton.test.js b/src/test/components/roulette/ClearBetsButton.test.js new file mode 100644 index 0000000..98a834a --- /dev/null +++ b/src/test/components/roulette/ClearBetsButton.test.js @@ -0,0 +1,45 @@ +import renderer from 'react-test-renderer'; + +import { ClearBetsButton } from '../../../components/roulette/ClearBetsButton'; +import { PendingBet } from '../../../common/PendingBet'; + +describe('ClearBetsButton', () => { + it('renders enabled when there are bets and wheel is not spinning', () => { + const sut = + {}} + pendingBets={[new PendingBet("test", 10)]} + wheelIsSpinning={false} + />; + + const view = renderer.create(sut); + + expect(view).toMatchSnapshot(); + }); + + it('renders disabled when there are no bets', () => { + const sut = + {}} + pendingBets={[]} + wheelIsSpinning={false} + />; + + const view = renderer.create(sut); + + expect(view).toMatchSnapshot(); + }); + + it('renders disabled when wheel is spinning', () => { + const sut = + {}} + pendingBets={[new PendingBet("test", 10)]} + wheelIsSpinning={true} + />; + + const view = renderer.create(sut); + + expect(view).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/src/test/components/roulette/__snapshots__/ClearBetsButton.test.js.snap b/src/test/components/roulette/__snapshots__/ClearBetsButton.test.js.snap new file mode 100644 index 0000000..2831174 --- /dev/null +++ b/src/test/components/roulette/__snapshots__/ClearBetsButton.test.js.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClearBetsButton renders disabled when there are no bets 1`] = ` +
+ +
+`; + +exports[`ClearBetsButton renders disabled when wheel is spinning 1`] = ` +
+ +
+`; + +exports[`ClearBetsButton renders enabled when there are bets and wheel is not spinning 1`] = ` +
+ +
+`; diff --git a/src/test/components/roulette/__snapshots__/Roulette.test.js.snap b/src/test/components/roulette/__snapshots__/Roulette.test.js.snap index 1c9c102..f510ede 100644 --- a/src/test/components/roulette/__snapshots__/Roulette.test.js.snap +++ b/src/test/components/roulette/__snapshots__/Roulette.test.js.snap @@ -2138,6 +2138,30 @@ exports[`Roulette renders 1`] = ` -
+
+ +