Skip to content

Commit

Permalink
feat(ECO-2874): Add arena broker data to app state
Browse files Browse the repository at this point in the history
  • Loading branch information
xbtmatt committed Feb 28, 2025
1 parent 7d6f21c commit a70ded2
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 74 deletions.
2 changes: 2 additions & 0 deletions src/typescript/frontend/src/app/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SubscribeToHomePageEvents } from "@/components/pages/home/components/SubscribeToHomePageEvents";
import { ARENA_MODULE_ADDRESS } from "@sdk/const";
import {
type ArenaInfoModel,
Expand Down Expand Up @@ -67,6 +68,7 @@ export default async function HomePageComponent({
sortBy={sortBy}
searchBytes={searchBytes}
/>
<SubscribeToHomePageEvents info={meleeData?.melee} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 <></>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -99,10 +98,6 @@ const EmojiTable = (props: EmojiTableProps) => {

const rowLength = useGridRowLength();

useReliableSubscribe({
eventTypes: ["MarketLatestState"],
});

return (
<>
<ButtonsBlock
Expand Down
17 changes: 17 additions & 0 deletions src/typescript/frontend/src/hooks/use-latest-melee-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEventStore } from "context/event-store-context";
import { useMemo } from "react";

export const useLatestMeleeID = () => {
const melees = useEventStore((s) => s.meleeEvents);

const latest = useMemo(
() =>
melees
.map(({ melee }) => melee.meleeID)
.sort()
.at(-1) ?? -1n,
[melees]
);

return latest;
};
16 changes: 3 additions & 13 deletions src/typescript/frontend/src/hooks/use-reliable-subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
};
36 changes: 36 additions & 0 deletions src/typescript/frontend/src/lib/store/arena/store.ts
Original file line number Diff line number Diff line change
@@ -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<Map<bigint, MeleeState>>;
};

export type ArenaActions = {
loadArenaInfoFromServer: (info: ArenaInfoModel) => void;
};

export const createInitialMeleeState = (): WritableDraft<MeleeState> => ({
swaps: [],
enters: [],
exits: [],
});

export const initializeArenaStore = (): ArenaState => ({
arenaInfoFromServer: undefined,
meleeEvents: [],
melees: new Map(),
});
67 changes: 67 additions & 0 deletions src/typescript/frontend/src/lib/store/arena/utils.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ArenaEventModelWithMeleeID>(models: T[]) => {
const map = new Map<bigint, T[]>();

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<ArenaEventModels, ArenaVaultBalanceUpdateModel>
): 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;
};
43 changes: 41 additions & 2 deletions src/typescript/frontend/src/lib/store/event/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
pushPeriodicStateEvents,
toMappedMarketEvents,
initialState,
ensureMeleeInStore,
} from "./utils";
import { periodEnumToRawDuration } from "@sdk/const";
import { createWebSocketClientStore, type WebSocketClientStore } from "../websocket/store";
Expand All @@ -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<EventStore & WebSocketClientStore>()(
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;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
}
}
}
}
});
Expand Down
18 changes: 14 additions & 4 deletions src/typescript/frontend/src/lib/store/event/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -28,8 +30,12 @@ export type CandlestickData = {
latestBar: LatestBar | undefined;
};

export type MarketStoreMetadata = Flatten<
Omit<MarketMetadataModel, "time" | "marketNonce" | "trigger">
>;

export type MarketEventStore = {
marketMetadata: MarketMetadataModel;
marketMetadata: MarketStoreMetadata;
dailyVolume?: bigint;
swapEvents: readonly Swap[];
liquidityEvents: readonly Liquidity[];
Expand Down Expand Up @@ -59,7 +65,7 @@ export type PeriodSubscription = {
};

export type SetLatestBarsArgs = {
marketMetadata: MarketMetadataModel;
marketMetadata: MarketStoreMetadata;
latestBars: readonly LatestBar[];
};

Expand All @@ -74,9 +80,13 @@ export type EventActions = {
unsubscribeFromPeriod: ({ marketEmojis, period }: Omit<PeriodSubscription, "cb">) => 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:
Expand Down
19 changes: 15 additions & 4 deletions src/typescript/frontend/src/lib/store/event/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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"];

Expand All @@ -23,7 +28,7 @@ export const createInitialCandlestickData = (): WritableDraft<CandlestickData> =
});

export const createInitialMarketState = (
marketMetadata: MarketMetadataModel
marketMetadata: MarketStoreMetadata
): WritableDraft<MarketEventStore> => ({
marketMetadata,
swapEvents: [],
Expand All @@ -41,14 +46,20 @@ export const createInitialMarketState = (

export const ensureMarketInStore = (
state: WritableDraft<EventState>,
market: MarketMetadataModel
market: MarketStoreMetadata
) => {
const key = market.symbolData.symbol;
if (!state.markets.has(key)) {
state.markets.set(key, createInitialMarketState(market));
}
};

export const ensureMeleeInStore = (state: WritableDraft<ArenaState>, meleeID: bigint) => {
if (!state.melees.has(meleeID)) {
state.melees.set(meleeID, createInitialMeleeState());
}
};

export const pushPeriodicStateEvents = (
market: WritableDraft<MarketEventStore>,
periodicStateEvents: PeriodicState[]
Expand Down
Loading

0 comments on commit a70ded2

Please sign in to comment.