diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index 15ed3ffdc..c6eaae8a2 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -1,35 +1,31 @@ import React, { useCallback, useRef } from "react" -import { ONE_SEC_IN_MILLISECONDS, queryKeysFactory } from "#/constants" import { useActionFlowPause, useActionFlowTokenAmount, useAppDispatch, + useBitcoinBalance, useCancelPromise, useDepositBTCTransaction, - useInvalidateQueries, useStakeFlowContext, useVerifyDepositAddress, } from "#/hooks" import { usePostHogCapture } from "#/hooks/posthog/usePostHogCapture" import { PostHogEvent } from "#/posthog/events" import { setStatus, setTxHash } from "#/store/action-flow" +import { ONE_SEC_IN_MILLISECONDS } from "#/constants" import { PROCESS_STATUSES } from "#/types" import { eip1193, logPromiseFailure } from "#/utils" import { useTimeout } from "@chakra-ui/react" import { useMutation } from "@tanstack/react-query" import WalletInteractionModal from "../WalletInteractionModal" -const { userKeys } = queryKeysFactory - export default function DepositBTCModal() { const tokenAmount = useActionFlowTokenAmount() const { btcAddress, depositReceipt, stake } = useStakeFlowContext() const verifyDepositAddress = useVerifyDepositAddress() const dispatch = useAppDispatch() const { handlePause } = useActionFlowPause() - const handleBitcoinBalanceInvalidation = useInvalidateQueries({ - queryKey: userKeys.balance(), - }) + const { refetch: refetchBitcoinBalance } = useBitcoinBalance() const { handleCapture, handleCaptureWithCause } = usePostHogCapture() const sessionId = useRef(Math.random()) @@ -39,9 +35,9 @@ export default function DepositBTCModal() { ) const onStakeBTCSuccess = useCallback(() => { - handleBitcoinBalanceInvalidation() + logPromiseFailure(refetchBitcoinBalance()) dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)) - }, [dispatch, handleBitcoinBalanceInvalidation]) + }, [dispatch, refetchBitcoinBalance]) const onError = useCallback( (error: unknown) => { diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 9cc68bc06..53784597f 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -3,40 +3,37 @@ import { useActionFlowPause, useActionFlowTokenAmount, useAppDispatch, + useBitcoinPosition, useCancelPromise, - useInvalidateQueries, useModal, useTimeout, useTransactionDetails, } from "#/hooks" -import { ACTION_FLOW_TYPES, PROCESS_STATUSES } from "#/types" -import { dateToUnixTimestamp, eip1193 } from "#/utils" +import { ACTION_FLOW_TYPES, Activity, PROCESS_STATUSES } from "#/types" +import { dateToUnixTimestamp, eip1193, logPromiseFailure } from "#/utils" import { setStatus } from "#/store/action-flow" import { useInitializeWithdraw } from "#/acre-react/hooks" import { ONE_SEC_IN_MILLISECONDS, queryKeysFactory } from "#/constants" -import { activityInitialized } from "#/store/wallet" -import { useMutation } from "@tanstack/react-query" +import { useMutation, useQueryClient } from "@tanstack/react-query" import { PostHogEvent } from "#/posthog/events" import { usePostHogCapture } from "#/hooks/posthog/usePostHogCapture" import BuildTransactionModal from "./BuildTransactionModal" import WalletInteractionModal from "../WalletInteractionModal" -const { userKeys } = queryKeysFactory - type WithdrawalStatus = "building-data" | "built-data" | "signature" export default function SignMessageModal() { const [status, setWaitingStatus] = useState("building-data") const dispatch = useAppDispatch() + const queryClient = useQueryClient() const tokenAmount = useActionFlowTokenAmount() const amount = tokenAmount?.amount const { closeModal } = useModal() const { handlePause } = useActionFlowPause() const initializeWithdraw = useInitializeWithdraw() - const handleBitcoinPositionInvalidation = useInvalidateQueries({ - queryKey: userKeys.position(), - }) + const { refetch: refetchBitcoinPosition } = useBitcoinPosition() + const sessionId = useRef(Math.random()) const { cancel, resolve, sessionIdToPromise } = useCancelPromise( sessionId.current, @@ -59,10 +56,10 @@ export default function SignMessageModal() { }, [resolve]) const onSignMessageSuccess = useCallback(() => { - handleBitcoinPositionInvalidation() + logPromiseFailure(refetchBitcoinPosition()) dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)) handleCapture(PostHogEvent.WithdrawalSuccess) - }, [dispatch, handleBitcoinPositionInvalidation, handleCapture]) + }, [dispatch, refetchBitcoinPosition, handleCapture]) const onSignMessageError = useCallback( (error: unknown) => { @@ -103,38 +100,44 @@ export default function SignMessageModal() { onSignMessageCallback, ) - dispatch( - activityInitialized({ - // Note that the withdraw id returned from the Acre SDK while fetching - // the withdrawals has the following pattern: - // `-`. The redemption key returned during the - // withdrawal initialization does not contain the `-` suffix - // because there may be delay between indexing the Acre subgraph and - // the time when a transaction was actually made and it's hard to get - // the exact number of the redemptions with the same key. Eg: - // - a user initialized a withdraw, - // - the Acre SDK is asking the subgraph for the number of withdrawals - // with the same redemption key, - // - the Acre subgraph may or may not be up to date with the chain and - // we are not sure if we should add +1 to the counter or the - // returned value already includes the requested withdraw from the - // first step. So we can't create the correct withdraw id. - // So here we set the id as a redemption key. Only one pending - // withdrawal can exist with the same redemption key, so when the user - // can initialize the next withdrawal with the same redemption key, we - // assume the dapp should already re-fetch all withdrawals with the - // correct IDs and move the `pending` redemption to `completed` - // section with the proper id. - id: redemptionKey, - type: "withdraw", - status: "pending", - // This is a requested amount. The amount of BTC received will be - // around: `amount - transactionFee.total`. - amount: amount - transactionFee.acre, - initializedAt: dateToUnixTimestamp(), - // The message is signed immediately after the initialization. - finalizedAt: dateToUnixTimestamp(), - }), + queryClient.setQueriesData( + { queryKey: queryKeysFactory.userKeys.activities() }, + (oldData: Activity[] | undefined) => { + const newActivity: Activity = { + // Note that the withdraw id returned from the Acre SDK while fetching + // the withdrawals has the following pattern: + // `-`. The redemption key returned during the + // withdrawal initialization does not contain the `-` suffix + // because there may be delay between indexing the Acre subgraph and + // the time when a transaction was actually made and it's hard to get + // the exact number of the redemptions with the same key. Eg: + // - a user initialized a withdraw, + // - the Acre SDK is asking the subgraph for the number of withdrawals + // with the same redemption key, + // - the Acre subgraph may or may not be up to date with the chain and + // we are not sure if we should add +1 to the counter or the + // returned value already includes the requested withdraw from the + // first step. So we can't create the correct withdraw id. + // So here we set the id as a redemption key. Only one pending + // withdrawal can exist with the same redemption key, so when the user + // can initialize the next withdrawal with the same redemption key, we + // assume the dapp should already re-fetch all withdrawals with the + // correct IDs and move the `pending` redemption to `completed` + // section with the proper id. + id: redemptionKey, + type: "withdraw", + status: "pending", + // This is a requested amount. The amount of BTC received will be + // around: `amount - transactionFee.total`. + amount: amount - transactionFee.acre, + initializedAt: dateToUnixTimestamp(), + // The message is signed immediately after the initialization. + finalizedAt: dateToUnixTimestamp(), + } + + if (oldData) return [newActivity, ...oldData] + return [newActivity] + }, ) }, onSuccess: onSignMessageSuccess, diff --git a/dapp/src/components/TransactionModal/index.tsx b/dapp/src/components/TransactionModal/index.tsx index dd30a73ad..92d805380 100644 --- a/dapp/src/components/TransactionModal/index.tsx +++ b/dapp/src/components/TransactionModal/index.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from "react" import { StakeFlowProvider } from "#/contexts" import { + useActivities, useAppDispatch, - useFetchActivities, useIsSignedMessage, useTransactionModal, } from "#/hooks" @@ -18,7 +18,7 @@ type TransactionModalProps = { type: ActionFlowType } & BaseModalProps function TransactionModalBase({ type, closeModal }: TransactionModalProps) { const dispatch = useAppDispatch() - const fetchActivities = useFetchActivities() + const { refetch: refetchActivities } = useActivities() useEffect(() => { dispatch(setType(type)) @@ -28,9 +28,9 @@ function TransactionModalBase({ type, closeModal }: TransactionModalProps) { useEffect(() => { return () => { dispatch(resetState()) - logPromiseFailure(fetchActivities()) + logPromiseFailure(refetchActivities()) } - }, [dispatch, fetchActivities]) + }, [dispatch, refetchActivities]) return ( diff --git a/dapp/src/constants/queryKeysFactory.ts b/dapp/src/constants/queryKeysFactory.ts index f7a543e0c..95a82ff5c 100644 --- a/dapp/src/constants/queryKeysFactory.ts +++ b/dapp/src/constants/queryKeysFactory.ts @@ -2,6 +2,7 @@ const userKeys = { all: ["user"] as const, balance: () => [...userKeys.all, "balance"] as const, position: () => [...userKeys.all, "position"] as const, + activities: () => [...userKeys.all, "activities"] as const, pointsData: () => [...userKeys.all, "points-data"] as const, } diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 5f3c7a336..03882703f 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -19,20 +19,23 @@ export * from "./useVerifyDepositAddress" export { default as useStatistics } from "./useStatistics" export * from "./useDisconnectWallet" export { default as useWalletConnectionAlert } from "./useWalletConnectionAlert" -export { default as useInvalidateQueries } from "./useInvalidateQueries" export { default as useResetWalletState } from "./useResetWalletState" export { default as useMobileMode } from "./useMobileMode" export { default as useBitcoinRecoveryAddress } from "./useBitcoinRecoveryAddress" export { default as useIsFetchedWalletData } from "./useIsFetchedWalletData" export { default as useLocalStorage } from "./useLocalStorage" export { default as useReferral } from "./useReferral" -export { default as useMats } from "./useMats" export { default as useIsEmbed } from "./useIsEmbed" export { default as useTriggerConnectWalletModal } from "./useTriggerConnectWalletModal" export { default as useLastUsedBtcAddress } from "./useLastUsedBtcAddress" -export { default as useAcrePoints } from "./useAcrePoints" +export { default as useAggregatedAcrePointsData } from "./useAggregatedAcrePointsData" export { default as useSignMessageAndCreateSession } from "./useSignMessageAndCreateSession" export { default as useAccessCode } from "./useAccessCode" export { default as useFormField } from "./useFormField" export { default as useDepositBTCTransaction } from "./useDepositBTCTransaction" export { default as useCancelPromise } from "./useCancelPromise" +export { default as useActivitiesCount } from "./useActivitiesCount" +export { default as useActivities } from "./useActivities" +export { default as useBitcoinBalance } from "./useBitcoinBalance" +export { default as useBitcoinPosition } from "./useBitcoinPosition" +export { default as useMats } from "./useMats" diff --git a/dapp/src/hooks/orangeKit/index.ts b/dapp/src/hooks/orangeKit/index.ts index 96fd2cb7a..dcc5160a5 100644 --- a/dapp/src/hooks/orangeKit/index.ts +++ b/dapp/src/hooks/orangeKit/index.ts @@ -4,4 +4,3 @@ export * from "./useConnectors" export * from "./useAccountsChangedUnisat" export * from "./useAccountsChangedOKX" export * from "./useAccountChangedOKX" -export { default as useBitcoinBalance } from "./useBitcoinBalance" diff --git a/dapp/src/hooks/sdk/index.ts b/dapp/src/hooks/sdk/index.ts index 0759a84ea..dca3d2ffa 100644 --- a/dapp/src/hooks/sdk/index.ts +++ b/dapp/src/hooks/sdk/index.ts @@ -1,6 +1,4 @@ export * from "./useInitializeAcreSdk" export * from "./useFetchMinDepositAmount" export * from "./useInitDataFromSdk" -export * from "./useFetchActivities" export * from "./useMinWithdrawAmount" -export { default as useBitcoinPosition } from "./useBitcoinPosition" diff --git a/dapp/src/hooks/sdk/useFetchActivities.ts b/dapp/src/hooks/sdk/useFetchActivities.ts deleted file mode 100644 index 0d35bcbcc..000000000 --- a/dapp/src/hooks/sdk/useFetchActivities.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback } from "react" -import { setActivities } from "#/store/wallet" -import { useAcreContext } from "#/acre-react/hooks" -import { Activity } from "#/types" -import { DepositStatus } from "@acre-btc/sdk" -import { useAppDispatch } from "../store/useAppDispatch" -import { useWallet } from "../useWallet" - -export function useFetchActivities() { - const dispatch = useAppDispatch() - const { address } = useWallet() - const { acre, isConnected } = useAcreContext() - - return useCallback(async () => { - if (!acre || !isConnected || !address) return - - const deposits: Activity[] = (await acre.account.getDeposits()).map( - (deposit) => ({ - ...deposit, - status: - deposit.status === DepositStatus.Finalized ? "completed" : "pending", - type: "deposit", - }), - ) - - const withdrawals: Activity[] = (await acre.account.getWithdrawals()).map( - (withdraw) => { - const { bitcoinTransactionId, status, ...rest } = withdraw - - return { - ...rest, - txHash: bitcoinTransactionId, - status: status === "finalized" ? "completed" : "pending", - type: "withdraw", - } - }, - ) - - dispatch(setActivities([...deposits, ...withdrawals])) - }, [acre, dispatch, isConnected, address]) -} diff --git a/dapp/src/hooks/sdk/useInitDataFromSdk.ts b/dapp/src/hooks/sdk/useInitDataFromSdk.ts index fdc19e82f..f912be723 100644 --- a/dapp/src/hooks/sdk/useInitDataFromSdk.ts +++ b/dapp/src/hooks/sdk/useInitDataFromSdk.ts @@ -1,24 +1,5 @@ -import { useEffect } from "react" -import { useInterval } from "@chakra-ui/react" -import { logPromiseFailure } from "#/utils" -import { REFETCH_INTERVAL_IN_MILLISECONDS } from "#/constants" import { useFetchMinDepositAmount } from "./useFetchMinDepositAmount" -import { useFetchActivities } from "./useFetchActivities" -import { useWallet } from "../useWallet" export function useInitDataFromSdk() { - const { address } = useWallet() - const fetchActivities = useFetchActivities() - - useEffect(() => { - if (address) { - logPromiseFailure(fetchActivities()) - } - }, [address, fetchActivities]) - useFetchMinDepositAmount() - useInterval( - () => logPromiseFailure(fetchActivities()), - REFETCH_INTERVAL_IN_MILLISECONDS, - ) } diff --git a/dapp/src/hooks/store/index.ts b/dapp/src/hooks/store/index.ts index 07b9a19c0..b88527385 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -6,8 +6,6 @@ export * from "./useActionFlowStatus" export * from "./useActionFlowActiveStep" export * from "./useActionFlowTokenAmount" export * from "./useActionFlowTxHash" -export * from "./useAllActivitiesCount" export * from "./useActionFlowPause" export * from "./useIsSignedMessage" -export { default as useHasFetchedActivities } from "./useHasFetchedActivities" -export { default as useActivities } from "./useActivities" +export { default as useWalletAddress } from "./useWalletAddress" diff --git a/dapp/src/hooks/store/useActivities.ts b/dapp/src/hooks/store/useActivities.ts deleted file mode 100644 index d1a04765c..000000000 --- a/dapp/src/hooks/store/useActivities.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { selectActivities, selectHasPendingActivities } from "#/store/wallet" -import { useAppSelector } from "./useAppSelector" - -export default function useActivities() { - const activities = useAppSelector(selectActivities) - const hasPendingActivities = useAppSelector(selectHasPendingActivities) - - return { activities, hasPendingActivities } -} diff --git a/dapp/src/hooks/store/useAllActivitiesCount.ts b/dapp/src/hooks/store/useAllActivitiesCount.ts deleted file mode 100644 index cbf4e711f..000000000 --- a/dapp/src/hooks/store/useAllActivitiesCount.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { selectAllActivitiesCount } from "#/store/wallet" -import { useAppSelector } from "./useAppSelector" - -export function useAllActivitiesCount() { - return useAppSelector(selectAllActivitiesCount) -} diff --git a/dapp/src/hooks/store/useHasFetchedActivities.ts b/dapp/src/hooks/store/useHasFetchedActivities.ts deleted file mode 100644 index 569978be9..000000000 --- a/dapp/src/hooks/store/useHasFetchedActivities.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { selectHasFetchedActivities } from "#/store/wallet" -import { useAppSelector } from "./useAppSelector" - -function useHasFetchedActivities() { - return useAppSelector(selectHasFetchedActivities) -} - -export default useHasFetchedActivities diff --git a/dapp/src/hooks/store/useWalletAddress.ts b/dapp/src/hooks/store/useWalletAddress.ts new file mode 100644 index 000000000..424d4cd8f --- /dev/null +++ b/dapp/src/hooks/store/useWalletAddress.ts @@ -0,0 +1,6 @@ +import { selectWalletAddress } from "#/store/wallet" +import { useAppSelector } from "./useAppSelector" + +export default function useWalletAddress() { + return useAppSelector(selectWalletAddress) +} diff --git a/dapp/src/hooks/useAcrePointsData.ts b/dapp/src/hooks/useAcrePointsData.ts new file mode 100644 index 000000000..f46474a27 --- /dev/null +++ b/dapp/src/hooks/useAcrePointsData.ts @@ -0,0 +1,13 @@ +import { queryKeysFactory, REFETCH_INTERVAL_IN_MILLISECONDS } from "#/constants" +import { useQuery } from "@tanstack/react-query" +import { acreApi } from "#/utils" + +const { acreKeys } = queryKeysFactory + +export default function useAcrePointsData() { + return useQuery({ + queryKey: [...acreKeys.pointsData()], + queryFn: async () => acreApi.getPointsData(), + refetchInterval: REFETCH_INTERVAL_IN_MILLISECONDS, + }) +} diff --git a/dapp/src/hooks/useActivities.ts b/dapp/src/hooks/useActivities.ts new file mode 100644 index 000000000..adeb004fe --- /dev/null +++ b/dapp/src/hooks/useActivities.ts @@ -0,0 +1,48 @@ +import { queryKeysFactory, REFETCH_INTERVAL_IN_MILLISECONDS } from "#/constants" +import { useQuery } from "@tanstack/react-query" +import { Activity } from "#/types" +import { DepositStatus } from "@acre-btc/sdk" +import { useAcreContext } from "#/acre-react/hooks" +import { sortActivitiesByTimestamp } from "#/utils" +import { useWallet } from "./useWallet" + +const { userKeys } = queryKeysFactory + +export default function useActivities() { + const { address } = useWallet() + const { acre, isConnected } = useAcreContext() + + return useQuery({ + queryKey: [...userKeys.activities(), { acre, isConnected, address }], + enabled: isConnected && !!acre && !!address, + queryFn: async () => { + if (!acre) return undefined + + const deposits: Activity[] = (await acre.account.getDeposits()).map( + (deposit) => ({ + ...deposit, + status: + deposit.status === DepositStatus.Finalized + ? "completed" + : "pending", + type: "deposit", + }), + ) + + const withdrawals: Activity[] = (await acre.account.getWithdrawals()).map( + (withdraw) => { + const { bitcoinTransactionId, status, ...rest } = withdraw + + return { + ...rest, + txHash: bitcoinTransactionId, + status: status === "finalized" ? "completed" : "pending", + type: "withdraw", + } + }, + ) + return sortActivitiesByTimestamp([...deposits, ...withdrawals]) + }, + refetchInterval: REFETCH_INTERVAL_IN_MILLISECONDS, + }) +} diff --git a/dapp/src/hooks/useActivitiesCount.ts b/dapp/src/hooks/useActivitiesCount.ts new file mode 100644 index 000000000..a1ef7dba8 --- /dev/null +++ b/dapp/src/hooks/useActivitiesCount.ts @@ -0,0 +1,6 @@ +import useActivities from "./useActivities" + +export default function useActivitiesCount() { + const { data } = useActivities() + return data ? data.length : 0 +} diff --git a/dapp/src/hooks/useAcrePoints.ts b/dapp/src/hooks/useAggregatedAcrePointsData.ts similarity index 72% rename from dapp/src/hooks/useAcrePoints.ts rename to dapp/src/hooks/useAggregatedAcrePointsData.ts index eb5f71126..3e7309a31 100644 --- a/dapp/src/hooks/useAcrePoints.ts +++ b/dapp/src/hooks/useAggregatedAcrePointsData.ts @@ -1,15 +1,14 @@ -import { useMutation, useQuery } from "@tanstack/react-query" +import { useMutation } from "@tanstack/react-query" import { acreApi } from "#/utils" -import { queryKeysFactory, REFETCH_INTERVAL_IN_MILLISECONDS } from "#/constants" import { MODAL_TYPES } from "#/types" import { PostHogEvent } from "#/posthog/events" import { useWallet } from "./useWallet" import { useModal } from "./useModal" import { usePostHogCapture } from "./posthog/usePostHogCapture" +import useUserPointsData from "./useUserPointsData" +import useAcrePointsData from "./useAcrePointsData" -const { userKeys, acreKeys } = queryKeysFactory - -type UseAcrePointsReturnType = { +type UseAggregatedAcrePointsDataReturnType = { totalBalance: number claimableBalance: number nextDropTimestamp?: number @@ -20,22 +19,13 @@ type UseAcrePointsReturnType = { totalPoolBalance: number } -export default function useAcrePoints(): UseAcrePointsReturnType { +export default function useAggregatedAcrePointsData(): UseAggregatedAcrePointsDataReturnType { const { ethAddress = "" } = useWallet() const { openModal } = useModal() const { handleCapture, handleCaptureWithCause } = usePostHogCapture() - const userPointsDataQuery = useQuery({ - queryKey: [...userKeys.pointsData(), ethAddress], - enabled: !!ethAddress, - queryFn: async () => acreApi.getPointsDataByUser(ethAddress), - }) - - const pointsDataQuery = useQuery({ - queryKey: [...acreKeys.pointsData()], - queryFn: async () => acreApi.getPointsData(), - refetchInterval: REFETCH_INTERVAL_IN_MILLISECONDS, - }) + const userPointsDataQuery = useUserPointsData() + const pointsDataQuery = useAcrePointsData() const { mutate: claimPoints } = useMutation({ mutationFn: async () => acreApi.claimPoints(ethAddress), diff --git a/dapp/src/hooks/orangeKit/useBitcoinBalance.ts b/dapp/src/hooks/useBitcoinBalance.ts similarity index 72% rename from dapp/src/hooks/orangeKit/useBitcoinBalance.ts rename to dapp/src/hooks/useBitcoinBalance.ts index b7ed40eb8..d3f17aee6 100644 --- a/dapp/src/hooks/orangeKit/useBitcoinBalance.ts +++ b/dapp/src/hooks/useBitcoinBalance.ts @@ -1,10 +1,12 @@ import { useQuery } from "@tanstack/react-query" import { REFETCH_INTERVAL_IN_MILLISECONDS, queryKeysFactory } from "#/constants" -import { useBitcoinProvider } from "./useBitcoinProvider" +import useWalletAddress from "./store/useWalletAddress" +import { useBitcoinProvider } from "./orangeKit/useBitcoinProvider" const { userKeys } = queryKeysFactory -export default function useBitcoinBalance(address: string | undefined) { +export default function useBitcoinBalance() { + const address = useWalletAddress() const provider = useBitcoinProvider() return useQuery({ diff --git a/dapp/src/hooks/sdk/useBitcoinPosition.ts b/dapp/src/hooks/useBitcoinPosition.ts similarity index 95% rename from dapp/src/hooks/sdk/useBitcoinPosition.ts rename to dapp/src/hooks/useBitcoinPosition.ts index f1036ef1a..8f208b958 100644 --- a/dapp/src/hooks/sdk/useBitcoinPosition.ts +++ b/dapp/src/hooks/useBitcoinPosition.ts @@ -1,7 +1,7 @@ import { useAcreContext } from "#/acre-react/hooks" import { useQuery } from "@tanstack/react-query" import { REFETCH_INTERVAL_IN_MILLISECONDS, queryKeysFactory } from "#/constants" -import { useWallet } from "../useWallet" +import { useWallet } from "./useWallet" const { userKeys } = queryKeysFactory diff --git a/dapp/src/hooks/useInvalidateQueries.ts b/dapp/src/hooks/useInvalidateQueries.ts deleted file mode 100644 index 2f64ae0f1..000000000 --- a/dapp/src/hooks/useInvalidateQueries.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { logPromiseFailure } from "#/utils" -import { QueryClient, useQueryClient } from "@tanstack/react-query" -import { useCallback } from "react" - -type InvalidateQueriesParams = Parameters - -export default function useInvalidateQueries( - ...params: InvalidateQueriesParams -) { - const queryClient = useQueryClient() - - return useCallback( - () => logPromiseFailure(queryClient.invalidateQueries(...params)), - [params, queryClient], - ) -} diff --git a/dapp/src/hooks/useIsFetchedWalletData.ts b/dapp/src/hooks/useIsFetchedWalletData.ts index 1d69280d4..0f2a99868 100644 --- a/dapp/src/hooks/useIsFetchedWalletData.ts +++ b/dapp/src/hooks/useIsFetchedWalletData.ts @@ -1,19 +1,21 @@ -import { queryKeysFactory } from "#/constants" -import { useIsFetching } from "@tanstack/react-query" -import { useHasFetchedActivities, useIsSignedMessage } from "./store" - -const { userKeys } = queryKeysFactory +import { useIsSignedMessage } from "./store" +import useActivities from "./useActivities" +import useBitcoinBalance from "./useBitcoinBalance" +import useBitcoinPosition from "./useBitcoinPosition" +import useUserPointsData from "./useUserPointsData" export default function useIsFetchedWalletData() { const isSignedMessage = useIsSignedMessage() - const hasFetchedActivities = useHasFetchedActivities() - const fetchingQueries = useIsFetching({ - queryKey: userKeys.all, - predicate: (query) => query.state.data === undefined, - }) + const { isFetched: isBitcoinBalanceFetched } = useBitcoinBalance() + const { isFetched: isBitcoinPositionFetched } = useBitcoinPosition() + const { isFetched: isActivitiesFetched } = useActivities() + const { isFetched: isPointsDataFetched } = useUserPointsData() + + const isFetchedData = + isBitcoinBalanceFetched && + isActivitiesFetched && + isBitcoinPositionFetched && + isPointsDataFetched - return ( - (isSignedMessage && fetchingQueries === 0 && hasFetchedActivities) || - !isSignedMessage - ) + return (isSignedMessage && isFetchedData) || !isSignedMessage } diff --git a/dapp/src/hooks/useUserPointsData.ts b/dapp/src/hooks/useUserPointsData.ts new file mode 100644 index 000000000..9e6e2cad2 --- /dev/null +++ b/dapp/src/hooks/useUserPointsData.ts @@ -0,0 +1,16 @@ +import { queryKeysFactory } from "#/constants" +import { useQuery } from "@tanstack/react-query" +import { acreApi } from "#/utils" +import { useWallet } from "./useWallet" + +const { userKeys } = queryKeysFactory + +export default function useUserPointsData() { + const { ethAddress = "" } = useWallet() + + return useQuery({ + queryKey: [...userKeys.pointsData(), ethAddress], + enabled: !!ethAddress, + queryFn: async () => acreApi.getPointsDataByUser(ethAddress), + }) +} diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts index 504b0a453..8125fd51a 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -16,11 +16,12 @@ import { Status, } from "#/types" import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useDispatch, useSelector } from "react-redux" -import { selectWalletAddress, setAddress } from "#/store/wallet" -import useBitcoinBalance from "./orangeKit/useBitcoinBalance" +import { useDispatch } from "react-redux" +import { setAddress } from "#/store/wallet" import useResetWalletState from "./useResetWalletState" import useLastUsedBtcAddress from "./useLastUsedBtcAddress" +import useBitcoinBalance from "./useBitcoinBalance" +import { useWalletAddress } from "./store" const { typeConversionToConnector, typeConversionToOrangeKitConnector } = orangeKit @@ -45,12 +46,12 @@ type UseWalletReturn = { export function useWallet(): UseWalletReturn { const queryClient = useQueryClient() const dispatch = useDispatch() - const btcAddress = useSelector(selectWalletAddress) + const btcAddress = useWalletAddress() const resetWalletState = useResetWalletState() const { setAddressInLocalStorage, removeAddressFromLocalStorage } = useLastUsedBtcAddress() - const { data: balance } = useBitcoinBalance(btcAddress) + const { data: balance } = useBitcoinBalance() const chainId = useChainId() const config = useConfig() diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard.tsx index b49bd45f4..4b4f8be46 100644 --- a/dapp/src/pages/DashboardPage/AcrePointsCard.tsx +++ b/dapp/src/pages/DashboardPage/AcrePointsCard.tsx @@ -12,7 +12,7 @@ import { } from "@chakra-ui/react" import Countdown from "#/components/shared/Countdown" import { logPromiseFailure, numberToLocaleString } from "#/utils" -import { useAcrePoints, useWallet } from "#/hooks" +import { useAggregatedAcrePointsData, useWallet } from "#/hooks" import Spinner from "#/components/shared/Spinner" import UserDataSkeleton from "#/components/shared/UserDataSkeleton" import TooltipIcon from "#/components/shared/TooltipIcon" @@ -37,7 +37,7 @@ export default function AcrePointsCard(props: CardProps) { updatePointsData, isCalculationInProgress, totalPoolBalance, - } = useAcrePoints() + } = useAggregatedAcrePointsData() const { isConnected } = useWallet() const debouncedClaimPoints = useDebounce(claimPoints, ONE_SEC_IN_MILLISECONDS) diff --git a/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx b/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx index ad75cefdd..32b8e13a1 100644 --- a/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx +++ b/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx @@ -1,6 +1,6 @@ import React from "react" import { Box, HStack, StackProps, VStack } from "@chakra-ui/react" -import { useAllActivitiesCount, useStatistics, useWallet } from "#/hooks" +import { useActivitiesCount, useStatistics, useWallet } from "#/hooks" import { IconBolt } from "@tabler/icons-react" import { TextMd } from "#/components/shared/Typography" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" @@ -10,7 +10,7 @@ type AcreTVLMessageProps = Omit export default function AcreTVLMessage(props: AcreTVLMessageProps) { const { tvl } = useStatistics() const { isConnected } = useWallet() - const activitiesCount = useAllActivitiesCount() + const activitiesCount = useActivitiesCount() const isFirstTimeUser = activitiesCount === 0 diff --git a/dapp/src/pages/DashboardPage/PositionDetails.tsx b/dapp/src/pages/DashboardPage/PositionDetails.tsx index d1839940a..d403519f6 100644 --- a/dapp/src/pages/DashboardPage/PositionDetails.tsx +++ b/dapp/src/pages/DashboardPage/PositionDetails.tsx @@ -1,7 +1,7 @@ import React from "react" import { CurrencyBalanceWithConversion } from "#/components/shared/CurrencyBalanceWithConversion" import { - useAllActivitiesCount, + useActivitiesCount, useBitcoinPosition, useTransactionModal, useStatistics, @@ -24,6 +24,7 @@ import { featureFlags } from "#/constants" import { TextMd } from "#/components/shared/Typography" import { IconClockHour5Filled } from "@tabler/icons-react" import TooltipIcon from "#/components/shared/TooltipIcon" +import { hasPendingDeposits } from "#/utils" import AcreTVLMessage from "./AcreTVLMessage" const isWithdrawalFlowEnabled = featureFlags.WITHDRAWALS_ENABLED @@ -39,12 +40,13 @@ const buttonStyles: ButtonProps = { } export default function PositionDetails() { - const { data } = useBitcoinPosition() - const bitcoinAmount = data?.estimatedBitcoinBalance ?? 0n + const { data: bitcoinPosition } = useBitcoinPosition() + const bitcoinAmount = bitcoinPosition?.estimatedBitcoinBalance ?? 0n const openDepositModal = useTransactionModal(ACTION_FLOW_TYPES.STAKE) const openWithdrawModal = useTransactionModal(ACTION_FLOW_TYPES.UNSTAKE) - const activitiesCount = useAllActivitiesCount() + const activitiesCount = useActivitiesCount() + const { data: activities } = useActivities() const isMobileMode = useMobileMode() const { tvl } = useStatistics() @@ -54,15 +56,13 @@ export default function PositionDetails() { const isDisabledForMobileMode = isMobileMode && !featureFlags.MOBILE_MODE_ENABLED - const { hasPendingActivities } = useActivities() - return ( {/* TODO: Component should be moved to `CardHeader` */} Your Acre balance - {hasPendingActivities && ( + {hasPendingDeposits(activities ?? []) && ( + {(pageData: Activity[]) => pageData.map((activity) => ( diff --git a/dapp/src/pages/DashboardPage/TransactionHistory/index.tsx b/dapp/src/pages/DashboardPage/TransactionHistory/index.tsx index 960d7f363..539dffdf3 100644 --- a/dapp/src/pages/DashboardPage/TransactionHistory/index.tsx +++ b/dapp/src/pages/DashboardPage/TransactionHistory/index.tsx @@ -1,13 +1,13 @@ import React from "react" import { StackProps, VStack, Image } from "@chakra-ui/react" import { TextMd } from "#/components/shared/Typography" -import { useAllActivitiesCount, useIsFetchedWalletData } from "#/hooks" +import { useActivitiesCount, useIsFetchedWalletData } from "#/hooks" import UserDataSkeleton from "#/components/shared/UserDataSkeleton" import emptyStateIllustration from "#/assets/images/empty-state.svg" import TransactionTable from "./TransactionTable" function TransactionHistoryContent() { - const activitiesCount = useAllActivitiesCount() + const activitiesCount = useActivitiesCount() const isFetchedWalletData = useIsFetchedWalletData() if (!isFetchedWalletData) diff --git a/dapp/src/store/wallet/walletSelector.ts b/dapp/src/store/wallet/walletSelector.ts index 9e2372513..9df077205 100644 --- a/dapp/src/store/wallet/walletSelector.ts +++ b/dapp/src/store/wallet/walletSelector.ts @@ -1,5 +1,3 @@ -import { createSelector } from "@reduxjs/toolkit" -import { sortActivitiesByTimestamp } from "#/utils" import { RootState } from ".." export const selectEstimatedBtcBalance = (state: RootState): bigint => @@ -8,30 +6,8 @@ export const selectEstimatedBtcBalance = (state: RootState): bigint => export const selectSharesBalance = (state: RootState): bigint => state.wallet.sharesBalance -export const selectActivities = createSelector( - (state: RootState) => state.wallet.activities, - (activities) => sortActivitiesByTimestamp(activities), -) - -export const selectHasPendingActivities = createSelector( - (state: RootState) => state.wallet.activities, - (activities) => - activities.some( - (activity) => - activity.status === "pending" && activity.type === "deposit", - ), -) - -export const selectAllActivitiesCount = createSelector( - (state: RootState) => state.wallet.activities, - (activities) => activities.length, -) - export const selectIsSignedMessage = (state: RootState): boolean => state.wallet.isSignedMessage -export const selectHasFetchedActivities = (state: RootState): boolean => - state.wallet.hasFetchedActivities - export const selectWalletAddress = (state: RootState): string | undefined => state.wallet.address diff --git a/dapp/src/store/wallet/walletSlice.ts b/dapp/src/store/wallet/walletSlice.ts index fb0296445..a067fbcca 100644 --- a/dapp/src/store/wallet/walletSlice.ts +++ b/dapp/src/store/wallet/walletSlice.ts @@ -1,12 +1,9 @@ -import { Activity } from "#/types" import { PayloadAction, createSlice } from "@reduxjs/toolkit" export type WalletState = { estimatedBtcBalance: bigint sharesBalance: bigint isSignedMessage: boolean - activities: Activity[] - hasFetchedActivities: boolean address: string | undefined } @@ -14,8 +11,6 @@ export const initialState: WalletState = { estimatedBtcBalance: 0n, sharesBalance: 0n, isSignedMessage: false, - activities: [], - hasFetchedActivities: false, address: undefined, } @@ -32,15 +27,7 @@ export const walletSlice = createSlice({ setIsSignedMessage(state, action: PayloadAction) { state.isSignedMessage = action.payload }, - setActivities(state, action: PayloadAction) { - state.activities = action.payload - state.hasFetchedActivities = true - }, resetState: (state) => ({ ...initialState, address: state.address }), - activityInitialized(state, action: PayloadAction) { - const activity = action.payload - state.activities = [...state.activities, activity] - }, setAddress(state, action: PayloadAction) { state.address = action.payload }, @@ -51,9 +38,7 @@ export const { setSharesBalance, setEstimatedBtcBalance, setIsSignedMessage, - setActivities, resetState, - activityInitialized, setAddress, } = walletSlice.actions export default walletSlice.reducer diff --git a/dapp/src/utils/activities.ts b/dapp/src/utils/activities.ts index a6d1716da..8d599f64b 100644 --- a/dapp/src/utils/activities.ts +++ b/dapp/src/utils/activities.ts @@ -9,6 +9,11 @@ export const isActivityCompleted = (activity: Activity): boolean => export const getActivityTimestamp = (activity: Activity): number => activity?.finalizedAt ?? activity.initializedAt +export const hasPendingDeposits = (activities: Activity[]): boolean => + activities.some( + (activity) => activity.status === "pending" && activity.type === "deposit", + ) + export const sortActivitiesByTimestamp = (activities: Activity[]): Activity[] => [...activities].sort( (activity1, activity2) => diff --git a/dapp/test/store/walletSlice.test.ts b/dapp/test/store/walletSlice.test.ts deleted file mode 100644 index 8fbea1955..000000000 --- a/dapp/test/store/walletSlice.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest" -import { Activity } from "#/types" -import reducer, { - WalletState, - initialState, - setActivities, -} from "#/store/wallet/walletSlice" -import { dateToUnixTimestamp, randomInteger } from "#/utils" - -const createActivity = (overrides: Partial = {}): Activity => ({ - id: crypto.randomUUID(), - initializedAt: dateToUnixTimestamp() - randomInteger(0, 1000000), - amount: BigInt(randomInteger(1000000, 1000000000)), - txHash: "c9625ecc138bbd241439f158f65f43e152968ff35e203dec89cfb78237d6a2d8", - status: "completed", - type: "deposit", - ...overrides, -}) - -const isSignedMessage = false -const hasFetchedActivities = true -const pendingActivityId = "0" -const pendingActivity = createActivity({ - id: pendingActivityId, - status: "pending", -}) - -const activities = [ - pendingActivity, - createActivity({ id: "1" }), - createActivity({ id: "2" }), -] - -describe("Wallet redux slice", () => { - describe("deposits", () => { - let state: WalletState - - beforeEach(() => { - state = { - ...initialState, - activities, - isSignedMessage, - hasFetchedActivities, - } - }) - - it("should update activities when the status of item changes", () => { - const newActivities = [...activities] - const completedActivity: Activity = { - ...pendingActivity, - status: "completed", - } - const foundIndex = newActivities.findIndex( - ({ id }) => id === pendingActivityId, - ) - newActivities[foundIndex] = completedActivity - - expect(reducer(state, setActivities(newActivities))).toEqual({ - ...initialState, - activities: newActivities, - isSignedMessage, - hasFetchedActivities, - }) - }) - }) - - describe("withdrawals", () => { - let state: WalletState - const pendingWithdrawRedemptionKey = - "0x047078deab9f2325ce5adc483d6b28dfb32547017ffb73f857482b51b622d5eb" - const pendingWithdrawActivity = createActivity({ - // After the successful withdrawal flow we set the id to redemption key - // w/o the `-` suffix because it's hard to get the exact number of - // withdrawals with the same redemption key. There can only be one pending - // withdrawal with the same redemption key at a time. - id: pendingWithdrawRedemptionKey, - status: "pending", - type: "withdraw", - }) - - // Let's assume the user has already made 2 withdrawals and these 2 - // withdrawals have the same redemption key as the newly created. Both are - // completed. - const currentActivities = [ - createActivity({ - type: "withdraw", - id: `${pendingWithdrawRedemptionKey}-1`, - }), - createActivity({ - type: "withdraw", - id: `${pendingWithdrawRedemptionKey}-2`, - }), - ] - - describe("when withdrawal is still pending", () => { - // This is our pending withdrawal but with the full id with the `-` - // suffix returned by backend. - const pendingWithdrawActivityWithFullId = { - ...pendingWithdrawActivity, - id: `${pendingWithdrawRedemptionKey}-3`, - } - // The new data returned from the backend and they includes our pending - // withdrawal. - const newActivities = [ - ...currentActivities, - pendingWithdrawActivityWithFullId, - ] - - beforeEach(() => { - state = { - ...initialState, - activities: currentActivities, - isSignedMessage, - hasFetchedActivities, - } - }) - - it("should not update pending withdraw state and should set correct id", () => { - expect(reducer(state, setActivities(newActivities))).toEqual({ - ...initialState, - activities: newActivities, - isSignedMessage, - hasFetchedActivities, - }) - }) - }) - - describe("when withdrawal is already complete", () => { - const withdrawActivityCompleted: Activity = { - ...pendingWithdrawActivity, - status: "completed", - id: `${pendingWithdrawRedemptionKey}-3`, - } - - // Let's assume the pending withdrawal is already completed and the - // backend returns it but with the full id. Note that the pending activity - // is still in the `latestActivities` map but w/o the full id (id is - // redemption key). - const newActivities = [...currentActivities, withdrawActivityCompleted] - - beforeEach(() => { - state = { - ...initialState, - activities: currentActivities, - isSignedMessage, - } - }) - - it("should mark the latest pending withdraw activity as completed", () => { - expect(reducer(state, setActivities(newActivities))).toEqual({ - ...initialState, - activities: newActivities, - isSignedMessage, - hasFetchedActivities, - }) - }) - }) - }) -})