diff --git a/dapp/.env b/dapp/.env deleted file mode 100644 index fdb71b35d..000000000 --- a/dapp/.env +++ /dev/null @@ -1,30 +0,0 @@ -VITE_USE_TESTNET=true - -# Configuration of sentry.io -VITE_SENTRY_SUPPORT=false -# TODO: Sentry DSN will be added during the application building process when it is ready -VITE_SENTRY_DSN="" - -# TODO: Use a more general source -VITE_ETH_HOSTNAME_HTTP="https://sepolia.infura.io/v3/c80e8ccdcc4c4a809bce4fc165310617" -VITE_REFERRAL=0 - -# ENDPOINTS -VITE_TBTC_API_ENDPOINT="" -VITE_ACRE_API_ENDPOINT="http://localhost:8788/api/v1/" - -# API KEYS -VITE_GELATO_RELAY_API_KEY="htaJCy_XHj8WsE3w53WBMurfySDtjLP_TrNPPa6IPIc_" # this key should not be used on production -# Get the API key from: https://thegraph.com/studio/apikeys/. -VITE_SUBGRAPH_API_KEY="" - -# Feature flags -VITE_FEATURE_FLAG_GAMIFICATION_ENABLED="false" -VITE_FEATURE_FLAG_WITHDRAWALS_ENABLED="false" -VITE_FEATURE_FLAG_OKX_WALLET_ENABLED="false" -VITE_FEATURE_FLAG_XVERSE_WALLET_ENABLED="false" -VITE_FEATURE_FLAG_ACRE_POINTS_ENABLED="true" -VITE_FEATURE_FLAG_TVL_ENABLED="true" -VITE_FEATURE_GATING_DAPP_ENABLED="true" -VITE_FEATURE_MOBILE_MODE_ENABLED="true" - diff --git a/dapp/.env.example b/dapp/.env.example new file mode 100644 index 000000000..745d7f59b --- /dev/null +++ b/dapp/.env.example @@ -0,0 +1,32 @@ +# Network +VITE_USE_TESTNET=true + +# Basic UI settings +VITE_REFERRAL=0 + +# Endpoints +VITE_ETH_HOSTNAME_HTTP="" +VITE_ACRE_API_ENDPOINT="http://localhost:8788/api/v1/" +VITE_TBTC_API_ENDPOINT="http://localhost:8788/tbtc-api/v1/" + +# API keys +VITE_GELATO_RELAY_API_KEY="htaJCy_XHj8WsE3w53WBMurfySDtjLP_TrNPPa6IPIc_" # this key should not be used on production +VITE_SUBGRAPH_API_KEY="" + +# Sentry +VITE_SENTRY_SUPPORT=false +VITE_SENTRY_DSN="" + +# Posthog +VITE_POSTHOG_API_HOST="https://us.i.posthog.com" +VITE_POSTHOG_API_KEY="" + +# Feature flags +VITE_FEATURE_FLAG_WITHDRAWALS_ENABLED="true" +VITE_FEATURE_FLAG_OKX_WALLET_ENABLED="true" +VITE_FEATURE_FLAG_XVERSE_WALLET_ENABLED="true" +VITE_FEATURE_FLAG_ACRE_POINTS_ENABLED="true" +VITE_FEATURE_FLAG_TVL_ENABLED="true" +VITE_FEATURE_GATING_DAPP_ENABLED="true" +VITE_FEATURE_POSTHOG_ENABLED="false" +VITE_FEATURE_MOBILE_MODE_ENABLED="true" diff --git a/dapp/README.md b/dapp/README.md index 349464532..3d3f02761 100644 --- a/dapp/README.md +++ b/dapp/README.md @@ -1,10 +1,10 @@ # Acre dApp -The application is integrate with OrangeKit and allows people to earn yield on their Bitcoin via yield farming on Ethereum. +The application is integrated with OrangeKit and allows people to earn yield on their Bitcoin via yield farming on Ethereum. This project was bootstrapped with [Create Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite). -To access the dApp in Ledger Live import manifest as described in the +To access the dApp in Ledger Live import the manifest as described in the [Ledger Live Setup](#ledger-live-setup) section. ### Development @@ -29,7 +29,7 @@ Install dependencies and start the dApp: ### Environmental variables -To make sure dApp is running correctly, include the following variables in `.env` file: +To make sure dApp is running correctly, include the following variables in the `.env` file: ```bash VITE_TBTC_API_ENDPOINT= diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-development.json b/dapp/manifests/ledger-live/ledger-live-manifest-development.json index 84e58fcad..bffaf7026 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-development.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-development.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json index 281992191..22f74bbc5 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json index 32dd4a45f..a6ea2019c 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-manifest-template.json b/dapp/manifests/ledger-live/ledger-manifest-template.json index e999601d2..980c407d2 100644 --- a/dapp/manifests/ledger-live/ledger-manifest-template.json +++ b/dapp/manifests/ledger-live/ledger-manifest-template.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/package.json b/dapp/package.json index 97601a1a3..b5db82c64 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -23,8 +23,8 @@ "@emotion/styled": "^11.11.0", "@ledgerhq/wallet-api-acre-module": "0.1.0", "@ledgerhq/wallet-api-client": "1.6.0", - "@orangekit/react": "1.0.0-beta.33", - "@orangekit/sign-in-with-wallet": "1.0.0-beta.6", + "@orangekit/react": "1.0.0-beta.34", + "@orangekit/sign-in-with-wallet": "1.0.0-beta.7", "@reduxjs/toolkit": "^2.2.0", "@rehooks/local-storage": "^2.4.5", "@safe-global/safe-core-sdk-types": "^5.0.1", @@ -40,6 +40,7 @@ "framer-motion": "^10.16.5", "luxon": "^3.5.0", "mustache": "^4.2.0", + "posthog-js": "^1.186.1", "react": "^18.2.0", "react-confetti-explosion": "^2.1.2", "react-dom": "^18.2.0", diff --git a/dapp/src/DApp.tsx b/dapp/src/DApp.tsx index 31255b53d..51149f8f7 100644 --- a/dapp/src/DApp.tsx +++ b/dapp/src/DApp.tsx @@ -7,11 +7,7 @@ import { QueryClientProvider } from "@tanstack/react-query" import { ReactQueryDevtools } from "@tanstack/react-query-devtools" import { AcreSdkProvider } from "./acre-react/contexts" import GlobalStyles from "./components/GlobalStyles" -import { - DocsDrawerContextProvider, - SidebarContextProvider, - WalletConnectionErrorContextProvider, -} from "./contexts" +import { WalletConnectionAlertContextProvider } from "./contexts" import { useInitApp } from "./hooks" import { router } from "./router" import { store } from "./store" @@ -19,6 +15,7 @@ import getWagmiConfig from "./wagmiConfig" import queryClient from "./queryClient" import { delay, logPromiseFailure } from "./utils" import { AcreLogo } from "./assets/icons" +import PostHogProvider from "./posthog/PostHogProvider" function SplashPage() { return ( @@ -65,15 +62,13 @@ function DAppProviders() { - - - - - - - - - + + + + + + + diff --git a/dapp/src/assets/icons/ArrowUpRight.tsx b/dapp/src/assets/icons/ArrowUpRight.tsx deleted file mode 100644 index a61f54b38..000000000 --- a/dapp/src/assets/icons/ArrowUpRight.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react" -import { createIcon } from "@chakra-ui/react" - -export const ArrowUpRight = createIcon({ - displayName: "ArrowUpRight", - viewBox: "0 0 16 17", - path: ( - - ), -}) diff --git a/dapp/src/assets/icons/BoltFilled.tsx b/dapp/src/assets/icons/BoltFilled.tsx deleted file mode 100644 index 8252422d9..000000000 --- a/dapp/src/assets/icons/BoltFilled.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react" -import { createIcon } from "@chakra-ui/react" - -export default createIcon({ - displayName: "BoltFilled", - viewBox: "0 0 24 24", - path: [ - , - ], -}) diff --git a/dapp/src/assets/icons/index.ts b/dapp/src/assets/icons/index.ts index 7a8fe57c2..eb50fc819 100644 --- a/dapp/src/assets/icons/index.ts +++ b/dapp/src/assets/icons/index.ts @@ -1,4 +1,3 @@ -export * from "./ArrowUpRight" export * from "./AcreLogo" export * from "./Pause" export { default as LoadingSpinnerSuccessIcon } from "./LoadingSpinnerSuccessIcon" @@ -8,4 +7,3 @@ export * from "./MezoSignIcon" export * from "./AcreSignIcon" export * from "./BitcoinsStackErrorIcon" export { default as MatsIcon } from "./MatsIcon" -export { default as BoltFilled } from "./BoltFilled" diff --git a/dapp/src/assets/images/benefits/bibos-beehive.svg b/dapp/src/assets/images/benefits/bibos-beehive.svg deleted file mode 100644 index 784888e3c..000000000 --- a/dapp/src/assets/images/benefits/bibos-beehive.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dapp/src/assets/images/benefits/index.ts b/dapp/src/assets/images/benefits/index.ts deleted file mode 100644 index 072363998..000000000 --- a/dapp/src/assets/images/benefits/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as rewardsBoostImage } from "#/assets/images/benefits/rewards-boost.svg" -export { default as seasonKeyImage } from "#/assets/images/benefits/season-key.svg" -export { default as rewardsBoostArrowImage } from "#/assets/images/benefits/rewards-boost-arrow.svg" -export { default as bibosBeehiveImage } from "#/assets/images/benefits/bibos-beehive.svg" diff --git a/dapp/src/assets/images/benefits/rewards-boost-arrow.svg b/dapp/src/assets/images/benefits/rewards-boost-arrow.svg deleted file mode 100644 index b8cb985cf..000000000 --- a/dapp/src/assets/images/benefits/rewards-boost-arrow.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dapp/src/assets/images/benefits/rewards-boost.svg b/dapp/src/assets/images/benefits/rewards-boost.svg deleted file mode 100644 index 70d7f5144..000000000 --- a/dapp/src/assets/images/benefits/rewards-boost.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dapp/src/assets/images/benefits/season-key.svg b/dapp/src/assets/images/benefits/season-key.svg deleted file mode 100644 index e40917ff3..000000000 --- a/dapp/src/assets/images/benefits/season-key.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dapp/src/assets/images/mezo-beehive-modal-illustration.svg b/dapp/src/assets/images/mezo-beehive-modal-illustration.svg deleted file mode 100644 index 18ee51bba..000000000 --- a/dapp/src/assets/images/mezo-beehive-modal-illustration.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dapp/src/assets/images/season-section-background.png b/dapp/src/assets/images/season-section-background.png deleted file mode 100644 index a6f2dfbdc..000000000 Binary files a/dapp/src/assets/images/season-section-background.png and /dev/null differ diff --git a/dapp/src/assets/images/season-section-foreground.png b/dapp/src/assets/images/season-section-foreground.png deleted file mode 100644 index e8554ba51..000000000 Binary files a/dapp/src/assets/images/season-section-foreground.png and /dev/null differ diff --git a/dapp/src/assets/webps/confetti.webp b/dapp/src/assets/webps/confetti.webp deleted file mode 100644 index f99786801..000000000 Binary files a/dapp/src/assets/webps/confetti.webp and /dev/null differ diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletAlert.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletAlert.tsx new file mode 100644 index 000000000..44a499d87 --- /dev/null +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletAlert.tsx @@ -0,0 +1,118 @@ +import React from "react" +import { AlertStatus, Box, Link, VStack } from "@chakra-ui/react" +import { AnimatePresence, Variants, motion } from "framer-motion" +import { EXTERNAL_HREF } from "#/constants" +import { + Alert, + AlertDescription, + AlertTitle, + AlertIcon, + AlertProps, +} from "../shared/Alert" + +export enum ConnectionAlert { + Rejected = "REJECTED", + NotSupported = "NOT_SUPPORTED", + NetworkMismatch = "NETWORK_MISMATCH", + InvalidSIWWSignature = "INVALID_SIWW_SIGNATURE", + Default = "DEFAULT", +} + +type ConnectionAlertData = { + title: string + description?: React.ReactNode + status?: AlertStatus + colorScheme?: string +} + +type ConnectionAlerts = Record + +function ContactSupport() { + return ( + + If the problem persists, contact{" "} + + Acre support + + . + + ) +} + +const CONNECTION_ALERTS: ConnectionAlerts = { + [ConnectionAlert.Rejected]: { + title: "Please connect your wallet to start using Acre", + status: "info", + colorScheme: "blue", + }, + [ConnectionAlert.NotSupported]: { + title: "Not supported.", + description: + "Only Native SegWit, Nested SegWit, or Legacy addresses are supported. Please use a compatible address or switch to a different wallet.", + }, + [ConnectionAlert.NetworkMismatch]: { + title: "Incorrect network detected in your wallet.", + description: + "Please connect your wallet to the correct Bitcoin network and try again.", + }, + [ConnectionAlert.Default]: { + title: "Wallet connection failed. Please try again.", + description: , + }, + [ConnectionAlert.InvalidSIWWSignature]: { + title: "Invalid sign-in signature. Please try again.", + description: , + }, +} + +const collapseVariants: Variants = { + collapsed: { height: 0 }, + expanded: { height: "auto" }, +} + +type ConnectWalletAlertProps = Omit & { + type?: ConnectionAlert +} + +export default function ConnectWalletAlert(props: ConnectWalletAlertProps) { + const { type, ...restProps } = props + + const { + status = "error", + title, + description, + ...restData + } = (type ? CONNECTION_ALERTS[type] : {}) as ConnectionAlertData + + return ( + + {type && ( + + + + + {title} + {description && ( + {description} + )} + + + + )} + + ) +} diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx index 0747fdc8e..b950b2b2c 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useEffect, useRef, useState } from "react" -import { CONNECTION_ERRORS, ONE_SEC_IN_MILLISECONDS } from "#/constants" +import { ONE_SEC_IN_MILLISECONDS } from "#/constants" import { useAppDispatch, useIsEmbed, useModal, useSignMessageAndCreateSession, useWallet, - useWalletConnectionError, + useWalletConnectionAlert, } from "#/hooks" import { setIsSignedMessage } from "#/store/wallet" import { OrangeKitConnector, OrangeKitError, OnSuccessCallback } from "#/types" @@ -24,10 +24,12 @@ import { } from "@chakra-ui/react" import { IconArrowNarrowRight } from "@tabler/icons-react" import { AnimatePresence, Variants, motion } from "framer-motion" +import { usePostHogIdentity } from "#/hooks/posthog" import ArrivingSoonTooltip from "../ArrivingSoonTooltip" import { TextLg, TextMd } from "../shared/Typography" import ConnectWalletStatusLabel from "./ConnectWalletStatusLabel" import Spinner from "../shared/Spinner" +import { ConnectionAlert } from "./ConnectWalletAlert" type ConnectWalletButtonProps = { label: string @@ -66,15 +68,16 @@ export default function ConnectWalletButton({ } = useWallet() const { signMessageStatus, resetMessageStatus, signMessageAndCreateSession } = useSignMessageAndCreateSession() - const { connectionError, setConnectionError, resetConnectionError } = - useWalletConnectionError() + const { type, setConnectionAlert, resetConnectionAlert } = + useWalletConnectionAlert() const { closeModal } = useModal() const dispatch = useAppDispatch() const isMounted = useRef(false) + const { handleIdentification } = usePostHogIdentity() const [isLoading, setIsLoading] = useState(false) - const hasConnectionError = connectionError || connectionStatus === "error" + const hasConnectionError = type || connectionStatus === "error" const hasSignMessageErrorStatus = signMessageStatus === "error" const shouldShowStatuses = isSelected && !hasConnectionError const shouldShowRetryButton = address && hasSignMessageErrorStatus @@ -99,14 +102,14 @@ export default function ConnectWalletButton({ onDisconnect() console.error("Failed to sign siww message", error) - setConnectionError(CONNECTION_ERRORS.INVALID_SIWW_SIGNATURE) + setConnectionAlert(ConnectionAlert.InvalidSIWWSignature) } }, [ signMessageAndCreateSession, onSuccessSignMessage, onDisconnect, - setConnectionError, + setConnectionAlert, ], ) @@ -117,8 +120,11 @@ export default function ConnectWalletButton({ if (!btcAddress) return await handleSignMessageAndCreateSession(connector, btcAddress) + handleIdentification(btcAddress, { + connector: connectedConnector.id, + }) }, - [connector, handleSignMessageAndCreateSession], + [connector, handleSignMessageAndCreateSession, handleIdentification], ) const handleConnection = useCallback(() => { @@ -129,14 +135,19 @@ export default function ConnectWalletButton({ }, onError: (error: OrangeKitError) => { const errorData = orangeKit.parseOrangeKitConnectionError(error) - setConnectionError(errorData) + + if (errorData === ConnectionAlert.Default) { + console.error("Failed to connect", error) + } + + setConnectionAlert(errorData) }, }) }, [ onConnect, connector, onSuccessConnection, - setConnectionError, + setConnectionAlert, isReconnecting, ]) @@ -159,7 +170,7 @@ export default function ConnectWalletButton({ if (shouldShowStatuses) return if (!isReconnecting) onDisconnect() - resetConnectionError() + resetConnectionAlert() resetMessageStatus() const isInstalled = orangeKit.isWalletInstalled(connector) diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx deleted file mode 100644 index 2e97b59ea..000000000 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react" -import { Box, VStack } from "@chakra-ui/react" -import { AnimatePresence, Variants, motion } from "framer-motion" -import { ConnectionErrorData } from "#/types" -import { - Alert, - AlertDescription, - AlertTitle, - AlertIcon, - AlertProps, -} from "../shared/Alert" - -type ConnectWalletErrorAlertProps = AlertProps & Partial - -const collapseVariants: Variants = { - collapsed: { height: 0 }, - expanded: { height: "auto" }, -} - -export default function ConnectWalletErrorAlert( - props: ConnectWalletErrorAlertProps, -) { - const { title, description, ...restProps } = props - - const shouldRender = !!(title && description) - - return ( - - {shouldRender && ( - - - - - {title} - {description} - - - - )} - - ) -} diff --git a/dapp/src/components/ConnectWalletModal/index.tsx b/dapp/src/components/ConnectWalletModal/index.tsx index 66816f2be..23404c9b9 100644 --- a/dapp/src/components/ConnectWalletModal/index.tsx +++ b/dapp/src/components/ConnectWalletModal/index.tsx @@ -5,13 +5,13 @@ import { useIsEmbed, useIsSignedMessage, useWallet, - useWalletConnectionError, + useWalletConnectionAlert, } from "#/hooks" import { OrangeKitConnector, BaseModalProps, OnSuccessCallback } from "#/types" import { wallets } from "#/constants" import withBaseModal from "../ModalRoot/withBaseModal" import ConnectWalletButton from "./ConnectWalletButton" -import ConnectWalletErrorAlert from "./ConnectWalletErrorAlert" +import ConnectWalletAlert from "./ConnectWalletAlert" export function ConnectWalletModalBase({ onSuccess, @@ -30,7 +30,7 @@ export function ConnectWalletModalBase({ })) const [selectedConnectorId, setSelectedConnectorId] = useState() - const { connectionError, resetConnectionError } = useWalletConnectionError() + const { type, resetConnectionAlert } = useWalletConnectionAlert() const isSignedMessage = useIsSignedMessage() const handleButtonOnClick = (connector: OrangeKitConnector) => { @@ -48,7 +48,7 @@ export function ConnectWalletModalBase({ {withCloseButton && ( { - resetConnectionError() + resetConnectionAlert() if (!isSignedMessage) { onDisconnect() @@ -59,7 +59,7 @@ export function ConnectWalletModalBase({ {`Select your ${isEmbed ? "account" : "wallet"}`} - + {enabledConnectors.map((connector) => ( - - - - {/* TODO: Add a documentation */} - Documentation - - - - ) -} diff --git a/dapp/src/components/Footer.tsx b/dapp/src/components/Footer.tsx index d80161d7f..c32c4c7ed 100644 --- a/dapp/src/components/Footer.tsx +++ b/dapp/src/components/Footer.tsx @@ -10,8 +10,9 @@ import { Icon, } from "@chakra-ui/react" import { EXTERNAL_HREF } from "#/constants" -import { AcreSignIcon, ArrowUpRight } from "#/assets/icons" +import { AcreSignIcon } from "#/assets/icons" import { useMobileMode } from "#/hooks" +import { IconArrowUpRight } from "@tabler/icons-react" type FooterListItem = Pick @@ -64,7 +65,7 @@ const getItemsList = ( as={Link} __css={styles.link} iconSpacing={0} - rightIcon={} + rightIcon={} {...link} isExternal /> diff --git a/dapp/src/components/GlobalStyles/index.tsx b/dapp/src/components/GlobalStyles.tsx similarity index 100% rename from dapp/src/components/GlobalStyles/index.tsx rename to dapp/src/components/GlobalStyles.tsx diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index b1b598746..8040b47da 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -5,11 +5,15 @@ import { HStack, Icon, IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, StackDivider, useClipboard, useMultiStyleConfig, } from "@chakra-ui/react" -import { useIsEmbed, useModal, useWallet } from "#/hooks" +import { useIsEmbed, useMobileMode, useModal, useWallet } from "#/hooks" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { BitcoinIcon } from "#/assets/icons" @@ -21,8 +25,11 @@ import { IconLogout, IconWallet, IconUserCode, + IconChevronDown, + IconChevronUp, } from "@tabler/icons-react" import { useMatch } from "react-router-dom" +import { usePostHogIdentity } from "#/hooks/posthog" import Tooltip from "../shared/Tooltip" function isChangeAccountFeatureSupported(embeddedApp: string | undefined) { @@ -39,11 +46,18 @@ export default function ConnectWallet() { size: "lg", }) const isDashboardPage = useMatch("/dashboard") + const { resetIdentity } = usePostHogIdentity() + const isMobile = useMobileMode() const handleConnectWallet = (isReconnecting: boolean = false) => { openModal(MODAL_TYPES.CONNECT_WALLET, { isReconnecting }) } + const handleDisconnectWallet = () => { + onDisconnect() + resetIdentity() + } + if (!address) { return ( diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 915a9436a..53784597f 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -1,66 +1,49 @@ -import React, { useCallback, useEffect, useRef, useState } from "react" +import React, { useCallback, useRef, useState } from "react" import { useActionFlowPause, useActionFlowTokenAmount, useAppDispatch, - useInvalidateQueries, + useBitcoinPosition, + useCancelPromise, 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" -const sessionIdToPromise: Record< - number, - { - promise: Promise - cancel: (reason: Error) => void - shouldOpenErrorModal: boolean - } -> = {} - 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, + "Withdrawal cancelled", + ) const { transactionFee } = useTransactionDetails( amount, ACTION_FLOW_TYPES.UNSTAKE, ) - - useEffect(() => { - let cancel = (_: Error) => {} - const promise: Promise = new Promise((_, reject) => { - cancel = reject - }) - - sessionIdToPromise[sessionId.current] = { - cancel, - promise, - shouldOpenErrorModal: true, - } - }, []) + const { handleCapture, handleCaptureWithCause } = usePostHogCapture() const dataBuiltStepCallback = useCallback(() => { setWaitingStatus("built-data") @@ -69,16 +52,14 @@ export default function SignMessageModal() { const onSignMessageCallback = useCallback(async () => { setWaitingStatus("signature") - return Promise.race([ - sessionIdToPromise[sessionId.current].promise, - Promise.resolve(), - ]) - }, []) + return resolve() + }, [resolve]) const onSignMessageSuccess = useCallback(() => { - handleBitcoinPositionInvalidation() + logPromiseFailure(refetchBitcoinPosition()) dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)) - }, [dispatch, handleBitcoinPositionInvalidation]) + handleCapture(PostHogEvent.WithdrawalSuccess) + }, [dispatch, refetchBitcoinPosition, handleCapture]) const onSignMessageError = useCallback( (error: unknown) => { @@ -97,8 +78,15 @@ export default function SignMessageModal() { } else { onSignMessageError(error) } + + handleCaptureWithCause(error, PostHogEvent.WithdrawalFailure) }, - [onSignMessageError, handlePause], + [ + sessionIdToPromise, + handlePause, + onSignMessageError, + handleCaptureWithCause, + ], ) const { mutate: handleSignMessage } = useMutation({ @@ -112,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, @@ -151,16 +145,7 @@ export default function SignMessageModal() { }) const onClose = () => { - const currentSessionId = sessionId.current - const sessionData = sessionIdToPromise[currentSessionId] - sessionIdToPromise[currentSessionId] = { - ...sessionData, - shouldOpenErrorModal: false, - } - - sessionIdToPromise[currentSessionId].cancel( - new Error("Withdrawal cancelled"), - ) + cancel() closeModal() } diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/UnstakeDetails.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/UnstakeDetails.tsx index b457f1347..4ac3b86ad 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/UnstakeDetails.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/UnstakeDetails.tsx @@ -1,6 +1,5 @@ import React from "react" -import { Flex, List } from "@chakra-ui/react" -import TransactionDetailsAmountItem from "#/components/shared/TransactionDetails/AmountItem" +import { List } from "@chakra-ui/react" import { TOKEN_AMOUNT_FIELD_NAME } from "#/components/shared/TokenAmountForm/TokenAmountFormBase" import { useFormField, @@ -8,18 +7,12 @@ import { useTransactionDetails, } from "#/hooks" import { ACTION_FLOW_TYPES, CurrencyType } from "#/types" -import { DESIRED_DECIMALS_FOR_FEE, featureFlags } from "#/constants" -import FeesDetailsAmountItem from "#/components/shared/FeesDetails/FeesItem" -import WithdrawWarning from "./WithdrawWarning" +import { DESIRED_DECIMALS_FOR_FEE } from "#/constants" +import FeesDetailsAmountItem from "#/components/shared/FeesDetails/FeesDetailsAmountItem" +import TransactionDetailsAmountItem from "#/components/shared/TransactionDetails/TransactionDetailsAmountItem" import { FeesTooltip } from "../../FeesTooltip" -function UnstakeDetails({ - balance, - currency, -}: { - balance: bigint - currency: CurrencyType -}) { +function UnstakeDetails({ currency }: { currency: CurrencyType }) { const { value = 0n } = useFormField( TOKEN_AMOUNT_FIELD_NAME, ) @@ -30,35 +23,30 @@ function UnstakeDetails({ const { total, ...restFees } = details.transactionFee return ( - - {featureFlags.GAMIFICATION_ENABLED && ( - - )} - - } - from={{ - currency, - amount: total, - desiredDecimals: DESIRED_DECIMALS_FOR_FEE, - withRoundUp: true, - }} - to={{ - currency: "usd", - }} - /> - - - + + } + from={{ + currency, + amount: total, + desiredDecimals: DESIRED_DECIMALS_FOR_FEE, + withRoundUp: true, + }} + to={{ + currency: "usd", + }} + /> + + ) } diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/WithdrawWarning.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/WithdrawWarning.tsx deleted file mode 100644 index fb835df86..000000000 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/WithdrawWarning.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react" -import { Box } from "@chakra-ui/react" -import { CurrencyType } from "#/types" -import { MINIMUM_BALANCE } from "#/constants" -import { formatSatoshiAmount, getCurrencyByType } from "#/utils" -import { TextMd } from "#/components/shared/Typography" -import { TOKEN_AMOUNT_FIELD_NAME } from "#/components/shared/TokenAmountForm/TokenAmountFormBase" -import { Alert, AlertTitle, AlertIcon } from "#/components/shared/Alert" -import { useFormField } from "#/hooks" - -function WithdrawWarning({ - balance, - currency, -}: { - balance: bigint - currency: CurrencyType -}) { - const { value, isValid } = useFormField( - TOKEN_AMOUNT_FIELD_NAME, - ) - const amount = value ?? 0n - - const { symbol } = getCurrencyByType(currency) - - const minimumBalanceText = `${formatSatoshiAmount( - MINIMUM_BALANCE, - )} ${symbol} ` - - const newBalance = balance - amount - const isMinimumBalanceExceeded = newBalance < MINIMUM_BALANCE - - if (isMinimumBalanceExceeded && isValid) { - return ( - - - - - The new balance is below the required minimum of - {minimumBalanceText}. Withdrawing your funds - will result in the loss of your current rewards. - - - ) - } - - return ( - - - - - A minimum balance of - {minimumBalanceText} is required to keep all - rewards active. - - - ) -} - -export default WithdrawWarning diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/index.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/index.tsx index afab79189..a27f677d1 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/index.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/UnstakeFormModal/index.tsx @@ -38,7 +38,7 @@ function UnstakeFormModal({ withMaxButton defaultAmount={defaultAmount} > - + Withdraw diff --git a/dapp/src/components/TransactionModal/FeesTooltip/FeesTooltip.tsx b/dapp/src/components/TransactionModal/FeesTooltip/FeesTooltip.tsx index d976bf810..4c630bbbd 100644 --- a/dapp/src/components/TransactionModal/FeesTooltip/FeesTooltip.tsx +++ b/dapp/src/components/TransactionModal/FeesTooltip/FeesTooltip.tsx @@ -1,6 +1,6 @@ import React from "react" import { List } from "@chakra-ui/react" -import InfoTooltip from "#/components/shared/InfoTooltip" +import TooltipIcon from "#/components/shared/TooltipIcon" import { FeesTooltipItem } from "./FeesTooltipItem" import { Fee as AcreFee } from "../../../types/fee" @@ -21,7 +21,7 @@ const mapFeeToLabel = (feeId: keyof AcreFee) => { export function FeesTooltip({ fees }: Props) { return ( - diff --git a/dapp/src/components/TransactionModal/ResumeModal.tsx b/dapp/src/components/TransactionModal/ResumeModal.tsx index 16cb45999..3547673dc 100644 --- a/dapp/src/components/TransactionModal/ResumeModal.tsx +++ b/dapp/src/components/TransactionModal/ResumeModal.tsx @@ -22,7 +22,7 @@ export default function ResumeModal({ closeModal }: BaseModalProps) { return ( <> - + Paused diff --git a/dapp/src/components/TransactionModal/SuccessModal.tsx b/dapp/src/components/TransactionModal/SuccessModal.tsx index e03d49658..7fa7f8d31 100644 --- a/dapp/src/components/TransactionModal/SuccessModal.tsx +++ b/dapp/src/components/TransactionModal/SuccessModal.tsx @@ -32,7 +32,7 @@ export default function SuccessModal({ type }: SuccessModalProps) { return ( <> - + {ACTION_FLOW_TYPES.UNSTAKE === type ? "Withdrawal initiated!" : "Deposit received!"} diff --git a/dapp/src/components/TransactionModal/UnsupportedBitcoinAddressModal.tsx b/dapp/src/components/TransactionModal/UnsupportedBitcoinAddressModal.tsx deleted file mode 100644 index ffca86507..000000000 --- a/dapp/src/components/TransactionModal/UnsupportedBitcoinAddressModal.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from "react" -import { - Box, - Button, - ModalBody, - ModalCloseButton, - ModalFooter, - ModalHeader, - Tag, -} from "@chakra-ui/react" -import { TextMd, TextSm } from "#/components/shared/Typography" -import { logPromiseFailure } from "#/utils" -import { BitcoinIcon } from "#/assets/icons" -import { CurrencyBalance } from "../shared/CurrencyBalance" -import { Alert, AlertIcon } from "../shared/Alert" - -type UnsupportedBitcoinAddressModalProps = { - account?: { - name: string - balance: bigint - } - requestAccount: () => Promise -} - -export default function UnsupportedBitcoinAddressModal({ - account, - requestAccount, -}: UnsupportedBitcoinAddressModalProps) { - const handleClick = () => { - logPromiseFailure(requestAccount()) - } - - return ( - <> - - - Account not supported - - - - {account && ( - - - - - - {account.name} - - - - - - Unsupported - - - )} - - - We currently support Legacy,{" "} - Native SegWit and Nested SegWit{" "} - accounts only. - - - - - - - - ) -} diff --git a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx index dbbf4c53b..8c3926d1f 100644 --- a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx +++ b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx @@ -10,8 +10,8 @@ import { ProgressProps, } from "@chakra-ui/react" import { AcreSignIcon } from "#/assets/icons" -import { useActionFlowType, useConnector } from "#/hooks" -import { ACTION_FLOW_TYPES } from "#/types" +import { useActionFlowType, useConnector, useIsEmbed } from "#/hooks" +import { ACTION_FLOW_TYPES, DappMode } from "#/types" import { Alert, AlertIcon } from "../shared/Alert" import { TextMd } from "../shared/Typography" @@ -22,11 +22,16 @@ const ICON_STYLES = { type WalletInteractionStep = "opening-wallet" | "awaiting-transaction" +const CONTENT_BY_DAPP_MODE: Record = { + standalone: "wallet", + "ledger-live": "Ledger Device", +} + const DATA: Record< WalletInteractionStep, { header: string - description: (action: string) => string + description: (action: string, mode: DappMode) => string progressProps?: ProgressProps } > = { @@ -37,24 +42,28 @@ const DATA: Record< }, "awaiting-transaction": { header: "Awaiting signature confirmation", - description: () => "Waiting for your wallet to confirm the transaction.", + description: (_, mode: DappMode) => + `Communicating with your ${CONTENT_BY_DAPP_MODE[mode]}...`, progressProps: { transform: "scaleX(-1)" }, }, } export default function WalletInteractionModal({ step, + onClose, }: { step: WalletInteractionStep + onClose?: () => void }) { const actionType = useActionFlowType() const connector = useConnector() const { header, description, progressProps } = DATA[step] + const { embeddedApp } = useIsEmbed() return ( <> - {step === "opening-wallet" && } - + {step === "opening-wallet" && } + {header} @@ -78,6 +87,7 @@ export default function WalletInteractionModal({ {description( actionType === ACTION_FLOW_TYPES.STAKE ? "deposit" : "withdraw", + embeddedApp ?? "standalone", )} {step === "awaiting-transaction" && ( @@ -85,7 +95,6 @@ export default function WalletInteractionModal({ This may take up to a minute. - Don't close this window. )} 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/components/WelcomeModal.tsx b/dapp/src/components/WelcomeModal.tsx index d29d17fdf..b68a6ce75 100644 --- a/dapp/src/components/WelcomeModal.tsx +++ b/dapp/src/components/WelcomeModal.tsx @@ -136,7 +136,7 @@ function WelcomeModalBase({ closeModal }: BaseModalProps) { {activeStepData.title} - + {activeStepData.content(embeddedApp)} - } - {...props} - > - {children} - - ) -} diff --git a/dapp/src/components/shared/CurrencyBalance/index.tsx b/dapp/src/components/shared/CurrencyBalance.tsx similarity index 98% rename from dapp/src/components/shared/CurrencyBalance/index.tsx rename to dapp/src/components/shared/CurrencyBalance.tsx index bf7f65750..1b5aa3be7 100644 --- a/dapp/src/components/shared/CurrencyBalance/index.tsx +++ b/dapp/src/components/shared/CurrencyBalance.tsx @@ -12,7 +12,7 @@ import { numberToLocaleString, } from "#/utils" import { CurrencyType, AmountType } from "#/types" -import Tooltip from "../Tooltip" +import Tooltip from "./Tooltip" export type CurrencyBalanceProps = { currency: CurrencyType diff --git a/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx b/dapp/src/components/shared/CurrencyBalanceWithConversion.tsx similarity index 85% rename from dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx rename to dapp/src/components/shared/CurrencyBalanceWithConversion.tsx index 5d19dd126..ea9f64850 100644 --- a/dapp/src/components/shared/CurrencyBalanceWithConversion/index.tsx +++ b/dapp/src/components/shared/CurrencyBalanceWithConversion.tsx @@ -1,6 +1,6 @@ import React from "react" import { useCurrencyConversion } from "#/hooks" -import { CurrencyBalance, CurrencyBalanceProps } from "../CurrencyBalance" +import { CurrencyBalance, CurrencyBalanceProps } from "./CurrencyBalance" export function CurrencyBalanceWithConversion({ from, diff --git a/dapp/src/components/shared/FeesDetails/FeesItem.tsx b/dapp/src/components/shared/FeesDetails/FeesDetailsAmountItem.tsx similarity index 100% rename from dapp/src/components/shared/FeesDetails/FeesItem.tsx rename to dapp/src/components/shared/FeesDetails/FeesDetailsAmountItem.tsx diff --git a/dapp/src/components/shared/IconTag.tsx b/dapp/src/components/shared/IconTag.tsx deleted file mode 100644 index 7c2f8b51e..000000000 --- a/dapp/src/components/shared/IconTag.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react" -import { Tag, TagLeftIcon, TagLabel, TagProps, Icon } from "@chakra-ui/react" - -type IconTagProps = TagProps & { - icon: typeof Icon -} - -export default function IconTag(props: IconTagProps) { - const { children, icon, ...restProps } = props - - return ( - - - {children} - - ) -} diff --git a/dapp/src/components/shared/NavLink.tsx b/dapp/src/components/shared/NavLink.tsx deleted file mode 100644 index 305bbfe53..000000000 --- a/dapp/src/components/shared/NavLink.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react" -import { - Link as ChakraLink, - LinkProps as ChakraLinkProps, -} from "@chakra-ui/react" -import { - NavLink as RouterNavLink, - NavLinkProps as RouterNavLinkProps, -} from "react-router-dom" - -export type NavLinkProps = Omit & - Pick - -export function NavLink(props: NavLinkProps) { - const { children, ...restProps } = props - return ( - - {children as React.ReactNode} - - ) -} diff --git a/dapp/src/components/shared/NumberFormatInput/index.tsx b/dapp/src/components/shared/NumberFormatInput.tsx similarity index 100% rename from dapp/src/components/shared/NumberFormatInput/index.tsx rename to dapp/src/components/shared/NumberFormatInput.tsx diff --git a/dapp/src/components/shared/ProgressBar.tsx b/dapp/src/components/shared/ProgressBar.tsx index e5467bc2d..ca8b52c1d 100644 --- a/dapp/src/components/shared/ProgressBar.tsx +++ b/dapp/src/components/shared/ProgressBar.tsx @@ -1,6 +1,6 @@ import React from "react" import { Progress, ProgressProps, ProgressLabel, Icon } from "@chakra-ui/react" -import { BoltFilled } from "#/assets/icons" +import { IconBolt } from "@tabler/icons-react" type ProgressBarProps = ProgressProps & { withBoltIcon?: boolean @@ -23,7 +23,8 @@ function ProgressBar(props: ProgressBarProps) { transform="auto" translateX="-100%" translateY="-50%" - as={BoltFilled} + as={IconBolt} + fill="currentcolor" mx={-1} /> )} diff --git a/dapp/src/components/shared/SeasonSectionBackground.tsx b/dapp/src/components/shared/SeasonSectionBackground.tsx deleted file mode 100644 index 027775059..000000000 --- a/dapp/src/components/shared/SeasonSectionBackground.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useRef } from "react" -import { Box, BoxProps } from "@chakra-ui/react" -import { useSize } from "@chakra-ui/react-use-size" -import { - MotionValue, - motion, - useScroll, - useSpring, - useTransform, - useTime, - wrap, -} from "framer-motion" -import seasonBackground from "#/assets/images/season-section-background.png" -import seasonForeground from "#/assets/images/season-section-foreground.png" - -export function SeasonSectionBackground(props: BoxProps) { - const containerRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: containerRef, - offset: ["center start", "start end"], - }) - const smoothScrollYProgress = useSpring(scrollYProgress, { - damping: 10, - stiffness: 90, - mass: 0.75, - }) as MotionValue - const foregroundParallax = useTransform( - smoothScrollYProgress, - [0, 1], - ["45%", "65%"], - ) - const time = useTime() - // Seed value is wrapped to prevent infinite increment causing potential memory leaks - const seed = useTransform(time, (value) => wrap(0, 2137, Math.floor(value))) - - const size = useSize(containerRef) - - return ( - - - - - - - - - - - - - - - - - - ) -} diff --git a/dapp/src/components/shared/Skeleton/index.tsx b/dapp/src/components/shared/Skeleton.tsx similarity index 100% rename from dapp/src/components/shared/Skeleton/index.tsx rename to dapp/src/components/shared/Skeleton.tsx diff --git a/dapp/src/components/shared/Spinner/index.tsx b/dapp/src/components/shared/Spinner.tsx similarity index 100% rename from dapp/src/components/shared/Spinner/index.tsx rename to dapp/src/components/shared/Spinner.tsx diff --git a/dapp/src/components/shared/TokenBalanceInput/index.tsx b/dapp/src/components/shared/TokenBalanceInput.tsx similarity index 96% rename from dapp/src/components/shared/TokenBalanceInput/index.tsx rename to dapp/src/components/shared/TokenBalanceInput.tsx index cd92ff160..69067f35d 100644 --- a/dapp/src/components/shared/TokenBalanceInput/index.tsx +++ b/dapp/src/components/shared/TokenBalanceInput.tsx @@ -21,9 +21,9 @@ import { useCurrencyConversion } from "#/hooks" import NumberFormatInput, { NumberFormatInputValues, NumberFormatInputProps, -} from "../NumberFormatInput" -import { CurrencyBalance } from "../CurrencyBalance" -import HelperErrorText, { HelperErrorTextProps } from "../Form/HelperErrorText" +} from "./NumberFormatInput" +import { CurrencyBalance } from "./CurrencyBalance" +import HelperErrorText, { HelperErrorTextProps } from "./Form/HelperErrorText" type FiatCurrencyBalanceProps = { amount: bigint diff --git a/dapp/src/components/shared/Tooltip.tsx b/dapp/src/components/shared/Tooltip.tsx index 924e0e950..6c6944593 100644 --- a/dapp/src/components/shared/Tooltip.tsx +++ b/dapp/src/components/shared/Tooltip.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react" -import { Box, Tooltip as ChakraTooltip, TooltipProps } from "@chakra-ui/react" +import { Tooltip as ChakraTooltip, TooltipProps, Flex } from "@chakra-ui/react" export default function Tooltip(props: TooltipProps) { const { children, ...restProps } = props @@ -7,13 +7,13 @@ export default function Tooltip(props: TooltipProps) { return ( - setIsOpen(true)} onMouseLeave={() => setIsOpen(false)} onClick={() => setIsOpen(true)} > {children} - + ) } diff --git a/dapp/src/components/shared/InfoTooltip.tsx b/dapp/src/components/shared/TooltipIcon.tsx similarity index 50% rename from dapp/src/components/shared/InfoTooltip.tsx rename to dapp/src/components/shared/TooltipIcon.tsx index a9394b460..1ef97745f 100644 --- a/dapp/src/components/shared/InfoTooltip.tsx +++ b/dapp/src/components/shared/TooltipIcon.tsx @@ -1,16 +1,21 @@ import React from "react" -import { IconInfoCircleFilled } from "@tabler/icons-react" +import { IconInfoCircleFilled, TablerIcon } from "@tabler/icons-react" import { Icon, TooltipProps } from "@chakra-ui/react" import Tooltip from "./Tooltip" // TODO: Define in the new color palette const ICON_COLOR = "#3A3328" -export default function InfoTooltip(props: Omit) { +type TooltipIconProps = Omit & { + icon?: TablerIcon +} + +export default function TooltipIcon(props: TooltipIconProps) { + const { icon, ...restProps } = props return ( - + = { - REJECTED: { - title: "Wallet connection rejected.", - description: "If you encountered an error, please try again.", - }, - NOT_SUPPORTED: { - title: "Not supported.", - description: - "Only Native SegWit, Nested SegWit or Legacy addresses supported at this time. Please try a different address or another wallet.", - }, - NETWORK_MISMATCH: { - title: "Error!", - description: - "Incorrect network detected in your wallet. Please choose proper network and try again.", - }, - DEFAULT: { - title: "Something went wrong...", - description: "We encountered an error. Please try again.", - }, - INVALID_SIWW_SIGNATURE: { - title: "Invalid Sign In With Wallet signature", - description: "We encountered an error. Please try again.", - }, -} +import { ACTION_FLOW_TYPES } from "#/types" export const TOKEN_FORM_ERRORS = { REQUIRED: "Please enter an amount.", diff --git a/dapp/src/constants/featureFlags.ts b/dapp/src/constants/featureFlags.ts index ddc1ef2ee..4a70ec623 100644 --- a/dapp/src/constants/featureFlags.ts +++ b/dapp/src/constants/featureFlags.ts @@ -1,6 +1,3 @@ -const GAMIFICATION_ENABLED = - import.meta.env.VITE_FEATURE_FLAG_GAMIFICATION_ENABLED === "true" - const OKX_WALLET_ENABLED = import.meta.env.VITE_FEATURE_FLAG_OKX_WALLET_ENABLED === "true" @@ -18,17 +15,19 @@ const TVL_ENABLED = import.meta.env.VITE_FEATURE_FLAG_TVL_ENABLED === "true" const GATING_DAPP_ENABLED = import.meta.env.VITE_FEATURE_GATING_DAPP_ENABLED === "true" +const POSTHOG_ENABLED = import.meta.env.VITE_FEATURE_POSTHOG_ENABLED === "true" + const MOBILE_MODE_ENABLED = import.meta.env.VITE_FEATURE_MOBILE_MODE_ENABLED === "true" const featureFlags = { - GAMIFICATION_ENABLED, OKX_WALLET_ENABLED, XVERSE_WALLET_ENABLED, WITHDRAWALS_ENABLED, ACRE_POINTS_ENABLED, TVL_ENABLED, GATING_DAPP_ENABLED, + POSTHOG_ENABLED, MOBILE_MODE_ENABLED, } diff --git a/dapp/src/constants/index.ts b/dapp/src/constants/index.ts index 122333c6f..a5e49268c 100644 --- a/dapp/src/constants/index.ts +++ b/dapp/src/constants/index.ts @@ -1,4 +1,3 @@ -export * from "./benefits" export * from "./chains" export * from "./currency" export { default as env } from "./env" @@ -7,7 +6,6 @@ export * from "./externalHref" export { default as featureFlags } from "./featureFlags" export { default as queryKeysFactory } from "./queryKeysFactory" export { default as screen } from "./screen" -export * from "./staking" export { default as tbtc } from "./tbtc" export * from "./time" export { default as wallets } from "./wallets" 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/constants/staking.ts b/dapp/src/constants/staking.ts deleted file mode 100644 index 8b4bd6086..000000000 --- a/dapp/src/constants/staking.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Read the value from the SDK, once we expose it -export const MINIMUM_BALANCE = BigInt(String(5e6)) // 0.05 BTC diff --git a/dapp/src/contexts/DocsDrawerContext.tsx b/dapp/src/contexts/DocsDrawerContext.tsx deleted file mode 100644 index f5097fc96..000000000 --- a/dapp/src/contexts/DocsDrawerContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { createContext, useCallback, useMemo, useState } from "react" - -type DocsDrawerContextValue = { - isOpen: boolean - onOpen: () => void - onClose: () => void -} - -export const DocsDrawerContext = createContext({ - isOpen: false, - onOpen: () => {}, - onClose: () => {}, -}) - -export function DocsDrawerContextProvider({ - children, -}: { - children: React.ReactNode -}): React.ReactElement { - const [isOpen, setIsOpen] = useState(false) - - const onOpen = useCallback(() => { - setIsOpen(true) - }, []) - - const onClose = useCallback(() => { - setIsOpen(false) - }, []) - - const contextValue: DocsDrawerContextValue = useMemo( - () => ({ - isOpen, - onOpen, - onClose, - }), - [isOpen, onClose, onOpen], - ) - - return ( - - {children} - - ) -} diff --git a/dapp/src/contexts/SidebarContext.tsx b/dapp/src/contexts/SidebarContext.tsx deleted file mode 100644 index 125b871a1..000000000 --- a/dapp/src/contexts/SidebarContext.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import useIsEmbed from "#/hooks/useIsEmbed" -import React, { createContext, useCallback, useMemo, useState } from "react" - -type SidebarContextValue = { - isOpen: boolean - onOpen: () => void - onClose: () => void -} - -export const SidebarContext = createContext({ - isOpen: false, - onOpen: () => {}, - onClose: () => {}, -}) - -export function SidebarContextProvider({ - children, -}: { - children: React.ReactNode -}): React.ReactElement { - const { isEmbed } = useIsEmbed() - - const [isOpen, setIsOpen] = useState(false) - - const onOpen = useCallback(() => { - if (isEmbed) return - - setIsOpen(true) - }, [isEmbed]) - - const onClose = useCallback(() => { - setIsOpen(false) - }, []) - - const contextValue: SidebarContextValue = useMemo( - () => ({ - isOpen, - onOpen, - onClose, - }), - [isOpen, onClose, onOpen], - ) - - return ( - - {children} - - ) -} diff --git a/dapp/src/contexts/WalletConnectionAlertContext.tsx b/dapp/src/contexts/WalletConnectionAlertContext.tsx new file mode 100644 index 000000000..ee9faab0c --- /dev/null +++ b/dapp/src/contexts/WalletConnectionAlertContext.tsx @@ -0,0 +1,48 @@ +import { ConnectionAlert } from "#/components/ConnectWalletModal/ConnectWalletAlert" +import React, { createContext, useCallback, useMemo, useState } from "react" + +type WalletConnectionAlertContextValue = { + type?: ConnectionAlert + setConnectionAlert: (type: ConnectionAlert) => void + resetConnectionAlert: () => void +} + +export const WalletConnectionAlertContext = + createContext( + {} as WalletConnectionAlertContextValue, + ) + +export function WalletConnectionAlertContextProvider({ + children, +}: { + children: React.ReactNode +}): React.ReactElement { + const [type, setType] = useState() + + const resetConnectionAlert = useCallback(() => { + setType(undefined) + }, [setType]) + + const setConnectionAlert = useCallback( + (connectionAlert: ConnectionAlert) => { + setType(connectionAlert) + }, + [setType], + ) + + const contextValue: WalletConnectionAlertContextValue = + useMemo( + () => ({ + type, + setConnectionAlert, + resetConnectionAlert, + }), + [resetConnectionAlert, setConnectionAlert, type], + ) + + return ( + + {children} + + ) +} diff --git a/dapp/src/contexts/WalletConnectionErrorContext.tsx b/dapp/src/contexts/WalletConnectionErrorContext.tsx deleted file mode 100644 index 23838bac2..000000000 --- a/dapp/src/contexts/WalletConnectionErrorContext.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ConnectionErrorData } from "#/types" -import React, { createContext, useCallback, useMemo, useState } from "react" - -type WalletConnectionErrorContextValue = { - connectionError: ConnectionErrorData | undefined - setConnectionError: (data: ConnectionErrorData) => void - resetConnectionError: () => void -} - -export const WalletConnectionErrorContext = - createContext( - {} as WalletConnectionErrorContextValue, - ) - -export function WalletConnectionErrorContextProvider({ - children, -}: { - children: React.ReactNode -}): React.ReactElement { - const [connectionError, setConnectionError] = useState() - - const resetConnectionError = useCallback( - () => setConnectionError(undefined), - [setConnectionError], - ) - - const contextValue: WalletConnectionErrorContextValue = - useMemo( - () => ({ - connectionError, - setConnectionError, - resetConnectionError, - }), - [connectionError, resetConnectionError, setConnectionError], - ) - - return ( - - {children} - - ) -} diff --git a/dapp/src/contexts/index.tsx b/dapp/src/contexts/index.tsx index 5781528dd..e3f7fa1aa 100644 --- a/dapp/src/contexts/index.tsx +++ b/dapp/src/contexts/index.tsx @@ -1,5 +1,3 @@ -export * from "./DocsDrawerContext" -export * from "./SidebarContext" export * from "./StakeFlowContext" export * from "./PaginationContext" -export * from "./WalletConnectionErrorContext" +export * from "./WalletConnectionAlertContext" diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 9584fa5eb..df7ca251c 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -2,8 +2,6 @@ export * from "./store" export * from "./sdk" export * from "./orangeKit" export * from "./useDetectThemeMode" -export * from "./useSidebar" -export * from "./useDocsDrawer" export * from "./useTransactionDetails" export * from "./useStakeFlowContext" export * from "./useInitApp" @@ -20,22 +18,26 @@ export * from "./useTransactionModal" export * from "./useVerifyDepositAddress" export { default as useStatistics } from "./useStatistics" export * from "./useDisconnectWallet" -export * from "./useWalletConnectionError" -export { default as useInvalidateQueries } from "./useInvalidateQueries" +export { default as useWalletConnectionAlert } from "./useWalletConnectionAlert" 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 useDetectReferral } from "./useDetectReferral" 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 useSignMessageAndCreateSession } from "./useSignMessageAndCreateSession" -export { default as useScrollbarVisibility } from "./useScrollbarVisibility" 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" +export { default as useAcrePointsData } from "./useAcrePointsData" +export { default as useUserPointsData } from "./useUserPointsData" +export { default as useClaimPoints } from "./useClaimPoints" 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/posthog/index.ts b/dapp/src/hooks/posthog/index.ts new file mode 100644 index 000000000..1b0317599 --- /dev/null +++ b/dapp/src/hooks/posthog/index.ts @@ -0,0 +1,3 @@ +export * from "./usePostHogIdentity" +export * from "./usePostHogCapture" +export * from "./usePostHogPageViewCapture" diff --git a/dapp/src/hooks/posthog/usePostHogCapture.ts b/dapp/src/hooks/posthog/usePostHogCapture.ts new file mode 100644 index 000000000..d789691cf --- /dev/null +++ b/dapp/src/hooks/posthog/usePostHogCapture.ts @@ -0,0 +1,40 @@ +import { PostHogEvent } from "#/posthog/events" +import { PostHog, usePostHog } from "posthog-js/react" +import { useCallback } from "react" + +type CaptureArgs = [ + eventName: PostHogEvent, + ...rest: Parameters extends [unknown, ...infer R] + ? R + : never, +] + +export const usePostHogCapture = () => { + const posthog = usePostHog() + + const handleCapture = useCallback( + (...captureArgs: CaptureArgs) => { + posthog.capture(...captureArgs) + }, + [posthog], + ) + + const handleCaptureWithCause = useCallback( + (error: unknown, ...captureArgs: CaptureArgs) => { + const [eventName, parameters, ...rest] = captureArgs + + const captureParameters = + error instanceof Error + ? { + ...parameters, + cause: error.message, + } + : undefined + + handleCapture(eventName, captureParameters, ...rest) + }, + [handleCapture], + ) + + return { handleCapture, handleCaptureWithCause } +} diff --git a/dapp/src/hooks/posthog/usePostHogIdentity.ts b/dapp/src/hooks/posthog/usePostHogIdentity.ts new file mode 100644 index 000000000..d4d977d21 --- /dev/null +++ b/dapp/src/hooks/posthog/usePostHogIdentity.ts @@ -0,0 +1,27 @@ +import { PostHog, usePostHog } from "posthog-js/react" +import { useCallback } from "react" +import { sha256, toUtf8Bytes } from "ethers" + +type IdentifyArgs = Parameters + +export const usePostHogIdentity = () => { + const posthog = usePostHog() + + const handleIdentification = useCallback( + (...identifyArgs: IdentifyArgs) => { + const [id, ...rest] = identifyArgs + if (!id) return + + const hashedId = sha256(toUtf8Bytes(id.toLowerCase())).slice(2, 12) + + posthog.identify(hashedId, ...rest) + }, + [posthog], + ) + + const resetIdentity = useCallback(() => { + posthog.reset() + }, [posthog]) + + return { handleIdentification, resetIdentity } +} diff --git a/dapp/src/hooks/posthog/usePostHogPageViewCapture.ts b/dapp/src/hooks/posthog/usePostHogPageViewCapture.ts new file mode 100644 index 000000000..f1cae87e1 --- /dev/null +++ b/dapp/src/hooks/posthog/usePostHogPageViewCapture.ts @@ -0,0 +1,15 @@ +import { PostHogEvent } from "#/posthog/events" +import { useEffect } from "react" +import { useLocation } from "react-router-dom" +import { usePostHogCapture } from "./usePostHogCapture" + +export const usePostHogPageViewCapture = () => { + const { handleCapture } = usePostHogCapture() + const location = useLocation() + + useEffect(() => { + handleCapture(PostHogEvent.PageView) + }, [location, handleCapture]) + + return handleCapture +} diff --git a/dapp/src/hooks/router/index.ts b/dapp/src/hooks/router/index.ts index 5b97eb715..c8edda5c3 100644 --- a/dapp/src/hooks/router/index.ts +++ b/dapp/src/hooks/router/index.ts @@ -1,2 +1 @@ -export * from "./useIsActiveRoute" export { default as useAppNavigate } from "./useAppNavigate" diff --git a/dapp/src/hooks/router/useIsActiveRoute.ts b/dapp/src/hooks/router/useIsActiveRoute.ts deleted file mode 100644 index 715c3702a..000000000 --- a/dapp/src/hooks/router/useIsActiveRoute.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { routerPath } from "#/router/path" -import { useLocation } from "react-router-dom" - -export const useIsActiveRoute = (route: string) => { - const location = useLocation() - - return location.pathname === route -} - -export const useIsHomeRouteActive = () => useIsActiveRoute(routerPath.home) 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 75791ee54..b88527385 100644 --- a/dapp/src/hooks/store/index.ts +++ b/dapp/src/hooks/store/index.ts @@ -6,9 +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 b9f022b29..000000000 --- a/dapp/src/hooks/store/useActivities.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { selectActivities } from "#/store/wallet" -import { useAppSelector } from "./useAppSelector" - -export default function useActivities() { - return useAppSelector(selectActivities) -} 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/useAcrePoints.ts b/dapp/src/hooks/useAcrePoints.ts deleted file mode 100644 index 81c329c14..000000000 --- a/dapp/src/hooks/useAcrePoints.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query" -import { acreApi } from "#/utils" -import { queryKeysFactory, REFETCH_INTERVAL_IN_MILLISECONDS } from "#/constants" -import { MODAL_TYPES } from "#/types" -import { useWallet } from "./useWallet" -import { useModal } from "./useModal" - -const { userKeys, acreKeys } = queryKeysFactory - -type UseAcrePointsReturnType = { - totalBalance: number - claimableBalance: number - nextDropTimestamp?: number - isCalculationInProgress?: boolean - claimPoints: () => void - updateUserPointsData: () => Promise - updatePointsData: () => Promise - totalPoolBalance: number -} - -export default function useAcrePoints(): UseAcrePointsReturnType { - const { ethAddress = "" } = useWallet() - const { openModal } = useModal() - - 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 { mutate: claimPoints } = useMutation({ - mutationFn: async () => acreApi.claimPoints(ethAddress), - onSettled: async () => { - await userPointsDataQuery.refetch() - }, - onSuccess: (data) => { - const claimedAmount = Number(data.claimed) - const totalAmount = Number(data.total) - - openModal(MODAL_TYPES.ACRE_POINTS_CLAIM, { - claimedAmount, - totalAmount, - }) - }, - // TODO: Add the case when something goes wrong - // onError: (error) => {}, - }) - - return { - totalBalance: userPointsDataQuery.data?.claimed || 0, - claimableBalance: userPointsDataQuery.data?.unclaimed || 0, - nextDropTimestamp: pointsDataQuery.data?.dropAt, - isCalculationInProgress: pointsDataQuery.data?.isCalculationInProgress, - claimPoints, - updateUserPointsData: userPointsDataQuery.refetch, - updatePointsData: pointsDataQuery.refetch, - totalPoolBalance: pointsDataQuery.data?.totalPool || 0, - } -} 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/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/useCancelPromise.ts b/dapp/src/hooks/useCancelPromise.ts new file mode 100644 index 000000000..a6bb1666b --- /dev/null +++ b/dapp/src/hooks/useCancelPromise.ts @@ -0,0 +1,50 @@ +import { useCallback, useEffect } from "react" + +const sessionIdToPromise: Record< + number, + { + promise: Promise + cancel: (reason: Error) => void + shouldOpenErrorModal: boolean + } +> = {} + +export default function useCancelPromise( + sessionId: number, + errorMsgText: string, +) { + useEffect(() => { + let cancel = (_: Error) => {} + const promise: Promise = new Promise((_, reject) => { + cancel = reject + }) + + sessionIdToPromise[sessionId] = { + cancel, + promise, + shouldOpenErrorModal: true, + } + }, [sessionId]) + + const cancel = useCallback(() => { + const sessionData = sessionIdToPromise[sessionId] + sessionIdToPromise[sessionId] = { + ...sessionData, + shouldOpenErrorModal: false, + } + + sessionIdToPromise[sessionId].cancel(new Error(errorMsgText)) + }, [errorMsgText, sessionId]) + + const resolve = useCallback( + () => + Promise.race([sessionIdToPromise[sessionId].promise, Promise.resolve()]), + [sessionId], + ) + + return { + cancel, + resolve, + sessionIdToPromise, + } +} diff --git a/dapp/src/hooks/useClaimPoints.ts b/dapp/src/hooks/useClaimPoints.ts new file mode 100644 index 000000000..0baca126f --- /dev/null +++ b/dapp/src/hooks/useClaimPoints.ts @@ -0,0 +1,39 @@ +import { useMutation } from "@tanstack/react-query" +import { acreApi } from "#/utils" +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" + +export default function useClaimPoints() { + const { ethAddress = "" } = useWallet() + const { openModal } = useModal() + const { handleCapture, handleCaptureWithCause } = usePostHogCapture() + const { refetch: userPointsDataRefetch } = useUserPointsData() + + return useMutation({ + mutationFn: async () => acreApi.claimPoints(ethAddress), + onSettled: async () => { + await userPointsDataRefetch() + }, + onSuccess: (data) => { + const claimedAmount = Number(data.claimed) + const totalAmount = Number(data.total) + + openModal(MODAL_TYPES.ACRE_POINTS_CLAIM, { + claimedAmount, + totalAmount, + }) + + handleCapture(PostHogEvent.PointsClaimSuccess, { + claimedAmount, + totalAmount, + }) + }, + onError: (error) => { + handleCaptureWithCause(error, PostHogEvent.PointsClaimFailure) + }, + }) +} diff --git a/dapp/src/hooks/useDetectReferral.ts b/dapp/src/hooks/useDetectReferral.ts deleted file mode 100644 index e32a457b6..000000000 --- a/dapp/src/hooks/useDetectReferral.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useEffect } from "react" -import useReferral from "./useReferral" - -export default function useDetectReferral() { - const { detectReferral } = useReferral() - - useEffect(() => { - detectReferral() - }, [detectReferral]) -} diff --git a/dapp/src/hooks/useDocsDrawer.ts b/dapp/src/hooks/useDocsDrawer.ts deleted file mode 100644 index 3536dba2a..000000000 --- a/dapp/src/hooks/useDocsDrawer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from "react" -import { DocsDrawerContext } from "#/contexts" - -export function useDocsDrawer() { - const context = useContext(DocsDrawerContext) - - if (!context) { - throw new Error( - "DocsDrawerContext used outside of DocsDrawerContext component", - ) - } - - return context -} diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index e87e8955a..caf792595 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -4,7 +4,6 @@ import { useAccountsChangedOKX } from "./orangeKit/useAccountsChangedOKX" import { useInitDataFromSdk, useInitializeAcreSdk } from "./sdk" import { useSentry } from "./sentry" import useDetectEmbed from "./useDetectEmbed" -import useDetectReferral from "./useDetectReferral" import { useDisconnectWallet } from "./useDisconnectWallet" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" @@ -13,7 +12,6 @@ export function useInitApp() { // useDetectThemeMode() useSentry() useDetectEmbed() - useDetectReferral() useInitializeAcreSdk() useInitDataFromSdk() useFetchBTCPriceUSD() 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/useLocalStorage.ts b/dapp/src/hooks/useLocalStorage.ts index d3d03e990..73272cecd 100644 --- a/dapp/src/hooks/useLocalStorage.ts +++ b/dapp/src/hooks/useLocalStorage.ts @@ -1,4 +1,7 @@ -import { useLocalStorage as useRehooksLocalStorage } from "@rehooks/local-storage" +import { + useLocalStorage as useRehooksLocalStorage, + writeStorage, +} from "@rehooks/local-storage" export const parseLocalStorageValue = (value: string | null | undefined) => { if ( @@ -12,6 +15,8 @@ export const parseLocalStorageValue = (value: string | null | undefined) => { return value } +export { writeStorage } + export const getLocalStorageItem = (key: string): string | undefined => { const value = localStorage.getItem(key) return parseLocalStorageValue(value) diff --git a/dapp/src/hooks/useReferral.ts b/dapp/src/hooks/useReferral.ts index d919850e9..4e6f7566c 100644 --- a/dapp/src/hooks/useReferral.ts +++ b/dapp/src/hooks/useReferral.ts @@ -1,52 +1,23 @@ import { env } from "#/constants" -import { referralProgram } from "#/utils" - import { useCallback, useMemo } from "react" -import { MODAL_TYPES } from "#/types" -import useIsEmbed from "./useIsEmbed" -import useLocalStorage from "./useLocalStorage" -import { useModal } from "./useModal" +import useLocalStorage, { writeStorage } from "./useLocalStorage" type UseReferralReturn = { referral: number | null - detectReferral: () => void resetReferral: () => void } +const LOCAL_STORAGE_KEY = "acre.referral" + +export const writeReferral = (value: string) => { + writeStorage(LOCAL_STORAGE_KEY, value) +} + export default function useReferral(): UseReferralReturn { const [referral, setReferral] = useLocalStorage( - "acre.referral", + LOCAL_STORAGE_KEY, env.REFERRAL, ) - const { openModal } = useModal() - const { isEmbed, embeddedApp } = useIsEmbed() - - const detectReferral = useCallback(() => { - const param = referralProgram.getReferralFromURL() - - if (isEmbed && embeddedApp) { - setReferral(referralProgram.getReferralByEmbeddedApp(embeddedApp)) - return - } - - if (param === null) { - setReferral(env.REFERRAL) - return - } - - const convertedReferral = Number(param) - - if (referralProgram.isValidReferral(convertedReferral)) { - setReferral(convertedReferral) - } else { - console.error("Incorrect referral") - setReferral(null) - openModal(MODAL_TYPES.UNEXPECTED_ERROR, { - withCloseButton: false, - closeOnEsc: false, - }) - } - }, [isEmbed, embeddedApp, openModal, setReferral]) const resetReferral = useCallback(() => { setReferral(env.REFERRAL) @@ -54,10 +25,9 @@ export default function useReferral(): UseReferralReturn { return useMemo( () => ({ - detectReferral, resetReferral, referral, }), - [detectReferral, resetReferral, referral], + [resetReferral, referral], ) } diff --git a/dapp/src/hooks/useScrollbarVisibility.ts b/dapp/src/hooks/useScrollbarVisibility.ts deleted file mode 100644 index c1ce79034..000000000 --- a/dapp/src/hooks/useScrollbarVisibility.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback, useEffect, useState } from "react" - -const SCROLLBAR_WIDTH = `${window.innerWidth - document.body.offsetWidth}px` - -function isScrollbarVisible(selector: string) { - const element = document.querySelector(selector) - - if (!element) return false - - return element?.scrollHeight > element?.clientHeight -} - -export default function useScrollbarVisibility(selector: string) { - const [isVisible, setIsVisible] = useState(false) - - useEffect(() => { - const handleResize = () => { - setIsVisible(isScrollbarVisible(selector)) - } - window.addEventListener("resize", handleResize) - - return () => { - window.removeEventListener("resize", handleResize) - } - }, [selector]) - - const refreshState = useCallback(() => { - setIsVisible(isScrollbarVisible(selector)) - }, [selector]) - - return { isVisible, scrollbarWidth: SCROLLBAR_WIDTH, refreshState } -} diff --git a/dapp/src/hooks/useSidebar.ts b/dapp/src/hooks/useSidebar.ts deleted file mode 100644 index 944364076..000000000 --- a/dapp/src/hooks/useSidebar.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from "react" -import { SidebarContext } from "#/contexts" - -export function useSidebar() { - const context = useContext(SidebarContext) - - if (!context) { - throw new Error("SidebarContext used outside of SidebarContext component") - } - - return context -} diff --git a/dapp/src/hooks/useTransactionModal.ts b/dapp/src/hooks/useTransactionModal.ts index e052ed131..7e4b9d0be 100644 --- a/dapp/src/hooks/useTransactionModal.ts +++ b/dapp/src/hooks/useTransactionModal.ts @@ -1,4 +1,4 @@ -import { ACTION_FLOW_TYPES, ActionFlowType, MODAL_TYPES } from "#/types" +import { ActionFlowType, MODAL_TYPES } from "#/types" import { useCallback } from "react" import { useModal } from "./useModal" @@ -8,7 +8,7 @@ export function useTransactionModal(type: ActionFlowType) { return useCallback(() => { openModal(MODAL_TYPES[type], { type, - closeOnEsc: type !== ACTION_FLOW_TYPES.UNSTAKE, + closeOnEsc: false, }) }, [openModal, type]) } 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 1462cfa3e..8125fd51a 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -8,6 +8,7 @@ import { useDisconnect, } from "wagmi" import { orangeKit } from "#/utils" +import sentry from "#/sentry" import { OnErrorCallback, OrangeKitConnector, @@ -17,10 +18,10 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query" import { useDispatch } from "react-redux" import { setAddress } from "#/store/wallet" -import useBitcoinBalance from "./orangeKit/useBitcoinBalance" import useResetWalletState from "./useResetWalletState" import useLastUsedBtcAddress from "./useLastUsedBtcAddress" -import useWalletAddress from "./store/useWalletAddress" +import useBitcoinBalance from "./useBitcoinBalance" +import { useWalletAddress } from "./store" const { typeConversionToConnector, typeConversionToOrangeKitConnector } = orangeKit @@ -50,7 +51,7 @@ export function useWallet(): UseWalletReturn { const { setAddressInLocalStorage, removeAddressFromLocalStorage } = useLastUsedBtcAddress() - const { data: balance } = useBitcoinBalance(btcAddress) + const { data: balance } = useBitcoinBalance() const chainId = useChainId() const config = useConfig() @@ -76,6 +77,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) + sentry.setUser(bitcoinAddress) }, }, }) @@ -87,6 +89,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(undefined)) removeAddressFromLocalStorage() resetWalletState() + sentry.setUser(undefined) }, }, }) @@ -125,6 +128,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) + sentry.setUser(bitcoinAddress) }, }, queryClient, diff --git a/dapp/src/hooks/useWalletConnectionAlert.ts b/dapp/src/hooks/useWalletConnectionAlert.ts new file mode 100644 index 000000000..bab475294 --- /dev/null +++ b/dapp/src/hooks/useWalletConnectionAlert.ts @@ -0,0 +1,6 @@ +import { WalletConnectionAlertContext } from "#/contexts" +import { useContext } from "react" + +export default function useWalletConnectionAlert() { + return useContext(WalletConnectionAlertContext) +} diff --git a/dapp/src/hooks/useWalletConnectionError.ts b/dapp/src/hooks/useWalletConnectionError.ts deleted file mode 100644 index 55fae3a56..000000000 --- a/dapp/src/hooks/useWalletConnectionError.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WalletConnectionErrorContext } from "#/contexts" -import { useContext } from "react" - -export function useWalletConnectionError() { - return useContext(WalletConnectionErrorContext) -} diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard.tsx deleted file mode 100644 index 193c6c5d1..000000000 --- a/dapp/src/pages/DashboardPage/AcrePointsCard.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React from "react" -import { H4, TextMd } from "#/components/shared/Typography" -import { - Button, - Card, - CardBody, - CardHeader, - CardProps, - HStack, - Image, - VStack, -} from "@chakra-ui/react" -import Countdown from "#/components/shared/Countdown" -import { logPromiseFailure, numberToLocaleString } from "#/utils" -import { useAcrePoints, useWallet } from "#/hooks" -import Spinner from "#/components/shared/Spinner" -import UserDataSkeleton from "#/components/shared/UserDataSkeleton" -import InfoTooltip from "#/components/shared/InfoTooltip" -import useDebounce from "#/hooks/useDebounce" -import { ONE_SEC_IN_MILLISECONDS } from "#/constants" -import acrePointsIllustrationSrc from "#/assets/images/acre-points-illustration.png" - -// TODO: Define colors as theme value -const COLOR_TEXT_LIGHT_PRIMARY = "#1C1A16" -const COLOR_TEXT_LIGHT_TERTIARY = "#7D6A4B" -// TODO: Update `Button` component theme -const COLOR_BUTTON_LABEL = "#FBF7EC" -const COLOR_BUTTON_BACKGROUND = "#33A321" - -export default function AcrePointsCard(props: CardProps) { - const { - claimableBalance, - nextDropTimestamp, - totalBalance, - claimPoints, - updateUserPointsData, - updatePointsData, - isCalculationInProgress, - totalPoolBalance, - } = useAcrePoints() - const { isConnected } = useWallet() - - const debouncedClaimPoints = useDebounce(claimPoints, ONE_SEC_IN_MILLISECONDS) - - const formattedTotalPointsAmount = numberToLocaleString(totalBalance) - const formattedClaimablePointsAmount = numberToLocaleString(claimableBalance) - const formattedTotalPoolBalance = numberToLocaleString(totalPoolBalance) - - const handleOnCountdownEnd = () => { - logPromiseFailure(updatePointsData()) - logPromiseFailure(updateUserPointsData()) - } - - const isDataReady = - isCalculationInProgress || !!nextDropTimestamp || !!claimableBalance - - return ( - - - - {isConnected ? "Your" : "Total"} Acre points - - - - - - - -

