diff --git a/apps/app/src/components/app/Address/AccountAlerts.tsx b/apps/app/src/components/app/Address/AccountAlerts.tsx index c8e01642..b9930f4c 100644 --- a/apps/app/src/components/app/Address/AccountAlerts.tsx +++ b/apps/app/src/components/app/Address/AccountAlerts.tsx @@ -2,8 +2,8 @@ import { convertToUTC, nanoToMilli } from '@/utils/app/libs'; import { useEffect, useState, useCallback } from 'react'; import WarningIcon from '../Icons/WarningIcon'; -import useRpc from '@/hooks/useRpc'; import { AccountDataInfo, ContractCodeInfo } from '@/utils/types'; +import useRpc from '@/hooks/app/useRpc'; export default function AccountAlerts({ id, diff --git a/apps/app/src/components/app/Address/AccountMoreInfo.tsx b/apps/app/src/components/app/Address/AccountMoreInfo.tsx index 26ebe4b0..c26e1bc2 100644 --- a/apps/app/src/components/app/Address/AccountMoreInfo.tsx +++ b/apps/app/src/components/app/Address/AccountMoreInfo.tsx @@ -9,10 +9,10 @@ import { yoctoToNear, } from '@/utils/app/libs'; import TokenImage from '../common/TokenImage'; -import useRpc from '@/hooks/useRpc'; import { AccountDataInfo, ContractCodeInfo } from '@/utils/types'; import { Link } from '@/i18n/routing'; import { useTranslations } from 'next-intl'; +import useRpc from '@/hooks/app/useRpc'; export default function AccountMoreInfo({ id, diff --git a/apps/app/src/components/app/Address/AccountOverview.tsx b/apps/app/src/components/app/Address/AccountOverview.tsx index c5751a5d..775302eb 100644 --- a/apps/app/src/components/app/Address/AccountOverview.tsx +++ b/apps/app/src/components/app/Address/AccountOverview.tsx @@ -3,11 +3,11 @@ import React, { useEffect, useState } from 'react'; import { dollarFormat, fiatValue, yoctoToNear } from '@/utils/app/libs'; import TokenHoldings from '../common/TokenHoldings'; import FaExternalLinkAlt from '../Icons/FaExternalLinkAlt'; -import useRpc from '@/hooks/useRpc'; import Big from 'big.js'; import { FtInfo, TokenListInfo } from '@/utils/types'; import { useTranslations } from 'next-intl'; import { useConfig } from '@/hooks/app/useConfig'; +import useRpc from '@/hooks/app/useRpc'; export default function AccountOverview({ id, diff --git a/apps/app/src/components/app/Address/Contract/Info.tsx b/apps/app/src/components/app/Address/Contract/Info.tsx index 1888ec31..671839b5 100644 --- a/apps/app/src/components/app/Address/Contract/Info.tsx +++ b/apps/app/src/components/app/Address/Contract/Info.tsx @@ -1,6 +1,6 @@ 'use client'; import Question from '@/components/Icons/Question'; -import useRpc from '@/hooks/useRpc'; +import useRpc from '@/hooks/app/useRpc'; import { Link } from '@/i18n/routing'; import { useRpcStore } from '@/stores/rpc'; import { convertToUTC, nanoToMilli } from '@/utils/libs'; diff --git a/apps/app/src/components/app/Address/Contract/OverviewActions.tsx b/apps/app/src/components/app/Address/Contract/OverviewActions.tsx index e1bebf0a..607925e2 100644 --- a/apps/app/src/components/app/Address/Contract/OverviewActions.tsx +++ b/apps/app/src/components/app/Address/Contract/OverviewActions.tsx @@ -7,7 +7,7 @@ import { VerificationData, VerifierStatus, } from '@/utils/types'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import Info from './Info'; import { Tooltip } from '@reach/tooltip'; @@ -16,9 +16,10 @@ import ViewOrChange from './ViewOrChange'; import { useAuthStore } from '@/stores/auth'; import ViewOrChangeAbi from './ViewOrChangeAbi'; import ContractCode from './ContractCode'; -import useRpc from '@/hooks/useRpc'; -import { useRpcStore } from '@/stores/rpc'; import { verifierConfig } from '@/utils/app/config'; +import useRpc from '@/hooks/app/useRpc'; +import { useRpcProvider } from '@/hooks/app/useRpcProvider'; +import { useRpcStore } from '@/stores/app/rpc'; interface Props { id: string; @@ -65,7 +66,22 @@ const OverviewActions = (props: Props) => { Record >({}); const [rpcError, setRpcError] = useState(false); - const switchRpc: () => void = useRpcStore((state) => state.switchRpc); + const initializedRef = useRef(false); + + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + + const { switchRpc } = useRpcStoreWithProviders(); useEffect(() => { if (rpcError) { diff --git a/apps/app/src/components/app/Address/Contract/Verifier.tsx b/apps/app/src/components/app/Address/Contract/Verifier.tsx index e0b6c875..061f0f00 100644 --- a/apps/app/src/components/app/Address/Contract/Verifier.tsx +++ b/apps/app/src/components/app/Address/Contract/Verifier.tsx @@ -3,12 +3,13 @@ import ErrorMessage from '@/components/common/ErrorMessage'; import LoadingCircular from '@/components/common/LoadingCircular'; import ArrowDown from '@/components/Icons/ArrowDown'; import FaInbox from '@/components/Icons/FaInbox'; -import useRpc from '@/hooks/useRpc'; -import { useRpcStore } from '@/stores/rpc'; +import useRpc from '@/hooks/app/useRpc'; +import { useRpcProvider } from '@/hooks/app/useRpcProvider'; +import { useRpcStore } from '@/stores/app/rpc'; import { verifierConfig } from '@/utils/config'; import { parseGitHubLink, parseLink } from '@/utils/libs'; import { ContractMetadata } from '@/utils/types'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; type ContractFormProps = { accountId: string; @@ -34,8 +35,22 @@ const Verifier: React.FC = ({ const [onChainCodeHash, setOnChainCodeHash] = useState(null); const [verified, setVerified] = useState(false); const [rpcError, setRpcError] = useState(false); - const switchRpc: () => void = useRpcStore((state) => state.switchRpc); + const initializedRef = useRef(false); + + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + const { switchRpc } = useRpcStoreWithProviders(); const { contractCode, getContractMetadata, getVerifierData } = useRpc(); useEffect(() => { diff --git a/apps/app/src/components/app/Contact/FormContact.tsx b/apps/app/src/components/app/Contact/FormContact.tsx index 54572570..ba00b6fb 100644 --- a/apps/app/src/components/app/Contact/FormContact.tsx +++ b/apps/app/src/components/app/Contact/FormContact.tsx @@ -1,3 +1,4 @@ +'use client'; import { useEffect, useRef, useState } from 'react'; import ArrowDown from '../Icons/ArrowDown'; import { toast } from 'react-toastify'; @@ -7,18 +8,17 @@ import type { TurnstileInstance } from '@marsidev/react-turnstile'; import { useTranslations } from 'next-intl'; import LoadingCircular from '@/components/common/LoadingCircular'; import { useThemeStore } from '@/stores/theme'; +import { useConfig } from '@/hooks/app/useConfig'; interface Props { selectValue?: string; getContactDetails: any; } -const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; - const FormContact = ({ selectValue, getContactDetails }: Props) => { const theme = useThemeStore((store) => store.theme); const t = useTranslations('contact'); - + const { siteKey } = useConfig(); const [loading, setLoading] = useState(false); const [name, setName] = useState(''); const [email, setEmail] = useState(''); diff --git a/apps/app/src/components/app/Layouts/RpcMenu.tsx b/apps/app/src/components/app/Layouts/RpcMenu.tsx index b2807bab..7b6cd773 100644 --- a/apps/app/src/components/app/Layouts/RpcMenu.tsx +++ b/apps/app/src/components/app/Layouts/RpcMenu.tsx @@ -1,14 +1,33 @@ 'use client'; -import { useRpcStore } from '@/stores/rpc'; import { useEffect, useRef, useState } from 'react'; import Rpc from '../Icons/Rpc'; import ArrowDown from '../Icons/ArrowDown'; import Check from '../Icons/Check'; -import { RpcProviders } from '@/utils/app/rpc'; +import { useRpcStore } from '@/stores/app/rpc'; +import { useRpcProvider } from '@/hooks/app/useRpcProvider'; +import { useRouter } from 'next/navigation'; const RpcMenu = () => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); + const { RpcProviders } = useRpcProvider(); + const initializedRef = useRef(false); + const router = useRouter(); + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + + const { setRpc, rpc: rpcUrl } = useRpcStoreWithProviders(); const toggleDropdown = () => { setIsOpen(!isOpen); @@ -17,6 +36,7 @@ const RpcMenu = () => { const handleSelect = (url: string) => { setRpc(url); setIsOpen(false); + router.refresh(); }; useEffect(() => { @@ -35,9 +55,6 @@ const RpcMenu = () => { }; }, [dropdownRef]); - const rpcUrl = useRpcStore((state) => state.rpc); - const setRpc = useRpcStore((state) => state.setRpc); - return (
  • diff --git a/apps/app/src/components/app/NodeExplorer/Delegators.tsx b/apps/app/src/components/app/NodeExplorer/Delegators.tsx index 5fd7e35b..b31d42f9 100644 --- a/apps/app/src/components/app/NodeExplorer/Delegators.tsx +++ b/apps/app/src/components/app/NodeExplorer/Delegators.tsx @@ -4,18 +4,19 @@ import { DelegatorInfo, RewardFraction, ValidatorStatus } from '@/utils/types'; import { CurrentEpochValidatorInfo, ValidatorDescription } from 'nb-types'; import { useEffect, useRef, useState } from 'react'; import { Tooltip } from '@reach/tooltip'; -import useRpc from '@/hooks/useRpc'; import Image from 'next/image'; import { debounce } from 'lodash'; import Skeleton from '../skeleton/common/Skeleton'; import Table from '../common/Table'; import ErrorMessage from '../common/ErrorMessage'; import FaInbox from '../Icons/FaInbox'; -import { useRpcStore } from '@/stores/rpc'; import { Link } from '@/i18n/routing'; import { useSearchParams } from 'next/navigation'; import { useConfig } from '@/hooks/app/useConfig'; import { useThemeStore } from '@/stores/theme'; +import useRpc from '@/hooks/app/useRpc'; +import { useRpcProvider } from '@/hooks/app/useRpcProvider'; +import { useRpcStore } from '@/stores/app/rpc'; interface Props { accountId: string; @@ -45,10 +46,23 @@ const Delegators = ({ accountId }: Props) => { const [searchResults, setSearchResults] = useState([]); const [status, setStatus] = useState(); const [count, setCount] = useState(); - const rpcUrl: string = useRpcStore((state) => state.rpc); - const switchRpc: () => void = useRpcStore((state) => state.switchRpc); const [_allRpcProviderError, setAllRpcProviderError] = useState(false); + const initializedRef = useRef(false); + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + + const { switchRpc, rpc: rpcUrl } = useRpcStoreWithProviders(); const start = (pagination.page - 1) * pagination.per_page; const getStatusColorClass = (status: string) => { diff --git a/apps/app/src/components/app/Tokens/FT/TokenFilter.tsx b/apps/app/src/components/app/Tokens/FT/TokenFilter.tsx index 50cc36c1..bf39d3f3 100644 --- a/apps/app/src/components/app/Tokens/FT/TokenFilter.tsx +++ b/apps/app/src/components/app/Tokens/FT/TokenFilter.tsx @@ -3,10 +3,10 @@ import { FtInfo, FtsInfo, InventoryInfo, TokenListInfo } from '@/utils/types'; import Big from 'big.js'; import { useEffect, useState } from 'react'; import { dollarFormat, localFormat } from '@/utils/libs'; -import useRpc from '@/hooks/useRpc'; import FaAddressBook from '@/components/Icons/FaAddressBook'; import Skeleton from '@/components/skeleton/common/Skeleton'; import { Link } from '@/i18n/routing'; +import useRpc from '@/hooks/app/useRpc'; interface Props { id: string; diff --git a/apps/app/src/components/app/Tokens/FTTransfersActions.tsx b/apps/app/src/components/app/Tokens/FTTransfersActions.tsx index 0c6e4aeb..927c97e3 100644 --- a/apps/app/src/components/app/Tokens/FTTransfersActions.tsx +++ b/apps/app/src/components/app/Tokens/FTTransfersActions.tsx @@ -16,10 +16,10 @@ import Clock from '@/components/Icons/Clock'; import ErrorMessage from '@/components/common/ErrorMessage'; import FaInbox from '@/components/Icons/FaInbox'; import TokenImage from '@/components/common/TokenImage'; -import useRpc from '@/hooks/useRpc'; import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import Table from '../common/Table'; +import useRpc from '@/hooks/app/useRpc'; interface ListProps { data: { diff --git a/apps/app/src/components/app/Tokens/NFTTransfersActions.tsx b/apps/app/src/components/app/Tokens/NFTTransfersActions.tsx index 983fcd49..f3557247 100644 --- a/apps/app/src/components/app/Tokens/NFTTransfersActions.tsx +++ b/apps/app/src/components/app/Tokens/NFTTransfersActions.tsx @@ -3,7 +3,6 @@ import { TransactionInfo } from '@/utils/types'; import { Tooltip } from '@reach/tooltip'; import { useEffect, useState } from 'react'; import { getTimeAgoString, localFormat, nanoToMilli } from '@/utils/libs'; -import useRpc from '@/hooks/useRpc'; import TxnStatus from '@/components/common/Status'; import FaLongArrowAltRight from '@/components/Icons/FaLongArrowAltRight'; import TokenImage from '@/components/common/TokenImage'; @@ -14,6 +13,7 @@ import FaInbox from '@/components/Icons/FaInbox'; import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import Table from '../common/Table'; +import useRpc from '@/hooks/app/useRpc'; interface ListProps { data: { diff --git a/apps/app/src/components/app/Transactions/Details.tsx b/apps/app/src/components/app/Transactions/Details.tsx index 89437826..b502a3ab 100644 --- a/apps/app/src/components/app/Transactions/Details.tsx +++ b/apps/app/src/components/app/Transactions/Details.tsx @@ -23,7 +23,6 @@ import { yoctoToNear, } from '@/utils/libs'; import { - ExecutionOutcomeWithIdView, FtsInfo, InventoryInfo, NftsInfo, @@ -37,15 +36,7 @@ import ArrowUp from '../Icons/ArrowUp'; import Question from '../Icons/Question'; import TxnStatus from '../common/Status'; import { Tooltip } from '@reach/tooltip'; -import { - calculateGasUsed, - calculateTotalDeposit, - calculateTotalGas, - txnActions, - txnErrorMessage, - txnFee, - txnLogs, -} from '@/utils/near'; +import { txnActions, txnErrorMessage, txnLogs } from '@/utils/near'; import EventLogs from './Action'; import Actions from './Actions'; import TokenImage, { NFTImage } from '../common/TokenImage'; @@ -53,8 +44,6 @@ import { isEmpty } from 'lodash'; import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import FaRight from '@/components/Icons/FaRight'; -import useRpc from '@/hooks/useRpc'; -import { useRpcStore } from '@/stores/rpc'; import ErrorMessage from '@/components/common/ErrorMessage'; import FileSlash from '../Icons/FileSlash'; import { useConfig } from '@/hooks/app/useConfig'; @@ -62,31 +51,30 @@ import { useConfig } from '@/hooks/app/useConfig'; interface Props { loading: boolean; txn: TransactionInfo; - statsData: any; isContract: boolean; price: string; hash: string; + rpcTxn: any; + statsData: { + stats: Array<{ + near_price: string; + }>; + }; } const Details = (props: Props) => { const { loading, - txn: txnData, + txn, statsData, price, isContract = false, hash, + rpcTxn, } = props; - const { transactionStatus, getBlockDetails } = useRpc(); - const rpcUrl: string = useRpcStore((state) => state.rpc); - const switchRpc: () => void = useRpcStore((state) => state.switchRpc); const [more, setMore] = useState(false); - const [rpcTxn, setRpcTxn] = useState({}); - const [rpcData, setRpcData] = useState({}); - const [rpcError, setRpcError] = useState(false); - const { networkId } = useConfig(); - const txn = txnData ? txnData : rpcData; + const { networkId } = useConfig(); const t = useTranslations(); const { fts, nfts } = useMemo(() => { @@ -146,7 +134,6 @@ const Details = (props: Props) => { } const currentPrice = statsData?.stats?.[0]?.near_price || 0; - const [logs, actions, errorMessage] = useMemo(() => { if (!isEmpty(rpcTxn)) { return [txnLogs(rpcTxn), txnActions(rpcTxn), txnErrorMessage(rpcTxn)]; @@ -168,95 +155,6 @@ const Details = (props: Props) => { } }, [logs, actions]); - useEffect(() => { - const fetchTransactionStatus = async () => { - if (!txn) return; - - try { - setRpcError(false); - const res = await transactionStatus( - txn.transaction_hash, - txn.signer_account_id, - ); - setRpcTxn(res); - } catch { - setRpcError(true); - } - }; - - fetchTransactionStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, rpcUrl]); - - useEffect(() => { - const checkTxnExistence = async () => { - if (txn === undefined || txn === null) { - try { - setRpcError(false); - const txnExists: any = await transactionStatus(hash, 'bowen'); - const status = txnExists.status?.Failure ? false : true; - let block: any = {}; - - if (txnExists) { - block = await getBlockDetails( - txnExists.transaction_outcome.block_hash, - ); - } - - const modifiedTxns = { - transaction_hash: txnExists.transaction_outcome.id, - included_in_block_hash: txnExists.transaction_outcome.block_hash, - outcomes: { status: status }, - block: { block_height: block?.header.height }, - block_timestamp: block?.header.timestamp_nanosec, - receiver_account_id: txnExists.transaction.receiver_id, - signer_account_id: txnExists.transaction.signer_id, - receipt_conversion_gas_burnt: - txnExists.transaction_outcome.outcome.gas_burnt.toString(), - receipt_conversion_tokens_burnt: - txnExists.transaction_outcome.outcome.tokens_burnt, - actions_agg: { - deposit: calculateTotalDeposit(txnExists?.transaction.actions), - gas_attached: calculateTotalGas(txnExists?.transaction.actions), - }, - outcomes_agg: { - transaction_fee: txnFee( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', - ), - gas_used: calculateGasUsed( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', - ), - }, - }; - if (txnExists) { - setRpcTxn(txnExists); - setRpcData(modifiedTxns); - } - } catch (error) { - setRpcError(true); - } - } - }; - - checkTxnExistence(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, hash, rpcUrl]); - - useEffect(() => { - if (rpcError) { - try { - switchRpc(); - } catch (error) { - setRpcError(true); - console.error('Failed to switch RPC:', error); - } - } - }, [rpcError, switchRpc]); - const FailedReceipts = ({ data }: any) => { const failedReceiptCount = (data?.receipts_outcome || []).filter( (receipt: any) => receipt?.outcome?.status?.Failure, diff --git a/apps/app/src/components/app/Transactions/Execution.tsx b/apps/app/src/components/app/Transactions/Execution.tsx index f88c1a01..a06082f6 100644 --- a/apps/app/src/components/app/Transactions/Execution.tsx +++ b/apps/app/src/components/app/Transactions/Execution.tsx @@ -1,15 +1,10 @@ 'use client'; import { - calculateGasUsed, - calculateTotalDeposit, - calculateTotalGas, collectNestedReceiptWithOutcomeOld, parseOutcomeOld, parseReceipt, - txnFee, } from '@/utils/near'; import { - ExecutionOutcomeWithIdView, FailedToFindReceipt, NestedReceiptWithOutcome, RPCTransactionInfo, @@ -20,24 +15,18 @@ import FaHourglassStart from '../Icons/FaHourglassStart'; import Skeleton from '../skeleton/common/Skeleton'; import TransactionReceipt from './Receipts/TransactionReceipt'; import { isEmpty } from 'lodash'; -import useRpc from '@/hooks/useRpc'; -import { useRpcStore } from '@/stores/rpc'; + import ErrorMessage from '../common/ErrorMessage'; import FileSlash from '../Icons/FileSlash'; interface Props { txn: TransactionInfo; hash: string; + rpcTxn: RPCTransactionInfo; } const Execution = (props: Props) => { - const { txn: txnData, hash } = props; - const rpcUrl: string = useRpcStore((state) => state.rpc); - const { transactionStatus, getBlockDetails } = useRpc(); - const [rpcTxn, setRpcTxn] = useState({}); - const [rpcData, setRpcData] = useState({}); - - const txn = txnData ? txnData : rpcData; + const { txn, hash, rpcTxn } = props; const [receipt, setReceipt] = useState< NestedReceiptWithOutcome | FailedToFindReceipt | any @@ -78,78 +67,6 @@ const Execution = (props: Props) => { return receipts; } - useEffect(() => { - const checkTxnExistence = async () => { - if (txn === null) { - try { - const txnExists: any = await transactionStatus(hash, 'bowen'); - const status = txnExists.status?.Failure ? false : true; - let block: any = {}; - - if (txnExists) { - block = await getBlockDetails( - txnExists.transaction_outcome.block_hash, - ); - } - - const modifiedTxns = { - transaction_hash: txnExists.transaction_outcome.id, - included_in_block_hash: txnExists.transaction_outcome.block_hash, - outcomes: { status: status }, - block: { block_height: block?.header.height }, - block_timestamp: block?.header.timestamp_nanosec, - receiver_account_id: txnExists.transaction.receiver_id, - signer_account_id: txnExists.transaction.signer_id, - receipt_conversion_gas_burnt: - txnExists.transaction_outcome.outcome.gas_burnt.toString(), - receipt_conversion_tokens_burnt: - txnExists.transaction_outcome.outcome.tokens_burnt, - actions_agg: { - deposit: calculateTotalDeposit(txnExists?.transaction.actions), - gas_attached: calculateTotalGas(txnExists?.transaction.actions), - }, - outcomes_agg: { - transaction_fee: txnFee( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', - ), - gas_used: calculateGasUsed( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', - ), - }, - }; - if (txnExists) { - setRpcTxn(txnExists); - setRpcData(modifiedTxns); - } - } catch (error) {} - } - }; - - checkTxnExistence(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, hash, rpcUrl]); - - useEffect(() => { - const fetchTransactionStatus = async () => { - if (!txn) return; - - try { - const res = await transactionStatus( - txn.transaction_hash, - txn.signer_account_id, - ); - setRpcTxn(res); - } catch {} - }; - - fetchTransactionStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, rpcUrl]); - useEffect(() => { if (!isEmpty(rpcTxn)) { setReceipt(transactionReceipts(rpcTxn)); diff --git a/apps/app/src/components/app/Transactions/Receipt.tsx b/apps/app/src/components/app/Transactions/Receipt.tsx index de4175a7..f556055c 100644 --- a/apps/app/src/components/app/Transactions/Receipt.tsx +++ b/apps/app/src/components/app/Transactions/Receipt.tsx @@ -1,41 +1,29 @@ 'use client'; -import { - calculateGasUsed, - calculateTotalDeposit, - calculateTotalGas, - mapRpcActionToAction, - txnFee, -} from '@/utils/near'; -import { - ExecutionOutcomeWithIdView, - RPCTransactionInfo, - TransactionInfo, -} from '@/utils/types'; +import { mapRpcActionToAction } from '@/utils/near'; +import { RPCTransactionInfo, TransactionInfo } from '@/utils/types'; import { useEffect, useState } from 'react'; import FaHourglassStart from '../Icons/FaHourglassStart'; import ReceiptRow from './Receipts/ReceiptRow'; import { isEmpty } from 'lodash'; -import useRpc from '@/hooks/useRpc'; -import { useRpcStore } from '@/stores/rpc'; + import ErrorMessage from '../common/ErrorMessage'; import FileSlash from '../Icons/FileSlash'; interface Props { txn: TransactionInfo; - loading: boolean; hash: string; + rpcTxn: RPCTransactionInfo; + statsData: { + stats: Array<{ + near_price: string; + }>; + }; } const Receipt = (props: Props) => { - const { txn: txnData, loading, hash } = props; - const { transactionStatus, getBlockDetails } = useRpc(); - const rpcUrl: string = useRpcStore((state) => state.rpc); + const { txn, hash, rpcTxn, statsData } = props; const [receipt, setReceipt] = useState(null); - const [rpcTxn, setRpcTxn] = useState({}); - const [rpcData, setRpcData] = useState({}); - - const txn = txnData ? txnData : rpcData; function transactionReceipts(txn: RPCTransactionInfo) { const actions: any = @@ -97,78 +85,6 @@ const Receipt = (props: Props) => { return collectReceipts(receiptsOutcome[0]?.id); } - useEffect(() => { - const checkTxnExistence = async () => { - if (txn === null) { - try { - const txnExists: any = await transactionStatus(hash, 'bowen'); - const status = txnExists.status?.Failure ? false : true; - let block: any = {}; - - if (txnExists) { - block = await getBlockDetails( - txnExists.transaction_outcome.block_hash, - ); - } - - const modifiedTxns = { - transaction_hash: txnExists.transaction_outcome.id, - included_in_block_hash: txnExists.transaction_outcome.block_hash, - outcomes: { status: status }, - block: { block_height: block?.header.height }, - block_timestamp: block?.header.timestamp_nanosec, - receiver_account_id: txnExists.transaction.receiver_id, - signer_account_id: txnExists.transaction.signer_id, - receipt_conversion_gas_burnt: - txnExists.transaction_outcome.outcome.gas_burnt.toString(), - receipt_conversion_tokens_burnt: - txnExists.transaction_outcome.outcome.tokens_burnt, - actions_agg: { - deposit: calculateTotalDeposit(txnExists?.transaction.actions), - gas_attached: calculateTotalGas(txnExists?.transaction.actions), - }, - outcomes_agg: { - transaction_fee: txnFee( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', - ), - gas_used: calculateGasUsed( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', - ), - }, - }; - if (txnExists) { - setRpcTxn(txnExists); - setRpcData(modifiedTxns); - } - } catch (error) {} - } - }; - - checkTxnExistence(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, hash, rpcUrl]); - - useEffect(() => { - const fetchTransactionStatus = async () => { - if (!txn) return; - - try { - const res = await transactionStatus( - txn.transaction_hash, - txn.signer_account_id, - ); - setRpcTxn(res); - } catch {} - }; - - fetchTransactionStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, rpcUrl]); - useEffect(() => { if (!isEmpty(rpcTxn)) { setReceipt(transactionReceipts(rpcTxn)); @@ -177,6 +93,7 @@ const Receipt = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [rpcTxn]); const txnsPending = txn?.outcomes?.status === null; + return ( <> {!txn ? ( @@ -205,7 +122,7 @@ const Receipt = (props: Props) => { ) : ( - + )} )} diff --git a/apps/app/src/components/app/Transactions/ReceiptSummary.tsx b/apps/app/src/components/app/Transactions/ReceiptSummary.tsx index 333a64c6..269d8cad 100644 --- a/apps/app/src/components/app/Transactions/ReceiptSummary.tsx +++ b/apps/app/src/components/app/Transactions/ReceiptSummary.tsx @@ -1,16 +1,6 @@ 'use client'; -import { - calculateGasUsed, - calculateTotalDeposit, - calculateTotalGas, - mapRpcActionToAction, - txnFee, -} from '@/utils/near'; -import { - ExecutionOutcomeWithIdView, - RPCTransactionInfo, - TransactionInfo, -} from '@/utils/types'; +import { mapRpcActionToAction } from '@/utils/near'; +import { RPCTransactionInfo, TransactionInfo } from '@/utils/types'; import { isEmpty } from 'lodash'; import { useEffect, useState } from 'react'; import FaHourglassStart from '../Icons/FaHourglassStart'; @@ -19,8 +9,7 @@ import ErrorMessage from '../common/ErrorMessage'; import FaInbox from '../Icons/FaInbox'; import ReceiptSummaryRow from './Receipts/ReceiptSummaryRow'; import { useTranslations } from 'next-intl'; -import { useRpcStore } from '@/stores/rpc'; -import useRpc from '@/hooks/useRpc'; + import FileSlash from '../Icons/FileSlash'; interface Props { @@ -28,16 +17,16 @@ interface Props { loading: boolean; price: string; hash: string; + rpcTxn: RPCTransactionInfo; + statsData: { + stats: Array<{ + near_price: string; + }>; + }; } const ReceiptSummary = (props: Props) => { - const { txn: txnData, loading, price, hash } = props; - const rpcUrl: string = useRpcStore((state) => state.rpc); - const { transactionStatus, getBlockDetails } = useRpc(); - const [rpcTxn, setRpcTxn] = useState({}); - const [rpcData, setRpcData] = useState({}); - - const txn = txnData ? txnData : rpcData; + const { txn, loading, price, hash, rpcTxn, statsData } = props; const t = useTranslations(); const [receipt, setReceipt] = useState(null); @@ -101,78 +90,6 @@ const ReceiptSummary = (props: Props) => { return collectReceipts(receiptsOutcome[0]?.id); } - useEffect(() => { - const checkTxnExistence = async () => { - if (txn === null) { - try { - const txnExists: any = await transactionStatus(hash, 'bowen'); - const status = txnExists.status?.Failure ? false : true; - let block: any = {}; - - if (txnExists) { - block = await getBlockDetails( - txnExists.transaction_outcome.block_hash, - ); - } - - const modifiedTxns = { - transaction_hash: txnExists.transaction_outcome.id, - included_in_block_hash: txnExists.transaction_outcome.block_hash, - outcomes: { status: status }, - block: { block_height: block?.header.height }, - block_timestamp: block?.header.timestamp_nanosec, - receiver_account_id: txnExists.transaction.receiver_id, - signer_account_id: txnExists.transaction.signer_id, - receipt_conversion_gas_burnt: - txnExists.transaction_outcome.outcome.gas_burnt.toString(), - receipt_conversion_tokens_burnt: - txnExists.transaction_outcome.outcome.tokens_burnt, - actions_agg: { - deposit: calculateTotalDeposit(txnExists?.transaction.actions), - gas_attached: calculateTotalGas(txnExists?.transaction.actions), - }, - outcomes_agg: { - transaction_fee: txnFee( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', - ), - gas_used: calculateGasUsed( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', - ), - }, - }; - if (txnExists) { - setRpcTxn(txnExists); - setRpcData(modifiedTxns); - } - } catch (error) {} - } - }; - - checkTxnExistence(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, hash, rpcUrl]); - - useEffect(() => { - const fetchTransactionStatus = async () => { - if (!txn) return; - - try { - const res = await transactionStatus( - txn.transaction_hash, - txn.signer_account_id, - ); - setRpcTxn(res); - } catch {} - }; - - fetchTransactionStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, rpcUrl]); - useEffect(() => { if (!isEmpty(rpcTxn)) { setReceipt(transactionReceipts(rpcTxn)); @@ -313,6 +230,7 @@ const ReceiptSummary = (props: Props) => { txn={txn} receipt={receipt} price={price} + statsData={statsData} /> )}{' '} diff --git a/apps/app/src/components/app/Transactions/Receipts/ReceiptInfo.tsx b/apps/app/src/components/app/Transactions/Receipts/ReceiptInfo.tsx index 25e8ddfb..6b24568c 100644 --- a/apps/app/src/components/app/Transactions/Receipts/ReceiptInfo.tsx +++ b/apps/app/src/components/app/Transactions/Receipts/ReceiptInfo.tsx @@ -1,6 +1,6 @@ import TxnsReceiptStatus from '@/components/common/TxnsReceiptStatus'; import Question from '@/components/Icons/Question'; -import useRpc from '@/hooks/useRpc'; +import useRpc from '@/hooks/app/useRpc'; import { Link } from '@/i18n/routing'; import { hexy } from '@/utils/hexy'; import { convertToMetricPrefix, localFormat, yoctoToNear } from '@/utils/libs'; diff --git a/apps/app/src/components/app/Transactions/Receipts/ReceiptRow.tsx b/apps/app/src/components/app/Transactions/Receipts/ReceiptRow.tsx index d2bcbd20..05ba48b8 100644 --- a/apps/app/src/components/app/Transactions/Receipts/ReceiptRow.tsx +++ b/apps/app/src/components/app/Transactions/Receipts/ReceiptRow.tsx @@ -5,27 +5,36 @@ import { Tooltip } from '@reach/tooltip'; import TransactionActions from './TransactionActions'; import ReceiptStatus from './ReceiptStatus'; import { useEffect, useRef, useState } from 'react'; -import useRpc from '@/hooks/useRpc'; import TxnsReceiptStatus from '@/components/common/TxnsReceiptStatus'; import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import useHash from '@/hooks/app/useHash'; +import { useConfig } from '@/hooks/app/useConfig'; +import { fiatValue } from '@/utils/app/libs'; +import useRpc from '@/hooks/app/useRpc'; interface Props { receipt: ReceiptsPropsInfo | any; borderFlag?: boolean; - loading: boolean; + statsData: { + stats: Array<{ + near_price: string; + }>; + }; } const ReceiptRow = (props: Props) => { - const { receipt, borderFlag, loading } = props; + const { receipt, borderFlag, statsData } = props; const t = useTranslations(); const [block, setBlock] = useState<{ height: string } | null>(null); const { getBlockDetails } = useRpc(); const [pageHash] = useHash(); - + const loading = false; + const currentPrice = statsData?.stats?.[0]?.near_price || 0; + const deposit = receipt?.actions?.[0]?.args?.deposit ?? 0; const lastBlockHash = useRef(null); const rowRef = useRef(null); + const { networkId } = useConfig(); useEffect(() => { if (receipt?.block_hash && receipt.block_hash !== lastBlockHash.current) { @@ -116,7 +125,7 @@ const ReceiptRow = (props: Props) => { )} -
    +
    { '' )}
    +
    +
    + +
    + +
    +
    + Value +
    + {!receipt || loading ? ( +
    + + + +
    + ) : ( +
    + {receipt && deposit ? yoctoToNear(deposit, true) : deposit ?? '0'}{' '} + Ⓝ + {currentPrice && networkId === 'mainnet' + ? ` ($${fiatValue( + yoctoToNear(deposit ?? 0, false), + currentPrice, + )})` + : ''} +
    + )} +
    { {receipt?.outcome?.outgoing_receipts?.map((rcpt: any) => (
    - +
    ))} diff --git a/apps/app/src/components/app/Transactions/Receipts/ReceiptSummaryRow.tsx b/apps/app/src/components/app/Transactions/Receipts/ReceiptSummaryRow.tsx index b70bfa8f..2f46411c 100644 --- a/apps/app/src/components/app/Transactions/Receipts/ReceiptSummaryRow.tsx +++ b/apps/app/src/components/app/Transactions/Receipts/ReceiptSummaryRow.tsx @@ -18,14 +18,19 @@ interface Props { receipt: ReceiptsPropsInfo | any; borderFlag?: boolean; price: string; + statsData: { + stats: Array<{ + near_price: string; + }>; + }; } const ReceiptSummaryRow = (props: Props) => { const { networkId } = useConfig(); - const { receipt, txn, price } = props; + const { receipt, txn, price, statsData } = props; - const currentPrice = price ? price : 0; + const currentPrice = statsData?.stats?.[0]?.near_price || 0; function formatActionKind(actionKind: string) { return actionKind.replace(/([a-z])([A-Z])/g, '$1 $2'); @@ -153,6 +158,7 @@ const ReceiptSummaryRow = (props: Props) => { borderFlag={true} txn={txn} price={price} + statsData={statsData} /> ))} diff --git a/apps/app/src/components/app/Transactions/Tree.tsx b/apps/app/src/components/app/Transactions/Tree.tsx index bb47c927..e007aa5f 100644 --- a/apps/app/src/components/app/Transactions/Tree.tsx +++ b/apps/app/src/components/app/Transactions/Tree.tsx @@ -1,40 +1,24 @@ 'use client'; -import { - calculateGasUsed, - calculateTotalDeposit, - calculateTotalGas, - mapRpcActionToAction, - txnFee, -} from '@/utils/near'; -import { - ExecutionOutcomeWithIdView, - RPCTransactionInfo, - TransactionInfo, -} from '@/utils/types'; +import { mapRpcActionToAction } from '@/utils/near'; +import { RPCTransactionInfo, TransactionInfo } from '@/utils/types'; import { isEmpty } from 'lodash'; import { useEffect, useState } from 'react'; import FaHourglassStart from '../Icons/FaHourglassStart'; import Skeleton from '../skeleton/common/Skeleton'; import TreeReceipt from './TreeReceipts/TreeReceipt'; import TreeReceiptDetails from './TreeReceipts/TreeReceiptDetails'; -import { useRpcStore } from '@/stores/rpc'; -import useRpc from '@/hooks/useRpc'; + import ErrorMessage from '../common/ErrorMessage'; import FileSlash from '../Icons/FileSlash'; interface Props { txn: TransactionInfo; hash: string; + rpcTxn: RPCTransactionInfo; } const Tree = (props: Props) => { - const { txn: txnData, hash } = props; - const rpcUrl: string = useRpcStore((state) => state.rpc); - const { transactionStatus, getBlockDetails } = useRpc(); - const [rpcTxn, setRpcTxn] = useState({}); - const [rpcData, setRpcData] = useState({}); - - const txn = txnData ? txnData : rpcData; + const { txn, hash, rpcTxn } = props; const [receipt, setReceipt] = useState(null); const [show, setShow] = useState(null); @@ -98,78 +82,6 @@ const Tree = (props: Props) => { return collectReceipts(receiptsOutcome[0]?.id); } - useEffect(() => { - const checkTxnExistence = async () => { - if (txn === null) { - try { - const txnExists: any = await transactionStatus(hash, 'bowen'); - const status = txnExists.status?.Failure ? false : true; - let block: any = {}; - - if (txnExists) { - block = await getBlockDetails( - txnExists.transaction_outcome.block_hash, - ); - } - - const modifiedTxns = { - transaction_hash: txnExists.transaction_outcome.id, - included_in_block_hash: txnExists.transaction_outcome.block_hash, - outcomes: { status: status }, - block: { block_height: block?.header.height }, - block_timestamp: block?.header.timestamp_nanosec, - receiver_account_id: txnExists.transaction.receiver_id, - signer_account_id: txnExists.transaction.signer_id, - receipt_conversion_gas_burnt: - txnExists.transaction_outcome.outcome.gas_burnt.toString(), - receipt_conversion_tokens_burnt: - txnExists.transaction_outcome.outcome.tokens_burnt, - actions_agg: { - deposit: calculateTotalDeposit(txnExists?.transaction.actions), - gas_attached: calculateTotalGas(txnExists?.transaction.actions), - }, - outcomes_agg: { - transaction_fee: txnFee( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', - ), - gas_used: calculateGasUsed( - (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? - [], - txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', - ), - }, - }; - if (txnExists) { - setRpcTxn(txnExists); - setRpcData(modifiedTxns); - } - } catch (error) {} - } - }; - - checkTxnExistence(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, hash, rpcUrl]); - - useEffect(() => { - const fetchTransactionStatus = async () => { - if (!txn) return; - - try { - const res = await transactionStatus( - txn.transaction_hash, - txn.signer_account_id, - ); - setRpcTxn(res); - } catch {} - }; - - fetchTransactionStatus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txn, rpcUrl]); - useEffect(() => { if (!isEmpty(rpcTxn)) { const receipt = transactionReceipts(rpcTxn); diff --git a/apps/app/src/components/app/Transactions/TxnsTabActions.tsx b/apps/app/src/components/app/Transactions/TxnsTabActions.tsx new file mode 100644 index 00000000..7f4ac9c7 --- /dev/null +++ b/apps/app/src/components/app/Transactions/TxnsTabActions.tsx @@ -0,0 +1,278 @@ +'use client'; +import { useEffect, useRef, useState } from 'react'; +import Details from './Details'; +import Execution from './Execution'; +import Receipt from './Receipt'; +import ReceiptSummary from './ReceiptSummary'; +import Tree from './Tree'; +import { + calculateGasUsed, + calculateTotalDeposit, + calculateTotalGas, + txnFee, +} from '@/utils/near'; +import { ExecutionOutcomeWithIdView } from '@/utils/types'; +import { useRpcProvider } from '@/hooks/app/useRpcProvider'; +import { useRpcStore } from '@/stores/app/rpc'; +import { useRouter } from 'next/navigation'; +import { usePathname } from '@/i18n/routing'; +import { Link } from '@/i18n/routing'; +import classNames from 'classnames'; +import ErrorMessage from '../common/ErrorMessage'; +import FileSlash from '../Icons/FileSlash'; +import useRpc from '@/hooks/app/useRpc'; + +export type RpcProvider = { + name: string; + url: string; +}; + +const TxnsTabActions = ({ tab, txn, stats, isContract, price, hash }: any) => { + const { transactionStatus, getBlockDetails } = useRpc(); + const [rpcError, setRpcError] = useState(false); + const [rpcTxn, setRpcTxn] = useState({}); + const [rpcData, setRpcData] = useState({}); + const [allRpcProviderError, setAllRpcProviderError] = useState(false); + const initializedRef = useRef(false); + const retryCount = useRef(0); + const timeoutRef = useRef(null); + const router = useRouter(); + const pathname = usePathname(); + + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + + const { switchRpc, rpc: rpcUrl } = useRpcStoreWithProviders(); + + useEffect(() => { + if (txn === null || !txn) { + const delay = Math.min(1000 * 2 ** retryCount.current, 150000); + timeoutRef.current = setTimeout(() => { + router.replace(pathname); + retryCount.current += 1; + }, delay); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [txn, router, retryCount, rpcUrl]); + + useEffect(() => { + if (rpcError) { + try { + switchRpc(); + } catch (error) { + setRpcError(true); + setAllRpcProviderError(true); + console.error('Failed to switch RPC:', error); + } + } + }, [rpcError, switchRpc]); + + useEffect(() => { + const checkTxnExistence = async () => { + if (txn === undefined || txn === null) { + try { + setRpcError(false); + const txnExists: any = await transactionStatus(hash, 'bowen'); + const status = txnExists.status?.Failure ? false : true; + let block: any = {}; + + if (txnExists) { + block = await getBlockDetails( + txnExists.transaction_outcome.block_hash, + ); + } + + const modifiedTxns = { + transaction_hash: txnExists.transaction_outcome.id, + included_in_block_hash: txnExists.transaction_outcome.block_hash, + outcomes: { status: status }, + block: { block_height: block?.header.height }, + block_timestamp: block?.header.timestamp_nanosec, + receiver_account_id: txnExists.transaction.receiver_id, + signer_account_id: txnExists.transaction.signer_id, + receipt_conversion_gas_burnt: + txnExists.transaction_outcome.outcome.gas_burnt.toString(), + receipt_conversion_tokens_burnt: + txnExists.transaction_outcome.outcome.tokens_burnt, + actions_agg: { + deposit: calculateTotalDeposit(txnExists?.transaction.actions), + gas_attached: calculateTotalGas(txnExists?.transaction.actions), + }, + outcomes_agg: { + transaction_fee: txnFee( + (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? + [], + txnExists?.transaction_outcome.outcome.tokens_burnt ?? '0', + ), + gas_used: calculateGasUsed( + (txnExists?.receipts_outcome as ExecutionOutcomeWithIdView[]) ?? + [], + txnExists?.transaction_outcome.outcome.gas_burnt ?? '0', + ), + }, + }; + if (txnExists) { + setRpcTxn(txnExists); + setRpcData(modifiedTxns); + } + } catch (error) { + setRpcError(true); + } + } + }; + + checkTxnExistence(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [txn, hash, rpcUrl]); + + useEffect(() => { + const fetchTransactionStatus = async () => { + if (!txn) return; + + try { + setRpcError(false); + const res = await transactionStatus( + txn.transaction_hash, + txn.signer_account_id, + ); + setRpcTxn(res); + } catch { + setRpcError(true); + } + }; + + fetchTransactionStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [txn, rpcUrl]); + + const tabs = [ + { name: 'overview', message: 'txn.tabs.overview', label: 'Overview' }, + { + name: 'execution', + message: 'txn.tabs.execution', + label: 'Execution Plan', + }, + { name: 'enhanced', message: 'tokenTxns', label: 'Enhanced Plan' }, + { name: 'tree', message: 'nftTokenTxns', label: 'Tree Plan' }, + { name: 'summary', message: 'accessKeys', label: 'Reciept Summary' }, + ]; + + const getClassName = (selected: boolean) => + classNames( + 'text-xs leading-4 font-medium overflow-hidden inline-block cursor-pointer p-2 mb-3 mr-2 focus:outline-none rounded-lg', + { + 'hover:bg-neargray-800 bg-neargray-700 dark:bg-black-200 hover:text-nearblue-600 text-nearblue-600 dark:text-neargray-10': + !selected, + 'bg-green-600 dark:bg-green-250 text-white': selected, + }, + ); + + return ( + <> +
    + {rpcError && (!txn || allRpcProviderError) ? ( +
    +
    + } + message="Sorry, we are unable to locate this transaction hash. Please try again later." + mutedText={hash || ''} + /> +
    +
    + ) : ( + <> +
    +
    + {tabs?.map(({ name, label }) => { + return ( + +

    + {label} + {name === 'enhanced' && ( +
    + NEW +
    + )} +

    + + ); + })} +
    +
    +
    + <> + {tab === 'overview' ? ( +
    + ) : null} + {tab === 'execution' ? ( + + ) : null} + {tab === 'enhanced' ? ( + + ) : null} + {tab === 'tree' ? ( + + ) : null} + {tab === 'summary' ? ( + + ) : null} + +
    + + )} +
    +
    + + ); +}; + +export default TxnsTabActions; diff --git a/apps/app/src/components/app/Transactions/TxnsTabs.tsx b/apps/app/src/components/app/Transactions/TxnsTabs.tsx index e1059c11..5ef8ab82 100644 --- a/apps/app/src/components/app/Transactions/TxnsTabs.tsx +++ b/apps/app/src/components/app/Transactions/TxnsTabs.tsx @@ -1,12 +1,6 @@ -import { Link } from '@/i18n/routing'; -import classNames from 'classnames'; -import Details from '@/components/app/Transactions/Details'; -import Receipt from '@/components/app/Transactions/Receipt'; -import Execution from '@/components/app/Transactions/Execution'; -import Tree from '@/components/app/Transactions/Tree'; -import ReceiptSummary from '@/components/app/Transactions/ReceiptSummary'; import { getRequest } from '@/utils/app/api'; import { nanoToMilli } from '@/utils/app/libs'; +import TxnsTabActions from './TxnsTabActions'; export default async function TxnsTabs({ hash, @@ -17,6 +11,7 @@ export default async function TxnsTabs({ searchParams: any; }) { const data = (await getRequest(`txns/${hash}`)) || {}; + const stats = (await getRequest(`stats`)) || []; const txn = data?.txns?.[0]; let price: number | null = null; if (txn?.block_timestamp) { @@ -43,88 +38,14 @@ export default async function TxnsTabs({ const tab = searchParams?.tab || 'overview'; - const tabs = [ - { name: 'overview', message: 'txn.tabs.overview', label: 'Overview' }, - { - name: 'execution', - message: 'txn.tabs.execution', - label: 'Execution Plan', - }, - { name: 'enhanced', message: 'tokenTxns', label: 'Enhanced Plan' }, - { name: 'tree', message: 'nftTokenTxns', label: 'Tree Plan' }, - { name: 'summary', message: 'accessKeys', label: 'Reciept Summary' }, - ]; - - const getClassName = (selected: boolean) => - classNames( - 'text-xs leading-4 font-medium overflow-hidden inline-block cursor-pointer p-2 mb-3 mr-2 focus:outline-none rounded-lg', - { - 'hover:bg-neargray-800 bg-neargray-700 dark:bg-black-200 hover:text-nearblue-600 text-nearblue-600 dark:text-neargray-10': - !selected, - 'bg-green-600 dark:bg-green-250 text-white': selected, - }, - ); - return ( - <> -
    - <> -
    -
    - {tabs?.map(({ name, label }) => { - return ( - -

    - {label} - {name === 'enhanced' && ( -
    - NEW -
    - )} -

    - - ); - })} -
    -
    -
    - {tab === 'overview' ? ( -
    - ) : null} - - {tab === 'execution' ? ( - - ) : null} - - {tab === 'enhanced' ? : null} - {tab === 'tree' ? : null} - {tab === 'summary' ? ( - - ) : null} -
    - -
    -
    - + ); } diff --git a/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx b/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx index 39821d50..25810134 100644 --- a/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx +++ b/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx @@ -32,7 +32,6 @@ function TxnsTabsSkeleton({ tab, hash }: { tab: string; hash: string }) { return ( <>
    - {/* */} <>
    diff --git a/apps/app/src/components/common/TokenInfo.tsx b/apps/app/src/components/common/TokenInfo.tsx index 592ba8d7..16a53017 100644 --- a/apps/app/src/components/common/TokenInfo.tsx +++ b/apps/app/src/components/common/TokenInfo.tsx @@ -6,9 +6,9 @@ import { } from '@/utils/libs'; import { MetaInfo, TokenInfoProps } from '@/utils/types'; import { useEffect, useState } from 'react'; -import useRpc from '@/hooks/useRpc'; import TokenImage from './TokenImage'; import { Link } from '@/i18n/routing'; +import useRpc from '@/hooks/app/useRpc'; const TokenInfo = (props: TokenInfoProps) => { const { contract, amount, decimals } = props; diff --git a/apps/app/src/hooks/app/useConfig.ts b/apps/app/src/hooks/app/useConfig.ts index 6afbec94..f7d9354a 100644 --- a/apps/app/src/hooks/app/useConfig.ts +++ b/apps/app/src/hooks/app/useConfig.ts @@ -14,6 +14,7 @@ export const useConfig = () => { NEXT_PUBLIC_BOS_NETWORK, NEXT_PUBLIC_MAINNET_URL, NEXT_PUBLIC_TESTNET_URL, + NEXT_PUBLIC_TURNSTILE_SITE_KEY, } = useEnvContext(); const networkId: NetworkId = @@ -28,6 +29,8 @@ export const useConfig = () => { const network = networks[networkId]; + const siteKey = NEXT_PUBLIC_TURNSTILE_SITE_KEY; + const apiUrl = networkId === 'mainnet' ? 'https://api3.nearblocks.io/v1/' @@ -78,5 +81,6 @@ export const useConfig = () => { docsUrl, aurorablocksUrl, verifierConfig, + siteKey, }; }; diff --git a/apps/app/src/hooks/app/useRpc.ts b/apps/app/src/hooks/app/useRpc.ts new file mode 100644 index 00000000..de5ba5e5 --- /dev/null +++ b/apps/app/src/hooks/app/useRpc.ts @@ -0,0 +1,272 @@ +import { providers } from 'near-api-js'; +import { AccessInfo } from '@/utils/types'; +import { baseDecode } from 'borsh'; +import { decodeArgs, encodeArgs } from '@/utils/app/near'; +import { useRpcProvider } from './useRpcProvider'; +import { useEffect, useRef } from 'react'; +import { useRpcStore } from '@/stores/app/rpc'; + +const useRpc = () => { + const initializedRef = useRef(false); + const useRpcStoreWithProviders = () => { + const setProviders = useRpcStore((state) => state.setProviders); + const { RpcProviders } = useRpcProvider(); + useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + setProviders(RpcProviders); + } + }, [RpcProviders, setProviders]); + + return useRpcStore((state) => state); + }; + const { rpc: rpcUrl } = useRpcStoreWithProviders(); + const jsonProviders = [new providers.JsonRpcProvider({ url: rpcUrl })]; + + const provider = new providers.FailoverRpcProvider(jsonProviders); + + const getBlockDetails = async (blockId: number | string) => { + try { + const block = await provider.block({ blockId }); + return block; + } catch (error) { + console.error('Error fetching latest block details:', error); + return null; + } + }; + + const contractCode = async (address: string) => + provider.query({ + request_type: 'view_code', + finality: 'final', + account_id: address, + }); + + const viewAccessKeys = async (address: string) => + provider.query({ + request_type: 'view_access_key_list', + finality: 'final', + account_id: address, + }); + + const viewAccount = async (accountId: string) => + provider.query({ + request_type: 'view_account', + finality: 'final', + account_id: accountId, + }); + + const ftBalanceOf = async ( + contract: string, + account_id: string | undefined, + ) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'final', + account_id: contract, + method_name: 'ft_balance_of', + args_base64: encodeArgs({ account_id }), + }); + const result = (resp as any).result; + + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const viewAccessKey = async ( + address: string, + key: string, + ): Promise => { + const response = await provider.query({ + request_type: 'view_access_key', + finality: 'final', + account_id: address, + public_key: key, + }); + return response as unknown as AccessInfo; + }; + + const getAccount = async (poolId: string, account_id: string | undefined) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: poolId, + method_name: 'get_account', + args_base64: encodeArgs({ account_id }), + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const getNumberOfAccounts = async (poolId: string) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: poolId, + method_name: 'get_number_of_accounts', + args_base64: 'e30=', + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const getAccounts = async ( + poolId: string, + start: number, + limit: number, + setLoading: (loading: boolean) => void, + setError: (error: boolean) => void, + ) => { + setLoading(true); + setError(false); + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: poolId, + method_name: 'get_accounts', + args_base64: encodeArgs({ from_index: start, limit: limit }), + }); + const result = (resp as any).result; + setLoading(false); + return decodeArgs(result); + } catch (error) { + setError(true); + setLoading(false); + console.error('Error fetching accounts:', error); + return null; + } finally { + setLoading(false); + } + }; + + const getValidators = async () => { + try { + const validators = await provider.validators(null); + return validators; + } catch (error) { + console.error('Error fetching validators:', error); + return null; + } + }; + + const getRewardFeeFraction = async (poolId: string) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: poolId, + method_name: 'get_reward_fee_fraction', + args_base64: 'e30=', + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const getFieldsByPool = async (poolId: string) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: 'pool-details.near', + method_name: 'get_fields_by_pool', + args_base64: encodeArgs({ pool_id: poolId }), + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const ftMetadata = async (contract: string) => { + try { + const resp = (await provider.query({ + request_type: 'call_function', + finality: 'final', + account_id: contract, + method_name: 'ft_metadata', + args_base64: '', + })) as any; + + return decodeArgs(resp.result); + } catch (error) { + return {}; + } + }; + + const transactionStatus = async (hash: any, signer: any) => { + const decodedHash = baseDecode(hash); + const uint8ArrayHash = new Uint8Array(decodedHash); + return provider.txStatusReceipts(uint8ArrayHash, signer, 'NONE'); + }; + + const getContractMetadata = async (accountId: string) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: accountId, + method_name: 'contract_source_metadata', + args_base64: '', + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + const getVerifierData = async ( + accountId: string, + verifierAccountId: string, + ) => { + try { + const resp = await provider.query({ + request_type: 'call_function', + finality: 'optimistic', + account_id: verifierAccountId, + method_name: 'get_contract', + args_base64: encodeArgs({ account_id: accountId }), + }); + const result = (resp as any).result; + return decodeArgs(result); + } catch (error) { + return null; + } + }; + + return { + getBlockDetails, + contractCode, + viewAccessKeys, + viewAccount, + ftBalanceOf, + viewAccessKey, + getAccount, + getAccounts, + getNumberOfAccounts, + getValidators, + getRewardFeeFraction, + getFieldsByPool, + ftMetadata, + transactionStatus, + getContractMetadata, + getVerifierData, + }; +}; +export default useRpc; diff --git a/apps/app/src/hooks/app/useRpcProvider.ts b/apps/app/src/hooks/app/useRpcProvider.ts new file mode 100644 index 00000000..9eba18c5 --- /dev/null +++ b/apps/app/src/hooks/app/useRpcProvider.ts @@ -0,0 +1,46 @@ +import { useConfig } from './useConfig'; + +export const useRpcProvider = () => { + const { networkId } = useConfig(); + const RpcProviders = + networkId === 'mainnet' + ? [ + { + name: 'NEAR (Archival)', + url: 'https://archival-rpc.mainnet.near.org', + }, + { + name: 'NEAR', + url: 'https://rpc.mainnet.near.org', + }, + { + name: 'NEAR (Beta)', + url: 'https://beta.rpc.mainnet.near.org', + }, + { + name: 'FASTNEAR Free', + url: 'https://free.rpc.fastnear.com', + }, + { + name: 'Lava Network', + url: 'https://near.lava.build', + }, + { + name: 'dRPC', + url: 'https://near.drpc.org', + }, + ] + : [ + { + name: 'NEAR (Archival)', + url: 'https://archival-rpc.testnet.near.org', + }, + { + name: 'NEAR', + url: 'https://rpc.testnet.near.org', + }, + ]; + return { + RpcProviders, + }; +}; diff --git a/apps/app/src/stores/app/rpc.ts b/apps/app/src/stores/app/rpc.ts new file mode 100644 index 00000000..3f55955d --- /dev/null +++ b/apps/app/src/stores/app/rpc.ts @@ -0,0 +1,78 @@ +import { create } from 'zustand'; + +export type RpcProvider = { + name: string; + url: string; +}; + +const setClientCookie = (name: string, value: string, days = 365) => { + if (typeof document !== 'undefined') { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + const expires = `expires=${date.toUTCString()}`; + document.cookie = `${name}=${value};${expires};path=/`; + } +}; + +const getClientCookie = (name: string) => { + if (typeof document !== 'undefined') { + const cookies = document.cookie.split(';').map((cookie) => cookie.trim()); + const targetCookie = cookies.find((cookie) => + cookie.startsWith(`${name}=`), + ); + if (targetCookie) { + return targetCookie.split('=')[1]; + } + } + return null; +}; + +type RpcState = { + rpc: string; + errorCount: number; + providers: RpcProvider[]; + setRpc: (rpc: string) => void; + switchRpc: () => void; + resetErrorCount: () => void; + setProviders: (providers: RpcProvider[]) => void; +}; + +export const useRpcStore = create((set, get) => ({ + rpc: getClientCookie('rpcUrl') || '', + errorCount: 0, + providers: [], + setProviders: (providers) => { + set({ providers }); + const { rpc } = get(); + if (!rpc || !providers?.find((p) => p.url === rpc)) { + const defaultRpc = providers[0]?.url || ''; + if (defaultRpc) { + setClientCookie('rpcUrl', defaultRpc); + set({ rpc: defaultRpc }); + } + } + }, + setRpc: (rpc: string) => { + setClientCookie('rpcUrl', rpc); + set(() => ({ rpc, errorCount: 0 })); + }, + switchRpc: () => { + const { rpc, errorCount, providers } = get(); + + if (errorCount >= providers.length) { + throw new Error('All RPC providers have resulted in errors.'); + } + + const currentIndex = providers.findIndex( + (provider) => provider.url === rpc, + ); + const nextIndex = (currentIndex + 1) % providers.length; + const nextRpc = providers[nextIndex].url; + + setClientCookie('rpcUrl', nextRpc); + set({ rpc: nextRpc, errorCount: errorCount + 1 }); + }, + resetErrorCount: () => { + set({ errorCount: 0 }); + }, +})); diff --git a/apps/app/src/stores/rpc.ts b/apps/app/src/stores/rpc.ts index b989805f..2a3fe0e3 100644 --- a/apps/app/src/stores/rpc.ts +++ b/apps/app/src/stores/rpc.ts @@ -13,8 +13,8 @@ const setClientCookie = (name: string, value: string, days = 365) => { const getClientCookie = (name: string) => { if (typeof document !== 'undefined') { const value = `; ${document.cookie}`; - const parts = value && value?.split(`; ${name}=`); - if (parts?.length === 2) return parts && parts?.pop()?.split(';').shift(); + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop()?.split(';').shift(); } return null; }; diff --git a/apps/app/src/utils/app/rpc.ts b/apps/app/src/utils/app/rpc.ts index a1a6bb78..4fad8cc8 100644 --- a/apps/app/src/utils/app/rpc.ts +++ b/apps/app/src/utils/app/rpc.ts @@ -23,10 +23,6 @@ export const RpcProviders = name: 'Lava Network', url: 'https://near.lava.build', }, - { - name: 'Lavender.Five', - url: 'https://near.lavenderfive.com/', - }, { name: 'dRPC', url: 'https://near.drpc.org',