Skip to content

Commit

Permalink
feat: add manage tokens, closes #5643
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Oct 15, 2024
1 parent 775adf8 commit 7d3ac6f
Show file tree
Hide file tree
Showing 43 changed files with 2,133 additions and 1,051 deletions.
19 changes: 18 additions & 1 deletion config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,22 @@
},
"recoverUninscribedTaprootUtxosFeatureEnabled": true,
"runesEnabled": true,
"swapsEnabled": true
"swapsEnabled": true,
"tokensEnabledByDefault": [
"DOGGOTOTHEMOON",
"RSICGENESISRUNE",
"PUPSWORLDPEACE",
"UNCOMMONGOODS",
"SP3NE50GEXFG9SZGTT51P40X2CKYSZ5CC4ZTZ7A2G.welshcorgicoin-token::welshcorgicoin",
"SP1AY6K3PQV5MRT6R4S671NWW2FRVPKM0BR162CT6.leo-token::leo",
"SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token::miamicoin",
"SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-aeusdc::aeusdc",
"SP2XD7417HGPRTREMKF748VNEQPDRR0RMANB7X1NK.token-abtc::bridged-btc",
"SP2XD7417HGPRTREMKF748VNEQPDRR0RMANB7X1NK.token-susdt::bridged-usdt",
"SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex::alex",
"SM26NBC8SFHNW4P1Y4DFH27974P56WN86C92HPEHH.token-lqstx::lqstx",
"SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.velar-token::velar",
"SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token::ststx",
"SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.arkadiko-token::diko"
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"@leather.io/constants": "0.12.1",
"@leather.io/crypto": "1.6.2",
"@leather.io/models": "0.17.0",
"@leather.io/query": "2.13.1",
"@leather.io/query": "2.14.1",
"@leather.io/stacks": "1.1.5",
"@leather.io/tokens": "0.9.0",
"@leather.io/ui": "1.27.1",
Expand Down
1,673 changes: 883 additions & 790 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions src/app/common/hooks/use-filtered-sip10-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ interface UseSip10TokensArgs {
filter?: Sip10CryptoAssetFilter;
}
// TODO: Migrate to mono
export function useCombinedFilteredSip10Tokens({ address, filter = 'all' }: UseSip10TokensArgs) {
export function useCombinedFilteredSip10Tokens({ address }: UseSip10TokensArgs) {
const { isLoading, tokens = [] } = useFilteredSip10Tokens({ address });
const { data: alexSwapAssets = [] } = useAlexSwappableAssets(address);
const { data: bitflowSwapAssets = [] } = useBitflowSwappableAssets(address);
const filteredTokens = useMemo(
() => filterSip10Tokens([...alexSwapAssets, ...bitflowSwapAssets], tokens, filter),
[alexSwapAssets, bitflowSwapAssets, tokens, filter]
);
return { isLoading, tokens: filteredTokens };

const filteredTokens = useMemo(() => {
const assets = [...alexSwapAssets, ...bitflowSwapAssets];
return {
allTokens: filterSip10Tokens(assets, tokens, 'all'),
supportedTokens: filterSip10Tokens(assets, tokens, 'supported'),
unsupportedTokens: filterSip10Tokens(assets, tokens, 'unsupported'),
};
}, [alexSwapAssets, bitflowSwapAssets, tokens]);

return { isLoading, ...filteredTokens };
}
2 changes: 2 additions & 0 deletions src/app/common/hooks/use-key-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useStacksClient } from '@app/store/common/api-clients.hooks';
import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions';
import { bitcoinKeysSlice } from '@app/store/ledger/bitcoin/bitcoin-key.slice';
import { stacksKeysSlice } from '@app/store/ledger/stacks/stacks-key.slice';
import { manageTokensSlice } from '@app/store/manage-tokens/manage-tokens.slice';
import { networksSlice } from '@app/store/networks/networks.slice';
import { clearWalletSession } from '@app/store/session-restore';
import { keyActions } from '@app/store/software-keys/software-key.actions';
Expand Down Expand Up @@ -63,6 +64,7 @@ export function useKeyActions() {
dispatch(keyActions.signOut());
dispatch(bitcoinKeysSlice.actions.signOut());
dispatch(stacksKeysSlice.actions.signOut());
dispatch(manageTokensSlice.actions.removeAllTokens());
await clearChromeStorage();
partiallyClearLocalStorage();
void analytics.track('sign_out');
Expand Down
49 changes: 49 additions & 0 deletions src/app/common/hooks/use-manage-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useConfigTokensEnabledByDefault } from '@leather.io/query';

import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

interface IsTokenEnabledArgs {
tokenId: string;
preEnabledTokensIds: string[];
}
export type AssetFilter = 'all' | 'enabled' | 'disabled';
interface FilterTokensArgs<T> {
tokens: T[];
filter: AssetFilter;
getTokenId(token: T): string;
preEnabledTokensIds: string[];
}

export function useManageTokens() {
const configEnabledTokens = useConfigTokensEnabledByDefault();

const accountIndex = useCurrentAccountIndex();
const userTokensList = useUserAllTokens();

function isTokenEnabled({ tokenId, preEnabledTokensIds }: IsTokenEnabledArgs) {
const token = userTokensList.find(t => t.accountIndex === accountIndex && t.id === tokenId);
const isEnabledByDefault =
configEnabledTokens.includes(tokenId) || preEnabledTokensIds?.includes(tokenId);

return token?.enabled ?? isEnabledByDefault;
}

function filterTokens<T>({
tokens,
filter = 'all',
getTokenId,
preEnabledTokensIds,
}: FilterTokensArgs<T>): T[] {
if (filter === 'all') return tokens;

return tokens.filter(t => {
const tokenId = getTokenId(t);
const tokenEnabled = isTokenEnabled({ tokenId, preEnabledTokensIds });

return filter === 'enabled' ? tokenEnabled : !tokenEnabled;
});
}

return { isTokenEnabled, filterTokens };
}
60 changes: 60 additions & 0 deletions src/app/components/crypto-asset-item/crypto-asset-item-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';

import { sanitize } from 'dompurify';
import { Box, VStack } from 'leather-styles/jsx';

import { ItemLayout, Pressable, Switch } from '@leather.io/ui';
import { spamFilter } from '@leather.io/utils';

import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { manageTokensSlice } from '@app/store/manage-tokens/manage-tokens.slice';

export interface CryptoAssetItemToggleProps {
captionLeft: string;
icon: React.ReactNode;
titleLeft: string;
assetId: string;
isCheckedByDefault: boolean;
}

export function CryptoAssetItemToggle({
captionLeft,
icon,
titleLeft,
assetId,
isCheckedByDefault = false,
}: CryptoAssetItemToggleProps) {
const accountIndex = useCurrentAccountIndex();
const dispatch = useDispatch();

const [isChecked, setIsChecked] = useState(isCheckedByDefault);

function handleSelection(enabled: boolean) {
setIsChecked(enabled);
dispatch(
manageTokensSlice.actions.userTogglesTokenVisibility({ id: assetId, enabled, accountIndex })
);
}

const toggle = (
<VStack h="100%" justifyContent="center">
<Switch.Root onCheckedChange={handleSelection} checked={isChecked} id={assetId}>
<Switch.Thumb />
</Switch.Root>
</VStack>
);

return (
<Box my="space.02">
<Pressable onClick={() => handleSelection(!isChecked)} data-testid={sanitize(assetId)}>
<ItemLayout
img={icon}
titleLeft={spamFilter(titleLeft)}
captionLeft={spamFilter(captionLeft)}
titleRight={toggle}
/>
</Pressable>
</Box>
);
}
14 changes: 10 additions & 4 deletions src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sanitize } from 'dompurify';
import { Box, Flex } from 'leather-styles/jsx';

