diff --git a/indexer/src/adventurers.ts b/indexer/src/adventurers.ts index 8fff81c7e..a0956430e 100644 --- a/indexer/src/adventurers.ts +++ b/indexer/src/adventurers.ts @@ -1,57 +1,60 @@ import type { Config } from "https://esm.sh/@apibara/indexer"; -import type { Block, Starknet } from "https://esm.sh/@apibara/indexer/starknet"; -import type { Mongo } from "https://esm.sh/@apibara/indexer/sink/mongo"; import type { Console } from "https://esm.sh/@apibara/indexer/sink/console"; +import type { Mongo } from "https://esm.sh/@apibara/indexer/sink/mongo"; +import type { Block, Starknet } from "https://esm.sh/@apibara/indexer/starknet"; +import { MONGO_CONNECTION_STRING } from "./utils/constants.ts"; +import { getLevelFromXp } from "./utils/encode.ts"; import { + ADVENTURER_DIED, ADVENTURER_UPGRADED, + AMBUSHED_BY_BEAST, + ATTACKED_BEAST, + ATTACKED_BY_BEAST, + DISCOVERED_BEAST, DISCOVERED_GOLD, DISCOVERED_HEALTH, + DISCOVERED_LOOT, + DODGED_OBSTACLE, + DROPPED_ITEMS, + EQUIPPED_ITEMS, + FLEE_FAILED, + FLEE_SUCCEEDED, + HIT_BY_OBSTACLE, + ITEMS_LEVELED_UP, + parseAdventurerDied, parseAdventurerUpgraded, + parseAmbushedByBeast, + parseAttackedByBeast, + parseDiscoveredBeast, parseDiscoveredGold, parseDiscoveredHealth, - parseStartGame, - START_GAME, - PURCHASED_ITEMS, - ATTACKED_BY_BEAST, - ADVENTURER_DIED, - parseAdventurerDied, - parseAttackedByBeast, - AMBUSHED_BY_BEAST, - parseAmbushedByBeast, - ATTACKED_BEAST, - SLAYED_BEAST, - parseSlayedBeast, - FLEE_FAILED, + parseDiscoveredLoot, + parseDodgedObstacle, + parseDroppedItems, + parseEquippedItems, parseFleeFailed, - FLEE_SUCCEEDED, parseFleeSucceeded, - ITEMS_LEVELED_UP, + parseHitByObstacle, parseItemsLeveledUp, - EQUIPPED_ITEMS, parsePurchasedItems, - parseEquippedItems, - DROPPED_ITEMS, - parseDroppedItems, - HIT_BY_OBSTACLE, - parseHitByObstacle, - DODGED_OBSTACLE, - parseDodgedObstacle, - UPGRADES_AVAILABLE, + parseSlayedBeast, + parseStartGame, + parseTransfer, + parseUpdateAdventurerName, parseUpgradesAvailable, - DISCOVERED_BEAST, - parseDiscoveredBeast, - DISCOVERED_LOOT, - parseDiscoveredLoot, + PURCHASED_ITEMS, + SLAYED_BEAST, + START_GAME, TRANSFER, - parseTransfer, + UPDATE_ADVENTURER_NAME, + UPGRADES_AVAILABLE, } from "./utils/events.ts"; import { insertAdventurer, updateAdventurer, + updateAdventurerName, updateAdventurerOwner, } from "./utils/helpers.ts"; -import { MONGO_CONNECTION_STRING } from "./utils/constants.ts"; -import { getLevelFromXp } from "./utils/encode.ts"; const GAME = Deno.env.get("GAME"); const START = +(Deno.env.get("START") || 0); @@ -83,6 +86,7 @@ const filter = { { fromAddress: GAME, keys: [UPGRADES_AVAILABLE] }, { fromAddress: GAME, keys: [ADVENTURER_UPGRADED] }, { fromAddress: GAME, keys: [TRANSFER] }, + { fromAddress: GAME, keys: [UPDATE_ADVENTURER_NAME] }, ], }; @@ -351,6 +355,17 @@ export default function transform({ header, events }: Block) { }), ]; } + return []; + } + case UPDATE_ADVENTURER_NAME: { + console.log("UPDATE_ADVENTURER_NAME", "->", "ADVENTURER UPDATES"); + const { value } = parseUpdateAdventurerName(event.data, 0); + return [ + updateAdventurerName({ + adventurerId: value.adventurerId, + adventurerName: value.name, + }), + ]; } default: { console.warn("Unknown event", event.keys[0]); diff --git a/indexer/src/utils/events.ts b/indexer/src/utils/events.ts index d93501c1f..91d7644c7 100644 --- a/indexer/src/utils/events.ts +++ b/indexer/src/utils/events.ts @@ -8,8 +8,8 @@ import { parseU128, parseU16, parseU256, - parseU64, parseU32, + parseU64, parseU8, } from "./parser.ts"; @@ -53,6 +53,7 @@ export const NEW_COLLECTION_TOTAL = eventKey("NewCollectionTotal"); // Tokens export const TRANSFER = eventKey("Transfer"); export const CLAIMED_FREE_GAME = eventKey("ClaimedFreeGame"); +export const UPDATE_ADVENTURER_NAME = eventKey("UpdatedAdventurerName"); export const parseStats = combineParsers({ strength: { index: 0, parser: parseU8 }, @@ -384,3 +385,8 @@ export const parseNewCollectionTotal = combineParsers({ xp: { index: 1, parser: parseU32 }, gamesPlayed: { index: 2, parser: parseU32 }, }); + +export const parseUpdateAdventurerName = combineParsers({ + adventurerId: { index: 0, parser: parseFelt252 }, + name: { index: 1, parser: parseFelt252 }, +}); diff --git a/indexer/src/utils/helpers.ts b/indexer/src/utils/helpers.ts index 926a8e4bc..a267c0a4e 100644 --- a/indexer/src/utils/helpers.ts +++ b/indexer/src/utils/helpers.ts @@ -1,5 +1,5 @@ +import { checkExistsInt, computeHash, getLevelFromXp } from "./encode.ts"; import { parseAdventurerState } from "./events.ts"; -import { computeHash, checkExistsInt, getLevelFromXp } from "./encode.ts"; export function insertAdventurer({ id, @@ -235,6 +235,22 @@ export function updateAdventurerOwner({ }; } +export function updateAdventurerName({ adventurerId, adventurerName }: any) { + const entity = { + id: checkExistsInt(parseInt(adventurerId)), + }; + + return { + entity, + update: { + $set: { + ...entity, + name: checkExistsInt(BigInt(adventurerName).toString(16)), + }, + }, + }; +} + export function insertDiscovery({ txHash, adventurerId, diff --git a/ui/src/app/components/navigation/TxActivity.tsx b/ui/src/app/components/navigation/TxActivity.tsx index 478e3ea15..5b0047ab2 100644 --- a/ui/src/app/components/navigation/TxActivity.tsx +++ b/ui/src/app/components/navigation/TxActivity.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from "react"; -import { InvokeTransactionReceiptResponse } from "starknet"; -import { useWaitForTransaction } from "@starknet-react/core"; -import { displayAddress, padAddress } from "@/app/lib/utils"; -import useLoadingStore from "@/app/hooks/useLoadingStore"; import LootIconLoader from "@/app/components/icons/Loader"; +import useLoadingStore from "@/app/hooks/useLoadingStore"; import useTransactionCartStore from "@/app/hooks/useTransactionCartStore"; import useUIStore from "@/app/hooks/useUIStore"; import { networkConfig } from "@/app/lib/networkConfig"; +import { displayAddress, padAddress } from "@/app/lib/utils"; +import { useWaitForTransaction } from "@starknet-react/core"; +import { useEffect, useState } from "react"; +import { InvokeTransactionReceiptResponse } from "starknet"; export const TxActivity = () => { const stopLoading = useLoadingStore((state) => state.stopLoading); diff --git a/ui/src/app/components/start/AdventurerListCard.tsx b/ui/src/app/components/start/AdventurerListCard.tsx index fa1f366f2..3ec31c088 100644 --- a/ui/src/app/components/start/AdventurerListCard.tsx +++ b/ui/src/app/components/start/AdventurerListCard.tsx @@ -1,17 +1,19 @@ -import { useEffect, useState } from "react"; +import Info from "@/app/components/adventurer/Info"; import { Button } from "@/app/components/buttons/Button"; +import useAdventurerStore from "@/app/hooks/useAdventurerStore"; +import useNetworkAccount from "@/app/hooks/useNetworkAccount"; +import useTransactionCartStore from "@/app/hooks/useTransactionCartStore"; +import { padAddress } from "@/app/lib/utils"; import { Adventurer } from "@/app/types"; -import Info from "@/app/components/adventurer/Info"; +import { useProvider } from "@starknet-react/core"; +import { ChangeEvent, useEffect, useState } from "react"; +import { MdClose } from "react-icons/md"; import { AccountInterface, + constants, Contract, validateAndParseAddress, - constants, } from "starknet"; -import useTransactionCartStore from "@/app/hooks/useTransactionCartStore"; -import { useAccount, useProvider } from "@starknet-react/core"; -import useAdventurerStore from "@/app/hooks/useAdventurerStore"; -import { padAddress } from "@/app/lib/utils"; import { StarknetIdNavigator } from "starknetid.js"; import { CartridgeIcon, StarknetIdIcon } from "../icons/Icons"; @@ -25,6 +27,13 @@ export interface AdventurerListCardProps { from: string, recipient: string ) => Promise; + changeAdventurerName: ( + account: AccountInterface, + adventurerId: number, + name: string, + index: number + ) => Promise; + index: number; } export const AdventurerListCard = ({ @@ -32,8 +41,10 @@ export const AdventurerListCard = ({ gameContract, handleSwitchAdventurer, transferAdventurer, + changeAdventurerName, + index, }: AdventurerListCardProps) => { - const { account, address } = useAccount(); + const { account, address } = useNetworkAccount(); const { provider } = useProvider(); const starknetIdNavigator = new StarknetIdNavigator( provider, @@ -55,6 +66,15 @@ export const AdventurerListCard = ({ "send" | "addToCart" | null >(null); + const [isEditOpen, setIsEditOpen] = useState(false); + const [adventurerName, setAdventurerName] = useState(adventurer?.name || ""); + + useEffect(() => { + setAdventurerName(adventurer?.name!); + }, [adventurer]); + + const [isMaxLength, setIsMaxLength] = useState(false); + const setAdventurer = useAdventurerStore((state) => state.setAdventurer); useEffect(() => { @@ -144,18 +164,39 @@ export const AdventurerListCard = ({ setIsTransferOpen(false); }; + const handleNameChange = ( + e: ChangeEvent + ) => { + const value = e.target.value; + setAdventurerName(value.slice(0, 31)); + if (value.length >= 31) { + setIsMaxLength(true); + } else { + setIsMaxLength(false); + } + }; + + const birthstamp = parseInt(adventurer?.birthDate!); + const currentTimestamp = Math.floor(Date.now() / 1000); + const tenDaysInSeconds = 10 * 24 * 60 * 60; // 10 days in seconds + const futureTimestamp = birthstamp + tenDaysInSeconds; + + const expired = currentTimestamp >= futureTimestamp; + const dead = adventurer?.health === 0; + return ( <> {adventurer && (
@@ -163,19 +204,24 @@ export const AdventurerListCard = ({ size={"lg"} variant={"token"} onClick={() => setIsTransferOpen(!isTransferOpen)} - className={`w-1/2 ${isTransferOpen && "animate-pulse"}`} + className={`w-1/4 ${isTransferOpen && "animate-pulse"}`} > Transfer + {isTransferOpen && ( <>
-
- - Enter Address - - + + + Enter Address + +
+ setIsTransferOpen(false)} + className="w-10 h-10 cursor-pointer" + /> +
)} + {isEditOpen && ( + <> +
+
+
+ + Change Adventurer Name + +
+ setIsEditOpen(false)} + className="w-10 h-10 cursor-pointer" + /> +
+
+
+ + {isMaxLength && ( +

MAX LENGTH!

+ )} +
+ +
+
+
+ + )}
)} - {isTransferOpen && ( + {(isTransferOpen || isEditOpen) && (
)} diff --git a/ui/src/app/components/start/AdventurersList.tsx b/ui/src/app/components/start/AdventurersList.tsx index 4aeefa544..c15c6a9e3 100644 --- a/ui/src/app/components/start/AdventurersList.tsx +++ b/ui/src/app/components/start/AdventurersList.tsx @@ -19,7 +19,14 @@ import useUIStore from "@/app/hooks/useUIStore"; import { calculateLevel, indexAddress, padAddress } from "@/app/lib/utils"; import { Adventurer } from "@/app/types"; import { useProvider } from "@starknet-react/core"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { AccountInterface, constants, @@ -41,6 +48,12 @@ export interface AdventurerListProps { from: string, recipient: string ) => Promise; + changeAdventurerName: ( + account: AccountInterface, + adventurerId: number, + name: string, + index: number + ) => Promise; } export const AdventurersList = ({ @@ -51,6 +64,7 @@ export const AdventurersList = ({ adventurersCount, aliveAdventurersCount, transferAdventurer, + changeAdventurerName, }: AdventurerListProps) => { const { provider } = useProvider(); const starknetIdNavigator = new StarknetIdNavigator( @@ -60,8 +74,12 @@ export const AdventurersList = ({ const [selectedIndex, setSelectedIndex] = useState(-1); const [showZeroHealth, setShowZeroHealth] = useState(false); const [isTransferOpen, setIsTransferOpen] = useState(false); + const [isEditOpen, setIsEditOpen] = useState(false); const [adventurerForTransfer, setAdventurerForTransfer] = useState(null); + const [adventurerForEdit, setAdventurerForEdit] = useState( + null + ); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); const network = useUIStore((state) => state.network); const { account, address } = useNetworkAccount(); @@ -70,7 +88,7 @@ export const AdventurersList = ({ const [currentPage, setCurrentPage] = useState(1); const skip = (currentPage - 1) * adventurersPerPage; - const { refetch, setData } = useQueriesStore(); + const { refetch, setData, data } = useQueriesStore(); const setAdventurer = useAdventurerStore((state) => state.setAdventurer); @@ -89,6 +107,10 @@ export const AdventurersList = ({ "send" | "addToCart" | null >(null); + const [adventurerName, setAdventurerName] = useState(""); + + const [isMaxLength, setIsMaxLength] = useState(false); + const addToCalls = useTransactionCartStore((state) => state.addToCalls); const handleAddTransferTx = (recipient: string, adventurerId: number) => { @@ -177,9 +199,14 @@ export const AdventurersList = ({ owner === "" ); + useEffect(() => { + setData("adventurersByOwnerQuery", adventurersData); + }, [adventurersData]); + const isLoading = adventurersData === undefined; - const adventurers: Adventurer[] = adventurersData?.adventurers ?? []; + const adventurers: Adventurer[] = + data.adventurersByOwnerQuery?.adventurers ?? []; const totalPages = useMemo( () => @@ -258,34 +285,44 @@ export const AdventurersList = ({ setIsTransferOpen(false); }; + const handleNameChange = ( + e: ChangeEvent + ) => { + const value = e.target.value; + setAdventurerName(value.slice(0, 31)); + if (value.length >= 31) { + setIsMaxLength(true); + } else { + setIsMaxLength(false); + } + }; + return (
- {formatAdventurersCount > 0 ? ( -
-
- -

- Adventurers -

- {formatAdventurersCount > 0 && ( - - )} -
-
- {isLoading && ( -
- -
- )} -
- {!isTransferOpen ? ( +
+
+ +

+ Adventurers +

+ +
+
+ {isLoading && ( +
+ +
+ )} +
+ {formatAdventurersCount > 0 ? ( + !isTransferOpen && !isEditOpen ? ( adventurers.map((adventurer, index) => { const birthstamp = parseInt(adventurer.birthDate!); const currentTimestamp = Math.floor(Date.now() / 1000); @@ -313,6 +350,7 @@ export const AdventurersList = ({ setAdventurer(adventurer); handleSwitchAdventurer(adventurer.id!); }} + disabled={dead || expired} > Play @@ -327,6 +365,18 @@ export const AdventurersList = ({ > Transfer +
)} @@ -342,7 +392,6 @@ export const AdventurersList = ({ setSelectedIndex(index); await handleSelectAdventurer(adventurer.id!); }} - disabled={dead || expired} > {expired && !dead && (
@@ -389,7 +438,7 @@ export const AdventurersList = ({
); }) - ) : ( + ) : isTransferOpen ? (
- )} -
- {formatAdventurersCount > 10 && ( -
- - - -
+ ) : ( +
+ +
+ + Change Adventurer Name + +
+
+ + {isMaxLength && ( +

MAX LENGTH!

+ )} +
+ +
+
+
+ ) + ) : ( +

+ You do not have any adventurers! +

)}
-
-
- {isLoading ? ( -
- + {formatAdventurersCount > 10 && ( +
+ + +
- ) : ( - )}
- ) : ( -

- You do not have any adventurers! -

- )} +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} +
+
); }; diff --git a/ui/src/app/containers/AdventurerScreen.tsx b/ui/src/app/containers/AdventurerScreen.tsx index af70e3d42..99eeba7aa 100644 --- a/ui/src/app/containers/AdventurerScreen.tsx +++ b/ui/src/app/containers/AdventurerScreen.tsx @@ -35,6 +35,12 @@ interface AdventurerScreenProps { from: string, recipient: string ) => Promise; + changeAdventurerName: ( + account: AccountInterface, + adventurerId: number, + name: string, + index: number + ) => Promise; } /** @@ -50,6 +56,7 @@ export default function AdventurerScreen({ getBalances, costToPlay, transferAdventurer, + changeAdventurerName, }: AdventurerScreenProps) { const [activeMenu, setActiveMenu] = useState(0); const setAdventurer = useAdventurerStore((state) => state.setAdventurer); @@ -152,6 +159,7 @@ export default function AdventurerScreen({ adventurersCount={adventurersByOwnerCount} aliveAdventurersCount={aliveAdventurersByOwnerCount} transferAdventurer={transferAdventurer} + changeAdventurerName={changeAdventurerName} />
)} diff --git a/ui/src/app/lib/constants.ts b/ui/src/app/lib/constants.ts index 6b00b4a59..c2f7b68c7 100644 --- a/ui/src/app/lib/constants.ts +++ b/ui/src/app/lib/constants.ts @@ -40,9 +40,7 @@ export const getWaitRetryInterval = (network: string) => export const ETH_INCREMENT = 0.001; export const LORDS_INCREMENT = 5; export const getMaxFee = (network: string) => - network === "mainnet" || network === "sepolia" - ? 0.3 * 10 ** 18 - : 0.03 * 10 ** 18; // 0.3ETH on mainnet or sepolia, 0.0003ETH on goerli + network === "mainnet" ? 0.001 * 10 ** 18 : 0.3 * 10 ** 18; // 0.001ETH on mainnet, 0.3ETH on sepolia export const ETH_PREFUND_AMOUNT = (network: string) => network === "mainnet" || network === "sepolia" ? "0x2386F26FC10000" diff --git a/ui/src/app/lib/utils/syscalls.ts b/ui/src/app/lib/utils/syscalls.ts index e45a0d385..9aa827183 100644 --- a/ui/src/app/lib/utils/syscalls.ts +++ b/ui/src/app/lib/utils/syscalls.ts @@ -3,7 +3,7 @@ import { QueryData, QueryKey } from "@/app/hooks/useQueryStore"; import { Network, ScreenPage } from "@/app/hooks/useUIStore"; import { AdventurerClass } from "@/app/lib/classes"; import { checkArcadeConnector } from "@/app/lib/connectors"; -import { getWaitRetryInterval } from "@/app/lib/constants"; +import { getMaxFee, getWaitRetryInterval } from "@/app/lib/constants"; import { GameData } from "@/app/lib/data/GameData"; import { DataType, @@ -1497,7 +1497,7 @@ export function createSyscalls({ } setIsMintingLords(false); - getBalances(); + !onKatana && getBalances(); } catch (e) { setIsMintingLords(false); console.log(e); @@ -1543,9 +1543,19 @@ export function createSyscalls({ ? [transferEthTx, transferLordsTx] : [transferEthTx]; - const { transaction_hash } = await account.execute(calls); + const isArcade = checkArcadeConnector(connector!); - const result = await provider.waitForTransaction(transaction_hash, { + const tx = await handleSubmitCalls( + account!, + [...calls], + isArcade, + Number(ethBalance), + showTopUpDialog, + setTopUpAccount, + network + ); + + const result = await provider.waitForTransaction(tx?.transaction_hash, { retryInterval: getWaitRetryInterval(network!), }); @@ -1576,12 +1586,20 @@ export function createSyscalls({ calldata: [from, recipient, adventurerId.toString() ?? "", "0"], }; - const { transaction_hash } = await account.execute([ - ...calls, - transferTx, - ]); + const isArcade = checkArcadeConnector(connector!); + + const tx = await handleSubmitCalls( + account!, + [...calls, transferTx], + isArcade, + Number(ethBalance), + showTopUpDialog, + setTopUpAccount, + network + ); + setTxHash(tx?.transaction_hash); - const result = await provider.waitForTransaction(transaction_hash, { + const result = await provider.waitForTransaction(tx?.transaction_hash, { retryInterval: getWaitRetryInterval(network!), }); @@ -1593,7 +1611,7 @@ export function createSyscalls({ adventurers: [adventurer], }); - getBalances(); + !onKatana && getBalances(); stopLoading("Transferred Adventurer", false, "Transfer"); } catch (error) { console.error(error); @@ -1601,6 +1619,77 @@ export function createSyscalls({ } }; + const changeAdventurerName = async ( + account: AccountInterface, + adventurerId: number, + name: string, + index: number + ) => { + startLoading( + "Change Name", + "Changing Adventurer Name", + undefined, + undefined + ); + + try { + const changeNameTx = { + contractAddress: gameContract?.address ?? "", + entrypoint: "update_adventurer_name", + calldata: [ + adventurerId.toString() ?? "", + stringToFelt(name).toString(), + ], + }; + + const isArcade = checkArcadeConnector(connector!); + + const maxFee = getMaxFee(network!); + + if (!onKatana && ethBalance < maxFee) { + showTopUpDialog(true); + setTopUpAccount("eth"); + throw new Error("Not enough eth for gas."); + } else { + const tx = await handleSubmitCalls( + account!, + [...calls, changeNameTx], + isArcade, + Number(ethBalance), + showTopUpDialog, + setTopUpAccount, + network + ); + setTxHash(tx?.transaction_hash); + + const result = await provider.waitForTransaction(tx?.transaction_hash, { + retryInterval: getWaitRetryInterval(network!), + }); + + if (!result) { + throw new Error("Transaction did not complete successfully."); + } + + const adventurers = + queryData.adventurersByOwnerQuery?.adventurers ?? []; + + setData("adventurersByOwnerQuery", { + adventurers: [ + ...adventurers.slice(0, index), + { ...adventurers[index], name: name }, + ...adventurers.slice(index + 1), + ], + }); + + !onKatana && getBalances(); + stopLoading("Changed Adventurer Name", false, "Change Name"); + } + } catch (error) { + console.error(error); + stopLoading(error, true); + } + }; + return { spawn, explore, @@ -1611,6 +1700,7 @@ export function createSyscalls({ mintLords, withdraw, transferAdventurer, + changeAdventurerName, }; } diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index 55e34ac8e..d5f605b0f 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -329,6 +329,7 @@ function Home() { mintLords, withdraw, transferAdventurer, + changeAdventurerName, } = useSyscalls({ gameContract: gameContract!, ethContract: ethContract!, @@ -884,6 +885,7 @@ function Home() { getBalances={getBalances} costToPlay={costToPlay} transferAdventurer={transferAdventurer} + changeAdventurerName={changeAdventurerName} /> )} {screen === "play" && (