diff --git a/packages/client/src/components/AuctionAllowance.tsx b/packages/client/src/components/AuctionAllowanceModal.tsx similarity index 65% rename from packages/client/src/components/AuctionAllowance.tsx rename to packages/client/src/components/AuctionAllowanceModal.tsx index 727e732e6..c4836271a 100644 --- a/packages/client/src/components/AuctionAllowance.tsx +++ b/packages/client/src/components/AuctionAllowanceModal.tsx @@ -12,22 +12,20 @@ import { ModalFooter, ModalHeader, ModalOverlay, - Skeleton, - Switch, Text, VStack, } from '@chakra-ui/react'; import { useComponentValue } from '@latticexyz/react'; import { singletonEntity } from '@latticexyz/store-sync/recs'; import { useCallback, useEffect, useState } from 'react'; -import { Address, erc20Abi, parseEther } from 'viem'; -import { useAccount, useBalance, useWalletClient } from 'wagmi'; +import { Address, erc20Abi, formatEther, parseEther } from 'viem'; +import { useAccount, useWalletClient } from 'wagmi'; import { useMUD } from '../contexts/MUDContext'; import { useToast } from '../hooks/useToast'; -import { ERC_1155ABI } from '../utils/constants'; +import { ERC_1155_ABI } from '../utils/constants'; import { ConnectWalletButton } from './ConnectWalletButton'; -export const AuctionAllowance = ({ +export const AuctionAllowanceModal = ({ isOpen, onClose, }: { @@ -38,12 +36,10 @@ export const AuctionAllowance = ({ const { data: externalWalletClient } = useWalletClient(); const { isConnected, address } = useAccount(); const { - network: { walletClient, worldContract, publicClient }, + network: { publicClient }, components: { UltimateDominionConfig }, } = useMUD(); - useBalance({ - address: externalWalletClient?.account.address, - }); + const { goldToken } = useComponentValue( UltimateDominionConfig, singletonEntity, @@ -54,16 +50,18 @@ export const AuctionAllowance = ({ singletonEntity, ) ?? { items: null }; - const [goldAllowance, setGoldAllowance] = useState('100'); + const [goldAllowance, setGoldAllowance] = useState('0'); const [isApprovingGold, setIsApprovingGold] = useState(false); const [goldErrorMessage, setGoldErrorMessage] = useState(null); - const [itemsApprovedInitial, setItemsApprovedInitial] = useState< - boolean | null - >(null); - const [itemAllowed, setItemAllowed] = useState(false); + const [itemsApproved, setItemsApproved] = useState(false); const [isApprovingItems, setIsApprovingItems] = useState(false); + const { auctionHouse: auctionHouseAddress } = useComponentValue( + UltimateDominionConfig, + singletonEntity, + ) ?? { auctionHouse: null }; + // Reset errorMessage state when any of the form fields change useEffect(() => { setGoldErrorMessage(null); @@ -71,52 +69,71 @@ export const AuctionAllowance = ({ useEffect(() => { if (isOpen) { - setGoldAllowance('100'); - if (externalWalletClient && itemsApprovedInitial == null) { - (async function () { - const auction = await worldContract.read.UD__auctionHouseAddress(); - const t = await publicClient.readContract({ + if (auctionHouseAddress && externalWalletClient) { + (async () => { + const _itemsApproved = !!(await publicClient.readContract({ address: itemsContract as Address, - abi: ERC_1155ABI, + abi: ERC_1155_ABI, functionName: 'isApprovedForAll', - args: [externalWalletClient.account.address, auction as Address], + args: [externalWalletClient.account.address, auctionHouseAddress], + })) as boolean; + + const _goldAllowance = await publicClient.readContract({ + address: goldToken as Address, + abi: erc20Abi, + functionName: 'allowance', + args: [ + externalWalletClient.account.address, + auctionHouseAddress as Address, + ], }); - setItemAllowed(t as boolean); - setItemsApprovedInitial(true); + + setItemsApproved(_itemsApproved); + setGoldAllowance(formatEther(_goldAllowance)); })(); } } }, [ + auctionHouseAddress, externalWalletClient, + goldToken, isOpen, - itemsApprovedInitial, itemsContract, publicClient, - walletClient.account, - worldContract.read, ]); - const onGoldAllowance = useCallback(async () => { + const onApproveGoldAllowance = useCallback(async () => { try { + setIsApprovingGold(true); + if (!externalWalletClient) { throw new Error('No external wallet client found.'); } - setIsApprovingGold(true); + if (!auctionHouseAddress) { + throw new Error('No Auction House address found.'); + } + if (!goldAllowance || parseEther(goldAllowance) <= 0) { setGoldErrorMessage('Amount must be greater than 0.'); return; } - const auction = await worldContract.read.UD__auctionHouseAddress(); - const { request } = await publicClient.simulateContract({ address: goldToken as Address, abi: erc20Abi, functionName: 'approve', - args: [auction, parseEther(goldAllowance)], + args: [auctionHouseAddress as Address, parseEther(goldAllowance)], + }); + + const txHash = await externalWalletClient.writeContract(request); + const { status } = await publicClient.waitForTransactionReceipt({ + hash: txHash, }); - await externalWalletClient.writeContract(request); + + if (status !== 'success') { + throw new Error('Transaction failed.'); + } setGoldAllowance(goldAllowance); renderSuccess('Gold allowance successfully set!'); @@ -126,46 +143,60 @@ export const AuctionAllowance = ({ setIsApprovingGold(false); } }, [ + auctionHouseAddress, externalWalletClient, goldAllowance, goldToken, publicClient, renderError, renderSuccess, - worldContract.read, ]); - const onItemsApproved = useCallback(async () => { + + const onSetApprovalForAllItems = useCallback(async () => { try { + setIsApprovingItems(true); + if (!externalWalletClient) { throw new Error('No external wallet client found.'); } - setIsApprovingItems(true); - const auction = await worldContract.read.UD__auctionHouseAddress(); + if (!auctionHouseAddress) { + throw new Error('No Auction House address found.'); + } const { request } = await publicClient.simulateContract({ address: itemsContract as Address, - abi: ERC_1155ABI, + abi: ERC_1155_ABI, functionName: 'setApprovalForAll', - args: [auction as Address, !itemAllowed], + args: [auctionHouseAddress, !itemsApproved], }); - await externalWalletClient.writeContract(request); - setItemAllowed(!itemAllowed); - setIsApprovingItems(false); - renderSuccess('Item allowance successfully set!'); + + const txHash = await externalWalletClient.writeContract(request); + const { status } = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + if (status !== 'success') { + throw new Error('Transaction failed.'); + } + + setItemsApproved(!itemsApproved); + renderSuccess( + `Item allowance successfully ${itemsApproved ? 'disallowed' : 'allowed'}!`, + ); } catch (e) { renderError((e as Error)?.message ?? 'Error setting item allowance.', e); } finally { setIsApprovingItems(false); } }, [ + auctionHouseAddress, externalWalletClient, - itemAllowed, + itemsApproved, itemsContract, publicClient, renderError, renderSuccess, - worldContract.read, ]); return ( @@ -201,7 +232,7 @@ export const AuctionAllowance = ({ diff --git a/packages/client/src/components/AuctionRow.tsx b/packages/client/src/components/AuctionRow.tsx index 356de0502..a304eefe9 100644 --- a/packages/client/src/components/AuctionRow.tsx +++ b/packages/client/src/components/AuctionRow.tsx @@ -11,7 +11,7 @@ import { IoIosArrowForward } from 'react-icons/io'; import { useNavigate } from 'react-router-dom'; import { ITEM_PATH } from '../Routes'; -import { removeEmoji } from '../utils/helpers'; +import { getEmoji, removeEmoji } from '../utils/helpers'; import { type ArmorTemplate, ItemType, @@ -20,14 +20,13 @@ import { } from '../utils/types'; export const AuctionRow = ({ - name, + floor, + itemType, minLevel, - emoji, + name, tokenId, - floor, ...item }: (ArmorTemplate | SpellTemplate | WeaponTemplate) & { - emoji: string; floor: string; }): JSX.Element => { const navigate = useNavigate(); @@ -59,13 +58,13 @@ export const AuctionRow = ({ name={' '} backgroundColor={'grey300'} > - {emoji} + {getEmoji(name)} {removeEmoji(name)} - {item.itemType !== ItemType.Spell && ( + {itemType !== ItemType.Spell && ( HP {(item as ArmorTemplate | WeaponTemplate).hpModifier} • STR{' '} {(item as ArmorTemplate | WeaponTemplate).strModifier} • AGI{' '} @@ -77,18 +76,6 @@ export const AuctionRow = ({ - {/* -
- {itemClass == '0' && } - {itemClass == '1' && } - {itemClass == '2' && } -
-
*/} { - const { data: externalWalletClient } = useWalletClient(); +type OrderRowProps = { + item: ArmorTemplate | WeaponTemplate | SpellTemplate; + order: Order; + refetchOrders: () => void; +}; +export const OrderRow = ({ + item, + order, + refetchOrders, +}: OrderRowProps): JSX.Element => { const { - network: { publicClient, worldContract }, + systemCalls: { cancelOrder, fulfillOrder }, } = useMUD(); const { renderSuccess, renderError } = useToast(); const [isCancelling, setIsCancelling] = useState(false); const [isFilling, setIsFilling] = useState(false); - const cancelOrder = async function () { - if (!externalWalletClient) { - renderError('Wallet not connected.'); - return; - } + const onCancelOrder = useCallback(async () => { try { setIsCancelling(true); - const { request } = await publicClient.simulateContract({ - address: worldContract.address, - abi: worldAbi, - functionName: 'UD__cancelOrder', - args: [orderHash as Address], - account: externalWalletClient.account, - }); - await externalWalletClient.writeContract(request); + + const { error, success } = await cancelOrder(order.orderHash); + + if (error && !success) { + throw new Error(error); + } + renderSuccess('Order canceled successfully!'); + refetchOrders(); } catch (e) { renderError((e as Error)?.message ?? 'Error cancelling order.', e); } finally { setIsCancelling(false); } - }; + }, [cancelOrder, order, refetchOrders, renderError, renderSuccess]); - const fillOrder = async function () { - if (!externalWalletClient) { - renderError('Wallet not connected.'); - return; - } + const onFulfillOrder = useCallback(async () => { try { setIsFilling(true); - const { request } = await publicClient.simulateContract({ - address: worldContract.address, - abi: worldAbi, - functionName: 'UD__fulfillOrder', - args: [orderHash as Address], - account: externalWalletClient.account, - }); - await externalWalletClient?.writeContract(request); + + const { error, success } = await fulfillOrder(order.orderHash); + + if (error && !success) { + throw new Error(error); + } + renderSuccess('Order filled successfully!'); + refetchOrders(); } catch (e) { renderError((e as Error)?.message ?? 'Error cancelling order.', e); } finally { setIsFilling(false); } - }; + }, [fulfillOrder, order, refetchOrders, renderError, renderSuccess]); + + const { consideration, offer } = order; + return ( - {emoji} + {getEmoji(item.name)} - From: {from} - {/*
- {entityClass == StatsClasses.Warrior && } - {entityClass == StatsClasses.Rogue && } - {entityClass == StatsClasses.Mage && } -
*/} + + From: {consideration.recipient} +
- Wants {consideration} {considerationItem.trim()} for {offer}{' '} - {offerItem.trim()} + Wants {consideration.amount}{' '} + {consideration.tokenType === TokenType.ERC20 + ? '$GOLD' + : removeEmoji(item.name)}{' '} + for {offer.amount}{' '} + {offer.tokenType === TokenType.ERC20 + ? '$GOLD' + : removeEmoji(item.name)}
@@ -130,7 +124,7 @@ export const OrderRow = ({ size="sm" variant="solid" isLoading={isFilling} - onClick={() => fillOrder()} + onClick={onFulfillOrder} > {' '} @@ -142,7 +136,7 @@ export const OrderRow = ({ backgroundColor="red" color="white" isLoading={isCancelling} - onClick={() => cancelOrder()} + onClick={onCancelOrder} > diff --git a/packages/client/src/components/StatsPanel.tsx b/packages/client/src/components/StatsPanel.tsx index 7d91c7ee5..8a3a2c767 100644 --- a/packages/client/src/components/StatsPanel.tsx +++ b/packages/client/src/components/StatsPanel.tsx @@ -239,39 +239,37 @@ export const StatsPanel = (): JSX.Element => {
{isDesktop && ( - <> - - - Auction House - - - Leaderboard - - - + + + Auction House + + + Leaderboard + + )}
); diff --git a/packages/client/src/components/WalletDetailsModal.tsx b/packages/client/src/components/WalletDetailsModal.tsx index 9e56d1512..9f476bb37 100644 --- a/packages/client/src/components/WalletDetailsModal.tsx +++ b/packages/client/src/components/WalletDetailsModal.tsx @@ -26,7 +26,7 @@ import { useAccount, useBalance, useWalletClient } from 'wagmi'; import { useMUD } from '../contexts/MUDContext'; import { useToast } from '../hooks/useToast'; -import { ERC_1155ABI } from '../utils/constants'; +import { ERC_1155_ABI } from '../utils/constants'; import { shortenAddress } from '../utils/helpers'; import { ConnectWalletButton } from './ConnectWalletButton'; import { CopyText } from './CopyText'; @@ -101,7 +101,7 @@ export const WalletDetailsModal = ({ const auction = await worldContract.read.UD__auctionHouseAddress(); const t = await publicClient.readContract({ address: itemsContract as Address, - abi: ERC_1155ABI, + abi: ERC_1155_ABI, functionName: 'isApprovedForAll', args: [externalWalletClient.account.address, auction as Address], }); @@ -245,7 +245,7 @@ export const WalletDetailsModal = ({ const { request } = await publicClient.simulateContract({ address: itemsContract as Address, - abi: ERC_1155ABI, + abi: ERC_1155_ABI, functionName: 'setApprovalForAll', args: [auction as Address, !itemAllowed], }); diff --git a/packages/client/src/lib/mud/createSystemCalls.ts b/packages/client/src/lib/mud/createSystemCalls.ts index 573ceb4b4..6cc4209cc 100644 --- a/packages/client/src/lib/mud/createSystemCalls.ts +++ b/packages/client/src/lib/mud/createSystemCalls.ts @@ -20,6 +20,7 @@ import { Address, BaseError, ContractFunctionRevertedError, + Hash, InsufficientFundsError, keccak256, stringToHex, @@ -27,7 +28,13 @@ import { } from 'viem'; import { INSUFFICIENT_FUNDS_MESSAGE } from '../../utils/errors'; -import { EncounterType, EntityStats, StatsClasses } from '../../utils/types'; +import { + EncounterType, + type EntityStats, + type NewOrder, + OrderStatus, + StatsClasses, +} from '../../utils/types'; import { ClientComponents } from './createClientComponents'; import { SetupNetworkResult } from './setupNetwork'; @@ -44,7 +51,7 @@ const getContractError = (error: BaseError): string => { ); if (revertError instanceof ContractFunctionRevertedError) { const args = revertError.data?.args ?? []; - return args[0] as string; + return (args[0] as string) ?? 'An error occurred calling the contract.'; } const insufficientFundsError = error.walk( e => e instanceof InsufficientFundsError, @@ -87,11 +94,47 @@ export function createSystemCalls( Characters, CharactersTokenURI, CombatEncounter, + Orders, Position, Spawned, Stats, }: ClientComponents, ) { + const cancelOrder = async (orderHash: string): SystemCallReturn => { + try { + await publicClient.simulateContract({ + abi: worldContract.abi, + account: delegatorAddress, + address: worldContract.address, + args: [orderHash as Hash], + functionName: 'UD__cancelOrder', + }); + + const tx = await worldContract.write.UD__cancelOrder([orderHash as Hash]); + + await waitForTransaction(tx); + + const success = + getComponentValue( + Orders, + encodeEntity( + { orderHash: 'bytes32' }, + { orderHash: orderHash as Hash }, + ), + )?.orderStatus === OrderStatus.Canceled; + + return { + error: success ? undefined : 'Failed to cancel order.', + success, + }; + } catch (e) { + return { + error: getContractError(e as BaseError), + success: false, + }; + } + }; + const createEncounter = async ( encounterType: EncounterType, attackers: string[], @@ -150,6 +193,38 @@ export function createSystemCalls( } }; + const createOrder = async (order: NewOrder): SystemCallReturn => { + try { + const simulatedTx = await publicClient.simulateContract({ + abi: worldContract.abi, + account: delegatorAddress, + address: worldContract.address, + args: [order], + functionName: 'UD__createOrder', + }); + + const orderHash = simulatedTx.result; + + const tx = await worldContract.write.UD__createOrder([order]); + await waitForTransaction(tx); + + const success = !!getComponentValue( + Orders, + encodeEntity({ orderHash: 'bytes32' }, { orderHash: orderHash }), + ); + + return { + error: success ? undefined : 'Failed to create order.', + success, + }; + } catch (e) { + return { + error: getContractError(e as BaseError), + success: false, + }; + } + }; + const endTurn = async ( encounterId: Entity, playerId: Entity, @@ -291,6 +366,43 @@ export function createSystemCalls( } }; + const fulfillOrder = async (orderHash: string): SystemCallReturn => { + try { + await publicClient.simulateContract({ + abi: worldContract.abi, + account: delegatorAddress, + address: worldContract.address, + args: [orderHash as Hash], + functionName: 'UD__fulfillOrder', + }); + + const tx = await worldContract.write.UD__fulfillOrder([ + orderHash as Hash, + ]); + + await waitForTransaction(tx); + + const success = + getComponentValue( + Orders, + encodeEntity( + { orderHash: 'bytes32' }, + { orderHash: orderHash as Hash }, + ), + )?.orderStatus === OrderStatus.Fulfilled; + + return { + error: success ? undefined : 'Failed to fulfill order.', + success, + }; + } catch (e) { + return { + error: getContractError(e as BaseError), + success: false, + }; + } + }; + const levelCharacter = async ( characterId: Entity, entityStats: Omit & { @@ -629,10 +741,13 @@ export function createSystemCalls( // }; return { + cancelOrder, createEncounter, + createOrder, endTurn, enterGame, equipItems, + fulfillOrder, levelCharacter, mintCharacter, move, diff --git a/packages/client/src/pages/AuctionHouse.tsx b/packages/client/src/pages/AuctionHouse.tsx index 90489c85a..890423a2c 100644 --- a/packages/client/src/pages/AuctionHouse.tsx +++ b/packages/client/src/pages/AuctionHouse.tsx @@ -13,7 +13,12 @@ import { VStack, } from '@chakra-ui/react'; import { useComponentValue } from '@latticexyz/react'; -import { Has, runQuery } from '@latticexyz/recs'; +import { + getComponentValueStrict, + Has, + HasValue, + runQuery, +} from '@latticexyz/recs'; import { singletonEntity } from '@latticexyz/store-sync/recs'; import FuzzySearch from 'fuzzy-search'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -21,7 +26,7 @@ import { FaSearch, FaSortAmountDown, FaSortAmountUp } from 'react-icons/fa'; import { FaBackwardStep, FaForwardStep } from 'react-icons/fa6'; import { IoCaretBack, IoCaretForward } from 'react-icons/io5'; import { useNavigate } from 'react-router-dom'; -import { Address, maxUint256 } from 'viem'; +import { formatEther } from 'viem'; import { useAccount } from 'wagmi'; import { AuctionRow } from '../components/AuctionRow'; @@ -29,23 +34,31 @@ import { useItems } from '../contexts/ItemsContext'; import { useMUD } from '../contexts/MUDContext'; import { useToast } from '../hooks/useToast'; import { HOME_PATH } from '../Routes'; -import { getEmoji } from '../utils/helpers'; import { type ArmorTemplate, type ConsiderationData, ItemType, type OfferData, type Order, + OrderStatus, type SpellTemplate, + TokenType, type WeaponTemplate, } from '../utils/types'; -interface Price { - tokenId: string; - floor: string; - ceiling: string; +enum FilterOptions { + All = 'all', + Armor = 'armor', + Spell = 'spell', + Weapon = 'weapon', +} + +enum SortOptions { + Level = 'Level', + FloorPrice = 'Floor Price', } -const PER_PAGE = 10; + +const ITEMS_PER_PAGE = 10; export const AuctionHouse = (): JSX.Element => { const { renderError } = useToast(); @@ -53,9 +66,7 @@ export const AuctionHouse = (): JSX.Element => { const { isConnected } = useAccount(); const { - components: { UltimateDominionConfig, Offers }, - - network: { worldContract }, + components: { Considerations, Offers, Orders, UltimateDominionConfig }, } = useMUD(); const { armorTemplates, @@ -65,16 +76,19 @@ export const AuctionHouse = (): JSX.Element => { } = useItems(); const [isFetchingOrders, setIsFetchingOrders] = useState(false); - const [prices, setPrices] = useState(null); + const [activeOrders, setActiveOrders] = useState([]); - const [entries, setEntries] = useState< + const [items, setItems] = useState< (ArmorTemplate | SpellTemplate | WeaponTemplate)[] >([]); - const [, setOrders] = useState(null); - const [sort, setSort] = useState({ sorted: 'byLevel', reversed: false }); - const [filter, setFilter] = useState({ filtered: 'all' }); + const [sort, setSort] = useState({ + reversed: false, + sorted: SortOptions.Level, + }); + const [filter, setFilter] = useState(FilterOptions.All); const [query, setQuery] = useState(''); + const [page, setPage] = useState('1'); const [pageLimit, setPageLimit] = useState(0); @@ -83,77 +97,82 @@ export const AuctionHouse = (): JSX.Element => { singletonEntity, ) ?? { goldToken: null }; - const fetchOrders = useCallback(async () => { + const fetchOrders = useCallback(() => { try { setIsFetchingOrders(true); - setPrices(null); - setOrders( - await Promise.all( - Array.from(runQuery([Has(Offers)])).map(async orderHash => { - const offerData = await worldContract.read.UD__getOffer([ - orderHash as Address, - ]); - const considerationData = - await worldContract.read.UD__getConsideration([ - orderHash as Address, - ]); - const orderStatus = await worldContract.read.UD__getOrderStatus([ - orderHash as Address, - ]); - const price = { - tokenId: - considerationData.token == goldToken - ? offerData.identifier.toString() - : considerationData.identifier.toString(), - floor: '0', - ceiling: maxUint256.toString(), - } as Price; - if (considerationData.token == goldToken && orderStatus == 1) { - price.floor = BigInt( - considerationData.amount / offerData.amount < - BigInt(price.floor) - ? considerationData.amount / offerData.amount - : BigInt(price.floor), - ).toString(); - } - if (offerData.token == goldToken && orderStatus == 1) { - price.ceiling = BigInt( - offerData.amount / considerationData.amount > - BigInt(price.ceiling) - ? offerData.amount / considerationData.amount - : BigInt(price.ceiling), - ).toString(); - } - if (price != null) setPrices([price]); - else { - setPrices(prev => [...(prev as []), price]); - } - return { - orderHash: orderHash.toString(), - orderStatus: orderStatus.toString(), - offer: { - amount: offerData.amount.toString(), - identifier: offerData.identifier.toString(), - token: offerData.token.toString(), - tokenType: offerData.tokenType.toString(), - } as OfferData, - consideration: { - amount: considerationData.amount.toString(), - identifier: considerationData.identifier.toString(), - token: considerationData.token.toString(), - tokenType: considerationData.tokenType.toString(), - recipient: considerationData.recipient.toString(), - } as ConsiderationData, - } as Order; - }), - ), - ); - } catch (err) { - renderError('Could not get order data.'); + + const _activeOrders = Array.from( + runQuery([ + Has(Considerations), + Has(Offers), + Has(Orders), + HasValue(Orders, { orderStatus: OrderStatus.Active }), + ]), + ).map(orderHash => { + const considerationData = getComponentValueStrict( + Considerations, + orderHash, + ); + const offerData = getComponentValueStrict(Offers, orderHash); + const orderStatus = getComponentValueStrict( + Orders, + orderHash, + ).orderStatus; + + return { + orderHash: orderHash.toString(), + orderStatus: orderStatus.toString(), + offer: { + amount: + offerData.tokenType === TokenType.ERC20 + ? formatEther(offerData.amount) + : offerData.amount.toString(), + identifier: offerData.identifier.toString(), + token: offerData.token.toString(), + tokenType: offerData.tokenType, + } as OfferData, + consideration: { + amount: + considerationData.tokenType === TokenType.ERC20 + ? formatEther(considerationData.amount) + : considerationData.amount.toString(), + identifier: considerationData.identifier.toString(), + token: considerationData.token.toString(), + tokenType: considerationData.tokenType, + recipient: considerationData.recipient.toString(), + } as ConsiderationData, + } as Order; + }); + + setActiveOrders(_activeOrders); + } catch (e) { + renderError((e as Error)?.message ?? 'Failed to get order data.', e); } finally { setIsFetchingOrders(false); } - }, [Offers, goldToken, renderError, worldContract.read]); + }, [Considerations, Offers, Orders, renderError]); + + useEffect(() => { + fetchOrders(); + }, [fetchOrders]); + + const itemFloorPrices = useMemo(() => { + const itemFloorPrices: { [key: string]: string } = {}; + + activeOrders.forEach(order => { + const price = itemFloorPrices[order.offer.identifier]; + if ( + !price || + BigInt( + order.consideration.amount && order.consideration.token === goldToken, + ) < BigInt(price) + ) { + itemFloorPrices[order.offer.identifier] = order.consideration.amount; + } + }); + + return itemFloorPrices; + }, [activeOrders, goldToken]); useEffect(() => { if (!isConnected) { @@ -169,13 +188,7 @@ export const AuctionHouse = (): JSX.Element => { return Number(page); }, [page]); - useEffect(() => { - (async (): Promise => { - await fetchOrders(); - })(); - }, [fetchOrders]); - - const items = useMemo( + const unfilteredItems = useMemo( () => [...armorTemplates, ...spellTemplates, ...weaponTemplates], [armorTemplates, spellTemplates, weaponTemplates], ); @@ -184,71 +197,67 @@ export const AuctionHouse = (): JSX.Element => { if (pageNumber < 1 || isLoadingItemTemplates) { return; } - let entriesCopy = items; + let itemsCopy = unfilteredItems; - entriesCopy = [...entriesCopy].sort((entryA, entryB) => { + itemsCopy = [...itemsCopy].sort((itemA, itemB) => { let result = false; - const floorA = - prices?.filter(price => entryA.tokenId == price.tokenId)[0]?.floor || - '0'; - const floorB = - prices?.filter(price => entryB.tokenId == price.tokenId)[0]?.floor || - '0'; + const floorA = itemFloorPrices[itemA.tokenId] ?? '0'; + const floorB = itemFloorPrices[itemB.tokenId] ?? '0'; + switch (sort.sorted) { - case 'byLevel': + case SortOptions.Level: result = sort.reversed - ? BigInt(entryA?.minLevel || '0') >= BigInt(entryB?.minLevel || '0') - : BigInt(entryB?.minLevel || '0') > BigInt(entryA?.minLevel || '0'); + ? BigInt(itemA?.minLevel || '0') >= BigInt(itemB?.minLevel || '0') + : BigInt(itemB?.minLevel || '0') > BigInt(itemA?.minLevel || '0'); break; - case 'byPrice': + case SortOptions.FloorPrice: result = sort.reversed ? BigInt(floorA) >= BigInt(floorB) : BigInt(floorB) > BigInt(floorA); break; default: - // result = - // entryA.class.toString().localeCompare(entryB.class.toString()) > 0; break; } return result ? 1 : -1; }); - entriesCopy = [...entriesCopy].filter(entry => { - switch (filter.filtered) { - case 'byArmor': + itemsCopy = [...itemsCopy].filter(entry => { + switch (filter) { + case FilterOptions.Armor: return entry.itemType == ItemType.Armor; - case 'bySpell': + case FilterOptions.Spell: return entry.itemType == ItemType.Spell; - case 'byWeapon': + case FilterOptions.Weapon: return entry.itemType == ItemType.Weapon; default: return true; } }); - const searcher = new FuzzySearch( - [...entriesCopy], - ['name', 'description'], - { caseSensitive: false }, - ); - entriesCopy = searcher.search(query); + const searcher = new FuzzySearch([...itemsCopy], ['name', 'description']); + itemsCopy = searcher.search(query); + const _pageLimit = - Math.floor(Math.ceil(entriesCopy.length / PER_PAGE)) || 1; + Math.floor(Math.ceil(itemsCopy.length / ITEMS_PER_PAGE)) || 1; + setPageLimit(_pageLimit); - setEntries( - entriesCopy.slice((pageNumber - 1) * PER_PAGE, pageNumber * PER_PAGE), + setItems( + itemsCopy.slice( + (pageNumber - 1) * ITEMS_PER_PAGE, + pageNumber * ITEMS_PER_PAGE, + ), ); if (pageNumber > _pageLimit) { setPage(_pageLimit.toString()); } }, [ - filter.filtered, + filter, isLoadingItemTemplates, - items, + itemFloorPrices, pageNumber, - prices, query, sort.reversed, sort.sorted, + unfilteredItems, ]); if (isLoadingItemTemplates || isFetchingOrders) { @@ -278,56 +287,43 @@ export const AuctionHouse = (): JSX.Element => { /> - - {Object.values(ItemType) - .filter( - key => !isNaN(Number(ItemType[key as keyof typeof ItemType])), - ) - .sort() - .filter(key => - items - ? items?.filter( - item => - item.itemType == ItemType[key as keyof typeof ItemType], - )?.length > 0 - : false, - ) - .map(k => { - return ( - - ); - })} + {Object.keys(FilterOptions).map(k => { + return ( + + ); + })} - Items {entries.length} + Items {items.length} - {['byLevel', 'byPrice'].map(s => { + {Array.from(Object.values(SortOptions)).map(s => { return ( + Item not found + + ); + } + + if (isFetchingOrders || isLoadingItemTemplates) { + return ( + + + + + + + ); + } return ( - +