Skip to content

Commit

Permalink
fix: token duplication on dashboard (#431)
Browse files Browse the repository at this point in the history
* fix: token duplication on dashboard

* lint: comply with rules
  • Loading branch information
alexruzenhack authored Mar 4, 2024
1 parent 35b7cec commit 527a75d
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 51 deletions.
8 changes: 2 additions & 6 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,19 +534,15 @@ class _AppStackWrapper extends React.Component {
if (!this.props.wallet?.storage) {
return;
}
const tokens = [...INITIAL_TOKENS];
const tokens = { ...INITIAL_TOKENS };
const iterator = this.props.wallet.storage.getRegisteredTokens();
let next = await iterator.next();
// XXX: The "for await" syntax wouldbe better but this is failing due to
// redux-saga messing with the for operator runtime
while (!next.done) {
const token = next.value;
// We need to filter the token data to remove the metadata from this list (e.g. balance)
tokens.push({
uid: token.uid,
symbol: token.symbol,
name: token.name,
});
tokens[token.uid] = { ...token };
// eslint-disable-next-line no-await-in-loop
next = await iterator.next();
}
Expand Down
2 changes: 1 addition & 1 deletion src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export const updateSelectedToken = (selectedToken) => (
export const newToken = (token) => ({ type: types.NEW_TOKEN, payload: token });

/**
* tokens {Array} list of tokens to update state
* @param {Object.<string, {uid: string, name: string; symbol: string}>} tokens map of tokens to update state
*/
export const setTokens = (tokens) => ({ type: types.SET_TOKENS, payload: tokens });

Expand Down
5 changes: 3 additions & 2 deletions src/components/TokenSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ import { COLORS } from '../styles/themes';
* @param {Record<string,TokenBalance>} props.tokensBalance
* @param {{ uid: string }} props.selectedToken
* @param {unknown} props.tokenMetadata
* @param {unknown} props.tokens
* @param {{ [uid: string]: { uid: string; name: string; symbol: string; }}} props.tokens
* @param {unknown} props.header
* @param {boolean} props.renderArrow
* @param {function} props.onItemPress
*/
const TokenSelect = (props) => {
const tokens = Object.values(props.tokens);
const renderItem = ({ item, index }) => {
const symbolWrapperStyle = [styles.symbolWrapper];
const symbolTextStyle = [styles.text, styles.leftText, styles.symbolText];
Expand Down Expand Up @@ -95,7 +96,7 @@ const TokenSelect = (props) => {
{props.header}
<View style={styles.listWrapper}>
<FlatList
data={props.tokens}
data={tokens}
// use extraData to make sure list updates (props.tokens might remain the same object)
extraData={[props.tokensBalance, props.selectedToken.uid]}
renderItem={renderItem}
Expand Down
15 changes: 10 additions & 5 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ export const _IS_MULTI_TOKEN = true;

/**
* Default token.
*
* Example config:
* @constant
* @type {{
* uid: string;
* name: string;
* symbol: string;
* }}
* @default
* {
* name: 'YanCoin',
* symbol: 'YAN',
* uid: '000003a3b261e142d3dfd84970d3a50a93b5bc3a66a3b6ba973956148a3eb824'
* name: 'Hathor',
* symbol: 'HTR',
* uid: '00'
* }
*/
export const _DEFAULT_TOKEN = hathorLib.constants.HATHOR_TOKEN_CONFIG;
Expand Down
14 changes: 11 additions & 3 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@ export const STAGE = 'mainnet';
export const NETWORK_MAINNET = 'mainnet';

/**
* Default tokens for the wallet (to start on redux)
*/
export const INITIAL_TOKENS = [DEFAULT_TOKEN];
* Default tokens for the wallet (to start on redux).
* @constant
* @type{{
* [uid: string]: {
* uid: string;
* name: string;
* symbol: string;
* }
* }}
*/
export const INITIAL_TOKENS = { [DEFAULT_TOKEN.uid]: DEFAULT_TOKEN };

/**
* Wallet will lock if app goes to background for more than LOCK_TIMEOUT seconds
Expand Down
33 changes: 27 additions & 6 deletions src/reducers/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { WALLET_STATUS } from '../sagas/wallet';
* token {Object} payment should be made in this token
* }
* invoicePayment {Object} null if not paid or the tx that settles latestInvoice
* tokens {Array} array of tokens added [{name, symbol, uid}]
* selectedToken {Object} token currently selected by the user
* isOnline {bool} Indicates whether the wallet is connected to the fullnode's websocket
* serverInfo {Object} {
Expand Down Expand Up @@ -81,7 +80,24 @@ const initialState = {
loadHistoryStatus: { active: true, error: false },
latestInvoice: null,
invoicePayment: null,
/**
* tokens {Object.<string, Object>} Map of tokens added plus initial tokens,
* @see {@link INITIAL_TOKENS}
*/
tokens: INITIAL_TOKENS,
/**
* selectedToken {{
* uid: string;
* name; string;
* symbol: string
* }} Token selected to operate with
* @example
* {
* name: 'YanCoin',
* symbol: 'YAN',
* uid: '000003a3b261e142d3dfd84970d3a50a93b5bc3a66a3b6ba973956148a3eb824'
* }
*/
selectedToken: DEFAULT_TOKEN,
isOnline: false,
serverInfo: { version: '', network: '' },
Expand Down Expand Up @@ -499,24 +515,29 @@ const onUpdateSelectedToken = (state, action) => ({

/**
* Add a new token to the list of available tokens in this wallet
* @param {object} state
* @param {{payload: { uid: string }}} action containing a token data in payload,
* @see {@link initialState.tokens}
*/
const onNewToken = (state, action) => ({
const onNewToken = (state, { payload }) => ({
...state,
tokens: [...state.tokens, action.payload],
tokens: { ...state.tokens, [payload.uid]: { ...payload } },
});

/**
* Set the list of tokens added in this wallet
* @param {object} state
* @param {{ payload }} action containing all registered tokens in payload
*/
const onSetTokens = (state, action) => {
const onSetTokens = (state, { payload }) => {
let { selectedToken } = state;
if (action.payload.indexOf(selectedToken) === -1) {
if (payload[selectedToken.uid] == null) {
// We have unregistered this token
selectedToken = DEFAULT_TOKEN;
}
return {
...state,
tokens: [...action.payload],
tokens: { ...payload },
selectedToken,
};
};
Expand Down
29 changes: 20 additions & 9 deletions src/sagas/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,17 @@ export function isUnlockScreen(action) {
* Get registered tokens from the wallet instance.
* @param {HathorWallet} wallet
* @param {boolean} excludeHTR If we should exclude the HTR token.
* @returns {Promise<{ uid: string, symbol: string, name: string }[]>}
* @returns {Promise<{
* [uid: string]: {
* uid: string;
* symbol: string;
* name: string;
* }
* }>}
*/
export async function getRegisteredTokens(wallet, excludeHTR = false) {
const htrUid = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid;
const tokens = [];
const tokens = {};

// redux-saga generator magic does not work well with the "for await..of" syntax
// The asyncGenerator is not recognized as an iterable and it throws an exception
Expand All @@ -167,24 +173,29 @@ export async function getRegisteredTokens(wallet, excludeHTR = false) {
while (!next.done) {
const token = next.value;
if ((!excludeHTR) || token.uid !== htrUid) {
tokens.push({
uid: token.uid,
symbol: token.symbol,
name: token.name,
});
tokens[token.uid] = { ...token };
}
// eslint-disable-next-line no-await-in-loop
next = await iterator.next();
}

// XXX: This will add any default tokens configured, not only HTR
if (!excludeHTR) {
tokens.unshift(...INITIAL_TOKENS);
return { ...INITIAL_TOKENS, ...tokens };
}

return tokens;
}

/**
* Flat registered tokens to uid.
* @param {{ tokens: Object }} Map of registered tokens by uid
* @returns {string[]} Array of token uid
*/
export function getRegisteredTokenUids({ tokens }) {
return Object.keys(tokens);
}

/**
* Check if a token is registered in the context of the saga functions.
* @param {HathorWallet} wallet
Expand All @@ -193,7 +204,7 @@ export async function getRegisteredTokens(wallet, excludeHTR = false) {
*/
export async function isTokenRegistered(wallet, tokenUid) {
const tokens = await getRegisteredTokens(wallet);
return tokens.some((token) => token.uid === tokenUid);
return tokens[tokenUid] != null;
}

export async function getFullnodeNetwork() {
Expand Down
7 changes: 4 additions & 3 deletions src/sagas/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { metadataApi } from '@hathor/wallet-lib';
import { channel } from 'redux-saga';
import { get } from 'lodash';
import { specificTypeAndPayload, dispatchAndWait } from './helpers';
import { specificTypeAndPayload, dispatchAndWait, getRegisteredTokenUids } from './helpers';
import { mapTokenHistory } from '../utils';
import {
types,
Expand Down Expand Up @@ -139,6 +139,7 @@ function* fetchTokenHistory(action) {
* This saga will route the actions dispatched from SET_TOKEN and NEW_TOKEN to the
* TOKEN_FETCH_BALANCE_REQUESTED saga, the idea is to load the balance for new tokens
* registered or created on the app.
* @param {{type: string; payload: Object;}} action to route
*/
function* routeTokenChange(action) {
const wallet = yield select((state) => state.wallet);
Expand All @@ -153,8 +154,8 @@ function* routeTokenChange(action) {
break;
case 'SET_TOKENS':
default:
for (const token of action.payload) {
yield put({ type: types.TOKEN_FETCH_BALANCE_REQUESTED, tokenId: token.uid });
for (const uid of getRegisteredTokenUids({ tokens: action.payload })) {
yield put({ type: types.TOKEN_FETCH_BALANCE_REQUESTED, tokenId: uid });
}
break;
}
Expand Down
21 changes: 11 additions & 10 deletions src/sagas/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
checkForFeatureFlag,
getRegisteredTokens,
getNetworkSettings,
getRegisteredTokenUids,
} from './helpers';
import { setKeychainPin } from '../utils';

Expand Down Expand Up @@ -289,6 +290,7 @@ export function* startWallet(action) {
* and dispatch actions to asynchronously load all registered tokens.
*
* Will throw an error if the download fails for any token.
* @returns {string[]} Array of token uid
*/
export function* loadTokens() {
const customTokenUid = DEFAULT_TOKEN.uid;
Expand All @@ -304,11 +306,11 @@ export function* loadTokens() {

const wallet = yield select((state) => state.wallet);

const registeredTokens = yield getRegisteredTokens(wallet);
const tokens = yield getRegisteredTokens(wallet);

yield put(setTokens(registeredTokens));
yield put(setTokens(tokens));

const registeredUids = registeredTokens.map((t) => t.uid);
const registeredUids = getRegisteredTokenUids({ tokens });

// We don't need to wait for the metadatas response, so we can just
// spawn a new "thread" to handle it.
Expand Down Expand Up @@ -451,8 +453,7 @@ export function* handleTx(action) {
}

// find tokens affected by the transaction
const stateTokens = yield select((state) => state.tokens);
const registeredTokens = stateTokens.map((token) => token.uid);
const registeredUids = yield select(getRegisteredTokenUids);

// To be able to only download balances for tokens belonging to this wallet, we
// need a list of tokens and addresses involved in the transaction from both the
Expand All @@ -467,18 +468,18 @@ export function* handleTx(action) {
return acc;
}

const { token, decoded: { address } } = io;
const { token: tokenUid, decoded: { address } } = io;

// We are only interested in registered tokens
if (registeredTokens.indexOf(token) === -1) {
if (registeredUids.indexOf(tokenUid) === -1) {
return acc;
}

if (!acc[0][token]) {
acc[0][token] = new Set([]);
if (!acc[0][tokenUid]) {
acc[0][tokenUid] = new Set([]);
}

acc[0][token].add(address);
acc[0][tokenUid].add(address);
acc[1].add(address);

return acc;
Expand Down
8 changes: 2 additions & 6 deletions src/screens/UnregisterToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class UnregisterToken extends React.Component {
// XXX: maybe we should create a new action `removeToken`
// so we dont need to get all registered tokens to call setTokens
const promise = this.props.storage.unregisterToken(tokenUnregister).then(async () => {
const newTokens = [];
const newTokens = {};

const iterator = this.props.storage.getRegisteredTokens();
let next = await iterator.next();
Expand All @@ -85,11 +85,7 @@ class UnregisterToken extends React.Component {
while (!next.done) {
const token = next.value;
// We need to filter the token data to remove the metadata from this list (e.g. balance)
newTokens.push({
uid: token.uid,
symbol: token.symbol,
name: token.name,
});
newTokens[token.uid] = { ...token };
// eslint-disable-next-line no-await-in-loop
next = await iterator.next();
}
Expand Down

0 comments on commit 527a75d

Please sign in to comment.