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: (
+ <>
+
+ >
+ ),
+ 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: (
+ <>
+
+ >
+ ),
+ 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: (
+ <>
+
+ >
+ ),
+ 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:
+
+
+
+
+
+
+
+
+ );
+};
+
+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 = ({
<>
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) => {