import type { Money } from '@leather.io/models';
Expand All @@ -16,7 +17,7 @@ import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip';

import { parseCryptoAssetBalance } from './crypto-asset-item.layout.utils';

interface CryptoAssetItemLayoutProps {
export interface CryptoAssetItemLayoutProps {
availableBalance: Money;
balanceSuffix?: string;
captionLeft: string;
Expand All @@ -30,6 +31,7 @@ interface CryptoAssetItemLayoutProps {
onSelectAsset?(symbol: string, contractId?: string): void;
titleLeft: string;
titleRightBulletInfo?: React.ReactNode;
dataTestId: string;
}
export function CryptoAssetItemLayout({
availableBalance,
Expand All @@ -45,9 +47,9 @@ export function CryptoAssetItemLayout({
onSelectAsset,
titleLeft,
titleRightBulletInfo,
dataTestId,
}: CryptoAssetItemLayoutProps) {
const { availableBalanceString, dataTestId, formattedBalance } =
parseCryptoAssetBalance(availableBalance);
const { availableBalanceString, formattedBalance } = parseCryptoAssetBalance(availableBalance);

const titleRight = (
<SkeletonLoader width="126px" isLoading={isLoading}>
Expand Down Expand Up @@ -113,5 +115,9 @@ export function CryptoAssetItemLayout({
</Pressable>
);

return <Box my="space.02">{content}</Box>;
return (
<Box my="space.02" data-testid={sanitize(dataTestId)}>
{content}
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors';

import type { Money } from '@leather.io/models';
import { formatMoneyWithoutSymbol } from '@leather.io/utils';

import { formatBalance } from '@app/common/format-balance';

export function parseCryptoAssetBalance(availableBalance: Money) {
const availableBalanceString = formatMoneyWithoutSymbol(availableBalance);
const dataTestId = CryptoAssetSelectors.CryptoAssetListItem.replace(
'{symbol}',
availableBalance.symbol.toLowerCase()
);
const formattedBalance = formatBalance(availableBalanceString);

return {
availableBalanceString,
dataTestId,
formattedBalance,
};
}
20 changes: 20 additions & 0 deletions src/app/components/crypto-asset-item/crypto-asset-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CryptoAssetItemToggle, type CryptoAssetItemToggleProps } from './crypto-asset-item-toggle';
import { CryptoAssetItemLayout, type CryptoAssetItemLayoutProps } from './crypto-asset-item.layout';

interface CryptoAssetItemProps {
isToggleMode: boolean;
toggleProps?: CryptoAssetItemToggleProps;
itemProps?: CryptoAssetItemLayoutProps;
}

export function CryptoAssetItem({ isToggleMode, toggleProps, itemProps }: CryptoAssetItemProps) {
if (isToggleMode && toggleProps) {
return <CryptoAssetItemToggle {...toggleProps} />;
}

if (itemProps) {
return <CryptoAssetItemLayout {...itemProps} />;
}

return null;
}
46 changes: 36 additions & 10 deletions src/app/components/loaders/brc20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import type { Brc20CryptoAssetInfo, CryptoAssetBalance, MarketData } from '@leather.io/models';

import { type AssetFilter, useManageTokens } from '@app/common/hooks/use-manage-tokens';
import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks';

interface Brc20TokenItem {
balance: CryptoAssetBalance;
info: Brc20CryptoAssetInfo;
holderAddress: string;
marketData: MarketData;
}

interface Brc20TokensLoaderProps {
children(
tokens: {
balance: CryptoAssetBalance;
info: Brc20CryptoAssetInfo;
holderAddress: string;
marketData: MarketData;
}[]
): React.ReactNode;
filter?: AssetFilter;
children({
tokens,
preEnabledTokensIds,
}: {
tokens: Brc20TokenItem[];
preEnabledTokensIds: string[];
}): React.ReactNode;
}

function getTokenId(token: Brc20TokenItem) {
return token.info.symbol;
}
export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) {
export function Brc20TokensLoader({ children, filter = 'all' }: Brc20TokensLoaderProps) {
const tokens = useBrc20Tokens();
return children(tokens);

const { filterTokens } = useManageTokens();

const preEnabledTokensIds = tokens
.filter(t => t.marketData.price.amount.isGreaterThan(0))
.map(t => t.info.symbol);

const filteredTokens = filterTokens({
tokens,
filter,
getTokenId,
preEnabledTokensIds,
});

return children({ tokens: filteredTokens, preEnabledTokensIds });
}
39 changes: 34 additions & 5 deletions src/app/components/loaders/runes-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
import type { CryptoAssetBalance, MarketData, RuneCryptoAssetInfo } from '@leather.io/models';
import { useRuneTokens } from '@leather.io/query';

import { type AssetFilter, useManageTokens } from '@app/common/hooks/use-manage-tokens';

interface RunesLoaderProps {
addresses: string[];
children(
runes: { balance: CryptoAssetBalance; info: RuneCryptoAssetInfo; marketData: MarketData }[]
): React.ReactNode;
children({
tokens,
preEnabledTokensIds,
}: {
tokens: RuneTokenItem[];
preEnabledTokensIds: string[];
}): React.ReactNode;
filter?: AssetFilter;
}

interface RuneTokenItem {
balance: CryptoAssetBalance;
info: RuneCryptoAssetInfo;
marketData: MarketData;
}

function getTokenId(token: RuneTokenItem) {
return token.info.runeName;
}
export function RunesLoader({ addresses, children }: RunesLoaderProps) {

export function RunesLoader({ addresses, children, filter = 'all' }: RunesLoaderProps) {
const { runes = [] } = useRuneTokens(addresses);
return children(runes);

const { filterTokens } = useManageTokens();

const preEnabledTokensIds: string[] = [];
const filteredTokens = filterTokens({
tokens: runes,
filter,
getTokenId,
preEnabledTokensIds,
});

return children({ tokens: filteredTokens, preEnabledTokensIds });
}
Loading

0 comments on commit 7d3ac6f

Please sign in to comment.