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 9, 2024
1 parent 749908e commit 1430dd0
Show file tree
Hide file tree
Showing 38 changed files with 2,106 additions and 1,020 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"
]
}
1,619 changes: 829 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
56 changes: 56 additions & 0 deletions src/app/common/hooks/use-manage-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import get from 'lodash.get';

import { useRemoteConfig } 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 config = useRemoteConfig();
const configEnabledTokens: string[] = get(config, 'tokensEnabledByDefault', []);
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);

switch (filter) {
case 'enabled':
return isTokenEnabled({ tokenId, preEnabledTokensIds });
case 'disabled':
return !isTokenEnabled({ tokenId, preEnabledTokensIds });
default:
return true;
}
});
}

return { isTokenEnabled, filterTokens };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';

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';

interface CryptoAssetItemToggleLayoutProps {
captionLeft: string;
icon: React.ReactNode;
titleLeft: string;
assetId: string;
isCheckedByDefault?: boolean;
}

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

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

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

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

const content = (
<Pressable onClick={() => handleSelection(!isChecked)} data-testid={assetId}>
<ItemLayout
img={icon}
titleLeft={spamFilter(titleLeft)}
captionLeft={spamFilter(captionLeft)}
titleRight={toggle}
/>
</Pressable>
);

return <Box my="space.02">{content}</Box>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface CryptoAssetItemLayoutProps {
onSelectAsset?(symbol: string, contractId?: string): void;
titleLeft: string;
titleRightBulletInfo?: React.ReactNode;
dataTestId: string;
}
export function CryptoAssetItemLayout({
availableBalance,
Expand All @@ -45,9 +46,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 +114,9 @@ export function CryptoAssetItemLayout({
</Pressable>
);

return <Box my="space.02">{content}</Box>;
return (
<Box my="space.02" data-testid={dataTestId}>
{content}
</Box>
);
}
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 });
}
45 changes: 39 additions & 6 deletions src/app/components/loaders/sip10-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import { type Sip10CryptoAssetFilter, type Sip10TokenAssetDetails } from '@leather.io/query';
import { type Sip10TokenAssetDetails } from '@leather.io/query';

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

interface Sip10TokensLoaderProps {
address: string;
filter: Sip10CryptoAssetFilter;
children(isLoading: boolean, tokens: Sip10TokenAssetDetails[]): React.ReactNode;
assetFilter?: AssetFilter;

children({
isLoading,
tokens,
preEnabledTokensIds,
}: {
isLoading: boolean;
tokens: Sip10TokenAssetDetails[];
preEnabledTokensIds: string[];
}): React.ReactNode;
}

function getTokenId(token: Sip10TokenAssetDetails) {
return token.info.contractId;
}
export function Sip10TokensLoader({ address, filter, children }: Sip10TokensLoaderProps) {
const { isLoading, tokens = [] } = useCombinedFilteredSip10Tokens({ address, filter });
return children(isLoading, tokens);

export function Sip10TokensLoader({
address,
assetFilter = 'all',
children,
}: Sip10TokensLoaderProps) {
const {
isLoading,
allTokens = [],
supportedTokens,
} = useCombinedFilteredSip10Tokens({ address, filter: 'all' });

const { filterTokens } = useManageTokens();
const preEnabledTokensIds = supportedTokens.map(t => t.info.contractId);
const filteredTokens = filterTokens({
tokens: allTokens,
filter: assetFilter,
getTokenId,
preEnabledTokensIds,
});

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

0 comments on commit 1430dd0

Please sign in to comment.