diff --git a/apps/app/public/images/metamask.svg b/apps/app/public/images/metamask.svg new file mode 100644 index 000000000..5e8961984 --- /dev/null +++ b/apps/app/public/images/metamask.svg @@ -0,0 +1,46 @@ + + + + + metamask + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/app/src/app/[locale]/about/page.tsx b/apps/app/src/app/[locale]/about/page.tsx index 97dca693d..a4529353c 100644 --- a/apps/app/src/app/[locale]/about/page.tsx +++ b/apps/app/src/app/[locale]/about/page.tsx @@ -11,7 +11,7 @@ export default async function About({ return ( <>
-
+

{t('About Nearblocks')}

diff --git a/apps/app/src/app/[locale]/address/[id]/layout.tsx b/apps/app/src/app/[locale]/address/[id]/layout.tsx index 762f85cf7..0872232fc 100644 --- a/apps/app/src/app/[locale]/address/[id]/layout.tsx +++ b/apps/app/src/app/[locale]/address/[id]/layout.tsx @@ -6,21 +6,16 @@ import SponserdText from '@/components/app/SponserdText'; import FaCheckCircle from '@/components/Icons/FaCheckCircle'; import ListCheck from '@/components/Icons/ListCheck'; import { appUrl, networkId } from '@/utils/app/config'; - const network = process.env.NEXT_PUBLIC_NETWORK_ID; - export async function generateMetadata({ params: { id, locale }, }: { params: { id: string; locale: string }; }) { unstable_setRequestLocale(locale); - const thumbnail = `${appUrl}/api/og?account=true&address=${id}`; - const metaTitle = `Near Account ${id} | NearBlocks`; const metaDescription = `Near Account ${id} page allows users to view transactions, balances, token holdings and transfers.`; - return { alternates: { canonical: `${appUrl}/address/${id}`, @@ -55,10 +50,10 @@ export default function AddressLayout({ ); return ( <> -
-
-
-
+
+
+
+

Near Account:  {id && ( @@ -68,38 +63,42 @@ export default function AddressLayout({ )}

- +
-
+
diff --git a/apps/app/src/app/[locale]/advertise/page.tsx b/apps/app/src/app/[locale]/advertise/page.tsx index d92eedc9a..92d49a15f 100644 --- a/apps/app/src/app/[locale]/advertise/page.tsx +++ b/apps/app/src/app/[locale]/advertise/page.tsx @@ -52,7 +52,7 @@ export default async function Advertise({ }) { const t = await getTranslations({ locale }); return ( -
+

diff --git a/apps/app/src/app/[locale]/blocks/[hash]/loading.tsx b/apps/app/src/app/[locale]/blocks/[hash]/loading.tsx index 2473ef0ed..1c05cc72e 100644 --- a/apps/app/src/app/[locale]/blocks/[hash]/loading.tsx +++ b/apps/app/src/app/[locale]/blocks/[hash]/loading.tsx @@ -3,7 +3,7 @@ import HashLoading from '@/components/app/skeleton/blocks/hash'; export default function Loading() { return ( <> -

+
diff --git a/apps/app/src/app/[locale]/blocks/[hash]/page.tsx b/apps/app/src/app/[locale]/blocks/[hash]/page.tsx index d722b4d72..ac5fb71f4 100644 --- a/apps/app/src/app/[locale]/blocks/[hash]/page.tsx +++ b/apps/app/src/app/[locale]/blocks/[hash]/page.tsx @@ -19,7 +19,7 @@ export default async function Hash({ return ( <> -
+
}>
-
+

{t('blockHeading') || 'Latest Near Protocol Blocks'}

-
+
}> diff --git a/apps/app/src/app/[locale]/charts/active-accounts/page.tsx b/apps/app/src/app/[locale]/charts/active-accounts/page.tsx index 73d73f70d..71c09a988 100644 --- a/apps/app/src/app/[locale]/charts/active-accounts/page.tsx +++ b/apps/app/src/app/[locale]/charts/active-accounts/page.tsx @@ -17,14 +17,14 @@ export default async function Tps({ return (
-
+

{t('addressesCharts.heading')}

-
-
+
+
} diff --git a/apps/app/src/app/[locale]/charts/addresses/page.tsx b/apps/app/src/app/[locale]/charts/addresses/page.tsx index aa1e96ca5..312ff55d8 100644 --- a/apps/app/src/app/[locale]/charts/addresses/page.tsx +++ b/apps/app/src/app/[locale]/charts/addresses/page.tsx @@ -56,14 +56,14 @@ export default async function AddressesChart({ return (
-
+

{t('addressesCharts.heading')}

-
-
+
+
}>
-
+

{t('blocksCharts.heading')}

-
-
+
+
}>
-
+

{t('marketCapCharts.heading')}

-
-
+
+
}>
-
+

{t('charts.nearPrice.heading')}

-
-
+
+
}>
-
+

{t('nearSupplyCharts.heading')}

-
-
+
+
}>
-
+

{t('charts.heading')}

-
+
}> diff --git a/apps/app/src/app/[locale]/charts/tps/page.tsx b/apps/app/src/app/[locale]/charts/tps/page.tsx index e457a0ca4..309430a77 100644 --- a/apps/app/src/app/[locale]/charts/tps/page.tsx +++ b/apps/app/src/app/[locale]/charts/tps/page.tsx @@ -52,14 +52,14 @@ export default async function Tps() { return (
-
+

Near Transactions per Second Chart

-
-
+
+
}> diff --git a/apps/app/src/app/[locale]/charts/txn-fee/page.tsx b/apps/app/src/app/[locale]/charts/txn-fee/page.tsx index 15d70fd82..b36a4589e 100644 --- a/apps/app/src/app/[locale]/charts/txn-fee/page.tsx +++ b/apps/app/src/app/[locale]/charts/txn-fee/page.tsx @@ -56,14 +56,14 @@ export default async function TxnFeeChart({ return (
-
+

{t('txnFeeCharts.heading')}

-
-
+
+
}>
-
+

{t('txnVolumeCharts.heading')}

-
-
+
+
}>
-
+

{t('txnsCharts.heading')}

-
-
+
+
}> diff --git a/apps/app/src/app/[locale]/contact/page.tsx b/apps/app/src/app/[locale]/contact/page.tsx index d7af87a1e..366e23c69 100644 --- a/apps/app/src/app/[locale]/contact/page.tsx +++ b/apps/app/src/app/[locale]/contact/page.tsx @@ -54,7 +54,7 @@ export default async function Contact({ <>
-
+

{`Contact NearBlocks`}

diff --git a/apps/app/src/app/[locale]/nft-token/[id]/page.tsx b/apps/app/src/app/[locale]/nft-token/[id]/page.tsx index 30e927a48..419400104 100644 --- a/apps/app/src/app/[locale]/nft-token/[id]/page.tsx +++ b/apps/app/src/app/[locale]/nft-token/[id]/page.tsx @@ -14,7 +14,7 @@ export default async function TokenIndex({ searchParams: { cursor?: string; order: string; p?: string; tab: string }; }) { return ( -
+
-
+

Non-Fungible Token Tracker (NEP-171)

-
+
}>{children} diff --git a/apps/app/src/app/[locale]/nft-tokentxns/page.tsx b/apps/app/src/app/[locale]/nft-tokentxns/page.tsx index d737fac2e..3dc863304 100644 --- a/apps/app/src/app/[locale]/nft-tokentxns/page.tsx +++ b/apps/app/src/app/[locale]/nft-tokentxns/page.tsx @@ -16,13 +16,13 @@ export default async function NFTTokentxns({ return (
-
+

{t ? t('nfts.heading') : 'Non-Fungible Token Transfers'}

-
+
diff --git a/apps/app/src/app/[locale]/node-explorer/[id]/page.tsx b/apps/app/src/app/[locale]/node-explorer/[id]/page.tsx index a321c9801..aeb116b08 100644 --- a/apps/app/src/app/[locale]/node-explorer/[id]/page.tsx +++ b/apps/app/src/app/[locale]/node-explorer/[id]/page.tsx @@ -13,7 +13,7 @@ export default async function Delegator({ params: { id: string }; }) { return ( -
+
{!id ? (
diff --git a/apps/app/src/app/[locale]/node-explorer/page.tsx b/apps/app/src/app/[locale]/node-explorer/page.tsx index a51822fe7..c5d374241 100644 --- a/apps/app/src/app/[locale]/node-explorer/page.tsx +++ b/apps/app/src/app/[locale]/node-explorer/page.tsx @@ -9,13 +9,13 @@ export default async function NodeExplorer({ searchParams }: any) { return ( <>
-
+

NEAR Protocol Validator Explorer

-
+
}> diff --git a/apps/app/src/app/[locale]/token/[id]/page.tsx b/apps/app/src/app/[locale]/token/[id]/page.tsx index bf03419da..922b54deb 100644 --- a/apps/app/src/app/[locale]/token/[id]/page.tsx +++ b/apps/app/src/app/[locale]/token/[id]/page.tsx @@ -14,7 +14,7 @@ export default async function TokenIndex({ searchParams: { cursor?: string; order: string; p?: string; tab: string }; }) { return ( -
+
}> diff --git a/apps/app/src/app/[locale]/tokens/layout.tsx b/apps/app/src/app/[locale]/tokens/layout.tsx index fcf07771b..d6f5ee7fb 100644 --- a/apps/app/src/app/[locale]/tokens/layout.tsx +++ b/apps/app/src/app/[locale]/tokens/layout.tsx @@ -53,13 +53,13 @@ export default async function TokensLayout({ <>
-
+

Near Protocol Ecosystem Tokens (NEP-141)

-
+
}>{children} diff --git a/apps/app/src/app/[locale]/tokentxns/page.tsx b/apps/app/src/app/[locale]/tokentxns/page.tsx index b82955e15..b9d13ebe7 100644 --- a/apps/app/src/app/[locale]/tokentxns/page.tsx +++ b/apps/app/src/app/[locale]/tokentxns/page.tsx @@ -18,13 +18,13 @@ export default async function TokenTxns({ return (
-
+

{t ? t('fts.heading') : 'Token Transfers'}

-
+
}> diff --git a/apps/app/src/app/[locale]/txns/[hash]/layout.tsx b/apps/app/src/app/[locale]/txns/[hash]/layout.tsx index 0cdb60506..919402a98 100644 --- a/apps/app/src/app/[locale]/txns/[hash]/layout.tsx +++ b/apps/app/src/app/[locale]/txns/[hash]/layout.tsx @@ -63,8 +63,8 @@ export default async function TxnsLayout({ ); return ( <> -
-
+
+

{t ? t('txn.heading') : 'Transaction Details'}

@@ -100,7 +100,7 @@ export default async function TxnsLayout({
-
+
diff --git a/apps/app/src/app/[locale]/txns/page.tsx b/apps/app/src/app/[locale]/txns/page.tsx index 34c0a8d0b..1182e54e7 100644 --- a/apps/app/src/app/[locale]/txns/page.tsx +++ b/apps/app/src/app/[locale]/txns/page.tsx @@ -18,7 +18,7 @@ export default async function TransactionList({ return ( <>
-
+

-
+
}> diff --git a/apps/app/src/app/[locale]/verify-contract/page.tsx b/apps/app/src/app/[locale]/verify-contract/page.tsx index fd67ffadd..e946b3610 100644 --- a/apps/app/src/app/[locale]/verify-contract/page.tsx +++ b/apps/app/src/app/[locale]/verify-contract/page.tsx @@ -46,8 +46,8 @@ export default async function VerifyContract({ return ( <>
-
-
+
+

Verify & Publish Contract Source Code

diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx index 56fb8fa8a..8bc9566b5 100644 --- a/apps/app/src/app/layout.tsx +++ b/apps/app/src/app/layout.tsx @@ -11,7 +11,6 @@ interface paramTypes { } export const viewport: Viewport = { - initialScale: 0, userScalable: false, width: 'device-width', }; diff --git a/apps/app/src/components/app/Address/AccountMoreInfo.tsx b/apps/app/src/components/app/Address/AccountMoreInfo.tsx index 1c88527e1..31237bdd1 100644 --- a/apps/app/src/components/app/Address/AccountMoreInfo.tsx +++ b/apps/app/src/components/app/Address/AccountMoreInfo.tsx @@ -14,6 +14,7 @@ import { } from '@/utils/app/libs'; import { AccountDataInfo, ContractCodeInfo } from '@/utils/types'; +import dayjs from '../../../utils/dayjs'; import TokenImage from '../common/TokenImage'; export default function AccountMoreInfo({ @@ -37,12 +38,10 @@ export default function AccountMoreInfo({ console.error(`Error fetching contract code for ${id}:`, error); return null; }), - viewAccessKeys(id as string).catch((error: any) => { console.error(`Error fetching access keys for ${id}:`, error); return null; }), - viewAccount(id as string).catch((error: any) => { console.error(`Error fetching account for ${id}:`, error); return null; @@ -58,7 +57,6 @@ export default function AccountMoreInfo({ } else { setContract(null); } - const locked = (keys?.keys || []).every( (key: { access_key: { @@ -68,7 +66,6 @@ export default function AccountMoreInfo({ public_key: string; }) => key.access_key.permission !== 'FullAccess', ); - if (account) { setAccountView(account); } else { @@ -83,6 +80,19 @@ export default function AccountMoreInfo({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); + let genesis = + accountView !== null && + accountView?.block_hash === undefined && + accountData?.deleted?.transaction_hash + ? false + : accountData?.created?.transaction_hash + ? false + : accountData?.code_hash + ? true + : false; + + const tokenTracker = tokenData?.name || nftTokenData?.name; + return (
@@ -90,155 +100,274 @@ export default function AccountMoreInfo({ {t('moreInfo') || 'Account information'}
-
-
-
- Staked {t('balance') || 'Balance'}: -
- -
- {accountData?.locked - ? yoctoToNear(accountData?.locked, true) + ' Ⓝ' - : accountData?.locked ?? ''} -
-
-
-
- {t('storageUsed') || 'Storage used'}: -
- -
- {accountData?.storage_usage - ? weight(accountData?.storage_usage) - : accountData?.storage_usage ?? ''} -
-
-
- -
-
-
- {accountView !== null && - accountView?.block_hash === undefined && - accountData?.deleted?.transaction_hash - ? 'Deleted At:' - : 'Created At:'} -
- -
- {accountView !== null && - accountView?.block_hash === undefined && - accountData?.deleted?.transaction_hash - ? convertToUTC( - nanoToMilli(accountData.deleted.block_timestamp), - false, - ) - : accountData?.created?.transaction_hash - ? convertToUTC( - nanoToMilli(accountData.created.block_timestamp), - false, - ) - : accountData?.code_hash - ? 'Genesis' - : 'N/A'} +
+
+
+
+ Staked {t('balance') || 'Balance'} +
+
+ {accountData?.locked + ? yoctoToNear(accountData?.locked, true) + ' Ⓝ' + : accountData?.locked ?? ''} +
- {contract && contract?.hash ? ( -
-
Contract Locked:
-
- {contract?.code_base64 && isLocked ? 'Yes' : 'No'} +
+
+
+ {t('storageUsed') || 'Storage used'} +
+
+ {accountData?.storage_usage + ? weight(accountData?.storage_usage) + : accountData?.storage_usage ?? ''}
- ) : ( -
- )} +
- {deploymentData?.receipt_predecessor_account_id && ( -
-
- Contract Creator: -
-
- - {shortenAddress( - deploymentData.receipt_predecessor_account_id ?? '', - )} - - {' at txn '} - - {shortenAddress(deploymentData.transaction_hash ?? '')} - + {(deploymentData?.receipt_predecessor_account_id || + (contract && contract?.hash)) && ( +
+
+ {deploymentData?.receipt_predecessor_account_id && ( +
+
+ Contract Creator +
+
+ + + {shortenAddress( + deploymentData.receipt_predecessor_account_id ?? '', + )} + + + + at txn + + {shortenAddress( + deploymentData.transaction_hash ?? '', + )} + + +
+
+ )}
-
- )} - {tokenData?.name && ( -
-
Token Tracker:
-
-
- - - - {tokenData.name} - - ( - - {tokenData.symbol} - - ) - - {tokenData.price && ( -
- (@ ${localFormat(tokenData.price)}) + {!genesis && ( +
+ {contract && contract?.hash ? ( +
+
+ Contract Locked +
+
+ {contract?.code_base64 && isLocked ? 'Yes' : 'No'} +
+ ) : ( + <> )}
-
+ )}
)} - {nftTokenData?.name && ( -
-
- NFT Token Tracker: +
+ {tokenTracker && ( +
+ {tokenData?.name && ( +
+
+ Token Tracker +
+
+ + + + + + {tokenData.name} + + ( + + {tokenData.symbol} + + ) + + + {tokenData.price && ( + + (@ ${localFormat(tokenData.price)}) + + )} + +
+
+ )} + {nftTokenData?.name && ( +
+
+ NFT Token Tracker +
+
+ + + + + + {nftTokenData?.name} + + ( + + {nftTokenData?.symbol} + + ) + + + +
+
+ )}
-
-
- - - - {nftTokenData?.name} + )} +
+
+
+ {accountView !== null && + accountView?.block_hash === undefined && + accountData?.deleted?.transaction_hash + ? 'Deleted At' + : 'Created At'} +
+ {accountView !== null && + accountView?.block_hash === undefined && + accountData?.deleted?.transaction_hash ? ( +
+ + {dayjs().diff( + dayjs( + convertToUTC( + nanoToMilli(accountData.deleted.block_timestamp), + false, + ), + ), + 'days', + ) === 0 + ? 'Today' + : `${dayjs().diff( + dayjs( + convertToUTC( + nanoToMilli( + accountData.deleted.block_timestamp, + ), + false, + ), + ), + 'days', + )} days ago`} - ( - - {nftTokenData?.symbol} + {!deploymentData?.receipt_predecessor_account_id && ( + + {` at txn`} + + {shortenAddress( + accountData?.deleted?.transaction_hash, + )} + + + )} +
+ ) : accountData?.created?.transaction_hash ? ( +
+ + {dayjs().diff( + dayjs( + convertToUTC( + nanoToMilli(accountData.created.block_timestamp), + false, + ), + ), + 'days', + ) === 0 + ? 'Today' + : `${dayjs().diff( + dayjs( + convertToUTC( + nanoToMilli( + accountData.created.block_timestamp, + ), + false, + ), + ), + 'days', + )} days ago`} - ) - -
+ {!deploymentData?.receipt_predecessor_account_id && ( + + {` at txn`} + + {shortenAddress( + accountData?.created?.transaction_hash, + )} + + + )} +
+ ) : accountData?.code_hash ? ( + 'Genesis' + ) : ( + 'N/A' + )}
- )} + {genesis && ( +
+ {contract && contract?.hash ? ( +
+
+ Contract Locked +
+
+ {contract?.code_base64 && isLocked ? 'Yes' : 'No'} +
+
+ ) : ( + <> + )} +
+ )} +
diff --git a/apps/app/src/components/app/Address/AccountOverview.tsx b/apps/app/src/components/app/Address/AccountOverview.tsx index 6b749bd5d..e600fa5ae 100644 --- a/apps/app/src/components/app/Address/AccountOverview.tsx +++ b/apps/app/src/components/app/Address/AccountOverview.tsx @@ -24,7 +24,6 @@ export default function AccountOverview({ const [ft, setFT] = useState({} as FtInfo); const t = useTranslations(); const { networkId } = useConfig(); - const balance = accountData?.amount ?? ''; const nearPrice = statsData?.near_price ?? ''; @@ -129,7 +128,7 @@ export default function AccountOverview({ )}
-
+
{t('balance') || 'Balance'}:
@@ -139,13 +138,13 @@ export default function AccountOverview({
{networkId === 'mainnet' && ( -
+
{t('value') || 'Value:'}
- + {accountData?.amount && statsData?.near_price ? '$' + fiatValue( @@ -163,8 +162,8 @@ export default function AccountOverview({
)} -
-
+
+
{t('tokens') || 'Tokens:'}
diff --git a/apps/app/src/components/app/Address/AccountTabs.tsx b/apps/app/src/components/app/Address/AccountTabs.tsx index d0d8c3aff..d8f1c1509 100644 --- a/apps/app/src/components/app/Address/AccountTabs.tsx +++ b/apps/app/src/components/app/Address/AccountTabs.tsx @@ -12,6 +12,8 @@ import TabPanelGeneralSkeleton from '@/components/app/skeleton/address/dynamicTa import { Link } from '@/i18n/routing'; import { getRequest } from '@/utils/app/api'; +import MultiChainTransactions from './ChainTxns'; + export default async function AccountTabs({ id, locale, @@ -30,6 +32,11 @@ export default async function AccountTabs({ { label: 'Receipts', message: 'Receipts', name: 'receipts' }, { label: 'Token Txns', message: 'tokenTxns', name: 'tokentxns' }, { label: 'NFT Token Txns', message: 'nftTokenTxns', name: 'nfttokentxns' }, + { + label: 'Multichain Transactions', + message: 'Multichain Transactions', + name: 'multichaintxns', + }, { label: 'Access Keys', message: 'accessKeys', name: 'accesskeys' }, { label: 'Contract', message: 'contract', name: 'contract' }, ]; @@ -95,6 +102,10 @@ export default async function AccountTabs({ ) : null} + {tab === 'multichaintxns' ? ( + + ) : null} + {tab === 'accesskeys' ? ( ) : null} diff --git a/apps/app/src/components/app/Address/Balance.tsx b/apps/app/src/components/app/Address/Balance.tsx index 8924f6542..880c2c1e5 100644 --- a/apps/app/src/components/app/Address/Balance.tsx +++ b/apps/app/src/components/app/Address/Balance.tsx @@ -7,6 +7,7 @@ import { SpamToken } from '@/utils/types'; import AccountAlerts from './AccountAlerts'; import AccountMoreInfo from './AccountMoreInfo'; import AccountOverview from './AccountOverview'; +import MultichainInfo from './MultichainInfo'; const getCookieFromRequest = (cookieName: string): null | string => { const cookie = cookies().get(cookieName); @@ -23,6 +24,7 @@ export default async function Balance({ id }: { id: string }) { inventoryData, deploymentData, nftTokenData, + multiChainAccountsData, ] = await Promise.all([ getRequest(`account/${id}?rpc=${rpcUrl}`), getRequest('stats'), @@ -30,6 +32,7 @@ export default async function Balance({ id }: { id: string }) { getRequest(`account/${id}/inventory`), getRequest(`account/${id}/contract/deployments?rpc=${rpcUrl}`), getRequest(`nfts/${id}`), + getRequest(`chain-abstraction/${id}/multi-chain-accounts`), ]); const spamList: SpamToken = await fetch( @@ -44,7 +47,7 @@ export default async function Balance({ id }: { id: string }) { return ( <> -
+
+
); diff --git a/apps/app/src/components/app/Address/ChainTxns.tsx b/apps/app/src/components/app/Address/ChainTxns.tsx new file mode 100644 index 000000000..0ea14b552 --- /dev/null +++ b/apps/app/src/components/app/Address/ChainTxns.tsx @@ -0,0 +1,21 @@ +import { getRequest } from '@/utils/app/api'; + +import MultiChainTxns from './MultiChainTxns'; + +const MultiChainTransactions = async ({ id, searchParams }: any) => { + const [data, count] = await Promise.all([ + getRequest(`chain-abstraction/${id}/txns`, searchParams), + getRequest(`chain-abstraction/${id}/txns/count`, searchParams), + ]); + + return ( + + ); +}; + +export default MultiChainTransactions; diff --git a/apps/app/src/components/app/Address/MultiChainTxns.tsx b/apps/app/src/components/app/Address/MultiChainTxns.tsx new file mode 100644 index 000000000..ec1733961 --- /dev/null +++ b/apps/app/src/components/app/Address/MultiChainTxns.tsx @@ -0,0 +1,544 @@ +'use client'; +import { Menu, MenuButton, MenuList } from '@reach/menu-button'; +import { Tooltip } from '@reach/tooltip'; +import { useTranslations } from 'next-intl'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import QueryString from 'qs'; +import React, { useState } from 'react'; + +import { Link } from '@/i18n/routing'; +import { chainAbstractionExplorerUrl } from '@/utils/app/config'; +import { localFormat, truncateString } from '@/utils/libs'; +import { MultiChainTxnInfo } from '@/utils/types'; + +import ErrorMessage from '../common/ErrorMessage'; +import Filters from '../common/Filters'; +import TxnStatus from '../common/Status'; +import Table from '../common/Table'; +import TableSummary from '../common/TableSummary'; +import TimeStamp from '../common/TimeStamp'; +import Bitcoin from '../Icons/Bitcoin'; +import Clock from '../Icons/Clock'; +import Ethereum from '../Icons/Ethereum'; +import FaInbox from '../Icons/FaInbox'; +import Filter from '../Icons/Filter'; +import SortIcon from '../Icons/SortIcon'; +import Skeleton from '../skeleton/common/Skeleton'; + +const initialForm = { + chain: '', + from: '', + multichain_address: '', +}; + +interface TxnsProps { + count: string; + cursor: string; + error: boolean; + txns: MultiChainTxnInfo[]; +} + +const MultiChainTxns = ({ count, cursor, error, txns }: TxnsProps) => { + const t = useTranslations(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [page, setPage] = useState(1); + const [form, setForm] = useState(initialForm); + const [showAge, setShowAge] = useState(true); + const errorMessage = t ? t('noTxns') : ' No transactions found!'; + const [address, setAddress] = useState(''); + + const toggleShowAge = () => setShowAge((s) => !s); + + const onChange = (e: any) => { + const name = e.target.name; + const value = e.target.value; + + return setForm((f) => ({ ...f, [name]: value })); + }; + + const onFilter = (e: React.FormEvent) => { + e.preventDefault(); + + setPage(1); + + const { chain, from, multichain_address } = form; + const { cursor, p, ...updatedQuery } = QueryString.parse( + searchParams?.toString() || '', + ); + + const queryParams = { + ...(from && { from }), + ...(multichain_address && { multichain_address }), + ...(chain && { chain }), + }; + + const finalQuery = QueryString.stringify({ + ...updatedQuery, + ...queryParams, + }); + + router.push(`${pathname}?${finalQuery}`); + }; + + const currentParams = QueryString.parse(searchParams?.toString() || ''); + + const onOrder = () => { + const currentOrder = searchParams?.get('order') || 'desc'; + const newOrder = currentOrder === 'asc' ? 'desc' : 'asc'; + const newParams = { ...currentParams, order: newOrder }; + const newQueryString = QueryString.stringify(newParams); + + router.push(`${pathname}?${newQueryString}`); + }; + + const onHandleMouseOver = (e: any, id: string) => { + e.preventDefault(); + + setAddress(id); + }; + + const handleMouseLeave = () => { + setAddress(''); + }; + + const onClear = (e: React.MouseEvent) => { + const { name } = e.currentTarget; + + setPage(1); + const { cursor, p, ...restQuery } = QueryString.parse( + searchParams?.toString() || '', + ); + + setForm((f) => ({ ...f, [name]: '' })); + const { [name]: _, ...newQuery } = restQuery; + const newQueryString = QueryString.stringify(newQuery); + router.push(`${pathname}?${newQueryString}`); + }; + + const onAllClear = () => { + setForm(initialForm); + const { chain, cursor, from, multichain_address, p, ...newQuery } = + QueryString.parse(searchParams?.toString() || ''); + const newQueryString = QueryString.stringify(newQuery); + router.push(`${pathname}?${newQueryString}`); + }; + + const handleChainSelect = (chain: string, address: string) => { + return chain in chainAbstractionExplorerUrl + ? chainAbstractionExplorerUrl[ + chain as keyof typeof chainAbstractionExplorerUrl + ]?.address(address) + : ''; + }; + + const columns: any = [ + { + cell: (row: MultiChainTxnInfo) => ( + <> + + + ), + header: , + key: '', + tdClassName: + 'pl-5 py-2 whitespace-nowrap text-sm text-nearblue-600 dark:text-neargray-10', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + + + + {row?.receipt_id} + + + + + ), + header: RECEIPT ID, + key: 'receipt_id', + tdClassName: 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10', + thClassName: + 'px-4 py-4 text-left whitespace-nowrap text-xs font-semibold text-nearblue-600 dark:text-neargray-10 uppercase tracking-wider', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + + + onHandleMouseOver(e, row?.transaction_hash)} + > + {row?.transaction_hash} + + + + + ), + header: {'SOURCE TXN HASH'}, + key: 'source_transaction_hash', + tdClassName: 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10', + thClassName: + 'px-4 py-4 text-left whitespace-nowrap text-xs font-semibold text-nearblue-600 dark:text-neargray-10 uppercase tracking-wider', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + + + onHandleMouseOver(e, row?.account_id)} + > + {row?.account_id && truncateString(row?.account_id, 15, '...')} + + + + + ), + header: ( + <> + + + {'FROM'} + + +
+ +
+ + +
+
+
+
+ + ), + key: 'from', + tdClassName: + 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10 font-medium ', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + +
+ onHandleMouseOver(e, row?.chain)} + target="_blank" + > + {row?.chain && ( +
+
+
+ {row?.chain === 'BITCOIN' && ( + + )} + {row?.chain === 'ETHEREUM' && ( + + )} +
+ {row?.chain} +
+
+ )} + +
+
+
+ ), + header: ( + <> + + + {'DESTINATION CHAIN'}{' '} + + + +
+ +
+ + +
+
+
+
+ + ), + key: 'chain', + tdClassName: + 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10 font-medium ', + }, + { + cell: () => ( + + + - + + + ), + header: {'DESTINATION TXN HASH'}, + key: 'destination_transaction_hash', + tdClassName: 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10', + thClassName: + 'px-4 py-4 text-left whitespace-nowrap text-xs font-semibold text-nearblue-600 dark:text-neargray-10 uppercase tracking-wider', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + +
+ onHandleMouseOver(e, row?.derived_address)} + target="_blank" + > + {row?.derived_address && + truncateString(row?.derived_address, 15, '...')} + +
+
+
+ ), + header: ( + <> + + + {'DESTINATION ADDRESS'}{' '} + + + +
+ +
+ + +
+
+
+
+ + ), + key: 'multichain_address', + tdClassName: + 'px-4 py-2 text-sm text-nearblue-600 dark:text-neargray-10 font-medium ', + }, + { + cell: (row: MultiChainTxnInfo) => ( + + + + ), + header: ( +
+ + + + +
+ ), + key: 'block_timestamp', + tdClassName: + 'px-4 py-2 whitespace-nowrap text-sm text-nearblue-600 dark:text-neargray-10 w-48', + thClassName: 'whitespace-nowrap', + }, + ]; + + function removeCursor() { + const queryParams = QueryString.parse(searchParams?.toString() || ''); + const { cursor, filter, keyword, order, p, query, tab, ...rest } = + queryParams; + + return rest; + } + + const modifiedFilter = removeCursor(); + + return ( + <> + {!count ? ( +
+ +
+ ) : ( + } + text={ + txns && + !error && + `A total of${' '} + ${count ? localFormat && localFormat(count.toString()) : 0}${' '} + multichain transactions found` + } + /> + )} + } + message={errorMessage || ''} + mutedText="Please try again later" + /> + } + limit={25} + page={page} + setPage={setPage} + /> + + ); +}; +export default MultiChainTxns; diff --git a/apps/app/src/components/app/Address/MultichainInfo.tsx b/apps/app/src/components/app/Address/MultichainInfo.tsx new file mode 100644 index 000000000..f4c2b8d95 --- /dev/null +++ b/apps/app/src/components/app/Address/MultichainInfo.tsx @@ -0,0 +1,152 @@ +'use client'; +import { Menu, MenuButton, MenuItems, MenuPopover } from '@reach/menu-button'; +import React, { useRef, useState } from 'react'; +import PerfectScrollbar from 'react-perfect-scrollbar'; + +import { chainAbstractionExplorerUrl } from '@/utils/app/config'; +import { shortenHex } from '@/utils/app/libs'; + +import ArrowDown from '../Icons/ArrowDown'; +import Bitcoin from '../Icons/Bitcoin'; +import Ethereum from '../Icons/Ethereum'; +import FaExternalLinkAlt from '../Icons/FaExternalLinkAlt'; + +interface Props { + multiChainAccounts: any; +} + +const MultichainInfo = ({ multiChainAccounts }: Props) => { + const buttonRef = useRef(null); + const [positionClass, setPositionClass] = useState('left-0'); + const [hoveredIndex, setHoveredIndex] = useState(null); + + const handleChainSelect = (chain: string, address: string) => { + const url = + chain in chainAbstractionExplorerUrl + ? chainAbstractionExplorerUrl[ + chain as keyof typeof chainAbstractionExplorerUrl + ]?.address(address) + : ''; + + url ? window.open(url, '_blank') : ''; + }; + + const handleMenuOpen = () => { + if (buttonRef.current) { + const buttonRect = buttonRef.current.getBoundingClientRect(); + const menuWidth = 300; + const availableSpaceRight = window.innerWidth - buttonRect.right; + + if (availableSpaceRight < menuWidth) { + setPositionClass('right-0'); + } else { + setPositionClass('left-0'); + } + } + }; + + return ( +
+
+

+ Multichain Information +

+
+
+ + {multiChainAccounts?.length ? multiChainAccounts?.length : 'No'}{' '} + {multiChainAccounts?.length === 1 ? 'address' : 'addresses'} found + on: + +
+ + + {'Foreign Chain'} + + + + +
+ +
+ {multiChainAccounts?.map( + (address: any, index: any) => ( +
+ handleChainSelect( + address.chain.toLowerCase(), + address.derived_address, + ) + } + onMouseEnter={() => setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > + {address.chain && ( +
+
+ {address.chain === 'BITCOIN' && ( + + )} + {address.chain === 'ETHEREUM' && ( + + )} + + {address.path + .toLowerCase() + .includes( + address.chain.toLowerCase() + ',', + ) || + address.path + .toLowerCase() + .includes( + address.chain.toLowerCase() + '-', + ) + ? address.path + : address.chain} + + + ({shortenHex(address.derived_address)}) + +
+ + + +
+ )} +
+ ), + )} +
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default MultichainInfo; diff --git a/apps/app/src/components/app/Address/TransactionActions.tsx b/apps/app/src/components/app/Address/TransactionActions.tsx index 7962255fa..e064e4aa5 100644 --- a/apps/app/src/components/app/Address/TransactionActions.tsx +++ b/apps/app/src/components/app/Address/TransactionActions.tsx @@ -280,7 +280,7 @@ const TransactionActions = ({ <> - {t ? t('txns:from') : 'FROM'}{' '} + {t ? t('from') : 'FROM'}{' '} @@ -290,9 +290,7 @@ const TransactionActions = ({ name="from" onChange={onChange} placeholder={ - t - ? t('txns:filter.placeholder') - : 'Search by address e.g. Ⓝ..' + t ? t('filter.placeholder') : 'Search by address e.g. Ⓝ..' } value={form.from} /> @@ -302,7 +300,7 @@ const TransactionActions = ({ type="submit" > {' '} - {t ? t('txns:filter.filter') : 'Filter'} + {t ? t('filter.filter') : 'Filter'} diff --git a/apps/app/src/components/app/Apis/ApiActions.tsx b/apps/app/src/components/app/Apis/ApiActions.tsx index 9d255bdf8..75e929ab3 100644 --- a/apps/app/src/components/app/Apis/ApiActions.tsx +++ b/apps/app/src/components/app/Apis/ApiActions.tsx @@ -82,7 +82,7 @@ const ApiActions = ({ return ( <> {' '} -
+
{status === 'cancelled' && (
diff --git a/apps/app/src/components/app/Home.tsx b/apps/app/src/components/app/Home.tsx index 86261ee66..5c443fa44 100644 --- a/apps/app/src/components/app/Home.tsx +++ b/apps/app/src/components/app/Home.tsx @@ -77,7 +77,7 @@ export default async function Home({ locale }: { locale: string }) { return (
-
+

@@ -106,7 +106,7 @@ export default async function Home({ locale }: { locale: string }) {

-
+
diff --git a/apps/app/src/components/app/Icons/Bitcoin.tsx b/apps/app/src/components/app/Icons/Bitcoin.tsx new file mode 100644 index 000000000..56098829c --- /dev/null +++ b/apps/app/src/components/app/Icons/Bitcoin.tsx @@ -0,0 +1,18 @@ +interface Props { + className: string; +} + +const Bitcoin = (props: Props) => { + return ( + + + + ); +}; + +export default Bitcoin; diff --git a/apps/app/src/components/app/Icons/Ethereum.tsx b/apps/app/src/components/app/Icons/Ethereum.tsx new file mode 100644 index 000000000..fa43ff494 --- /dev/null +++ b/apps/app/src/components/app/Icons/Ethereum.tsx @@ -0,0 +1,18 @@ +interface Props { + className: string; +} + +const Ethereum = (props: Props) => { + return ( + + + + ); +}; + +export default Ethereum; diff --git a/apps/app/src/components/app/Layouts/Footer.tsx b/apps/app/src/components/app/Layouts/Footer.tsx index c6747d155..16291ae02 100644 --- a/apps/app/src/components/app/Layouts/Footer.tsx +++ b/apps/app/src/components/app/Layouts/Footer.tsx @@ -14,7 +14,7 @@ const Footer = ({ theme }: any) => {
-
+
diff --git a/apps/app/src/components/app/Layouts/Header.tsx b/apps/app/src/components/app/Layouts/Header.tsx index 9cd2cf212..fe5693ddc 100644 --- a/apps/app/src/components/app/Layouts/Header.tsx +++ b/apps/app/src/components/app/Layouts/Header.tsx @@ -218,7 +218,7 @@ const Header = ({
-
+
@@ -347,7 +347,7 @@ const Header = ({
)} -
+
@@ -637,7 +637,7 @@ const Header = ({
  • + {isOpen && ( +
      +
    • + +
    • +
    + )} + +
+
)}
diff --git a/apps/app/src/components/app/Tokens/NFT/NFTDetails.tsx b/apps/app/src/components/app/Tokens/NFT/NFTDetails.tsx index 5c0e99545..115fa7873 100644 --- a/apps/app/src/components/app/Tokens/NFT/NFTDetails.tsx +++ b/apps/app/src/components/app/Tokens/NFT/NFTDetails.tsx @@ -70,7 +70,7 @@ const NFTDetails = ({ setIsVisible(false); }; return ( -
+
{isTokenSpam(token?.contract || id) && isVisible && ( <>
diff --git a/apps/app/src/components/app/Transactions/Overview.tsx b/apps/app/src/components/app/Transactions/Overview.tsx index f7b5de734..bea0323ac 100644 --- a/apps/app/src/components/app/Transactions/Overview.tsx +++ b/apps/app/src/components/app/Transactions/Overview.tsx @@ -33,7 +33,7 @@ const Overview = ({ chartsDetails, error, stats }: Props) => { const LoadingSkeleton = () => { return ( -
+
{ const totalTxns = stats?.total_txns ?? 0; return ( -
+
{ return ( <> -
+
{rpcError && (!txn || allRpcProviderError) ? (
diff --git a/apps/app/src/components/app/common/TableSummary.tsx b/apps/app/src/components/app/common/TableSummary.tsx new file mode 100644 index 000000000..6972c8f47 --- /dev/null +++ b/apps/app/src/components/app/common/TableSummary.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +const TableSummary = ({ + filters = <>, + linkToDowload = <>, + text = <>, +}: any) => { + return ( +
+
+

+ {text} +

+
+
+
{filters}
+
+ {linkToDowload} +
+
+
+ ); +}; +export default TableSummary; diff --git a/apps/app/src/components/app/common/TokenHoldings.tsx b/apps/app/src/components/app/common/TokenHoldings.tsx index 515962468..5d8e119d6 100644 --- a/apps/app/src/components/app/common/TokenHoldings.tsx +++ b/apps/app/src/components/app/common/TokenHoldings.tsx @@ -83,22 +83,29 @@ const TokenHoldings = (props: Props) => { {props.ft?.tokens?.map((token, index) => (
-
-
+
+
{token?.ft_meta?.name} { + e.currentTarget.src = + '/images/tokenplaceholder.svg'; + /* eslint-disable @next/next/no-img-element */ + }} src={ token?.ft_meta?.icon ?? '/images/tokenplaceholder.svg' } width={16} - /* eslint-disable-next-line @next/next/no-img-element */ />
@@ -112,7 +119,7 @@ const TokenHoldings = (props: Props) => { ({token?.ft_meta?.symbol})
-
+
{token?.rpcAmount ? localFormat(token?.rpcAmount) : token?.rpcAmount ?? ''} @@ -154,22 +161,26 @@ const TokenHoldings = (props: Props) => { {nfts.map((nft) => (
-
-
+
+
{nft?.nft_meta?.name} { + e.currentTarget.src = + '/images/tokenplaceholder.svg'; + /* eslint-disable @next/next/no-img-element */ + }} src={ nft?.nft_meta?.icon ?? '/images/tokenplaceholder.svg' } width={16} - /* eslint-disable-next-line @next/next/no-img-element */ />
@@ -183,7 +194,7 @@ const TokenHoldings = (props: Props) => { ({nft?.nft_meta?.symbol})
-
+
{nft?.quantity ? localFormat(nft?.quantity) : nft?.quantity ?? ''} diff --git a/apps/app/src/components/app/skeleton/address/balance.tsx b/apps/app/src/components/app/skeleton/address/balance.tsx index 1f220577b..e4d366c74 100644 --- a/apps/app/src/components/app/skeleton/address/balance.tsx +++ b/apps/app/src/components/app/skeleton/address/balance.tsx @@ -10,36 +10,36 @@ export default function BalanceSkeleton() { const t = useTranslations(); const { networkId } = useConfig(); return ( -
+

{t('overview') || 'Overview'}

- +
-
-
+
+
{t('balance') || 'Balance'}:
- +
{networkId === 'mainnet' && ( -
-
+
+
{t('value') || 'Value:'}
- +
)} -
-
+
+
{t('tokens') || 'Tokens:'}
-
- +
+
@@ -51,41 +51,58 @@ export default function BalanceSkeleton() { {t('moreInfo') || 'Account information'}
-
-
-
- Staked {t('balance') || 'Balance'}: -
-
- +
+
+
+
+ Staked {t('balance') || 'Balance'}: +
+
+ +
-
-
- {t('storageUsed') || 'Storage Used'}: -
-
- +
+
+
+ {t('storageUsed') || 'Storage Used'}: +
+
+ +
-
-
-
Created At
-
- -
-
-
- -
-
+
+
Contract Creator:
+
+
+
+ Created At +
+
+ +
+
+
+
+
+
+
+
+

+ Multichain Information +

+
+
+ +
diff --git a/apps/app/src/components/app/skeleton/address/tab.tsx b/apps/app/src/components/app/skeleton/address/tab.tsx index 2dc053f5e..767a20e86 100644 --- a/apps/app/src/components/app/skeleton/address/tab.tsx +++ b/apps/app/src/components/app/skeleton/address/tab.tsx @@ -11,6 +11,11 @@ const tabs = [ { label: 'Receipts', message: 'Receipts', name: 'receipts' }, { label: 'Token Txns', message: 'tokenTxns', name: 'tokentxns' }, { label: 'NFT Token Txns', message: 'nftTokenTxns', name: 'nfttokentxns' }, + { + label: 'Multichain Transactions', + message: 'Multichain Transactions', + name: 'multichaintxns', + }, { label: 'Access Keys', message: 'accessKeys', name: 'accesskeys' }, { label: 'Contract', message: 'contract', name: 'contract' }, ]; @@ -59,7 +64,9 @@ export default function TabSkeletion() { {tab === 'nfttokentxns' ? ( ) : null} - + {tab === 'multichaintxns' ? ( + + ) : null} {tab === 'accesskeys' ? ( ) : null} diff --git a/apps/app/src/components/app/skeleton/nft/NFTDetail.tsx b/apps/app/src/components/app/skeleton/nft/NFTDetail.tsx index 76ad22975..7a8426814 100644 --- a/apps/app/src/components/app/skeleton/nft/NFTDetail.tsx +++ b/apps/app/src/components/app/skeleton/nft/NFTDetail.tsx @@ -7,7 +7,7 @@ interface Props { const NFTDetail = forwardRef((props: Props, ref: Ref) => { return (
-
+
diff --git a/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx b/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx index 2eb408125..043bad534 100644 --- a/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx +++ b/apps/app/src/components/app/skeleton/txns/TxnsTabs.tsx @@ -33,7 +33,7 @@ function TxnsTabsSkeleton({ hash, tab }: { hash: string; tab: string }) { ); return ( <> -
+
<>
diff --git a/apps/app/src/components/common/TableSummary.tsx b/apps/app/src/components/common/TableSummary.tsx index 6972c8f47..153899114 100644 --- a/apps/app/src/components/common/TableSummary.tsx +++ b/apps/app/src/components/common/TableSummary.tsx @@ -7,16 +7,16 @@ const TableSummary = ({ }: any) => { return (
-
-

+

+

{text}

-
-
{filters}
-
+
+
{filters}
+
{linkToDowload}
diff --git a/apps/app/src/components/skeleton/home/Overview.tsx b/apps/app/src/components/skeleton/home/Overview.tsx index bf76fb26a..ca996baf2 100644 --- a/apps/app/src/components/skeleton/home/Overview.tsx +++ b/apps/app/src/components/skeleton/home/Overview.tsx @@ -15,7 +15,7 @@ const Overview = forwardRef( const { theme } = useTheme(); return (
-
+
) => { return (
-
+
diff --git a/apps/app/src/styles/globals.css b/apps/app/src/styles/globals.css index 75235ab74..712167233 100644 --- a/apps/app/src/styles/globals.css +++ b/apps/app/src/styles/globals.css @@ -275,4 +275,10 @@ a { .contact-accordian [data-reach-accordion-button][data-state='open'] .contact-icon { transform: rotate(-180deg); color: #31766a; -} \ No newline at end of file +} + +@media (min-width: 1400px) { + .container-xxl { + max-width: 1400px; + } +} diff --git a/apps/app/src/utils/app/config.ts b/apps/app/src/utils/app/config.ts index ec1ae29ee..bbe8be14c 100644 --- a/apps/app/src/utils/app/config.ts +++ b/apps/app/src/utils/app/config.ts @@ -59,6 +59,29 @@ export const verifierConfig = }, ]; +export const chainAbstractionExplorerUrl = + networkId === 'mainnet' + ? { + bitcoin: { + address: (address: string) => + `https://blockchain.com/explorer/addresses/btc/${address}`, + }, + ethereum: { + address: (address: string) => + `https://etherscan.io/address/${address}`, + }, + } + : { + bitcoin: { + address: (address: string) => + `https://blockexplorer.one/bitcoin/testnet/address/${address}`, + }, + ethereum: { + address: (address: string) => + `https://sepolia.etherscan.io/address/${address}`, + }, + }; + const evmWalletChains = { mainnet: { chainId: 397, diff --git a/apps/app/src/utils/types.ts b/apps/app/src/utils/types.ts index bdc007cd0..48170a20d 100644 --- a/apps/app/src/utils/types.ts +++ b/apps/app/src/utils/types.ts @@ -718,6 +718,7 @@ export type Token = { twitter: string; }; name: string; + nep518_hex_address: string; nft: Token; onchain_market_cap: string; price: string; @@ -1688,3 +1689,18 @@ export type ContractData = { contractMetadata: ContractMetadata | null; onChainCodeHash: string; }; + +export type MultiChainTxnInfo = { + account_id: string; + block_height: string; + block_timestamp: string; + chain: string; + derived_address: string; + derived_transaction: string; + id: string; + path: string; + public_key: string; + receipt_id: string; + status: boolean; + transaction_hash: string; +};