- {isConnected - ? formattedTotalPointsAmount - : formattedTotalPoolBalance} -

-
- - - - - {isDataReady && ( - - {isCalculationInProgress ? ( - - {!claimableBalance && ( - Please wait... - )} - - - - Your drop is being prepared. - - - - ) : ( - - - Next drop in - - - - )} - - {claimableBalance && ( - - )} - - )} - -
-
- ) -} diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard/AcrePointsLabel.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard/AcrePointsLabel.tsx new file mode 100644 index 000000000..62797243c --- /dev/null +++ b/dapp/src/pages/DashboardPage/AcrePointsCard/AcrePointsLabel.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { TextMd } from "#/components/shared/Typography" +import { HStack } from "@chakra-ui/react" +import Countdown from "#/components/shared/Countdown" +import { logPromiseFailure } from "#/utils" +import { useAcrePointsData, useUserPointsData } from "#/hooks" +import LabelWrapper from "./LabelWrapper" + +// TODO: Define colors as theme value +const COLOR_TEXT_LIGHT_PRIMARY = "#1C1A16" +const COLOR_TEXT_LIGHT_TERTIARY = "#7D6A4B" + +export function NextDropTimestampLabel() { + const { data: acrePointsData, refetch: acrePointsDataRefetch } = + useAcrePointsData() + const { refetch: userPointsDataRefetch } = useUserPointsData() + + const handleOnCountdownEnd = () => { + logPromiseFailure(acrePointsDataRefetch()) + logPromiseFailure(userPointsDataRefetch()) + } + + if (!acrePointsData?.nextDropTimestamp) return null + + return ( + + + Next drop in + + + + ) +} + +export default function AcrePointsLabel() { + const { data } = useAcrePointsData() + + if (!data) return null + + return ( + + + + ) +} diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard/LabelWrapper.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard/LabelWrapper.tsx new file mode 100644 index 000000000..77ea4c33d --- /dev/null +++ b/dapp/src/pages/DashboardPage/AcrePointsCard/LabelWrapper.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from "react" +import { VStack } from "@chakra-ui/react" + +export default function LabelWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard/UserPointsLabel.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard/UserPointsLabel.tsx new file mode 100644 index 000000000..edae95326 --- /dev/null +++ b/dapp/src/pages/DashboardPage/AcrePointsCard/UserPointsLabel.tsx @@ -0,0 +1,88 @@ +import React from "react" +import { TextMd } from "#/components/shared/Typography" +import { Button, HStack, VStack } from "@chakra-ui/react" +import { numberToLocaleString } from "#/utils" +import { useAcrePointsData, useClaimPoints, useUserPointsData } from "#/hooks" +import Spinner from "#/components/shared/Spinner" +import TooltipIcon from "#/components/shared/TooltipIcon" +import useDebounce from "#/hooks/useDebounce" +import { ONE_SEC_IN_MILLISECONDS } from "#/constants" +import LabelWrapper from "./LabelWrapper" +import { NextDropTimestampLabel } from "./AcrePointsLabel" + +// TODO: Update `Button` component theme +const COLOR_BUTTON_LABEL = "#FBF7EC" +const COLOR_BUTTON_BACKGROUND = "#33A321" + +function ClaimableBalanceLabel() { + const { mutate: claimPoints } = useClaimPoints() + const { data: userPointsData } = useUserPointsData() + const debouncedClaimPoints = useDebounce(claimPoints, ONE_SEC_IN_MILLISECONDS) + + const claimableBalance = userPointsData?.claimableBalance || 0 + const formattedClaimablePointsAmount = numberToLocaleString(claimableBalance) + + if (claimableBalance <= 0) return null + + return ( + + ) +} + +function CalculationInProgressLabel() { + const { data } = useUserPointsData() + + return ( + + {!data?.claimableBalance && ( + Please wait... + )} + + + Your drop is being prepared. + + + + ) +} + +export default function UserPointsLabel() { + const { data: acrePointsData } = useAcrePointsData() + const { data: userPointsData } = useUserPointsData() + + if (acrePointsData || userPointsData?.isEligible) { + if (acrePointsData?.isCalculationInProgress) + return ( + + + + + ) + + return ( + + + + + ) + } + + return null +} diff --git a/dapp/src/pages/DashboardPage/AcrePointsCard/index.tsx b/dapp/src/pages/DashboardPage/AcrePointsCard/index.tsx new file mode 100644 index 000000000..4e41ed8b3 --- /dev/null +++ b/dapp/src/pages/DashboardPage/AcrePointsCard/index.tsx @@ -0,0 +1,65 @@ +import React from "react" +import { H4, TextMd } from "#/components/shared/Typography" +import { + Card, + CardBody, + CardHeader, + CardProps, + HStack, + Image, +} from "@chakra-ui/react" +import { numberToLocaleString } from "#/utils" +import { useAcrePointsData, useUserPointsData, useWallet } from "#/hooks" +import UserDataSkeleton from "#/components/shared/UserDataSkeleton" +import TooltipIcon from "#/components/shared/TooltipIcon" +import acrePointsIllustrationSrc from "#/assets/images/acre-points-illustration.png" +import AcrePointsLabel from "./AcrePointsLabel" +import UserPointsLabel from "./UserPointsLabel" + +export default function AcrePointsCard(props: CardProps) { + const { data: acrePointsData } = useAcrePointsData() + const { data: userPointsData } = useUserPointsData() + const { isConnected } = useWallet() + + const formattedUserTotalBalance = numberToLocaleString( + userPointsData?.totalBalance ?? 0, + ) + const formattedTotalPoolBalance = numberToLocaleString( + acrePointsData?.totalPoolBalance ?? 0, + ) + + return ( + + + + {isConnected ? "Your" : "Total"} Acre points + + + + + + + +

