From 71b08bae464a57ba655bba8b81f795a385dd2ea4 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 1 Jul 2024 22:27:12 -0600 Subject: [PATCH 1/6] Redirect to home if not connected --- packages/client/src/lib/mud/supportedChains.ts | 6 ++++-- packages/client/src/lib/web3/constants.ts | 2 +- packages/client/src/pages/CharacterCreation.tsx | 15 ++++++++++++++- packages/client/src/pages/GameBoard.tsx | 8 +++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/client/src/lib/mud/supportedChains.ts b/packages/client/src/lib/mud/supportedChains.ts index 06b07f5ce..68e2d4e0c 100644 --- a/packages/client/src/lib/mud/supportedChains.ts +++ b/packages/client/src/lib/mud/supportedChains.ts @@ -10,9 +10,9 @@ * */ -import { MUDChain } from '@latticexyz/common/chains'; +import { MUDChain, mudFoundry } from '@latticexyz/common/chains'; -import { DEFAULT_CHAIN_ID, POSSIBLE_SUPPORTED_CHAINS } from '../web3'; +import { DEFAULT_CHAIN_ID } from '../web3'; export const baseSepolia = { name: 'Base Sepolia', @@ -37,6 +37,8 @@ export const baseSepolia = { }, }; +const POSSIBLE_SUPPORTED_CHAINS = [baseSepolia, mudFoundry]; + const getSupportedChains = () => { if (import.meta.env.DEV) { return POSSIBLE_SUPPORTED_CHAINS.filter( diff --git a/packages/client/src/lib/web3/constants.ts b/packages/client/src/lib/web3/constants.ts index 938521094..51ba81ca6 100644 --- a/packages/client/src/lib/web3/constants.ts +++ b/packages/client/src/lib/web3/constants.ts @@ -13,7 +13,7 @@ export const CHAIN_ID_TO_LABEL: { [key: number]: string } = { [baseSepolia.id]: 'Base Sepolia', }; -export const POSSIBLE_SUPPORTED_CHAINS = [baseSepolia, anvil]; +const POSSIBLE_SUPPORTED_CHAINS = [baseSepolia, anvil]; export const DEFAULT_CHAIN_ID = import.meta.env.VITE_CHAIN_ID ? Number(import.meta.env.VITE_CHAIN_ID) diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx index 4b4974527..3ea6cd624 100644 --- a/packages/client/src/pages/CharacterCreation.tsx +++ b/packages/client/src/pages/CharacterCreation.tsx @@ -22,6 +22,7 @@ import { singletonEntity } from '@latticexyz/store-sync/recs'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FaLock } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; +import { useWalletClient } from 'wagmi'; import { useCharacter } from '../contexts/CharacterContext'; import { useMUD } from '../contexts/MUDContext'; @@ -35,6 +36,7 @@ export const CharacterCreation = (): JSX.Element => { const navigate = useNavigate(); const { renderSuccess, renderError } = useToast(); const isSmallScreen = useBreakpointValue({ base: true, lg: false }); + const { data: externalWalletClient } = useWalletClient(); const { burnerBalance, components: { UltimateDominionConfig }, @@ -259,10 +261,21 @@ export const CharacterCreation = (): JSX.Element => { navigate('/game-board'); } + if (!externalWalletClient) { + navigate('/'); + } + if (!delegatorAddress && isSynced) { navigate('/'); } - }, [character, delegatorAddress, isSynced, navigate, rolledOnce]); + }, [ + character, + delegatorAddress, + externalWalletClient, + isSynced, + navigate, + rolledOnce, + ]); return ( { + const { data: externalWalletClient } = useWalletClient(); const navigate = useNavigate(); const { delegatorAddress, isSynced } = useMUD(); const { character } = useCharacter(); useEffect(() => { + if (!externalWalletClient) { + navigate('/'); + } + if (isSynced && !delegatorAddress) { navigate('/'); } @@ -31,7 +37,7 @@ export const GameBoard = (): JSX.Element => { if (character?.locked) { navigate('/game-board'); } - }, [character, delegatorAddress, isSynced, navigate]); + }, [character, delegatorAddress, externalWalletClient, isSynced, navigate]); return ( Date: Mon, 1 Jul 2024 22:34:42 -0600 Subject: [PATCH 2/6] Fix character creation image flicker --- .../client/src/pages/CharacterCreation.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/client/src/pages/CharacterCreation.tsx b/packages/client/src/pages/CharacterCreation.tsx index 3ea6cd624..67368d7b4 100644 --- a/packages/client/src/pages/CharacterCreation.tsx +++ b/packages/client/src/pages/CharacterCreation.tsx @@ -277,6 +277,17 @@ export const CharacterCreation = (): JSX.Element => { rolledOnce, ]); + const UploadedAvatar = useMemo(() => { + return ( +
+ +
+ ); + }, [avatar]); + return ( { gap={{ base: 4, sm: 8 }} w="100%" > -
- -
+ {UploadedAvatar} Date: Mon, 1 Jul 2024 22:41:19 -0600 Subject: [PATCH 3/6] Prevent session wallet funding if low balance --- .../src/components/WalletDetailsModal.tsx | 30 +++++++++++++------ .../client/src/pages/CharacterCreation.tsx | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/client/src/components/WalletDetailsModal.tsx b/packages/client/src/components/WalletDetailsModal.tsx index 1b752fbd5..4efe25f53 100644 --- a/packages/client/src/components/WalletDetailsModal.tsx +++ b/packages/client/src/components/WalletDetailsModal.tsx @@ -43,23 +43,28 @@ export const WalletDetailsModal = ({ const [amount, setAmount] = useState('0'); const [isDepositing, setIsDepositing] = useState(false); - const [showError, setShowError] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); - // Reset showError state when any of the form fields change + // Reset errorMessage state when any of the form fields change useEffect(() => { - setShowError(false); + setErrorMessage(null); }, [amount]); const onDeposit = useCallback(async () => { try { setIsDepositing(true); - if (!externalWalletClient) { + if (!(externalWalletBalance && externalWalletClient)) { throw new Error('No external wallet client found'); } if (!amount || parseEther(amount) <= 0) { - setShowError(true); + setErrorMessage('Amount must be greater than 0'); + return; + } + + if (parseEther(amount) > externalWalletBalance.value) { + setErrorMessage('Insufficient funds in external wallet'); return; } @@ -75,7 +80,14 @@ export const WalletDetailsModal = ({ } finally { setIsDepositing(false); } - }, [amount, burnerAddress, externalWalletClient, renderError, renderSuccess]); + }, [ + amount, + burnerAddress, + externalWalletBalance, + externalWalletClient, + renderError, + renderSuccess, + ]); return ( @@ -120,13 +132,13 @@ export const WalletDetailsModal = ({ time. - + Deposit to session wallet - {showError && ( + {!!errorMessage && ( - Amount must be greater than 0 + {errorMessage} )} { {character.name} - {character.description} + {character.description} Class:{' '} From 88fb0051aa7b3e91391019d87a36e5e335c60b00 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 1 Jul 2024 22:50:36 -0600 Subject: [PATCH 4/6] Add a withdraw from session wallet flow --- .../src/components/WalletDetailsModal.tsx | 109 +++++++++++++++--- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/packages/client/src/components/WalletDetailsModal.tsx b/packages/client/src/components/WalletDetailsModal.tsx index 4efe25f53..c9635cea3 100644 --- a/packages/client/src/components/WalletDetailsModal.tsx +++ b/packages/client/src/components/WalletDetailsModal.tsx @@ -36,19 +36,32 @@ export const WalletDetailsModal = ({ const { renderSuccess, renderError } = useToast(); const { data: externalWalletClient } = useWalletClient(); const { isConnected, address } = useAccount(); - const { burnerAddress, burnerBalance } = useMUD(); + const { + burnerAddress, + burnerBalance, + network: { walletClient }, + } = useMUD(); const { data: externalWalletBalance } = useBalance({ address: externalWalletClient?.account.address, }); - const [amount, setAmount] = useState('0'); + const [depositAmount, setDepositAmount] = useState('0'); const [isDepositing, setIsDepositing] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); + const [depositErrorMessage, setDepositErrorMessage] = useState( + null, + ); + + const [withdrawAmount, setWithdrawAmount] = useState('0'); + const [isWithdrawing, setIsWithdrawing] = useState(false); + const [withdrawErrorMessage, setWithdrawErrorMessage] = useState< + string | null + >(null); // Reset errorMessage state when any of the form fields change useEffect(() => { - setErrorMessage(null); - }, [amount]); + setDepositErrorMessage(null); + setWithdrawErrorMessage(null); + }, [depositAmount, withdrawAmount]); const onDeposit = useCallback(async () => { try { @@ -58,22 +71,22 @@ export const WalletDetailsModal = ({ throw new Error('No external wallet client found'); } - if (!amount || parseEther(amount) <= 0) { - setErrorMessage('Amount must be greater than 0'); + if (!depositAmount || parseEther(depositAmount) <= 0) { + setDepositErrorMessage('Amount must be greater than 0'); return; } - if (parseEther(amount) > externalWalletBalance.value) { - setErrorMessage('Insufficient funds in external wallet'); + if (parseEther(depositAmount) > externalWalletBalance.value) { + setDepositErrorMessage('Insufficient funds in external wallet'); return; } await externalWalletClient.sendTransaction({ to: burnerAddress, - value: parseEther(amount), + value: parseEther(depositAmount), }); - setAmount('0'); + setDepositAmount('0'); renderSuccess('Funds deposited successfully!'); } catch (error) { renderError(error, 'Error depositing funds'); @@ -81,14 +94,49 @@ export const WalletDetailsModal = ({ setIsDepositing(false); } }, [ - amount, burnerAddress, + depositAmount, externalWalletBalance, externalWalletClient, renderError, renderSuccess, ]); + const onWithdraw = useCallback(async () => { + try { + setIsWithdrawing(true); + + if (!withdrawAmount || parseEther(withdrawAmount) <= 0) { + setWithdrawErrorMessage('Amount must be greater than 0'); + return; + } + + if (parseEther(withdrawAmount) > parseEther(burnerBalance)) { + setWithdrawErrorMessage('Insufficient funds in session wallet'); + return; + } + + await walletClient.sendTransaction({ + to: address, + value: parseEther(withdrawAmount), + }); + + setWithdrawAmount('0'); + renderSuccess('Funds withdrawn successfully!'); + } catch (error) { + renderError(error, 'Error withdrawing funds'); + } finally { + setIsWithdrawing(false); + } + }, [ + address, + burnerBalance, + renderError, + renderSuccess, + walletClient, + withdrawAmount, + ]); + return ( @@ -132,21 +180,21 @@ export const WalletDetailsModal = ({ time. - + Deposit to session wallet - {!!errorMessage && ( + {!!depositErrorMessage && ( - {errorMessage} + {depositErrorMessage} )} setAmount(e.target.value)} + onChange={e => setDepositAmount(e.target.value)} placeholder="Amount" type="number" - value={amount} + value={depositAmount} /> + ) : ( From 7cb1ca44849af495134e2494140ae8554a630016 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 1 Jul 2024 23:56:39 -0600 Subject: [PATCH 5/6] Fix stats updating issue --- .../client/src/contexts/CharacterContext.tsx | 51 +++++++++++-------- .../client/src/pages/CharacterCreation.tsx | 15 +++--- packages/client/src/utils/types.ts | 11 ++-- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/packages/client/src/contexts/CharacterContext.tsx b/packages/client/src/contexts/CharacterContext.tsx index 83713cf6c..6cc00824e 100644 --- a/packages/client/src/contexts/CharacterContext.tsx +++ b/packages/client/src/contexts/CharacterContext.tsx @@ -1,10 +1,6 @@ -import { - getComponentValue, - getComponentValueStrict, - HasValue, - runQuery, -} from '@latticexyz/recs'; -import { decodeEntity } from '@latticexyz/store-sync/recs'; +import { useComponentValue } from '@latticexyz/react'; +import { getComponentValueStrict, HasValue, runQuery } from '@latticexyz/recs'; +import { decodeEntity, encodeEntity } from '@latticexyz/store-sync/recs'; import { createContext, ReactNode, @@ -17,17 +13,25 @@ import { formatEther, getContract, hexToString } from 'viem'; import { useToast } from '../hooks/useToast'; import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; -import type { Character } from '../utils/types'; +import type { Character, CharacterStats } from '../utils/types'; import { useMUD } from './MUDContext'; type CharacterContextType = { character: Character | null; + characterStats: CharacterStats; isRefreshing: boolean; refreshCharacter: () => void; }; const CharacterContext = createContext({ character: null, + characterStats: { + agility: '0', + experience: '0', + hitPoints: '0', + intelligence: '0', + strength: '0', + }, isRefreshing: false, refreshCharacter: () => {}, }); @@ -49,6 +53,16 @@ export const CharacterProvider = ({ const [characterDetails, setCharacterDetails] = useState( null, ); + const characterStats = useComponentValue( + CharacterStats, + characterDetails + ? encodeEntity( + { characterId: 'uint256' }, + { characterId: BigInt(characterDetails.characterId) }, + ) + : undefined, + ); + const [isRefreshing, setIsRefreshing] = useState(false); const getCharacterData = useCallback(async () => { @@ -61,22 +75,16 @@ export const CharacterProvider = ({ ]), ).map(entity => { const characterData = getComponentValueStrict(Characters, entity); - const characterStats = getComponentValue(CharacterStats, entity); return { - agility: characterStats?.agility.toString() ?? '0', - experience: characterStats?.experience.toString() ?? '0', characterClass: characterData.class, characterId: decodeEntity( { characterId: 'uint256' }, entity, ).characterId.toString(), - hitPoints: characterStats?.hitPoints.toString() ?? '0', - intelligence: characterStats?.intelligence.toString() ?? '0', locked: characterData.locked, name: hexToString(characterData.name as `0x${string}`, { size: 32 }), owner: characterData.owner, - strength: characterStats?.strength.toString() ?? '0', }; })[0]; @@ -154,13 +162,7 @@ export const CharacterProvider = ({ ...fetachedMetadata, goldBalance: formatEther(BigInt(goldBalance)).toString(), }); - }, [ - Characters, - CharacterStats, - delegatorAddress, - publicClient, - worldContract, - ]); + }, [Characters, delegatorAddress, publicClient, worldContract]); const refreshCharacter = useCallback(async () => { setIsRefreshing(true); @@ -182,6 +184,13 @@ export const CharacterProvider = ({ { isSynced, systemCalls: { enterGame, mintCharacter, rollStats }, } = useMUD(); - const { character, isRefreshing, refreshCharacter } = useCharacter(); + const { character, characterStats, isRefreshing, refreshCharacter } = + useCharacter(); const { file: avatar, setFile: setAvatar, @@ -217,8 +218,8 @@ export const CharacterCreation = (): JSX.Element => { ]); const rolledOnce = useMemo(() => { - return character?.hitPoints !== '0'; - }, [character]); + return characterStats.hitPoints !== '0'; + }, [characterStats]); const onEnterGame = useCallback(async () => { try { @@ -501,19 +502,19 @@ export const CharacterCreation = (): JSX.Element => { HP - Hit - {character?.hitPoints ?? '0'} + {characterStats.hitPoints ?? '0'} STR - Strength - {character?.strength ?? '0'} + {characterStats.strength ?? '0'} AGI - Agility - {character?.agility ?? '0'} + {characterStats.agility ?? '0'} INT - Intelligence - {character?.intelligence ?? '0'} + {characterStats.intelligence ?? '0'} diff --git a/packages/client/src/utils/types.ts b/packages/client/src/utils/types.ts index fb03f7a29..a89c6b644 100644 --- a/packages/client/src/utils/types.ts +++ b/packages/client/src/utils/types.ts @@ -1,13 +1,16 @@ export type Character = Metadata & { - agility: string; - experience: string; characterClass: CharacterClasses; characterId: string; goldBalance: string; - hitPoints: string; - intelligence: string; locked: boolean; owner: string; +}; + +export type CharacterStats = { + agility: string; + experience: string; + hitPoints: string; + intelligence: string; strength: string; }; From 097dd4db5d7d3e99b43bebbc45bfa445355fea69 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 2 Jul 2024 00:01:29 -0600 Subject: [PATCH 6/6] Disable navigation until previous move completes --- packages/client/src/components/MapPanel.tsx | 13 ++++++++++++- packages/client/src/components/StatsPanel.tsx | 19 ++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/MapPanel.tsx b/packages/client/src/components/MapPanel.tsx index 97245979e..296b19973 100644 --- a/packages/client/src/components/MapPanel.tsx +++ b/packages/client/src/components/MapPanel.tsx @@ -31,6 +31,7 @@ export const MapPanel = (): JSX.Element => { const { character } = useCharacter(); const [isSpawning, setIsSpawning] = useState(false); + const [isMoving, setIsMoving] = useState(false); const position = useComponentValue( Position, @@ -90,6 +91,8 @@ export const MapPanel = (): JSX.Element => { const onMove = useCallback( async (direction: 'up' | 'down' | 'left' | 'right') => { try { + setIsMoving(true); + if (!delegatorAddress) { throw new Error('Burner not found'); } @@ -140,6 +143,8 @@ export const MapPanel = (): JSX.Element => { } } catch (e) { renderError(e, 'Failed to move.'); + } finally { + setIsMoving(false); } }, [character, delegatorAddress, move, position, renderError], @@ -207,7 +212,7 @@ export const MapPanel = (): JSX.Element => {
{isSpawned ? ( - + ) : (