From e1f39d62247bd3dd5b9d4486dc7fc6ba97b49946 Mon Sep 17 00:00:00 2001 From: belopash Date: Thu, 24 Oct 2024 13:08:03 +0500 Subject: [PATCH] feat: properly handle lock period for delegations --- src/api/contracts/subsquid.generated.ts | 21 ++++++ .../{useFixWorkers.ts => fixes.ts} | 70 ++++++++++++++++++- .../subsquid-network-squid/workers-graphql.ts | 34 +++++---- src/pages/DelegationsPage/DelegationsPage.tsx | 1 + src/pages/WorkersPage/Worker.tsx | 9 ++- src/pages/WorkersPage/WorkerUndelegate.tsx | 53 +++++++------- src/pages/WorkersPage/WorkerWithdraw.tsx | 13 ++-- src/pages/WorkersPage/WorkersPage.tsx | 7 +- wagmi.config.ts | 13 ++++ 9 files changed, 172 insertions(+), 49 deletions(-) rename src/api/subsquid-network-squid/{useFixWorkers.ts => fixes.ts} (59%) diff --git a/src/api/contracts/subsquid.generated.ts b/src/api/contracts/subsquid.generated.ts index e3a81cb..9ff2a35 100644 --- a/src/api/contracts/subsquid.generated.ts +++ b/src/api/contracts/subsquid.generated.ts @@ -402,6 +402,19 @@ export const softCapAbi = [ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// export const stakingAbi = [ + { + type: 'function', + inputs: [ + { name: 'staker', internalType: 'address', type: 'address' }, + { name: 'worker', internalType: 'uint256', type: 'uint256' }, + ], + name: 'getDeposit', + outputs: [ + { name: 'depositAmount', internalType: 'uint256', type: 'uint256' }, + { name: 'withdrawAllowed', internalType: 'uint256', type: 'uint256' }, + ], + stateMutability: 'view', + }, { type: 'function', inputs: [ @@ -1144,6 +1157,14 @@ export const useReadStaking = /*#__PURE__*/ createUseReadContract({ abi: stakingAbi, }) +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link stakingAbi}__ and `functionName` set to `"getDeposit"` + */ +export const useReadStakingGetDeposit = /*#__PURE__*/ createUseReadContract({ + abi: stakingAbi, + functionName: 'getDeposit', +}) + /** * Wraps __{@link useReadContract}__ with `abi` set to __{@link stakingAbi}__ and `functionName` set to `"claimable"` */ diff --git a/src/api/subsquid-network-squid/useFixWorkers.ts b/src/api/subsquid-network-squid/fixes.ts similarity index 59% rename from src/api/subsquid-network-squid/useFixWorkers.ts rename to src/api/subsquid-network-squid/fixes.ts index 78b21a7..3bfd987 100644 --- a/src/api/subsquid-network-squid/useFixWorkers.ts +++ b/src/api/subsquid-network-squid/fixes.ts @@ -1,12 +1,15 @@ import { useMemo } from 'react'; import { getBlockTime } from '@lib/network'; +import { Simplify } from 'type-fest'; import { useBlock, useReadContracts } from 'wagmi'; import { Worker, WorkerStatus } from '@api/subsquid-network-squid'; import { useContracts } from '@network/useContracts'; import { + stakingAbi, + useReadRouterStaking, useReadRouterWorkerRegistration, useReadWorkerRegistryLockPeriod, workerRegistryAbi, @@ -83,7 +86,7 @@ export function useFixWorkers>({ locked: true, unlockedAt: new Date( timestamp + getBlockTime(unlockBlock - lastL1Block.number), - ).toString(), + ).toISOString(), } : { locked: false }; @@ -106,3 +109,68 @@ export function useFixWorkers>({ data, }; } + +export function useFixDelegations< + T extends { id: string; delegations: { owner: { id: string } }[] }, +>({ workers }: { workers?: T[] }) { + const { ROUTER, CHAIN_ID_L1 } = useContracts(); + + const { data: stakingAddress, isLoading: isStakingAddressLoading } = useReadRouterStaking({ + address: ROUTER, + query: { enabled: !!ROUTER }, + }); + + const { data: lastL1Block, isLoading: isLastL1BlockLoading } = useBlock({ + chainId: CHAIN_ID_L1, + includeTransactions: false, + }); + + const { data: delegationsInfo, isLoading: isDelegationsInfoLoading } = useReadContracts({ + contracts: workers?.flatMap(worker => + worker.delegations.map( + delegation => + ({ + abi: stakingAbi, + address: stakingAddress || '0x', + functionName: 'getDeposit', + args: [delegation.owner.id as `0x${string}`, worker.id], + }) as const, + ), + ), + allowFailure: false, + query: { enabled: !!workers && !!stakingAddress }, + }); + + type R = Simplify< + Omit & { + delegations: Simplify[]; + } + >; + + const data = useMemo(() => { + if (!delegationsInfo || !lastL1Block || !workers) return workers; + + let index = 0; + return workers.map( + worker => + ({ + ...worker, + delegations: worker.delegations.map(delegation => { + const [, unlockBlock] = delegationsInfo[index++]; + const timestamp = Number(lastL1Block.timestamp) * 1000; + const locked = lastL1Block.number < unlockBlock; + const unlockedAt = locked + ? new Date(timestamp + getBlockTime(unlockBlock - lastL1Block.number)).toISOString() + : undefined; + + return { ...delegation, locked, unlockedAt }; + }), + }) as R, + ); + }, [delegationsInfo, lastL1Block, workers]); + + return { + data, + isLoading: isDelegationsInfoLoading || isLastL1BlockLoading || isStakingAddressLoading, + }; +} diff --git a/src/api/subsquid-network-squid/workers-graphql.ts b/src/api/subsquid-network-squid/workers-graphql.ts index f70781e..683f395 100644 --- a/src/api/subsquid-network-squid/workers-graphql.ts +++ b/src/api/subsquid-network-squid/workers-graphql.ts @@ -8,6 +8,7 @@ import { PartialDeep, SimplifyDeep } from 'type-fest'; import { useAccount } from '@network/useAccount.ts'; import { useSquid } from './datasource'; +import { useFixDelegations, useFixWorkers } from './fixes'; import { AccountType, Delegation, @@ -21,7 +22,6 @@ import { Worker, } from './graphql'; import { useNetworkSettings } from './settings-graphql'; -import { useFixWorkers } from './useFixWorkers'; // inherit API interface for internal class // export interface BlockchainApiWorker extends Omit { @@ -381,10 +381,14 @@ export function useMyDelegations({ sortBy, sortDir }: { sortBy: WorkerSortBy; so { address: address || '0x' }, ); - const { data: fixedDelegations, isLoading: isFixedDelegationsLoading } = useFixWorkers({ + const { data: fixedWorkers, isLoading: isFixedWorkersLoading } = useFixWorkers({ workers: delegationsQuery?.workers, }); + const { data: fixedDelegations, isLoading: isFixedDelegationsLoading } = useFixDelegations({ + workers: fixedWorkers, + }); + const data = useMemo(() => { type W = SimplifyDeep< Pick< @@ -402,6 +406,7 @@ export function useMyDelegations({ sortBy, sortDir }: { sortBy: WorkerSortBy; so Pick & { delegations: (Pick & { owner: { id: string; type: AccountType }; + unlockedAt?: string; })[]; } >; @@ -434,12 +439,7 @@ export function useMyDelegations({ sortBy, sortDir }: { sortBy: WorkerSortBy; so .toFixed(); worker.delegations.push({ - deposit: d.deposit, - locked: d.locked, - owner: { - id: d.owner.id, - type: d.owner.type, - }, + ...d, }); }); @@ -450,7 +450,11 @@ export function useMyDelegations({ sortBy, sortDir }: { sortBy: WorkerSortBy; so }, [fixedDelegations, sortBy, sortDir]); return { - isLoading: isSettingsLoading || isDelegationsQueryLoading || isFixedDelegationsLoading, + isLoading: + isSettingsLoading || + isDelegationsQueryLoading || + isFixedWorkersLoading || + isFixedDelegationsLoading, data, }; } @@ -536,7 +540,7 @@ export function useMyWorkerDelegations({ }) { const { address } = useAccount(); const datasource = useSquid(); - const { data, isLoading } = useMyDelegationsQuery( + const { data: delegations, isLoading: isDelegationsLoading } = useMyDelegationsQuery( datasource, { workerId: peerId || '', @@ -544,14 +548,18 @@ export function useMyWorkerDelegations({ }, { select: res => { - return res.workers[0]?.delegations || []; + return res.workers; }, enabled, }, ); + const { data: fixedDelegations, isLoading: isFixedDelegationsLoading } = useFixDelegations({ + workers: delegations, + }); + return { - isLoading: isLoading, - data, + isLoading: isDelegationsLoading || isFixedDelegationsLoading, + data: fixedDelegations?.[0]?.delegations, }; } diff --git a/src/pages/DelegationsPage/DelegationsPage.tsx b/src/pages/DelegationsPage/DelegationsPage.tsx index 66ab0f8..3eb83cb 100644 --- a/src/pages/DelegationsPage/DelegationsPage.tsx +++ b/src/pages/DelegationsPage/DelegationsPage.tsx @@ -103,6 +103,7 @@ export function MyDelegations() { type: d.owner.type, balance: d.deposit, locked: d.locked || false, + unlockedAt: d.unlockedAt || '', }))} disabled={!worker.delegations.some(d => !d.locked)} /> diff --git a/src/pages/WorkersPage/Worker.tsx b/src/pages/WorkersPage/Worker.tsx index 3a12471..2c86131 100644 --- a/src/pages/WorkersPage/Worker.tsx +++ b/src/pages/WorkersPage/Worker.tsx @@ -117,6 +117,8 @@ export const Worker = ({ backPath }: { backPath: string }) => { type: d.owner.type, balance: d.deposit, locked: d.locked || false, + // FIXME: some issue with types + unlockedAt: (d as any).unlockedAt, }))} disabled={isLoading || !delegations?.some(d => !d.locked)} /> @@ -272,7 +274,12 @@ export const Worker = ({ backPath }: { backPath: string }) => { worker.status === WorkerStatus.Deregistering ? ( ) : ( diff --git a/src/pages/WorkersPage/WorkerUndelegate.tsx b/src/pages/WorkersPage/WorkerUndelegate.tsx index cc7aba0..6c75ad2 100644 --- a/src/pages/WorkersPage/WorkerUndelegate.tsx +++ b/src/pages/WorkersPage/WorkerUndelegate.tsx @@ -14,13 +14,7 @@ import { useDebounce } from 'use-debounce'; import { stakingAbi, useReadRouterStaking } from '@api/contracts'; import { useWriteSQDTransaction } from '@api/contracts/useWriteTransaction'; import { errorMessage } from '@api/contracts/utils'; -import { - Account, - AccountType, - Delegation, - SourceWalletWithBalance, - Worker, -} from '@api/subsquid-network-squid'; +import { AccountType, SourceWalletWithBalance, Worker } from '@api/subsquid-network-squid'; import { ContractCallDialog } from '@components/ContractCallDialog'; import { Form, FormDivider, FormikSelect, FormikTextInput, FormRow } from '@components/Form'; import { HelpTooltip } from '@components/HelpTooltip'; @@ -32,6 +26,7 @@ import { EXPECTED_APR_TIP, useExpectedAprAfterDelegation } from './WorkerDelegat export type SourceWalletWithDelegation = SourceWalletWithBalance & { locked: boolean; + unlockedAt?: string; }; export const undelegateSchema = yup.object({ @@ -52,12 +47,7 @@ export function WorkerUndelegate({ sources, }: { sources?: SourceWalletWithDelegation[]; - worker?: Pick & { - delegations: (Pick & { - owner: Pick; - unlockedAt?: string; - })[]; - }; + worker?: Pick; disabled?: boolean; }) { const [open, setOpen] = useState(false); @@ -146,23 +136,32 @@ export function WorkerUndelegate({ // return !!worker?.delegations.some(d => !d.locked && BigNumber(d.deposit).gt(0)); // }, [worker?.delegations]); - const isLocked = useMemo( - () => !!worker?.delegations.length && !worker.delegations.some(d => !d.locked), - [worker], - ); - const unlockedAt = useMemo(() => { - return ''; - }, []); + const isLocked = useMemo(() => !!sources?.length && !sources?.some(d => !d.locked), [sources]); + + const [curTimestamp] = useDebounce(Date.now(), 1000); + const { unlockedAt, timeLeft } = useMemo(() => { + const min = sources?.reduce( + (r, d) => { + if (!d.unlockedAt) return r; + + const ms = new Date(d.unlockedAt).getTime(); + return !r || ms < r ? ms : r; + }, + undefined as number | undefined, + ); + + return { + unlockedAt: min ? new Date(min).toISOString() : undefined, + timeLeft: min ? relativeDateFormat(curTimestamp, min) : undefined, + }; + }, [curTimestamp, sources]); return ( <> {isLocked && ( { - const wallet = worker?.delegations.find(s => s.owner.id === e.target.value); + const wallet = sources?.find(s => s.id === e.target.value); if (!wallet) return; - formik.setFieldValue('source', wallet.owner.id); - formik.setFieldValue('max', fromSqd(wallet.deposit).toFixed()); + formik.setFieldValue('source', wallet.id); + formik.setFieldValue('max', fromSqd(wallet.balance).toFixed()); }} /> diff --git a/src/pages/WorkersPage/WorkerWithdraw.tsx b/src/pages/WorkersPage/WorkerWithdraw.tsx index db1590a..bfd2528 100644 --- a/src/pages/WorkersPage/WorkerWithdraw.tsx +++ b/src/pages/WorkersPage/WorkerWithdraw.tsx @@ -24,10 +24,11 @@ export function WorkerWithdrawButton({ sx, }: { sx?: SxProps; - worker: Pick & { + worker: Pick; + source: SourceWallet & { + locked: boolean; unlockedAt?: string; }; - source: SourceWallet; disabled?: boolean; }) { const [open, setOpen] = useState(false); @@ -35,10 +36,10 @@ export function WorkerWithdrawButton({ return ( <> - {worker.locked && ( + {source.locked && ( setOpen(true)} variant="outlined" color="error" - disabled={disabled || worker.locked} + disabled={disabled || source.locked} > WITHDRAW diff --git a/src/pages/WorkersPage/WorkersPage.tsx b/src/pages/WorkersPage/WorkersPage.tsx index f0418e1..217e68b 100644 --- a/src/pages/WorkersPage/WorkersPage.tsx +++ b/src/pages/WorkersPage/WorkersPage.tsx @@ -120,7 +120,12 @@ export function MyWorkers() { worker.status === WorkerStatus.Deregistering ? ( ) : ( diff --git a/wagmi.config.ts b/wagmi.config.ts index 0120876..cd985f2 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -190,6 +190,19 @@ export default defineConfig({ { name: 'Staking', abi: [ + { + type: 'function', + name: 'getDeposit', + inputs: [ + { name: 'staker', type: 'address', internalType: 'address' }, + { name: 'worker', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [ + { name: 'depositAmount', type: 'uint256', internalType: 'uint256' }, + { name: 'withdrawAllowed', type: 'uint256', internalType: 'uint256' }, + ], + stateMutability: 'view', + }, { type: 'function', name: 'deposit',