diff --git a/.changeset/flat-wombats-breathe.md b/.changeset/flat-wombats-breathe.md new file mode 100644 index 0000000000..03ed902ae2 --- /dev/null +++ b/.changeset/flat-wombats-breathe.md @@ -0,0 +1,5 @@ +--- +"@talismn/balances": patch +--- + +rename "direct staking" => "delegated staking" diff --git a/.changeset/loud-maps-pay.md b/.changeset/loud-maps-pay.md new file mode 100644 index 0000000000..ceead5b348 --- /dev/null +++ b/.changeset/loud-maps-pay.md @@ -0,0 +1,5 @@ +--- +"@talismn/util": patch +--- + +added formatTokenDecimals util fn diff --git a/.changeset/slimy-moons-hammer.md b/.changeset/slimy-moons-hammer.md new file mode 100644 index 0000000000..9255a38389 --- /dev/null +++ b/.changeset/slimy-moons-hammer.md @@ -0,0 +1,5 @@ +--- +"@talismn/chaindata-provider": patch +--- + +Update init data diff --git a/apps/extension/.env.sample b/apps/extension/.env.sample index f85583430c..192823b924 100644 --- a/apps/extension/.env.sample +++ b/apps/extension/.env.sample @@ -5,7 +5,7 @@ # TEST_MNEMONIC=blarg blarg blarg blarg blarg blarg blarg blarg blarg blarg blarg blarg # for dev only, Coingecko api settings -# # with a paid api key, use url https://pro-api.coingecko.com +# # with a paid api key, use url https://pro-api.coingecko.com # COINGECKO_API_URL=https://api.coingecko.com # # with a paid api key, use header name x-cg-pro-api-key # COINGECKO_API_KEY_NAME=x-cg-demo-api-key @@ -25,3 +25,6 @@ # Talisman core dev team setup for tx analysis : # BLOWFISH_API_KEY= # BLOWFISH_QA_API_KEY= # optional : only used for canary, ci & qa builds + +# TAOSTATS_BASE_PATH=https://taostats-api-proxy.talismn.workers.dev +# TAOSTATS_API_KEY= diff --git a/apps/extension/package.json b/apps/extension/package.json index 66f785aa4a..d930337a4f 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "extension", - "version": "2.1.1", + "version": "2.2.0", "private": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/extension/src/@talisman/components/ScrollContainerDraggableHorizontal.tsx b/apps/extension/src/@talisman/components/ScrollContainerDraggableHorizontal.tsx new file mode 100644 index 0000000000..792ea4e342 --- /dev/null +++ b/apps/extension/src/@talisman/components/ScrollContainerDraggableHorizontal.tsx @@ -0,0 +1,111 @@ +import { classNames } from "@talismn/util" +import React, { MouseEvent, TouchEvent, useEffect, useRef, useState } from "react" + +type ScrollContainerDraggableHorizontalProps = { + children?: React.ReactNode + className?: string +} + +export const ScrollContainerDraggableHorizontal = ({ + children, + className, +}: ScrollContainerDraggableHorizontalProps) => { + const containerRef = useRef(null) + + // State to track dragging + const [isDragging, setIsDragging] = useState(false) + const [startPosition, setStartPosition] = useState(0) + const [scrollLeft, setScrollLeft] = useState(0) + + // State to track if there's more content to the left or right + const [more, setMore] = useState<{ left: boolean; right: boolean }>({ + left: false, + right: false, + }) + + useEffect(() => { + const scrollable = containerRef.current + if (!scrollable) return + + const handleDetectScroll = () => { + setMore({ + left: scrollable.scrollLeft > 0, + right: scrollable.scrollWidth - scrollable.scrollLeft > scrollable.clientWidth, + }) + } + + // Attach event listeners + scrollable.addEventListener("scroll", handleDetectScroll) + window.addEventListener("resize", handleDetectScroll) + + // Initial detection + handleDetectScroll() + + // Fix for initial load when scrollWidth might not be calculated yet + setTimeout(handleDetectScroll, 50) + + // Cleanup + return () => { + scrollable.removeEventListener("scroll", handleDetectScroll) + window.removeEventListener("resize", handleDetectScroll) + } + }, []) + + const handleDragStart = (e: MouseEvent | TouchEvent) => { + e.preventDefault() + setIsDragging(true) + containerRef.current?.classList.add("cursor-grabbing") + const pageX = "touches" in e ? e.touches[0].pageX : e.pageX + setStartPosition(pageX) + setScrollLeft(containerRef.current?.scrollLeft || 0) + } + + const handleDragEnd = () => { + setIsDragging(false) + containerRef.current?.classList.remove("cursor-grabbing") + } + + const handleDragMove = (e: MouseEvent | TouchEvent) => { + if (!isDragging) return + e.preventDefault() + const pageX = "touches" in e ? e.touches[0].pageX : e.pageX + const distance = pageX - startPosition + if (containerRef.current) { + containerRef.current.scrollLeft = scrollLeft - distance + } + } + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+ {children} +
+ {/* Left gradient overlay */} +
+ {/* Right gradient overlay */} +
+
+ ) +} diff --git a/apps/extension/src/ui/api/api.ts b/apps/extension/src/ui/api/api.ts index 3a35d67645..69ab527796 100644 --- a/apps/extension/src/ui/api/api.ts +++ b/apps/extension/src/ui/api/api.ts @@ -346,8 +346,8 @@ export const api: MessageTypes = { }), // asset discovery - assetDiscoveryStartScan: (mode, addresses) => - messageService.sendMessage("pri(assetDiscovery.scan.start)", { mode, addresses }), + assetDiscoveryStartScan: (scope) => + messageService.sendMessage("pri(assetDiscovery.scan.start)", scope), assetDiscoveryStopScan: () => messageService.sendMessage("pri(assetDiscovery.scan.stop)", null), // nfts diff --git a/apps/extension/src/ui/api/types.ts b/apps/extension/src/ui/api/types.ts index 38d70e6e28..0270240bf4 100644 --- a/apps/extension/src/ui/api/types.ts +++ b/apps/extension/src/ui/api/types.ts @@ -1,7 +1,7 @@ import type { KeyringPair$Json } from "@polkadot/keyring/types" import type { HexString } from "@polkadot/util/types" import { KeypairType } from "@polkadot/util-crypto/types" -import { Address, BalanceJson } from "@talismn/balances" +import { BalanceJson } from "@talismn/balances" import { Chain, ChainId, @@ -25,7 +25,7 @@ import { AddressesByChain, AnalyticsCaptureRequest, AnyEthRequestChainId, - AssetDiscoveryMode, + AssetDiscoveryScanScope, AssetTransferMethod, AuthorisedSiteUpdate, AuthorizedSite, @@ -335,7 +335,7 @@ export default interface MessageTypes { blockHash?: HexString, ) => Promise - assetDiscoveryStartScan: (mode: AssetDiscoveryMode, addresses?: Address[]) => Promise + assetDiscoveryStartScan: (scope: AssetDiscoveryScanScope) => Promise assetDiscoveryStopScan: () => Promise nftsSubscribe: (cb: (data: NftData) => void) => UnsubscribeFn diff --git a/apps/extension/src/ui/apps/dashboard/index.tsx b/apps/extension/src/ui/apps/dashboard/index.tsx index e6973d9ce5..9b85371d7f 100644 --- a/apps/extension/src/ui/apps/dashboard/index.tsx +++ b/apps/extension/src/ui/apps/dashboard/index.tsx @@ -7,7 +7,6 @@ import { FullScreenLocked } from "@talisman/components/FullScreenLocked" import { NavigateWithQuery } from "@talisman/components/NavigateWithQuery" import { SuspenseTracker } from "@talisman/components/SuspenseTracker" import { api } from "@ui/api" -import { AssetDiscoveryDashboardAlert } from "@ui/domains/AssetDiscovery/AssetDiscoveryDashboardAlert" import { DatabaseErrorAlert } from "@ui/domains/Settings/DatabaseErrorAlert" import { useLoginCheck } from "@ui/hooks/useLoginCheck" import { useModalSubscription } from "@ui/hooks/useModalSubscription" @@ -181,7 +180,6 @@ const Dashboard = () => ( - ) diff --git a/apps/extension/src/ui/apps/dashboard/layout/notifications/DashboardNotificationsAndModals.tsx b/apps/extension/src/ui/apps/dashboard/layout/notifications/DashboardNotificationsAndModals.tsx index 8a2a3eab01..efe014c40a 100644 --- a/apps/extension/src/ui/apps/dashboard/layout/notifications/DashboardNotificationsAndModals.tsx +++ b/apps/extension/src/ui/apps/dashboard/layout/notifications/DashboardNotificationsAndModals.tsx @@ -9,9 +9,9 @@ import { BuyTokensModal } from "@ui/domains/Asset/Buy/BuyTokensModal" import { CopyAddressModal } from "@ui/domains/CopyAddress" import { GetStartedModals } from "@ui/domains/Portfolio/GetStarted/GetStartedModals" import { MigratePasswordModal } from "@ui/domains/Settings/MigratePassword/MigratePasswordModal" -import { NomPoolBondModal } from "@ui/domains/Staking/NomPoolBond/NomPoolBondModal" -import { NomPoolUnbondModal } from "@ui/domains/Staking/NomPoolUnbond/NomPoolUnbondModal" +import { BondModal } from "@ui/domains/Staking/Bond/BondModal" import { NomPoolWithdrawModal } from "@ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawModal" +import { UnbondModal } from "@ui/domains/Staking/Unbond/UnbondModal" import { ExplorerNetworkPickerModal } from "@ui/domains/ViewOnExplorer" import DashboardNotifications from "." @@ -49,8 +49,8 @@ export const DashboardNotificationsAndModals = () => { - - + + diff --git a/apps/extension/src/ui/apps/dashboard/routes/Settings/AssetsDiscovery/AssetDiscoveryPage.tsx b/apps/extension/src/ui/apps/dashboard/routes/Settings/AssetsDiscovery/AssetDiscoveryPage.tsx index 4ed035e240..05f2b40542 100644 --- a/apps/extension/src/ui/apps/dashboard/routes/Settings/AssetsDiscovery/AssetDiscoveryPage.tsx +++ b/apps/extension/src/ui/apps/dashboard/routes/Settings/AssetsDiscovery/AssetDiscoveryPage.tsx @@ -1,6 +1,6 @@ import { bind } from "@react-rxjs/core" import { Address, BalanceFormatter } from "@talismn/balances" -import { EvmNetworkId, Token, TokenId } from "@talismn/chaindata-provider" +import { Chain, EvmNetwork, EvmNetworkId, Token, TokenId } from "@talismn/chaindata-provider" import { ChevronDownIcon, DiamondIcon, @@ -11,7 +11,7 @@ import { XIcon, } from "@talismn/icons" import { classNames } from "@talismn/util" -import { ChangeEventHandler, FC, ReactNode, useCallback, useEffect, useMemo, useRef } from "react" +import { ChangeEventHandler, FC, ReactNode, useCallback, useMemo, useRef } from "react" import { Trans, useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { useIntersection } from "react-use" @@ -33,7 +33,6 @@ import { AccountJsonAny, activeEvmNetworksStore, activeTokensStore, - AssetDiscoveryMode, DiscoveredBalance, isEvmNetworkActive, isTokenActive, @@ -45,6 +44,7 @@ import { api } from "@ui/api" import { AnalyticsPage } from "@ui/api/analytics" import { AccountIcon } from "@ui/domains/Account/AccountIcon" import { AccountsStack } from "@ui/domains/Account/AccountIconsStack" +import { ChainLogo } from "@ui/domains/Asset/ChainLogo" import { Fiat } from "@ui/domains/Asset/Fiat" import { TokenLogo } from "@ui/domains/Asset/TokenLogo" import Tokens from "@ui/domains/Asset/Tokens" @@ -55,15 +55,16 @@ import { useAccounts, useActiveEvmNetworksState, useActiveTokensState, - useAppState, useAssetDiscoveryScan, useAssetDiscoveryScanProgress, useBalancesHydrate, + useChainsMap, useEvmNetwork, useEvmNetworks, useEvmNetworksMap, useSetting, useToken, + useTokens, useTokensMap, } from "@ui/state" import { isErc20Token } from "@ui/util/isErc20Token" @@ -105,7 +106,11 @@ const AccountsTooltip: FC<{ addresses: Address[] }> = ({ addresses }) => { key={account.address} className="flex w-[30rem] items-center gap-2 overflow-hidden whitespace-nowrap text-sm" > - +
{account.name}
{shortenAddress(account.address)}
@@ -114,6 +119,47 @@ const AccountsTooltip: FC<{ addresses: Address[] }> = ({ addresses }) => { ) } +const NetworksTooltip: FC<{ networks: (EvmNetwork | Chain)[] }> = ({ networks }) => { + const { t } = useTranslation() + + const tokens = useTokens() + const networksWIthTokens = useMemo( + () => + networks + .map( + (n) => + [ + n, + tokens.filter((t) => t.evmNetwork?.id === n.id || t.chain?.id === n.id).length, + ] as const, + ) + .sort((a, b) => b[1] - a[1]), + [networks, tokens], + ) + + return ( +
+
{t("Networks")}
+
+ {networksWIthTokens.slice(0, 5).map(([network, tokens]) => ( +
+ +
{network.name}
+
{t("{{count}} tokens", { count: tokens })}
+
+ ))} + {networksWIthTokens.length > 5 && ( +
+ {t("and {{count}} more", { count: networksWIthTokens.length - 5 })} +
+ )} +
+ ) +} + const useBlockExplorerUrl = (token: Token | null) => { const evmNetwork = useEvmNetwork(token?.evmNetwork?.id) @@ -332,22 +378,28 @@ const AssetTable: FC = () => { const Header: FC = () => { const { t } = useTranslation("admin") const isInitializing = useIsInitializingScan() - const { balances, accountsCount, tokensCount, percent, isInProgress } = + const { balances, accountsCount, networksCount, tokensCount, percent, isInProgress } = useAssetDiscoveryScanProgress() const [includeTestnets] = useSetting("useTestnets") const activeNetworks = useEvmNetworks({ activeOnly: true, includeTestnets }) const allNetworks = useEvmNetworks({ activeOnly: false, includeTestnets }) + const allAccounts = useAccounts("all") + const addresses = allAccounts.map((a) => a.address) + const effectivePercent = isInitializing ? 0 : percent const handleScanClick = useCallback( - (mode: AssetDiscoveryMode) => async () => { + (all: boolean) => async () => { isInitializingScan$.next(true) - await api.assetDiscoveryStartScan(mode) + await api.assetDiscoveryStartScan({ + networkIds: (all ? allNetworks : activeNetworks).map((n) => n.id), + addresses, + }) isInitializingScan$.next(false) }, - [], + [activeNetworks, addresses, allNetworks], ) const handleCancelScanClick = useCallback(() => { @@ -363,21 +415,29 @@ const Header: FC = () => { )} />
- {isInitializing || isInProgress || balances.length ? ( + {isInitializing || isInProgress || balances.length || !!percent ? ( <>
{isInitializing ? t("Initialising...") : isInProgress - ? t("Scanning {{tokensCount}} tokens for {{count}} account(s)", { - tokensCount, - count: accountsCount, - }) - : t("Scanned {{tokensCount}} tokens for {{count}} account(s)", { - tokensCount, - count: accountsCount, - })} + ? t( + "Scanning {{tokensCount}} tokens for {{accountsCount}} account(s) on {{networksCount}} network(s)", + { + tokensCount, + accountsCount, + networksCount, + }, + ) + : t( + "Scanned {{tokensCount}} tokens for {{accountsCount}} account(s) on {{networksCount}} network(s)", + { + tokensCount, + accountsCount, + networksCount, + }, + )}
{effectivePercent}%
@@ -425,10 +485,10 @@ const Header: FC = () => { - + {t("Scan active networks")} ({activeNetworks.length}) - + {t("Scan all networks")} ({allNetworks.length}) @@ -453,17 +513,33 @@ const AccountsWrapper: FC<{ ) } +const NetworksWrapper: FC<{ + children?: ReactNode + networks: (EvmNetwork | Chain)[] + className?: string +}> = ({ children, networks, className }) => { + return ( + + {children} + + + + + ) +} + const ScanInfo: FC = () => { const { t } = useTranslation("admin") const isInitializing = useIsInitializingScan() const { balancesByTokenId, balances, isInProgress } = useAssetDiscoveryScanProgress() - const { lastScanAccounts, lastScanTimestamp } = useAssetDiscoveryScan() + const { lastScanAccounts, lastScanNetworks, lastScanTimestamp } = useAssetDiscoveryScan() const activeEvmNetworks = useActiveEvmNetworksState() const activeTokens = useActiveTokensState() const tokensMap = useTokensMap() const evmNetworksMap = useEvmNetworksMap() + const chainsMap = useChainsMap() const canEnable = useMemo(() => { const tokenIds = Object.keys(balancesByTokenId) @@ -501,6 +577,14 @@ const ScanInfo: FC = () => { () => accounts.filter((a) => lastScanAccounts.includes(a.address)), [accounts, lastScanAccounts], ) + const lastNetworks = useMemo( + () => + lastScanNetworks.map((id) => evmNetworksMap[id] ?? chainsMap[id]).filter(Boolean) as ( + | EvmNetwork + | Chain + )[], + [chainsMap, evmNetworksMap, lastScanNetworks], + ) if (isInitializing) return null @@ -510,7 +594,7 @@ const ScanInfo: FC = () => { {!isInProgress && !!lastScanTimestamp && !!lastScanAccounts.length && ( { accounts={lastAccounts} /> ), + NetworksWrapper: ( + + ), DateWrapper: , }} - values={{ count: lastScanAccounts.length, timestamp: formatedTimestamp }} + values={{ + count: lastScanAccounts.length, + timestamp: formatedTimestamp, + networksCount: lastScanNetworks.length, + }} > )}
@@ -580,13 +674,6 @@ const Content = () => { useBalancesHydrate() // preload useAnalyticsPageView(ANALYTICS_PAGE) - const [showAssetDiscoveryAlert, setShowAssetDiscoveryAlert] = - useAppState("showAssetDiscoveryAlert") - - // hide alert when user browses this page - useEffect(() => { - if (showAssetDiscoveryAlert) setShowAssetDiscoveryAlert(false) - }, [setShowAssetDiscoveryAlert, showAssetDiscoveryAlert]) return ( <> diff --git a/apps/extension/src/ui/apps/popup/components/Navigation/BottomNav.tsx b/apps/extension/src/ui/apps/popup/components/Navigation/BottomNav.tsx index 771adeaa6c..1eabad5220 100644 --- a/apps/extension/src/ui/apps/popup/components/Navigation/BottomNav.tsx +++ b/apps/extension/src/ui/apps/popup/components/Navigation/BottomNav.tsx @@ -7,15 +7,13 @@ import { ZapIcon, } from "@talismn/icons" import { classNames } from "@talismn/util" -import { FC, ReactNode, Suspense, useCallback } from "react" +import { FC, ReactNode, useCallback } from "react" import { useTranslation } from "react-i18next" import { useLocation, useMatch, useNavigate } from "react-router-dom" import { TALISMAN_WEB_APP_STAKING_URL } from "@extension/shared" -import { SuspenseTracker } from "@talisman/components/SuspenseTracker" import { api } from "@ui/api" import { AnalyticsPage, sendAnalyticsEvent } from "@ui/api/analytics" -import { AssetDiscoveryPopupAlert } from "@ui/domains/AssetDiscovery/AssetDiscoveryPopupAlert" import { useMnemonicBackup } from "@ui/hooks/useMnemonicBackup" import { usePopupNavOpenClose } from "@ui/hooks/usePopupNavOpenClose" @@ -135,9 +133,6 @@ export const BottomNav = () => { /> )}
- }> - - ) diff --git a/apps/extension/src/ui/apps/popup/components/StakingBanner.tsx b/apps/extension/src/ui/apps/popup/components/StakingBanner.tsx deleted file mode 100644 index 44b449d001..0000000000 --- a/apps/extension/src/ui/apps/popup/components/StakingBanner.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { ExternalLinkIcon, XIcon, ZapIcon } from "@talismn/icons" -import { MouseEventHandler, Suspense, useCallback, useMemo } from "react" -import { Trans, useTranslation } from "react-i18next" - -import { TALISMAN_WEB_APP_STAKING_URL } from "@extension/shared" -import { SuspenseTracker } from "@talisman/components/SuspenseTracker" -import { useStakingBanner } from "@ui/domains/Staking/useStakingBanner" -import { useAnalytics } from "@ui/hooks/useAnalytics" - -export const StakingBannerInner = ({ addresses }: { addresses: string[] }) => { - const { showStakingBanner, dismissStakingBanner } = useStakingBanner() - const { genericEvent } = useAnalytics() - const { t } = useTranslation() - - const showNomPoolStakingBanner = useMemo( - () => showStakingBanner({ addresses }), - [addresses, showStakingBanner], - ) - - const handleClickStakingBanner = useCallback(() => { - window.open(TALISMAN_WEB_APP_STAKING_URL) - genericEvent("open web app staking from banner", { from: "popup" }) - }, [genericEvent]) - - const handleDismissStakingBanner: MouseEventHandler = useCallback( - (e) => { - e.preventDefault() - e.stopPropagation() - dismissStakingBanner() - genericEvent("dismiss staking banner", { from: "popup" }) - }, - [genericEvent, dismissStakingBanner], - ) - - return ( - <> - {showNomPoolStakingBanner && ( - - )} - - ) -} - -export const StakingBanner = ({ addresses }: { addresses: string[] }) => ( - }> - - -) diff --git a/apps/extension/src/ui/apps/popup/index.tsx b/apps/extension/src/ui/apps/popup/index.tsx index ff2f96ed90..877f922895 100644 --- a/apps/extension/src/ui/apps/popup/index.tsx +++ b/apps/extension/src/ui/apps/popup/index.tsx @@ -19,9 +19,9 @@ import { AccountRemoveModal } from "@ui/domains/Account/AccountRemoveModal" import { AccountRenameModal } from "@ui/domains/Account/AccountRenameModal" import { CopyAddressModal } from "@ui/domains/CopyAddress" import { DatabaseErrorAlert } from "@ui/domains/Settings/DatabaseErrorAlert" -import { NomPoolBondModal } from "@ui/domains/Staking/NomPoolBond/NomPoolBondModal" -import { NomPoolUnbondModal } from "@ui/domains/Staking/NomPoolUnbond/NomPoolUnbondModal" +import { BondModal } from "@ui/domains/Staking/Bond/BondModal" import { NomPoolWithdrawModal } from "@ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawModal" +import { UnbondModal } from "@ui/domains/Staking/Unbond/UnbondModal" import { ExplorerNetworkPickerModal } from "@ui/domains/ViewOnExplorer" import { useLoginCheck } from "@ui/hooks/useLoginCheck" @@ -91,8 +91,8 @@ const Popup = () => { - - + + {/* Render outside of suspense or it will never show in case of migration error */} diff --git a/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAccounts.tsx b/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAccounts.tsx index 24dd9adf05..7252975df8 100644 --- a/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAccounts.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAccounts.tsx @@ -28,7 +28,6 @@ import { api } from "@ui/api" import { AnalyticsPage, sendAnalyticsEvent } from "@ui/api/analytics" import { AllAccountsHeader } from "@ui/apps/popup/components/AllAccountsHeader" import { NewFeaturesButton } from "@ui/apps/popup/components/NewFeaturesButton" -import { StakingBanner } from "@ui/apps/popup/components/StakingBanner" import { AccountFolderIcon } from "@ui/domains/Account/AccountFolderIcon" import { AccountIconCopyAddressButton } from "@ui/domains/Account/AccountIconCopyAddressButton" import { AccountsLogoStack } from "@ui/domains/Account/AccountsLogoStack" @@ -183,9 +182,6 @@ const AccountButton: FC<{ option: AccountAccountOption }> = ({ option }) => { ) } -const accountTypeGuard = (option: AccountOption): option is AccountAccountOption => - option.type === "account" - const AccountsToolbar = () => { const { t } = useTranslation() const navigate = useNavigate() @@ -292,11 +288,6 @@ const Accounts = ({ const hasPortfolioOptions = portfolioOptions.length > 0 const hasWatchedOptions = watchedOptions.length > 0 - const addresses = useMemo( - () => portfolioOptions.filter(accountTypeGuard).map(({ address }) => address), - [portfolioOptions], - ) - return (
{folder ? ( @@ -305,7 +296,6 @@ const Accounts = ({ <> - )} diff --git a/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/index.ts b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/index.ts index ad35bd888d..165e94ee40 100644 --- a/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/index.ts +++ b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/index.ts @@ -11,6 +11,7 @@ import content1300 from "./v1.30.0" import content1310 from "./v1.31.0" import content2000 from "./v2.0.0" import content2100 from "./v2.1.0" +import content2200 from "./v2.2.0" export const latestUpdates: WhatsNewVersionData = { ...content1210, @@ -25,4 +26,5 @@ export const latestUpdates: WhatsNewVersionData = { ...content1310, ...content2000, ...content2100, + ...content2200, } diff --git a/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/content.md b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/content.md new file mode 100644 index 0000000000..6fe22e26f2 --- /dev/null +++ b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/content.md @@ -0,0 +1,5 @@ + + +**𝛕 Bittensor TAO Staking:** Talisman now supports Bittensor TAO staking directly in the Wallet. + +**🔎 Asset Discovery Improvements:** Asset Discovery now automatically adds discovered assets, and multiple asset scans can be queued. diff --git a/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/index.ts b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/index.ts new file mode 100644 index 0000000000..ce158cbfc0 --- /dev/null +++ b/apps/extension/src/ui/apps/popup/pages/WhatsNew/assets/v2.2.0/index.ts @@ -0,0 +1,11 @@ +import { WhatsNewVersionData } from "../types" +import content from "./content.md" + +const content220: WhatsNewVersionData = { + "2.2.0": { + content, + date: "December 2024", + }, +} + +export default content220 diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx index dedd15dbcb..668a55605b 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx @@ -20,7 +20,6 @@ import * as yup from "yup" import { AccountAddressType, - AssetDiscoveryMode, RequestAccountCreateOptions, UiAccountAddressType, } from "@extension/core" @@ -200,8 +199,6 @@ const AccountAddDerivedFormInner: FC = ({ onSuccess }) => { onSuccess(address) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, [address]) - notifyUpdate(notificationId, { type: "success", title: t("Account created"), diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddJson/context.ts b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddJson/context.ts index 01db7b9cd6..47c79f8979 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddJson/context.ts +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddJson/context.ts @@ -8,7 +8,7 @@ import { Address, Balances } from "@talismn/balances" import { encodeAnyAddress } from "@talismn/util" import { useCallback, useEffect, useMemo, useState } from "react" -import { AccountType, AssetDiscoveryMode } from "@extension/core" +import { AccountType } from "@extension/core" import { log } from "@extension/shared" import { provideContext } from "@talisman/util/provideContext" import { api } from "@ui/api" @@ -286,8 +286,6 @@ const useJsonAccountImportProvider = () => { const addresses = await api.accountCreateFromJson(unlockedPairs) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, addresses) - return addresses }, [pairs, selectedAccounts]) diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddLedger/context.ts b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddLedger/context.ts index 382daceb51..4b36fd99d7 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddLedger/context.ts +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddLedger/context.ts @@ -4,7 +4,6 @@ import { useSearchParams } from "react-router-dom" import { AccountAddressType, - AssetDiscoveryMode, RequestAccountCreateLedgerEthereum, RequestAccountCreateLedgerSubstrate, RequestAccountCreateLedgerSubstrateGeneric, @@ -90,8 +89,6 @@ const useAddLedgerAccountProvider = ({ onSuccess }: { onSuccess: (address: strin ), ) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, addresses) - return addresses }, [chain?.genesisHash, data.substrateAppType, data.type], diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/MnemonicForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/MnemonicForm.tsx index 732dc776cb..f3acda8e53 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/MnemonicForm.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/MnemonicForm.tsx @@ -16,12 +16,7 @@ import { } from "talisman-ui" import * as yup from "yup" -import { - AccountAddressType, - AssetDiscoveryMode, - getEthDerivationPath, - UiAccountAddressType, -} from "@extension/core" +import { AccountAddressType, getEthDerivationPath, UiAccountAddressType } from "@extension/core" import { HeaderBlock } from "@talisman/components/HeaderBlock" import { notify, notifyUpdate } from "@talisman/components/Notifications" import { Spacer } from "@talisman/components/Spacer" @@ -173,8 +168,6 @@ export const AccountAddMnemonicForm = () => { try { const address = await api.accountCreateFromSuri(name, suri, type) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, [address]) - onSuccess(address) notifyUpdate(notificationId, { type: "success", @@ -190,7 +183,7 @@ export const AccountAddMnemonicForm = () => { } } }, - [t, navigate, onSuccess, updateData], + [updateData, navigate, t, onSuccess], ) const handleTypeChange = useCallback( diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/context.ts b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/context.ts index 3157c48bb8..24b5923267 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/context.ts +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddMnemonic/context.ts @@ -2,7 +2,6 @@ import { useCallback, useState } from "react" import { useSearchParams } from "react-router-dom" import { - AssetDiscoveryMode, getEthDerivationPath, RequestAccountCreateFromSuri, UiAccountAddressType, @@ -23,6 +22,7 @@ type AccountAddSecretInputs = { const useAccountAddMnemonicProvider = ({ onSuccess }: { onSuccess: (address: string) => void }) => { const [params] = useSearchParams() + const [data, setData] = useState>(() => ({ type: params.get("type") as UiAccountAddressType, mode: "first", @@ -44,8 +44,6 @@ const useAccountAddMnemonicProvider = ({ onSuccess }: { onSuccess: (address: str for (const { name, suri, type } of accounts) addresses.push(await api.accountCreateFromSuri(name, suri, type)) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, addresses) - return addresses }, []) diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddPrivateKeyForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddPrivateKeyForm.tsx index 70a6e6e031..b5ae6ba1f0 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddPrivateKeyForm.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddPrivateKeyForm.tsx @@ -18,7 +18,6 @@ import { isHex, toHex } from "viem" import { publicKeyToAddress } from "viem/accounts" import * as yup from "yup" -import { AssetDiscoveryMode } from "@extension/core" import { HeaderBlock } from "@talisman/components/HeaderBlock" import { notify, notifyUpdate } from "@talisman/components/Notifications" import { Spacer } from "@talisman/components/Spacer" @@ -102,7 +101,6 @@ const schema = yup export const AccountAddPrivateKeyForm = ({ onSuccess }: AccountAddPageProps) => { const { t } = useTranslation("admin") - const allAccounts = useAccounts() const accountEthAddresses = useMemo( () => allAccounts.filter(({ type }) => type === "ethereum").map((a) => a.address), @@ -151,8 +149,6 @@ export const AccountAddPrivateKeyForm = ({ onSuccess }: AccountAddPageProps) => try { const address = await api.accountCreateFromSuri(name, privateKey, "ethereum") - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, [address]) - onSuccess(address) notifyUpdate(notificationId, { type: "success", diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddQr/context.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddQr/context.tsx index 870d8e949c..81c17065dd 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddQr/context.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddQr/context.tsx @@ -3,7 +3,7 @@ import { decodeAnyAddress, sleep } from "@talismn/util" import { useCallback, useReducer } from "react" import { useTranslation } from "react-i18next" -import { AssetDiscoveryMode, VerifierCertificateType } from "@extension/core" +import { VerifierCertificateType } from "@extension/core" import { notify, notifyUpdate } from "@talisman/components/Notifications" import { provideContext } from "@talisman/util/provideContext" import { api } from "@ui/api" @@ -204,8 +204,6 @@ const useAccountAddQrContext = ({ onSuccess }: AccountAddPageProps) => { lockToNetwork ? genesisHash : null, ) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, [createdAddress]) - onSuccess(createdAddress) notifyUpdate(notificationId, { type: "success", diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddWatchedForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddWatchedForm.tsx index e86e4dd7e3..868b770896 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddWatchedForm.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddWatchedForm.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next" import { Button, FormFieldContainer, FormFieldInputText, Toggle } from "talisman-ui" import * as yup from "yup" -import { AssetDiscoveryMode, UiAccountAddressType } from "@extension/core" +import { UiAccountAddressType } from "@extension/core" import { notify, notifyUpdate } from "@talisman/components/Notifications" import { api } from "@ui/api" import { AccountAddPageProps } from "@ui/domains/Account/AccountAdd/types" @@ -104,8 +104,6 @@ export const AccountAddWatchedForm = ({ onSuccess }: AccountAddPageProps) => { try { onSuccess(await api.accountCreateWatched(name, address, isPortfolio)) - api.assetDiscoveryStartScan(AssetDiscoveryMode.ACTIVE_NETWORKS, [address]) - notifyUpdate(notificationId, { type: "success", title: t("Account added"), diff --git a/apps/extension/src/ui/domains/Account/AccountContextMenu.tsx b/apps/extension/src/ui/domains/Account/AccountContextMenu.tsx index 072734a5b0..f5398d0e0c 100644 --- a/apps/extension/src/ui/domains/Account/AccountContextMenu.tsx +++ b/apps/extension/src/ui/domains/Account/AccountContextMenu.tsx @@ -11,7 +11,7 @@ import { PopoverOptions, } from "talisman-ui" -import { AccountJsonAny, AssetDiscoveryMode } from "@extension/core" +import { AccountJsonAny } from "@extension/core" import { SuspenseTracker } from "@talisman/components/SuspenseTracker" import { api } from "@ui/api" import { useAccountExportModal } from "@ui/domains/Account/AccountExportModal" @@ -21,6 +21,7 @@ import { useAccountRenameModal } from "@ui/domains/Account/AccountRenameModal" import { useCopyAddressModal } from "@ui/domains/CopyAddress" import { useViewOnExplorer } from "@ui/domains/ViewOnExplorer" import { useAccountToggleIsPortfolio } from "@ui/hooks/useAccountToggleIsPortfolio" +import { useActiveAssetDiscoveryNetworkIds } from "@ui/hooks/useAllActiveNetworkIds" import { useAnalytics } from "@ui/hooks/useAnalytics" import { useAccountByAddress, useChainByGenesisHash } from "@ui/state" import { IS_EMBEDDED_POPUP, IS_POPUP } from "@ui/util/constants" @@ -126,15 +127,16 @@ export const AccountContextMenu = forwardRef(function Accoun [_openAccountRemoveModal, account], ) + const networkIds = useActiveAssetDiscoveryNetworkIds() const canScanTokens = useMemo(() => isEthereumAddress(account?.address), [account]) const scanTokensClick = useCallback(() => { if (!account) return - api.assetDiscoveryStartScan(AssetDiscoveryMode.ALL_NETWORKS, [account.address]) + api.assetDiscoveryStartScan({ networkIds, addresses: [account.address] }) if (IS_POPUP) { api.dashboardOpen("/settings/networks-tokens/asset-discovery") if (IS_EMBEDDED_POPUP) window.close() } - }, [account]) + }, [account, networkIds]) const goToManageAccounts = useCallback(() => navigate("/settings/accounts"), [navigate]) diff --git a/apps/extension/src/ui/domains/Asset/Tokens.tsx b/apps/extension/src/ui/domains/Asset/Tokens.tsx index 205f55b512..524bccb4fe 100644 --- a/apps/extension/src/ui/domains/Asset/Tokens.tsx +++ b/apps/extension/src/ui/domains/Asset/Tokens.tsx @@ -67,15 +67,14 @@ export const Tokens: FC = ({ const { refReveal, isRevealable, isRevealed, isHidden, effectiveNoCountUp } = useRevealableBalance(isBalance, noCountUp) - const tooltip = useMemo( + const tooltipAmount = useMemo( () => - noTooltip - ? null - : `${formatDecimals(amount, decimals ?? MAX_DECIMALS_FORMAT, { notation: "standard" })} ${ - symbol ?? "" - }`.trim(), - [amount, decimals, noTooltip, symbol], + `${formatDecimals(amount, decimals ?? MAX_DECIMALS_FORMAT, { notation: "standard" })} ${ + symbol ?? "" + }`.trim(), + [amount, decimals, symbol], ) + const tooltip = useMemo(() => (noTooltip ? null : tooltipAmount), [noTooltip, tooltipAmount]) const render = amount !== null && amount !== undefined @@ -92,7 +91,7 @@ export const Tokens: FC = ({ {render && ( - + { - const { t } = useTranslation() - - const { isInProgress, percent, hasDetectedNewTokens } = useAssetDiscoveryAlert() - - return ( -
- -
-
- {hasDetectedNewTokens ? t("New tokens detected") : t("Scanning for tokens")} -
-
- {isInProgress - ? t("Scan in progress: {{percent}}%", { percent }) - : t("Click here to review")} -
-
-
- ) -} - -const CloseButton = () => { - const { dismissAlert } = useAssetDiscoveryAlert() - - const handleClick: MouseEventHandler = useCallback( - (e) => { - e.stopPropagation() - dismissAlert() - }, - [dismissAlert], - ) - - return ( - - - - ) -} - -export const AssetDiscoveryDashboardAlert = () => { - const { showAlert } = useAssetDiscoveryAlert() - const { t } = useTranslation() - const location = useLocation() - const navigate = useNavigate() - - useEffect(() => { - if (!showAlert || location.pathname === "/settings/networks-tokens/asset-discovery") { - toast.dismiss(ASSET_DISCOVERY_TOAST_ID) - return - } - - if (showAlert && !toast.isActive(ASSET_DISCOVERY_TOAST_ID)) { - toast( - () => ( - - - - ), - { - autoClose: 60_000, - hideProgressBar: true, - toastId: ASSET_DISCOVERY_TOAST_ID, - onClick: () => { - navigate("/settings/networks-tokens/asset-discovery") - }, - closeButton: () => ( - - - - ), - }, - ) - } - }, [location, navigate, showAlert, t]) - - return null -} diff --git a/apps/extension/src/ui/domains/AssetDiscovery/AssetDiscoveryPopupAlert.tsx b/apps/extension/src/ui/domains/AssetDiscovery/AssetDiscoveryPopupAlert.tsx deleted file mode 100644 index ea4113cb85..0000000000 --- a/apps/extension/src/ui/domains/AssetDiscovery/AssetDiscoveryPopupAlert.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { DiamondIcon, XIcon } from "@talismn/icons" -import { useCallback } from "react" -import { useTranslation } from "react-i18next" -import { IconButton } from "talisman-ui" - -import { api } from "@ui/api" - -import { useAssetDiscoveryAlert } from "./useAssetDiscoveryAlert" - -export const AssetDiscoveryPopupAlert = () => { - const { showAlert, dismissAlert, isInProgress, percent, hasDetectedNewTokens } = - useAssetDiscoveryAlert() - const { t } = useTranslation() - - const handleGoToClick = useCallback(async () => { - await api.dashboardOpen("/settings/networks-tokens/asset-discovery") - window.close() - }, []) - - if (!showAlert) return null - - return ( - <> -
{/* push bottom nav up */}
-
- -
- - {hasDetectedNewTokens ? t("New tokens detected.") : t("Scanning for tokens.")} - - -
- - - -
- - ) -} diff --git a/apps/extension/src/ui/domains/AssetDiscovery/useAssetDiscoveryAlert.ts b/apps/extension/src/ui/domains/AssetDiscovery/useAssetDiscoveryAlert.ts deleted file mode 100644 index bf10163624..0000000000 --- a/apps/extension/src/ui/domains/AssetDiscovery/useAssetDiscoveryAlert.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback } from "react" - -import { - useAppState, - useAssetDiscoveryScan, - useAssetDiscoveryScanProgress, - useIsLoggedIn, -} from "@ui/state" - -export const useAssetDiscoveryAlert = () => { - const isLoggedIn = useIsLoggedIn() - const [showAssetDiscoveryAlert, setShowAssetDiscoveryAlert] = - useAppState("showAssetDiscoveryAlert") - const [, setDismissedAssetDiscoveryAlertScanId] = useAppState( - "dismissedAssetDiscoveryAlertScanId", - ) - const { currentScanId } = useAssetDiscoveryScan() - const { isInProgress, percent, balances } = useAssetDiscoveryScanProgress() - - const dismissAlert = useCallback(() => { - setShowAssetDiscoveryAlert(false) - setDismissedAssetDiscoveryAlertScanId(currentScanId ?? "") - }, [currentScanId, setDismissedAssetDiscoveryAlertScanId, setShowAssetDiscoveryAlert]) - - return { - showAlert: showAssetDiscoveryAlert && isLoggedIn, - dismissAlert, - isInProgress, - percent, - hasDetectedNewTokens: !!balances.length, - } -} diff --git a/apps/extension/src/ui/domains/Portfolio/AssetDetails/DashboardAssetDetails.tsx b/apps/extension/src/ui/domains/Portfolio/AssetDetails/DashboardAssetDetails.tsx index bfc74c5320..2863ac0afe 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetDetails/DashboardAssetDetails.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetDetails/DashboardAssetDetails.tsx @@ -13,10 +13,10 @@ import { TokenLogo } from "@ui/domains/Asset/TokenLogo" import Tokens from "@ui/domains/Asset/Tokens" import { AssetBalanceCellValue } from "@ui/domains/Portfolio/AssetBalanceCellValue" import { NoTokensMessage } from "@ui/domains/Portfolio/NoTokensMessage" -import { NomPoolBondButton } from "@ui/domains/Staking/NomPoolBond/NomPoolBondButton" -import { NomPoolUnbondButton } from "@ui/domains/Staking/NomPoolUnbond/NomPoolUnbondButton" +import { BondButton } from "@ui/domains/Staking/Bond/BondButton" +import { useNomPoolStakingStatus } from "@ui/domains/Staking/hooks/nomPools/useNomPoolStakingStatus" import { NomPoolWithdrawButton } from "@ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton" -import { useNomPoolStakingStatus } from "@ui/domains/Staking/shared/useNomPoolStakingStatus" +import { UnbondButton } from "@ui/domains/Staking/Unbond/UnbondButton" import { BalancesStatus } from "@ui/hooks/useBalancesStatus" import { useSelectedCurrency } from "@ui/state" @@ -35,11 +35,15 @@ const AssetState = ({ description, render, address, + isLoading, + locked, }: { title: string description?: string render: boolean address?: Address + isLoading?: boolean + locked?: boolean }) => { if (!render) return null return ( @@ -55,6 +59,9 @@ const AssetState = ({
)} {/* show description below title when address is not set */} + {isLoading && !description && locked && ( +
+ )} {description && !address && (
{description}
)} @@ -123,7 +130,7 @@ const ChainTokenBalances = ({ chainId, balances }: AssetRowProps) => { />
- {tokenId && } + {tokenId && }
- +
{!row.locked &&
}
@@ -263,7 +277,9 @@ const ChainTokenBalancesDetailRow = ({ symbol={symbol} locked={row.locked} balancesStatus={status} - className={classNames(status.status === "fetching" && "animate-pulse transition-opacity")} + className={classNames( + (status.status === "fetching" || row.isLoading) && "animate-pulse transition-opacity", + )} />
{!!row.locked && row.meta && tokenId && ( @@ -305,12 +321,12 @@ const LockedExtra: FC<{ [accountStatus?.canWithdrawIn, rowMeta.unbonding], ) - if (!rowAddress || !accountStatus) return null + if (!rowAddress) return null return (
{rowMeta.unbonding ? ( - accountStatus.canWithdraw ? ( + accountStatus?.canWithdraw ? ( ) : ( <> @@ -327,8 +343,13 @@ const LockedExtra: FC<{ )} ) - ) : accountStatus.canUnstake ? ( - + ) : accountStatus?.canUnstake || tokenId === "bittensor-substrate-native" ? ( + ) : null}
) diff --git a/apps/extension/src/ui/domains/Portfolio/AssetDetails/PopupAssetDetails.tsx b/apps/extension/src/ui/domains/Portfolio/AssetDetails/PopupAssetDetails.tsx index 241d25ec7d..39e34d4004 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetDetails/PopupAssetDetails.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetDetails/PopupAssetDetails.tsx @@ -15,16 +15,16 @@ import { Fiat } from "@ui/domains/Asset/Fiat" import { TokenLogo } from "@ui/domains/Asset/TokenLogo" import Tokens from "@ui/domains/Asset/Tokens" import { useCopyAddressModal } from "@ui/domains/CopyAddress" -import { NomPoolBondButton } from "@ui/domains/Staking/NomPoolBond/NomPoolBondButton" -import { NomPoolUnbondButton } from "@ui/domains/Staking/NomPoolUnbond/NomPoolUnbondButton" +import { usePortfolioNavigation } from "@ui/domains/Portfolio/usePortfolioNavigation" +import { BondButton } from "@ui/domains/Staking/Bond/BondButton" +import { useNomPoolStakingStatus } from "@ui/domains/Staking/hooks/nomPools/useNomPoolStakingStatus" import { NomPoolWithdrawButton } from "@ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton" -import { useNomPoolStakingStatus } from "@ui/domains/Staking/shared/useNomPoolStakingStatus" +import { UnbondButton } from "@ui/domains/Staking/Unbond/UnbondButton" import { useAnalytics } from "@ui/hooks/useAnalytics" import { BalancesStatus } from "@ui/hooks/useBalancesStatus" import { useFeatureFlag, useSelectedCurrency } from "@ui/state" import { StaleBalancesIcon } from "../StaleBalancesIcon" -import { usePortfolioNavigation } from "../usePortfolioNavigation" import { CopyAddressButton } from "./CopyAddressIconButton" import { PortfolioAccount } from "./PortfolioAccount" import { SendFundsButton } from "./SendFundsIconButton" @@ -76,7 +76,7 @@ const ChainTokenBalances = ({ chainId, balances }: AssetRowProps) => { {tokenId && (
}> - +
)} @@ -211,7 +211,7 @@ const ChainTokenBalancesDetailRow = ({ tokenId={tokenId} address={row.address} rowMeta={row.meta} - isLoading={status.status === "fetching"} + isLoading={status.status === "fetching" || !!row.isLoading} /> )}
@@ -220,6 +220,9 @@ const ChainTokenBalancesDetailRow = ({ )} + {row.isLoading && !row.description && row.locked && ( +
+ )} {!row.address && row.description && (
{row.description} @@ -279,12 +282,12 @@ const LockedExtra: FC<{ [accountStatus?.canWithdrawIn, rowMeta.unbonding], ) - if (!rowAddress || !accountStatus) return null + if (!rowAddress) return null return ( <> {rowMeta.unbonding ? ( - accountStatus.canWithdraw ? ( + accountStatus?.canWithdraw ? ( ) : ( @@ -305,8 +308,13 @@ const LockedExtra: FC<{ ) ) : //eslint-disable-next-line @typescript-eslint/no-explicit-any - accountStatus.canUnstake ? ( - + accountStatus?.canUnstake || tokenId === "bittensor-substrate-native" ? ( + ) : null} ) diff --git a/apps/extension/src/ui/domains/Portfolio/AssetDetails/TokenContextMenu.tsx b/apps/extension/src/ui/domains/Portfolio/AssetDetails/TokenContextMenu.tsx index 98042c2b81..dacf15206c 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetDetails/TokenContextMenu.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetDetails/TokenContextMenu.tsx @@ -14,8 +14,8 @@ import { import urlJoin from "url-join" import { SuspenseTracker } from "@talisman/components/SuspenseTracker" -import { useNomPoolBondModal } from "@ui/domains/Staking/NomPoolBond/useNomPoolBondModal" -import { useNomPoolStakingStatus } from "@ui/domains/Staking/shared/useNomPoolStakingStatus" +import { useBondModal } from "@ui/domains/Staking/Bond/useBondModal" +import { useNomPoolStakingStatus } from "@ui/domains/Staking/hooks/nomPools/useNomPoolStakingStatus" import { useViewOnExplorer } from "@ui/domains/ViewOnExplorer" import { useAnalytics } from "@ui/hooks/useAnalytics" import { useToken } from "@ui/state" @@ -54,7 +54,7 @@ const StakeMenuItem: FC<{ tokenId: string }> = ({ tokenId }) => { const { t } = useTranslation() const { genericEvent } = useAnalytics() - const { open } = useNomPoolBondModal() + const { open } = useBondModal() const { data: stakingStatus } = useNomPoolStakingStatus(tokenId) const openArgs = useMemo[0] | undefined>(() => { diff --git a/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts b/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts index 64734f3466..92b68ce9e0 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts +++ b/apps/extension/src/ui/domains/Portfolio/AssetDetails/useChainTokenBalances.ts @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next" import { Address, Balances } from "@extension/core" import { sortBigBy } from "@talisman/util/bigHelper" import { cleanupNomPoolName } from "@ui/domains/Staking/helpers" +import { useCombineBittensorStakeInfo } from "@ui/domains/Staking/hooks/bittensor/useCombineBittensorStakeInfo" import { useBalancesStatus } from "@ui/hooks/useBalancesStatus" import { useNetworkCategory } from "@ui/hooks/useNetworkCategory" import { useChain, useSelectedCurrency } from "@ui/state" @@ -23,6 +24,7 @@ export type DetailRow = { locked: boolean address?: Address meta?: any // eslint-disable-line @typescript-eslint/no-explicit-any + isLoading?: boolean } type ChainTokenBalancesParams = { @@ -39,6 +41,11 @@ export const useChainTokenBalances = ({ chainId, balances }: ChainTokenBalancesP const currency = useSelectedCurrency() + const { combinedStakeInfo: subtensor } = useCombineBittensorStakeInfo({ + address: account?.address, + balances: balances, + }) + const detailRows = useMemo((): DetailRow[] => { if (!summary) return [] @@ -123,26 +130,10 @@ export const useChainTokenBalances = ({ chainId, balances }: ChainTokenBalancesP })), ) - // STAKED (SUBTENSOR) - const subtensor = tokenBalances.each.flatMap((b) => - b.subtensor.map((subtensor, index) => ({ - key: `${b.id}-subtensor-${index}`, - title: getLockTitle(subtensor, { balance: b }), - - description: undefined, - tokens: BigNumber(subtensor.amount.tokens), - fiat: subtensor.amount.fiat(currency), - locked: true, - // only show address when we're viewing balances for all accounts - address: account ? undefined : b.address, - meta: subtensor.meta, - })), - ) - return [...available, ...locked, ...reserved, ...staked, ...crowdloans, ...subtensor] .filter((row) => row && row.tokens.gt(0)) .sort(sortBigBy("tokens", true)) - }, [summary, account, t, tokenBalances, currency]) + }, [summary, account, t, tokenBalances.each, subtensor, currency]) const { evmNetwork } = balances.sorted[0] const relay = useChain(chain?.relay?.id) diff --git a/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetRow.tsx b/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetRow.tsx index f487953b5e..2deaa43bd5 100644 --- a/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetRow.tsx +++ b/apps/extension/src/ui/domains/Portfolio/AssetsTable/DashboardAssetRow.tsx @@ -1,14 +1,13 @@ -import { ExternalLinkIcon, XIcon, ZapFastIcon, ZapIcon } from "@talismn/icons" +import { ZapFastIcon } from "@talismn/icons" import { classNames } from "@talismn/util" import { useCallback } from "react" -import { Trans, useTranslation } from "react-i18next" +import { useTranslation } from "react-i18next" import { Balances } from "@extension/core" import { AssetPrice } from "@ui/domains/Asset/AssetPrice" import { Fiat } from "@ui/domains/Asset/Fiat" -import { NomPoolBondPillButton } from "@ui/domains/Staking/NomPoolBond/NomPoolBondPillButton" -import { useNomPoolBondButton } from "@ui/domains/Staking/NomPoolBond/useNomPoolBondButton" -import { useShowStakingBanner } from "@ui/domains/Staking/useShowStakingBanner" +import { BondPillButton } from "@ui/domains/Staking/Bond/BondPillButton" +import { useBondButton } from "@ui/domains/Staking/Bond/useBondButton" import { useAnalytics } from "@ui/hooks/useAnalytics" import { useBalancesStatus } from "@ui/hooks/useBalancesStatus" import { useNavigateWithQuery } from "@ui/hooks/useNavigateWithQuery" @@ -20,47 +19,6 @@ import { useTokenBalancesSummary } from "../useTokenBalancesSummary" import { NetworksLogoStack } from "./NetworksLogoStack" import { usePortfolioNetworkIds } from "./usePortfolioNetworkIds" -const AssetRowStakingReminder = (props: ReturnType) => { - const { t } = useTranslation() - - const { message, colours, handleClickStakingBanner, handleDismissStakingBanner } = props - const { token, summary } = useTokenBalancesSummary(props.balances) - - if (!token || !summary) return null - - return ( -
- - -
- ) -} - type AssetRowProps = { balances: Balances } @@ -83,21 +41,16 @@ export const AssetRow = ({ balances }: AssetRowProps) => { const isUniswapV2LpToken = token?.type === "evm-uniswapv2" const tvl = useUniswapV2LpTokenTotalValueLocked(token, rate?.price, balances) - const { canBondNomPool } = useNomPoolBondButton({ tokenId: token?.id, balances }) - - const stakingReminder = useShowStakingBanner(balances) + const { canBondNomPool } = useBondButton({ tokenId: token?.id, balances }) if (!token || !summary) return null return (
- {stakingReminder.showBanner && } - + ) +} diff --git a/apps/extension/src/ui/domains/Staking/Bittensor/BittensorUnbondingPeriod.tsx b/apps/extension/src/ui/domains/Staking/Bittensor/BittensorUnbondingPeriod.tsx new file mode 100644 index 0000000000..c1d63e9661 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/Bittensor/BittensorUnbondingPeriod.tsx @@ -0,0 +1,7 @@ +import { useTranslation } from "react-i18next" + +export const BittensorUnbondingPeriod = () => { + const { t } = useTranslation() + + return
{t("None")}
+} diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountPicker.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondAccountPicker.tsx similarity index 87% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountPicker.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondAccountPicker.tsx index 784d899b92..1bfafb03fd 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountPicker.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondAccountPicker.tsx @@ -9,14 +9,14 @@ import { SearchInput } from "@talisman/components/SearchInput" import { useAccounts, useChain } from "@ui/state" import { isEvmToken } from "@ui/util/isEvmToken" -import { NomPoolBondAccountsList } from "./NomPoolBondAccountsList" -import { useNomPoolBondModal } from "./useNomPoolBondModal" -import { useNomPoolBondWizard } from "./useNomPoolBondWizard" +import { BondAccountsList } from "./BondAccountsList" +import { useBondModal } from "./useBondModal" +import { useBondWizard } from "./useBondWizard" -export const NomPoolBondAccountPicker = () => { +export const BondAccountPicker = () => { const { t } = useTranslation() - const { close } = useNomPoolBondModal() - const { account, token, setAddress, accountPicker } = useNomPoolBondWizard() + const { close } = useBondModal() + const { account, token, setAddress, accountPicker } = useBondWizard() const [search, setSearch] = useState("") const chain = useChain(token?.chain?.id) @@ -71,7 +71,7 @@ export const NomPoolBondAccountPicker = () => {
- void } -export const NomPoolBondAccountPillButton: FC = ({ +export const BondAccountPillButton: FC = ({ address, genesisHash: tokenGenesisHash, className, diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountsList.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondAccountsList.tsx similarity index 98% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountsList.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondAccountsList.tsx index 1b0259b09a..6e663f50ae 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondAccountsList.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondAccountsList.tsx @@ -131,7 +131,7 @@ type NomPoolBondAccountsListProps = { tokenId?: string } -export const NomPoolBondAccountsList: FC = ({ +export const BondAccountsList: FC = ({ selected, accounts, noFormat, diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondButton.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondButton.tsx similarity index 75% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondButton.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondButton.tsx index 1a3e842342..c2d28d224f 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondButton.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondButton.tsx @@ -5,14 +5,11 @@ import { FC } from "react" import { useTranslation } from "react-i18next" import { Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" -import { useNomPoolBondButton } from "./useNomPoolBondButton" +import { useBondButton } from "./useBondButton" -export const NomPoolBondButton: FC<{ tokenId: TokenId; balances: Balances }> = ({ - tokenId, - balances, -}) => { +export const BondButton: FC<{ tokenId: TokenId; balances: Balances }> = ({ tokenId, balances }) => { const { t } = useTranslation() - const { onClick, isNomPoolStaking } = useNomPoolBondButton({ tokenId, balances }) + const { onClick, isNomPoolStaking } = useBondButton({ tokenId, balances }) if (!onClick) return null diff --git a/apps/extension/src/ui/domains/Staking/Bond/BondFollowUp.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondFollowUp.tsx new file mode 100644 index 0000000000..e96f9499cc --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/Bond/BondFollowUp.tsx @@ -0,0 +1,12 @@ +import { TxProgress } from "../../Transactions" +import { useBondModal } from "./useBondModal" +import { useBondWizard } from "./useBondWizard" + +export const BondFollowUp = () => { + const { close } = useBondModal() + const { hash, token } = useBondWizard() + + if (!hash || !token?.chain?.id) return null + + return +} diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondForm.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondForm.tsx similarity index 77% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondForm.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondForm.tsx index 56403c2499..03154acea3 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondForm.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondForm.tsx @@ -24,26 +24,36 @@ import { Fiat } from "../../Asset/Fiat" import { TokenLogo } from "../../Asset/TokenLogo" import Tokens from "../../Asset/Tokens" import { TokensAndFiat } from "../../Asset/TokensAndFiat" -import { NomPoolName } from "../shared/NomPoolName" +import { useGetBittensorValidator } from "../hooks/bittensor/useGetBittensorValidator" +import { useStakingAPR } from "../hooks/nomPools/useStakingAPR" +import { BondPoolName } from "../shared/BondPoolName" import { StakingFeeEstimate } from "../shared/StakingFeeEstimate" import { StakingUnbondingPeriod } from "../shared/StakingUnbondingPeriod" -import { useStakingAPR } from "../shared/useStakingAPR" -import { NomPoolBondAccountPicker } from "./NomPoolBondAccountPicker" -import { NomPoolBondAccountPillButton } from "./NomPoolBondAccountPillButton" -import { useNomPoolBondWizard } from "./useNomPoolBondWizard" +import { BondAccountPicker } from "./BondAccountPicker" +import { BondAccountPillButton } from "./BondAccountPillButton" +import { useBondWizard } from "./useBondWizard" const AssetPill: FC<{ token: Token | null }> = ({ token }) => { const { t } = useTranslation() if (!token) return null + const stakeAssetLabel = () => { + switch (token.chain?.id) { + case "bittensor": + return t("Delegated Staking") + default: + return t("Pooled Staking") + } + } + return (
{token.symbol}
-
{t("Pooled Staking")}
+
{stakeAssetLabel()}
) @@ -72,7 +82,7 @@ const DisplayContainer: FC = ({ children }) => { const FiatDisplay = () => { const currency = useSelectedCurrency() - const { tokenRates, formatter } = useNomPoolBondWizard() + const { tokenRates, formatter } = useBondWizard() if (!tokenRates) return null @@ -84,7 +94,7 @@ const FiatDisplay = () => { } const TokenDisplay = () => { - const { token, formatter } = useNomPoolBondWizard() + const { token, formatter } = useBondWizard() if (!token) return null @@ -101,7 +111,7 @@ const TokenDisplay = () => { } const TokenInput = () => { - const { token, formatter, setPlancks } = useNomPoolBondWizard() + const { token, formatter, setPlancks } = useBondWizard() const defaultValue = useMemo(() => formatter?.tokens ?? "", [formatter?.tokens]) @@ -156,7 +166,7 @@ const TokenInput = () => { } const FiatInput = () => { - const { token, tokenRates, formatter, setPlancks } = useNomPoolBondWizard() + const { token, tokenRates, formatter, setPlancks } = useBondWizard() const currency = useSelectedCurrency() const defaultValue = useMemo(() => { @@ -226,9 +236,10 @@ export const AmountEdit = () => { displayMode, toggleDisplayMode, inputErrorMessage, + stakeWarningMessage, maxPlancks, setPlancks, - } = useNomPoolBondWizard() + } = useBondWizard() const onSetMaxClick = useCallback(() => { if (!maxPlancks) return @@ -269,6 +280,27 @@ export const AmountEdit = () => {
{inputErrorMessage}
+ {stakeWarningMessage && ( +
+ + +
+ {stakeWarningMessage} +
+
+ +
+
+ {t( + "The Bittensor network requires a wait period of 360 blocks since your last stake / unstake action.", + )} +
+
{t("Please try again later.")}
+
+
+
+
+ )}
)} @@ -276,14 +308,36 @@ export const AmountEdit = () => { ) } -const NomPoolsApr = () => { - const { token } = useNomPoolBondWizard() - const { data: apr, isLoading } = useStakingAPR(token?.chain?.id) +const StakeApr = () => { + const { token, poolId } = useBondWizard() + let data, + isLoading = false, + isError = false, + apr = 0 + + const hookMap = { + nominationPool: useStakingAPR, + bittensor: useGetBittensorValidator, + } + + switch (token?.chain?.id) { + case "bittensor": + ;({ data, isLoading, isError } = hookMap["bittensor"](poolId)) + apr = Number(data?.data?.[0].apr) + break + default: + ;({ data, isLoading, isError } = hookMap["nominationPool"](token?.chain?.id)) + apr = Number(data) + break + } + const display = useMemo(() => (apr ? `${(apr * 100).toFixed(2)}%` : "N/A"), [apr]) if (isLoading) return
15.00%
+ if (isError) return
Unable to fetch APR data
+ return ( {display} @@ -292,7 +346,7 @@ const NomPoolsApr = () => { } const FeeEstimate = () => { - const { feeEstimate, feeToken, isLoadingFeeEstimate, errorFeeEstimate } = useNomPoolBondWizard() + const { feeEstimate, feeToken, isLoadingFeeEstimate, errorFeeEstimate } = useBondWizard() return ( { ) } -export const NomPoolBondForm = () => { +export const BondForm = () => { const { t } = useTranslation() - const { account, poolId, accountPicker, token, payload, setStep } = useNomPoolBondWizard() + const { account, accountPicker, token, payload, setStep, poolId, bondType } = useBondWizard() + + const bondRowLabel = bondType === "bittensor" ? t("Validator") : t("Pool") return (
@@ -321,10 +377,7 @@ export const NomPoolBondForm = () => {
{t("Account")}
}> - +
@@ -336,26 +389,27 @@ export const NomPoolBondForm = () => {
{!!token && !!account && }
-
+
-
{t("Pool")}
+
{bondRowLabel}
- +
-
- {t("APR")} +
+ {t("APR")} +
{t("Estimated Annual Percentage Rate (APR)")}
- +
@@ -376,7 +430,7 @@ export const NomPoolBondForm = () => { {t("Review")} - +
) } diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondModal.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondModal.tsx similarity index 52% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondModal.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondModal.tsx index 302bed3a29..81f6e19f37 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondModal.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondModal.tsx @@ -5,25 +5,27 @@ import { useTranslation } from "react-i18next" import { IconButton, Modal } from "talisman-ui" import { SuspenseTracker } from "@talisman/components/SuspenseTracker" +import { TokenLogo } from "@ui/domains/Asset/TokenLogo" import { IS_POPUP } from "@ui/util/constants" -import { NomPoolBondFollowUp } from "./NomPoolBondFollowUp" -import { NomPoolBondForm } from "./NomPoolBondForm" -import { NomPoolBondReview } from "./NomPoolBondReview" -import { useNomPoolBondModal } from "./useNomPoolBondModal" -import { useNomPoolBondWizard } from "./useNomPoolBondWizard" +import { BittensorBondDelegateSelect } from "../Bittensor/BittensorBondDelegateSelect" +import { BondFollowUp } from "./BondFollowUp" +import { BondForm } from "./BondForm" +import { BondReview } from "./BondReview" +import { useBondModal } from "./useBondModal" +import { useBondWizard } from "./useBondWizard" const ModalHeader = () => { const { t } = useTranslation() - const { step, setStep } = useNomPoolBondWizard() - const { close } = useNomPoolBondModal() + const { step, setStep, token } = useBondWizard() + const { close } = useBondModal() const handleBackClick = useCallback(() => setStep("form"), [setStep]) return (
@@ -36,6 +38,22 @@ const ModalHeader = () => {
{step === "form" && {t("Staking")}} {step === "review" && t("Confirm")} + {step === "select" && ( +
+ + + +
+
{t("Select Validator")}
+
+ +
{token?.symbol}
+
+
{t("Delegated Staking")}
+
+
+
+ )}
@@ -45,15 +63,17 @@ const ModalHeader = () => { } const ModalContent = () => { - const { step } = useNomPoolBondWizard() + const { step } = useBondWizard() switch (step) { + case "select": + return case "form": - return + return case "review": - return + return case "follow-up": - return + return } } @@ -72,8 +92,8 @@ const Content = () => (
) -export const NomPoolBondModal = () => { - const { isOpen, close } = useNomPoolBondModal() +export const BondModal = () => { + const { isOpen, close } = useBondModal() return ( diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondPillButton.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondPillButton.tsx similarity index 83% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondPillButton.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondPillButton.tsx index fc75503f28..8ba4c9e8cc 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondPillButton.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondPillButton.tsx @@ -5,15 +5,15 @@ import { Balances } from "extension-core" import { FC } from "react" import { useTranslation } from "react-i18next" -import { useNomPoolBondButton } from "./useNomPoolBondButton" +import { useBondButton } from "./useBondButton" -export const NomPoolBondPillButton: FC<{ +export const BondPillButton: FC<{ tokenId: TokenId balances: Balances className?: string }> = ({ tokenId, balances, className }) => { const { t } = useTranslation() - const { onClick } = useNomPoolBondButton({ tokenId, balances }) + const { onClick } = useBondButton({ tokenId, balances }) if (!onClick) return null diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondReview.tsx b/apps/extension/src/ui/domains/Staking/Bond/BondReview.tsx similarity index 90% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondReview.tsx rename to apps/extension/src/ui/domains/Staking/Bond/BondReview.tsx index 77b2e63b49..2f231e1cb1 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondReview.tsx +++ b/apps/extension/src/ui/domains/Staking/Bond/BondReview.tsx @@ -4,16 +4,15 @@ import { useTranslation } from "react-i18next" import { TokenLogo } from "../../Asset/TokenLogo" import { TokensAndFiat } from "../../Asset/TokensAndFiat" import { SapiSendButton } from "../../Transactions/SapiSendButton" -import { NomPoolName } from "../shared/NomPoolName" +import { BondPoolName } from "../shared/BondPoolName" import { StakingAccountDisplay } from "../shared/StakingAccountDisplay" import { StakingFeeEstimate } from "../shared/StakingFeeEstimate" import { StakingUnbondingPeriod } from "../shared/StakingUnbondingPeriod" -import { useNomPoolBondWizard } from "./useNomPoolBondWizard" +import { useBondWizard } from "./useBondWizard" -export const NomPoolBondReview = () => { +export const BondReview = () => { const { t } = useTranslation() - const { token, poolId, formatter, account, onSubmitted, payload, txMetadata } = - useNomPoolBondWizard() + const { token, formatter, account, onSubmitted, payload, txMetadata, poolId } = useBondWizard() const [isDisabled, setIsDisabled] = useState(true) @@ -56,7 +55,7 @@ export const NomPoolBondReview = () => {
{t("Pool")}
- +
@@ -88,7 +87,7 @@ export const NomPoolBondReview = () => { } const FeeEstimate = () => { - const { feeEstimate, feeToken, isLoadingFeeEstimate, errorFeeEstimate } = useNomPoolBondWizard() + const { feeEstimate, feeToken, isLoadingFeeEstimate, errorFeeEstimate } = useBondWizard() return ( ownedAccounts.map(({ address }) => address), [ownedAccounts]) + + const sorted = useMemo(() => { + if (!balances || !tokenId) return [] + return balances + .find({ tokenId }) + .each.filter((b) => ownedAddresses.includes(b.address)) + .sort((a, b) => { + if (a.transferable.planck === b.transferable.planck) return 0 + return a.transferable.planck > b.transferable.planck ? -1 : 1 + }) + }, [balances, ownedAddresses, tokenId]) + + const address = sorted[0]?.address + + const { data: hotkeys } = useGetBittensorStakeHotkeys({ + chainId: token?.chain?.id, + address, + }) const [openArgs, isNomPoolStaking] = useMemo<[Parameters[0] | null, boolean]>(() => { if (!balances || !tokenId || !token?.chain || token?.type !== "substrate-native") return [null, false] try { let isNomPoolStaking = false - let poolId = remoteConfig.nominationPools[token.chain.id]?.[0] - if (!poolId) return [null, false] - const ownedAddresses = ownedAccounts.map(({ address }) => address) + let poolId = + remoteConfig.stakingPools[token.chain.id]?.[0] || + remoteConfig.nominationPools[token.chain.id]?.[0] - const sorted = balances - .find({ tokenId }) - .each.filter((b) => ownedAddresses.includes(b.address)) - .sort((a, b) => { - if (a.transferable.planck === b.transferable.planck) return 0 - return a.transferable.planck > b.transferable.planck ? -1 : 1 - }) + if (!poolId) return [null, false] // if a watch-only account is selected, there will be no entries here if (!sorted.length) return [null, false] - const address = sorted[0].address - // lookup existing poolId for that account for (const balance of sorted.filter((b) => b.address === address)) { type Meta = { poolId?: number } - const pool = balance.nompools.find((np) => !!(np.meta as Meta).poolId) - const meta = pool?.meta as Meta | undefined - if (meta?.poolId) { - poolId = meta.poolId - isNomPoolStaking = true - break + let pool + let meta + switch (token.chain.id) { + case "bittensor": + poolId = hotkeys?.[0] ?? poolId + break + default: + pool = balance.nompools.find((np) => !!(np.meta as Meta).poolId) + meta = pool?.meta as Meta | undefined + if (meta?.poolId) { + poolId = meta.poolId + isNomPoolStaking = true + break + } + break } } @@ -63,7 +85,7 @@ export const useNomPoolBondButton = ({ } return [null, false] - }, [balances, ownedAccounts, remoteConfig.nominationPools, tokenId, token?.chain, token?.type]) + }, [balances, remoteConfig, tokenId, token?.chain, token?.type, hotkeys, address, sorted]) const handleClick: MouseEventHandler = useCallback( (e) => { diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondModal.ts b/apps/extension/src/ui/domains/Staking/Bond/useBondModal.ts similarity index 58% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondModal.ts rename to apps/extension/src/ui/domains/Staking/Bond/useBondModal.ts index 0f06827236..4c4f343712 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondModal.ts +++ b/apps/extension/src/ui/domains/Staking/Bond/useBondModal.ts @@ -4,16 +4,29 @@ import { useCallback } from "react" import { useGlobalOpenClose } from "@talisman/hooks/useGlobalOpenClose" -import { useResetNomPoolBondWizard } from "./useNomPoolBondWizard" +import { useResetNomPoolBondWizard } from "./useBondWizard" -export const useNomPoolBondModal = () => { +export const useBondModal = () => { const reset = useResetNomPoolBondWizard() const { isOpen, open: innerOpen, close } = useGlobalOpenClose("NomPoolBondModal") const open = useCallback( - ({ address, tokenId, poolId }: { address: Address; tokenId: TokenId; poolId: number }) => { - reset({ address, tokenId, poolId }) + ({ + address, + tokenId, + poolId, + }: { + address: Address + tokenId: TokenId + poolId: number | string + }) => { + reset({ + address, + tokenId, + poolId, + step: "form", + }) // then open the modal innerOpen() diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondWizard.ts b/apps/extension/src/ui/domains/Staking/Bond/useBondWizard.ts similarity index 57% rename from apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondWizard.ts rename to apps/extension/src/ui/domains/Staking/Bond/useBondWizard.ts index ce2bf87fda..7ed1fec2fb 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/useNomPoolBondWizard.ts +++ b/apps/extension/src/ui/domains/Staking/Bond/useBondWizard.ts @@ -1,6 +1,5 @@ import { bind } from "@react-rxjs/core" import { TokenId } from "@talismn/chaindata-provider" -import { useQuery } from "@tanstack/react-query" import { Address, BalanceFormatter } from "extension-core" import { SetStateAction, useCallback, useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" @@ -13,24 +12,20 @@ import { useAccountByAddress, useBalance, useToken, useTokenRates } from "@ui/st import { useExistentialDeposit } from "../../../hooks/useExistentialDeposit" import { useFeeToken } from "../../SendFunds/useFeeToken" -import { getNomPoolStakingPayload } from "../helpers" -import { useIsSoloStaking } from "../shared/useIsSoloStaking" -import { useNomPoolByMember } from "../shared/useNomPoolByMember" -import { useNomPoolsClaimPermission } from "../shared/useNomPoolsClaimPermission" -import { useNomPoolsMinJoinBond } from "../shared/useNomPoolsMinJoinBond" -import { useNomPoolState } from "../shared/useNomPoolState" +import { useGetStakeInfo } from "../shared/useGetStakeInfo" -type WizardStep = "form" | "review" | "follow-up" +type WizardStep = "form" | "review" | "follow-up" | "select" type WizardState = { step: WizardStep address: Address | null tokenId: TokenId | null - poolId: number | null + poolId: number | string | null plancks: bigint | null displayMode: "token" | "fiat" isAccountPickerOpen: boolean hash: Hex | null + isDefaultOption: boolean } const DEFAULT_STATE: WizardState = { @@ -42,6 +37,7 @@ const DEFAULT_STATE: WizardState = { displayMode: "token", isAccountPickerOpen: false, hash: null, + isDefaultOption: true, } const wizardState$ = new BehaviorSubject(DEFAULT_STATE) @@ -76,7 +72,7 @@ const useInnerOpenClose = (key: "isAccountPickerOpen") => { export const useResetNomPoolBondWizard = () => { const reset = useCallback( - (init: Pick) => + (init: Pick) => setWizardState({ ...DEFAULT_STATE, ...init }), [], ) @@ -84,11 +80,12 @@ export const useResetNomPoolBondWizard = () => { return reset } -export const useNomPoolBondWizard = () => { +export const useBondWizard = () => { const { t } = useTranslation() const { genericEvent } = useAnalytics() - const { poolId, step, displayMode, hash, tokenId, address, plancks } = useWizardState() + const { poolId, step, displayMode, hash, tokenId, address, plancks, isDefaultOption } = + useWizardState() const balance = useBalance(address, tokenId) const account = useAccountByAddress(address) @@ -98,11 +95,31 @@ export const useNomPoolBondWizard = () => { const existentialDeposit = useExistentialDeposit(token?.id) const accountPicker = useInnerOpenClose("isAccountPickerOpen") - const { data: minJoinBond } = useNomPoolsMinJoinBond(token?.chain?.id) - const { data: claimPermission } = useNomPoolsClaimPermission(token?.chain?.id, address) - const { data: isSoloStaking } = useIsSoloStaking(token?.chain?.id, address) - const { data: currentPool } = useNomPoolByMember(token?.chain?.id, address) - const { data: poolState } = useNomPoolState(token?.chain?.id, poolId) + const { data: sapi } = useScaleApi(token?.chain?.id) + + const { + payload, + txMetadata, + isLoadingPayload, + errorPayload, + feeEstimate, + errorFeeEstimate, + isLoadingFeeEstimate, + bondType, + currentPoolId, + hasJoinedNomPool, + minJoinBond, + isSoloStaking, + poolState, + canStake, + isCanStakeLoading, + } = useGetStakeInfo({ + sapi, + address, + poolId, + plancks, + chainId: token?.chain?.id, + }) // TODO rename to amountToStake const formatter = useMemo( @@ -124,7 +141,7 @@ export const useNomPoolBondWizard = () => { ) const setPoolId = useCallback( - (poolId: number) => setWizardState((prev) => ({ ...prev, poolId })), + (poolId: number | string) => setWizardState((prev) => ({ ...prev, poolId })), [], ) @@ -133,6 +150,11 @@ export const useNomPoolBondWizard = () => { [], ) + const setIsDefaultOption = useCallback( + (isDefaultOption: boolean) => setWizardState((prev) => ({ ...prev, isDefaultOption })), + [], + ) + const toggleDisplayMode = useCallback(() => { setWizardState((prev) => ({ ...prev, @@ -140,17 +162,27 @@ export const useNomPoolBondWizard = () => { })) }, []) - useEffect(() => { - // if user is already staking in pool, set poolId to that pool - if (currentPool && currentPool.pool_id !== poolId) - setWizardState((prev) => ({ ...prev, poolId: currentPool.pool_id })) - }, [currentPool, poolId]) - const isFormValid = useMemo( - () => !!account && !!token && !!poolId && !!formatter && typeof minJoinBond === "bigint", - [account, formatter, minJoinBond, poolId, token], + () => + !!account && + !!token && + !!poolId && + !!formatter && + typeof minJoinBond === "bigint" && + plancks && + plancks > 0n, + [account, formatter, minJoinBond, plancks, poolId, token], ) + useEffect(() => { + /** + * if user is already staking in pool, set poolId to that pool + * If the user chooses to stake in a different pool, we should not set the poolId to the one the user is currently staking in + */ + if (!!currentPoolId && currentPoolId !== poolId && isDefaultOption) + setWizardState((prev) => ({ ...prev, poolId: currentPoolId })) + }, [bondType, currentPoolId, isDefaultOption, poolId, step, tokenId]) + const setStep = useCallback( (step: WizardStep) => { setWizardState((prev) => { @@ -162,116 +194,32 @@ export const useNomPoolBondWizard = () => { [isFormValid], ) - // we must craft a different extrinsic if the user is already staking in a pool - const hasJoinedNomPool = useMemo(() => !!currentPool, [currentPool]) - - const withSetClaimPermission = useMemo(() => { - switch (claimPermission) { - case "PermissionlessCompound": - case "PermissionlessAll": - return false - default: - // if the user is already staking in a pool, we shouldn't change the claim permission - return !hasJoinedNomPool - } - }, [claimPermission, hasJoinedNomPool]) - - const { data: sapi } = useScaleApi(token?.chain?.id) - - const { - data: payloadAndMetadata, - isLoading: isLoadingPayload, - error: errorPayload, - } = useQuery({ - queryKey: [ - "getNomPoolStakingPayload", - sapi?.id, - address, - poolId, - plancks?.toString(), - isFormValid, - hasJoinedNomPool, - withSetClaimPermission, - ], - queryFn: async () => { - if (!sapi || !address || !poolId || !plancks) return null - if (!isFormValid) return null - - return getNomPoolStakingPayload( - sapi, - address, - poolId, - plancks, - hasJoinedNomPool, - withSetClaimPermission, - ) - }, - enabled: !!sapi, - }) - - const { payload, txMetadata } = payloadAndMetadata || {} - - const { - // used to get an estimate before amount is known, for estimating maxPlancks - data: fakeFeeEstimate, - } = useQuery({ - queryKey: [ - "getNomPoolStakingPayload/estimateFee", - sapi?.id, - poolId, - address, - minJoinBond?.toString(), - hasJoinedNomPool, - withSetClaimPermission, - ], - queryFn: async () => { - if (!sapi || !address || !poolId || typeof minJoinBond !== "bigint") return null - - const { payload } = await getNomPoolStakingPayload( - sapi, - address, - poolId, - minJoinBond, - hasJoinedNomPool, - withSetClaimPermission, - ) - return sapi.getFeeEstimate(payload) - }, - enabled: !!sapi, - }) - - const { - data: feeEstimate, - isLoading: isLoadingFeeEstimate, - error: errorFeeEstimate, - } = useQuery({ - queryKey: ["feeEstimate", sapi?.id, payload], // safe stringify because contains bigint - queryFn: () => { - if (!sapi || !payload) return null - return sapi.getFeeEstimate(payload) - }, - }) - const onSubmitted = useCallback( (hash: Hex) => { - genericEvent("NomPool Bond", { tokenId, isBondExtra: hasJoinedNomPool }) + genericEvent(`${bondType} Bond`, { tokenId, isBondExtra: hasJoinedNomPool }) if (hash) setWizardState((prev) => ({ ...prev, step: "follow-up", hash })) }, - [genericEvent, hasJoinedNomPool, tokenId], + [genericEvent, hasJoinedNomPool, tokenId, bondType], ) const maxPlancks = useMemo(() => { - if (!balance || !existentialDeposit || !fakeFeeEstimate) return null - // use 11x fake fee estimate as we block form based on 10x the real fee estimate - if (existentialDeposit.planck + fakeFeeEstimate * 11n > balance.transferable.planck) return null - return balance.transferable.planck - existentialDeposit.planck - fakeFeeEstimate * 11n - }, [balance, existentialDeposit, fakeFeeEstimate]) + if (!balance || !existentialDeposit || !feeEstimate) return null + if (existentialDeposit.planck + feeEstimate * 11n > balance.transferable.planck) return null + return balance.transferable.planck - existentialDeposit.planck - feeEstimate * 11n + }, [balance, existentialDeposit, feeEstimate]) + + const stakeWarningMessage = useMemo(() => { + if (!canStake) return t("Stake/unstake currently paused") + return null + }, [canStake, t]) const inputErrorMessage = useMemo(() => { - if (isSoloStaking) return t("Account is already staking") + if (isSoloStaking) + return t("Account has an open validator staking position, please unbond first") - if (!currentPool && poolState?.isFull) return t("This nomination pool is full") - if (!currentPool && poolState && !poolState.isOpen) return t("This nomination pool is not open") + if (!currentPoolId && poolState?.isFull) return t("This nomination pool is full") + if (!currentPoolId && poolState && !poolState.isOpen) + return t("This nomination pool is not open") if (!formatter || typeof minJoinBond !== "bigint") return null @@ -316,7 +264,7 @@ export const useNomPoolBondWizard = () => { }, [ isSoloStaking, t, - currentPool, + currentPoolId, poolState, formatter, minJoinBond, @@ -342,8 +290,10 @@ export const useNomPoolBondWizard = () => { feeToken, maxPlancks, inputErrorMessage, + stakeWarningMessage, + bondType, - payload: !inputErrorMessage ? payload : null, + payload: !inputErrorMessage && isFormValid && canStake && !isCanStakeLoading ? payload : null, txMetadata, isLoadingPayload, errorPayload, @@ -357,6 +307,7 @@ export const useNomPoolBondWizard = () => { setPoolId, setPlancks, setStep, + setIsDefaultOption, toggleDisplayMode, onSubmitted, diff --git a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondFollowUp.tsx b/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondFollowUp.tsx deleted file mode 100644 index 7aefccbfbd..0000000000 --- a/apps/extension/src/ui/domains/Staking/NomPoolBond/NomPoolBondFollowUp.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TxProgress } from "../../Transactions" -import { useNomPoolBondModal } from "./useNomPoolBondModal" -import { useNomPoolBondWizard } from "./useNomPoolBondWizard" - -export const NomPoolBondFollowUp = () => { - const { close } = useNomPoolBondModal() - const { hash, token } = useNomPoolBondWizard() - - if (!hash || !token?.chain?.id) return null - - return -} diff --git a/apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondFollowUp.tsx b/apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondFollowUp.tsx deleted file mode 100644 index 374fb34d68..0000000000 --- a/apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondFollowUp.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TxProgress } from "../../Transactions" -import { useNomPoolUnbondModal } from "./useNomPoolUnbondModal" -import { useNomPoolUnbondWizard } from "./useNomPoolUnbondWizard" - -export const NomPoolUnbondFollowUp = () => { - const { close } = useNomPoolUnbondModal() - const { hash, token } = useNomPoolUnbondWizard() - - if (!hash || !token?.chain?.id) return null - - return -} diff --git a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton.tsx b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton.tsx index 842bd61c6f..a404472155 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton.tsx +++ b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawButton.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { useNomPoolStakingStatus } from "../shared/useNomPoolStakingStatus" +import { useNomPoolStakingStatus } from "../hooks/nomPools/useNomPoolStakingStatus" import { useNomPoolWithdrawModal } from "./useNomPoolWithdrawModal" export const NomPoolWithdrawButton: FC<{ diff --git a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawReview.tsx b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawReview.tsx index 26d6be28a9..436b60821d 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawReview.tsx +++ b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/NomPoolWithdrawReview.tsx @@ -5,7 +5,7 @@ import { Button } from "talisman-ui" import { TokenLogo } from "../../Asset/TokenLogo" import { TokensAndFiat } from "../../Asset/TokensAndFiat" import { SapiSendButton } from "../../Transactions/SapiSendButton" -import { NomPoolName } from "../shared/NomPoolName" +import { BondPoolName } from "../shared/BondPoolName" import { StakingAccountDisplay } from "../shared/StakingAccountDisplay" import { StakingFeeEstimate } from "../shared/StakingFeeEstimate" import { useNomPoolWithdrawWizard } from "./useNomPoolWithdrawWizard" @@ -14,7 +14,6 @@ export const NomPoolWithdrawReview = () => { const { t } = useTranslation() const { token, - poolId, amountToWithdraw, account, onSubmitted, @@ -25,6 +24,7 @@ export const NomPoolWithdrawReview = () => { isLoadingFeeEstimate, errorFeeEstimate, errorMessage, + poolId, } = useNomPoolWithdrawWizard() if (!account) return null @@ -59,7 +59,7 @@ export const NomPoolWithdrawReview = () => {
{t("Pool")}
- +
diff --git a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/useNomPoolWithdrawWizard.ts b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/useNomPoolWithdrawWizard.ts index 59a65796d4..8e3ab8dd0c 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/useNomPoolWithdrawWizard.ts +++ b/apps/extension/src/ui/domains/Staking/NomPoolWithdraw/useNomPoolWithdrawWizard.ts @@ -14,8 +14,8 @@ import { useAnalytics } from "@ui/hooks/useAnalytics" import { useAccountByAddress, useBalance, useToken, useTokenRates } from "@ui/state" import { useExistentialDeposit } from "../../../hooks/useExistentialDeposit" -import { useCurrentStakingEra } from "../shared/useCurrentStakingEra" -import { useNomPoolByMember } from "../shared/useNomPoolByMember" +import { useCurrentStakingEra } from "../hooks/nomPools/useCurrentStakingEra" +import { useNomPoolByMember } from "../hooks/nomPools/useNomPoolByMember" type WizardStep = "review" | "follow-up" diff --git a/apps/extension/src/ui/domains/Staking/NominationPools/NomPoolUnbondingPeriod.tsx b/apps/extension/src/ui/domains/Staking/NominationPools/NomPoolUnbondingPeriod.tsx new file mode 100644 index 0000000000..dc9e587777 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/NominationPools/NomPoolUnbondingPeriod.tsx @@ -0,0 +1,27 @@ +import { formatDistance } from "date-fns" +import { ChainId } from "extension-core" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +import { useStakingBondingDuration } from "../hooks/nomPools/useStakingBondingDuration" + +type NomPoolUnbondingPeriodProps = { + chainId: ChainId | null | undefined +} + +export const NomPoolUnbondingPeriod = ({ chainId }: NomPoolUnbondingPeriodProps) => { + const { data, isLoading, isError } = useStakingBondingDuration(chainId) + const { t } = useTranslation() + + const display = useMemo( + () => (data ? formatDistance(0, Number(data?.toString()) || 0) : t("N/A")), + [data, t], + ) + + if (isLoading) + return
28 Days
+ + if (isError) return
{t("Unable to fetch unbonding period")}
+ + return <>{display} +} diff --git a/apps/extension/src/ui/domains/Staking/NominationPools/NominationPoolName.tsx b/apps/extension/src/ui/domains/Staking/NominationPools/NominationPoolName.tsx new file mode 100644 index 0000000000..71b9afaf5b --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/NominationPools/NominationPoolName.tsx @@ -0,0 +1,21 @@ +import { ChainId } from "extension-core" + +import { useNomPoolName } from "../hooks/nomPools/useNomPoolName" + +type NominationPoolNameProps = { + poolId: string | number | undefined | null + chainId: ChainId | undefined +} + +export const NominationPoolName = ({ chainId, poolId }: NominationPoolNameProps) => { + const { data: poolName, isLoading, isError } = useNomPoolName(chainId, poolId) + + const defaultPoolName = "Talisman Pool" + + if (isLoading) + return
+ + if (isError || !poolName) return <>{defaultPoolName} + + return <>{poolName} +} diff --git a/apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondButton.tsx b/apps/extension/src/ui/domains/Staking/Unbond/UnbondButton.tsx similarity index 67% rename from apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondButton.tsx rename to apps/extension/src/ui/domains/Staking/Unbond/UnbondButton.tsx index 154150a647..614ced5c85 100644 --- a/apps/extension/src/ui/domains/Staking/NomPoolUnbond/NomPoolUnbondButton.tsx +++ b/apps/extension/src/ui/domains/Staking/Unbond/UnbondButton.tsx @@ -6,32 +6,35 @@ import { useTranslation } from "react-i18next" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { useNomPoolStakingStatus } from "../shared/useNomPoolStakingStatus" -import { useNomPoolUnbondModal } from "./useNomPoolUnbondModal" +import { useNomPoolStakingStatus } from "../hooks/nomPools/useNomPoolStakingStatus" +import { useUnbondModal } from "./useUnbondModal" -export const NomPoolUnbondButton: FC<{ +export const UnbondButton: FC<{ tokenId: TokenId address: string className?: string variant: "small" | "large" -}> = ({ tokenId, address, className, variant }) => { + poolId: number | string | undefined +}> = ({ tokenId, address, className, variant, poolId }) => { const { t } = useTranslation() - const { open } = useNomPoolUnbondModal() + const { open } = useUnbondModal() const { data: stakingStatus } = useNomPoolStakingStatus(tokenId) const { genericEvent } = useAnalytics() const canUnstake = useMemo( - () => !!stakingStatus?.accounts.find((s) => s.address === address && s.canUnstake), - [address, stakingStatus], + () => + !!stakingStatus?.accounts.find((s) => s.address === address && s.canUnstake) || + tokenId === "bittensor-substrate-native", + [address, stakingStatus?.accounts, tokenId], ) const handleClick = useCallback(() => { - open({ tokenId, address }) + open({ tokenId, address, poolId }) genericEvent("open inline unbonding modal", { from: "asset details", tokenId }) - }, [address, genericEvent, open, tokenId]) + }, [address, genericEvent, open, poolId, tokenId]) - if (!canUnstake) return null // no nompool staking on this network + if (!canUnstake) return null // no nompool/tao staking on this network return ( + ))} + +
+
+
{t("Name")}
+
{t("Est. Rewards")}
+
+ + {isLoading && bondOptions.length === 0 + ? Array(6) + .fill(null) + .map((_, i) => ) + : bondOptions.map((option, i) => ( + <> + + {/* add a separator after the recommended it, which should be first */} + {i === 0 &&
} + + ))} + {isError && ( +
+ {t("Unable to fetch validators")} +
+ )} + +
+ +
+ ) +} diff --git a/apps/extension/src/ui/domains/Staking/shared/BondOption.tsx b/apps/extension/src/ui/domains/Staking/shared/BondOption.tsx new file mode 100644 index 0000000000..8ed83ba6f3 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/BondOption.tsx @@ -0,0 +1,102 @@ +import { TalismanHandIcon, UserIcon } from "@talismn/icons" +import { classNames, planckToTokens } from "@talismn/util" +import { useTranslation } from "react-i18next" + +import { Tokens } from "@ui/domains/Asset/Tokens" +import { useToken } from "@ui/state" + +import { BondOption as BondOptionType } from "../hooks/bittensor/types" + +type BondDrawerProps = { + option: BondOptionType + selectedPoolId: number | string | null | undefined + handleSelectPoolId: (poolId: number | string) => void + tokenId: string +} + +export const BondOptionSkeleton = ({ isRecommended }: { isRecommended?: boolean }) => { + return ( +
+
+
+ {isRecommended && ( +
+ )} +
+
+
+
+
+
+
+ ) +} + +export const BondOption = ({ + option, + selectedPoolId, + handleSelectPoolId, + tokenId, +}: BondDrawerProps) => { + const { t } = useTranslation() + const token = useToken(tokenId) + const isSelected = option.poolId === selectedPoolId + return ( + + ) +} diff --git a/apps/extension/src/ui/domains/Staking/shared/BondPoolName.tsx b/apps/extension/src/ui/domains/Staking/shared/BondPoolName.tsx new file mode 100644 index 0000000000..0380ee2eac --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/BondPoolName.tsx @@ -0,0 +1,19 @@ +import { ChainId } from "extension-core" + +import { BittensorDelegatorNameButton } from "../Bittensor/BittensorDelegatorNameButton" +import { NominationPoolName } from "../NominationPools/NominationPoolName" + +export const BondPoolName = ({ + poolId, + chainId, +}: { + poolId: string | number | undefined | null + chainId: ChainId | undefined +}) => { + switch (chainId) { + case "bittensor": + return + default: + return + } +} diff --git a/apps/extension/src/ui/domains/Staking/shared/NomPoolName.tsx b/apps/extension/src/ui/domains/Staking/shared/NomPoolName.tsx deleted file mode 100644 index 9936bc0826..0000000000 --- a/apps/extension/src/ui/domains/Staking/shared/NomPoolName.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { ChainId } from "extension-core" -import { FC } from "react" - -import { useNomPoolName } from "./useNomPoolName" - -export const NomPoolName: FC<{ - chainId: ChainId | null | undefined - poolId: number | null | undefined -}> = ({ chainId, poolId }) => { - const { data: poolName, isLoading } = useNomPoolName(chainId, poolId) - - if (isLoading) - return
Talisman Pool
- - return <>{poolName} -} diff --git a/apps/extension/src/ui/domains/Staking/shared/StakingFeeEstimate.tsx b/apps/extension/src/ui/domains/Staking/shared/StakingFeeEstimate.tsx index ca843a04a4..411790897b 100644 --- a/apps/extension/src/ui/domains/Staking/shared/StakingFeeEstimate.tsx +++ b/apps/extension/src/ui/domains/Staking/shared/StakingFeeEstimate.tsx @@ -18,7 +18,7 @@ export const StakingFeeEstimate: FC<{
Failed to estimate fee
- ) : plancks && tokenId ? ( + ) : (plancks || plancks === 0n) && tokenId ? ( = ({ chainId, }) => { - const { t } = useTranslation() - const { data: duration, isLoading } = useStakingBondingDuration(chainId) - const locale = useDateFnsLocale() - - const display = useMemo( - () => - duration - ? formatDuration(durationFromMs(Number(duration)), { - locale, - }) - : t("N/A"), - [duration, locale, t], - ) - - if (isLoading) - return
28 Days
- - return <>{display} -} - -const durationFromMs = (ms: number): Duration => { - // returns the best possible looking duration object from ms - const seconds = Math.floor(ms / 1000) - const minutes = Math.floor(seconds / 60) - const hours = Math.floor(minutes / 60) - const days = Math.floor(hours / 24) - - const result = { - seconds: seconds % 60, - minutes: minutes % 60, - hours: hours % 24, - days: days, + switch (chainId) { + case "bittensor": + return + default: + return } - - return result } diff --git a/apps/extension/src/ui/domains/Staking/shared/useGetFeeEstimate.ts b/apps/extension/src/ui/domains/Staking/shared/useGetFeeEstimate.ts new file mode 100644 index 0000000000..10b17e46e8 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useGetFeeEstimate.ts @@ -0,0 +1,20 @@ +import { SignerPayloadJSON } from "@polkadot/types/types" +import { useQuery } from "@tanstack/react-query" + +import { ScaleApi } from "@ui/util/scaleApi" + +type GetNomPoolFeeEstimate = { + sapi: ScaleApi | undefined | null + payload: SignerPayloadJSON | undefined +} + +export const useGetFeeEstimate = ({ sapi, payload }: GetNomPoolFeeEstimate) => { + return useQuery({ + queryKey: ["feeEstimate", sapi?.id, payload], // safe stringify because contains bigint + queryFn: () => { + if (!sapi || !payload) return null + return sapi.getFeeEstimate(payload) + }, + enabled: !!sapi && !!payload, + }) +} diff --git a/apps/extension/src/ui/domains/Staking/shared/useGetLatestBlockNumber.ts b/apps/extension/src/ui/domains/Staking/shared/useGetLatestBlockNumber.ts new file mode 100644 index 0000000000..9e5245d6cf --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useGetLatestBlockNumber.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query" + +import { ScaleApi } from "@ui/util/scaleApi" + +type GetLatestBlockNumber = { + sapi: ScaleApi | undefined | null + isEnabled: boolean +} + +export const useGetLatestBlockNumber = ({ sapi, isEnabled }: GetLatestBlockNumber) => { + return useQuery({ + queryKey: ["useGetLatestBlockNumber", sapi?.id], + queryFn: async () => { + return sapi?.getStorage("System", "Number", []) + }, + enabled: isEnabled && !!sapi, + }) +} diff --git a/apps/extension/src/ui/domains/Staking/shared/useGetMinJoinBond.ts b/apps/extension/src/ui/domains/Staking/shared/useGetMinJoinBond.ts new file mode 100644 index 0000000000..7f558d4bc0 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useGetMinJoinBond.ts @@ -0,0 +1,20 @@ +import { ChainId } from "extension-core" + +import { useGetBittensorMinJoinBond } from "../hooks/bittensor/useGetBittensorMinJoinBond" +import { useNomPoolsMinJoinBond } from "../hooks/nomPools/useNomPoolsMinJoinBond" + +export const useGetMinJoinBond = (chainId: ChainId | null | undefined) => { + const minNomPoolsJoinBond = useNomPoolsMinJoinBond({ + chainId, + isEnabled: chainId !== "bittensor", + }) + + const minBittensorJoinBond = useGetBittensorMinJoinBond({ chainId }) + + switch (chainId) { + case "bittensor": + return minBittensorJoinBond + default: + return minNomPoolsJoinBond + } +} diff --git a/apps/extension/src/ui/domains/Staking/shared/useGetStakeInfo.ts b/apps/extension/src/ui/domains/Staking/shared/useGetStakeInfo.ts new file mode 100644 index 0000000000..394406a63a --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useGetStakeInfo.ts @@ -0,0 +1,125 @@ +import { ChainId } from "extension-core" +import { useMemo } from "react" + +import { ScaleApi } from "@ui/util/scaleApi" + +import { useCanStakeBittensor } from "../hooks/bittensor/useCanStakeBittensor" +import { useGetBittensorStakeHotkeys } from "../hooks/bittensor/useGetBittensorStakeHotkeys" +import { useGetBittensorStakingPayload } from "../hooks/bittensor/useGetBittensorStakingPayload" +import { useGetNomPoolStakingPayload } from "../hooks/nomPools/useGetNomPoolStakingPayload" +import { useIsSoloStaking } from "../hooks/nomPools/useIsSoloStaking" +import { useNomPoolByMember } from "../hooks/nomPools/useNomPoolByMember" +import { useNomPoolsClaimPermission } from "../hooks/nomPools/useNomPoolsClaimPermission" +import { useNomPoolState } from "../hooks/nomPools/useNomPoolState" +import { useGetFeeEstimate } from "./useGetFeeEstimate" +import { useGetMinJoinBond } from "./useGetMinJoinBond" + +type GetStakeInfo = { + sapi: ScaleApi | undefined | null + address: string | null + poolId: string | number | null | undefined + plancks: bigint | null + chainId: ChainId | undefined +} + +type BondType = "bittensor" | "nomPools" + +export const useGetStakeInfo = ({ sapi, address, poolId, plancks, chainId }: GetStakeInfo) => { + const { data: minJoinBond } = useGetMinJoinBond(chainId) + + const bittensorStakingPayload = useGetBittensorStakingPayload({ + sapi, + address, + poolId, + plancks, + minJoinBond, + isEnabled: chainId === "bittensor", + }) + + const { canStake, isLoading: isCanStakeLoading } = useCanStakeBittensor({ + sapi, + address, + hotkey: poolId, + chainId, + }) + + const { data: hotkeys } = useGetBittensorStakeHotkeys({ address, chainId }) + + const { data: claimPermission } = useNomPoolsClaimPermission(chainId, address) + + let payloadInfo + let bondType: BondType + let currentPoolId: string | number | undefined | null = 0 + + // we must craft a different extrinsic if the user is already staking in a pool + const hasJoinedNomPool = useMemo(() => !!currentPoolId, [currentPoolId]) + + const withSetClaimPermission = useMemo(() => { + switch (claimPermission) { + case "PermissionlessCompound": + case "PermissionlessAll": + return false + default: + // if the user is already staking in a pool, we shouldn't change the claim permission + return !hasJoinedNomPool + } + }, [claimPermission, hasJoinedNomPool]) + + const nomPoolStakingPayload = useGetNomPoolStakingPayload({ + sapi, + address, + poolId, + plancks, + hasJoinedNomPool, + withSetClaimPermission, + minJoinBond, + }) + + const { data: currentNomPool } = useNomPoolByMember(chainId, address) + const { data: isSoloStaking } = useIsSoloStaking(chainId, address) + const { data: poolState } = useNomPoolState(chainId, poolId) + + switch (chainId) { + case "bittensor": + payloadInfo = bittensorStakingPayload + bondType = "bittensor" + currentPoolId = hotkeys?.[0] ?? poolId + break + default: + payloadInfo = nomPoolStakingPayload + bondType = "nomPools" + currentPoolId = currentNomPool?.pool_id + break + } + const { + data: payloadAndMetadata, + isLoading: isLoadingPayload, + error: errorPayload, + } = payloadInfo || {} + + const { payload, txMetadata } = payloadAndMetadata || {} + + const { + data: feeEstimate, + isLoading: isLoadingFeeEstimate, + error: errorFeeEstimate, + } = useGetFeeEstimate({ sapi, payload }) + + return { + payload, + txMetadata, + isLoadingPayload, + errorPayload, + feeEstimate, + isLoadingFeeEstimate, + errorFeeEstimate, + bondType, + currentPoolId, + hasJoinedNomPool, + minJoinBond, + isSoloStaking, + poolState, + canStake, + isCanStakeLoading, + } +} diff --git a/apps/extension/src/ui/domains/Staking/shared/useGetUnbondInfo.ts b/apps/extension/src/ui/domains/Staking/shared/useGetUnbondInfo.ts new file mode 100644 index 0000000000..79f6a3fe01 --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useGetUnbondInfo.ts @@ -0,0 +1,117 @@ +import { ChainId } from "extension-core" + +import { ScaleApi } from "@ui/util/scaleApi" + +import { useCanStakeBittensor } from "../hooks/bittensor/useCanStakeBittensor" +import { useGetBittensorStakeByHotKey } from "../hooks/bittensor/useGetBittensorStakeByHotKey" +import { useGetBittensorUnbondPayload } from "../hooks/bittensor/useGetBittensorUnbondPayload" +import { useUpsertBittensorUnbondBlockNumber } from "../hooks/bittensor/useUpsertBittensorUnbondBlockNumber" +import { useGetNomPoolPlanksToUnbond } from "../hooks/nomPools/useGetNomPoolPlanksToUnbond" +import { useGetNomPoolUnbondPayload } from "../hooks/nomPools/useGetNomPoolUnbondPayload" +import { useNomPoolByMember } from "../hooks/nomPools/useNomPoolByMember" +import { useGetFeeEstimate } from "./useGetFeeEstimate" + +type GetUnbondInfo = { + sapi: ScaleApi | undefined | null + chainId: ChainId | undefined + address: string | undefined + unstakePoolId: number | string | undefined +} + +type UnbondType = "bittensor" | "nomPools" + +export const useGetUnbondInfo = ({ sapi, chainId, address, unstakePoolId }: GetUnbondInfo) => { + const { data: pool } = useNomPoolByMember(chainId, address) + const { data: nomPoolPlanksToUnbond } = useGetNomPoolPlanksToUnbond({ + sapi, + pool, + isEnabled: chainId !== "bittensor", + }) + const nomPoolUnbondPayload = useGetNomPoolUnbondPayload({ + sapi, + address, + pool, + isEnabled: chainId !== "bittensor", + }) + + const { data: bittensorPlanks } = useGetBittensorStakeByHotKey({ + address, + hotkey: unstakePoolId, + }) + + const bittensorUnbondPayload = useGetBittensorUnbondPayload({ + sapi, + address, + hotkey: unstakePoolId, + isEnabled: chainId === "bittensor", + plancks: bittensorPlanks, + }) + + const { mutate: upsertBittensorUnbondBlockNumber } = useUpsertBittensorUnbondBlockNumber() + + const handleBittensorUnbondSuccess = (blockNumber: number) => { + upsertBittensorUnbondBlockNumber({ + account: address, + delegator: unstakePoolId, + blockNumber, + }) + } + + let payloadInfo + let plancksToUnbond + let poolId + let unbondType: UnbondType + + switch (chainId) { + case "bittensor": + payloadInfo = bittensorUnbondPayload + plancksToUnbond = bittensorPlanks + poolId = unstakePoolId + unbondType = "bittensor" + break + default: + payloadInfo = nomPoolUnbondPayload + plancksToUnbond = nomPoolPlanksToUnbond + poolId = pool?.pool_id + unbondType = "bittensor" + break + } + + const { + data: payloadAndMetadata, + isLoading: isLoadingPayload, + error: errorPayload, + } = payloadInfo || {} + + const { payload, txMetadata } = payloadAndMetadata || {} + + const { + data: feeEstimate, + isLoading: isLoadingFeeEstimate, + error: errorFeeEstimate, + } = useGetFeeEstimate({ sapi, payload }) + + const { canStake, isLoading: isCanStakeLoading } = useCanStakeBittensor({ + sapi, + address, + hotkey: poolId, + chainId, + }) + + return { + plancksToUnbond, + pool, + poolId, + payload, + txMetadata, + isLoadingPayload, + errorPayload, + feeEstimate, + isLoadingFeeEstimate, + errorFeeEstimate, + unbondType, + canStake, + isCanStakeLoading, + handleSuccess: chainId === "bittensor" ? handleBittensorUnbondSuccess : () => {}, + } +} diff --git a/apps/extension/src/ui/domains/Staking/shared/useRecommendedPoolsIds.ts b/apps/extension/src/ui/domains/Staking/shared/useRecommendedPoolsIds.ts new file mode 100644 index 0000000000..a838ee3e1a --- /dev/null +++ b/apps/extension/src/ui/domains/Staking/shared/useRecommendedPoolsIds.ts @@ -0,0 +1,13 @@ +import { useMemo } from "react" + +import { ChainId } from "@extension/core" +import { useRemoteConfig } from "@ui/state" + +export const useRecommendedPoolsIds = (chainId?: ChainId | null | undefined) => { + const remoteConfig = useRemoteConfig() + + return useMemo(() => { + if (!chainId) return null + return remoteConfig.stakingPools?.[chainId] ?? null + }, [chainId, remoteConfig.stakingPools]) +} diff --git a/apps/extension/src/ui/domains/Staking/useShowStakingBanner.ts b/apps/extension/src/ui/domains/Staking/useShowStakingBanner.ts deleted file mode 100644 index 51128ec291..0000000000 --- a/apps/extension/src/ui/domains/Staking/useShowStakingBanner.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useCallback } from "react" - -import { Balances } from "@extension/core" -import { TALISMAN_WEB_APP_STAKING_URL } from "@extension/shared" -import { useAnalytics } from "@ui/hooks/useAnalytics" - -import { useTokenBalancesSummary } from "../Portfolio/useTokenBalancesSummary" -import { isStakingSupportedChain } from "./helpers" -import { useStakingBanner } from "./useStakingBanner" - -export const useShowStakingBanner = (balances: Balances) => { - const { genericEvent } = useAnalytics() - - const { token } = useTokenBalancesSummary(balances) - - const { showTokenStakingBanner, dismissStakingBanner, getStakingMessage, getBannerColours } = - useStakingBanner() - const showBanner = showTokenStakingBanner({ - token, - addresses: Array.from(new Set(balances.each.map((b) => b.address))), - }) - const message = getStakingMessage({ token }) - const colours = getBannerColours({ token }) - - const handleClickStakingBanner = useCallback(() => { - window.open(TALISMAN_WEB_APP_STAKING_URL) - genericEvent("open web app staking from banner", { from: "dashboard", symbol: token?.symbol }) - }, [genericEvent, token?.symbol]) - - const handleDismissStakingBanner = useCallback(() => { - const unsafeChainId = token?.chain?.id || token?.evmNetwork?.id - if (unsafeChainId && isStakingSupportedChain(unsafeChainId)) dismissStakingBanner(unsafeChainId) - genericEvent("dismiss staking banner", { from: "dashboard", symbol: token?.symbol }) - }, [token?.chain?.id, token?.evmNetwork?.id, token?.symbol, dismissStakingBanner, genericEvent]) - - return { - balances, - showBanner, - message, - colours, - handleClickStakingBanner, - handleDismissStakingBanner, - } -} - -export type ShowStakingReminderProps = ReturnType diff --git a/apps/extension/src/ui/domains/Staking/useStakingBanner.tsx b/apps/extension/src/ui/domains/Staking/useStakingBanner.tsx deleted file mode 100644 index b3b9464eeb..0000000000 --- a/apps/extension/src/ui/domains/Staking/useStakingBanner.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { Token } from "@talismn/chaindata-provider" -import { useCallback, useMemo } from "react" -import { useTranslation } from "react-i18next" - -import { - Address, - appStore, - EVM_LSD_PAIRS, - STAKING_BANNER_CHAINS, - StakingSupportedChain, -} from "@extension/core" -import { useAccounts, useAppState, useStakingBannerStore } from "@ui/state" - -import { colours, isNomPoolChain, isStakingSupportedChain } from "./helpers" - -const useEvmLsdStakingEligibility = () => { - const chainAddressEligibility = useStakingBannerStore().evmLsd - const accounts = useAccounts("owned") - const ownedAddresses = useMemo( - () => accounts.filter(({ type }) => type === "ethereum").map(({ address }) => address), - [accounts], - ) - - const evmLsdStakingAddressesEligible = useCallback( - ({ addresses }: { addresses: Address[] }) => { - const eligibleAddresses = Object.values(chainAddressEligibility) - .map((chainData) => Object.entries(chainData)) - .flat() - .reduce( - (acc, [address, showBanner]) => { - acc[address] = acc[address] || showBanner - return acc - }, - {} as Record, - ) - - return addresses.some( - (address) => ownedAddresses.includes(address) && eligibleAddresses[address], - ) - }, - [chainAddressEligibility, ownedAddresses], - ) - - const evmLsdStakingTokenEligible = useCallback( - ({ token, addresses }: { token: Token; addresses: Address[] }) => { - const addressesEligible = chainAddressEligibility[token.id] - return addresses.some( - (address) => ownedAddresses.includes(address) && addressesEligible[address], - ) - }, - [chainAddressEligibility, ownedAddresses], - ) - - return { evmLsdStakingAddressesEligible, evmLsdStakingTokenEligible } -} - -export const useStakingBanner = () => { - const { evmLsdStakingAddressesEligible, evmLsdStakingTokenEligible } = - useEvmLsdStakingEligibility() - const [hideBannerSetting] = useAppState("hideStakingBanner") - - const { t } = useTranslation() - - const dismissStakingBanner = useCallback( - (chainId?: StakingSupportedChain) => - appStore.mutate((existing) => { - const newValue = chainId ? [chainId] : STAKING_BANNER_CHAINS - return { - ...existing, - hideStakingBanner: Array.from(new Set([...existing.hideStakingBanner, ...newValue])), - } - }), - [], - ) - - const showTokenStakingBanner = useCallback( - ({ token, addresses }: { token?: Token; addresses: Address[] }) => { - if (!token) return false - let result = false - const lsdTokenBases = Object.values(EVM_LSD_PAIRS) - .flatMap((pairInfo) => Object.values(pairInfo)) - .map(({ base }) => base) - - if (lsdTokenBases.includes(token.id)) - result = evmLsdStakingTokenEligible({ token, addresses }) - - return ( - result && - !hideBannerSetting.includes( - (token?.chain?.id || token?.evmNetwork?.id) as StakingSupportedChain, - ) - ) - }, - [evmLsdStakingTokenEligible, hideBannerSetting], - ) - - const showStakingBanner = useCallback( - ({ addresses }: { addresses: Address[] }) => - evmLsdStakingAddressesEligible({ addresses }) && - hideBannerSetting.length < STAKING_BANNER_CHAINS.length, - [evmLsdStakingAddressesEligible, hideBannerSetting], - ) - - const getStakingMessage = useCallback( - ({ token }: { token?: Token }) => { - if (!token) return - const lsdTokenBases = Object.values(EVM_LSD_PAIRS) - .flatMap((pairInfo) => Object.values(pairInfo)) - .map(({ base }) => base) - - if (lsdTokenBases.includes(token.id)) - return t("This balance is eligible for Liquid Staking via the Talisman Portal.") - else if (token?.chain?.id && isNomPoolChain(token.chain.id)) - return t("This balance is eligible for Nomination Pool Staking via the Talisman Portal.") - return - }, - [t], - ) - - const getBannerColours = useCallback(({ token }: { token?: Token }) => { - const chainId = token?.chain?.id || token?.evmNetwork?.id - if (chainId && isStakingSupportedChain(chainId)) { - return colours[chainId as StakingSupportedChain] - } - return - }, []) - - return { - showStakingBanner, - showTokenStakingBanner, - dismissStakingBanner, - getStakingMessage, - getBannerColours, - } -} diff --git a/apps/extension/src/ui/hooks/__tests__/useAppState.spec.ts b/apps/extension/src/ui/hooks/__tests__/useAppState.spec.ts index 8953128cc0..c81f2cfb8a 100644 --- a/apps/extension/src/ui/hooks/__tests__/useAppState.spec.ts +++ b/apps/extension/src/ui/hooks/__tests__/useAppState.spec.ts @@ -53,10 +53,3 @@ test("Can get onboarded appState data", async () => { }) await waitFor(() => expect(result.current[0]).toBe(DEFAULT_APP_STATE.onboarded)) }) - -test("Can get showStakingBanner appState data", async () => { - const { result } = renderHook(() => useAppState("hideStakingBanner"), { - wrapper: TestWrapper, - }) - await waitFor(() => expect(result.current[0]).toStrictEqual(DEFAULT_APP_STATE.hideStakingBanner)) -}) diff --git a/apps/extension/src/ui/hooks/useAllActiveNetworkIds.ts b/apps/extension/src/ui/hooks/useAllActiveNetworkIds.ts new file mode 100644 index 0000000000..b93cfffb18 --- /dev/null +++ b/apps/extension/src/ui/hooks/useAllActiveNetworkIds.ts @@ -0,0 +1,12 @@ +import { useMemo } from "react" + +import { useChains, useEvmNetworks } from "@ui/state" + +export const useActiveAssetDiscoveryNetworkIds = () => { + const evmNetworks = useEvmNetworks({ activeOnly: true, includeTestnets: false }) + const chains = useChains({ activeOnly: true, includeTestnets: false }) + return useMemo( + () => evmNetworks.map((n) => n.id).concat(chains.map((n) => n.id)), + [chains, evmNetworks], + ) +} diff --git a/apps/extension/src/ui/hooks/useNetworkInfo.ts b/apps/extension/src/ui/hooks/useNetworkInfo.ts index 44edcb0c29..d7440a3116 100644 --- a/apps/extension/src/ui/hooks/useNetworkInfo.ts +++ b/apps/extension/src/ui/hooks/useNetworkInfo.ts @@ -11,25 +11,23 @@ export type NetworkInfoProps = { } export const getNetworkInfo = (t: TFunction, { chain, evmNetwork, relay }: NetworkInfoProps) => { - if (evmNetwork) - return { - label: evmNetwork.name, - type: evmNetwork.isTestnet ? t("EVM Testnet") : t("EVM Blockchain"), - } + if (evmNetwork) { + const label = evmNetwork.name + return { label, type: evmNetwork.isTestnet ? t("EVM Testnet") : t("EVM Blockchain") } + } if (chain) { - if (chain.isTestnet) return { label: chain.name, type: t("Testnet") } - if (chain.paraId) - return { - label: chain.name, - type: relay?.chainName - ? t("{{name}} Parachain", { name: relay?.chainName }) - : t("Parachain"), + const label = chain.name + const type = (() => { + if (chain.isTestnet) return t("Testnet") + if (chain.paraId) { + if (relay?.name) return t("{{name}} Parachain", { name: relay?.name }) + return t("Parachain") } - return { - label: chain.name, - type: (chain.parathreads || []).length > 0 ? t("Relay Chain") : t("Blockchain"), - } + return (chain.parathreads || []).length > 0 ? t("Relay Chain") : t("Blockchain") + })() + + return { label, type } } return { label: "", type: "" } diff --git a/apps/extension/src/ui/state/assetDiscovery.ts b/apps/extension/src/ui/state/assetDiscovery.ts index 4e033ad333..d7640625a3 100644 --- a/apps/extension/src/ui/state/assetDiscovery.ts +++ b/apps/extension/src/ui/state/assetDiscovery.ts @@ -1,21 +1,17 @@ import { bind } from "@react-rxjs/core" import { liveQuery } from "dexie" -import { assetDiscoveryStore, db, DiscoveredBalance } from "extension-core" +import { assetDiscoveryStore, db } from "extension-core" import groupBy from "lodash/groupBy" import sortBy from "lodash/sortBy" -import { combineLatest, from, map, Observable, shareReplay, throttleTime } from "rxjs" +import { combineLatest, from, map, shareReplay, throttleTime } from "rxjs" import { getTokensMap$ } from "./chaindata" -import { debugObservable } from "./util/debugObservable" -// TODO return dexie observable directly ? -export const assetDiscoveryBalances$ = new Observable((subscriber) => { - const sub = from(liveQuery(() => db.assetDiscovery.toArray())) - .pipe(throttleTime(500, undefined, { leading: true, trailing: true })) - .subscribe(subscriber) - - return () => sub.unsubscribe() -}).pipe(debugObservable("assetDiscoveryBalances$"), shareReplay(1)) +// debounced to prevent hammering coingecko api +const assetDiscoveryBalances$ = from(liveQuery(() => db.assetDiscovery.toArray())).pipe( + throttleTime(500, undefined, { leading: true, trailing: true }), + shareReplay(1), +) export const [useAssetDiscoveryScan, assetDiscoveryScan$] = bind(assetDiscoveryStore.observable) @@ -27,11 +23,11 @@ export const [useAssetDiscoveryScanProgress, assetDiscoveryScanProgress$] = bind ]).pipe( map(([scan, balances, tokensMap]) => { const { - currentScanId, + currentScanScope, currentScanProgressPercent: percent, - currentScanAccounts, currentScanTokensCount, lastScanAccounts, + lastScanNetworks, lastScanTokensCount, } = scan @@ -42,9 +38,13 @@ export const [useAssetDiscoveryScanProgress, assetDiscoveryScanProgress$] = bind (tokenId) => tokensMap[tokenId]?.symbol, ) - const isInProgress = !!currentScanId - const accounts = isInProgress ? currentScanAccounts : lastScanAccounts + const isInProgress = !!currentScanScope + + const accounts = isInProgress ? currentScanScope.addresses : lastScanAccounts const tokensCount = isInProgress ? currentScanTokensCount : lastScanTokensCount + const networksCount = isInProgress + ? currentScanScope?.networkIds.length + : lastScanNetworks.length return { isInProgress, @@ -54,6 +54,7 @@ export const [useAssetDiscoveryScanProgress, assetDiscoveryScanProgress$] = bind tokensCount, accounts, accountsCount: accounts.length, + networksCount, tokenIds, } }), diff --git a/apps/extension/src/ui/state/index.ts b/apps/extension/src/ui/state/index.ts index 70a612f71c..4b35a05f30 100644 --- a/apps/extension/src/ui/state/index.ts +++ b/apps/extension/src/ui/state/index.ts @@ -13,7 +13,6 @@ export * from "./chaindata" export * from "./remoteConfig" export * from "./requests" export * from "./settings" -export * from "./stakingBanner" export * from "./tokenRates" export * from "./transactions" export * from "./browser" diff --git a/apps/extension/src/ui/state/stakingBanner.ts b/apps/extension/src/ui/state/stakingBanner.ts deleted file mode 100644 index 72fdcd55fa..0000000000 --- a/apps/extension/src/ui/state/stakingBanner.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { bind } from "@react-rxjs/core" -import { stakingBannerStore } from "extension-core" - -import { debugObservable } from "./util/debugObservable" - -export const [useStakingBannerStore, stakingBannerStore$] = bind( - stakingBannerStore.observable.pipe(debugObservable("stakingBannerStore$")), -) diff --git a/apps/extension/src/ui/util/scaleApi/sapi.ts b/apps/extension/src/ui/util/scaleApi/sapi.ts index c67fe5f036..d1246fd1bc 100644 --- a/apps/extension/src/ui/util/scaleApi/sapi.ts +++ b/apps/extension/src/ui/util/scaleApi/sapi.ts @@ -355,7 +355,8 @@ const getFeeEstimate = async ( "query_info", [binary, bytes.length], ) - if (!result?.partial_fee) { + // Do not throw if partialFee is 0n. This is a valid response, eg: Bittensor remove_stake fee estimation is 0n. + if (!result?.partial_fee && result.partial_fee !== 0n) { throw new Error("partialFee is not found") } return result.partial_fee diff --git a/apps/extension/webpack/utils.js b/apps/extension/webpack/utils.js index 47740bada3..22edb185bb 100644 --- a/apps/extension/webpack/utils.js +++ b/apps/extension/webpack/utils.js @@ -113,14 +113,6 @@ const updateManifestDetails = async (env, manifest) => { else if (env.build === "canary") { manifest.name = `${manifest.name} - Canary` manifest.action.default_title = `${manifest.action.default_title} - Canary` - - for (const key in manifest.icons) { - const filename = manifest.icons[key] - const name = filename.split(".").slice(0, -1).join() - const extension = filename.split(".").slice(-1).join() - - manifest.icons[key] = `${name}-canary.${extension}` - } } return { ...manifest, ...browserSpecificManifestDetails } diff --git a/apps/extension/webpack/webpack.common.js b/apps/extension/webpack/webpack.common.js index 342717edeb..55ca84ecd6 100644 --- a/apps/extension/webpack/webpack.common.js +++ b/apps/extension/webpack/webpack.common.js @@ -176,6 +176,12 @@ const config = (env) => ({ "process.env.COINGECKO_API_KEY_VALUE": JSON.stringify( env.build === "dev" ? process.env.COINGECKO_API_KEY_VALUE || "" : "", ), + "process.env.TAOSTATS_API_KEY": JSON.stringify( + env.build === "dev" ? process.env.TAOSTATS_API_KEY || "" : "", + ), + "process.env.TAOSTATS_BASE_PATH": JSON.stringify( + env.build === "dev" ? process.env.TAOSTATS_BASE_PATH || "" : "", + ), "process.env.BLOWFISH_BASE_PATH": JSON.stringify( env.build === "dev" ? process.env.BLOWFISH_BASE_PATH || "" : "", ), diff --git a/packages/balances/src/modules/SubstrateNativeModule/util/balanceLockTypes.ts b/packages/balances/src/modules/SubstrateNativeModule/util/balanceLockTypes.ts index 938637194b..bca68dfb7a 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/util/balanceLockTypes.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/util/balanceLockTypes.ts @@ -95,7 +95,7 @@ export const getLockTitle = ( } if (lock.label === "nompools-staking") return "Pooled Staking" if (lock.label === "nompools-unbonding") return "Pooled Staking" - if (lock.label === "subtensor-staking") return "Delegate Staking" + if (lock.label === "subtensor-staking") return "Delegated Staking" if (lock.label === "dapp-staking") return "DApp Staking" if (lock.label === "fees") return "Locked (Fees)" if (lock.label === "misc") return "Locked" diff --git a/packages/chaindata-provider/src/init/chains.ts b/packages/chaindata-provider/src/init/chains.ts index d1b6a321ba..5c35ce6a58 100644 --- a/packages/chaindata-provider/src/init/chains.ts +++ b/packages/chaindata-provider/src/init/chains.ts @@ -130,11 +130,6 @@ export const chains = [ paraId: 2091, name: "Frequency", }, - { - id: "geminis", - paraId: 2038, - name: "Geminis", - }, { id: "hashed", paraId: 2093, @@ -210,16 +205,6 @@ export const chains = [ paraId: 2026, name: "Nodle", }, - { - id: "oak", - paraId: 2090, - name: "Oak", - }, - { - id: "omnibtc", - paraId: 2053, - name: "OmniBTC", - }, { id: "peaq", paraId: 3338, @@ -280,11 +265,6 @@ export const chains = [ paraId: 2025, name: "Sora", }, - { - id: "subdao", - paraId: 2018, - name: "SubDAO", - }, { id: "subsocial-polkadot", paraId: 2101, @@ -474,11 +454,6 @@ export const chains = [ paraId: 2092, name: "Kintsugi", }, - { - id: "kpron", - paraId: 2019, - name: "Kpron", - }, { id: "kreivo", paraId: 2281, @@ -534,11 +509,6 @@ export const chains = [ paraId: 2048, name: "Robonomics Kusama", }, - { - id: "sakura", - paraId: 2016, - name: "Sakura", - }, { id: "shadow-kusama", paraId: 2012, @@ -768,7 +738,7 @@ export const chains = [ id: "polkadot-asset-hub", isTestnet: false, isDefault: true, - sortIndex: 754, + sortIndex: 753, genesisHash: "0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f", prefix: 0, name: "Polkadot Asset Hub", @@ -801,6 +771,9 @@ export const chains = [ { id: "polkadot-asset-hub-substrate-assets-17-wifd", }, + { + id: "polkadot-asset-hub-substrate-assets-555-game", + }, { id: "polkadot-asset-hub-substrate-assets-690-bork", }, @@ -927,6 +900,11 @@ export const chains = [ symbol: "WIFD", logo: "https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/wifd.svg", }, + { + assetId: 555, + symbol: "GAME", + logo: "https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/game.svg", + }, { assetId: 690, symbol: "BORK", @@ -1100,7 +1078,7 @@ export const chains = [ id: "polkadot-bridge-hub", isTestnet: false, isDefault: true, - sortIndex: 755, + sortIndex: 754, genesisHash: "0xdcf691b5a3fbe24adc99ddc959c0561b973e329b1aef4c4b22e7bb2ddecb4464", prefix: 0, name: "Polkadot Bridge Hub", diff --git a/packages/chaindata-provider/src/init/evm-networks.ts b/packages/chaindata-provider/src/init/evm-networks.ts index f0864681ca..519a1dd247 100644 --- a/packages/chaindata-provider/src/init/evm-networks.ts +++ b/packages/chaindata-provider/src/init/evm-networks.ts @@ -2,7 +2,7 @@ export const evmNetworks = [ { id: "1", isTestnet: false, - sortIndex: 322, + sortIndex: 324, name: "Ethereum Mainnet", themeColor: "#62688f", logo: "https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/chains/1.svg", diff --git a/packages/chaindata-provider/src/init/mini-metadatas.ts b/packages/chaindata-provider/src/init/mini-metadatas.ts index 786db448aa..9024c53501 100644 --- a/packages/chaindata-provider/src/init/mini-metadatas.ts +++ b/packages/chaindata-provider/src/init/mini-metadatas.ts @@ -233,13 +233,13 @@ export const miniMetadatas = [ extra: '{"isTestnet":false}', }, { - id: "da272377c17e5711", + id: "120cbbbf5bf31633", source: "substrate-assets", chainId: "polkadot-asset-hub", specName: "statemint", specVersion: "1003004", balancesConfig: - '{"tokens":[{"assetId":1337,"symbol":"USDC","coingeckoId":"usd-coin","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/usdc.svg"},{"assetId":1984,"symbol":"USDT","coingeckoId":"tether","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/usdt.svg"},{"assetId":18,"symbol":"DOTA","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/dota.svg"},{"assetId":23,"symbol":"PINK","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/pink.svg"},{"assetId":30,"symbol":"DED","coingeckoId":"dot-is-ded","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/ded.svg"},{"assetId":17,"symbol":"WIFD","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/wifd.svg"},{"assetId":690,"symbol":"BORK","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/bork.svg"},{"assetId":31337,"symbol":"WUD","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/wud.webp"}]}', + '{"tokens":[{"assetId":1337,"symbol":"USDC","coingeckoId":"usd-coin","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/usdc.svg"},{"assetId":1984,"symbol":"USDT","coingeckoId":"tether","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/usdt.svg"},{"assetId":18,"symbol":"DOTA","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/dota.svg"},{"assetId":23,"symbol":"PINK","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/pink.svg"},{"assetId":30,"symbol":"DED","coingeckoId":"dot-is-ded","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/ded.svg"},{"assetId":17,"symbol":"WIFD","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/wifd.svg"},{"assetId":555,"symbol":"GAME","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/game.svg"},{"assetId":690,"symbol":"BORK","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/bork.svg"},{"assetId":31337,"symbol":"WUD","logo":"https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/wud.webp"}]}', version: 15, data: "0x6d6574610f40000c1c73705f636f72651863727970746f2c4163636f756e7449643332000004000401205b75383b2033325d0000040000032000000008000800000503000c00000505001000000507001400000500001800000208001c0000040000200c3470616c6c65745f61737365747314747970657330417373657444657461696c730c1c42616c616e63650110244163636f756e7449640100384465706f73697442616c616e63650110003001146f776e65720001244163636f756e7449640001186973737565720001244163636f756e74496400011461646d696e0001244163636f756e74496400011c667265657a65720001244163636f756e744964000118737570706c7910011c42616c616e636500011c6465706f7369741001384465706f73697442616c616e636500012c6d696e5f62616c616e636510011c42616c616e636500013469735f73756666696369656e74140110626f6f6c0001206163636f756e74730c010c75333200012c73756666696369656e74730c010c753332000124617070726f76616c730c010c75333200011873746174757324012c41737365745374617475730000240c3470616c6c65745f6173736574731474797065732c417373657453746174757300010c104c6976650000001846726f7a656e0001002844657374726f79696e670002000028000004080c00002c0c3470616c6c65745f6173736574731474797065733041737365744163636f756e74101c42616c616e63650110384465706f73697442616c616e63650110144578747261011c244163636f756e74496401000010011c62616c616e636510011c42616c616e63650001187374617475733001344163636f756e74537461747573000118726561736f6e3401a84578697374656e6365526561736f6e3c4465706f73697442616c616e63652c204163636f756e7449643e00011465787472611c011445787472610000300c3470616c6c65745f617373657473147479706573344163636f756e7453746174757300010c184c69717569640000001846726f7a656e0001001c426c6f636b656400020000340c3470616c6c65745f6173736574731474797065733c4578697374656e6365526561736f6e081c42616c616e63650110244163636f756e7449640100011420436f6e73756d65720000002853756666696369656e740001002c4465706f73697448656c64040010011c42616c616e63650002003c4465706f736974526566756e6465640003002c4465706f73697446726f6d08000001244163636f756e744964000010011c42616c616e636500040000380c3470616c6c65745f6173736574731474797065733441737365744d6574616461746108384465706f73697442616c616e6365011034426f756e646564537472696e67013c0014011c6465706f7369741001384465706f73697442616c616e63650001106e616d653c0134426f756e646564537472696e6700011873796d626f6c3c0134426f756e646564537472696e67000120646563696d616c73080108753800012469735f66726f7a656e140110626f6f6c00003c0c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004001801185665633c543e0000041841737365747301184173736574730c144173736574000104020c20040004542044657461696c73206f6620616e2061737365742e1c4163636f756e740001080202282c040004e42054686520686f6c64696e6773206f662061207370656369666963206163636f756e7420666f7220612073706563696669632061737365742e204d65746164617461010104020c385000000000000000000000000000000000000000000458204d65746164617461206f6620616e2061737365742e0000000032000400000000004d070000000000", extra: '{"isTestnet":false}', diff --git a/packages/chaindata-provider/src/init/tokens.ts b/packages/chaindata-provider/src/init/tokens.ts index 53a15c0ef5..27e015e7e0 100644 --- a/packages/chaindata-provider/src/init/tokens.ts +++ b/packages/chaindata-provider/src/init/tokens.ts @@ -196,6 +196,21 @@ export const tokens = [ id: "polkadot-asset-hub", }, }, + { + id: "polkadot-asset-hub-substrate-assets-555-game", + type: "substrate-assets", + isTestnet: false, + isDefault: true, + symbol: "GAME", + decimals: 10, + logo: "https://raw.githubusercontent.com/TalismanSociety/chaindata/main/assets/tokens/game.svg", + existentialDeposit: "100000", + assetId: "555", + isFrozen: false, + chain: { + id: "polkadot-asset-hub", + }, + }, { id: "polkadot-asset-hub-substrate-assets-690-bork", type: "substrate-assets", diff --git a/packages/extension-core/src/domains/app/popupSummaries.ts b/packages/extension-core/src/domains/app/popupSummaries.ts index f0029d340c..d45462e335 100644 --- a/packages/extension-core/src/domains/app/popupSummaries.ts +++ b/packages/extension-core/src/domains/app/popupSummaries.ts @@ -1,6 +1,3 @@ import { trackBalanceTotals } from "../balances/utils" -import { trackStakingBannerDisplay } from "../staking/utils" -export const trackPopupSummaryData = async () => { - return Promise.all([trackBalanceTotals(), trackStakingBannerDisplay()]) -} +export const trackPopupSummaryData = trackBalanceTotals diff --git a/packages/extension-core/src/domains/app/store.app.ts b/packages/extension-core/src/domains/app/store.app.ts index e765dbebff..e4d35bae5a 100644 --- a/packages/extension-core/src/domains/app/store.app.ts +++ b/packages/extension-core/src/domains/app/store.app.ts @@ -4,7 +4,6 @@ import { gt } from "semver" import { GeneralReport } from "../../libs/GeneralReport" import { migratePasswordV2ToV1 } from "../../libs/migrations/legacyMigrations" import { StorageProvider } from "../../libs/Store" -import { StakingSupportedChain } from "../staking/types" import { TalismanNotOnboardedError } from "./utils" type ONBOARDED_TRUE = "TRUE" @@ -15,6 +14,11 @@ const FALSE: ONBOARDED_FALSE = "FALSE" const UNKNOWN: ONBOARDED_UNKNOWN = "UNKNOWN" export type OnboardedType = ONBOARDED_TRUE | ONBOARDED_FALSE | ONBOARDED_UNKNOWN +export type BlockNumberByDelegator = { + [delegator: string | number]: number +} + +export type DelegatorsBlockNumberByAccount = Record export type AppStoreData = { onboarded: OnboardedType @@ -25,16 +29,14 @@ export type AppStoreData = { analyticsReport?: GeneralReport hideBackupWarningUntil?: number hasSpiritKey: boolean - hideStakingBanner: StakingSupportedChain[] needsSpiritKeyUpdate: boolean popupSizeDelta: [number, number] vaultVerifierCertificateMnemonicId?: string | null - showAssetDiscoveryAlert?: boolean - dismissedAssetDiscoveryAlertScanId?: string isAssetDiscoveryScanPending?: boolean showLedgerPolkadotGenericMigrationAlert?: boolean hideManageAccountsWelcome?: boolean hideGetStarted?: boolean + bittensorUnbondBlockNumber: DelegatorsBlockNumberByAccount } const ANALYTICS_VERSION = "1.5.0" @@ -48,10 +50,9 @@ export const DEFAULT_APP_STATE: AppStoreData = { analyticsRequestShown: gt(process.env.VERSION!, ANALYTICS_VERSION), // assume user has onboarded with analytics if current version is newer hasSpiritKey: false, needsSpiritKeyUpdate: false, - hideStakingBanner: [], popupSizeDelta: [0, IS_FIREFOX ? 30 : 0], - showAssetDiscoveryAlert: false, showLedgerPolkadotGenericMigrationAlert: false, + bittensorUnbondBlockNumber: {}, } export class AppStore extends StorageProvider { @@ -110,7 +111,6 @@ if (DEBUG) { hideBraveWarning: false, hasBraveWarningBeenShown: false, analyticsRequestShown: false, - hideStakingBanner: [], hideBackupWarningUntil: undefined, hideManageAccountsWelcome: false, hideGetStarted: false, diff --git a/packages/extension-core/src/domains/app/store.remoteConfig.ts b/packages/extension-core/src/domains/app/store.remoteConfig.ts index 4a3b7354f3..acfc9c173b 100644 --- a/packages/extension-core/src/domains/app/store.remoteConfig.ts +++ b/packages/extension-core/src/domains/app/store.remoteConfig.ts @@ -20,6 +20,12 @@ export const DEFAULT_REMOTE_CONFIG: RemoteConfigStoreData = { // "vara-testnet": [1], // "aleph-zero-testnet": [1], }, + stakingPools: { + // uncomment for testing on testnets + // "avail-turing-testnet": [1], + // "vara-testnet": [1], + // "aleph-zero-testnet": [1], + }, } const CONFIG_TIMEOUT = 30 * 60 * 1000 // 30 minutes diff --git a/packages/extension-core/src/domains/app/types.ts b/packages/extension-core/src/domains/app/types.ts index a8d39f022f..9dd41fde87 100644 --- a/packages/extension-core/src/domains/app/types.ts +++ b/packages/extension-core/src/domains/app/types.ts @@ -15,6 +15,7 @@ export type RemoteConfigStoreData = { apiKeyValue?: string } nominationPools: Record + stakingPools: Record postHogUrl: string } diff --git a/packages/extension-core/src/domains/assetDiscovery/autoEnable.ts b/packages/extension-core/src/domains/assetDiscovery/autoEnable.ts deleted file mode 100644 index fd78187122..0000000000 --- a/packages/extension-core/src/domains/assetDiscovery/autoEnable.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EvmNetwork, EvmNetworkId } from "@talismn/chaindata-provider" -import { liveQuery } from "dexie" -import { log } from "extension-shared" -import { uniq } from "lodash" -import { BehaviorSubject, combineLatest, distinctUntilChanged } from "rxjs" - -import { db } from "../../db" -import { chaindataProvider } from "../../rpcs/chaindata" -import { isEvmToken } from "../../util/isEvmToken" -import { activeEvmNetworksStore } from "../ethereum/store.activeEvmNetworks" -import { activeTokensStore } from "../tokens/store.activeTokens" -import { assetDiscoveryStore } from "./store" - -const isEnabled$ = new BehaviorSubject(false) - -export const setAutoEnableDiscoveredAssets = (enable: boolean) => { - isEnabled$.next(enable) -} - -isEnabled$.pipe(distinctUntilChanged()).subscribe((isEnabled) => { - if (!isEnabled) return - - return combineLatest([ - assetDiscoveryStore.observable, - liveQuery(() => db.assetDiscovery.toArray()), - activeTokensStore.observable, - activeEvmNetworksStore.observable, - ]).subscribe(async ([assetDiscovery, discoveredBalances, activeTokens, activeEvmNetworks]) => { - try { - // exit if a scan is active, to avoid restarting balance subscription too often - if (assetDiscovery.currentScanId) return - - const tokenIds = uniq(discoveredBalances.map((entry) => entry.tokenId)) - const tokens = ( - await Promise.all(tokenIds.map((tokenId) => chaindataProvider.tokenById(tokenId))) - ).filter(isEvmToken) - - const evmNetworkIds = uniq( - tokens.map((token) => token.evmNetwork?.id).filter((id): id is EvmNetworkId => !!id), - ) - const evmNetworks = ( - await Promise.all(evmNetworkIds.map((id) => chaindataProvider.evmNetworkById(id))) - ).filter((network): network is EvmNetwork => !!network) - - // activate tokens that have not been explicitely disabled - for (const token of tokens) - if (activeTokens[token.id] === undefined) { - log.debug("[AssetDiscovery] Automatically enabling discovered asset", { token }) - activeTokensStore.setActive(token.id, true) - } - - // activate networks that have not been explicitely disabled - for (const evmNetwork of evmNetworks) - if (activeEvmNetworks[evmNetwork.id] === undefined) { - log.debug("[AssetDiscovery] Automatically enabling discovered network", { evmNetwork }) - activeEvmNetworksStore.setActive(evmNetwork.id, true) - } - } catch (err) { - log.error("[AssetDiscovery] Failed to automatically enable discovered assets", { - err, - discoveredBalances, - }) - } - }) -}) diff --git a/packages/extension-core/src/domains/assetDiscovery/handler.ts b/packages/extension-core/src/domains/assetDiscovery/handler.ts index eac9dc2668..09a697e9da 100644 --- a/packages/extension-core/src/domains/assetDiscovery/handler.ts +++ b/packages/extension-core/src/domains/assetDiscovery/handler.ts @@ -2,7 +2,7 @@ import { ExtensionHandler } from "../../libs/Handler" import { updateAndWaitForUpdatedChaindata } from "../../rpcs/mini-metadata-updater" import { MessageTypes, RequestTypes, ResponseType } from "../../types" import { assetDiscoveryScanner } from "./scanner" -import { RequestAssetDiscoveryStartScan } from "./types" +import { AssetDiscoveryScanScope } from "./types" export class AssetDiscoveryHandler extends ExtensionHandler { public async handle( @@ -13,7 +13,7 @@ export class AssetDiscoveryHandler extends ExtensionHandler { switch (type) { case "pri(assetDiscovery.scan.start)": await updateAndWaitForUpdatedChaindata({ updateSubstrateChains: false }) - return assetDiscoveryScanner.startScan(request as RequestAssetDiscoveryStartScan) + return assetDiscoveryScanner.startScan(request as AssetDiscoveryScanScope, true) case "pri(assetDiscovery.scan.stop)": return assetDiscoveryScanner.stopScan() diff --git a/packages/extension-core/src/domains/assetDiscovery/migrations/index.ts b/packages/extension-core/src/domains/assetDiscovery/migrations/index.ts index 6dbbe193ca..b9eb872837 100644 --- a/packages/extension-core/src/domains/assetDiscovery/migrations/index.ts +++ b/packages/extension-core/src/domains/assetDiscovery/migrations/index.ts @@ -1,5 +1,7 @@ +import { db } from "../../../db" import { Migration, MigrationFunction } from "../../../libs/migrations/types" import { appStore } from "../../app/store.app" +import { assetDiscoveryStore } from "../store" // purpose of this migration is to run an initial scan on existing accounts, when the feature is rolled out export const migrateAssetDiscoveryRollout: Migration = { @@ -9,3 +11,14 @@ export const migrateAssetDiscoveryRollout: Migration = { await appStore.set({ isAssetDiscoveryScanPending: true }) }), } + +// purpose of this migration is clear existing stores due to property changes, and start a scan +export const migrateAssetDiscoveryV2: Migration = { + forward: new MigrationFunction(async () => { + await db.assetDiscovery.clear() + await assetDiscoveryStore.reset() + // we can't start a scan right away because chaindata will only fetch new tokens on first front end subscription + // => flag that a scan is pending, and start it as soon as new tokens are fetched + await appStore.set({ isAssetDiscoveryScanPending: true }) + }), +} diff --git a/packages/extension-core/src/domains/assetDiscovery/scanner.ts b/packages/extension-core/src/domains/assetDiscovery/scanner.ts index c576621f61..3f36c2f037 100644 --- a/packages/extension-core/src/domains/assetDiscovery/scanner.ts +++ b/packages/extension-core/src/domains/assetDiscovery/scanner.ts @@ -2,13 +2,14 @@ import keyring from "@polkadot/ui-keyring" import PromisePool from "@supercharge/promise-pool" import { erc20Abi, erc20BalancesAggregatorAbi, EvmErc20Token } from "@talismn/balances" import { abiMulticall } from "@talismn/balances/src/modules/abis/multicall" -import { EvmNetworkId, Token, TokenId, TokenList } from "@talismn/chaindata-provider" +import { EvmNetwork, EvmNetworkId, Token, TokenId, TokenList } from "@talismn/chaindata-provider" import { isEthereumAddress, throwAfter } from "@talismn/util" import { log } from "extension-shared" +import { isEqual, uniq } from "lodash" import chunk from "lodash/chunk" import groupBy from "lodash/groupBy" import sortBy from "lodash/sortBy" -import { firstValueFrom, map } from "rxjs" +import { combineLatest, debounceTime, distinctUntilKeyChanged, skip } from "rxjs" import { PublicClient } from "viem" import { sentry } from "../../config/sentry" @@ -18,13 +19,11 @@ import { chaindataProvider } from "../../rpcs/chaindata" import { awaitKeyringLoaded } from "../../util/awaitKeyringLoaded" import { isEvmToken } from "../../util/isEvmToken" import { appStore } from "../app/store.app" -import { settingsStore } from "../app/store.settings" import { activeEvmNetworksStore, isEvmNetworkActive } from "../ethereum/store.activeEvmNetworks" import { EvmAddress } from "../ethereum/types" import { activeTokensStore, isTokenActive } from "../tokens/store.activeTokens" -import { setAutoEnableDiscoveredAssets } from "./autoEnable" -import { assetDiscoveryStore } from "./store" -import { AssetDiscoveryMode, DiscoveredBalance, RequestAssetDiscoveryStartScan } from "./types" +import { AssetDiscoveryScanState, assetDiscoveryStore } from "./store" +import { AssetDiscoveryScanScope, DiscoveredBalance } from "./types" // TODO - flag these tokens as ignored from chaindata const IGNORED_COINGECKO_IDS = [ @@ -53,257 +52,352 @@ const getSortableIdentifier = (tokenId: TokenId, address: string, tokens: TokenL } class AssetDiscoveryScanner { - // as scan can be both manually started and resumed on startup (2 triggers), - // track resumed scans to prevent them from running twice simultaneously - #resumedScans: string[] = [] + #isBusy = false + #preventAutoStart = false constructor() { - this.init() + this.watchNewAccounts() + this.watchEnabledNetworks() + this.resume() } - private async init(): Promise { - setTimeout(async () => { - this.resumeScan() - // resume after 15 sec to not interfere with other startup routines - }, 15_000) - } + private watchNewAccounts = async () => { + await awaitKeyringLoaded() - public async startScan({ mode, addresses }: RequestAssetDiscoveryStartScan): Promise { - const prevState = await assetDiscoveryStore.get() + let prevAllAddresses: string[] | null = null - if (prevState.currentScanId) { - // if a scan was already in progress it will be cancelled, merge previous and new addresses - addresses = [...prevState.currentScanAccounts, ...(addresses ?? [])] - } + // identify newly added accounts and scan those + keyring.accounts.subject.pipe(debounceTime(500)).subscribe(async (accounts) => { + try { + const allAddresses = Object.keys(accounts) - await awaitKeyringLoaded() - const currentScanAccounts = keyring - .getAccounts() - .filter((acc) => isEthereumAddress(acc.address)) // only scan ethereum accounts, for now - .map((acc) => acc.address) - .filter((address) => !addresses || addresses.includes(address)) + if (prevAllAddresses && !this.#preventAutoStart) { + const addresses = allAddresses.filter((k) => !(prevAllAddresses as string[]).includes(k)) + const networkIds = await getActiveNetworkIdsToScan() - // if no scan-compatible address, exit - if (!currentScanAccounts.length) return false + log.debug("[AssetDiscovery] New accounts detected, starting scan", { + addresses, + networkIds, + }) - // 1. Set scan status in state - await assetDiscoveryStore.set({ - currentScanId: crypto.randomUUID(), - currentScanMode: mode, - currentScanProgressPercent: 0, - currentScanCursors: {}, - currentScanAccounts, - currentScanTokensCount: 0, + this.startScan({ networkIds, addresses }) + } + + prevAllAddresses = allAddresses // update reference + } catch (err) { + log.error("[AssetDiscovery] Failed to start scan after account creation", { err }) + } }) + } - // 2. Clear scan table - await db.assetDiscovery.clear() + private watchEnabledNetworks = async () => { + let prevAllActiveNetworkIds: string[] | null = null - // 3. Inform the user that a scan is in progress - await appStore.set({ - showAssetDiscoveryAlert: true, - dismissedAssetDiscoveryAlertScanId: "", - }) + // identify newly enabled networks and scan those + combineLatest([chaindataProvider.evmNetworksByIdObservable, activeEvmNetworksStore.observable]) + .pipe(debounceTime(500)) + .subscribe(([networksById, activeNetworks]) => { + try { + const allActiveNetworkIds = Object.keys(activeNetworks).filter( + (k) => !!activeNetworks[k] && networksById[k] && !networksById.isTestnet, + ) + + if (prevAllActiveNetworkIds && !this.#preventAutoStart) { + const networkIds = allActiveNetworkIds.filter( + (k) => !(prevAllActiveNetworkIds as string[]).includes(k), + ) + const addresses = keyring.getAccounts().map((acc) => acc.address) + + log.debug("[AssetDiscovery] New enabled networks detected, starting scan", { + addresses, + networkIds, + }) + + this.startScan({ networkIds, addresses }) + } - // 3. Start scan - this.resumeScan() + prevAllActiveNetworkIds = allActiveNetworkIds + } catch (err) { + log.error("[AssetDiscovery] Failed to start scan after active networks list changed", { + err, + }) + } + }) + } + + private resume(): void { + setTimeout(async () => { + this.executeNextScan() + // resume after 5 sec to not interfere with other startup routines + // could be longer but because of MV3 it's better to start asap + }, 5_000) + } + + public async startScan(scope: AssetDiscoveryScanScope, dequeue?: boolean): Promise { + const evmNetworksMap = await chaindataProvider.evmNetworksById() + + // for now we only support ethereum addresses and networks + const addresses = scope.addresses.filter((address) => isEthereumAddress(address)) + const networkIds = scope.networkIds.filter((id) => evmNetworksMap[id]) + if (!addresses.length || !networkIds.length) return false + + log.debug("[AssetDiscovery] Enqueue scan", { addresses, networkIds }) + + // add to queue + await assetDiscoveryStore.mutate((state) => ({ + ...state, + queue: [...(state.queue ?? []), { addresses, networkIds }], + })) + + // for front end calls, dequeue as part of this promise to keep UI in sync + if (dequeue && !this.#isBusy) { + this.#isBusy = true + try { + await this.dequeue() + } finally { + this.#isBusy = false + } + } + + this.executeNextScan() return true } public async stopScan(): Promise { - await assetDiscoveryStore.set({ currentScanId: null }) + await assetDiscoveryStore.set({ + currentScanScope: null, + currentScanProgressPercent: undefined, + currentScanCursors: undefined, + currentScanTokensCount: undefined, + queue: [], + }) + await db.assetDiscovery.clear() return true } - private async resumeScan(): Promise { - const scanId = await assetDiscoveryStore.get("currentScanId") - if (!scanId) return + private async dequeue(): Promise { + const scope = await assetDiscoveryStore.get("currentScanScope") + + if (!scope) { + const queue = await assetDiscoveryStore.get("queue") + if (queue?.length) { + await this.enableDiscoveredTokens() // enable pending discovered tokens before flushing the table + + await db.assetDiscovery.clear() + + await assetDiscoveryStore.mutate((state): AssetDiscoveryScanState => { + const [next, ...others] = state.queue ?? [] + return { + ...state, + currentScanScope: next ?? null, + currentScanProgressPercent: 0, + currentScanTokensCount: 0, + currentScanCursors: {}, + queue: others, + } + }) + } + } + } - // ensure a scan can't run twice in parallel - if (this.#resumedScans.includes(scanId)) return - this.#resumedScans.push(scanId) + private async executeNextScan(): Promise { + if (this.#isBusy) return + this.#isBusy = true - setAutoEnableDiscoveredAssets(true) + const abortController = new AbortController() - const { - currentScanMode: mode, - currentScanAccounts: addresses, - currentScanCursors: cursors, - } = await assetDiscoveryStore.get() + try { + await this.dequeue() - const [allTokens, evmNetworks, activeTokens, activeEvmNetworks, settings] = await Promise.all([ - chaindataProvider.tokens(), - chaindataProvider.evmNetworksById(), - activeTokensStore.get(), - activeEvmNetworksStore.get(), - settingsStore.get(), - ]) + const scope = await assetDiscoveryStore.get("currentScanScope") + if (!scope) return - const tokensMap = Object.fromEntries(allTokens.map((token) => [token.id, token])) - - const tokensToScan = allTokens.filter(isEvmToken).filter((token) => { - const evmNetwork = evmNetworks[token.evmNetwork?.id ?? ""] - if (!evmNetwork) return false - if (!settings.useTestnets && (evmNetwork.isTestnet || token.isTestnet)) return false - if (token.coingeckoId && IGNORED_COINGECKO_IDS.includes(token.coingeckoId)) return false - if (token.noDiscovery) return false - if (mode === AssetDiscoveryMode.ALL_NETWORKS) - return ( - !isEvmNetworkActive(evmNetwork, activeEvmNetworks) || !isTokenActive(token, activeTokens) - ) - return ( - isEvmNetworkActive(evmNetwork, activeEvmNetworks) && !isTokenActive(token, activeTokens) - ) - }) + log.debug("[AssetDiscovery] Scanner proceeding with scan", scope) - const tokensByNetwork: Record = groupBy( - tokensToScan, - (t) => t.evmNetwork?.id, - ) + const { currentScanCursors: cursors } = await assetDiscoveryStore.get() - const totalChecks = tokensToScan.length * addresses.length - const totalTokens = tokensToScan.length + const [allTokens, evmNetworks, activeTokens] = await Promise.all([ + chaindataProvider.tokens(), + chaindataProvider.evmNetworksById(), + activeTokensStore.get(), + ]) - const currentScanId$ = assetDiscoveryStore.observable.pipe(map((state) => state.currentScanId)) + const tokensMap = Object.fromEntries(allTokens.map((token) => [token.id, token])) - const erc20aggregators = Object.fromEntries( - Object.values(evmNetworks) - .filter((n) => n.erc20aggregator) - .map((n) => [n.id, n.erc20aggregator] as const), - ) + const tokensToScan = allTokens + .filter(isEvmToken) + .filter((t) => scope.networkIds.includes(t.evmNetwork?.id ?? "")) + .filter((token) => { + const evmNetwork = evmNetworks[token.evmNetwork?.id ?? ""] + if (!evmNetwork) return false + if (evmNetwork.isTestnet || token.isTestnet) return false + if (token.coingeckoId && IGNORED_COINGECKO_IDS.includes(token.coingeckoId)) return false + if (token.noDiscovery) return false + return !isTokenActive(token, activeTokens) + }) - // process multiple networks at a time - await PromisePool.withConcurrency(MANUAL_SCAN_MAX_CONCURRENT_NETWORK) - .for(Object.keys(tokensByNetwork)) - .process(async (networkId) => { - // stop if scan was cancelled - if ((await firstValueFrom(currentScanId$)) !== scanId) return + await assetDiscoveryStore.mutate((prev) => ({ + ...prev, + currentScanTokensCount: tokensToScan.length, + })) - try { - const client = await chainConnectorEvm.getPublicClientForEvmNetwork(networkId) - if (!client) return - - // build the list of token+address to check balances for - const allChecks = sortBy( - tokensByNetwork[networkId] - .map((t) => addresses.map((a) => ({ tokenId: t.id, type: t.type, address: a }))) - .flat(), - (c) => getSortableIdentifier(c.tokenId, c.address, tokensMap), - ) - let startIndex = 0 + const tokensByNetwork: Record = groupBy( + tokensToScan, + (t) => t.evmNetwork?.id, + ) - // skip checks that were already scanned - if (cursors[networkId]) { - const { tokenId, address } = cursors[networkId] - startIndex = - 1 + allChecks.findIndex((c) => c.tokenId === tokenId && c.address === address) - } + const totalChecks = tokensToScan.length * scope.addresses.length + const totalTokens = tokensToScan.length - const remainingChecks = allChecks.slice(startIndex) + const subScopeChange = assetDiscoveryStore.observable + .pipe(distinctUntilKeyChanged("currentScanScope", isEqual), skip(1)) + .subscribe(() => { + abortController.abort() + subScopeChange.unsubscribe() + }) - //Split into chunks of 50 token+id - const chunkedChecks = chunk(remainingChecks, BALANCES_FETCH_CHUNK_SIZE) + const erc20aggregators = Object.fromEntries( + Object.values(evmNetworks) + .filter((n) => n.erc20aggregator) + .map((n) => [n.id, n.erc20aggregator] as const), + ) - for (const checks of chunkedChecks) { - // stop if scan was cancelled - if ((await firstValueFrom(currentScanId$)) !== scanId) return + // process multiple networks at a time + await PromisePool.withConcurrency(MANUAL_SCAN_MAX_CONCURRENT_NETWORK) + .for(Object.keys(tokensByNetwork)) + .process(async (networkId) => { + // stop if scan was cancelled + if (abortController.signal.aborted) return - const res = await getEvmTokenBalances( - client, - checks.map((c) => ({ - token: tokensMap[c.tokenId], - address: c.address as EvmAddress, - })), - erc20aggregators[networkId], + try { + const client = await chainConnectorEvm.getPublicClientForEvmNetwork(networkId) + if (!client) return + + // build the list of token+address to check balances for + const allChecks = sortBy( + tokensByNetwork[networkId] + .map((t) => + scope.addresses.map((a) => ({ tokenId: t.id, type: t.type, address: a })), + ) + .flat(), + (c) => getSortableIdentifier(c.tokenId, c.address, tokensMap), ) + let startIndex = 0 - // stop if scan was cancelled - if ((await firstValueFrom(currentScanId$)) !== scanId) return - - const newBalances = checks - .map((check, i) => [check, res[i]] as const) - .filter(([, res]) => res !== "0") - .map(([{ address, tokenId }, res]) => ({ - id: getSortableIdentifier(tokenId, address, tokensMap), - tokenId, - address, - balance: res, - })) - - const newState = await assetDiscoveryStore.mutate((prev) => { - if (prev.currentScanId !== scanId) return prev - - const currentScanCursors = { - ...prev.currentScanCursors, - [networkId]: { - address: checks[checks.length - 1].address, - tokenId: checks[checks.length - 1].tokenId, - scanned: (prev.currentScanCursors[networkId]?.scanned ?? 0) + checks.length, - }, - } + // skip checks that were already scanned + if (cursors[networkId]) { + const { tokenId, address } = cursors[networkId] + startIndex = + 1 + allChecks.findIndex((c) => c.tokenId === tokenId && c.address === address) + } - // Update progress - // in case of full scan it takes longer to scan networks - // in case of active scan it takes longer to scan tokens - // => use the min of both ratios as current progress - const totalScanned = Object.values(currentScanCursors).reduce( - (acc, cur) => acc + cur.scanned, - 0, - ) - const tokensProgress = Math.round((100 * totalScanned) / totalChecks) - const networksProgress = Math.round( - (100 * Object.keys(currentScanCursors).length) / - Object.keys(tokensByNetwork).length, - ) - const currentScanProgressPercent = Math.min(tokensProgress, networksProgress) + const remainingChecks = allChecks.slice(startIndex) - return { - ...prev, - currentScanCursors, - currentScanProgressPercent, - currentScanTokensCount: totalTokens, - } - }) + //Split into chunks of 50 token+id + const chunkedChecks = chunk(remainingChecks, BALANCES_FETCH_CHUNK_SIZE) - if (newState.currentScanId !== scanId) return + for (const checks of chunkedChecks) { + // stop if scan was cancelled + if (abortController.signal.aborted) return - if (newBalances.length) { - await db.assetDiscovery.bulkPut(newBalances) + const res = await getEvmTokenBalances( + client, + checks.map((c) => ({ + token: tokensMap[c.tokenId], + address: c.address as EvmAddress, + })), + erc20aggregators[networkId], + ) - // display alert if it has not been explicitely dismissed - // happens if user navigated away from asset discovery screen before a new token is found - const { showAssetDiscoveryAlert, dismissedAssetDiscoveryAlertScanId } = - await appStore.get() - if (!showAssetDiscoveryAlert && dismissedAssetDiscoveryAlertScanId !== scanId) - await appStore.set({ showAssetDiscoveryAlert: true }) + // stop if scan was cancelled + if (abortController.signal.aborted) return + + const newBalances = checks + .map((check, i) => [check, res[i]] as const) + .filter(([, res]) => res !== "0") + .map(([{ address, tokenId }, res]) => ({ + id: getSortableIdentifier(tokenId, address, tokensMap), + tokenId, + address, + balance: res, + })) + + await assetDiscoveryStore.mutate((prev) => { + if (abortController.signal.aborted) return prev + + const currentScanCursors = { + ...prev.currentScanCursors, + [networkId]: { + address: checks[checks.length - 1].address, + tokenId: checks[checks.length - 1].tokenId, + scanned: (prev.currentScanCursors[networkId]?.scanned ?? 0) + checks.length, + }, + } + + // Update progress + // in case of full scan it takes longer to scan networks + // in case of active scan it takes longer to scan tokens + // => use the min of both ratios as current progress + const totalScanned = Object.values(currentScanCursors).reduce( + (acc, cur) => acc + cur.scanned, + 0, + ) + const tokensProgress = Math.round((100 * totalScanned) / totalChecks) + const networksProgress = Math.round( + (100 * Object.keys(currentScanCursors).length) / + Object.keys(tokensByNetwork).length, + ) + const currentScanProgressPercent = Math.min(tokensProgress, networksProgress) + + return { + ...prev, + currentScanCursors, + currentScanProgressPercent, + currentScanTokensCount: totalTokens, + } + }) + + if (abortController.signal.aborted) return + + if (newBalances.length) { + await db.assetDiscovery.bulkPut(newBalances) + } } + } catch (err) { + log.error(`[AssetDiscovery] Could not scan network ${networkId}`, { err }) } - } catch (err) { - log.error(`Could not scan network ${networkId}`, { err }) + }) + + await assetDiscoveryStore.mutate((prev): AssetDiscoveryScanState => { + if (abortController.signal.aborted) return prev + return { + ...prev, + currentScanProgressPercent: 100, + currentScanScope: null, + lastScanTimestamp: Date.now(), + lastScanAccounts: prev.currentScanScope?.addresses ?? [], + lastScanNetworks: prev.currentScanScope?.networkIds ?? [], + lastScanTokensCount: prev.currentScanTokensCount, } }) - await assetDiscoveryStore.mutate((prev) => { - if (prev.currentScanId !== scanId) return prev - return { - ...prev, - currentScanId: null, - currentScanProgressPercent: 100, - currentScanCursors: {}, - currentScanAccounts: [], - lastScanTimestamp: Date.now(), - lastScanAccounts: prev.currentScanAccounts, - lastScanMode: prev.currentScanMode, - lastScanTokensCount: prev.currentScanTokensCount, - status: "idle", - } - }) + subScopeChange.unsubscribe() + + log.debug("[AssetDiscovery] Scan completed", scope) - if ((await db.assetDiscovery.count()) === 0) - await appStore.set({ showAssetDiscoveryAlert: false }) + await this.enableDiscoveredTokens() // if pending tokens to enable, do it now + } catch (cause) { + abortController.abort() + log.error("Error while scanning", { cause }) + } finally { + this.#isBusy = false + } + + // proceed with next scan in queue, if any + this.executeNextScan() } public async startPendingScan(): Promise { @@ -317,9 +411,82 @@ class AssetDiscoveryScanner { .filter((acc) => isEthereumAddress(acc.address)) .map((acc) => acc.address) - await this.startScan({ addresses, mode: AssetDiscoveryMode.ACTIVE_NETWORKS }) + // all active evm networks + const [evmNetworks, activeEvmNetworks] = await Promise.all([ + chaindataProvider.evmNetworks(), + activeEvmNetworksStore.get(), + ]) + + const networkIds = evmNetworks + .filter((n) => isEvmNetworkActive(n, activeEvmNetworks)) + .map((n) => n.id) + + // enqueue scan + this.startScan({ networkIds, addresses }) + await appStore.set({ isAssetDiscoveryScanPending: false }) } + + private async enableDiscoveredTokens(): Promise { + this.#preventAutoStart = true + + try { + const [discoveredBalances, activeEvmNetworks, activeTokens] = await Promise.all([ + db.assetDiscovery.toArray(), + activeEvmNetworksStore.get(), + activeTokensStore.get(), + ]) + + const tokenIds = uniq(discoveredBalances.map((entry) => entry.tokenId)) + const tokens = ( + await Promise.all(tokenIds.map((tokenId) => chaindataProvider.tokenById(tokenId))) + ).filter(isEvmToken) + + const evmNetworkIds = uniq( + tokens.map((token) => token.evmNetwork?.id).filter((id): id is EvmNetworkId => !!id), + ) + const evmNetworks = ( + await Promise.all(evmNetworkIds.map((id) => chaindataProvider.evmNetworkById(id))) + ).filter((network): network is EvmNetwork => !!network) + + // activate tokens that have not been explicitely disabled and that are not default tokens + for (const token of tokens) + if (activeTokens[token.id] === undefined && !isTokenActive(token, activeTokens)) { + log.debug("[AssetDiscovery] Automatically enabling discovered asset", { token }) + activeTokensStore.setActive(token.id, true) + } + + // activate networks that have not been explicitely disabled and that are not default networks + for (const evmNetwork of evmNetworks) + if ( + activeEvmNetworks[evmNetwork.id] === undefined && + !isEvmNetworkActive(evmNetwork, activeEvmNetworks) + ) { + log.debug("[AssetDiscovery] Automatically enabling discovered network", { evmNetwork }) + activeEvmNetworksStore.setActive(evmNetwork.id, true) + } + } catch (err) { + log.error("[AssetDiscovery] Failed to automatically enable discovered assets", { + err, + }) + } + + this.#preventAutoStart = false + } +} + +const getActiveNetworkIdsToScan = async () => { + const [evmNetworks, activeEvmNetworks] = await Promise.all([ + chaindataProvider.evmNetworks(), + activeEvmNetworksStore.get(), + // we dont scan substrate tokens for now + // chaindataProvider.chains(), + // activeChainsStore.get() + ]) + + return evmNetworks + .filter((n) => !n.isTestnet && isEvmNetworkActive(n, activeEvmNetworks)) + .map((n) => n.id) } const getEvmTokenBalance = async (client: PublicClient, token: Token, address: EvmAddress) => { diff --git a/packages/extension-core/src/domains/assetDiscovery/store.ts b/packages/extension-core/src/domains/assetDiscovery/store.ts index 149debcbd6..1239b38014 100644 --- a/packages/extension-core/src/domains/assetDiscovery/store.ts +++ b/packages/extension-core/src/domains/assetDiscovery/store.ts @@ -2,15 +2,13 @@ import { Address } from "@talismn/balances" import { ChainId, EvmNetworkId, TokenId } from "@talismn/chaindata-provider" import { StorageProvider } from "../../libs/Store" -import { AssetDiscoveryMode } from "./types" +import { AssetDiscoveryScanScope } from "./types" export type AssetDiscoveryScanType = "manual" // | "automatic" export type AssetDiscoveryScanState = { - currentScanId: string | null // a non-null value means that a scan is currently running - currentScanMode: AssetDiscoveryMode + currentScanScope: AssetDiscoveryScanScope | null // a non-null value means that a scan is currently running currentScanProgressPercent: number - currentScanAccounts: string[] currentScanTokensCount: number /** * To avoid creating empty balance rows for each token/account couple to track progress, which doesn't scale, we will use cursors : @@ -22,27 +20,31 @@ export type AssetDiscoveryScanState = { > lastScanTimestamp: number lastScanAccounts: string[] + lastScanNetworks: string[] lastScanTokensCount: number - lastScanMode: AssetDiscoveryMode + queue?: AssetDiscoveryScanScope[] // may be undefined for older installs : TODO migration ? } -const DEFAULT_STATE: AssetDiscoveryScanState = { - currentScanId: null, - currentScanMode: AssetDiscoveryMode.ACTIVE_NETWORKS, +export const DEFAULT_STATE: AssetDiscoveryScanState = { + currentScanScope: null, currentScanProgressPercent: 0, - currentScanAccounts: [], currentScanTokensCount: 0, currentScanCursors: {}, lastScanTimestamp: 0, lastScanAccounts: [], + lastScanNetworks: [], lastScanTokensCount: 0, - lastScanMode: AssetDiscoveryMode.ACTIVE_NETWORKS, + queue: [], } class AssetDiscoveryStore extends StorageProvider { constructor() { super("assetDiscovery", DEFAULT_STATE) } + + reset() { + return this.set(DEFAULT_STATE) + } } export const assetDiscoveryStore = new AssetDiscoveryStore() diff --git a/packages/extension-core/src/domains/assetDiscovery/types.ts b/packages/extension-core/src/domains/assetDiscovery/types.ts index 8c0349cc86..1d561cd805 100644 --- a/packages/extension-core/src/domains/assetDiscovery/types.ts +++ b/packages/extension-core/src/domains/assetDiscovery/types.ts @@ -1,5 +1,5 @@ import { Address } from "@talismn/balances" -import { TokenId } from "@talismn/chaindata-provider" +import { ChainId, EvmNetworkId, TokenId } from "@talismn/chaindata-provider" export type DiscoveredBalance = { id: string @@ -8,17 +8,12 @@ export type DiscoveredBalance = { balance: string } -export enum AssetDiscoveryMode { - ALL_NETWORKS = "ALL_NETWORKS", - ACTIVE_NETWORKS = "ACTIVE_NETWORKS", -} - -export type RequestAssetDiscoveryStartScan = { - mode: AssetDiscoveryMode - addresses?: Address[] +export type AssetDiscoveryScanScope = { + networkIds: (EvmNetworkId | ChainId)[] + addresses: Address[] } export interface AssetDiscoveryMessages { - "pri(assetDiscovery.scan.start)": [RequestAssetDiscoveryStartScan, boolean] + "pri(assetDiscovery.scan.start)": [AssetDiscoveryScanScope, boolean] "pri(assetDiscovery.scan.stop)": [null, boolean] } diff --git a/packages/extension-core/src/domains/staking/constants.ts b/packages/extension-core/src/domains/staking/constants.ts deleted file mode 100644 index cc2aa349d3..0000000000 --- a/packages/extension-core/src/domains/staking/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EvmNetworkId } from "@talismn/chaindata-provider" - -import { - EvmLsdSupportedChain, - EvmLsdSupportedPair, - NomPoolSupportedChain, - StakingSupportedChain, -} from "./types" - -export const NOM_POOL_SUPPORTED_CHAINS: NomPoolSupportedChain[] = [ - "avail", - "polkadot", - "kusama", - "aleph-zero", - "vara", -] -export const NOM_POOL_MIN_DEPOSIT: Record = { - "avail": "100000000000000000000", // 100 AVAIL - "polkadot": "10000000000", - "kusama": "001667000000", - "aleph-zero": "100000000000", - "vara": "100000000000", -} - -export const EVM_LSD_PAIRS: Record> = { - "1": { - "1-eth-steth": { - base: "1-evm-native", - derivative: "1-evm-erc20-0xae7ab96520de3a18e5e111b5eaab095312d7fe84", - }, - }, -} - -export const EVM_LSD_SUPPORTED_CHAINS: EvmLsdSupportedChain[] = ["1"] - -export const STAKING_BANNER_CHAINS: StakingSupportedChain[] = [ - ...NOM_POOL_SUPPORTED_CHAINS, - ...EVM_LSD_SUPPORTED_CHAINS, -] diff --git a/packages/extension-core/src/domains/staking/helpers.ts b/packages/extension-core/src/domains/staking/helpers.ts deleted file mode 100644 index f9f6f2fad9..0000000000 --- a/packages/extension-core/src/domains/staking/helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EVM_LSD_SUPPORTED_CHAINS, NOM_POOL_SUPPORTED_CHAINS } from "./constants" -import { EvmLsdSupportedChain, NomPoolSupportedChain, StakingSupportedChain } from "./types" - -export const isNomPoolChain = (chainId: string): chainId is NomPoolSupportedChain => - NOM_POOL_SUPPORTED_CHAINS.includes(chainId as NomPoolSupportedChain) - -export const isEvmLsdChain = (networkId: string): networkId is EvmLsdSupportedChain => - EVM_LSD_SUPPORTED_CHAINS.includes(networkId as EvmLsdSupportedChain) - -export const isStakingSupportedChain = (chainId: string): chainId is StakingSupportedChain => - isNomPoolChain(chainId) || isEvmLsdChain(chainId) diff --git a/packages/extension-core/src/domains/staking/store.StakingBanners.ts b/packages/extension-core/src/domains/staking/store.StakingBanners.ts deleted file mode 100644 index f5def67b5c..0000000000 --- a/packages/extension-core/src/domains/staking/store.StakingBanners.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Address } from "@talismn/balances" -import { ChainId, TokenId } from "@talismn/chaindata-provider" - -import { StorageProvider } from "../../libs/Store" - -type ShouldShowBanner = boolean -export type NomPoolStakingBannerStatus = Record> -export type EvmLSdStakingBannerStatus = Record> - -export type StakingBannerStatus = { - nomPool: NomPoolStakingBannerStatus - evmLsd: EvmLSdStakingBannerStatus -} - -export const stakingBannerStore = new StorageProvider("stakingBanners") diff --git a/packages/extension-core/src/domains/staking/types.ts b/packages/extension-core/src/domains/staking/types.ts deleted file mode 100644 index 9135acbf99..0000000000 --- a/packages/extension-core/src/domains/staking/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type NomPoolSupportedChain = "polkadot" | "kusama" | "aleph-zero" | "vara" | "avail" -export type EvmLsdSupportedPair = { base: string; derivative: string } -export type EvmLsdSupportedChain = "1" -export type StakingSupportedChain = NomPoolSupportedChain | EvmLsdSupportedChain diff --git a/packages/extension-core/src/domains/staking/utils.ts b/packages/extension-core/src/domains/staking/utils.ts deleted file mode 100644 index cd7b345c93..0000000000 --- a/packages/extension-core/src/domains/staking/utils.ts +++ /dev/null @@ -1,143 +0,0 @@ -import keyring from "@polkadot/ui-keyring" -import { Address, Balances } from "@talismn/balances" -import { ChainId, Token, TokenId } from "@talismn/chaindata-provider" -import { log } from "extension-shared" -import { combineLatest, debounceTime } from "rxjs" - -import { awaitKeyringLoaded } from "../../util/awaitKeyringLoaded" -import { balancePool } from "../balances/pool" -import { EVM_LSD_PAIRS, NOM_POOL_MIN_DEPOSIT, NOM_POOL_SUPPORTED_CHAINS } from "./constants" -import { stakingBannerStore } from "./store.StakingBanners" - -type ShouldShowBanner = boolean -export type NomPoolStakingBannerStatus = Record> -export type EvmLSdStakingBannerStatus = Record> - -export type StakingBannerStatus = { - nomPool: NomPoolStakingBannerStatus - evmLsd: EvmLSdStakingBannerStatus -} - -const MAX_UPDATE_INTERVAL = 2_000 // update every 2 seconds maximum - -type AddressStatuses = Record -export type ChainAddressStatuses = Record - -const safelyGetExistentialDeposit = (token?: Token | null): bigint => { - if (token && "existentialDeposit" in token && typeof token.existentialDeposit === "string") - return BigInt(token.existentialDeposit) - return 0n -} - -const shouldShowSubstrateNomPoolBanners = async ({ - addresses, - balances, -}: { - addresses: string[] - balances: Balances -}) => { - const totals = NOM_POOL_SUPPORTED_CHAINS.reduce((acc, chainId) => { - const balancesForChain = balances.find({ - chainId, - source: "substrate-native", - }) - - // for each address, get the free balance after accounting for ED and minimum staking deposit and the staked amount - const addressBalances = addresses.reduce((acc, address) => { - const addressBalances = balancesForChain.find({ address }) - if (!addressBalances) return acc - const balancesForAddress = addressBalances.each.reduce( - (result, balance) => { - // if the balance is less than the ED - minimum stake, it is not eligible - // however because we aggregate balances across accounts here, we don't want to store available values of less than 0 - const realAvailable = - balance.free.planck - - safelyGetExistentialDeposit(balance.token) - - BigInt(NOM_POOL_MIN_DEPOSIT[chainId] || 0) - - const available = realAvailable > 0n ? realAvailable : 0n - - const staked = balance.reserves - .filter( - (reserve) => - reserve.label === "nompools-staking" || reserve.label === "nompools-unbonding", - ) - .reduce((balanceSum, { amount }) => { - return balanceSum + amount.planck - }, 0n) - - return { staked: staked + result.staked, available: available + result.available } - }, - { available: 0n, staked: 0n } as { available: bigint; staked: bigint }, - ) - acc[address] = balancesForAddress.available > 0n && balancesForAddress.staked === 0n - return acc - }, {} as AddressStatuses) - - return { - ...acc, - [chainId]: addressBalances, - } - }, {} as NomPoolStakingBannerStatus) - - return totals -} - -const shouldShowEvmLsdBanners = async ({ - addresses, - balances, -}: { - addresses: string[] - balances: Balances -}) => { - return Object.values(EVM_LSD_PAIRS).reduce((acc, pairs) => { - Object.values(pairs).forEach(({ base, derivative }) => { - if (!(base in acc)) acc[base] = {} - const balancePairs = balances.find([{ tokenId: base }, { tokenId: derivative }]) - - // for each address, determine whether it has a balance of the base token but not the derivative - for (const address of addresses) { - const derivativeBalance = balancePairs.find({ address, tokenId: derivative }).sum.planck - .free - const baseBalance = balancePairs.find({ address, tokenId: base }).sum.planck.free - acc[base][address] = acc[base][address] || (baseBalance > 0n && derivativeBalance === 0n) - } - }) - return acc - }, {} as EvmLSdStakingBannerStatus) -} - -export const trackStakingBannerDisplay = async () => { - await awaitKeyringLoaded() - - combineLatest([keyring.accounts.subject, balancePool.observable]) - .pipe(debounceTime(MAX_UPDATE_INTERVAL)) - .subscribe(async ([accounts, rawBalances]) => { - try { - const balances = new Balances(rawBalances) - - const substrateAddresses = Object.values(accounts) - .filter(({ type }) => type === "sr25519") - .map(({ json }) => json.address) - - const showNomPoolBanners = await shouldShowSubstrateNomPoolBanners({ - addresses: substrateAddresses, - balances, - }) - - // only balances on ethereum accounts are eligible for lido staking - const ethereumAddresses = Object.values(accounts) - .filter(({ type }) => type === "ethereum") - .map(({ json }) => json.address) - - const showEvmLsdBanners = await shouldShowEvmLsdBanners({ - addresses: ethereumAddresses, - balances, - }) - - await stakingBannerStore.replace({ nomPool: showNomPoolBanners, evmLsd: showEvmLsdBanners }) - } catch (err) { - log.error("trackStakingBannerDisplay", { err }) - } - }) -} diff --git a/packages/extension-core/src/domains/tokens/handler.ts b/packages/extension-core/src/domains/tokens/handler.ts index 9fd8d1cb18..be43eea937 100644 --- a/packages/extension-core/src/domains/tokens/handler.ts +++ b/packages/extension-core/src/domains/tokens/handler.ts @@ -34,11 +34,12 @@ export default class TokensHandler extends ExtensionHandler { // -------------------------------------------------------------------- case "pri(tokens.subscribe)": { // TODO: Run this on a timer or something instead of when subscribing to tokens - updateAndWaitForUpdatedChaindata({ updateSubstrateChains: true }) + updateAndWaitForUpdatedChaindata({ updateSubstrateChains: true }).then(() => { + // triggers a pending scan if any + // doing this here as this is the only place where we hydrate tokens from github + assetDiscoveryScanner.startPendingScan() + }) - // triggers a pending scan if any - // doing this here as this is the only place where we hydrate tokens from github - assetDiscoveryScanner.startPendingScan() return genericSubscription( id, port, diff --git a/packages/extension-core/src/index.ts b/packages/extension-core/src/index.ts index dbe416bb87..ba2eadee2d 100644 --- a/packages/extension-core/src/index.ts +++ b/packages/extension-core/src/index.ts @@ -32,14 +32,8 @@ export { getCoinGeckoErc20Coin } from "./util/coingecko/getCoinGeckoErc20Coin" export { getCoingeckoToken } from "./util/coingecko/getCoinGeckoToken" export { getCoingeckoTokensList } from "./util/coingecko/getCoinGeckoTokensList" -export * from "./domains/staking/constants" export * from "./domains/ethereum/helpers" -export { - stakingBannerStore, - type StakingBannerStatus, -} from "./domains/staking/store.StakingBanners" - export { MnemonicSource, mnemonicsStore } from "./domains/mnemonics/store" export { assetDiscoveryStore } from "./domains/assetDiscovery/store" diff --git a/packages/extension-core/src/libs/migrations/index.ts b/packages/extension-core/src/libs/migrations/index.ts index 7bf493046e..0755cc3fef 100644 --- a/packages/extension-core/src/libs/migrations/index.ts +++ b/packages/extension-core/src/libs/migrations/index.ts @@ -11,7 +11,10 @@ import { hideGetStartedIfFunded, migrateAutoLockTimeoutToMinutes, } from "../../domains/app/migrations" -import { migrateAssetDiscoveryRollout } from "../../domains/assetDiscovery/migrations" +import { + migrateAssetDiscoveryRollout, + migrateAssetDiscoveryV2, +} from "../../domains/assetDiscovery/migrations" import { migrateToNewDefaultEvmNetworks } from "../../domains/ethereum/migrations" import { migrateSeedStoreToMultiple } from "../../domains/mnemonics/migrations" import { migrateTokenRates } from "../../domains/tokenRates/migrations" @@ -31,6 +34,7 @@ export const migrations: Migrations = [ hideGetStartedIfFunded, migrateAutoLockTimeoutToMinutes, migrateAnaliticsPurgePendingCaptures, + migrateAssetDiscoveryV2, migrateTokenRates, ] diff --git a/packages/extension-core/src/types/domains.ts b/packages/extension-core/src/types/domains.ts index bad3e336de..f568584ee9 100644 --- a/packages/extension-core/src/types/domains.ts +++ b/packages/extension-core/src/types/domains.ts @@ -9,7 +9,6 @@ export * from "../domains/metadata/types" export * from "../domains/mnemonics/types" export * from "../domains/signing/types" export * from "../domains/sitesAuthorised/types" -export * from "../domains/staking/types" export * from "../domains/substrate/types" export * from "../domains/talisman/types" export * from "../domains/tokenRates/types" diff --git a/packages/extension-shared/src/constants.ts b/packages/extension-shared/src/constants.ts index b2d673079b..5b5032cf27 100644 --- a/packages/extension-shared/src/constants.ts +++ b/packages/extension-shared/src/constants.ts @@ -31,6 +31,9 @@ export const BLOWFISH_BASE_PATH = process.env.BLOWFISH_BASE_PATH || "https://bfp export const BLOWFISH_API_KEY = process.env.BLOWFISH_API_KEY export const NFTS_API_KEY = process.env.NFTS_API_KEY export const NFTS_API_BASE_PATH = process.env.NFTS_API_BASE_PATH || "https://nfts-api.talisman.xyz" +export const TAOSTATS_BASE_PATH = + process.env.TAOSTATS_BASE_PATH || "https://taostats-api-proxy.talismn.workers.dev" +export const TAOSTATS_API_KEY = process.env.TAOSTATS_API_KEY export const TALISMAN_WEB_APP_DOMAIN = "app.talisman.xyz" export const TALISMAN_WEB_APP_URL = "https://app.talisman.xyz"