Skip to content
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

fix: token duplication on dashboard #431

Merged
merged 2 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const tokens = Object.values(props.tokens);
const tokens = getRegisteredTokenUids(props);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

He is getting the token objects here. The method getRegisteredTokenUids gets only the uid, if I'm not wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, @pedroferreira1. We should stick with the object here, not the uid.

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 @@ -288,6 +289,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 @@ -303,11 +305,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 @@ -450,8 +452,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 @@ -466,18 +467,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
Loading