-
Notifications
You must be signed in to change notification settings - Fork 0
test: State Management & Redux Violations #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="token-dashboard"> | ||
| <div className="dashboard-header"> | ||
| <h2>Token Dashboard</h2> | ||
| <button onClick={handleRefresh} disabled={loading}> | ||
| {loading ? 'Refreshing...' : 'Refresh'} | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="dashboard-stats"> | ||
| <div className="stat"> | ||
| <label>Total Balance:</label> | ||
| <span>${totalBalance.toFixed(2)}</span> | ||
| </div> | ||
| <div className="stat"> | ||
| <label>Total Tokens:</label> | ||
| <span>{tokens.length}</span> | ||
| </div> | ||
| <div className="stat"> | ||
| <label>Network:</label> | ||
| <span>{chainId}</span> | ||
| </div> | ||
| <div className="stat"> | ||
| <label>Last Update:</label> | ||
| <span>{new Date(lastUpdate).toLocaleTimeString()}</span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="token-list"> | ||
| {tokens.map((token: any) => { | ||
| const balance = balances.get(token.address) || '0'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling Map.get on potentially deserialized objectHigh Severity The code calls Additional Locations (1) |
||
| const price = prices[token.address] || 0; | ||
| const value = parseFloat(balance) * price; | ||
|
|
||
| return ( | ||
| <div key={token.address} className="token-row"> | ||
| <div className="token-info"> | ||
| <span className="token-symbol">{token.symbol}</span> | ||
| <span className="token-balance">{balance}</span> | ||
| </div> | ||
| <div className="token-value"> | ||
| <span className="token-price">${price.toFixed(2)}</span> | ||
| <span className="token-total">${value.toFixed(2)}</span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| interface Token { | ||
| address: string; | ||
| symbol: string; | ||
| decimals: number; | ||
| balance: string; | ||
| price: number; | ||
| } | ||
|
|
||
| interface TokensState { | ||
| tokens: Token[]; | ||
| balances: Map<string, string>; | ||
| prices: { [address: string]: number }; | ||
| loading: boolean; | ||
| lastUpdate: number; | ||
| fetchPromise: Promise<any> | null; | ||
| } | ||
|
|
||
| const initialState: TokensState = { | ||
| tokens: [], | ||
| balances: new Map(), | ||
| prices: {}, | ||
| loading: false, | ||
| lastUpdate: 0, | ||
| fetchPromise: null, | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-serializable Map and Promise in Redux stateHigh Severity The |
||
|
|
||
| export default function tokensReducer(state = initialState, action: any) { | ||
| switch (action.type) { | ||
| case 'ADD_TOKEN': | ||
| state.tokens.push(action.payload); | ||
| return state; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reducer directly mutates state causing silent failuresHigh Severity The reducer mutates state directly using Additional Locations (2) |
||
|
|
||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Async fetch inside reducer breaks state updatesHigh Severity The |
||
|
|
||
| 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', | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Identity function selector provides no memoization benefitLow Severity The |
||
|
|
||
| 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]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Random price generation produces incorrect token values
High Severity
The
handleRefreshfunction sets token prices usingMath.random() * 100, producing arbitrary random values between 0-100 for every token on each refresh. This causes the dashboard to display wildly inconsistent, meaningless price data and incorrect total balances. Token prices should be fetched from a price API, not randomly generated.