+ {isConnected + ? formattedUserTotalBalance + : formattedTotalPoolBalance} +

+
+ + + + + {isConnected ? : } + +
+
+ ) +} diff --git a/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx b/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx index 0960e95af..32b8e13a1 100644 --- a/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx +++ b/dapp/src/pages/DashboardPage/AcreTVLMessage.tsx @@ -1,7 +1,7 @@ import React from "react" import { Box, HStack, StackProps, VStack } from "@chakra-ui/react" -import { useAllActivitiesCount, useStatistics, useWallet } from "#/hooks" -import { BoltFilled } from "#/assets/icons" +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 @@ -20,7 +20,9 @@ export default function AcreTVLMessage(props: AcreTVLMessageProps) { return ( - + + + {tvl.isCapExceeded ? ( diff --git a/dapp/src/pages/DashboardPage/BeehiveCard.tsx b/dapp/src/pages/DashboardPage/BeehiveCard.tsx index 9168d0b5b..d768e94d7 100644 --- a/dapp/src/pages/DashboardPage/BeehiveCard.tsx +++ b/dapp/src/pages/DashboardPage/BeehiveCard.tsx @@ -1,5 +1,12 @@ import React from "react" +import { MezoSignIcon } from "#/assets/icons" +import beehiveIllustrationSrc from "#/assets/images/beehive-illustration.svg" +import TooltipIcon from "#/components/shared/TooltipIcon" import { H6, TextMd, TextSm } from "#/components/shared/Typography" +import UserDataSkeleton from "#/components/shared/UserDataSkeleton" +import { useMats, useModal } from "#/hooks" +import { MODAL_TYPES } from "#/types" +import { numberToLocaleString } from "#/utils" import { Box, Button, @@ -12,13 +19,6 @@ import { Image, VStack, } from "@chakra-ui/react" -import { MezoSignIcon } from "#/assets/icons" -import { useMats, useModal } from "#/hooks" -import { MODAL_TYPES } from "#/types" -import beehiveIllustrationSrc from "#/assets/images/beehive-illustration.svg" -import UserDataSkeleton from "#/components/shared/UserDataSkeleton" -import { numberToLocaleString } from "#/utils" -import InfoTooltip from "#/components/shared/InfoTooltip" export default function BeehiveCard(props: CardProps) { const { openModal } = useModal() @@ -32,7 +32,7 @@ export default function BeehiveCard(props: CardProps) { Additional rewards - diff --git a/dapp/src/pages/DashboardPage/GrantedSeasonPassCard.tsx b/dapp/src/pages/DashboardPage/GrantedSeasonPassCard.tsx deleted file mode 100644 index 4e618844f..000000000 --- a/dapp/src/pages/DashboardPage/GrantedSeasonPassCard.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react" -import { - Card, - CardBody, - CardHeader, - CardProps, - HStack, - Icon, -} from "@chakra-ui/react" -import { IconDiscountCheckFilled, IconLock } from "@tabler/icons-react" -import { TextMd } from "#/components/shared/Typography" -import UserDataSkeleton from "#/components/shared/UserDataSkeleton" - -export default function GrantedSeasonPassCard(props: CardProps) { - return ( - - - - Season 2 arriving soon - - - - - - - Your seat is secured. - - - - ) -} diff --git a/dapp/src/pages/DashboardPage/PositionDetails.tsx b/dapp/src/pages/DashboardPage/PositionDetails.tsx index 8346fcc8a..6b4f4267e 100644 --- a/dapp/src/pages/DashboardPage/PositionDetails.tsx +++ b/dapp/src/pages/DashboardPage/PositionDetails.tsx @@ -1,26 +1,23 @@ import React from "react" import { CurrencyBalanceWithConversion } from "#/components/shared/CurrencyBalanceWithConversion" import { - useAllActivitiesCount, + useActivitiesCount, useBitcoinPosition, useTransactionModal, useStatistics, useWallet, useMobileMode, + useActivities, } from "#/hooks" import { ACTION_FLOW_TYPES } from "#/types" -import { - Button, - ButtonProps, - Flex, - HStack, - // Tag, - VStack, -} from "@chakra-ui/react" +import { Button, ButtonProps, Flex, HStack, VStack } from "@chakra-ui/react" import ArrivingSoonTooltip from "#/components/ArrivingSoonTooltip" import UserDataSkeleton from "#/components/shared/UserDataSkeleton" 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 @@ -36,12 +33,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() @@ -55,25 +53,16 @@ export default function PositionDetails() { {/* TODO: Component should be moved to `CardHeader` */} - - Your deposit - {/* TODO: Uncomment when position will be implemented */} - {/* {positionPercentage && ( - - Top {positionPercentage}% - - )} */} - + + Your Acre balance + {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/pages/DashboardPage/UsefulLinks.tsx b/dapp/src/pages/DashboardPage/UsefulLinks.tsx deleted file mode 100644 index 6202c411f..000000000 --- a/dapp/src/pages/DashboardPage/UsefulLinks.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react" -import { StackProps, VStack } from "@chakra-ui/react" -import ButtonLink from "#/components/shared/ButtonLink" -import { EXTERNAL_HREF } from "#/constants" - -export default function UsefulLinks(props: StackProps) { - return ( - - {[ - { label: "Documentation", href: EXTERNAL_HREF.DOCS }, - { label: "Blog", href: EXTERNAL_HREF.BLOG }, - { label: "FAQ", href: EXTERNAL_HREF.FAQ }, - ].map(({ label, href }) => ( - - {label} - - ))} - - ) -} diff --git a/dapp/src/pages/DashboardPage/index.tsx b/dapp/src/pages/DashboardPage/index.tsx index e0fb11bc2..b35928aa6 100644 --- a/dapp/src/pages/DashboardPage/index.tsx +++ b/dapp/src/pages/DashboardPage/index.tsx @@ -3,7 +3,6 @@ import { featureFlags } from "#/constants" import { useTriggerConnectWalletModal } from "#/hooks" import { Grid } from "@chakra-ui/react" import DashboardCard from "./DashboardCard" -// import GrantedSeasonPassCard from "./GrantedSeasonPassCard" import AcrePointsCard from "./AcrePointsCard" import AcrePointsTemplateCard from "./AcrePointsTemplateCard" import BeehiveCard from "./BeehiveCard" @@ -52,9 +51,6 @@ export default function DashboardPage() { - {/* TODO: Uncomment in post-launch phases + add `gridArea` and update `templateAreas` */} - {/* */} - {featureFlags.ACRE_POINTS_ENABLED ? ( ) : ( diff --git a/dapp/src/posthog/PostHogProvider.tsx b/dapp/src/posthog/PostHogProvider.tsx new file mode 100644 index 000000000..39e733fee --- /dev/null +++ b/dapp/src/posthog/PostHogProvider.tsx @@ -0,0 +1,26 @@ +import React, { PropsWithChildren } from "react" +import { PostHogProvider as Provider } from "posthog-js/react" +import { PostHogConfig } from "posthog-js" +import { featureFlags, env } from "../constants" + +const options: Partial = { + api_host: env.POSTHOG_API_HOST, + capture_pageview: false, + persistence: "memory", +} + +function PostHogProvider(props: PropsWithChildren) { + const { children } = props + + if (!featureFlags.POSTHOG_ENABLED) { + return children + } + + return ( + + {children} + + ) +} + +export default PostHogProvider diff --git a/dapp/src/posthog/events.ts b/dapp/src/posthog/events.ts new file mode 100644 index 000000000..992b9958e --- /dev/null +++ b/dapp/src/posthog/events.ts @@ -0,0 +1,9 @@ +export enum PostHogEvent { + PageView = "$pageview", + DepositSuccess = "deposit_success", + DepositFailure = "deposit_failure", + WithdrawalSuccess = "withdrawal_success", + WithdrawalFailure = "withdrawal_failure", + PointsClaimSuccess = "points_claim_success", + PointsClaimFailure = "points_claim_failure", +} diff --git a/dapp/src/router/index.tsx b/dapp/src/router/index.tsx index 9d6a42b0f..351a8504e 100644 --- a/dapp/src/router/index.tsx +++ b/dapp/src/router/index.tsx @@ -12,6 +12,8 @@ import AccessPage from "#/pages/AccessPage" import WelcomePage from "#/pages/WelcomePage" import welcomePageLoader from "#/pages/WelcomePage/loader" import accessPageLoader from "#/pages/AccessPage/loader" +import { writeReferral } from "#/hooks/useReferral" +import { env } from "#/constants" import { routerPath } from "./path" const mainLayoutLoader: LoaderFunction = ({ request }) => { @@ -32,6 +34,24 @@ export const router = createBrowserRouter([ { path: "/", element: , + loader: ({ request }) => { + // TODO: display the error page/modal when the referral is invalid. + const referralCodeFromUrl = referralProgram.getReferralFromURL() + + const referralCode = referralProgram.isValidReferral(referralCodeFromUrl) + ? referralCodeFromUrl! + : env.REFERRAL + + writeReferral(referralCode.toString()) + + const embedApp = referralProgram.getEmbeddedApp(request.url) + if (referralProgram.isEmbedApp(embedApp)) { + writeReferral( + referralProgram.getReferralByEmbeddedApp(embedApp).toString(), + ) + } + return null + }, children: [ { index: true, diff --git a/dapp/src/sentry.ts b/dapp/src/sentry.ts new file mode 100644 index 000000000..f3707ede4 --- /dev/null +++ b/dapp/src/sentry.ts @@ -0,0 +1,50 @@ +import * as Sentry from "@sentry/react" +import { sha256, toUtf8Bytes } from "ethers" + +const initialize = (dsn: string) => { + Sentry.init({ + dsn, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.captureConsoleIntegration({ levels: ["error"] }), + Sentry.extraErrorDataIntegration(), + Sentry.httpClientIntegration(), + ], + // Attach stacktrace to errors logged by `console.error`. This is useful for + // the `captureConsoleIntegration` integration. + attachStacktrace: true, + // Performance Monitoring + tracesSampleRate: 0.1, + }) +} + +/** + * Sets the user in Sentry with an ID from hashed Bitcoin address. + * The Bitcoin address is first converted to lowercase and then hashed using SHA-256. + * The resulting hash is then converted to a hexadecimal string and the first 10 + * characters are set as the user ID. + * + * @param bitcoinAddress - The Bitcoin address of the user. If undefined, the user + * is set to null in Sentry. + */ +const setUser = (bitcoinAddress: string | undefined) => { + if (!bitcoinAddress) { + Sentry.setUser(null) + return + } + + const hashedBitcoinAddress = sha256(toUtf8Bytes(bitcoinAddress.toLowerCase())) + // Remove the 0x prefix and take the first 10 characters. + const id = hashedBitcoinAddress.slice(2, 12) + + Sentry.setUser({ id }) +} + +const captureException = (exception: unknown) => + Sentry.captureException(exception) + +export default { + initialize, + setUser, + captureException, +} diff --git a/dapp/src/sentry/index.ts b/dapp/src/sentry/index.ts deleted file mode 100644 index 0bb08159d..000000000 --- a/dapp/src/sentry/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from "@sentry/react" - -const initialize = (dsn: string) => { - Sentry.init({ - dsn, - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.captureConsoleIntegration({ levels: ["error"] }), - ], - // Attach stacktrace to errors logged by `console.error`. This is useful for - // the `captureConsoleIntegration` integration. - attachStacktrace: true, - // Performance Monitoring - tracesSampleRate: 0.1, - }) -} - -const captureException = (exception: unknown) => - Sentry.captureException(exception) - -export default { - initialize, - captureException, -} diff --git a/dapp/src/store/tests/walletSlice.test.ts b/dapp/src/store/tests/walletSlice.test.ts deleted file mode 100644 index b29009f60..000000000 --- a/dapp/src/store/tests/walletSlice.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest" -import { createActivity } from "#/tests/factories" -import { Activity } from "#/types" -import { WalletState } from "../wallet" -import reducer, { initialState, setActivities } from "../wallet/walletSlice" - -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, - }) - }) - }) - }) -}) diff --git a/dapp/src/store/wallet/walletSelector.ts b/dapp/src/store/wallet/walletSelector.ts index db8194cd6..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,21 +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 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/tests/factories.ts b/dapp/src/tests/factories.ts deleted file mode 100644 index 8f3d66c41..000000000 --- a/dapp/src/tests/factories.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Activity } from "#/types" -import { dateToUnixTimestamp, randomInteger } from "#/utils" - -export 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, -}) diff --git a/dapp/src/theme/Drawer.ts b/dapp/src/theme/Drawer.ts deleted file mode 100644 index b904c74c0..000000000 --- a/dapp/src/theme/Drawer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { drawerAnatomy as parts } from "@chakra-ui/anatomy" -import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" - -const baseStyleDialogContainer = defineStyle({ - zIndex: "drawer", -}) - -const baseStyleDialog = defineStyle({ - borderTop: "2px", - borderLeft: "2px", - boxShadow: "none", - borderColor: "white", - borderTopLeftRadius: "xl", - bg: "gold.100", -}) - -const baseStyleOverlay = defineStyle({ - bg: "none", - backdropFilter: "auto", - backdropBlur: "8px", -}) - -const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys) - -const baseStyle = multiStyleConfig.definePartsStyle({ - dialogContainer: baseStyleDialogContainer, - dialog: baseStyleDialog, - overlay: baseStyleOverlay, -}) - -export const drawerTheme = multiStyleConfig.defineMultiStyleConfig({ - baseStyle, -}) diff --git a/dapp/src/theme/Sidebar.ts b/dapp/src/theme/Sidebar.ts deleted file mode 100644 index 1b1a431b1..000000000 --- a/dapp/src/theme/Sidebar.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" - -const PARTS = ["sidebarContainer", "sidebar"] - -const baseStyleSidebarContainer = defineStyle({ - top: 0, - right: 0, - h: "100vh", - position: "fixed", - overflow: "hidden", - zIndex: "sidebar", - transition: "width 0.3s", -}) - -const baseStyleSidebar = defineStyle({ - p: 4, - height: "100%", - w: "sidebar_width", - bg: "gold.200", - borderTop: "2px", - borderLeft: "2px", - borderColor: "gold.100", - borderTopLeftRadius: "xl", - display: "flex", - flexDirection: "column", - gap: 3, -}) - -const multiStyleConfig = createMultiStyleConfigHelpers(PARTS) - -const baseStyle = multiStyleConfig.definePartsStyle({ - sidebarContainer: baseStyleSidebarContainer, - sidebar: baseStyleSidebar, -}) - -export const sidebarTheme = multiStyleConfig.defineMultiStyleConfig({ - baseStyle, -}) diff --git a/dapp/src/theme/AcreTVLProgress.ts b/dapp/src/theme/acreTVLProgressTheme.ts similarity index 100% rename from dapp/src/theme/AcreTVLProgress.ts rename to dapp/src/theme/acreTVLProgressTheme.ts diff --git a/dapp/src/theme/Alert.ts b/dapp/src/theme/alertTheme.ts similarity index 96% rename from dapp/src/theme/Alert.ts rename to dapp/src/theme/alertTheme.ts index 8b4aa91f9..905c63e97 100644 --- a/dapp/src/theme/Alert.ts +++ b/dapp/src/theme/alertTheme.ts @@ -12,10 +12,14 @@ const baseStyle = multiStyleConfig.definePartsStyle({ borderWidth: 1, borderStyle: "solid", borderColor: $borderColor.reference, - px: 5, - py: 5, + p: 4, rounded: "xl", }, + title: { + fontWeight: "semibold", + mr: 0, + }, + description: { fontWeight: "medium", textAlign: "start", diff --git a/dapp/src/theme/Button.ts b/dapp/src/theme/buttonTheme.ts similarity index 100% rename from dapp/src/theme/Button.ts rename to dapp/src/theme/buttonTheme.ts diff --git a/dapp/src/theme/Card.ts b/dapp/src/theme/cardTheme.ts similarity index 100% rename from dapp/src/theme/Card.ts rename to dapp/src/theme/cardTheme.ts diff --git a/dapp/src/theme/CloseButton.ts b/dapp/src/theme/closeButtonTheme.ts similarity index 100% rename from dapp/src/theme/CloseButton.ts rename to dapp/src/theme/closeButtonTheme.ts diff --git a/dapp/src/theme/Countdown.ts b/dapp/src/theme/countdownTheme.ts similarity index 100% rename from dapp/src/theme/Countdown.ts rename to dapp/src/theme/countdownTheme.ts diff --git a/dapp/src/theme/CurrencyBalance.ts b/dapp/src/theme/currencyBalanceTheme.ts similarity index 100% rename from dapp/src/theme/CurrencyBalance.ts rename to dapp/src/theme/currencyBalanceTheme.ts diff --git a/dapp/src/theme/Footer.ts b/dapp/src/theme/footerTheme.ts similarity index 100% rename from dapp/src/theme/Footer.ts rename to dapp/src/theme/footerTheme.ts diff --git a/dapp/src/theme/FormError.ts b/dapp/src/theme/formErrorTheme.ts similarity index 100% rename from dapp/src/theme/FormError.ts rename to dapp/src/theme/formErrorTheme.ts diff --git a/dapp/src/theme/FormLabel.ts b/dapp/src/theme/formLabelTheme.ts similarity index 100% rename from dapp/src/theme/FormLabel.ts rename to dapp/src/theme/formLabelTheme.ts diff --git a/dapp/src/theme/Form.ts b/dapp/src/theme/formTheme.ts similarity index 100% rename from dapp/src/theme/Form.ts rename to dapp/src/theme/formTheme.ts diff --git a/dapp/src/theme/Heading.ts b/dapp/src/theme/headingTheme.ts similarity index 100% rename from dapp/src/theme/Heading.ts rename to dapp/src/theme/headingTheme.ts diff --git a/dapp/src/theme/index.ts b/dapp/src/theme/index.ts index aaf6f4765..eb1c2cd41 100644 --- a/dapp/src/theme/index.ts +++ b/dapp/src/theme/index.ts @@ -1,5 +1,4 @@ import { extendTheme } from "@chakra-ui/react" -import { buttonTheme } from "./Button" import { colors, fonts, @@ -8,28 +7,27 @@ import { styles, zIndices, } from "./utils" -import { drawerTheme } from "./Drawer" -import { modalTheme } from "./Modal" -import { cardTheme } from "./Card" -import { tooltipTheme } from "./Tooltip" -import { headingTheme } from "./Heading" -import { sidebarTheme } from "./Sidebar" -import { currencyBalanceTheme } from "./CurrencyBalance" -import { tokenBalanceInputTheme } from "./TokenBalanceInput" -import { inputTheme } from "./Input" -import { alertTheme } from "./Alert" -import { formTheme } from "./Form" -import { formLabelTheme } from "./FormLabel" -import { formErrorTheme } from "./FormError" -import { tagTheme } from "./Tag" -import { spinnerTheme } from "./Spinner" -import { linkTheme } from "./Link" -import { skeletonTheme } from "./Skeleton" -import { closeButtonTheme } from "./CloseButton" -import { progressTheme } from "./Progress" -import { countdownTheme } from "./Countdown" -import { footerTheme } from "./Footer" -import { acreTVLProgressTheme } from "./AcreTVLProgress" +import { acreTVLProgressTheme } from "./acreTVLProgressTheme" +import { alertTheme } from "./alertTheme" +import { buttonTheme } from "./buttonTheme" +import { cardTheme } from "./cardTheme" +import { closeButtonTheme } from "./closeButtonTheme" +import { countdownTheme } from "./countdownTheme" +import { currencyBalanceTheme } from "./currencyBalanceTheme" +import { footerTheme } from "./footerTheme" +import { formErrorTheme } from "./formErrorTheme" +import { formLabelTheme } from "./formLabelTheme" +import { formTheme } from "./formTheme" +import { headingTheme } from "./headingTheme" +import { inputTheme } from "./inputTheme" +import { linkTheme } from "./linkTheme" +import { modalTheme } from "./modalTheme" +import { progressTheme } from "./progressTheme" +import { skeletonTheme } from "./skeletonTheme" +import { spinnerTheme } from "./spinnerTheme" +import { tagTheme } from "./tagTheme" +import { tokenBalanceInputTheme } from "./tokenBalanceInputTheme" +import { tooltipTheme } from "./tooltipTheme" const defaultTheme = { // TODO: Remove when dark mode is ready @@ -53,7 +51,6 @@ const defaultTheme = { Card: cardTheme, CloseButton: closeButtonTheme, CurrencyBalance: currencyBalanceTheme, - Drawer: drawerTheme, Form: formTheme, FormLabel: formLabelTheme, FormError: formErrorTheme, @@ -61,7 +58,6 @@ const defaultTheme = { Input: inputTheme, Link: linkTheme, Modal: modalTheme, - Sidebar: sidebarTheme, Spinner: spinnerTheme, Tag: tagTheme, TokenBalanceInput: tokenBalanceInputTheme, diff --git a/dapp/src/theme/Input.ts b/dapp/src/theme/inputTheme.ts similarity index 100% rename from dapp/src/theme/Input.ts rename to dapp/src/theme/inputTheme.ts diff --git a/dapp/src/theme/Link.ts b/dapp/src/theme/linkTheme.ts similarity index 100% rename from dapp/src/theme/Link.ts rename to dapp/src/theme/linkTheme.ts diff --git a/dapp/src/theme/Modal.ts b/dapp/src/theme/modalTheme.ts similarity index 84% rename from dapp/src/theme/Modal.ts rename to dapp/src/theme/modalTheme.ts index fd79f1bdc..d4cb15d5d 100644 --- a/dapp/src/theme/Modal.ts +++ b/dapp/src/theme/modalTheme.ts @@ -2,23 +2,27 @@ import { modalAnatomy as parts } from "@chakra-ui/anatomy" import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" const baseStyleContainer = defineStyle({ - px: 8, + px: { base: 3, sm: 8 }, }) const baseStyleDialog = defineStyle({ - marginTop: { base: 12, sm: "var(--chakra-space-modal_shift)" }, + marginTop: { + base: 12, + sm: "9.75rem", // 156px + }, marginBottom: 8, boxShadow: "none", borderRadius: "xl", + p: { base: 5, sm: 0 }, bg: "gold.100", border: "none", }) const baseCloseButton = defineStyle({ - top: -7, - right: -7, - boxSize: 7, - rounded: "100%", + top: { base: 3, sm: -7 }, + right: { base: 3, sm: -7 }, + boxSize: { sm: 7 }, + rounded: { sm: "100%" }, bg: "opacity.white.5", _hover: { @@ -37,8 +41,8 @@ const baseStyleHeader = defineStyle({ fontSize: "xl", lineHeight: "xl", fontWeight: "bold", - pt: 10, - px: 10, + pt: { sm: 8 }, + px: { sm: 8 }, pb: 8, }) @@ -50,15 +54,15 @@ const baseStyleBody = defineStyle({ alignItems: "center", gap: 6, pt: 0, - px: 8, - pb: 10, + px: { base: 0, sm: 8 }, + pb: { base: 0, sm: 8 }, }) const baseStyleFooter = defineStyle({ flexDirection: "column", gap: 6, - px: 8, - pb: 10, + px: { base: 0, sm: 8 }, + pb: { base: 0, sm: 8 }, }) const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys) diff --git a/dapp/src/theme/Progress.ts b/dapp/src/theme/progressTheme.ts similarity index 100% rename from dapp/src/theme/Progress.ts rename to dapp/src/theme/progressTheme.ts diff --git a/dapp/src/theme/Skeleton.ts b/dapp/src/theme/skeletonTheme.ts similarity index 100% rename from dapp/src/theme/Skeleton.ts rename to dapp/src/theme/skeletonTheme.ts diff --git a/dapp/src/theme/Spinner.ts b/dapp/src/theme/spinnerTheme.ts similarity index 100% rename from dapp/src/theme/Spinner.ts rename to dapp/src/theme/spinnerTheme.ts diff --git a/dapp/src/theme/Tag.ts b/dapp/src/theme/tagTheme.ts similarity index 100% rename from dapp/src/theme/Tag.ts rename to dapp/src/theme/tagTheme.ts diff --git a/dapp/src/theme/TokenBalanceInput.ts b/dapp/src/theme/tokenBalanceInputTheme.ts similarity index 100% rename from dapp/src/theme/TokenBalanceInput.ts rename to dapp/src/theme/tokenBalanceInputTheme.ts diff --git a/dapp/src/theme/Tooltip.ts b/dapp/src/theme/tooltipTheme.ts similarity index 100% rename from dapp/src/theme/Tooltip.ts rename to dapp/src/theme/tooltipTheme.ts diff --git a/dapp/src/theme/utils/semanticTokens.ts b/dapp/src/theme/utils/semanticTokens.ts index 8ad1298ba..8fb5ba562 100644 --- a/dapp/src/theme/utils/semanticTokens.ts +++ b/dapp/src/theme/utils/semanticTokens.ts @@ -1,11 +1,5 @@ export const semanticTokens = { space: { - header_height: 28, - header_height_xl: 36, - modal_shift: "9.75rem", // 156px dashboard_card_padding: 5, }, - sizes: { - sidebar_width: 80, - }, } diff --git a/dapp/src/theme/utils/zIndices.ts b/dapp/src/theme/utils/zIndices.ts index 3e9fb1a4b..3ada56278 100644 --- a/dapp/src/theme/utils/zIndices.ts +++ b/dapp/src/theme/utils/zIndices.ts @@ -1,9 +1,6 @@ export const zIndices = { - sidebar: 1450, - drawer: 1470, mobileBanner: 1500, header: 1400, footer: 1380, - modalContent: 1410, modalOverlay: 1390, } diff --git a/dapp/src/types/core.ts b/dapp/src/types/core.ts index 2fce09a01..9dcdb75b8 100644 --- a/dapp/src/types/core.ts +++ b/dapp/src/types/core.ts @@ -1,5 +1 @@ -export type Tuple = [T, T] - -export type WithRequired = T & { [P in K]-?: T[P] } - export type Optional = Pick, K> & Omit diff --git a/dapp/src/types/error.ts b/dapp/src/types/error.ts deleted file mode 100644 index 975af0f65..000000000 --- a/dapp/src/types/error.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ConnectionErrorData = { - title: string - description: string -} diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index 13bdda39c..7b39c9288 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -10,8 +10,6 @@ export * from "./fee" export * from "./modal" export * from "./form" export * from "./eip1193" -export * from "./error" export * from "./status" export * from "./orangekit" -export * from "./ledgerLive" export * from "./dapp-mode" diff --git a/dapp/src/types/ledgerLive.ts b/dapp/src/types/ledgerLive.ts deleted file mode 100644 index 6cd4aea65..000000000 --- a/dapp/src/types/ledgerLive.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type LedgerLiveError = { - message?: string - name?: string - stack?: string -} diff --git a/dapp/src/utils/acreApi.ts b/dapp/src/utils/acreApi.ts index 73a93b8a1..b67e3d918 100644 --- a/dapp/src/utils/acreApi.ts +++ b/dapp/src/utils/acreApi.ts @@ -45,9 +45,9 @@ const getPointsData = async () => { const response = await axios.get(url) return { - dropAt: response.data.dropAt, + nextDropTimestamp: response.data.dropAt, isCalculationInProgress: response.data.isCalculationInProgress, - totalPool: Number(response.data.totalPool), + totalPoolBalance: Number(response.data.totalPool), } } @@ -62,8 +62,8 @@ const getPointsDataByUser = async (address: string) => { const response = await axios.get(url) return { - claimed: Number(response.data.claimed), - unclaimed: Number(response.data.unclaimed), + totalBalance: Number(response.data.claimed), + claimableBalance: Number(response.data.unclaimed), isEligible: response.data.isEligible, } } diff --git a/dapp/src/utils/activities.ts b/dapp/src/utils/activities.ts index 7349a985b..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) => @@ -21,12 +26,31 @@ export function getEstimatedDuration( amount: bigint, type: ActivityType, ): string { + // Withdrawal duration is related to the tBTC redemption process, which takes + // approximately 5 - 7 hours. We use the average value of 6 hours. if (isWithdrawType(type)) return "6 hours" - if (amount < MIN_LIMIT_VALUE_DURATION) return "1 hour" - - if (amount >= MIN_LIMIT_VALUE_DURATION && amount < MAX_LIMIT_VALUE_DURATION) - return "2 hours" - + // Deposit duration is related to the tBTC minting process, which varies based + // on the amount of BTC deposited. + // Each threshold requires a different number of Bitcoin transaction confirmations: + // <0.1 BTC: 1 Bitcoin block confirmation (~10 minutes), + // >=0.1 BTC and <1 BTC: 3 Bitcoin block confirmations (~30 minutes), + // >=1 BTC: 6 Bitcoin block confirmations (~60 minutes). + // The duration of the transaction minting process depends on the Bitcoin network + // congestion, and the fee paid by the user. + // + // After the required number of Bitcoin block confirmations, the tBTC optimistic + // minting process starts. The optimistic minting process takes approximately + // 1 hour to complete. + // After optimistic minting is completed, the Acre bots will finalize the deposit + // in no more than 10 minutes. + // + // We round the estimated duration up to the nearest hour. + // + // For <0.1 BTC estimated duration is around 1 hour 20 minutes. + if (amount < MIN_LIMIT_VALUE_DURATION) return "2 hours" + // For <1 BTC estimated duration is around 1 hours 40 minutes. + if (amount < MAX_LIMIT_VALUE_DURATION) return "2 hours" + // For >=1 BTC estimated duration is around 2 hours 10 minutes. return "3 hours" } diff --git a/dapp/src/utils/index.ts b/dapp/src/utils/index.ts index c79d332b7..64e1ffc9b 100644 --- a/dapp/src/utils/index.ts +++ b/dapp/src/utils/index.ts @@ -3,7 +3,6 @@ export * from "./address" export * from "./forms" export * from "./currency" export * from "./chain" -export * from "./text" export * from "./time" export * from "./promise" export * from "./verifyDepositAddress" diff --git a/dapp/src/utils/orangekit/index.ts b/dapp/src/utils/orangekit/index.ts index 18f355df4..f900de0c9 100644 --- a/dapp/src/utils/orangekit/index.ts +++ b/dapp/src/utils/orangekit/index.ts @@ -1,19 +1,12 @@ -import { - ACRE_SESSION_EXPIRATION_TIME, - CONNECTION_ERRORS, - wallets, -} from "#/constants" -import { - ConnectionErrorData, - OrangeKitError, - OrangeKitConnector, -} from "#/types" +import { ACRE_SESSION_EXPIRATION_TIME, wallets } from "#/constants" +import { OrangeKitError, OrangeKitConnector } from "#/types" import { isUnsupportedBitcoinAddressError, isWalletNetworkDoesNotMatchProviderChainError, } from "@orangekit/react" import { Connector } from "wagmi" import { SignInWithWalletMessage } from "@orangekit/sign-in-with-wallet" +import { ConnectionAlert } from "#/components/ConnectWalletModal/ConnectWalletAlert" import { getExpirationDate } from "../time" import { getOrangeKitLedgerLiveConnector } from "./ledger-live" @@ -71,22 +64,22 @@ const typeConversionToConnector = (connector?: OrangeKitConnector): Connector => const parseOrangeKitConnectionError = ( error: OrangeKitError, -): ConnectionErrorData => { +): ConnectionAlert => { const { cause } = error if (isWalletConnectionRejectedError(cause)) { - return CONNECTION_ERRORS.REJECTED + return ConnectionAlert.Rejected } if (isUnsupportedBitcoinAddressError(cause)) { - return CONNECTION_ERRORS.NOT_SUPPORTED + return ConnectionAlert.NotSupported } if (isWalletNetworkDoesNotMatchProviderChainError(cause)) { - return CONNECTION_ERRORS.NETWORK_MISMATCH + return ConnectionAlert.NetworkMismatch } - return CONNECTION_ERRORS.DEFAULT + return ConnectionAlert.Default } async function verifySignInWithWalletMessage( @@ -107,6 +100,33 @@ async function verifySignInWithWalletMessage( return result.data } +/** + * Finds the extended public key (xpub) of the user's account from URL. Users + * can be redirected to the exact app in the Ledger Live application. One of the + * parameters passed via URL is `accountId` - the ID of the user's account in + * Ledger Live. + * @see https://developers.ledger.com/docs/ledger-live/exchange/earn/liveapp#url-parameters-for-direct-navigation + * + * @param {string} url Request url + * @returns The extended public key (xpub) of the user's account if the search + * parameter `accountId` exists in the URL. Otherwise `undefined`. + */ +function findXpubFromUrl(url: string): string | undefined { + const parsedUrl = new URL(url) + + const accountId = parsedUrl.searchParams.get("accountId") + + if (!accountId) return undefined + + // The fourth value separated by `:` is extended public key. See the + // account ID template: `js:2:bitcoin_testnet::`. + const xpubFromAccountId = accountId.split(":")[3] + + if (!xpubFromAccountId) return undefined + + return xpubFromAccountId +} + export default { getWalletInfo, isWalletInstalled, @@ -119,4 +139,5 @@ export default { isWalletConnectionRejectedError, verifySignInWithWalletMessage, getOrangeKitLedgerLiveConnector, + findXpubFromUrl, } diff --git a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts index 7dab9c5ad..316e628a4 100644 --- a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts +++ b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts @@ -58,9 +58,12 @@ function numberToValidHexString(value: number): string { return `0x${hex}` } -export type AcreLedgerLiveBitcoinProviderOptions = { - tryConnectToAddress: string | undefined -} +export type AcreLedgerLiveBitcoinProviderOptions = + | { + tryConnectToAddress?: string + tryConnectToAccountByXpub?: never + } + | { tryConnectToAddress?: never; tryConnectToAccountByXpub?: string } /** * Ledger Live Wallet API Bitcoin Provider. @@ -90,6 +93,7 @@ export default class AcreLedgerLiveBitcoinProvider network: BitcoinNetwork, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { const windowMessageTransport = new WindowMessageTransport() @@ -115,6 +119,7 @@ export default class AcreLedgerLiveBitcoinProvider walletApiClient: WalletAPIClient, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { this.#network = network @@ -140,12 +145,24 @@ export default class AcreLedgerLiveBitcoinProvider currencyIds, }) - if (this.#options.tryConnectToAddress) { + if ( + this.#options.tryConnectToAddress || + this.#options.tryConnectToAccountByXpub + ) { for (let i = 0; i < accounts.length; i += 1) { const acc = accounts[i] + if ( + this.#options.tryConnectToAccountByXpub && + // eslint-disable-next-line no-await-in-loop + (await this.#walletApiClient.bitcoin.getXPub(acc.id)) === + this.#options.tryConnectToAccountByXpub + ) { + this.#account = acc + break + } + // eslint-disable-next-line no-await-in-loop const address = await this.#getAddress(acc.id) - if (address === this.#options.tryConnectToAddress) { this.#account = acc break diff --git a/dapp/src/utils/referralProgram.ts b/dapp/src/utils/referralProgram.ts index 36f48b9a3..82a3bfbc5 100644 --- a/dapp/src/utils/referralProgram.ts +++ b/dapp/src/utils/referralProgram.ts @@ -9,9 +9,23 @@ const EMBEDDED_APP_TO_REFERRAL: Record = { "ledger-live": 2083, } -const isValidReferral = (value: number) => { - const isInteger = Number.isInteger(value) - return isInteger && value >= 0 && value <= MAX_UINT16 +const isValidReferral = (value: unknown) => { + let valueAsNumber: number | undefined + + if (typeof value === "string") { + // Only digits w/o leading zeros. + const isNumber = /^(?:[1-9][0-9]*|0)$/.test(value) + valueAsNumber = isNumber ? Number(value) : undefined + } else if (typeof value === "number") { + valueAsNumber = value + } + + return ( + !!valueAsNumber && + Number.isInteger(valueAsNumber) && + valueAsNumber >= 0 && + valueAsNumber <= MAX_UINT16 + ) } const getReferralFromURL = () => diff --git a/dapp/src/utils/router.ts b/dapp/src/utils/router.ts index 868ab3a0c..059f2872a 100644 --- a/dapp/src/utils/router.ts +++ b/dapp/src/utils/router.ts @@ -1,7 +1,4 @@ -import { To, redirect } from "react-router-dom" -import { isString } from "./type-check" - -const getURLPath = (to: To) => (isString(to) ? to : to.pathname) +import { redirect } from "react-router-dom" const getURLParamFromHref = (href: string, paramName: string) => { const { searchParams } = new URL(href) @@ -19,7 +16,6 @@ const redirectWithSearchParams = (url: string, to: string) => { } export default { - getURLPath, getURLParam, getURLParamFromHref, redirectWithSearchParams, diff --git a/dapp/src/utils/text.ts b/dapp/src/utils/text.ts deleted file mode 100644 index 36e034408..000000000 --- a/dapp/src/utils/text.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const capitalizeFirstLetter = (text: string): string => - text[0].toUpperCase() + text.slice(1) diff --git a/dapp/src/vite-env.d.ts b/dapp/src/vite-env.d.ts index 564bf6a80..eb8de6913 100644 --- a/dapp/src/vite-env.d.ts +++ b/dapp/src/vite-env.d.ts @@ -7,16 +7,18 @@ interface ImportMetaEnv { readonly VITE_REFERRAL: number readonly VITE_TBTC_API_ENDPOINT: string readonly VITE_GELATO_RELAY_API_KEY: string - readonly VITE_FEATURE_FLAG_GAMIFICATION_ENABLED: string readonly VITE_FEATURE_FLAG_WITHDRAWALS_ENABLED: string readonly VITE_FEATURE_FLAG_OKX_WALLET_ENABLED: string readonly VITE_FEATURE_FLAG_XVERSE_WALLET_ENABLED: string readonly VITE_FEATURE_FLAG_ACRE_POINTS_ENABLED: string readonly VITE_FEATURE_FLAG_TVL_ENABLED: string readonly VITE_FEATURE_GATING_DAPP_ENABLED: string + readonly VITE_FEATURE_POSTHOG_ENABLED: string readonly VITE_SUBGRAPH_API_KEY: string readonly VITE_LATEST_COMMIT_HASH: string readonly VITE_ACRE_API_ENDPOINT: string + readonly VITE_POSTHOG_API_HOST: string + readonly VITE_POSTHOG_API_KEY: string readonly VITE_FEATURE_MOBILE_MODE_ENABLED: string } diff --git a/dapp/src/wagmiConfig.ts b/dapp/src/wagmiConfig.ts index 19d1cf4e4..972a38fda 100644 --- a/dapp/src/wagmiConfig.ts +++ b/dapp/src/wagmiConfig.ts @@ -5,6 +5,7 @@ import { env } from "./constants" import { getLastUsedBtcAddress } from "./hooks/useLastUsedBtcAddress" import referralProgram, { EmbedApp } from "./utils/referralProgram" import { orangeKit } from "./utils" +import { AcreLedgerLiveBitcoinProviderOptions } from "./utils/orangekit/ledger-live/bitcoin-provider" const isTestnet = env.USE_TESTNET const CHAIN_ID = isTestnet ? sepolia.id : mainnet.id @@ -34,12 +35,19 @@ async function getWagmiConfig() { let createEmbedConnectorFn const embeddedApp = referralProgram.getEmbeddedApp() if (referralProgram.isEmbedApp(embeddedApp)) { + const lastUsedBtcAddress = getLastUsedBtcAddress() + const xpub = orangeKit.findXpubFromUrl(window.location.href) + const ledgerLiveConnectorOptions: AcreLedgerLiveBitcoinProviderOptions = + xpub + ? { tryConnectToAccountByXpub: xpub } + : { + tryConnectToAddress: lastUsedBtcAddress, + } + const orangeKitLedgerLiveConnector = orangeKit.getOrangeKitLedgerLiveConnector({ ...connectorConfig, - options: { - tryConnectToAddress: getLastUsedBtcAddress(), - }, + options: ledgerLiveConnectorOptions, }) const embedConnectorsMap: Record< diff --git a/dapp/test/sentry.test.ts b/dapp/test/sentry.test.ts new file mode 100644 index 000000000..cb7e9d896 --- /dev/null +++ b/dapp/test/sentry.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest" +import * as Sentry from "@sentry/react" +import sentry from "#/sentry" + +describe("sentry", () => { + describe("setUser", () => { + vi.mock("@sentry/react") + + const testCases = [ + { bitcoinAddress: undefined, expectedResult: null }, + { bitcoinAddress: "", expectedResult: null }, + { bitcoinAddress: " ", expectedResult: null }, + { + bitcoinAddress: "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem", + expectedResult: { id: "1f520a9757" }, + }, + { + bitcoinAddress: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + expectedResult: { id: "6cd42dab02" }, + }, + { + bitcoinAddress: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + expectedResult: { id: "6cd42dab02" }, + }, + ] + + describe.each(testCases)( + "when address is $bitcoinAddress", + ({ bitcoinAddress, expectedResult }) => { + it("should set expected user in Sentry", () => { + sentry.setUser(bitcoinAddress) + + expect(Sentry.setUser).toHaveBeenCalledWith(expectedResult) + }) + }, + ) + }) +}) diff --git a/dapp/test/utils/activities.test.ts b/dapp/test/utils/activities.test.ts new file mode 100644 index 000000000..c25c4f93a --- /dev/null +++ b/dapp/test/utils/activities.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest" +import { getEstimatedDuration } from "#/utils/activities" + +describe("Utils functions for activities", () => { + describe("getEstimatedDuration", () => { + describe("withdraw", () => { + describe.each([ + // 0.01 BTC + { value: 0.01, expectedResult: "6 hours" }, + // 0.1 BTC + { value: 0.1, expectedResult: "6 hours" }, + // 1 BTC + { value: 1, expectedResult: "6 hours" }, + // 10 BTC + { value: 10, expectedResult: "6 hours" }, + ])("when it is $value BTC", ({ value, expectedResult }) => { + it(`should return ${expectedResult}`, () => { + expect(getEstimatedDuration(BigInt(value * 1e8), "withdraw")).toEqual( + expectedResult, + ) + }) + }) + }) + + describe("deposit", () => { + describe.each([ + // 0.0001 BTC + { value: 0.0001, expectedResult: "2 hours" }, + // 0.001 BTC + { value: 0.001, expectedResult: "2 hours" }, + // 0.01 BTC + { value: 0.01, expectedResult: "2 hours" }, + // 0.09 BTC + { value: 0.09, expectedResult: "2 hours" }, + // 0.1 BTC + { value: 0.1, expectedResult: "2 hours" }, + // 0.9 BTC + { value: 0.9, expectedResult: "2 hours" }, + // 1 BTC + { value: 1, expectedResult: "3 hours" }, + // 10 BTC + { value: 10, expectedResult: "3 hours" }, + ])("when it is $value BTC", ({ value, expectedResult }) => { + it(`should return ${expectedResult}`, () => { + expect(getEstimatedDuration(BigInt(value * 1e8), "deposit")).toEqual( + expectedResult, + ) + }) + }) + }) + }) +}) diff --git a/dapp/src/utils/tests/numbers.test.ts b/dapp/test/utils/numbers.test.ts similarity index 96% rename from dapp/src/utils/tests/numbers.test.ts rename to dapp/test/utils/numbers.test.ts index 6151a888a..1699ee0d7 100644 --- a/dapp/src/utils/tests/numbers.test.ts +++ b/dapp/test/utils/numbers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest" -import { roundUp } from "../numbers" +import { roundUp } from "#/utils/numbers" describe("Utils functions for numbers", () => { describe("roundUp", () => { diff --git a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts b/dapp/test/utils/orangekit/ledger-live/bitcoin-provider.test.ts similarity index 99% rename from dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts rename to dapp/test/utils/orangekit/ledger-live/bitcoin-provider.test.ts index 58d1d0f89..36ed0375e 100644 --- a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts +++ b/dapp/test/utils/orangekit/ledger-live/bitcoin-provider.test.ts @@ -8,7 +8,7 @@ import { Balance } from "@orangekit/react/dist/src/wallet/bitcoin-wallet-provide import { AcreMessageType } from "@ledgerhq/wallet-api-acre-module" import { ZeroAddress } from "ethers" import BigNumber from "bignumber.js" -import AcreLedgerLiveBitcoinProvider from "../bitcoin-provider" +import AcreLedgerLiveBitcoinProvider from "#/utils/orangekit/ledger-live/bitcoin-provider" describe("AcreLedgerLiveBitcoinProvider", () => { const bitcoinAddress = "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf2c53ac6..b967a0030 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,11 +39,11 @@ importers: specifier: 1.6.0 version: 1.6.0 '@orangekit/react': - specifier: 1.0.0-beta.33 - version: 1.0.0-beta.33(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) + specifier: 1.0.0-beta.34 + version: 1.0.0-beta.34(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) '@orangekit/sign-in-with-wallet': - specifier: 1.0.0-beta.6 - version: 1.0.0-beta.6(bech32@2.0.0)(ethers@6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + specifier: 1.0.0-beta.7 + version: 1.0.0-beta.7(bech32@2.0.0)(ethers@6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@reduxjs/toolkit': specifier: ^2.2.0 version: 2.2.5(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1) @@ -89,6 +89,9 @@ importers: mustache: specifier: ^4.2.0 version: 4.2.0 + posthog-js: + specifier: ^1.186.1 + version: 1.186.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -172,8 +175,8 @@ importers: specifier: 2.5.0-dev.3 version: 2.5.0-dev.3(@keep-network/keep-core@1.8.1-dev.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@orangekit/sdk': - specifier: 1.0.0-beta.18 - version: 1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + specifier: 1.0.0-beta.19 + version: 1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) ethers: specifier: 6.10.0 version: 6.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -2692,17 +2695,17 @@ packages: '@orangekit/contracts@1.0.0-beta.3': resolution: {integrity: sha512-xP1Oz/JzuHypg5DcsHayINhFSL5M/tCmRP/stmNAvjeebXhrKuALKAHWn98H0Mo3QfYYrz8UcltQPeH6+68n6A==} - '@orangekit/react@1.0.0-beta.33': - resolution: {integrity: sha512-b5VMFB2tZad86FNdtylwsKaXlXlrSjY9cIMsaJMUh7EEWEoVvtndbCsHrISKbspm7y+S5nNmGlE3WcQvR8RWWw==} + '@orangekit/react@1.0.0-beta.34': + resolution: {integrity: sha512-/PTfYcu/BU4ssltIuy+AKqbaGzue+MEJHwd7+4aZhXhwJ+/xrYo3mJ1tSwj6K8lAJCl/N25vq7zNJgVgjLK1WQ==} - '@orangekit/sdk@1.0.0-beta.18': - resolution: {integrity: sha512-031JkI0F8gdGzj/7sKbJ52k0Xj/E4uwp/O0SWFDCXyZC0vwtjlw3ItAaZz+nIj820b6dSM6pjL+P7um3m9AK3Q==} + '@orangekit/sdk@1.0.0-beta.19': + resolution: {integrity: sha512-sIqzu3QTb0WkB4Ir4zHX4tP7hf+uZBXX60q/ww6UzwPjoqGL3dOdui59lTZSNygdMbydLE3y9FSNZPNkQvm+gA==} - '@orangekit/sign-in-with-wallet-parser@1.0.0-beta.6': - resolution: {integrity: sha512-Yi6ohSJV4/Ovrq5c7jD+kPE8pZxLhWtFbZjKRwUW8JL60P/tcyT5o0etul0reqcY2iBlIo5aoC2Hh0noRGl86w==} + '@orangekit/sign-in-with-wallet-parser@1.0.0-beta.7': + resolution: {integrity: sha512-C7gmliw2TJOQzIMpuhDZhIGlp7zFSqNsa/oevNaqnB+i0zFIsXZjsKg2oddr8smYG1+KhbX+V+O3AeaPCSsK9A==} - '@orangekit/sign-in-with-wallet@1.0.0-beta.6': - resolution: {integrity: sha512-UzNG8QHehem2wayWDyMBd13z1lQUBKsSq+NjGK6KHYWUbet6EizBeVZK7jSruzTHba3gDI8I3NH071E6sxTFtQ==} + '@orangekit/sign-in-with-wallet@1.0.0-beta.7': + resolution: {integrity: sha512-o2U5oi8b+2cc0Lel4p7myp1pQ6J2OLCdVTRhYQ4Te4+iqVFns1UlHx28pEo0zvGCJ5CwHQBmJHX6QguL02Kj4Q==} peerDependencies: bech32: '=2.0.0' ethers: ^6.0.8 @@ -3113,8 +3116,13 @@ packages: peerDependencies: ethers: 5.4.0 + '@safe-global/safe-core-sdk-types@4.1.1': + resolution: {integrity: sha512-5NIWG7OjV+C5iquux0yPcu8SHUzg1qJXJ/jAQcPwXGTC7ZVsFawnR43/l2Vg9zEwf0RF0xTm3W8DXkaBYORiEQ==} + deprecated: 'WARNING: This project has been renamed to @safe-global/types-kit. Please, migrate from @safe-global/safe-core-sdk-types@5.1.0 to @safe-global/types-kit@1.0.0.' + '@safe-global/safe-core-sdk-types@5.0.1': resolution: {integrity: sha512-xIlHZ9kaAIwEhR0OY0i2scdcQyrc0tDJ+eZZ04lhvg81cgYLY1Z5wfJQqazR2plPT1Hz0A9C79jYdUVvzoF/tw==} + deprecated: 'WARNING: This project has been renamed to @safe-global/types-kit. Please, migrate from @safe-global/safe-core-sdk-types@5.1.0 to @safe-global/types-kit@1.0.0.' '@safe-global/safe-deployments@1.37.0': resolution: {integrity: sha512-OInLNWC9EPem/eOsvPdlq4Gt/08Nfhslm9z6T92Jvjmcu6hs85vjfnDP1NrzwcOmsCarATU5NH2bTITd9VNCPw==} @@ -5063,6 +5071,9 @@ packages: core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -6068,6 +6079,9 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8616,6 +8630,9 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.186.1: + resolution: {integrity: sha512-m6TNW01nfqErwMxaZxNScYdMaUJO0s3bbmt/tboL29yZDnuHdOiYFbG+T4MCxdFxjWRa5gOR25bQD/SSt1t/4A==} + preact@10.22.0: resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} @@ -10486,6 +10503,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web3-bzz@1.10.4: resolution: {integrity: sha512-ZZ/X4sJ0Uh2teU9lAGNS8EjveEppoHNQiKlOXAjedsrdWuaMErBPdLQjXfcrYvN6WM6Su9PMsAxf3FXXZ+HwQw==} engines: {node: '>=8.0.0'} @@ -14831,9 +14851,9 @@ snapshots: transitivePeerDependencies: - ethers - '@orangekit/react@1.0.0-beta.33(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)': + '@orangekit/react@1.0.0-beta.34(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)': dependencies: - '@orangekit/sdk': 1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@orangekit/sdk': 1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@rainbow-me/rainbowkit': 2.0.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.8.16(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.5.12(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.8.16(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) react: 18.3.1 @@ -14870,13 +14890,14 @@ snapshots: - utf-8-validate - zod - '@orangekit/sdk@1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': + '@orangekit/sdk@1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@gelatonetwork/relay-sdk': 5.5.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@orangekit/contracts': 1.0.0-beta.3(ethers@6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@safe-global/protocol-kit': 3.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@safe-global/safe-core-sdk-types': 4.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) bitcoinjs-lib: 6.1.5 ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -14886,16 +14907,16 @@ snapshots: - supports-color - utf-8-validate - '@orangekit/sign-in-with-wallet-parser@1.0.0-beta.6': + '@orangekit/sign-in-with-wallet-parser@1.0.0-beta.7': dependencies: '@noble/hashes': 1.4.0 apg-js: 4.4.0 uri-js: 4.4.1 valid-url: 1.0.9 - '@orangekit/sign-in-with-wallet@1.0.0-beta.6(bech32@2.0.0)(ethers@6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + '@orangekit/sign-in-with-wallet@1.0.0-beta.7(bech32@2.0.0)(ethers@6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@orangekit/sign-in-with-wallet-parser': 1.0.0-beta.6 + '@orangekit/sign-in-with-wallet-parser': 1.0.0-beta.7 '@stablelib/random': 1.0.2 bech32: 2.0.0 ethers: 6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -15492,6 +15513,18 @@ snapshots: dependencies: ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@safe-global/safe-core-sdk-types@4.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@safe-global/safe-deployments': 1.37.0 + ethers: 6.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + web3-core: 1.10.4(encoding@0.1.13) + web3-utils: 1.10.4 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@safe-global/safe-core-sdk-types@5.0.1(typescript@5.4.5)(zod@3.23.8)': dependencies: abitype: 1.0.2(typescript@5.4.5)(zod@3.23.8) @@ -18323,6 +18356,8 @@ snapshots: dependencies: browserslist: 4.23.1 + core-js@3.39.0: {} + core-util-is@1.0.2: {} core-util-is@1.0.3: {} @@ -19729,6 +19764,8 @@ snapshots: dependencies: pend: 1.2.0 + fflate@0.4.8: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -22902,6 +22939,13 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + posthog-js@1.186.1: + dependencies: + core-js: 3.39.0 + fflate: 0.4.8 + preact: 10.22.0 + web-vitals: 4.2.4 + preact@10.22.0: {} prelude-ls@1.1.2: {} @@ -25085,6 +25129,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-vitals@4.2.4: {} + web3-bzz@1.10.4(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@types/node': 12.20.55 diff --git a/sdk/package.json b/sdk/package.json index 2a79b6f5d..9a67628dc 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -27,7 +27,7 @@ "dependencies": { "@acre-btc/contracts": "workspace:*", "@keep-network/tbtc-v2.ts": "2.5.0-dev.3", - "@orangekit/sdk": "1.0.0-beta.18", + "@orangekit/sdk": "1.0.0-beta.19", "ethers": "6.10.0", "ethers-v5": "npm:ethers@^5.5.2" } diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index d1a84eabd..652dc5a25 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -85,7 +85,9 @@ class Acre { const orangeKit = await OrangeKitSdk.init( Number(ethereumChainId), ethereumRpcUrl, - new GelatoTransactionSender(gelatoApiKey), + new GelatoTransactionSender(gelatoApiKey, { + backoffRetrier: { retries: 7, backoffStepMs: 3000 }, + }), ) const contracts = await getEthereumContracts( diff --git a/sdk/src/lib/api/HttpApi.ts b/sdk/src/lib/api/HttpApi.ts index d1759d6d2..eb9be45ca 100644 --- a/sdk/src/lib/api/HttpApi.ts +++ b/sdk/src/lib/api/HttpApi.ts @@ -1,11 +1,48 @@ +import { backoffRetrier, RetryOptions } from "../utils" + /** * Represents an abstract HTTP API. */ export default abstract class HttpApi { #apiUrl: string - constructor(apiUrl: string) { + /** + * Retry options for API requests. + */ + #retryOptions: RetryOptions + + constructor( + apiUrl: string, + retryOptions: RetryOptions = { + retries: 5, + backoffStepMs: 1000, + }, + ) { this.#apiUrl = apiUrl + this.#retryOptions = retryOptions + } + + /** + * Makes an HTTP request with retry logic. + * @param requestFn Function that returns a Promise of the HTTP response. + * @returns The HTTP response. + * @throws Error if the request fails after all retries. + */ + async #requestWithRetry( + requestFn: () => Promise, + ): Promise { + return backoffRetrier( + this.#retryOptions.retries, + this.#retryOptions.backoffStepMs, + )(async () => { + const response = await requestFn() + + if (!response.ok) { + throw new Error(`Request failed: ${await response.text()}`) + } + + return response + }) } /** @@ -18,10 +55,12 @@ export default abstract class HttpApi { endpoint: string, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - credentials: "include", - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + credentials: "include", + ...requestInit, + }), + ) } /** @@ -36,12 +75,14 @@ export default abstract class HttpApi { body: unknown, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - method: "POST", - body: JSON.stringify(body), - credentials: "include", - headers: { "Content-Type": "application/json" }, - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + method: "POST", + body: JSON.stringify(body), + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...requestInit, + }), + ) } } diff --git a/sdk/src/lib/api/TbtcApi.ts b/sdk/src/lib/api/TbtcApi.ts index 931e36601..c4ce503c4 100644 --- a/sdk/src/lib/api/TbtcApi.ts +++ b/sdk/src/lib/api/TbtcApi.ts @@ -38,12 +38,11 @@ export default class TbtcApi extends HttpApi { * otherwise. */ async saveReveal(revealData: SaveRevealRequest): Promise { - const response = await this.postRequest("reveals", revealData) - - if (!response.ok) - throw new Error( - `Reveal not saved properly in the database, response: ${response.status}`, - ) + const response = await this.postRequest("reveals", revealData).catch( + (error) => { + throw new Error(`Failed to save reveal: ${error}`) + }, + ) const { success } = (await response.json()) as { success: boolean } @@ -60,11 +59,11 @@ export default class TbtcApi extends HttpApi { depositStatus: DepositStatus fundingOutpoint: BitcoinTxOutpoint }> { - const response = await this.postRequest("deposits", depositData) - if (!response.ok) - throw new Error( - `Bitcoin deposit creation failed, response: ${response.status}`, - ) + const response = await this.postRequest("deposits", depositData).catch( + (error) => { + throw new Error(`Failed to create deposit: ${error}`) + }, + ) const responseData = (await response.json()) as CreateDepositResponse @@ -85,10 +84,9 @@ export default class TbtcApi extends HttpApi { async getDepositsByOwner(depositOwner: ChainIdentifier): Promise { const response = await this.getRequest( `deposits/${depositOwner.identifierHex}`, - ) - - if (!response.ok) - throw new Error(`Failed to fetch deposits: ${response.status}`) + ).catch((error) => { + throw new Error(`Failed to fetch deposits: ${error}`) + }) const responseData = (await response.json()) as Deposit[] diff --git a/solidity/README.md b/solidity/README.md index e842e5564..da86c24f0 100644 --- a/solidity/README.md +++ b/solidity/README.md @@ -18,7 +18,7 @@ pnpm install ### Testing -To run the test execute: +To run the tests execute: ``` $ pnpm test