diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 79328a587..f9761fa39 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -624,6 +624,12 @@ "tryAgain": { "message": "Try again" }, + "errorGettingBakerAddressMessageOnline": { + "message": "Failed to get baker's address. Please, reload the page and try again." + }, + "errorGettingBakerAddressMessage": { + "message": "Failed to get baker's address. Please, check your internet connection and try again." + }, "tezosMainnet": { "message": "Tezos Mainnet", "description": "Mainnet = main network" diff --git a/src/app/ErrorBoundary.tsx b/src/app/ErrorBoundary.tsx index 8093e9efd..b1edd733f 100644 --- a/src/app/ErrorBoundary.tsx +++ b/src/app/ErrorBoundary.tsx @@ -1,13 +1,21 @@ -import React, { Component, ErrorInfo } from 'react'; +import React, { Component, ErrorInfo, FC } from 'react'; import classNames from 'clsx'; import { ReactComponent as DangerIcon } from 'app/icons/danger.svg'; -import { T } from 'lib/i18n'; +import { t, T } from 'lib/i18n'; +import { getOnlineStatus } from 'lib/temple/front'; -interface ErrorBoundaryProps extends React.PropsWithChildren { +export interface ErrorBoundaryProps extends React.PropsWithChildren { className?: string; whileMessage?: string; + Content?: FC<{}>; +} + +export class BoundaryError extends Error { + constructor(public readonly message: string, public readonly beforeTryAgain: EmptyFn) { + super(message); + } } type ErrorBoundaryState = { @@ -33,37 +41,43 @@ export default class ErrorBoundary extends Component { }); } - tryAgain() { + async tryAgain() { + const { error } = this.state; + if (error instanceof BoundaryError) { + error.beforeTryAgain(); + } this.setState({ error: null }); } + getDefaultErrorMessage() { + const { whileMessage } = this.props; + const online = getOnlineStatus(); + const firstPart = whileMessage ? t('smthWentWrongWhile', [whileMessage]) : t('smthWentWrong'); + + return online ? firstPart : [firstPart, t('mayHappenBecauseYouAreOffline')].join('. '); + } + + componentDidUpdate(prevProps: ErrorBoundaryProps) { + if (prevProps.Content !== this.props.Content) { + this.setState({ error: null }); + } + } + render() { - if (this.state.error) { - const online = getOnlineStatus(); + const { className, Content, children: childrenFromProps } = this.props; + const { error } = this.state; + const children = Content ? : childrenFromProps; + if (error) { return ( -
+
{message =>

{message}

}

- {this.props.whileMessage ? ( - - ) : ( - - )} - {!online && ( - - {message => ( - <> - {'. '} - {message} - - )} - - )} + {error instanceof BoundaryError ? error.message : this.getDefaultErrorMessage()}

@@ -92,10 +106,6 @@ export default class ErrorBoundary extends Component { ); } - return this.props.children; + return children; } } - -function getOnlineStatus() { - return typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean' ? navigator.onLine : true; -} diff --git a/src/app/pages/AddAsset/AddAsset.tsx b/src/app/pages/AddAsset/AddAsset.tsx index 8d5fb100c..75cecffca 100644 --- a/src/app/pages/AddAsset/AddAsset.tsx +++ b/src/app/pages/AddAsset/AddAsset.tsx @@ -24,6 +24,7 @@ import { T, t } from 'lib/i18n'; import type { TokenMetadata } from 'lib/metadata'; import { fetchOneTokenMetadata } from 'lib/metadata/fetch'; import { TokenMetadataNotFoundError } from 'lib/metadata/on-chain'; +import { getCacheKey } from 'lib/swr'; import { loadContract } from 'lib/temple/contract'; import { useTezos, @@ -228,7 +229,7 @@ const Form: FC = () => { Repo.toAccountTokenKey(chainId, accountPkh, tokenSlug) ); - swrCache.delete(getBalanceSWRKey(tezos, tokenSlug, accountPkh)); + swrCache.delete(getCacheKey(getBalanceSWRKey(tezos, tokenSlug, accountPkh))); formAnalytics.trackSubmitSuccess(); diff --git a/src/app/pages/Home/ContentSection.tsx b/src/app/pages/Home/ContentSection.tsx index 9717ead2d..ced561a44 100644 --- a/src/app/pages/Home/ContentSection.tsx +++ b/src/app/pages/Home/ContentSection.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx'; import Spinner from 'app/atoms/Spinner/Spinner'; import { useTabSlug } from 'app/atoms/useTabSlug'; import { useAppEnv } from 'app/env'; -import ErrorBoundary from 'app/ErrorBoundary'; +import ErrorBoundary, { ErrorBoundaryProps } from 'app/ErrorBoundary'; import { ToolbarElement } from 'app/layouts/PageLayout'; import { ActivityComponent } from 'app/templates/activity/Activity'; import AssetInfo from 'app/templates/AssetInfo'; @@ -115,20 +115,21 @@ export const ContentSection: FC = ({ assetSlug, className }) => {
- - {Component && } - +
); }; -interface SuspenseContainerProps extends PropsWithChildren { +interface SuspenseContainerProps extends Omit { whileMessage: string; fallback?: ReactNode; } -const SuspenseContainer: FC = ({ whileMessage, fallback = , children }) => ( - +const SuspenseContainer: FC = ({ fallback = , children, ...restProps }) => ( + {children} ); diff --git a/src/app/pages/Home/OtherComponents/BakingSection.tsx b/src/app/pages/Home/OtherComponents/BakingSection.tsx index d1c900f94..3b5d3cc75 100644 --- a/src/app/pages/Home/OtherComponents/BakingSection.tsx +++ b/src/app/pages/Home/OtherComponents/BakingSection.tsx @@ -70,7 +70,7 @@ const links = [ const BakingSection = memo(() => { const acc = useAccount(); - const { data: myBakerPkh } = useDelegate(acc.publicKeyHash); + const { data: myBakerPkh } = useDelegate(acc.publicKeyHash, true, false); const canDelegate = acc.type !== TempleAccountType.WatchOnly; const chainId = useChainId(true); const { isDcpNetwork } = useGasToken(); diff --git a/src/lib/apis/tzkt/api.ts b/src/lib/apis/tzkt/api.ts index 40a632c44..dd61a847d 100644 --- a/src/lib/apis/tzkt/api.ts +++ b/src/lib/apis/tzkt/api.ts @@ -11,16 +11,17 @@ import { allInt32ParameterKeys, TzktGetRewardsParams, TzktGetRewardsResponse, - TzktRelatedContract + TzktRelatedContract, + TzktAccount } from './types'; const TZKT_API_BASE_URLS = { [TempleChainId.Mainnet]: 'https://api.tzkt.io/v1', - [TempleChainId.Jakartanet]: 'https://api.jakartanet.tzkt.io/v1', - [TempleChainId.Limanet]: 'https://api.limanet.tzkt.io/v1', + [TempleChainId.Mumbai]: 'https://api.mumbainet.tzkt.io/v1', + [TempleChainId.Nairobi]: 'https://api.nairobinet.tzkt.io/v1', [TempleChainId.Ghostnet]: 'https://api.ghostnet.tzkt.io/v1', [TempleChainId.Dcp]: 'https://explorer-api.tlnt.net/v1', - [TempleChainId.DcpTest]: 'https://explorer.tlnt.net:8009/v1' + [TempleChainId.DcpTest]: 'https://explorer-api.test.tlnt.net/v1' }; export type TzktApiChainId = keyof typeof TZKT_API_BASE_URLS; @@ -189,3 +190,6 @@ export const fetchAllTokensBalancesFromTzkt = async (selectedRpcUrl: string, acc return balances; }; + +export const getAccountStatsFromTzkt = async (account: string, chainId: TzktApiChainId) => + fetchGet(chainId, `/accounts/${account}`); diff --git a/src/lib/apis/tzkt/index.ts b/src/lib/apis/tzkt/index.ts index 52ec9fe1e..8dee3bd0a 100644 --- a/src/lib/apis/tzkt/index.ts +++ b/src/lib/apis/tzkt/index.ts @@ -9,9 +9,12 @@ export type { TzktAccountToken } from './types'; +export { TzktAccountType } from './types'; + export type { TzktApiChainId } from './api'; export { isKnownChainId, + getAccountStatsFromTzkt, getDelegatorRewards, getOneUserContracts, fetchTzktTokens, diff --git a/src/lib/apis/tzkt/types.ts b/src/lib/apis/tzkt/types.ts index d20e282dd..6adaab43b 100644 --- a/src/lib/apis/tzkt/types.ts +++ b/src/lib/apis/tzkt/types.ts @@ -221,3 +221,271 @@ export interface TzktTokenTransfer { }; transactionId: number; } + +export enum TzktAccountType { + User = 'user', + Delegate = 'delegate', + Contract = 'contract', + Ghost = 'ghost', + Rollup = 'rollup', + SmartRollup = 'smart_rollup', + Empty = 'empty' +} + +interface TzktAccountBase { + type: TzktAccountType; + address: string; + alias: string | nullish; +} + +interface TzktUserAccount extends TzktAccountBase { + type: TzktAccountType.User; + id: number; + publicKey: string; + revealed: boolean; + balance: number; + rollupBonds: number; + smartRollupBonds: number; + counter: number; + delegate: TzktAlias | nullish; + delegationLevel: number; + delegationTime: string | nullish; + numContracts: number; + rollupsCount: number; + smartRollupsCount: number; + activeTokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + numActivations: number; + numDelegations: number; + numOriginations: number; + numTransactions: number; + numReveals: number; + numRegisterConstants: number; + numSetDepositsLimits: number; + numMigrations: number; + txRollupOriginationCount: number; + txRollupSubmitBatchCount: number; + txRollupCommitCount: number; + txRollupReturnBondCount: number; + txRollupFinalizeCommitmentCount: number; + txRollupRemoveCommitmentCount: number; + txRollupRejectionCount: number; + txRollupDispatchTicketsCount: number; + transferTicketCount: number; + increasePaidStorageCount: number; + drainDelegateCount: number; + smartRollupAddMessagesCount: number; + smartRollupCementCount: number; + smartRollupExecuteCount: number; + smartRollupOriginateCount: number; + smartRollupPublishCount: number; + smartRollupRecoverBondCount: number; + smartRollupRefuteCount: number; + refutationGamesCount: number; + activeRefutationGamesCount: number; + firstActivity: number | nullish; + firstActivityTime: string | nullish; + lastActivity: number | nullish; + lastActivityTime: string | nullish; +} + +interface TzktDelegateAccount extends TzktAccountBase { + type: TzktAccountType.Delegate; + id: number; + active: boolean; + publicKey: string | nullish; + revealed: boolean; + balance: number; + rollupBonds: number; + smartRollupBonds: number; + frozenDeposit: number; + frozenDepositLimit: number | nullish; + counter: number; + activationLevel: number; + activationTime: string; + deactivationLevel: number | nullish; + deactivationTime: string | nullish; + stakingBalance: number; + delegatedBalance: number; + numContracts: number; + rollupsCount: number; + smartRollupsCount: number; + activeTokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + numDelegators: number; + numBlocks: number; + numEndorsements: number; + numPreendorsements: number; + numBallots: number; + numProposals: number; + numActivations: number; + numDoubleBaking: number; + numDoubleEndorsing: number; + numDoublePreendorsing: number; + numNonceRevelations: number; + vdfRevelationsCount: number; + numRevelationPenalties: number; + numEndorsingRewards: number; + numDelegations: number; + numOriginations: number; + numTransactions: number; + numReveals: number; + numRegisterConstants: number; + numSetDepositsLimits: number; + numMigrations: number; + txRollupOriginationCount: number; + txRollupSubmitBatchCount: number; + txRollupCommitCount: number; + txRollupReturnBondCount: number; + txRollupFinalizeCommitmentCount: number; + txRollupRemoveCommitmentCount: number; + txRollupRejectionCount: number; + txRollupDispatchTicketsCount: number; + transferTicketCount: number; + increasePaidStorageCount: number; + updateConsensusKeyCount: number; + drainDelegateCount: number; + smartRollupAddMessagesCount: number; + smartRollupCementCount: number; + smartRollupExecuteCount: number; + smartRollupOriginateCount: number; + smartRollupPublishCount: number; + smartRollupRecoverBondCount: number; + smartRollupRefuteCount: number; + refutationGamesCount: number; + activeRefutationGamesCount: number; + firstActivity: number; + firstActivityTime: string | nullish; + lastActivity: number; + lastActivityTime: string | nullish; + extras: unknown; + software: { date: string; version: string | nullish }; +} + +interface TzktContractAccount extends TzktAccountBase { + type: TzktAccountType.Contract; + id: number; + kind: 'delegator_contract' | 'smart_contract' | nullish; + tzips: string[] | nullish; + balance: number; + creator: TzktAlias | nullish; + manager: TzktAlias | nullish; + delegate: TzktAlias | nullish; + delegationLevel: number | nullish; + delegationTime: string | nullish; + numContracts: number; + activeTokensCount: number; + tokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + numDelegations: number; + numOriginations: number; + numTransactions: number; + numReveals: number; + numMigrations: number; + transferTicketCount: number; + increasePaidStorageCount: number; + eventsCount: number; + firstActivity: number; + firstActivityTime: string; + lastActivity: number; + lastActivityTime: string; + typeHash: number; + codeHash: number; + /** TZIP-16 metadata (with ?legacy=true this field will contain tzkt profile info). */ + metadata: unknown; + extras: unknown; + /** Contract storage value. Omitted by default. Use ?includeStorage=true to include it into response. */ + storage: unknown; +} + +interface TzktGhostAccount extends TzktAccountBase { + type: TzktAccountType.Ghost; + id: number; + activeTokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + firstActivity: number; + firstActivityTime: string; + lastActivity: number; + lastActivityTime: string; + extras: unknown; +} + +interface TzktRollupAccount extends TzktAccountBase { + type: TzktAccountType.Rollup; + id: number; + creator: TzktAlias | nullish; + rollupBonds: number; + activeTokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + numTransactions: number; + txRollupOriginationCount: number; + txRollupSubmitBatchCount: number; + txRollupCommitCount: number; + txRollupReturnBondCount: number; + txRollupFinalizeCommitmentCount: number; + txRollupRemoveCommitmentCount: number; + txRollupRejectionCount: number; + txRollupDispatchTicketsCount: number; + transferTicketCount: number; + firstActivity: number; + firstActivityTime: string; + lastActivity: number; + lastActivityTime: string; + extras: unknown; +} + +interface TzktSmartRollupAccount extends TzktAccountBase { + type: TzktAccountType.SmartRollup; + id: number; + creator: TzktAlias | nullish; + pvmKind: 'arith' | 'wasm' | nullish; + genesisCommitment: string | nullish; + lastCommitment: string | nullish; + inboxLevel: number; + totalStakers: number; + activeStakers: number; + executedCommitments: number; + cementedCommitments: number; + pendingCommitments: number; + refutedCommitments: number; + orphanCommitments: number; + smartRollupBonds: number; + activeTokensCount: number; + tokenBalancesCount: number; + tokenTransfersCount: number; + numTransactions: number; + transferTicketCount: number; + smartRollupCementCount: number; + smartRollupExecuteCount: number; + smartRollupOriginateCount: number; + smartRollupPublishCount: number; + smartRollupRecoverBondCount: number; + smartRollupRefuteCount: number; + refutationGamesCount: number; + activeRefutationGamesCount: number; + firstActivity: number; + firstActivityTime: string; + lastActivity: number; + lastActivityTime: string; + extras: unknown; +} + +interface TzktEmptyAccount extends TzktAccountBase { + type: TzktAccountType.Empty; + alias: undefined; + counter: number; +} + +export type TzktAccount = + | TzktUserAccount + | TzktDelegateAccount + | TzktContractAccount + | TzktGhostAccount + | TzktRollupAccount + | TzktSmartRollupAccount + | TzktEmptyAccount; diff --git a/src/lib/swr/index.ts b/src/lib/swr/index.ts index c7b117fce..d637a03a0 100644 --- a/src/lib/swr/index.ts +++ b/src/lib/swr/index.ts @@ -1,7 +1,9 @@ -import useSWR, { Key, Fetcher, SWRConfiguration, SWRResponse } from 'swr'; +import useSWR, { Key, Fetcher, SWRConfiguration, SWRResponse, unstable_serialize } from 'swr'; export const useRetryableSWR = ( key: SWRKey, fetcher: Fetcher | null, config: SWRConfiguration> | undefined ): SWRResponse => useSWR(key, fetcher, { errorRetryCount: 2, ...config }); + +export const getCacheKey = (key: Key) => `$swr$${unstable_serialize(key)}`; diff --git a/src/lib/temple/front/baking.ts b/src/lib/temple/front/baking.ts index dc1d37fc4..15f8609ee 100644 --- a/src/lib/temple/front/baking.ts +++ b/src/lib/temple/front/baking.ts @@ -1,30 +1,80 @@ import { useCallback, useMemo } from 'react'; +import retry from 'async-retry'; import BigNumber from 'bignumber.js'; +import useSWR, { useSWRConfig } from 'swr'; +import { BoundaryError } from 'app/ErrorBoundary'; import { BakingBadBaker, BakingBadBakerValueHistoryItem, bakingBadGetBaker, getAllBakersBakingBad } from 'lib/apis/baking-bad'; -import type { TzktRewardsEntry } from 'lib/apis/tzkt'; -import { useRetryableSWR } from 'lib/swr'; +import { getAccountStatsFromTzkt, isKnownChainId, TzktRewardsEntry, TzktAccountType } from 'lib/apis/tzkt'; +import { t } from 'lib/i18n'; +import { getCacheKey, useRetryableSWR } from 'lib/swr'; +import type { ReactiveTezosToolkit } from 'lib/temple/front'; -import { useNetwork, useTezos } from './ready'; +import { getOnlineStatus } from './get-online-status'; +import { useChainId, useNetwork, useTezos } from './ready'; -export function useDelegate(address: string, suspense = true) { +function getDelegateCacheKey( + tezos: ReactiveTezosToolkit, + address: string, + chainId: string | nullish, + shouldPreventErrorPropagation: boolean +) { + return getCacheKey(['delegate', tezos.checksum, address, chainId, shouldPreventErrorPropagation]); +} + +export function useDelegate(address: string, suspense = true, shouldPreventErrorPropagation = true) { const tezos = useTezos(); + const chainId = useChainId(suspense); + const { cache: swrCache } = useSWRConfig(); + + const resetDelegateCache = useCallback(() => { + swrCache.delete(getDelegateCacheKey(tezos, address, chainId, shouldPreventErrorPropagation)); + }, [address, tezos, chainId, swrCache, shouldPreventErrorPropagation]); const getDelegate = useCallback(async () => { try { - return await tezos.rpc.getDelegate(address); - } catch { - return null; + return await retry( + async () => { + const freshChainId = chainId ?? (await tezos.rpc.getChainId()); + if (freshChainId && isKnownChainId(freshChainId)) { + try { + const accountStats = await getAccountStatsFromTzkt(address, freshChainId); + + switch (accountStats.type) { + case TzktAccountType.Empty: + return null; + case TzktAccountType.User: + case TzktAccountType.Contract: + return accountStats.delegate?.address ?? null; + } + } catch (e) { + console.error(e); + } + } + + return await tezos.rpc.getDelegate(address); + }, + { retries: 3, minTimeout: 3000, maxTimeout: 5000 } + ); + } catch (e) { + if (shouldPreventErrorPropagation) { + return null; + } + + throw new BoundaryError( + getOnlineStatus() ? t('errorGettingBakerAddressMessageOnline') : t('errorGettingBakerAddressMessage'), + resetDelegateCache + ); } - }, [address, tezos]); + }, [chainId, tezos, address, shouldPreventErrorPropagation, resetDelegateCache]); - return useRetryableSWR(['delegate', tezos.checksum, address], getDelegate, { + return useSWR(['delegate', tezos.checksum, address, chainId, shouldPreventErrorPropagation], getDelegate, { dedupingInterval: 20_000, suspense }); diff --git a/src/lib/temple/front/blockexplorer.ts b/src/lib/temple/front/blockexplorer.ts index 81de4fc0f..6a526a152 100644 --- a/src/lib/temple/front/blockexplorer.ts +++ b/src/lib/temple/front/blockexplorer.ts @@ -39,18 +39,19 @@ export const BLOCK_EXPLORERS: BlockExplorer[] = [ } ], [ - TempleChainId.Jakartanet, + TempleChainId.Mumbai, { - account: 'https://jakartanet.tzkt.io', - transaction: 'https://jakartanet.tzkt.io' + account: 'https://mumbainet.tzkt.io', + transaction: 'https://mumbainet.tzkt.io', + api: 'https://api.mumbainet.tzkt.io' } ], [ - TempleChainId.Limanet, + TempleChainId.Nairobi, { - account: 'https://limanet.tzkt.io', - transaction: 'https://limanet.tzkt.io', - api: 'https://api.limanet.tzkt.io' + account: 'https://nairobinet.tzkt.io', + transaction: 'https://nairobinet.tzkt.io', + api: 'https://api.nairobinet.tzkt.io' } ] ]) @@ -70,8 +71,9 @@ export const BLOCK_EXPLORERS: BlockExplorer[] = [ [ TempleChainId.DcpTest, { - account: 'https://explorer.tlnt.net:444', - transaction: 'https://explorer.tlnt.net:444' + account: 'https://explorer.test.tlnt.net', + transaction: 'https://explorer.test.tlnt.net', + api: 'https://explorer-api.test.tlnt.net' } ] ]) diff --git a/src/lib/temple/front/get-online-status.ts b/src/lib/temple/front/get-online-status.ts new file mode 100644 index 000000000..f4c90705b --- /dev/null +++ b/src/lib/temple/front/get-online-status.ts @@ -0,0 +1,3 @@ +export function getOnlineStatus() { + return typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean' ? navigator.onLine : true; +} diff --git a/src/lib/temple/front/index.ts b/src/lib/temple/front/index.ts index dccc583ad..4bde1f873 100644 --- a/src/lib/temple/front/index.ts +++ b/src/lib/temple/front/index.ts @@ -64,3 +64,5 @@ export { buildCollectibleImageURLs, buildObjktCollectibleArtifactUri } from './image-uri'; + +export { getOnlineStatus } from './get-online-status'; diff --git a/src/lib/temple/networks.ts b/src/lib/temple/networks.ts index 6b7f055cc..451c9cf35 100644 --- a/src/lib/temple/networks.ts +++ b/src/lib/temple/networks.ts @@ -13,9 +13,8 @@ const getLastMonday = (date = new Date()) => { export const NETWORK_IDS = new Map([ [TempleChainId.Mainnet, 'mainnet'], [TempleChainId.Ghostnet, 'ghostnet'], - [TempleChainId.Jakartanet, 'jakartanet'], - [TempleChainId.Limanet, 'limanet'], - [TempleChainId.Kathmandunet, 'kathmandunet'] + [TempleChainId.Mumbai, 'mumbainet'], + [TempleChainId.Nairobi, 'nairobinet'] ]); const DCP_NETWORKS: TempleNetwork[] = [ diff --git a/src/lib/temple/types.ts b/src/lib/temple/types.ts index f0631e2a8..2f4adab0c 100644 --- a/src/lib/temple/types.ts +++ b/src/lib/temple/types.ts @@ -39,10 +39,9 @@ export interface TempleState { export enum TempleChainId { Mainnet = 'NetXdQprcVkpaWU', Ghostnet = 'NetXnHfVqm9iesp', - Jakartanet = 'NetXLH1uAxK7CCh', - Limanet = 'NetXizpkH94bocH', - Kathmandunet = 'NetXdnUSgMs7Xc3', Monday = 'NetXaqtQ8b5nihx', + Mumbai = 'NetXgbcrNtXD2yA', + Nairobi = 'NetXyuzvDo2Ugzb', Daily = 'NetXxkAx4woPLyu', Dcp = 'NetXooyhiru73tk', DcpTest = 'NetXX7Tz1sK8JTa'