diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index 40593916d..6d6fc48e6 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -1,3 +1,4 @@ +import { SubscribeToHomePageEvents } from "@/components/pages/home/components/SubscribeToHomePageEvents"; import { ARENA_MODULE_ADDRESS } from "@sdk/const"; import { type ArenaInfoModel, @@ -67,6 +68,7 @@ export default async function HomePageComponent({ sortBy={sortBy} searchBytes={searchBytes} /> + ); } diff --git a/src/typescript/frontend/src/components/pages/home/components/SubscribeToHomePageEvents.tsx b/src/typescript/frontend/src/components/pages/home/components/SubscribeToHomePageEvents.tsx new file mode 100644 index 000000000..48ed72721 --- /dev/null +++ b/src/typescript/frontend/src/components/pages/home/components/SubscribeToHomePageEvents.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useReliableSubscribe } from "@hooks/use-reliable-subscribe"; +import { type ArenaInfoModel } from "@sdk/indexer-v2"; +import { useEventStore } from "context/event-store-context"; +import { useEffect } from "react"; + +export const SubscribeToHomePageEvents = ({ info }: { info?: ArenaInfoModel }) => { + const loadArenaInfoFromServer = useEventStore((s) => s.loadArenaInfoFromServer); + useEffect(() => { + if (info) { + loadArenaInfoFromServer(info); + } + }, [loadArenaInfoFromServer, info]); + + useReliableSubscribe({ + arena: true, + eventTypes: ["MarketLatestState"], + }); + + return <>; +}; diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx index 831831a68..cfe99dc6d 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx @@ -31,7 +31,6 @@ import { Text } from "components/text"; import Link from "next/link"; import { ROUTES } from "router/routes"; import { type HomePageProps } from "app/home/HomePage"; -import { useReliableSubscribe } from "@hooks/use-reliable-subscribe"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { Emoji } from "utils/emoji"; @@ -99,10 +98,6 @@ const EmojiTable = (props: EmojiTableProps) => { const rowLength = useGridRowLength(); - useReliableSubscribe({ - eventTypes: ["MarketLatestState"], - }); - return ( <> { + const melees = useEventStore((s) => s.meleeEvents); + + const latest = useMemo( + () => + melees + .map(({ melee }) => melee.meleeID) + .sort() + .at(-1) ?? -1n, + [melees] + ); + + return latest; +}; diff --git a/src/typescript/frontend/src/hooks/use-reliable-subscribe.ts b/src/typescript/frontend/src/hooks/use-reliable-subscribe.ts index 09ff77675..057b11576 100644 --- a/src/typescript/frontend/src/hooks/use-reliable-subscribe.ts +++ b/src/typescript/frontend/src/hooks/use-reliable-subscribe.ts @@ -15,28 +15,18 @@ export const useReliableSubscribe = (args: ReliableSubscribeArgs) => { const { eventTypes, arena } = args; const subscribeEvents = useEventStore((s) => s.subscribeEvents); const unsubscribeEvents = useEventStore((s) => s.unsubscribeEvents); - const subscribeArena = useEventStore((s) => s.subscribeArena); - const unsubscribeArena = useEventStore((s) => s.unsubscribeArena); useEffect(() => { // Don't subscribe right away, to let other components unmounting time to unsubscribe, that way // components unmounting don't undo/overwrite another component subscribing. const timeout = window.setTimeout(() => { - subscribeEvents(eventTypes); - if (arena) { - subscribeArena(); - } else { - unsubscribeArena(); - } + subscribeEvents(eventTypes, arena); }, 250); // Unsubscribe from all topics passed into the hook when the component unmounts. return () => { clearTimeout(timeout); - unsubscribeEvents(eventTypes); - if (arena) { - unsubscribeArena(); - } + unsubscribeEvents(eventTypes, arena); }; - }, [eventTypes, arena, subscribeEvents, unsubscribeEvents, subscribeArena, unsubscribeArena]); + }, [eventTypes, arena, subscribeEvents, unsubscribeEvents]); }; diff --git a/src/typescript/frontend/src/lib/store/arena/store.ts b/src/typescript/frontend/src/lib/store/arena/store.ts new file mode 100644 index 000000000..91c504d63 --- /dev/null +++ b/src/typescript/frontend/src/lib/store/arena/store.ts @@ -0,0 +1,36 @@ +import { + type ArenaMeleeModel, + type ArenaEnterModel, + type ArenaExitModel, + type ArenaSwapModel, + type ArenaInfoModel, +} from "@sdk/indexer-v2"; +import { type WritableDraft } from "immer"; + +export type MeleeState = { + swaps: readonly ArenaSwapModel[]; + enters: readonly ArenaEnterModel[]; + exits: readonly ArenaExitModel[]; +}; + +export type ArenaState = { + arenaInfoFromServer?: ArenaInfoModel; + meleeEvents: readonly ArenaMeleeModel[]; + melees: Readonly>; +}; + +export type ArenaActions = { + loadArenaInfoFromServer: (info: ArenaInfoModel) => void; +}; + +export const createInitialMeleeState = (): WritableDraft => ({ + swaps: [], + enters: [], + exits: [], +}); + +export const initializeArenaStore = (): ArenaState => ({ + arenaInfoFromServer: undefined, + meleeEvents: [], + melees: new Map(), +}); diff --git a/src/typescript/frontend/src/lib/store/arena/utils.ts b/src/typescript/frontend/src/lib/store/arena/utils.ts new file mode 100644 index 000000000..f68861990 --- /dev/null +++ b/src/typescript/frontend/src/lib/store/arena/utils.ts @@ -0,0 +1,67 @@ +import { type MarketEmojiData, toMarketEmojiData } from "@sdk/emoji_data"; +import { type AccountAddressString } from "@sdk/emojicoin_dot_fun"; +import { + type ArenaEventModels, + type ArenaVaultBalanceUpdateModel, + type ArenaInfoModel, + type ArenaEventModelWithMeleeID, +} from "@sdk/indexer-v2"; +import { isArenaEnterModel, isArenaExitModel, isArenaMeleeModel } from "@sdk/types/arena-types"; + +export type ArenaMarketPair = { + market0: { + marketID: bigint; + marketAddress: AccountAddressString; + symbol: string; + } & MarketEmojiData; + market1: { + marketID: bigint; + marketAddress: AccountAddressString; + symbol: string; + } & MarketEmojiData; +}; + +export const toArenaMarketPair = (info: ArenaInfoModel): ArenaMarketPair => { + const symbol0 = info.emojicoin0Symbols.join(""); + const symbol1 = info.emojicoin1Symbols.join(""); + return { + market0: { + marketID: info.emojicoin0MarketID, + marketAddress: info.emojicoin0MarketAddress, + symbol: symbol0, + ...toMarketEmojiData(symbol0), + }, + market1: { + marketID: info.emojicoin1MarketID, + marketAddress: info.emojicoin1MarketAddress, + symbol: symbol1, + ...toMarketEmojiData(symbol1), + }, + }; +}; + +export const toMappedMelees = (models: T[]) => { + const map = new Map(); + + models.forEach((model) => { + const id = getMeleeIDFromArenaModel(model); + if (!map.has(id)) { + map.set(id, []); + } + map.get(id)!.push(model); + }); + return map; +}; + +export const getMeleeIDFromArenaModel = ( + model: Exclude +): bigint => { + if (isArenaMeleeModel(model)) { + return model.melee.meleeID; + } else if (isArenaEnterModel(model)) { + return model.enter.meleeID; + } else if (isArenaExitModel(model)) { + return model.exit.meleeID; + } + return model.swap.meleeID; +}; diff --git a/src/typescript/frontend/src/lib/store/event/event-store.ts b/src/typescript/frontend/src/lib/store/event/event-store.ts index f7a0ad947..f43e5e701 100644 --- a/src/typescript/frontend/src/lib/store/event/event-store.ts +++ b/src/typescript/frontend/src/lib/store/event/event-store.ts @@ -19,6 +19,7 @@ import { pushPeriodicStateEvents, toMappedMarketEvents, initialState, + ensureMeleeInStore, } from "./utils"; import { periodEnumToRawDuration } from "@sdk/const"; import { createWebSocketClientStore, type WebSocketClientStore } from "../websocket/store"; @@ -29,15 +30,30 @@ import { clearLocalStorage, LOCAL_STORAGE_EVENT_TYPES, } from "./local-storage"; +import { initializeArenaStore } from "../arena/store"; +import { + isArenaEnterModel, + isArenaEventModelWithMeleeID, + isArenaExitModel, + isArenaMeleeModel, + isArenaSwapModel, +} from "@sdk/types/arena-types"; +import { getMeleeIDFromArenaModel, toMappedMelees } from "../arena/utils"; export const createEventStore = () => { const store = createStore()( immer((set, get) => ({ ...initialState(), + ...initializeArenaStore(), getMarket: (emojis) => get().markets.get(emojis.join("")), getRegisteredMarkets: () => { return get().markets; }, + loadArenaInfoFromServer: (info) => { + set((state) => { + state.arenaInfoFromServer = info; + }); + }, loadMarketStateFromServer: (states) => { const filtered = states.filter((e) => { const marketEmojis = e.market.symbolEmojis; @@ -83,6 +99,17 @@ export const createEventStore = () => { pushPeriodicStateEvents(market, extractFilter(marketEvents, isPeriodicStateEventModel)); DEBUG_ASSERT(() => marketEvents.length === 0); }); + + const arenaEvents = events.filter(isArenaEventModelWithMeleeID); + const arenaMap = toMappedMelees(arenaEvents); + Array.from(arenaMap.entries()).forEach(([meleeID, events]) => { + ensureMeleeInStore(state, meleeID); + const melee = state.melees.get(meleeID)!; + melee.enters.push(...extractFilter(events, isArenaEnterModel)); + melee.exits.push(...extractFilter(events, isArenaExitModel)); + melee.swaps.push(...extractFilter(events, isArenaSwapModel)); + }); + state.meleeEvents.push(...extractFilter(arenaEvents, isArenaMeleeModel)); }); }, pushEventsFromClient: (eventsIn: BrokerEventModels[], pushToLocalStorage = false) => { @@ -121,8 +148,20 @@ export const createEventStore = () => { maybeUpdateLocalStorage(pushToLocalStorage, "periodic", event); } } else { - // Arena events. - // TODO. + if (isArenaEventModelWithMeleeID(event)) { + const meleeID = getMeleeIDFromArenaModel(event); + ensureMeleeInStore(state, meleeID); + const melee = state.melees.get(meleeID)!; + if (isArenaMeleeModel(event)) { + state.meleeEvents.unshift(event); + } else if (isArenaEnterModel(event)) { + melee.enters.unshift(event); + } else if (isArenaExitModel(event)) { + melee.exits.unshift(event); + } else if (isArenaSwapModel(event)) { + melee.swaps.unshift(event); + } + } } } }); diff --git a/src/typescript/frontend/src/lib/store/event/types.ts b/src/typescript/frontend/src/lib/store/event/types.ts index 68dd81182..044275543 100644 --- a/src/typescript/frontend/src/lib/store/event/types.ts +++ b/src/typescript/frontend/src/lib/store/event/types.ts @@ -9,6 +9,8 @@ import { type SubscribeBarsCallback } from "@static/charting_library/datafeed-ap import { type LatestBar } from "./candlestick-bars"; import { type WritableDraft } from "immer"; import { type ClientState, type ClientActions } from "../websocket/store"; +import { type ArenaActions, type ArenaState } from "../arena/store"; +import { type Flatten } from "@sdk-types"; // Aliased to avoid repeating the type names over and over. type Swap = DatabaseModels["swap_events"]; @@ -28,8 +30,12 @@ export type CandlestickData = { latestBar: LatestBar | undefined; }; +export type MarketStoreMetadata = Flatten< + Omit +>; + export type MarketEventStore = { - marketMetadata: MarketMetadataModel; + marketMetadata: MarketStoreMetadata; dailyVolume?: bigint; swapEvents: readonly Swap[]; liquidityEvents: readonly Liquidity[]; @@ -59,7 +65,7 @@ export type PeriodSubscription = { }; export type SetLatestBarsArgs = { - marketMetadata: MarketMetadataModel; + marketMetadata: MarketStoreMetadata; latestBars: readonly LatestBar[]; }; @@ -74,9 +80,13 @@ export type EventActions = { unsubscribeFromPeriod: ({ marketEmojis, period }: Omit) => void; }; -export type EventStore = EventState & EventActions; +export type EventStore = EventState & EventActions & ArenaState & ArenaActions; -export type EventAndClientStore = EventState & EventActions & ClientState & ClientActions; +export type EventAndClientStore = EventState & + EventActions & + ClientState & + ClientActions & + ArenaState; export type ImmerSetEventAndClientStore = ( nextStateOrUpdater: diff --git a/src/typescript/frontend/src/lib/store/event/utils.ts b/src/typescript/frontend/src/lib/store/event/utils.ts index d222f7615..b983f7467 100644 --- a/src/typescript/frontend/src/lib/store/event/utils.ts +++ b/src/typescript/frontend/src/lib/store/event/utils.ts @@ -1,9 +1,13 @@ import { Period, PERIODS, periodEnumToRawDuration } from "@sdk/const"; import { type SubscribeBarsCallback } from "@static/charting_library/datafeed-api"; import { type WritableDraft } from "immer"; -import { type EventState, type CandlestickData, type MarketEventStore } from "./types"; import { - type MarketMetadataModel, + type EventState, + type CandlestickData, + type MarketEventStore, + type MarketStoreMetadata, +} from "./types"; +import { type PeriodicStateEventModel, type SwapEventModel, type DatabaseModels, @@ -13,6 +17,7 @@ import { getPeriodStartTimeFromTime } from "@sdk/utils"; import { createBarFromPeriodicState, createBarFromSwap, type LatestBar } from "./candlestick-bars"; import { q64ToBig } from "@sdk/utils/nominal-price"; import { toNominal } from "lib/utils/decimals"; +import { type ArenaState, createInitialMeleeState } from "../arena/store"; type PeriodicState = DatabaseModels["periodic_state_events"]; @@ -23,7 +28,7 @@ export const createInitialCandlestickData = (): WritableDraft = }); export const createInitialMarketState = ( - marketMetadata: MarketMetadataModel + marketMetadata: MarketStoreMetadata ): WritableDraft => ({ marketMetadata, swapEvents: [], @@ -41,7 +46,7 @@ export const createInitialMarketState = ( export const ensureMarketInStore = ( state: WritableDraft, - market: MarketMetadataModel + market: MarketStoreMetadata ) => { const key = market.symbolData.symbol; if (!state.markets.has(key)) { @@ -49,6 +54,12 @@ export const ensureMarketInStore = ( } }; +export const ensureMeleeInStore = (state: WritableDraft, meleeID: bigint) => { + if (!state.melees.has(meleeID)) { + state.melees.set(meleeID, createInitialMeleeState()); + } +}; + export const pushPeriodicStateEvents = ( market: WritableDraft, periodicStateEvents: PeriodicState[] diff --git a/src/typescript/frontend/src/lib/store/websocket/store.ts b/src/typescript/frontend/src/lib/store/websocket/store.ts index 3e6270aac..afc890150 100644 --- a/src/typescript/frontend/src/lib/store/websocket/store.ts +++ b/src/typescript/frontend/src/lib/store/websocket/store.ts @@ -19,10 +19,8 @@ export type ClientState = { export type ClientActions = { close: () => void; - subscribeEvents: (events: SubscribableBrokerEvents[]) => void; - unsubscribeEvents: (events: SubscribableBrokerEvents[]) => void; - subscribeArena: () => void; - unsubscribeArena: () => void; + subscribeEvents: (events: SubscribableBrokerEvents[], arena?: boolean) => void; + unsubscribeEvents: (events: SubscribableBrokerEvents[], arena?: boolean) => void; }; export type WebSocketClientStore = ClientState & ClientActions; @@ -83,28 +81,18 @@ export const createWebSocketClientStore = ( close: () => { get().client.client.close(); }, - subscribeEvents: (e) => { + subscribeEvents: (e, arena) => { set((state) => { - state.client.subscribeEvents(e); + state.client.subscribeEvents(e, arena); + state.subscriptions.arena = !!arena; state.subscriptions = state.client.subscriptions; }); }, - unsubscribeEvents: (e) => { + unsubscribeEvents: (e, arena) => { set((state) => { - state.client.unsubscribeEvents(e); + state.client.unsubscribeEvents(e, arena); + state.subscriptions.arena = !!arena; state.subscriptions = state.client.subscriptions; }); }, - subscribeArena: () => { - set((state) => { - state.client.subscribeArena(); - state.subscriptions.arena = true; - }); - }, - unsubscribeArena: () => { - set((state) => { - state.client.unsubscribeArena(); - state.subscriptions.arena = false; - }); - }, }); diff --git a/src/typescript/sdk/src/broker-v2/client.ts b/src/typescript/sdk/src/broker-v2/client.ts index de95732c6..930319788 100644 --- a/src/typescript/sdk/src/broker-v2/client.ts +++ b/src/typescript/sdk/src/broker-v2/client.ts @@ -1,7 +1,6 @@ import { parseJSONWithBigInts } from "../indexer-v2/json-bigint"; import { type BrokerEventModels } from "../indexer-v2/types"; import { type BrokerJsonTypes } from "../indexer-v2/types/json-types"; -import { type AnyNumberString } from "../types"; import { ensureArray } from "../utils/misc"; import { type BrokerEvent, @@ -90,7 +89,6 @@ export class WebSocketClient { public setOnConnect(onConnect: NonNullable) { this.client.onopen = (e: Event) => { - this.sendToBroker(); onConnect(e); }; } @@ -108,43 +106,30 @@ export class WebSocketClient { } @SendToBroker - public subscribeMarkets(input: AnyNumberString | AnyNumberString[]) { - const newMarkets = new Set(ensureArray(input)); - newMarkets.forEach((e) => this.subscriptions.marketIDs.add(e)); - } - - @SendToBroker - public subscribeEvents(input: SubscribableBrokerEvents | SubscribableBrokerEvents[]) { + public subscribeEvents( + input: SubscribableBrokerEvents | SubscribableBrokerEvents[], + arena?: boolean + ) { const newTypes = new Set(ensureArray(input)); newTypes.forEach((e) => this.subscriptions.eventTypes.add(e)); if (this.permanentlySubscribeToMarketRegistrations) { this.subscriptions.eventTypes.add("MarketRegistration"); } + this.subscriptions.arena = !!arena; } @SendToBroker - public subscribeArena() { - this.subscriptions.arena = true; - } - - @SendToBroker - public unsubscribeMarkets(input: AnyNumberString | AnyNumberString[]) { - const newMarkets = new Set(ensureArray(input)); - newMarkets.forEach((e) => this.subscriptions.marketIDs.delete(e)); - } - - @SendToBroker - public unsubscribeEvents(input: SubscribableBrokerEvents | SubscribableBrokerEvents[]) { + public unsubscribeEvents( + input: SubscribableBrokerEvents | SubscribableBrokerEvents[], + arena?: boolean + ) { const newTypes = new Set(ensureArray(input)); if (this.permanentlySubscribeToMarketRegistrations) { newTypes.delete("MarketRegistration"); } newTypes.forEach((e) => this.subscriptions.eventTypes.delete(e)); - } - - @SendToBroker - public unsubscribeArena() { - this.subscriptions.arena = false; + // `arena: true` in the function args means it should be unsubscribed from. + this.subscriptions.arena = !arena; } public sendToBroker() { diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index 42a39791f..d1ab85caa 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -1039,6 +1039,15 @@ export type EventModelWithMarket = | DatabaseModels[TableName.MarketLatestStateEvent] | DatabaseModels[TableName.LiquidityEvents]; +export type ArenaEventModels = + | DatabaseModels[TableName.ArenaEnterEvents] + | DatabaseModels[TableName.ArenaMeleeEvents] + | DatabaseModels[TableName.ArenaExitEvents] + | DatabaseModels[TableName.ArenaSwapEvents] + | DatabaseModels[TableName.ArenaVaultBalanceUpdateEvents]; + +export type ArenaEventModelWithMeleeID = Exclude; + const extractEventType = (guid: string) => { const match = guid.match(/^.*::(\w+)::/u); return match ? match[1] : null; diff --git a/src/typescript/sdk/src/types/arena-types.ts b/src/typescript/sdk/src/types/arena-types.ts index 6a091ea80..e9dccb788 100644 --- a/src/typescript/sdk/src/types/arena-types.ts +++ b/src/typescript/sdk/src/types/arena-types.ts @@ -8,6 +8,8 @@ import { type ArenaMeleeModel, type ArenaSwapModel, type ArenaVaultBalanceUpdateModel, + type ArenaEventModels, + type ArenaEventModelWithMeleeID, } from "../indexer-v2"; import { postgresTimestampToDate } from "../indexer-v2/types/json-types"; import { dateFromMicroseconds, toAccountAddressString } from "../utils"; @@ -358,3 +360,11 @@ export const isArenaVaultBalanceUpdateModel = ( ): e is ArenaVaultBalanceUpdateModel => e.eventName === "ArenaVaultBalanceUpdate"; /* eslint-enable import/no-unused-modules */ + +export const isArenaEventModel = (e: BrokerEventModels): e is ArenaEventModels => + isArenaEventModelWithMeleeID(e) || isArenaVaultBalanceUpdateModel(e); + +export const isArenaEventModelWithMeleeID = ( + e: BrokerEventModels +): e is ArenaEventModelWithMeleeID => + isArenaEnterModel(e) || isArenaExitModel(e) || isArenaMeleeModel(e) || isArenaSwapModel(e);