From a8422ff0116c2981a114e518f1e3c40a9935ce0a Mon Sep 17 00:00:00 2001 From: aymericdelab Date: Thu, 12 Dec 2024 20:52:25 +0100 Subject: [PATCH] fix landing page sync --- .../components/modules/bridge-out-step-1.tsx | 2 +- .../components/modules/bridge-out-step-2.tsx | 61 +++++++++- landing/src/components/modules/swap-panel.tsx | 2 + landing/src/dojo/queries.ts | 104 ++++++++++++++++++ landing/src/dojo/setup.ts | 64 ++++++----- landing/src/hooks/helpers/use-sync-entity.tsx | 39 +++++++ landing/src/hooks/helpers/useResources.tsx | 21 ++-- landing/src/hooks/use-structures.tsx | 14 +-- 8 files changed, 257 insertions(+), 50 deletions(-) create mode 100644 landing/src/dojo/queries.ts create mode 100644 landing/src/hooks/helpers/use-sync-entity.tsx diff --git a/landing/src/components/modules/bridge-out-step-1.tsx b/landing/src/components/modules/bridge-out-step-1.tsx index 64a59a56a..7ad062653 100644 --- a/landing/src/components/modules/bridge-out-step-1.tsx +++ b/landing/src/components/modules/bridge-out-step-1.tsx @@ -31,13 +31,13 @@ function formatFee(fee: number) { export const BridgeOutStep1 = () => { const { address } = useAccount(); + const [realmEntityId, setRealmEntityId] = useState(""); const { getRealmNameById } = useRealm(); const { computeTravelTime } = useTravel(); const [isFeesOpen, setIsFeesOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [realmEntityId, setRealmEntityId] = useState(""); const { bridgeStartWithdrawFromRealm } = useBridgeAsset(); const [selectedResourceIds, setSelectedResourceIds] = useState([]); const [selectedResourceAmounts, setSelectedResourceAmounts] = useState<{ [key: string]: number }>({}); diff --git a/landing/src/components/modules/bridge-out-step-2.tsx b/landing/src/components/modules/bridge-out-step-2.tsx index 3f2be3a35..1b0c87640 100644 --- a/landing/src/components/modules/bridge-out-step-2.tsx +++ b/landing/src/components/modules/bridge-out-step-2.tsx @@ -1,9 +1,12 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { useDojo } from "@/hooks/context/DojoContext"; +import { useSyncEntity } from "@/hooks/helpers/use-sync-entity"; import { useEntities } from "@/hooks/helpers/useEntities"; import { useDonkeyArrivals } from "@/hooks/helpers/useResources"; import { useBridgeAsset } from "@/hooks/useBridge"; import { displayAddress } from "@/lib/utils"; import { ADMIN_BANK_ENTITY_ID, RESOURCE_PRECISION, ResourcesIds } from "@bibliothecadao/eternum"; +import { getComponentValue } from "@dojoengine/recs"; import { useAccount } from "@starknet-react/core"; import { ChevronDown, ChevronUp, Loader } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; @@ -19,8 +22,12 @@ import { BridgeFees } from "./bridge-fees"; export const BridgeOutStep2 = () => { const { address } = useAccount(); + const dojo = useDojo(); + const { getOwnerArrivalsAtBank, getDonkeyInfo } = useDonkeyArrivals(); + const [isLoading, setIsLoading] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); const [selectedResourceIds, setSelectedResourceIds] = useState([]); const [selectedResourceAmounts, setSelectedResourceAmounts] = useState<{ [key: string]: number }>({}); @@ -41,7 +48,21 @@ export const BridgeOutStep2 = () => { return playerRealms.map((realm) => realm!.entity_id); }, [playerRealms]); - const donkeysArrivals = useMemo(() => getOwnerArrivalsAtBank(realmEntityIds as number[]), [realmEntityIds]); + const [refreshTrigger, setRefreshTrigger] = useState(0); + const donkeysArrivals = useMemo( + () => getOwnerArrivalsAtBank(realmEntityIds as number[]), + [realmEntityIds, refreshTrigger], + ); + + const donkeyArrivalsEntityIds = useMemo(() => { + return donkeysArrivals.map((entity) => { + const position = getComponentValue(dojo.setup.components.Position, entity); + return position?.entity_id; + }); + }, [donkeysArrivals]) as number[]; + + useSyncEntity(donkeyArrivalsEntityIds); + const donkeyInfos = useMemo(() => { return donkeysArrivals.map((donkey) => getDonkeyInfo(donkey)); }, [donkeysArrivals]); @@ -55,6 +76,7 @@ export const BridgeOutStep2 = () => { tokenAddress: resourceAddresses[ResourcesIds[id].toUpperCase() as keyof typeof resourceAddresses][1], from_entity_id: Array.from(selectedDonkeys)[index], })); + try { setIsLoading(true); const tx = await bridgeFinishWithdrawFromRealm(donkeyResources, ADMIN_BANK_ENTITY_ID); @@ -104,6 +126,12 @@ export const BridgeOutStep2 = () => { updateResourcesFromSelectedDonkeys(newSelected); }, [donkeyInfos]); + const handleRefresh = () => { + setIsRefreshing(true); + setRefreshTrigger((prev) => prev + 1); + setTimeout(() => setIsRefreshing(false), 500); // Spin for 500ms + }; + return ( <>
@@ -122,7 +150,35 @@ export const BridgeOutStep2 = () => { Select Donkeys (optional)
- {isTableOpen ? : } +
+ + {isTableOpen ? : } +
@@ -145,7 +201,6 @@ export const BridgeOutStep2 = () => { } }); } - setSe; setSelectedDonkeys(newSelected); updateResourcesFromSelectedDonkeys(newSelected); }} diff --git a/landing/src/components/modules/swap-panel.tsx b/landing/src/components/modules/swap-panel.tsx index a5fb46b7c..62fc18fc0 100644 --- a/landing/src/components/modules/swap-panel.tsx +++ b/landing/src/components/modules/swap-panel.tsx @@ -1,10 +1,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useSyncPlayerRealms } from "@/hooks/helpers/use-sync-entity"; import { BookOpen } from "lucide-react"; import { BridgeIn } from "./bridge-in"; import { BridgeOutStep1 } from "./bridge-out-step-1"; import { BridgeOutStep2 } from "./bridge-out-step-2"; export const SwapPanel = () => { + useSyncPlayerRealms(); return (
diff --git a/landing/src/dojo/queries.ts b/landing/src/dojo/queries.ts new file mode 100644 index 000000000..a805e9763 --- /dev/null +++ b/landing/src/dojo/queries.ts @@ -0,0 +1,104 @@ +// onload -> fetch single key entities + +import { Component, Metadata, Schema } from "@dojoengine/recs"; +import { setEntities } from "@dojoengine/state"; +import { Clause, EntityKeysClause, PatternMatching, ToriiClient } from "@dojoengine/torii-client"; + +// on hexception -> fetch below queries based on entityID + +// background sync after load -> + +export const getEntities = async ( + client: ToriiClient, + clause: Clause | undefined, + components: Component[], + limit: number = 100, + logging: boolean = false, +) => { + if (logging) console.log("Starting getEntities"); + let offset = 0; + let continueFetching = true; + + while (continueFetching) { + const entities = await client.getEntities({ + limit, + offset, + clause, + dont_include_hashed_keys: false, + order_by: [], + }); + + setEntities(entities, components); + + if (Object.keys(entities).length < limit) { + continueFetching = false; + } else { + offset += limit; + } + } +}; + +export const syncEntitiesEternum = async ( + client: ToriiClient, + components: Component[], + entityKeyClause: EntityKeysClause[], + logging: boolean = false, +) => { + // if (logging) console.log("Starting syncEntities"); + return await client.onEntityUpdated(entityKeyClause, (fetchedEntities: any, data: any) => { + // if (logging) console.log("Entity updated", fetchedEntities); + + setEntities({ [fetchedEntities]: data }, components); + }); +}; + +export const addToSubscription = async ( + client: ToriiClient, + components: Component[], + entityID: string, + position?: { x: number; y: number }, +) => { + const positionClause: EntityKeysClause = { + Keys: { + keys: [String(position?.x || 0), String(position?.y || 0), undefined, undefined], + pattern_matching: "FixedLen" as PatternMatching, + models: [], + }, + }; + + await getEntities( + client, + { + Composite: { + operator: "Or", + clauses: [ + positionClause, + { + Keys: { + keys: [entityID], + pattern_matching: "FixedLen", + models: [], + }, + }, + { + Keys: { + keys: [entityID, undefined], + pattern_matching: "FixedLen", + models: [], + }, + }, + { + Keys: { + keys: [entityID, undefined, undefined], + pattern_matching: "FixedLen", + models: [], + }, + }, + ], + }, + }, + components, + 10_000, + false, + ); +}; diff --git a/landing/src/dojo/setup.ts b/landing/src/dojo/setup.ts index 0efd65ea0..310caae86 100644 --- a/landing/src/dojo/setup.ts +++ b/landing/src/dojo/setup.ts @@ -1,6 +1,6 @@ import { WORLD_CONFIG_ID } from "@bibliothecadao/eternum"; import { DojoConfig } from "@dojoengine/core"; -import { getSyncEntities, getSyncEvents, syncEntities } from "@dojoengine/state"; +import { getEvents, getSyncEntities } from "@dojoengine/state"; import { Clause } from "@dojoengine/torii-client"; import { createClientComponents } from "./createClientComponents"; import { createSystemCalls } from "./createSystemCalls"; @@ -15,16 +15,7 @@ export async function setup({ ...config }: DojoConfig) { const components = createClientComponents(network); const systemCalls = createSystemCalls(network); - // Helper function to filter components or events for syncing - const getFilteredComponents = (componentKeys: (keyof typeof network.contractComponents)[]) => { - return componentKeys.map((key) => network.contractComponents[key]); - }; - - const getFilteredEvents = (eventKeys: (keyof (typeof network.contractComponents)["events"])[]) => { - return eventKeys.map((key) => network.contractComponents["events"][key]); - }; - - const filteredComponents = getFilteredComponents([ + const filteredModels = [ "AddressName", "Realm", "Owner", @@ -40,27 +31,17 @@ export async function setup({ ...config }: DojoConfig) { "GuildMember", "EntityName", "Structure", - // todo: these are needed only for the bridge: how to improve this? - "Position", - "WeightConfig", "CapacityConfig", - "EntityOwner", - "ArrivalTime", - "OwnedResourcesTracker", - "Weight", - "Resource", - "SpeedConfig", - ]) as any; + ]; - const filteredEvents = getFilteredEvents([ + const filteredEvents = [ "BurnDonkey", // points "HyperstructureCoOwnersChange", "HyperstructureFinished", "GameEnded", - // count - "FragmentMineDiscovered", - ]) as any; + ]; + const clauses: Clause[] = [ { Keys: { @@ -87,17 +68,42 @@ export async function setup({ ...config }: DojoConfig) { // fetch all existing entities from torii with optional component filtering await getSyncEntities( network.toriiClient, - filteredComponents, + network.contractComponents as any, { Composite: { operator: "Or", clauses } }, [], 10_000, ); - const sync = await syncEntities(network.toriiClient, filteredComponents, [], false); + const sync = await getSyncEntities( + network.toriiClient, + network.contractComponents as any, + { + Keys: { + keys: [undefined], + pattern_matching: "VariableLen", + models: filteredModels.map((model) => `s0_eternum-${model}`), + }, + }, + [], + 10_000, + ); - configManager.setDojo(components); + const eventSync = getEvents( + network.toriiClient, + network.contractComponents.events as any, + undefined, + { + Keys: { + keys: [undefined], + pattern_matching: "VariableLen", + models: filteredEvents.map((event) => `s0_eternum-${event}`), + }, + }, + false, + false, + ); - const eventSync = getSyncEvents(network.toriiClient, filteredEvents, undefined, [], 20_000, false, false); + configManager.setDojo(components); return { network, diff --git a/landing/src/hooks/helpers/use-sync-entity.tsx b/landing/src/hooks/helpers/use-sync-entity.tsx new file mode 100644 index 000000000..e3511daff --- /dev/null +++ b/landing/src/hooks/helpers/use-sync-entity.tsx @@ -0,0 +1,39 @@ +import { addToSubscription } from "@/dojo/queries"; +import { useEffect, useMemo, useState } from "react"; +import { useDojo } from "../context/DojoContext"; +import { useEntities } from "./useEntities"; + +export const useSyncEntity = (entityIds: number | number[]) => { + const dojo = useDojo(); + const [isSyncing, setIsSyncing] = useState(false); + + useEffect(() => { + setIsSyncing(true); + const fetch = async () => { + try { + const ids = Array.isArray(entityIds) ? entityIds : [entityIds]; + await Promise.all( + ids.map((id) => + addToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, id.toString()), + ), + ); + } catch (error) { + console.error("Fetch failed", error); + } finally { + setIsSyncing(false); + } + }; + fetch(); + }, [entityIds]); + + return isSyncing; +}; + +export const useSyncPlayerRealms = () => { + const { playerRealms } = useEntities(); + const realmEntityIds = useMemo(() => { + return playerRealms.map((realm) => realm!.entity_id); + }, [playerRealms]); + + return useSyncEntity(realmEntityIds); +}; diff --git a/landing/src/hooks/helpers/useResources.tsx b/landing/src/hooks/helpers/useResources.tsx index bbef77688..0c374e284 100644 --- a/landing/src/hooks/helpers/useResources.tsx +++ b/landing/src/hooks/helpers/useResources.tsx @@ -32,17 +32,18 @@ export function useDonkeyArrivals() { const arrivals: any[] = []; for (const realmEntityId of realmEntityIds) { - arrivals.push( - ...runQuery([ - HasValue(Position, { x: bankPosition?.x ?? 0, y: bankPosition?.y ?? 0 }), - NotValue(OwnedResourcesTracker, { resource_types: 0n }), - Has(OwnedResourcesTracker), - Has(Weight), - Has(ArrivalTime), - HasValue(EntityOwner, { entity_owner_id: realmEntityId }), - ]), - ); + const res = runQuery([ + HasValue(Position, { x: bankPosition?.x ?? 0, y: bankPosition?.y ?? 0 }), + NotValue(OwnedResourcesTracker, { resource_types: 0n }), + Has(OwnedResourcesTracker), + Has(Weight), + Has(ArrivalTime), + HasValue(EntityOwner, { entity_owner_id: realmEntityId }), + ]); + + arrivals.push(...res); } + return arrivals; }; diff --git a/landing/src/hooks/use-structures.tsx b/landing/src/hooks/use-structures.tsx index 987370ae1..685e0098f 100644 --- a/landing/src/hooks/use-structures.tsx +++ b/landing/src/hooks/use-structures.tsx @@ -1,19 +1,19 @@ +import { StructureType } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { Has } from "@dojoengine/recs"; +import { Has, HasValue } from "@dojoengine/recs"; import { useDojo } from "./context/DojoContext"; export const useStructuresNumber = () => { const { setup: { - components: { - Hyperstructure, - Realm, - events: { FragmentMineDiscovered }, - }, + components: { Hyperstructure, Realm, Structure }, }, } = useDojo(); - const fragmentMinesCount = useEntityQuery([Has(FragmentMineDiscovered)]).length; + const fragmentMinesCount = useEntityQuery([ + HasValue(Structure, { category: StructureType[StructureType.FragmentMine] }), + ]).length; + const hyperstructuresCount = useEntityQuery([Has(Hyperstructure)]).length; const realmsCount = useEntityQuery([Has(Realm)]).length;