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 4, 2024
1 parent 0717b12 commit 83e092f
Show file tree
Hide file tree
Showing 33 changed files with 1,679 additions and 997 deletions.
21 changes: 20 additions & 1 deletion config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,24 @@
},
"recoverUninscribedTaprootUtxosFeatureEnabled": true,
"runesEnabled": true,
"swapsEnabled": true
"swapsEnabled": true,
"tokensEnabledByDefault": [
"Bitcoin",
"stacks",
"DOG•GO•TO•THE•MOON",
"RSIC•GENESIS•RUNE",
"PUPS•WORLD•PEACE",
"UNCOMMON•GOODS",
"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.

59 changes: 59 additions & 0 deletions src/app/common/hooks/use-default-enabled-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import get from 'lodash.get';

import { useFilteredSip10Tokens, useRemoteConfig } from '@leather.io/query';

import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import type { TokenUserSetting } from '@app/store/manage-tokens/manage-tokens.slice';

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

export function useDefaultEnabledTokens() {
const config = useRemoteConfig();
const configEnabledTokens: string[] = get(config, 'defaultEnabledTokens', []);
const stxAddress = useCurrentStacksAccount()?.address ?? '';
const sip10Tokens = useFilteredSip10Tokens({ address: stxAddress, filter: 'supported' });

function isTokenEnabled({ userTokensList, accountIndex, tokenId }: IsTokenEnabledArgs) {
const token = userTokensList.find(t => t.accountIndex === accountIndex && t.id === tokenId);
const isEnabledByDefault =
configEnabledTokens.includes(tokenId) ||
sip10Tokens.tokens.some(t => t.info.contractId === tokenId);
return token?.enabled ?? isEnabledByDefault;
}

function filterTokens<T>({
tokens,
accountIndex,
userTokensList,
filter = 'all',
getTokenId,
}: FilterTokensArgs<T>): T[] {
if (filter === 'all') return tokens;
return tokens.filter(t => {
const tokenId = getTokenId(t);

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

return { isTokenEnabled, filterTokens };
}
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
61 changes: 61 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,61 @@
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 { useDefaultEnabledTokens } from '@app/common/hooks/use-default-enabled-tokens';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { manageTokensSlice, useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

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

export function CryptoAssetItemToggleLayout({
captionLeft,
icon,
titleLeft,
assetId,
}: CryptoAssetItemToggleLayoutProps) {
const userTokensList = useUserAllTokens();
const accountIndex = useCurrentAccountIndex();
const dispatch = useDispatch();
const { isTokenEnabled } = useDefaultEnabledTokens();

const tokenId = assetId ?? titleLeft;
const [isChecked, setIsChecked] = useState(
isTokenEnabled({ accountIndex, userTokensList, tokenId })
);

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

const toggle = (
<VStack h="100%" justifyContent="center">
<Switch.Root onCheckedChange={handleSelection} checked={isChecked} id={tokenId}>
<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 @@ -113,5 +113,9 @@ export function CryptoAssetItemLayout({
</Pressable>
);

return <Box my="space.02">{content}</Box>;
return (
<Box my="space.02" data-testid={dataTestId}>
{content}
</Box>
);
}
44 changes: 34 additions & 10 deletions src/app/components/loaders/brc20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import type { Brc20CryptoAssetInfo, CryptoAssetBalance, MarketData } from '@leather.io/models';

import {
type AssetFilter,
useDefaultEnabledTokens,
} from '@app/common/hooks/use-default-enabled-tokens';
import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

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: Brc20TokenItem[]): 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 accountIndex = useCurrentAccountIndex();
const userTokensList = useUserAllTokens();

const { filterTokens } = useDefaultEnabledTokens();

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

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

import {
type AssetFilter,
useDefaultEnabledTokens,
} from '@app/common/hooks/use-default-enabled-tokens';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

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

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

function getTokenId(token: RuneTokenItem) {
return token.info.spacedRuneName ?? 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 accountIndex = useCurrentAccountIndex();
const userTokensList = useUserAllTokens();
const { filterTokens } = useDefaultEnabledTokens();

const filteredTokens = filterTokens({
tokens: runes,
accountIndex,
userTokensList,
filter,
getTokenId,
});

return children(filteredTokens);
}
34 changes: 32 additions & 2 deletions src/app/components/loaders/sip10-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
import { type Sip10CryptoAssetFilter, type Sip10TokenAssetDetails } from '@leather.io/query';

import {
type AssetFilter,
useDefaultEnabledTokens,
} from '@app/common/hooks/use-default-enabled-tokens';
import { useCombinedFilteredSip10Tokens } from '@app/common/hooks/use-filtered-sip10-tokens';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

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

function getTokenId(token: Sip10TokenAssetDetails) {
return token.info.contractId;
}

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

const accountIndex = useCurrentAccountIndex();
const userTokensList = useUserAllTokens();
const { filterTokens } = useDefaultEnabledTokens();

const filteredTokens = filterTokens({
tokens,
accountIndex,
userTokensList,
filter: assetFilter,
getTokenId,
});

return children(isLoading, filteredTokens);
}
28 changes: 26 additions & 2 deletions src/app/components/loaders/src20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import type { CryptoAssetBalance, Src20CryptoAssetInfo } from '@leather.io/models';
import { useSrc20TokensByAddress } from '@leather.io/query';

import {
type AssetFilter,
useDefaultEnabledTokens,
} from '@app/common/hooks/use-default-enabled-tokens';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUserAllTokens } from '@app/store/manage-tokens/manage-tokens.slice';

export interface Src20TokenAssetDetails {
balance: CryptoAssetBalance;
info: Src20CryptoAssetInfo;
}

interface Src20TokensLoaderProps {
address: string;
filter?: AssetFilter;
children(tokens: Src20TokenAssetDetails[]): React.ReactNode;
}
export function Src20TokensLoader({ address, children }: Src20TokensLoaderProps) {

function getTokenId(token: Src20TokenAssetDetails) {
return token.info.symbol;
}

export function Src20TokensLoader({ address, children, filter = 'all' }: Src20TokensLoaderProps) {
const { data: tokens = [] } = useSrc20TokensByAddress(address);
return children(tokens);
const accountIndex = useCurrentAccountIndex();
const { filterTokens } = useDefaultEnabledTokens();
const userTokensList = useUserAllTokens();

const filteredTokens = filterTokens({
tokens,
accountIndex,
userTokensList,
filter,
getTokenId,
});
return children(filteredTokens);
}
Loading

0 comments on commit 83e092f

Please sign in to comment.