diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1b632d..1b93fd12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Updated + +* Optimized loading of user stakes and pending withdrawals. + +### Fixed + +* Gateways count in site header should only count active gateways. + ## [1.4.0] - 2024-11-14 ### Added diff --git a/package.json b/package.json index 092e03a8..504bcbc4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "deploy": "yarn build && permaweb-deploy --ant-process ${DEPLOY_ANT_PROCESS_ID}" }, "dependencies": { - "@ar.io/sdk": "2.4.0", + "@ar.io/sdk": "2.5.0-alpha.3", "@fontsource/rubik": "^5.0.19", "@headlessui/react": "^1.7.19", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 615718eb..8a0b0729 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -73,7 +73,14 @@ const Header = () => { loading={!blockHeight} /> gateway.status === 'joined', + ).length + : undefined + } label="GATEWAYS" loading={gatewaysLoading} /> diff --git a/src/components/modals/CancelWithdrawalModal.tsx b/src/components/modals/CancelWithdrawalModal.tsx index 4e05ebf1..79f6886f 100644 --- a/src/components/modals/CancelWithdrawalModal.tsx +++ b/src/components/modals/CancelWithdrawalModal.tsx @@ -47,6 +47,10 @@ const CancelWithdrawalModal = ({ queryKey: ['gateways'], refetchType: 'all', }); + queryClient.invalidateQueries({ + queryKey: ['delegateStakes'], + refetchType: 'all', + }); setShowSuccessModal(true); } catch (e: any) { diff --git a/src/components/modals/InstantWithdrawalModal.tsx b/src/components/modals/InstantWithdrawalModal.tsx index fe30dc2a..4c1479fe 100644 --- a/src/components/modals/InstantWithdrawalModal.tsx +++ b/src/components/modals/InstantWithdrawalModal.tsx @@ -80,6 +80,10 @@ const InstantWithdrawalModal = ({ queryKey: ['balances'], refetchType: 'all', }); + queryClient.invalidateQueries({ + queryKey: ['delegateStakes'], + refetchType: 'all', + }); setShowSuccessModal(true); } catch (e: any) { diff --git a/src/components/modals/StakingModal.tsx b/src/components/modals/StakingModal.tsx index 5dc38897..1df49dd0 100644 --- a/src/components/modals/StakingModal.tsx +++ b/src/components/modals/StakingModal.tsx @@ -1,4 +1,4 @@ -import { AoGatewayDelegate, IOToken, mIOToken } from '@ar.io/sdk/web'; +import { IOToken, mIOToken } from '@ar.io/sdk/web'; import { EAY_TOOLTIP_FORMULA, EAY_TOOLTIP_TEXT, @@ -6,6 +6,7 @@ import { log, } from '@src/constants'; import useBalances from '@src/hooks/useBalances'; +import useDelegateStakes from '@src/hooks/useDelegateStakes'; import useGateway from '@src/hooks/useGateway'; import useRewardsInfo from '@src/hooks/useRewardsInfo'; import { useGlobalState } from '@src/store'; @@ -13,7 +14,7 @@ import { formatWithCommas } from '@src/utils'; import { showErrorToast } from '@src/utils/toast'; import { useQueryClient } from '@tanstack/react-query'; import { MathJax } from 'better-react-mathjax'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Button, { ButtonType } from '../Button'; import LabelValueRow from '../LabelValueRow'; import Tooltip from '../Tooltip'; @@ -48,6 +49,7 @@ const StakingModal = ({ const [userEnteredWalletAddress, setUserEnteredWalletAddress] = useState(''); + const [currentStake, setCurrentStake] = useState(0); const [amountToStake, setAmountToStake] = useState(''); const [amountToUnstake, setAmountToUnstake] = useState(''); @@ -62,16 +64,21 @@ const StakingModal = ({ ownerWalletAddress: gatewayOwnerWallet, }); + const { data: delegateStakes } = useDelegateStakes(walletAddress?.toString()); + + useEffect(() => { + if (!gateway || !delegateStakes) { + return; + } + const stake = delegateStakes.stakes.find( + (stake) => stake.gatewayAddress === gateway.gatewayAddress, + )?.balance; + setCurrentStake(new mIOToken(stake ?? 0).toIO().valueOf()); + }, [delegateStakes, gateway]); + const allowDelegatedStaking = gateway?.settings.allowDelegatedStaking ?? false; - const delegateData: AoGatewayDelegate | undefined = walletAddress - ? gateway?.delegates[walletAddress?.toString()] - : undefined; - const currentStake = new mIOToken(delegateData?.delegatedStake ?? 0) - .toIO() - .valueOf(); - const newTotalStake = tab == 0 ? currentStake + parseFloat(amountToStake) @@ -86,13 +93,10 @@ const StakingModal = ({ }) + '%' : '-'; - const existingStake = new mIOToken(delegateData?.delegatedStake ?? 0) - .toIO() - .valueOf(); const minDelegatedStake = gateway ? new mIOToken(gateway?.settings.minDelegatedStake).toIO().valueOf() : 500; - const minRequiredStakeToAdd = existingStake > 0 ? 1 : minDelegatedStake; + const minRequiredStakeToAdd = currentStake > 0 ? 1 : minDelegatedStake; const validators = { address: validateWalletAddress('Gateway Owner'), @@ -105,7 +109,7 @@ const StakingModal = ({ unstakeAmount: validateUnstakeAmount( 'Unstake Amount', ticker, - existingStake, + currentStake, minDelegatedStake, ), }; @@ -182,6 +186,10 @@ const StakingModal = ({ queryKey: ['balances'], refetchType: 'all', }); + queryClient.invalidateQueries({ + queryKey: ['delegateStakes'], + refetchType: 'all', + }); setShowSuccessModal(true); } catch (e: any) { @@ -308,7 +316,7 @@ const StakingModal = ({ )} ; + withdrawals: Array; +}; + +const useDelegateStakes = (address?: string) => { + const arIOReadSDK = useGlobalState((state) => state.arIOReadSDK); + + const res = useQuery({ + queryKey: ['delegateStakes', address], + queryFn: async () => { + if (!address) { + throw new Error('Address is not set'); + } + + const retVal: DelegateStakes = { + stakes: [], + withdrawals: [], + }; + + let cursor: string | undefined; + + do { + const pageResult = await arIOReadSDK.getDelegations({ + address, + cursor, + limit: 10, + }); + pageResult.items.forEach((d) => { + if (d.type === 'stake') { + retVal.stakes.push(d); + } else { + retVal.withdrawals.push(d); + } + }); + cursor = pageResult.nextCursor; + } while (cursor !== undefined); + + return retVal; + }, + staleTime: Infinity, + }); + + return res; +}; + +export default useDelegateStakes; diff --git a/src/pages/Gateway/index.tsx b/src/pages/Gateway/index.tsx index b5a9bac1..3906b476 100644 --- a/src/pages/Gateway/index.tsx +++ b/src/pages/Gateway/index.tsx @@ -61,6 +61,7 @@ const Gateway = () => { const queryClient = useQueryClient(); const walletAddress = useGlobalState((state) => state.walletAddress); + const arIOReadSDK = useGlobalState((state) => state.arIOReadSDK); const arIOWriteableSDK = useGlobalState((state) => state.arIOWriteableSDK); const ticker = useGlobalState((state) => state.ticker); const { data: protocolBalance } = useProtocolBalance(); @@ -83,6 +84,7 @@ const Gateway = () => { url: gatewayAddress, }); + const [numDelegates, setNumDelegates] = useState(); const [editing, setEditing] = useState(false); const [initialState, setInitialState] = useState< @@ -135,6 +137,18 @@ const Gateway = () => { }); }, [walletAddress]); + useEffect(() => { + if (!arIOReadSDK || !gateway) return; + const update = async () => { + const res = await arIOReadSDK.getGatewayDelegates({ + address: gateway.gatewayAddress, + limit: 1, + }); + setNumDelegates(res.totalItems); + }; + update(); + }, [gateway, arIOReadSDK]); + // This updates the form when the user toggles the delegated staking switch to false to reset the // form values and error messages back to the initial state. useEffect(() => { @@ -425,14 +439,7 @@ const Gateway = () => { : formatUptime(healthCheckRes.data?.uptime) } /> - + { const [isStakingModalOpen, setIsStakingModalOpen] = useState(false); - const { data: gateways } = useGateways(); - const { data: balances } = useBalances(walletAddress); + const { data: balances } = useBalances(walletAddress); const rewardsEarned = useRewardsEarned(walletAddress?.toString()); - useEffect(() => { - if (gateways && walletAddress) { - const amountStaking = Object.values(gateways).reduce((acc, gateway) => { - const userDelegate:AoGatewayDelegate = gateway.delegates[walletAddress.toString()]; - const delegatedStake = userDelegate?.delegatedStake ?? 0; - const withdrawn = userDelegate?.vaults - ? Object.values(userDelegate.vaults).reduce((acc, withdrawal) => { - return acc + withdrawal.balance; - }, 0) - : 0; + const { data: delegatedStakes } = useDelegateStakes( + walletAddress?.toString(), + ); - return acc + delegatedStake + withdrawn; + useEffect(() => { + if (delegatedStakes) { + const staked = delegatedStakes.stakes.reduce((acc, stake) => { + return acc + stake.balance; }, 0); - setAmountStaking(new mIOToken(amountStaking).toIO().valueOf()); + + const withdrawing = delegatedStakes.withdrawals.reduce( + (acc, withdrawal) => { + return acc + withdrawal.balance; + }, + 0, + ); + setAmountStaking(new mIOToken(staked + withdrawing).toIO().valueOf()); } - }, [gateways, walletAddress]); + }, [delegatedStakes]); const topPanels = [ { diff --git a/src/pages/Staking/MyStakesTable.tsx b/src/pages/Staking/MyStakesTable.tsx index 2181b432..55b6d4f5 100644 --- a/src/pages/Staking/MyStakesTable.tsx +++ b/src/pages/Staking/MyStakesTable.tsx @@ -1,9 +1,4 @@ -import { - AoGateway, - AoGatewayDelegate, - AoVaultData, - mIOToken, -} from '@ar.io/sdk/web'; +import { AoGateway, AoVaultData, mIOToken } from '@ar.io/sdk/web'; import AddressCell from '@src/components/AddressCell'; import Button, { ButtonType } from '@src/components/Button'; import Dropdown from '@src/components/Dropdown'; @@ -18,6 +13,7 @@ import CancelWithdrawalModal from '@src/components/modals/CancelWithdrawalModal' import InstantWithdrawalModal from '@src/components/modals/InstantWithdrawalModal'; import StakingModal from '@src/components/modals/StakingModal'; import UnstakeAllModal from '@src/components/modals/UnstakeAllModal'; +import useDelegateStakes from '@src/hooks/useDelegateStakes'; import useGateways from '@src/hooks/useGateways'; import { useGlobalState } from '@src/store'; import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; @@ -76,67 +72,50 @@ const MyStakesTable = () => { const navigate = useNavigate(); + const { data: delegateStakes } = useDelegateStakes(walletAddress?.toString()); + useEffect(() => { const activeStakes: Array | undefined = isFetching ? undefined - : !walletAddress || !gateways + : !delegateStakes || !gateways ? [] - : Object.keys(gateways).reduce((acc, key) => { - const gateway = gateways[key]; - - const delegate: AoGatewayDelegate = - gateway.delegates[walletAddress?.toString()]; - - if (delegate) { - return [ - ...acc, - { - owner: key, - delegatedStake: delegate.delegatedStake, - gateway, - pendingWithdrawals: Object.keys(delegate.vaults).length, - streak: - gateway.status == 'leaving' - ? Number.NEGATIVE_INFINITY - : gateway.stats.failedConsecutiveEpochs > 0 - ? -gateway.stats.failedConsecutiveEpochs - : gateway.stats.passedConsecutiveEpochs, - }, - ]; - } - return acc; - }, [] as Array); + : delegateStakes.stakes.map((stake) => { + const gateway = gateways[stake.gatewayAddress]; + return { + owner: stake.gatewayAddress, + delegatedStake: stake.balance, + gateway, + pendingWithdrawals: delegateStakes.withdrawals.filter( + (w) => w.gatewayAddress == stake.gatewayAddress, + ).length, + streak: + gateway.status == 'leaving' + ? Number.NEGATIVE_INFINITY + : gateway.stats.failedConsecutiveEpochs > 0 + ? -gateway.stats.failedConsecutiveEpochs + : gateway.stats.passedConsecutiveEpochs, + }; + }); const pendingWithdrawals: Array | undefined = isFetching ? undefined - : !walletAddress || !gateways + : !delegateStakes || !gateways ? [] - : Object.keys(gateways).reduce((acc, key) => { - const gateway = gateways[key]; - - const delegate: AoGatewayDelegate = - gateway.delegates[walletAddress?.toString()]; + : delegateStakes.withdrawals.map((withdrawal) => { + const gateway = gateways[withdrawal.gatewayAddress]; - if (delegate?.vaults) { - const withdrawals = Object.entries(delegate.vaults).map( - ([withdrawalId, withdrawal]) => { - return { - owner: key, - gateway, - withdrawal, - withdrawalId, - }; - }, - ); + return { + owner: withdrawal.gatewayAddress, + gateway, + withdrawal, + withdrawalId: withdrawal.vaultId, + }; + }); - return [...acc, ...withdrawals]; - } - return acc; - }, [] as Array); setActiveStakes(activeStakes); setPendingWithdrawals(pendingWithdrawals); - }, [gateways, walletAddress, isFetching]); + }, [delegateStakes, gateways, isFetching]); // Define columns for the active stakes table const activeStakesColumns: ColumnDef[] = [ diff --git a/yarn.lock b/yarn.lock index 2f371b11..470931fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,10 +35,10 @@ plimit-lit "^3.0.1" warp-contracts "1.4.45" -"@ar.io/sdk@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@ar.io/sdk/-/sdk-2.4.0.tgz#4ea0c0ffa629a4be0dac837d30e81d67452e64e3" - integrity sha512-IvletHvtz4gzlY8OQysHqt9fecftsqXZ8TTDvGRW1OkEHlb5AnOCrhoscYT0qDFA6LXo1nNcmDSucXfRemNCXA== +"@ar.io/sdk@2.5.0-alpha.3": + version "2.5.0-alpha.3" + resolved "https://registry.yarnpkg.com/@ar.io/sdk/-/sdk-2.5.0-alpha.3.tgz#f87847c1ad11b707a7943ba78c53e026578b6d38" + integrity sha512-Ht9psXowzRKLaqXki1STiD4qTy3qlNRgiiTfTJ0uNuRRswHxFIu7soUPmO06JJi/m9OA/gzeMrhdssO6kbMPhA== dependencies: "@dha-team/arbundles" "^1.0.1" "@permaweb/aoconnect" "^0.0.57"