diff --git a/src/components/balance-input.tsx b/src/components/balance-input.tsx index 9f8be37..5d87f61 100644 --- a/src/components/balance-input.tsx +++ b/src/components/balance-input.tsx @@ -2,11 +2,12 @@ import { formatBlanace } from "@/utils"; import Image from "next/image"; import InputLabel from "./input-label"; import { parseUnits } from "viem"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; export default function BalanceInput({ isReset, balance, + max, symbol, decimals, logoPath, @@ -17,6 +18,7 @@ export default function BalanceInput({ }: { isReset?: boolean; balance: bigint; + max?: bigint; symbol: string; decimals: number; logoPath?: string; @@ -34,6 +36,14 @@ export default function BalanceInput({ } }, [isReset]); + const placeholder = useMemo(() => { + if (typeof max === "bigint") { + return `Max: ${formatBlanace(max, decimals, { keepZero: false, precision: decimals })}`; + } else { + return `Balance: ${formatBlanace(balance, decimals, { keepZero: false, precision: decimals })}`; + } + }, [balance, decimals, max]); + return (
{label && } @@ -43,11 +53,15 @@ export default function BalanceInput({ }`} > { const _hasError = Number.isNaN(Number(e.target.value)); - setHasError(_hasError || balance < parseUnits(e.target.value, decimals)); + setHasError( + _hasError || + balance < parseUnits(e.target.value, decimals) || + (typeof max === "bigint" && max < parseUnits(e.target.value, decimals)) + ); if (!_hasError) { onChange(parseUnits(e.target.value, decimals)); diff --git a/src/components/bond-more-deposit-modal.tsx b/src/components/bond-more-deposit-modal.tsx index 0fa565d..47d5e0b 100644 --- a/src/components/bond-more-deposit-modal.tsx +++ b/src/components/bond-more-deposit-modal.tsx @@ -1,8 +1,8 @@ -import { Key, useCallback, useState } from "react"; +import { Key, useCallback, useEffect, useState } from "react"; import Modal from "./modal"; import CheckboxGroup from "./checkbox-group"; import { formatBlanace, getChainConfig, notifyTransaction } from "@/utils"; -import { useApp, useStaking } from "@/hooks"; +import { useApp, useDip6, useRateLimit, useStaking } from "@/hooks"; import { notification } from "./notification"; import { writeContract, waitForTransaction } from "@wagmi/core"; @@ -22,6 +22,14 @@ export default function BondMoreDepositModal({ const availableDeposits = deposits.filter(({ inUse }) => !inUse); const { nativeToken } = getChainConfig(activeChain); + const { isDip6Implemented } = useDip6(); + const { availableDeposit, updateRateLimit } = useRateLimit(); + useEffect(() => { + if (isOpen) { + updateRateLimit(); + } + }, [isOpen, updateRateLimit]); + const handleBond = useCallback(async () => { setBusy(true); const { contract, explorer } = getChainConfig(activeChain); @@ -37,6 +45,7 @@ export default function BondMoreDepositModal({ if (receipt.status === "success") { setCheckedDeposits([]); + updateRateLimit(); onClose(); } notifyTransaction(receipt, explorer); @@ -46,7 +55,7 @@ export default function BondMoreDepositModal({ } setBusy(false); - }, [activeChain, checkedDeposits, onClose]); + }, [activeChain, checkedDeposits, onClose, updateRateLimit]); return ( {availableDeposits.length ? ( <> + {isDip6Implemented && ( + + Max in this session: {formatBlanace(availableDeposit, nativeToken.decimals, { keepZero: false })}{" "} + {nativeToken.symbol} + + )} ({ value: id, @@ -74,6 +89,7 @@ export default function BondMoreDepositModal({ })} ${nativeToken.symbol}`}
), + disabled: isDip6Implemented && availableDeposit < value, }))} checkedValues={checkedDeposits} onChange={setCheckedDeposits as (values: Key[]) => void} diff --git a/src/components/bond-more-kton-modal.tsx b/src/components/bond-more-kton-modal.tsx index 29a50ca..6860b66 100644 --- a/src/components/bond-more-kton-modal.tsx +++ b/src/components/bond-more-kton-modal.tsx @@ -59,6 +59,7 @@ export default function BondMoreKtonModal({ symbol={ktonToken.symbol} decimals={ktonToken.decimals} balance={ktonBalance?.value || 0n} + max={0n} busy={busy} disabled={inputAmount <= 0n} isReset={inputAmount <= 0} diff --git a/src/components/bond-more-ring-modal.tsx b/src/components/bond-more-ring-modal.tsx index 9ba3327..7cd1c4e 100644 --- a/src/components/bond-more-ring-modal.tsx +++ b/src/components/bond-more-ring-modal.tsx @@ -1,8 +1,8 @@ import { getChainConfig, notifyTransaction } from "@/utils"; import BondMoreTokenModal from "./bond-more-token-modal"; import { useAccount, useBalance } from "wagmi"; -import { useApp } from "@/hooks"; -import { useCallback, useState } from "react"; +import { useApp, useDip6, useRateLimit } from "@/hooks"; +import { useCallback, useEffect, useState } from "react"; import { notification } from "./notification"; import { writeContract, waitForTransaction } from "@wagmi/core"; @@ -22,6 +22,14 @@ export default function BondMoreRingModal({ const { nativeToken } = getChainConfig(activeChain); + const { isDip6Implemented } = useDip6(); + const { availableDeposit, updateRateLimit } = useRateLimit(); + useEffect(() => { + if (isOpen) { + updateRateLimit(); + } + }, [isOpen, updateRateLimit]); + const handleBond = useCallback(async () => { if ((ringBalance?.value || 0n) < inputAmount) { notification.warn({ description: "Your balance is insufficient." }); @@ -40,6 +48,7 @@ export default function BondMoreRingModal({ if (receipt.status === "success") { setInputAmount(0n); + updateRateLimit(); onClose(); } notifyTransaction(receipt, explorer); @@ -49,7 +58,7 @@ export default function BondMoreRingModal({ setBusy(false); } - }, [activeChain, inputAmount, ringBalance?.value, onClose]); + }, [activeChain, inputAmount, ringBalance?.value, onClose, updateRateLimit]); return ( diff --git a/src/components/checkbox-group.tsx b/src/components/checkbox-group.tsx index 2161b6b..ebddd6d 100644 --- a/src/components/checkbox-group.tsx +++ b/src/components/checkbox-group.tsx @@ -3,6 +3,7 @@ import { Key, ReactElement } from "react"; interface Props { options: { label: ReactElement; + disabled?: boolean; value: Key; }[]; checkedValues: Key[]; @@ -13,7 +14,7 @@ interface Props { export default function CheckboxGroup({ options, checkedValues, className, onChange = () => undefined }: Props) { return (
- {options.map(({ label, value }) => { + {options.map(({ label, value, disabled }) => { const idx = checkedValues.findIndex((v) => v === value); const checked = idx >= 0; @@ -23,6 +24,7 @@ export default function CheckboxGroup({ options, checkedValues, className, onCha className={`relative h-4 w-4 rounded-sm border transition hover:scale-105 active:scale-95 ${ checked ? "border-primary bg-primary" : "border-white bg-transparent" }`} + disabled={disabled} onClick={() => { const checkeds = [...checkedValues]; if (checked) { diff --git a/src/components/collator-select-modal.tsx b/src/components/collator-select-modal.tsx index d42c7f4..5c2d3bf 100644 --- a/src/components/collator-select-modal.tsx +++ b/src/components/collator-select-modal.tsx @@ -5,10 +5,10 @@ import Image from "next/image"; import { useAccount } from "wagmi"; import Table, { ColumnType } from "./table"; import Jazzicon from "./jazzicon"; -import { prettyNumber } from "@/utils"; +import { formatBlanace, prettyNumber } from "@/utils"; import { notification } from "./notification"; import DisplayAccountName from "./display-account-name"; -import { useStaking } from "@/hooks"; +import { useDip6, useStaking } from "@/hooks"; import Tooltip from "./tooltip"; type TabKey = "active" | "waiting"; @@ -22,103 +22,101 @@ interface DataSource { sessionKey: string | undefined; } -const columns: ColumnType[] = [ - { - key: "collator", - dataIndex: "collator", - width: "32%", - title: Collator, - render: (row) => ( -
- - - Copy collator { - e.stopPropagation(); - try { - await navigator.clipboard.writeText(row.collator); - notification.success({ - title: "Copy address successfully", - disabledCloseBtn: true, - duration: 3000, - }); - } catch (err) { - console.error(err); - } - }} - /> - {row.sessionKey ? null : ( - - Warning - - )} -
- ), - }, - { - key: "power", - dataIndex: "power", - title: ( -
- Total-staked - - {`The Collator's total-staked is a dynamic value, inversely proportional to the commission set by - the Collator. Higher commission results in lower total-staked and vice versa. `} - - Learn More - -
- } - enabledSafePolygon - contentClassName="w-80" - > +function getColumns(activeTab: TabKey, isDip6Implemented: boolean) { + const columns: ColumnType[] = [ + { + key: "collator", + dataIndex: "collator", + width: "32%", + title: Collator, + render: (row) => ( +
+ + Info { + e.stopPropagation(); + try { + await navigator.clipboard.writeText(row.collator); + notification.success({ + title: "Copy address successfully", + disabledCloseBtn: true, + duration: 3000, + }); + } catch (err) { + console.error(err); + } + }} /> - -
- ), - render: (row) => {prettyNumber(row.power)}, - }, - { - key: "commission", - dataIndex: "commission", - width: "20%", - title: Commission, - render: (row) => {row.commission}, - }, - { - key: "blocks", - dataIndex: "blocks", - width: "20%", - title: ( -
- Blocks - Last session -
- ), - render: (row) => {row.blocks >= 0 ? row.blocks : "-"}, - }, -]; + {row.sessionKey ? null : ( + + Warning + + )} +
+ ), + }, + { + key: "power", + dataIndex: "power", + title: ( +
+ {activeTab === "active" ? "Vote" : "Total-staked"} + {activeTab === "active" && ( + Vote = Total-staked * (1 - Commission)} + enabledSafePolygon + contentClassName="w-80" + > + Info + + )} +
+ ), + render: (row) => { + if (activeTab === "active" && !isDip6Implemented) { + return {prettyNumber(row.power)}; + } + return {formatBlanace(row.power, 18, { keepZero: false, precision: 0 })}; + }, + }, + { + key: "commission", + dataIndex: "commission", + width: "20%", + title: Commission, + render: (row) => {row.commission}, + }, + { + key: "blocks", + dataIndex: "blocks", + width: "20%", + title: ( +
+ Blocks + Last session +
+ ), + render: (row) => {row.blocks >= 0 ? row.blocks : "-"}, + }, + ]; + + return columns; +} export default function CollatorSelectModal({ isOpen, @@ -190,6 +188,8 @@ export default function CollatorSelectModal({ } }, [address, nominatorCollators, isOpen]); + const { isDip6Implemented } = useDip6(); + return (
{/* ring */}
- {row.bondedTokens.unbondingRing.length > 0 ? ( + {row.bondedTokens.unbondingRing.length > 0 && !isDip6Implemented ? ( ringBusy ? ( ) : ( @@ -131,7 +132,7 @@ export default function RecordsBondedTokens({ row }: { row: StakingRecordsDataSo
{/* deposit */}
- {row.bondedTokens.unbondingDeposits.length > 0 ? ( + {row.bondedTokens.unbondingDeposits.length > 0 && !isDip6Implemented ? ( depositBusy ? ( ) : ( @@ -160,7 +161,7 @@ export default function RecordsBondedTokens({ row }: { row: StakingRecordsDataSo
{/* kton */}
- {row.bondedTokens.unbondingKton.length > 0 ? ( + {row.bondedTokens.unbondingKton.length > 0 && !isDip6Implemented ? ( ktonBusy ? ( ) : ( diff --git a/src/components/unbond-deposit-modal.tsx b/src/components/unbond-deposit-modal.tsx index 082b229..fead4f1 100644 --- a/src/components/unbond-deposit-modal.tsx +++ b/src/components/unbond-deposit-modal.tsx @@ -1,8 +1,8 @@ -import { Key, useCallback, useState } from "react"; +import { Key, useCallback, useEffect, useState } from "react"; import Modal from "./modal"; import CheckboxGroup from "./checkbox-group"; import { formatBlanace, getChainConfig, notifyTransaction } from "@/utils"; -import { useApp, useStaking } from "@/hooks"; +import { useApp, useDip6, useRateLimit, useStaking } from "@/hooks"; import { notification } from "./notification"; import { writeContract, waitForTransaction } from "@wagmi/core"; @@ -22,6 +22,14 @@ export default function UnbondDepositModal({ const availableDeposits = deposits.filter(({ id }) => stakedDeposits.includes(id)); const { nativeToken } = getChainConfig(activeChain); + const { isDip6Implemented } = useDip6(); + const { availableWithdraw, updateRateLimit } = useRateLimit(); + useEffect(() => { + if (isOpen) { + updateRateLimit(); + } + }, [isOpen, updateRateLimit]); + const handleUnbond = useCallback(async () => { setBusy(true); const { contract, explorer } = getChainConfig(activeChain); @@ -37,6 +45,7 @@ export default function UnbondDepositModal({ if (receipt.status === "success") { setCheckedDeposits([]); + updateRateLimit(); onClose(); } notifyTransaction(receipt, explorer); @@ -46,7 +55,7 @@ export default function UnbondDepositModal({ } setBusy(false); - }, [activeChain, checkedDeposits, onClose]); + }, [activeChain, checkedDeposits, onClose, updateRateLimit]); return ( {availableDeposits.length ? ( <> + {isDip6Implemented && ( + + Max in this session: {formatBlanace(availableWithdraw, nativeToken.decimals, { keepZero: false })}{" "} + {nativeToken.symbol} + + )} ({ value: id, @@ -74,6 +89,7 @@ export default function UnbondDepositModal({ })} ${nativeToken.symbol}`}
), + disabled: isDip6Implemented && availableWithdraw < value, }))} checkedValues={checkedDeposits} onChange={setCheckedDeposits as (values: Key[]) => void} diff --git a/src/components/unbond-ring-modal.tsx b/src/components/unbond-ring-modal.tsx index dc5cdae..bf76aab 100644 --- a/src/components/unbond-ring-modal.tsx +++ b/src/components/unbond-ring-modal.tsx @@ -1,7 +1,7 @@ import { formatBlanace, getChainConfig, notifyTransaction } from "@/utils"; import UnbondTokenModal from "./unbond-token-modal"; -import { useApp, useStaking } from "@/hooks"; -import { useCallback, useState } from "react"; +import { useApp, useDip6, useRateLimit, useStaking } from "@/hooks"; +import { useCallback, useEffect, useState } from "react"; import { notification } from "./notification"; import { writeContract, waitForTransaction } from "@wagmi/core"; @@ -20,6 +20,14 @@ export default function UnbondRingModal({ const { nativeToken } = getChainConfig(activeChain); + const { isDip6Implemented } = useDip6(); + const { availableWithdraw, updateRateLimit } = useRateLimit(); + useEffect(() => { + if (isOpen) { + updateRateLimit(); + } + }, [isOpen, updateRateLimit]); + const handleUnbond = useCallback(async () => { if (stakedRing < inputAmount) { notification.warn({ @@ -43,6 +51,7 @@ export default function UnbondRingModal({ if (receipt.status === "success") { setInputAmount(0n); + updateRateLimit(); onClose(); } notifyTransaction(receipt, explorer); @@ -53,7 +62,7 @@ export default function UnbondRingModal({ setBusy(false); } - }, [activeChain, stakedRing, inputAmount, nativeToken, onClose]); + }, [activeChain, stakedRing, inputAmount, nativeToken, onClose, updateRateLimit]); return ( void; }) { const isKton = symbol.endsWith("KTON"); + const { isDip6Implemented } = useDip6(); return ( <> -

+

This unbonding process will take 14 days to complete.

- {isKton && ( + {(isKton || isDip6Implemented) && (

{`There is no longer a 14-day period for unbonding ${symbol}.`}

)}
@@ -55,6 +59,7 @@ export default function UnbondTokenModal({ decimals={decimals} symbol={symbol} balance={balance} + max={max} isReset={isReset} onChange={onChange} /> diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 36e2ca2..559d64f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -9,7 +9,8 @@ export * from "./use-nominator-collators"; export * from "./use-collator-nominators"; export * from "./use-collator-commission"; export * from "./use-collator-power"; -export * from "./use-pool"; export * from "./use-active-collators"; export * from "./use-collator-last-session-blocks"; export * from "./use-collators-session-key"; +export * from "./use-rate-limit"; +export * from "./use-dip6"; diff --git a/src/hooks/use-collator-power.ts b/src/hooks/use-collator-power.ts index 2607db6..0dde9f7 100644 --- a/src/hooks/use-collator-power.ts +++ b/src/hooks/use-collator-power.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { from, of, forkJoin, switchMap, Subscription } from "rxjs"; import { useApi } from "./use-api"; import { DarwiniaStakingLedger } from "@/types"; -import { commissionWeightedPower, stakingToPower } from "@/utils"; interface DepositJson { id: number; @@ -13,11 +12,6 @@ interface DepositJson { } interface ExposuresJson { - nominators: { who: string; value: string }[]; - total: string; -} - -interface ExposuresJsonCache { nominators: { who: string; vote: string }[]; vote: string; } @@ -29,26 +23,8 @@ interface DefaultValue { type ExposureCacheState = "Previous" | "Current" | "Next"; -function isExposuresJsonCache(data: any): data is ExposuresJsonCache { - return data.vote; -} - -function formatExposuresData(data: unknown) { - if (isExposuresJsonCache(data)) { - return { - total: data.vote, - nominators: data.nominators.map(({ who, vote }) => ({ who, value: vote })), - } as ExposuresJson; - } else { - return data as ExposuresJson; - } -} - export const useCollatorPower = ( collatorNominators: { [collator: string]: string[] | undefined }, - collatorCommission: { [collator: string]: string | undefined }, - ringPool: bigint, - ktonPool: bigint, defaultValue: DefaultValue ) => { const [collatorPower, setCollatorPower] = useState(defaultValue.collatorPower); @@ -80,7 +56,7 @@ export const useCollatorPower = ( next: ([exposures, ledgers, deposits]) => { const parsedExposures = exposures.reduce((acc, cur) => { const address = (cur[0].toHuman() as string[])[0]; - const data = formatExposuresData(cur[1].toJSON() as unknown); + const data = cur[1].toJSON() as unknown as ExposuresJson; return { ...acc, [address]: data }; }, {} as { [address: string]: ExposuresJson | undefined }); @@ -101,34 +77,31 @@ export const useCollatorPower = ( collators.reduce((acc, cur) => { if (parsedExposures[cur]) { // active collator - return { ...acc, [cur]: BigInt(parsedExposures[cur]?.total || 0) }; + return { ...acc, [cur]: BigInt(parsedExposures[cur]?.vote || 0) }; } const nominators = collatorNominators[cur] || []; - const { stakedDeposit, stakedRing, stakedKton } = nominators.reduce( + const { stakedDeposit, stakedRing } = nominators.reduce( (acc, cur) => { const ledger = parsedLedgers[cur]; const deposits = parsedDeposits[cur] || []; if (ledger) { const stakedDeposit = deposits - .filter(({ id }) => ledger.stakedDeposits?.includes(id)) + .filter(({ id }) => (ledger.stakedDeposits || ledger.deposits)?.includes(id)) .reduce((acc, cur) => acc + BigInt(cur.value), 0n); return { stakedDeposit: acc.stakedDeposit + stakedDeposit, - stakedRing: acc.stakedRing + BigInt(ledger.stakedRing), - stakedKton: acc.stakedKton + BigInt(ledger.stakedKton), + stakedRing: acc.stakedRing + BigInt(ledger.stakedRing ?? ledger.ring ?? 0n), }; } return acc; }, - { stakedDeposit: 0n, stakedRing: 0n, stakedKton: 0n } + { stakedDeposit: 0n, stakedRing: 0n } ); - const power = stakingToPower(stakedRing + stakedDeposit, stakedKton, ringPool, ktonPool); - const commission = collatorCommission[cur] || "0.00%"; - return { ...acc, [cur]: commissionWeightedPower(power, commission) }; + return { ...acc, [cur]: stakedRing + stakedDeposit }; }, {} as { [collator: string]: bigint | undefined }) ); }, @@ -140,7 +113,7 @@ export const useCollatorPower = ( } return () => sub$$?.unsubscribe(); - }, [polkadotApi, collatorNominators, collatorCommission, ringPool, ktonPool]); + }, [polkadotApi, collatorNominators]); return { collatorPower, isCollatorPowerInitialized }; }; diff --git a/src/hooks/use-dip6.ts b/src/hooks/use-dip6.ts new file mode 100644 index 0000000..efbf7f6 --- /dev/null +++ b/src/hooks/use-dip6.ts @@ -0,0 +1,16 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useApi } from "./use-api"; +import { isFunction } from "@polkadot/util"; + +export function useDip6() { + const [isDip6Implemented, setIsDip6Implemented] = useState(false); + const { polkadotApi } = useApi(); + + useEffect(() => { + setIsDip6Implemented(isFunction(polkadotApi?.query.darwiniaStaking?.rateLimit)); + }, [polkadotApi?.query.darwiniaStaking?.rateLimit]); + + return { isDip6Implemented }; +} diff --git a/src/hooks/use-ledger.ts b/src/hooks/use-ledger.ts index b1a618e..c70cfb7 100644 --- a/src/hooks/use-ledger.ts +++ b/src/hooks/use-ledger.ts @@ -87,13 +87,13 @@ export const useLedger = (deposits: Deposit[], defaultValue: DefaultValue) => { setStakedDeposit( deposits - .filter(({ id }) => ledgerData.stakedDeposits?.includes(id)) + .filter(({ id }) => (ledgerData.stakedDeposits || ledgerData.deposits)?.includes(id)) .reduce((acc, cur) => acc + cur.value, 0n) ); - setStakedDeposits(ledgerData.stakedDeposits || []); + setStakedDeposits(ledgerData.stakedDeposits || ledgerData.deposits || []); - setStakedRing(BigInt(ledgerData.stakedRing)); - setStakedKton(BigInt(ledgerData.stakedKton)); + setStakedRing(BigInt(ledgerData.stakedRing ?? ledgerData.ring ?? 0)); + setStakedKton(BigInt(ledgerData.stakedKton ?? 0)); setUnbondingRing(_unbondingRing); setUnbondingKton(_unbondingKton); diff --git a/src/hooks/use-pool.ts b/src/hooks/use-pool.ts deleted file mode 100644 index 93e1d91..0000000 --- a/src/hooks/use-pool.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useState } from "react"; -import { useApi } from "./use-api"; -import type { Balance } from "@polkadot/types/interfaces"; - -interface DefaultValue { - ringPool: bigint; - ktonPool: bigint; - isRingPoolInitialized: boolean; - isKtonPoolInitialized: boolean; -} - -export const usePool = (defaultValue: DefaultValue) => { - const [ringPool, setRingPool] = useState(defaultValue.ringPool); - const [ktonPool, setKtonPool] = useState(defaultValue.ktonPool); - const [isRingPoolInitialized, setIsRingPoolInitialized] = useState(false); - const [isKtonPoolInitialized, setIsKtonPoolInitialized] = useState(false); - const { polkadotApi } = useApi(); - - // ring pool - useEffect(() => { - let unsub = () => undefined; - - polkadotApi?.query.darwiniaStaking - .ringPool((value: Balance) => setRingPool(value.toBigInt())) - .then((_unsub) => { - unsub = _unsub as unknown as typeof unsub; - }) - .catch(console.error) - .finally(() => setIsRingPoolInitialized(true)); - - return () => unsub(); - }, [polkadotApi]); - - // kton pool - useEffect(() => { - let unsub = () => undefined; - - polkadotApi?.query.darwiniaStaking - .ktonPool((value: Balance) => setKtonPool(value.toBigInt())) - .then((_unsub) => { - unsub = _unsub as unknown as typeof unsub; - }) - .catch(console.error) - .finally(() => setIsKtonPoolInitialized(true)); - - return () => unsub(); - }, [polkadotApi]); - - return { ringPool, ktonPool, isRingPoolInitialized, isKtonPoolInitialized }; -}; diff --git a/src/hooks/use-rate-limit.ts b/src/hooks/use-rate-limit.ts new file mode 100644 index 0000000..f0c8fab --- /dev/null +++ b/src/hooks/use-rate-limit.ts @@ -0,0 +1,59 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useApi } from "./use-api"; +import { isFunction } from "@polkadot/util"; +import { forkJoin } from "rxjs"; +import type { u128 } from "@polkadot/types"; +import { DarwiniaStakingRateLimiter } from "@/types"; + +const defaultValue = BigInt(Number.MAX_SAFE_INTEGER) * 10n ** 18n; // Token decimals: 18 + +export function useRateLimit() { + const [availableWithdraw, setAvailableWithdraw] = useState(defaultValue); + const [availableDeposit, setAvailableDeposit] = useState(defaultValue); + const { polkadotApi } = useApi(); + + const updateRateLimit = useCallback(() => { + if ( + isFunction(polkadotApi?.query.darwiniaStaking?.rateLimitState) && + isFunction(polkadotApi?.query.darwiniaStaking?.rateLimit) + ) { + return forkJoin([ + polkadotApi?.query.darwiniaStaking.rateLimitState() as unknown as Promise, + polkadotApi?.query.darwiniaStaking.rateLimit() as Promise, + ]).subscribe({ + next: ([rls, rl]) => { + const limit = rl.toBigInt(); + if (rls.isPos) { + const pos = rls.asPos.toBigInt(); + setAvailableWithdraw(limit + pos); + setAvailableDeposit(limit - pos); + } else { + const neg = rls.asNeg.toBigInt(); + setAvailableWithdraw(limit - neg); + setAvailableDeposit(limit + neg); + } + }, + error: (err) => { + console.error(err); + + setAvailableWithdraw(0n); + setAvailableDeposit(0n); + }, + }); + } else { + setAvailableWithdraw(defaultValue); + setAvailableDeposit(defaultValue); + } + }, [polkadotApi?.query.darwiniaStaking]); + + useEffect(() => { + const sub$$ = updateRateLimit(); + return () => { + sub$$?.unsubscribe(); + }; + }, [updateRateLimit]); + + return { availableWithdraw, availableDeposit, updateRateLimit }; +} diff --git a/src/providers/staking-provider.tsx b/src/providers/staking-provider.tsx index 9a355e4..795eb7d 100644 --- a/src/providers/staking-provider.tsx +++ b/src/providers/staking-provider.tsx @@ -11,9 +11,7 @@ import { useDeposits, useLedger, useNominatorCollators, - usePool, } from "@/hooks"; -import { stakingToPower } from "@/utils"; import { PropsWithChildren, createContext, useCallback, useEffect, useMemo, useState } from "react"; import type { Deposit, UnbondingInfo } from "@/types"; @@ -21,8 +19,6 @@ interface StakingCtx { deposits: Deposit[]; stakedDeposits: number[]; power: bigint; - ringPool: bigint; - ktonPool: bigint; stakedRing: bigint; stakedKton: bigint; stakedDeposit: bigint; @@ -41,8 +37,6 @@ interface StakingCtx { isLedgersInitialized: boolean; isDepositsInitialized: boolean; - isRingPoolInitialized: boolean; - isKtonPoolInitialized: boolean; isActiveCollatorsInitialized: boolean; isCollatorPowerInitialized: boolean; isCollatorSessionKeyInitialized: boolean; @@ -63,8 +57,6 @@ const defaultValue: StakingCtx = { deposits: [], stakedDeposits: [], power: 0n, - ringPool: 0n, - ktonPool: 0n, stakedRing: 0n, stakedKton: 0n, stakedDeposit: 0n, @@ -83,8 +75,6 @@ const defaultValue: StakingCtx = { isLedgersInitialized: false, isDepositsInitialized: false, - isRingPoolInitialized: false, - isKtonPoolInitialized: false, isActiveCollatorsInitialized: false, isCollatorPowerInitialized: false, isCollatorSessionKeyInitialized: false, @@ -110,7 +100,6 @@ export function StakingProvider({ children }: PropsWithChildren) { const { collatorLastSessionBlocks, isCollatorLastSessionBlocksInitialized } = useCollatorLastSessionBlocks(defaultValue); - const { ringPool, ktonPool, isRingPoolInitialized, isKtonPoolInitialized } = usePool(defaultValue); const { activeCollators, isActiveCollatorsInitialized } = useActiveCollators(defaultValue); const { deposits, isDepositsInitialized } = useDeposits(defaultValue); const { @@ -129,25 +118,11 @@ export function StakingProvider({ children }: PropsWithChildren) { const { nominatorCollators, isNominatorCollatorsInitialized, isNominatorCollatorsLoading, updateNominatorCollators } = useNominatorCollators(defaultValue); const { collatorNominators, isCollatorNominatorsInitialized } = useCollatorNominators(defaultValue); - const { collatorPower, isCollatorPowerInitialized } = useCollatorPower( - collatorNominators, - collatorCommission, - ringPool, - ktonPool, - defaultValue - ); + const { collatorPower, isCollatorPowerInitialized } = useCollatorPower(collatorNominators, defaultValue); - const power = useMemo( - () => stakingToPower(stakedRing + stakedDeposit, stakedKton, ringPool, ktonPool), - [stakedRing, stakedKton, ringPool, ktonPool, stakedDeposit] - ); + const power = useMemo(() => stakedRing + stakedDeposit, [stakedRing, stakedDeposit]); - const calcExtraPower = useCallback( - (stakingRing: bigint, stakingKton: bigint) => - stakingToPower(stakingRing, stakingKton, ringPool + stakingRing, ktonPool + stakingKton) - - stakingToPower(0n, 0n, ringPool, ktonPool), - [ringPool, ktonPool] - ); + const calcExtraPower = useCallback((stakingRing: bigint, stakingKton: bigint) => stakingRing + stakingKton, []); useEffect(() => { setMinimumDeposit(BigInt(polkadotApi?.consts.deposit.minLockingAmount.toString() || 0)); @@ -160,8 +135,6 @@ export function StakingProvider({ children }: PropsWithChildren) { power, deposits, stakedDeposits, - ringPool, - ktonPool, stakedRing, stakedKton, stakedDeposit, @@ -180,8 +153,6 @@ export function StakingProvider({ children }: PropsWithChildren) { isLedgersInitialized, isDepositsInitialized, - isRingPoolInitialized, - isKtonPoolInitialized, isActiveCollatorsInitialized, isCollatorPowerInitialized, isCollatorSessionKeyInitialized, diff --git a/src/types/staking.ts b/src/types/staking.ts index 6fd38b9..bdb5226 100644 --- a/src/types/staking.ts +++ b/src/types/staking.ts @@ -1,13 +1,16 @@ -import type { Struct, u16, u128, bool } from "@polkadot/types-codec"; +import type { Struct, Enum, u16, u128, bool } from "@polkadot/types-codec"; import type { Balance } from "@polkadot/types/interfaces"; export interface DarwiniaStakingLedger extends Struct { - stakedRing: string; - stakedKton: string; + stakedRing?: string; + stakedKton?: string; stakedDeposits?: number[]; unstakingDeposits?: [number, number][]; unstakingRing?: [number, number][]; unstakingKton?: [number, number][]; + + ring?: string; + deposits?: number[]; } export interface DepositCodec extends Struct { @@ -36,3 +39,10 @@ export interface UnbondingInfo { expiredTimestamp: number; // millisecond isExpired: boolean; } + +export interface DarwiniaStakingRateLimiter extends Enum { + readonly isPos: boolean; + readonly isNeg: boolean; + readonly asPos: Balance; + readonly asNeg: Balance; +}