Skip to content

Commit

Permalink
TW-683: Assets rework. Tokens. + Whitelist
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-tsx committed Sep 20, 2023
1 parent 30ef46c commit 5af4077
Show file tree
Hide file tree
Showing 39 changed files with 409 additions and 400 deletions.
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
- Migrate IndexDB tokens into Redux
- `token.metadata.null`
- No-cycle lint rule
- `export { dispatch } from 'app/store';`
- `export { useSelector, dispatch } from 'app/store';`
- Whitelisted tokens to Redux store
- Minimize persisted data
- Refactor `lib/assets/hooks`
- `useSlugsOf`
-
7 changes: 6 additions & 1 deletion src/app/hooks/use-assets-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useEffect } from 'react';

import { useDispatch } from 'react-redux';

import { loadAccountTokensActions } from 'app/store/assets/actions';
import { loadAccountTokensActions, loadTokensWhitelistActions } from 'app/store/assets/actions';
import { useAccount, useChainId } from 'lib/temple/front';
import { TempleChainId } from 'lib/temple/types';

export const useAssetsLoading = () => {
const chainId = useChainId()!;
Expand All @@ -14,4 +15,8 @@ export const useAssetsLoading = () => {
useEffect(() => {
dispatch(loadAccountTokensActions.submit({ account: publicKeyHash, chainId }));
}, [chainId, publicKeyHash]);

useEffect(() => {
if (chainId === TempleChainId.Mainnet) dispatch(loadTokensWhitelistActions.submit());
}, [chainId]);
};
11 changes: 1 addition & 10 deletions src/app/hooks/use-metadata-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@ import { isEqual } from 'lodash';
import { useDispatch } from 'react-redux';

import { useAccountTokensSelector } from 'app/store/assets/selectors';
import {
loadTokensMetadataAction,
loadWhitelistAction,
resetTokensMetadataLoadingAction
} from 'app/store/tokens-metadata/actions';
import { loadTokensMetadataAction, resetTokensMetadataLoadingAction } from 'app/store/tokens-metadata/actions';
import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors';
import { METADATA_SYNC_INTERVAL } from 'lib/fixed-times';
import { useAccount, useChainId, useTezos, useCollectibleTokens } from 'lib/temple/front';
import { TempleChainId } from 'lib/temple/types';
import { useInterval, useMemoWithCompare } from 'lib/ui/hooks';

export const useMetadataLoading = () => {
Expand Down Expand Up @@ -43,10 +38,6 @@ export const useMetadataLoading = () => {
isEqual
);

useEffect(() => {
if (chainId === TempleChainId.Mainnet) dispatch(loadWhitelistAction.submit());
}, [chainId]);

useEffect(() => {
dispatch(resetTokensMetadataLoadingAction());

Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/AddAsset/AddAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Alert, FormField, FormSubmitButton, NoSpaceField } from 'app/atoms';
import Spinner from 'app/atoms/Spinner/Spinner';
import { ReactComponent as AddIcon } from 'app/icons/add.svg';
import PageLayout from 'app/layouts/PageLayout';
import { addTokensMetadataAction } from 'app/store/tokens-metadata/actions';
import { putTokensMetadataAction } from 'app/store/tokens-metadata/actions';
import { useFormAnalytics } from 'lib/analytics';
import { TokenMetadataResponse } from 'lib/apis/temple';
import { toTokenSlug } from 'lib/assets';
Expand Down Expand Up @@ -215,7 +215,7 @@ const Form: FC = () => {
id: tokenId
};

dispatch(addTokensMetadataAction([tokenMetadata]));
dispatch(putTokensMetadataAction([tokenMetadata]));

await Repo.accountTokens.put(
{
Expand Down
9 changes: 5 additions & 4 deletions src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import { useIsEnabledAdsBannerSelector } from 'app/store/settings/selectors';
import { ButtonForManageDropdown } from 'app/templates/ManageDropdown';
import SearchAssetField from 'app/templates/SearchAssetField';
import { OptimalPromoVariantEnum } from 'lib/apis/optimal';
import { TEMPLE_TOKEN_SLUG, TEZ_TOKEN_SLUG, useDisplayedAccountTokens } from 'lib/assets';
import { TEZ_TOKEN_SLUG, TEMPLE_TOKEN_SLUG } from 'lib/assets';
import { useEnabledAccountTokens } from 'lib/assets/hooks';
import { useFilteredAssetsSlugs } from 'lib/assets/use-filtered';
import { T, t } from 'lib/i18n';
import { useAccount, useChainId } from 'lib/temple/front';
import { useAccount } from 'lib/temple/front';
import { useSyncTokens } from 'lib/temple/front/sync-tokens';
import { useMemoWithCompare } from 'lib/ui/hooks';
import { useLocalStorage } from 'lib/ui/local-storage';
Expand All @@ -45,7 +46,7 @@ export const TokensTab: FC = () => {
const { isSyncing } = useSyncTokens();
const { popup } = useAppEnv();

const tokens = useDisplayedAccountTokens();
const tokens = useEnabledAccountTokens();

const [isZeroBalancesHidden, setIsZeroBalancesHidden] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false);

Expand All @@ -54,7 +55,7 @@ export const TokensTab: FC = () => {
[setIsZeroBalancesHidden]
);

const slugs = useMemoWithCompare(() => tokens.map(({ slug }) => slug).sort(), [tokens], isEqual);
const slugs = useMemoWithCompare(() => tokens.map(({ slug }) => slug), [tokens], isEqual);

const { filteredAssets, searchValue, setSearchValue } = useFilteredAssetsSlugs(
slugs,
Expand Down
37 changes: 15 additions & 22 deletions src/app/pages/ManageAssets/ManageTokens.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, memo, useCallback } from 'react';
import React, { FC, memo, useCallback, useMemo } from 'react';

import classNames from 'clsx';
import { useDispatch } from 'react-redux';
Expand All @@ -8,13 +8,14 @@ import { ReactComponent as AddIcon } from 'app/icons/add-to-list.svg';
import { ReactComponent as CloseIcon } from 'app/icons/close.svg';
import { ReactComponent as SearchIcon } from 'app/icons/search.svg';
import { ManageAssetsSelectors } from 'app/pages/ManageAssets/ManageAssets.selectors';
import { setTokenStatusToRemovedAction, toggleTokenStatusAction } from 'app/store/assets/actions';
import { setTokenStatusAction } from 'app/store/assets/actions';
import { useAccountTokensAreLoadingSelector } from 'app/store/assets/selectors';
import { useTokensMetadataLoadingSelector } from 'app/store/tokens-metadata/selectors';
import { AssetIcon } from 'app/templates/AssetIcon';
import SearchAssetField from 'app/templates/SearchAssetField';
import { setAnotherSelector, setTestID } from 'lib/analytics';
import { TEMPLE_TOKEN_SLUG, useAccountTokens } from 'lib/assets';
import { TEMPLE_TOKEN_SLUG } from 'lib/assets';
import { useAccountTokens } from 'lib/assets/hooks';
import { useFilteredAssetsSlugs } from 'lib/assets/use-filtered';
import { T, t } from 'lib/i18n';
import { useAssetMetadata, getAssetName, getAssetSymbol } from 'lib/metadata';
Expand All @@ -32,9 +33,9 @@ export const ManageTokensContent: FC = memo(() => {

const tokens = useAccountTokens(publicKeyHash, chainId);

const managebleTokens = tokens.reduce<string[]>(
(acc, { slug, status }) => (slug === TEMPLE_TOKEN_SLUG || status === 'removed' ? acc : acc.concat(slug)),
[]
const managebleTokens = useMemo(
() => tokens.reduce<string[]>((acc, { slug }) => (slug === TEMPLE_TOKEN_SLUG ? acc : acc.concat(slug)), []),
[tokens]
);

const tokensAreLoading = useAccountTokensAreLoadingSelector();
Expand All @@ -56,8 +57,7 @@ export const ManageTokensContent: FC = memo(() => {
title: t('deleteTokenConfirm')
});

if (confirmed) return;
dispatch(setTokenStatusToRemovedAction({ account: publicKeyHash, chainId, slug }));
if (confirmed) dispatch(setTokenStatusAction({ account: publicKeyHash, chainId, slug, status: 'removed' }));
} catch (err: any) {
console.error(err);
alert(err.message);
Expand All @@ -67,14 +67,10 @@ export const ManageTokensContent: FC = memo(() => {
);

const toggleTokenStatus = useCallback(
async (slug: string) => {
try {
dispatch(toggleTokenStatusAction({ account: publicKeyHash, chainId, slug }));
} catch (err: any) {
console.error(err);
alert(err.message);
}
},
(slug: string, toDisable: boolean) =>
void dispatch(
setTokenStatusAction({ account: publicKeyHash, chainId, slug, status: toDisable ? 'disabled' : 'enabled' })
),
[chainId, publicKeyHash]
);

Expand Down Expand Up @@ -106,14 +102,13 @@ export const ManageTokensContent: FC = memo(() => {
{managableSlugs.map((slug, i, arr) => {
const last = i === arr.length - 1;
const status = tokens.find(t => t.slug === slug)!.status;
const checked = !status || status === 'enabled';

return (
<ListItem
key={slug}
assetSlug={slug}
last={last}
checked={checked}
checked={status === 'enabled'}
onRemove={removeToken}
onToggle={toggleTokenStatus}
/>
Expand All @@ -131,16 +126,14 @@ type ListItemProps = {
assetSlug: string;
last: boolean;
checked: boolean;
onToggle: (slug: string) => void;
onToggle: (slug: string, toDisable: boolean) => void;
onRemove: (slug: string) => void;
};

const ListItem = memo<ListItemProps>(({ assetSlug, last, checked, onToggle, onRemove }) => {
const metadata = useAssetMetadata(assetSlug);

const onCheckboxChange = useCallback(() => {
onToggle(assetSlug);
}, [assetSlug, onToggle]);
const onCheckboxChange = useCallback((checked: boolean) => void onToggle(assetSlug, !checked), [assetSlug, onToggle]);

const onRemoveBtnClick = useCallback<React.MouseEventHandler<HTMLDivElement>>(
event => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/Withdraw/Debit/AliceBob/steps/SellStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ReactComponent as CopyIcon } from 'app/icons/copy.svg';
import { WithdrawSelectors } from 'app/pages/Withdraw/Withdraw.selectors';
import { AnalyticsEventCategory, setTestID, useAnalytics, useFormAnalytics } from 'lib/analytics';
import { AliceBobOrderStatus, cancelAliceBobOrder } from 'lib/apis/temple';
import { toTransferParams } from 'lib/assets/utils';
import { toTransferParams } from 'lib/assets/contract.utils';
import { T, TID } from 'lib/i18n';
import { TEZOS_METADATA } from 'lib/metadata/defaults';
import { useAccount, useTezos } from 'lib/temple/front';
Expand Down
25 changes: 19 additions & 6 deletions src/app/store/assets/actions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { createAction } from '@reduxjs/toolkit';

import { WhitelistResponseToken } from 'lib/apis/temple';
import { createActions } from 'lib/store';

import { StorredToken } from './state';
import { StoredAssetStatus, StoredToken } from './state';

interface LoadTokensPayload {
/** PKH */
account: string;
chainId: string;
}

export const loadAccountTokensActions = createActions<
{ account: string; chainId: string },
{ account: string; chainId: string; slugs: string[] },
LoadTokensPayload,
LoadTokensPayload & { slugs: string[] },
{ code?: number }
>('assets/LOAD_ACCOUNT_TOKENS');

type TokenStatusAlterPayload = Pick<StorredToken, 'account' | 'chainId' | 'slug'>;
type LoadWhitelistPayload = WhitelistResponseToken[];

export const loadTokensWhitelistActions = createActions<void, LoadWhitelistPayload, { code?: number }>(
'assets/LOAD_TOKENS_WHITELIST'
);

export const setTokenStatusToRemovedAction = createAction<TokenStatusAlterPayload>('assets/SET_TOKEN_REMOVED');
interface SetTokenStatusPayload extends Pick<StoredToken, 'account' | 'chainId' | 'slug'> {
status: StoredAssetStatus;
}

export const toggleTokenStatusAction = createAction<TokenStatusAlterPayload>('assets/TOGGLE_TOKEN_STATUS');
export const setTokenStatusAction = createAction<SetTokenStatusPayload>('assets/SET_TOKEN_STATUS');
28 changes: 18 additions & 10 deletions src/app/store/assets/epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@ import { catchError, map, switchMap } from 'rxjs/operators';
import { Action } from 'ts-action';
import { ofType, toPayload } from 'ts-action-operators';

import { loadAccountTokensActions } from './actions';
import { fetchWhitelistTokens$ } from 'lib/apis/temple';

import { loadAccountTokensActions, loadTokensWhitelistActions } from './actions';
import { fetchAccountTokens } from './utils';

const loadAccountTokensEpic: Epic = (action$: Observable<Action>) =>
const loadAccountTokensEpic: Epic<Action> = action$ =>
action$.pipe(
ofType(loadAccountTokensActions.submit),
toPayload(),
switchMap(({ account, chainId }) =>
from(fetchAccountTokens(account, chainId)).pipe(
map(tokens =>
loadAccountTokensActions.success({
account,
chainId,
slugs: tokens.map(t => t.slug)
})
),
map(tokens => tokens.map(t => t.slug)),
map(slugs => loadAccountTokensActions.success({ account, chainId, slugs })),
catchError(err => of(loadAccountTokensActions.fail({ code: 404 })))
)
)
);

export const assetsEpics = combineEpics(loadAccountTokensEpic);
const loadTokensWhitelistEpic: Epic<Action> = action$ =>
action$.pipe(
ofType(loadTokensWhitelistActions.submit),
switchMap(() =>
fetchWhitelistTokens$().pipe(
map(loadTokensWhitelistActions.success),
catchError(err => of(loadTokensWhitelistActions.fail({ code: 404 })))
)
)
);

export const assetsEpics = combineEpics(loadAccountTokensEpic, loadTokensWhitelistEpic);
29 changes: 20 additions & 9 deletions src/app/store/assets/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';

import { loadAccountTokensActions, setTokenStatusToRemovedAction, toggleTokenStatusAction } from './actions';
import { toTokenSlug } from 'lib/assets';

import { loadAccountTokensActions, loadTokensWhitelistActions, setTokenStatusAction } from './actions';
import { initialState, SliceState } from './state';

export const assetsReducer = createReducer<SliceState>(initialState, builder => {
Expand Down Expand Up @@ -31,21 +33,30 @@ export const assetsReducer = createReducer<SliceState>(initialState, builder =>
}
});

builder.addCase(setTokenStatusToRemovedAction, (state, { payload: { account, chainId, slug } }) => {
const tokens = state.tokens.data;
const index = tokens.findIndex(t => t.account === account && t.chainId === chainId && t.slug === slug);
const token = tokens[index] ?? { account, chainId, slug };
builder.addCase(loadTokensWhitelistActions.submit, state => {
state.mainnetWhitelist.isLoading = true;
});

token.status = 'removed';
tokens[index === -1 ? tokens.length : index] = token;
builder.addCase(loadTokensWhitelistActions.fail, state => {
state.mainnetWhitelist.isLoading = false;
});

builder.addCase(loadTokensWhitelistActions.success, (state, { payload }) => {
state.mainnetWhitelist.isLoading = false;
delete state.mainnetWhitelist.error;

for (const token of payload) {
const slug = toTokenSlug(token.contractAddress, token.fa2TokenId);
if (!state.mainnetWhitelist.data.includes(slug)) state.mainnetWhitelist.data.push(slug);
}
});

builder.addCase(toggleTokenStatusAction, (state, { payload: { account, chainId, slug } }) => {
builder.addCase(setTokenStatusAction, (state, { payload: { account, chainId, slug, status } }) => {
const tokens = state.tokens.data;
const index = tokens.findIndex(t => t.account === account && t.chainId === chainId && t.slug === slug);
const token = tokens[index] ?? { account, chainId, slug };

token.status = token.status === 'disabled' ? 'enabled' : 'disabled';
token.status = status;
tokens[index === -1 ? tokens.length : index] = token;
});
});
4 changes: 3 additions & 1 deletion src/app/store/assets/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { isEqual } from 'lodash';

import { useMemoWithCompare } from 'lib/ui/hooks';

import { useSelector } from '../index';
import { useSelector } from '../root-state.selector';

export const useAccountTokensAreLoadingSelector = () => useSelector(state => state.assets.tokens.isLoading);

Expand All @@ -15,3 +15,5 @@ export const useAccountTokensSelector = (account: string, chainId: string) => {
isEqual
);
};

export const useMainnetTokensWhitelistSelector = () => useSelector(state => state.assets.mainnetWhitelist);
3 changes: 2 additions & 1 deletion src/app/store/assets/state.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { createEntity } from 'lib/store';
import { SliceState } from './state';

export const mockAssetsState: SliceState = {
tokens: createEntity([])
tokens: createEntity([]),
mainnetWhitelist: createEntity([])
};
Loading

0 comments on commit 5af4077

Please sign in to comment.