diff --git a/apps/multiplayer_demo/src/App.tsx b/apps/multiplayer_demo/src/App.tsx
index 55d6bd9..312fe03 100644
--- a/apps/multiplayer_demo/src/App.tsx
+++ b/apps/multiplayer_demo/src/App.tsx
@@ -1,3 +1,4 @@
+// App.tsx
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
import { PublicKey, Connection } from '@solana/web3.js'
@@ -25,6 +26,7 @@ const App = () => {
const [maxPlayers, setMaxPlayers] = useState('')
const [tokenMint, setTokenMint] = useState('So11111111111111111111111111111111111111112')
const [currentBlockchainTime, setCurrentBlockchainTime] = useState(null)
+ const [fetchTime, setFetchTime] = useState(null) // Record the local time when fetching data
const [gameDuration, setGameDuration] = useState('')
const [wagerAmount, setWagerAmount] = useState('')
const [winners, setWinners] = useState('')
@@ -38,13 +40,15 @@ const App = () => {
return new MultiplayerProvider(connection, wallet)
}, [connection, wallet])
- // Define fetchGames using useCallback to prevent unnecessary re-renders
+ // Fetch games and blockchain time
const fetchGames = useCallback(async () => {
if (!provider) return
try {
const gameAccounts = await provider.fetchGames()
const currentTimestamp = await provider.getCurrentBlockchainTime()
+ const fetchTime = Date.now() // Local time when data was fetched
setCurrentBlockchainTime(currentTimestamp)
+ setFetchTime(fetchTime)
setGames(gameAccounts)
} catch (error) {
console.error('Error fetching games:', error)
@@ -54,27 +58,27 @@ const App = () => {
useEffect(() => {
if (!wallet.connected || !provider) return
- // Fetch games initially
+ // Initial fetch
fetchGames()
// Subscribe to program account changes
const subscriptionId = connection.onProgramAccountChange(
new PublicKey(MULTIPLAYER_PROGRAM_ID),
- (info) => {
- // Whenever an account owned by the program changes, fetch updated games
+ () => {
+ // Fetch updated games when any program account changes
fetchGames()
},
'confirmed',
)
- // Cleanup subscription on unmount or dependency change
+ // Cleanup subscription on unmount
return () => {
connection.removeProgramAccountChangeListener(subscriptionId)
- };
+ }
}, [wallet.connected, provider, connection, fetchGames])
const gambaConfig = async () => {
- if (!provider) return;
+ if (!provider) return
try {
const gambaFeeAddress = new PublicKey('BoDeHdqeVd2ds6keWYp2r63hwpL4UfjvNEPCyvVz38mQ')
const gambaFeeBps = new BN(100) // 1%
@@ -93,7 +97,7 @@ const App = () => {
} catch (error) {
console.error('Error configuring gamba:', error)
}
- };
+ }
const createGame = async () => {
if (!provider) return
@@ -105,15 +109,15 @@ const App = () => {
gameType === WagerType.SameWager ? 0 : 1,
parseInt(wagerAmount),
parseInt(gameDuration),
- 600, // optional hard settle, if not defined settles in 24 hours
+ 600, // Optional hard settle duration (in seconds)
)
- const txId = await sendTransaction(provider.anchorProvider, instruction, undefined, 5000);
+ const txId = await sendTransaction(provider.anchorProvider, instruction, undefined, 5000)
console.log('Transaction ID:', txId)
} catch (error) {
console.error('Error creating game:', error)
}
- };
+ }
const joinGame = async (game, creatorAddressPubKey, creatorFee) => {
if (!provider) return
@@ -147,7 +151,7 @@ const App = () => {
} catch (error) {
console.error('Error settling game:', error)
}
- };
+ }
return (
@@ -218,15 +222,13 @@ const App = () => {
-
- {/* Removed the Refresh Games button since updates are handled via subscriptions */}
-
{games.map((game, index) => (
{
/>
))}
- {wallet.connected && wallet.publicKey && (
-
- )}
-
+ {wallet.connected && wallet.publicKey && }
- );
-};
+ )
+}
-export default App;
+export default App
diff --git a/apps/multiplayer_demo/src/components/GameCard.tsx b/apps/multiplayer_demo/src/components/GameCard.tsx
index 8cb2a40..6f204cc 100644
--- a/apps/multiplayer_demo/src/components/GameCard.tsx
+++ b/apps/multiplayer_demo/src/components/GameCard.tsx
@@ -1,29 +1,54 @@
-import React from 'react'
+// GameCard.tsx
+import React, { useState, useEffect } from 'react'
import { PublicKey } from '@solana/web3.js'
import { BN } from '@coral-xyz/anchor'
import { formatPublicKey, parseGameState, parseWagerType } from '../utils'
interface GameCardProps {
- game: any;
- currentBlockchainTime: number | null;
- joinGame: (game: any, creatorAddressPubKey: PublicKey, creatorFee: number) => void;
- leaveGame: (game: any) => void;
- settleGame: (game: any) => void;
- customWager: string;
- setCustomWager: React.Dispatch>;
+ game: any
+ currentBlockchainTime: number | null
+ fetchTime: number | null
+ joinGame: (game: any, creatorAddressPubKey: PublicKey, creatorFee: number) => void
+ leaveGame: (game: any) => void
+ settleGame: (game: any) => void
+ customWager: string
+ setCustomWager: React.Dispatch>
}
const GameCard: React.FC = ({
game,
currentBlockchainTime,
+ fetchTime,
joinGame,
leaveGame,
settleGame,
customWager,
setCustomWager,
}) => {
- const expirationTimestamp = new BN(game.account.softExpirationTimestamp).toNumber()
- const timeUntilExpiration = currentBlockchainTime ? Math.max(0, expirationTimestamp - currentBlockchainTime) : null
+ const [timeUntilExpiration, setTimeUntilExpiration] = useState(null)
+
+ useEffect(() => {
+ if (currentBlockchainTime !== null && fetchTime !== null) {
+ const expirationTimestamp = new BN(game.account.softExpirationTimestamp).toNumber()
+ const initialElapsedTime = (Date.now() - fetchTime) / 1000 // Time elapsed since data fetch in seconds
+ const initialTimeUntilExpiration = Math.max(
+ 0,
+ expirationTimestamp - (currentBlockchainTime + initialElapsedTime),
+ )
+ setTimeUntilExpiration(initialTimeUntilExpiration)
+
+ const interval = setInterval(() => {
+ setTimeUntilExpiration((prev) => {
+ if (prev !== null) {
+ return Math.max(0, prev - 1)
+ }
+ return null
+ })
+ }, 1000)
+
+ return () => clearInterval(interval)
+ }
+ }, [currentBlockchainTime, fetchTime, game.account.softExpirationTimestamp])
return (
@@ -35,7 +60,10 @@ const GameCard: React.FC
= ({
Winners: {game.account.winners.toString()}
Game ID: {game.account.gameId.toString()}
Game Expiration Timestamp: {game.account.softExpirationTimestamp.toString()}
- Time Until Expiration: {timeUntilExpiration !== null ? `${timeUntilExpiration} seconds` : 'Loading...'}
+
+ Time Until Expiration:{' '}
+ {timeUntilExpiration !== null ? `${Math.floor(timeUntilExpiration)} seconds` : 'Loading...'}
+
Wager Type: {parseWagerType(game.account.wagerType)}
Wager: {game.account.wager.toString()}
@@ -52,18 +80,30 @@ const GameCard: React.FC = ({
-
+
setCustomWager(e.target.value)}
+ onChange={(e) => setCustomWager(e.target.value)}
className="input-field"
/>
{(parseGameState(game.account.state) === 'Playing' || timeUntilExpiration === 0) && (
-
+
)}
diff --git a/apps/multiplayer_demo/src/components/RecentEvents.tsx b/apps/multiplayer_demo/src/components/RecentEvents.tsx
deleted file mode 100644
index 35c1c0a..0000000
--- a/apps/multiplayer_demo/src/components/RecentEvents.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import { fetchMultiplayerTransactions } from 'gamba-multiplayer-core/src/events'
-import { PublicKey, Connection } from '@solana/web3.js'
-
-const RecentEvents = ({ connection, address }) => {
- const [recentEvents, setRecentEvents] = useState([])
-
- const fetchRecentEvents = async () => {
- try {
- console.log('Address:', address) // Debugging log
-
- if (!address) {
- console.error('Address is null or undefined')
- return
- }
-
- const options = {
- limit: 10, // Adjust as needed
- }
-
- const events = await fetchMultiplayerTransactions(connection, address, options)
- console.log('Recent Events:', events)
- setRecentEvents(events)
- } catch (error) {
- console.error('Error fetching recent events:', error)
- }
- }
-
- useEffect(() => {
- const fetchInterval = 10000 // Fetch every 10 seconds
-
- const intervalId = setInterval(() => {
- fetchRecentEvents()
- }, fetchInterval)
-
- return () => clearInterval(intervalId)
- }, [address])
-
- return (
-
-
Recent Events
- {recentEvents.map((event, index) => (
-
-
Event: {event.name}
-
Time: {new Date(event.time).toLocaleString()}
-
Signature: {event.signature}
-
Data:
-
- {Object.entries(event.data).map(([key, value]) => (
-
- {key}: {typeof value === 'object' ? JSON.stringify(value) : value}
-
- ))}
-
-
- ))}
-
- )
-}
-
-export default RecentEvents
diff --git a/apps/multiplayer_demo/src/components/RecentMultiplayerEvents.tsx b/apps/multiplayer_demo/src/components/RecentMultiplayerEvents.tsx
index f221afe..e63b627 100644
--- a/apps/multiplayer_demo/src/components/RecentMultiplayerEvents.tsx
+++ b/apps/multiplayer_demo/src/components/RecentMultiplayerEvents.tsx
@@ -8,7 +8,7 @@ const RecentMultiplayerEvents = () => {
const [recentEvents, setRecentEvents] = useState([]);
useEffect(() => {
- const setupEventListener = async () => {
+ const setupEventListener = () => {
const programId = MULTIPLAYER_PROGRAM_ID;
// Subscribe to program logs
@@ -16,16 +16,16 @@ const RecentMultiplayerEvents = () => {
programId,
(logs, context) => {
const events = parseTransactionEvents(logs.logs);
- const newEvents = events.map(event => ({
+ const newEvents = events.map((event) => ({
...event,
signature: context.signature,
slot: context.slot,
time: Date.now(),
}));
- setRecentEvents(prevEvents => [...newEvents, ...prevEvents]);
+ setRecentEvents((prevEvents) => [...newEvents, ...prevEvents]);
},
- 'confirmed'
+ 'confirmed',
);
return () => {
@@ -33,7 +33,8 @@ const RecentMultiplayerEvents = () => {
};
};
- setupEventListener();
+ const cleanup = setupEventListener();
+ return cleanup;
}, [connection]);
return (
@@ -61,7 +62,8 @@ const RecentMultiplayerEvents = () => {
{Object.entries(event.data).map(([key, value]) => (
- {key}: {typeof value === 'object' ? JSON.stringify(value) : value}
+ {key}:{' '}
+ {typeof value === 'object' ? JSON.stringify(value) : value}
))}
diff --git a/apps/multiplayer_demo/src/hooks/useGambaEvents.ts b/apps/multiplayer_demo/src/hooks/useGambaEvents.ts
index 65bc79e..727da0b 100644
--- a/apps/multiplayer_demo/src/hooks/useGambaEvents.ts
+++ b/apps/multiplayer_demo/src/hooks/useGambaEvents.ts
@@ -1,7 +1,8 @@
+// usegambeevents.ts
import { useEffect, useState } from 'react'
import { useConnection } from '@solana/wallet-adapter-react'
+import { parseTransactionEvents, convertEventData } from 'gamba-multiplayer-core'
import { PublicKey } from '@solana/web3.js'
-import { MultiplayerEventType, fetchMultiplayerTransactions } from 'gamba-multiplayer-core'
export function useMultiplayerEventListener(eventType, { address, signatureLimit = 10 }) {
const { connection } = useConnection()
@@ -9,33 +10,33 @@ export function useMultiplayerEventListener(eventType, { address, signatureLimit
useEffect(() => {
// Function to listen to new events
- const subscribeToEvents = async () => {
- const subscription = connection.onLogs(address, (logs, context) => {
- const parsedEvents = parseTransactionEvents(logs.logs)
- const relevantEvents = parsedEvents.filter(event => event.name === eventType)
-
- // Mapping to more friendly data structure
- const newEvents = relevantEvents.map(event => ({
- signature: context.signature,
- slot: context.slot,
- time: new Date().getTime(), // Approximate time, better to use block time for accuracy
- data: event.data,
- }))
-
- setEvents(prevEvents => [...newEvents, ...prevEvents])
- }, 'confirmed')
+ const subscribeToEvents = () => {
+ const subscription = connection.onLogs(
+ address,
+ (logs, context) => {
+ const parsedEvents = parseTransactionEvents(logs.logs)
+ const relevantEvents = parsedEvents.filter((event) => event.name === eventType)
+
+ // Mapping to more friendly data structure
+ const newEvents = relevantEvents.map((event) => ({
+ signature: context.signature,
+ slot: context.slot,
+ time: Date.now(), // Approximate time
+ data: event.data,
+ }))
+
+ setEvents((prevEvents) => [...newEvents, ...prevEvents])
+ },
+ 'confirmed',
+ )
return () => {
connection.removeOnLogsListener(subscription)
}
}
- subscribeToEvents()
-
- return () => {
- // Clean up subscription
- connection.removeOnLogsListener(subscribeToEvents)
- }
+ const cleanup = subscribeToEvents()
+ return cleanup
}, [connection, address, eventType])
return events
diff --git a/packages/multiplayer-core/src/MultiplayerProvider.ts b/packages/multiplayer-core/src/MultiplayerProvider.ts
index 82ae096..fb7381f 100644
--- a/packages/multiplayer-core/src/MultiplayerProvider.ts
+++ b/packages/multiplayer-core/src/MultiplayerProvider.ts
@@ -112,7 +112,7 @@ export class MultiplayerProvider {
async joinGame(
- game: any,
+ game: anchor.ProgramAccount,
creatorAddressPubKey: PublicKey,
creatorFee: number,
wager: number,
@@ -157,7 +157,7 @@ export class MultiplayerProvider {
}
- async leaveGame(game: any) {
+ async leaveGame(game: anchor.ProgramAccount) {
const playerPubKey = this.wallet.publicKey
// If mint is wrapped SOL, set playerAta to null
@@ -186,7 +186,7 @@ export class MultiplayerProvider {
.instruction()
}
- async settleGame(game: any) {
+ async settleGame(game: anchor.ProgramAccount) {
if (game.account.mint.equals(WRAPPED_SOL_MINT)) {
return this.settleGameNative(game)
} else {
@@ -194,7 +194,7 @@ export class MultiplayerProvider {
}
}
- async settleGameSpl(game: any) {
+ async settleGameSpl(game: anchor.ProgramAccount) {
const gambaState = await getGambaStateAddress()
// Fetch gambaConfig to get the gambaFeeAddress
@@ -256,7 +256,7 @@ export class MultiplayerProvider {
// New function: settleWithUninitializedAccounts
- async settleWithUninitializedAccounts(game: any) {
+ async settleWithUninitializedAccounts(game: anchor.ProgramAccount) {
const gambaState = await getGambaStateAddress()
// Fetch gambaConfig to get addresses
@@ -336,7 +336,7 @@ export class MultiplayerProvider {
.instruction()
}
- async settleGameNative(game: any) {
+ async settleGameNative(game: anchor.ProgramAccount) {
const gambaState = await getGambaStateAddress()
// Fetch gambaConfig to get the gambaFeeAddress
diff --git a/packages/multiplayer-core/src/events.ts b/packages/multiplayer-core/src/events.ts
index 2a5e757..21e247a 100644
--- a/packages/multiplayer-core/src/events.ts
+++ b/packages/multiplayer-core/src/events.ts
@@ -1,49 +1,71 @@
-import { BorshCoder, EventParser, BN } from '@coral-xyz/anchor'
-import { Connection, ParsedTransactionWithMeta, PublicKey, SignaturesForAddressOptions } from '@solana/web3.js'
-import { AnyMultiplayerEvent, MultiplayerEvent, MultiplayerEventType, IDL, MULTIPLAYER_PROGRAM_ID } from '.'
+// events.ts
+import { BorshCoder, EventParser, BN } from '@coral-xyz/anchor';
+import { Connection, ParsedTransactionWithMeta, PublicKey, SignaturesForAddressOptions } from '@solana/web3.js';
+import { AnyMultiplayerEvent, MultiplayerEvent, MultiplayerEventType, IDL, MULTIPLAYER_PROGRAM_ID } from '.';
export type MultiplayerTransaction = {
- signature: string
- time: number
- name: Event
- data: MultiplayerEvent['data']
-}
+ signature: string;
+ time: number;
+ name: Event;
+ data: MultiplayerEvent['data'];
+};
-const eventParser = new EventParser(MULTIPLAYER_PROGRAM_ID, new BorshCoder(IDL))
+const eventParser = new EventParser(MULTIPLAYER_PROGRAM_ID, new BorshCoder(IDL));
/**
- * Converts event data fields from hex to decimal if they are hex strings
+ * Converts event data fields from BN and PublicKey to strings for display
*/
const convertEventData = (data: any) => {
- const keysToConvert = ['expirationTimestamp', 'wager', 'totalWager', 'totalGambaFee', 'durationSeconds', 'gameId']
- const convertedData = { ...data }
+ const convertedData = { ...data };
- keysToConvert.forEach((key) => {
- if (convertedData[key] && typeof convertedData[key] === 'string' && convertedData[key].startsWith('0x')) {
- convertedData[key] = parseInt(convertedData[key], 16)
+ for (const key in convertedData) {
+ const value = convertedData[key];
+ if (value instanceof BN) {
+ // Convert BN to string
+ convertedData[key] = value.toString();
+ } else if (value instanceof PublicKey) {
+ // Convert PublicKey to base58 string
+ convertedData[key] = value.toBase58();
+ } else if (Array.isArray(value)) {
+ // Process arrays recursively
+ convertedData[key] = value.map((item: any) => {
+ if (item instanceof BN) {
+ return item.toString();
+ } else if (item instanceof PublicKey) {
+ return item.toBase58();
+ } else {
+ return item;
+ }
+ });
}
- })
+ // else leave the value as is
+ }
- return convertedData
-}
+ return convertedData;
+};
/**
- * Extracts events from transaction logs
+ * Extracts events from transaction logs and converts event data
*/
export const parseTransactionEvents = (logs: string[]) => {
try {
- const parsedEvents: AnyMultiplayerEvent[] = []
- const events = eventParser.parseLogs(logs) as any as AnyMultiplayerEvent[]
+ const parsedEvents: AnyMultiplayerEvent[] = [];
+ const events = eventParser.parseLogs(logs);
for (const event of events) {
- console.log('Parsed Event:', event) // Log each parsed event for debugging
- parsedEvents.push(event)
+ const convertedData = convertEventData(event.data);
+ const convertedEvent = {
+ ...event,
+ data: convertedData,
+ };
+ console.log('Parsed Event:', convertedEvent); // Log each parsed event for debugging
+ parsedEvents.push(convertedEvent);
}
- return parsedEvents
+ return parsedEvents;
} catch (error) {
- console.error('Failed to parse transaction logs:', error)
- return []
+ console.error('Failed to parse transaction logs:', error);
+ return [];
}
-}
+};
/**
* Extracts events from a transaction
@@ -51,20 +73,18 @@ export const parseTransactionEvents = (logs: string[]) => {
export const parseMultiplayerTransaction = (
transaction: ParsedTransactionWithMeta,
) => {
- const logs = transaction.meta?.logMessages ?? []
- const events = parseTransactionEvents(logs)
+ const logs = transaction.meta?.logMessages ?? [];
+ const events = parseTransactionEvents(logs);
- return events.map((event): MultiplayerTransaction<'GameCreated'> | MultiplayerTransaction<'PlayerJoined'> | MultiplayerTransaction<'PlayerLeft'> | MultiplayerTransaction<'GameSettled'> => {
- const convertedData = convertEventData(event.data) // Convert event data
- console.log('Converted Event Data:', convertedData) // Log converted event data for debugging
+ return events.map((event): MultiplayerTransaction => {
return {
signature: transaction.transaction.signatures[0],
time: (transaction.blockTime ?? 0) * 1000,
- name: event.name as any,
- data: convertedData,
- }
- })
-}
+ name: event.name,
+ data: event.data,
+ };
+ });
+};
export async function fetchMultiplayerTransactionsFromSignatures(
connection: Connection,
@@ -76,9 +96,9 @@ export async function fetchMultiplayerTransactionsFromSignatures(
maxSupportedTransactionVersion: 0,
commitment: 'confirmed',
},
- )).flatMap((x) => x ? [x] : [])
+ )).flatMap((x) => (x ? [x] : []));
- return transactions.flatMap(parseMultiplayerTransaction)
+ return transactions.flatMap(parseMultiplayerTransaction);
}
/**
@@ -93,8 +113,11 @@ export async function fetchMultiplayerTransactions(
address,
options,
'confirmed',
- )
- const events = await fetchMultiplayerTransactionsFromSignatures(connection, signatureInfo.map((x) => x.signature))
+ );
+ const events = await fetchMultiplayerTransactionsFromSignatures(
+ connection,
+ signatureInfo.map((x) => x.signature),
+ );
- return events
+ return events;
}