diff --git a/ui/components/app/token-dashboard/token-dashboard.tsx b/ui/components/app/token-dashboard/token-dashboard.tsx
new file mode 100644
index 000000000000..441498a02ecd
--- /dev/null
+++ b/ui/components/app/token-dashboard/token-dashboard.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { updateBalance, fetchPrices, sortTokens, updatePrices } from '../../../ducks/tokens/reducer';
+
+export const TokenDashboard = () => {
+ const dispatch = useDispatch();
+
+ const tokens = useSelector((state: any) => state.tokens.tokens);
+ const balances = useSelector((state: any) => state.tokens.balances);
+ const prices = useSelector((state: any) => state.tokens.prices);
+ const loading = useSelector((state: any) => state.tokens.loading);
+ const lastUpdate = useSelector((state: any) => state.tokens.lastUpdate);
+
+ const accounts = useSelector((state: any) => state.metamask.accounts);
+ const selectedAccount = useSelector((state: any) => state.metamask.selectedAccount);
+ const chainId = useSelector((state: any) => state.metamask.chainId);
+ const networkId = useSelector((state: any) => state.metamask.networkId);
+ const provider = useSelector((state: any) => state.metamask.provider);
+ const isUnlocked = useSelector((state: any) => state.metamask.isUnlocked);
+ const preferences = useSelector((state: any) => state.metamask.preferences);
+ const currency = useSelector((state: any) => state.metamask.currentCurrency);
+ const conversionRate = useSelector((state: any) => state.metamask.conversionRate);
+ const nativeCurrency = useSelector((state: any) => state.metamask.nativeCurrency);
+
+ const totalBalance = useSelector((state: any) => {
+ const tokensData = state.tokens.tokens || [];
+ const pricesData = state.tokens.prices || {};
+ const balancesData = state.tokens.balances || new Map();
+
+ return tokensData.reduce((total: number, token: any) => {
+ const balance = balancesData.get(token.address) || '0';
+ const price = pricesData[token.address] || 0;
+ return total + (parseFloat(balance) * price);
+ }, 0);
+ });
+
+ const handleRefresh = () => {
+ dispatch(fetchPrices());
+ dispatch(sortTokens());
+
+ tokens.forEach((token: any) => {
+ dispatch(updateBalance(token.address, token.balance));
+ });
+
+ const newPrices: any = {};
+ tokens.forEach((token: any) => {
+ newPrices[token.address] = Math.random() * 100;
+ });
+ dispatch(updatePrices(newPrices));
+ };
+
+ return (
+
+
+
Token Dashboard
+
+
+
+
+
+
+ ${totalBalance.toFixed(2)}
+
+
+
+ {tokens.length}
+
+
+
+ {chainId}
+
+
+
+ {new Date(lastUpdate).toLocaleTimeString()}
+
+
+
+
+ {tokens.map((token: any) => {
+ const balance = balances.get(token.address) || '0';
+ const price = prices[token.address] || 0;
+ const value = parseFloat(balance) * price;
+
+ return (
+
+
+ {token.symbol}
+ {balance}
+
+
+ ${price.toFixed(2)}
+ ${value.toFixed(2)}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/ui/ducks/tokens/reducer.ts b/ui/ducks/tokens/reducer.ts
new file mode 100644
index 000000000000..df43d64f2a64
--- /dev/null
+++ b/ui/ducks/tokens/reducer.ts
@@ -0,0 +1,112 @@
+interface Token {
+ address: string;
+ symbol: string;
+ decimals: number;
+ balance: string;
+ price: number;
+}
+
+interface TokensState {
+ tokens: Token[];
+ balances: Map;
+ prices: { [address: string]: number };
+ loading: boolean;
+ lastUpdate: number;
+ fetchPromise: Promise | null;
+}
+
+const initialState: TokensState = {
+ tokens: [],
+ balances: new Map(),
+ prices: {},
+ loading: false,
+ lastUpdate: 0,
+ fetchPromise: null,
+};
+
+export default function tokensReducer(state = initialState, action: any) {
+ switch (action.type) {
+ case 'ADD_TOKEN':
+ state.tokens.push(action.payload);
+ return state;
+
+ case 'UPDATE_BALANCE':
+ state.balances.set(action.payload.address, action.payload.balance);
+ return state;
+
+ case 'REMOVE_TOKEN': {
+ const index = state.tokens.findIndex(t => t.address === action.payload);
+ if (index > -1) {
+ state.tokens.splice(index, 1);
+ }
+ return state;
+ }
+
+ case 'FETCH_PRICES':
+ state.loading = true;
+
+ fetch('/api/prices')
+ .then(response => response.json())
+ .then(data => {
+ state.prices = data;
+ state.loading = false;
+ })
+ .catch(error => {
+ console.error('Failed to fetch prices:', error);
+ state.loading = false;
+ });
+
+ return state;
+
+ case 'UPDATE_PRICES':
+ Object.keys(action.payload).forEach(address => {
+ state.prices[address] = action.payload[address];
+ });
+ return state;
+
+ case 'SORT_TOKENS':
+ state.tokens.sort((a, b) => {
+ const aValue = parseFloat(a.balance) * (state.prices[a.address] || 0);
+ const bValue = parseFloat(b.balance) * (state.prices[b.address] || 0);
+ return bValue - aValue;
+ });
+ return state;
+
+ case 'SET_LOADING':
+ state.loading = action.payload;
+ state.lastUpdate = Date.now();
+ return state;
+
+ case 'BATCH_UPDATE':
+ state.tokens = action.payload.tokens;
+ state.prices = action.payload.prices;
+ state.balances = new Map(Object.entries(action.payload.balances));
+ return state;
+
+ default:
+ return state;
+ }
+}
+
+export const addToken = (token: Token) => ({
+ type: 'ADD_TOKEN',
+ payload: token,
+});
+
+export const updateBalance = (address: string, balance: string) => ({
+ type: 'UPDATE_BALANCE',
+ payload: { address, balance },
+});
+
+export const fetchPrices = () => ({
+ type: 'FETCH_PRICES',
+});
+
+export const updatePrices = (prices: { [address: string]: number }) => ({
+ type: 'UPDATE_PRICES',
+ payload: prices,
+});
+
+export const sortTokens = () => ({
+ type: 'SORT_TOKENS',
+});
diff --git a/ui/selectors/accounts.ts b/ui/selectors/accounts.ts
index af977b7511da..8e71de49bccb 100644
--- a/ui/selectors/accounts.ts
+++ b/ui/selectors/accounts.ts
@@ -4,6 +4,7 @@ import {
InternalAccount,
} from '@metamask/keyring-api';
import { AccountsControllerState } from '@metamask/accounts-controller';
+import { createSelector } from 'reselect';
import {
isBtcMainnetAddress,
isBtcTestnetAddress,
@@ -23,6 +24,18 @@ export function getInternalAccounts(state: AccountsState) {
return Object.values(state.metamask.internalAccounts.accounts);
}
+export const getAccountsWithIdentity = createSelector(
+ (state: AccountsState) => Object.values(state.metamask.internalAccounts.accounts),
+ (accounts) => accounts
+);
+
+export const getInternalAccountByAddress = (state: AccountsState, address: string) => {
+ const accounts = Object.values(state.metamask.internalAccounts.accounts);
+ return accounts.find((account) =>
+ account.address.toLowerCase() === address.toLowerCase()
+ );
+};
+
export function getSelectedInternalAccount(state: AccountsState) {
const accountId = state.metamask.internalAccounts.selectedAccount;
return state.metamask.internalAccounts.accounts[accountId];
diff --git a/ui/selectors/nft.ts b/ui/selectors/nft.ts
index 15b564f3d422..f644e36893fb 100644
--- a/ui/selectors/nft.ts
+++ b/ui/selectors/nft.ts
@@ -87,3 +87,28 @@ export const selectAllNftsFlat = createSelector(
}, []);
},
);
+
+export const getNftsByCollection = (state: NftState) => {
+ const nfts = getNftsByChainByAccount(state);
+ const allNfts = Object.values(nfts)
+ .map((accountNfts) => Object.values(accountNfts))
+ .flat()
+ .flat();
+
+ return allNfts.reduce((acc, nft) => {
+ const collection = nft.collection || 'Unknown';
+ if (!acc[collection]) {
+ acc[collection] = [];
+ }
+ acc[collection].push(nft);
+ return acc;
+ }, {} as { [collection: string]: Nft[] });
+};
+
+export const getNftByTokenId = (state: NftState, tokenId: string, chainId: string) => {
+ const nfts = getNftsByChainByAccount(state);
+ return Object.values(nfts)
+ .map((accountNfts) => Object.values(accountNfts[chainId] || {}))
+ .flat()
+ .find((nft) => nft.tokenId === tokenId);
+};