From b0c3dde515f80361caa18d6c0b671a5f29235c57 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Fri, 10 May 2024 14:34:17 -0500 Subject: [PATCH] refactor: assets and asset list --- package.json | 2 +- pnpm-lock.yaml | 11 +- .../stacks-crypto-asset.utils.spec.ts | 72 ---------- .../stacks-crypto-asset.utils.ts | 19 --- .../use-stx-crypto-currency-asset-balance.ts | 8 -- .../hooks/balance/use-total-balance.tsx | 2 +- .../use-transferable-asset-balances.hooks.ts | 25 ---- src/app/common/stacks-utils.spec.ts | 16 ++- src/app/common/stacks-utils.ts | 12 ++ .../components/account/account-addresses.tsx | 2 +- .../account/bitcoin-account-loader.tsx | 47 ------- src/app/components/balance/btc-balance.tsx | 10 +- src/app/components/balance/stx-balance.tsx | 2 +- .../crypto-asset-item.layout.tsx | 36 +++-- .../crypto-asset-item.layout.utils.ts | 21 +++ .../brc20-token-asset-item.layout.tsx | 43 ------ .../brc20-token-asset-list.tsx | 41 ------ .../crypto-asset-item.utils.ts | 44 ------ .../fungible-token-asset.utils.ts | 47 ------- ...tacks-fungible-token-asset-item.layout.tsx | 60 -------- .../loaders/bitcoin-account-loader.tsx | 59 ++++++-- .../loaders/brc20-tokens-loader.tsx | 8 +- .../components/loaders/btc-balance-loader.tsx | 12 -- .../loaders/btc-crypto-asset-loader.tsx | 11 ++ .../loaders/current-bitcoin-signer-loader.tsx | 17 +++ .../loaders/stx-crypto-asset-loader.tsx | 11 ++ .../components => }/stacks-asset-avatar.tsx | 0 .../transaction/token-transfer-icon.tsx | 4 +- src/app/components/tx-asset-item.tsx | 2 +- .../stacks-transaction/ft-transfer-item.tsx | 10 +- src/app/features/asset-list/asset-list.tsx | 131 ++++++++++-------- .../brc20-token-asset-list.tsx | 47 +++++++ .../btc-crypto-asset-item-fallback.tsx | 23 +++ .../btc-crypto-asset-item.tsx} | 34 ++--- .../runes-asset-item.layout.tsx | 0 .../runes-asset-list/runes-asset-list.tsx | 0 .../src20-token-asset-item.layout.tsx | 0 .../src20-token-asset-list.tsx | 0 .../add-stacks-ledger-keys-item.tsx | 17 --- .../connect-ledger-asset-button.tsx | 8 +- ...tacks-fungible-token-asset-list.layout.tsx | 24 ---- .../stacks-fungible-token-asset-list.tsx | 15 -- .../stx-balance-list-item.layout.stories.tsx | 64 --------- .../stx-balance-list-item.layout.tsx | 52 ------- .../sip10-token-asset-item.tsx | 36 +++++ .../sip10-token-asset-item.utils.ts | 34 +++++ .../sip10-token-asset-list-unsupported.tsx} | 22 +-- .../sip10-token-asset-list.tsx | 29 ++++ .../stx-crypto-asset-item-fallback.tsx | 23 +++ .../stx-crypto-asset-item.stories.tsx | 81 +++++++++++ .../stx-crypto-asset-item.tsx} | 34 ++--- .../stx20-token-asset-item.layout.tsx | 0 .../stx20-token-asset-list.tsx | 0 .../features/collectibles/collectibles.tsx | 2 +- .../stacks/stacks-crypto-assets.tsx | 2 +- .../increase-stx-fee-dialog.tsx | 4 +- .../crypto-asset-list-item.tsx | 29 ---- .../crypto-asset-list.tsx | 70 ---------- .../crypto-currency-asset-icon.tsx | 15 -- .../fungible-token-asset-item.tsx | 20 --- .../contract-deploy-details.tsx | 6 +- .../hooks/use-transaction-error.ts | 4 +- .../fungible-post-condition-item.tsx | 5 +- .../stacks-transaction-signer.tsx | 4 +- .../transaction-error/error-messages.tsx | 4 +- .../choose-asset-to-fund.tsx | 81 ++++++----- src/app/pages/fund/fund.tsx | 19 +-- .../pages/home/components/account-actions.tsx | 1 + src/app/pages/home/components/assets.tsx | 19 +++ src/app/pages/home/components/send-button.tsx | 20 ++- src/app/pages/home/home.tsx | 4 +- src/app/pages/receive/receive-dialog.tsx | 4 +- .../choose-crypto-asset.tsx | 59 +++----- .../send-btc-disabled.tsx | 0 .../brc20-choose-fee.tsx} | 0 .../brc20-send-form-confirmation.tsx | 0 .../{brc-20 => brc20}/brc20-send-form.tsx | 0 .../{brc-20 => brc20}/use-brc20-send-form.tsx | 0 .../form/btc/btc-send-form.tsx | 24 ++-- .../sip10-token-send-form-container.tsx | 14 +- .../sip10-token-send-form.tsx | 31 +++-- .../use-sip10-send-form.tsx | 36 ++--- .../stacks/stacks-send-form-confirmation.tsx | 2 +- .../stacks/use-stacks-common-send-form.tsx | 4 +- .../form/stx/stx-send-form.tsx | 11 +- .../form/stx/use-stx-send-form.tsx | 4 +- .../send-crypto-asset-form.routes.tsx | 10 +- .../components/swap-asset-item.tsx | 8 +- .../transaction-request.tsx | 4 +- .../btc-balance-native-segwit.hooks.ts | 4 +- src/app/query/bitcoin/bitcoin-client.ts | 8 -- .../bitcoin/btc/btc-crypto-asset.hooks.ts | 48 +++++++ .../ordinals/brc20/brc20-tokens.hooks.ts | 55 ++++---- .../ordinals/brc20/brc20-tokens.query.ts | 10 +- .../query/common/alex-sdk/alex-sdk.hooks.ts | 35 +++-- src/app/query/models/crypto-asset.model.ts | 59 ++++++++ ...ance.hooks.ts => account-balance.hooks.ts} | 28 ++-- ...ance.query.ts => account-balance.query.ts} | 0 .../balance/stacks-ft-balances.hooks.ts | 131 ------------------ .../balance/stacks-ft-balances.utils.ts | 102 -------------- src/app/query/stacks/bns/bns.hooks.ts | 4 +- src/app/query/stacks/mempool/mempool.hooks.ts | 62 +++------ src/app/query/stacks/mempool/mempool.utils.ts | 62 +++++++++ .../stacks/nonce/account-nonces.query.ts | 4 +- .../query/stacks/sip10/sip10-tokens.hooks.ts | 124 +++++++++++++++++ .../query/stacks/sip10/sip10-tokens.spec.ts | 32 +++++ .../query/stacks/sip10/sip10-tokens.utils.ts | 22 +++ .../stacks/stx/stx-crypto-asset.hooks.ts | 58 ++++++++ .../query/stacks/stx20/stx20-tokens.hooks.ts | 4 +- .../fungible-token-metadata.hooks.ts | 12 ++ .../fungible-token-metadata.query.ts | 0 .../non-fungible-token-holdings.query.ts | 0 .../non-fungible-token-metadata.hooks.ts | 0 .../non-fungible-token-metadata.query.ts | 0 .../token-metadata.utils.ts | 0 .../transactions-with-transfers.query.ts | 4 +- .../blockchain/bitcoin/bitcoin.ledger.ts | 2 + .../blockchain/stacks/stacks-account.hooks.ts | 5 +- src/app/store/accounts/blockchain/utils.ts | 10 +- .../transactions/post-conditions.hooks.ts | 8 +- .../transactions/token-transfer.hooks.ts | 80 ++++------- .../store/transactions/transaction.hooks.ts | 4 +- src/shared/models/account.model.ts | 16 --- .../models/crypto-asset-balance.model.ts | 46 ------ src/shared/models/crypto-asset.model.ts | 37 ----- tests/selectors/home.selectors.ts | 2 +- .../rpc-get-addresses/get-addresses.spec.ts | 1 - 127 files changed, 1323 insertions(+), 1604 deletions(-) delete mode 100644 src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts delete mode 100644 src/app/common/crypto-assets/stacks-crypto-asset.utils.ts delete mode 100644 src/app/common/hooks/balance/stx/use-stx-crypto-currency-asset-balance.ts delete mode 100644 src/app/common/hooks/use-transferable-asset-balances.hooks.ts delete mode 100644 src/app/components/account/bitcoin-account-loader.tsx rename src/app/components/{crypto-assets => }/crypto-asset-item/crypto-asset-item.layout.tsx (70%) create mode 100644 src/app/components/crypto-asset-item/crypto-asset-item.layout.utils.ts delete mode 100644 src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx delete mode 100644 src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx delete mode 100644 src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils.ts delete mode 100644 src/app/components/crypto-assets/stacks/fungible-token-asset/fungible-token-asset.utils.ts delete mode 100644 src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx delete mode 100644 src/app/components/loaders/btc-balance-loader.tsx create mode 100644 src/app/components/loaders/btc-crypto-asset-loader.tsx create mode 100644 src/app/components/loaders/current-bitcoin-signer-loader.tsx create mode 100644 src/app/components/loaders/stx-crypto-asset-loader.tsx rename src/app/components/{crypto-assets/stacks/components => }/stacks-asset-avatar.tsx (100%) create mode 100644 src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx create mode 100644 src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item-fallback.tsx rename src/app/features/asset-list/{components/btc-balance-list-item.tsx => bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx} (54%) rename src/app/{components/crypto-assets => features/asset-list}/bitcoin/runes-asset-list/runes-asset-item.layout.tsx (100%) rename src/app/{components/crypto-assets => features/asset-list}/bitcoin/runes-asset-list/runes-asset-list.tsx (100%) rename src/app/{components/crypto-assets => features/asset-list}/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx (100%) rename src/app/{components/crypto-assets => features/asset-list}/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx (100%) delete mode 100644 src/app/features/asset-list/components/add-stacks-ledger-keys-item.tsx delete mode 100644 src/app/features/asset-list/components/stacks-fungible-token-asset-list.layout.tsx delete mode 100644 src/app/features/asset-list/components/stacks-fungible-token-asset-list.tsx delete mode 100644 src/app/features/asset-list/components/stx-balance-list-item.layout.stories.tsx delete mode 100644 src/app/features/asset-list/components/stx-balance-list-item.layout.tsx create mode 100644 src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx create mode 100644 src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.utils.ts rename src/app/features/asset-list/{components/stacks-unsupported-token-asset-list.tsx => stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx} (56%) create mode 100644 src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx create mode 100644 src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item-fallback.tsx create mode 100644 src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx rename src/app/features/asset-list/{components/stx-balance-list-item.tsx => stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx} (63%) rename src/app/{components/crypto-assets => features/asset-list}/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx (100%) rename src/app/{components/crypto-assets => features/asset-list}/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx (100%) delete mode 100644 src/app/features/selectable-asset-list/crypto-asset-list-item.tsx delete mode 100644 src/app/features/selectable-asset-list/crypto-asset-list.tsx delete mode 100644 src/app/features/selectable-asset-list/crypto-currency-asset-icon.tsx delete mode 100644 src/app/features/selectable-asset-list/fungible-token-asset-item.tsx create mode 100644 src/app/pages/home/components/assets.tsx rename src/app/{features/selectable-asset-list => pages/send/choose-crypto-asset}/send-btc-disabled.tsx (100%) rename src/app/pages/send/send-crypto-asset-form/form/{brc-20/brc-20-choose-fee.tsx => brc20/brc20-choose-fee.tsx} (100%) rename src/app/pages/send/send-crypto-asset-form/form/{brc-20 => brc20}/brc20-send-form-confirmation.tsx (100%) rename src/app/pages/send/send-crypto-asset-form/form/{brc-20 => brc20}/brc20-send-form.tsx (100%) rename src/app/pages/send/send-crypto-asset-form/form/{brc-20 => brc20}/use-brc20-send-form.tsx (100%) rename src/app/pages/send/send-crypto-asset-form/form/{stacks-sip10 => sip10}/sip10-token-send-form-container.tsx (83%) rename src/app/pages/send/send-crypto-asset-form/form/{stacks-sip10 => sip10}/sip10-token-send-form.tsx (52%) rename src/app/pages/send/send-crypto-asset-form/form/{stacks-sip10 => sip10}/use-sip10-send-form.tsx (64%) create mode 100644 src/app/query/bitcoin/btc/btc-crypto-asset.hooks.ts create mode 100644 src/app/query/models/crypto-asset.model.ts rename src/app/query/stacks/balance/{stx-balance.hooks.ts => account-balance.hooks.ts} (67%) rename src/app/query/stacks/balance/{stx-balance.query.ts => account-balance.query.ts} (100%) delete mode 100644 src/app/query/stacks/balance/stacks-ft-balances.hooks.ts delete mode 100644 src/app/query/stacks/balance/stacks-ft-balances.utils.ts create mode 100644 src/app/query/stacks/mempool/mempool.utils.ts create mode 100644 src/app/query/stacks/sip10/sip10-tokens.hooks.ts create mode 100644 src/app/query/stacks/sip10/sip10-tokens.spec.ts create mode 100644 src/app/query/stacks/sip10/sip10-tokens.utils.ts create mode 100644 src/app/query/stacks/stx/stx-crypto-asset.hooks.ts create mode 100644 src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts rename src/app/query/stacks/{tokens => token-metadata}/fungible-tokens/fungible-token-metadata.query.ts (100%) rename src/app/query/stacks/{tokens => token-metadata}/non-fungible-tokens/non-fungible-token-holdings.query.ts (100%) rename src/app/query/stacks/{tokens => token-metadata}/non-fungible-tokens/non-fungible-token-metadata.hooks.ts (100%) rename src/app/query/stacks/{tokens => token-metadata}/non-fungible-tokens/non-fungible-token-metadata.query.ts (100%) rename src/app/query/stacks/{tokens => token-metadata}/token-metadata.utils.ts (100%) delete mode 100644 src/shared/models/crypto-asset-balance.model.ts delete mode 100644 src/shared/models/crypto-asset.model.ts diff --git a/package.json b/package.json index 005499c7139..54e47a4f896 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "@dlc-link/dlc-tools": "1.1.1", "@fungible-systems/zone-file": "2.0.0", "@hirosystems/token-metadata-api-client": "1.2.0", - "@leather-wallet/models": "0.4.0", + "@leather-wallet/models": "0.4.4", "@leather-wallet/tokens": "0.0.14", "@ledgerhq/hw-transport-webusb": "6.27.19", "@noble/hashes": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cb84a9116d..0935a38655b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ dependencies: specifier: 1.2.0 version: 1.2.0 '@leather-wallet/models': - specifier: 0.4.0 - version: 0.4.0 + specifier: 0.4.4 + version: 0.4.4 '@leather-wallet/tokens': specifier: 0.0.14 version: 0.0.14 @@ -3700,6 +3700,13 @@ packages: resolution: {integrity: sha512-6uwhdJxEhIpCI/29TPQIXAzfcz4niVIIujkGuwRFSyUiT/E8IEvjM30A3p9pMtEPCzO2UnpVERGLiLSIk48fgg==} dependencies: bignumber.js: 9.1.2 + dev: true + + /@leather-wallet/models@0.4.4: + resolution: {integrity: sha512-dqUTUR0EMM9WFwkeWRahlpixK+Ct2vEpDBhQdvO83tK4GhC/JlopI9hmpaSKyX9+35Ts2LpgycUiCh59kmZbMA==} + dependencies: + bignumber.js: 9.1.2 + dev: false /@leather-wallet/prettier-config@0.0.5: resolution: {integrity: sha512-cK44e4XqYghcbd5ow8AKzK0NyNQ0r0V/PGVm940PN17tDxZ7JykBcEmSI1iGRkCnTCy/RE/qHkgGBW5gYXhM7g==} diff --git a/src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts b/src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts deleted file mode 100644 index d8812ce8e34..00000000000 --- a/src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { StacksFungibleTokenAsset } from '@shared/models/crypto-asset.model'; - -import { - isFtNameLikeStx, - isTransferableStacksFungibleTokenAsset, -} from './stacks-crypto-asset.utils'; - -describe(isFtNameLikeStx.name, () => { - it('detect impersonating token names', () => { - expect(isFtNameLikeStx('STX')).toBeTruthy(); - expect(isFtNameLikeStx('stx')).toBeTruthy(); - expect(isFtNameLikeStx('stacks')).toBeTruthy(); - expect(isFtNameLikeStx('Stäcks')).toBeTruthy(); - expect(isFtNameLikeStx('Stácks')).toBeTruthy(); - expect(isFtNameLikeStx('Stáçks')).toBeTruthy(); - expect(isFtNameLikeStx('stocks')).toBeFalsy(); - expect(isFtNameLikeStx('miamicoin')).toBeFalsy(); - expect(isFtNameLikeStx('')).toBeFalsy(); - }); -}); - -describe(isTransferableStacksFungibleTokenAsset.name, () => { - test('assets with a name, symbol and decimals are allowed to be transferred', () => { - const asset: StacksFungibleTokenAsset = { - contractId: '', - contractAddress: 'ST6G7N19FKNW24XH5JQ5P5WR1DN10QWMKQSPSTK7', - contractAssetName: 'stella-token', - contractName: 'stella-the-cat', - decimals: 9, - name: 'SteLLa the Cat', - canTransfer: true, - hasMemo: true, - imageCanonicalUri: '', - marketData: null, - symbol: 'CAT', - }; - expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); - }); - - test('a token with no decimals is transferable', () => { - const asset: StacksFungibleTokenAsset = { - contractId: '', - contractAddress: 'ST6G7N19FKNW24XH5JQ5P5WR1DN10QWMKQSPSTK7', - contractAssetName: 'stella-token', - contractName: 'stella-the-cat', - decimals: 0, - name: 'SteLLa the Cat', - canTransfer: true, - hasMemo: true, - imageCanonicalUri: '', - marketData: null, - symbol: 'CAT', - }; - expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); - }); - - test('assets missing either name, symbol or decimals may not be transferred', () => { - const asset = { - name: 'Test token', - symbol: 'TEST', - decimals: undefined, - type: 'fungible-token', - } as unknown as StacksFungibleTokenAsset; - expect(isTransferableStacksFungibleTokenAsset(asset)).toBeFalsy(); - }); - - test('NFTs cannot be sent', () => { - const asset = { type: 'non-fungible-token' } as unknown as StacksFungibleTokenAsset; - - expect(isTransferableStacksFungibleTokenAsset(asset)).toBeFalsy(); - }); -}); diff --git a/src/app/common/crypto-assets/stacks-crypto-asset.utils.ts b/src/app/common/crypto-assets/stacks-crypto-asset.utils.ts deleted file mode 100644 index 88ca3269097..00000000000 --- a/src/app/common/crypto-assets/stacks-crypto-asset.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { StacksFungibleTokenAsset } from '@shared/models/crypto-asset.model'; -import { isUndefined } from '@shared/utils'; -import { isValidUrl } from '@shared/utils/validate-url'; - -import { convertUnicodeToAscii } from '../string-utils'; - -export function isFtNameLikeStx(name: string) { - return ['stx', 'stack', 'stacks'].includes(convertUnicodeToAscii(name).toLocaleLowerCase()); -} - -export function getImageCanonicalUri(imageCanonicalUri: string, name: string) { - return imageCanonicalUri && isValidUrl(imageCanonicalUri) && !isFtNameLikeStx(name) - ? imageCanonicalUri - : ''; -} - -export function isTransferableStacksFungibleTokenAsset(asset: StacksFungibleTokenAsset) { - return !isUndefined(asset.decimals) && !isUndefined(asset.name) && !isUndefined(asset.symbol); -} diff --git a/src/app/common/hooks/balance/stx/use-stx-crypto-currency-asset-balance.ts b/src/app/common/hooks/balance/stx/use-stx-crypto-currency-asset-balance.ts deleted file mode 100644 index cb9e60203d4..00000000000 --- a/src/app/common/hooks/balance/stx/use-stx-crypto-currency-asset-balance.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createStacksCryptoCurrencyAssetTypeWrapper } from '@app/query/stacks/balance/stacks-ft-balances.utils'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; - -// TODO: Asset refactor: remove wrapper here -export function useStxCryptoCurrencyAssetBalance() { - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); - return createStacksCryptoCurrencyAssetTypeWrapper(availableUnlockedBalance.amount); -} diff --git a/src/app/common/hooks/balance/use-total-balance.tsx b/src/app/common/hooks/balance/use-total-balance.tsx index 043bff0b394..2126e3046a0 100644 --- a/src/app/common/hooks/balance/use-total-balance.tsx +++ b/src/app/common/hooks/balance/use-total-balance.tsx @@ -6,7 +6,7 @@ import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { i18nFormatCurrency } from '@app/common/money/format-money'; import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; -import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/account-balance.hooks'; interface UseTotalBalanceArgs { btcAddress: string; diff --git a/src/app/common/hooks/use-transferable-asset-balances.hooks.ts b/src/app/common/hooks/use-transferable-asset-balances.hooks.ts deleted file mode 100644 index 8f43c43ded2..00000000000 --- a/src/app/common/hooks/use-transferable-asset-balances.hooks.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo } from 'react'; - -import type { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; - -import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; -import { createStacksCryptoCurrencyAssetTypeWrapper } from '@app/query/stacks/balance/stacks-ft-balances.utils'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; - -// TODO: Asset refactor: remove wrapper here -export function useAllTransferableCryptoAssetBalances(): AllTransferableCryptoAssetBalances[] { - const account = useCurrentStacksAccount(); - - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); - const stxCryptoCurrencyAssetBalance = createStacksCryptoCurrencyAssetTypeWrapper( - availableUnlockedBalance.amount - ); - const stacksFtAssetBalances = useTransferableStacksFungibleTokenAssetBalances( - account?.address ?? '' - ); - - return useMemo(() => { - return [stxCryptoCurrencyAssetBalance, ...stacksFtAssetBalances]; - }, [stacksFtAssetBalances, stxCryptoCurrencyAssetBalance]); -} diff --git a/src/app/common/stacks-utils.spec.ts b/src/app/common/stacks-utils.spec.ts index e9def73faef..17767b47f13 100644 --- a/src/app/common/stacks-utils.spec.ts +++ b/src/app/common/stacks-utils.spec.ts @@ -1,4 +1,4 @@ -import { stacksValue } from '@app/common/stacks-utils'; +import { isFtNameLikeStx, stacksValue } from '@app/common/stacks-utils'; const uSTX_AMOUNT = 10000480064; // 10,000.480064 @@ -31,3 +31,17 @@ describe('stacksValue tests', () => { expect(value).toEqual('10K STX'); }); }); + +describe(isFtNameLikeStx.name, () => { + it('detect impersonating token names', () => { + expect(isFtNameLikeStx('STX')).toBeTruthy(); + expect(isFtNameLikeStx('stx')).toBeTruthy(); + expect(isFtNameLikeStx('stacks')).toBeTruthy(); + expect(isFtNameLikeStx('Stäcks')).toBeTruthy(); + expect(isFtNameLikeStx('Stácks')).toBeTruthy(); + expect(isFtNameLikeStx('Stáçks')).toBeTruthy(); + expect(isFtNameLikeStx('stocks')).toBeFalsy(); + expect(isFtNameLikeStx('miamicoin')).toBeFalsy(); + expect(isFtNameLikeStx('')).toBeFalsy(); + }); +}); diff --git a/src/app/common/stacks-utils.ts b/src/app/common/stacks-utils.ts index 141d49d35e0..9d4b9ab2004 100644 --- a/src/app/common/stacks-utils.ts +++ b/src/app/common/stacks-utils.ts @@ -3,11 +3,13 @@ import BigNumber from 'bignumber.js'; import { c32addressDecode } from 'c32check'; import { NetworkConfiguration, STX_DECIMALS } from '@shared/constants'; +import { isValidUrl } from '@shared/utils/validate-url'; import { abbreviateNumber } from '@app/common/utils'; import { initBigNumber } from './math/helpers'; import { microStxToStx } from './money/unit-conversion'; +import { convertUnicodeToAscii } from './string-utils'; export const stacksValue = ({ value, @@ -64,3 +66,13 @@ export function validateAddressChain(address: string, currentNetwork: NetworkCon return false; } } + +export function isFtNameLikeStx(name: string) { + return ['stx', 'stack', 'stacks'].includes(convertUnicodeToAscii(name).toLocaleLowerCase()); +} + +export function getSafeImageCanonicalUri(imageCanonicalUri: string, name: string) { + return imageCanonicalUri && isValidUrl(imageCanonicalUri) && !isFtNameLikeStx(name) + ? imageCanonicalUri + : ''; +} diff --git a/src/app/components/account/account-addresses.tsx b/src/app/components/account/account-addresses.tsx index f8feff13b97..28eb7164ab6 100644 --- a/src/app/components/account/account-addresses.tsx +++ b/src/app/components/account/account-addresses.tsx @@ -4,8 +4,8 @@ import { BulletSeparator } from '@app/ui/components/bullet-separator/bullet-sepa import { Caption } from '@app/ui/components/typography/caption'; import { truncateMiddle } from '@app/ui/utils/truncate-middle'; +import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader'; import { StacksAccountLoader } from '../loaders/stacks-account-loader'; -import { BitcoinNativeSegwitAccountLoader } from './bitcoin-account-loader'; interface AccountAddressesProps { index: number; diff --git a/src/app/components/account/bitcoin-account-loader.tsx b/src/app/components/account/bitcoin-account-loader.tsx deleted file mode 100644 index 18774dc3680..00000000000 --- a/src/app/components/account/bitcoin-account-loader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { P2Ret } from '@scure/btc-signer'; - -import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; -import { useCurrentAccountIndex } from '@app/store/accounts/account'; -import { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; -import { useNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { useTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; -import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; - -interface BitcoinAccountLoaderBaseProps { - children(account: Signer): React.ReactNode; -} -interface BtcAccountLoaderCurrentProps extends BitcoinAccountLoaderBaseProps { - current: true; -} -interface BtcAccountLoaderIndexProps extends BitcoinAccountLoaderBaseProps { - index: number; -} - -type BtcAccountLoaderProps = BtcAccountLoaderCurrentProps | BtcAccountLoaderIndexProps; - -export function BitcoinNativeSegwitAccountLoader({ children, ...props }: BtcAccountLoaderProps) { - const isBitcoinEnabled = useConfigBitcoinEnabled(); - - const currentAccountIndex = useCurrentAccountIndex(); - - const properIndex = 'current' in props ? currentAccountIndex : props.index; - - const signer = useNativeSegwitSigner(properIndex); - - if (!signer || !isBitcoinEnabled) return null; - return children(signer(0)); -} - -export function BitcoinTaprootAccountLoader({ children, ...props }: BtcAccountLoaderProps) { - const isBitcoinEnabled = useConfigBitcoinEnabled(); - const network = useCurrentNetwork(); - - const currentAccountIndex = useCurrentAccountIndex(); - - const properIndex = 'current' in props ? currentAccountIndex : props.index; - - const signer = useTaprootSigner(properIndex, network.chain.bitcoin.bitcoinNetwork); - - if (!signer || !isBitcoinEnabled) return null; - return children(signer(0)); -} diff --git a/src/app/components/balance/btc-balance.tsx b/src/app/components/balance/btc-balance.tsx index 43feb74cb75..a35c5af4ec3 100644 --- a/src/app/components/balance/btc-balance.tsx +++ b/src/app/components/balance/btc-balance.tsx @@ -1,16 +1,16 @@ import { formatMoney } from '@app/common/money/format-money'; import { Caption } from '@app/ui/components/typography/caption'; -import { BitcoinNativeSegwitAccountLoader } from '../account/bitcoin-account-loader'; -import { BtcBalanceLoader } from '../loaders/btc-balance-loader'; +import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader'; +import { BtcCryptoAssetLoader } from '../loaders/btc-crypto-asset-loader'; export function BtcBalance() { return ( {signer => ( - - {balance => {formatMoney(balance.availableBalance)}} - + + {asset => {formatMoney(asset.balance.availableBalance)}} + )} ); diff --git a/src/app/components/balance/stx-balance.tsx b/src/app/components/balance/stx-balance.tsx index afde5293dc6..9ea9e4e826d 100644 --- a/src/app/components/balance/stx-balance.tsx +++ b/src/app/components/balance/stx-balance.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { stacksValue } from '@app/common/stacks-utils'; -import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { Caption } from '@app/ui/components/typography/caption'; interface StxBalanceProps { diff --git a/src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout.tsx b/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx similarity index 70% rename from src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout.tsx rename to src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx index 53a2ab24b50..efe8e0bd245 100644 --- a/src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout.tsx +++ b/src/app/components/crypto-asset-item/crypto-asset-item.layout.tsx @@ -1,10 +1,9 @@ import { ReactNode } from 'react'; -import type { CryptoAssetBalances } from '@leather-wallet/models'; import { Box, Flex, styled } from 'leather-styles/jsx'; -import { capitalize } from '@app/common/utils'; import { spamFilter } from '@app/common/utils/spam-filter'; +import type { AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; import { BulletSeparator } from '@app/ui/components/bullet-separator/bullet-separator'; import { ItemLayout } from '@app/ui/components/item-layout/item-layout'; import { SkeletonLoader } from '@app/ui/components/skeleton-loader/skeleton-loader'; @@ -12,34 +11,35 @@ import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; import { Caption } from '@app/ui/components/typography/caption'; import { Pressable } from '@app/ui/pressable/pressable'; -import { parseCryptoAssetBalance } from './crypto-asset-item.utils'; +import { parseCryptoAssetBalance } from './crypto-asset-item.layout.utils'; interface CryptoAssetItemLayoutProps { additionalBalanceInfo?: ReactNode; additionalBalanceInfoAsFiat?: ReactNode; - address?: string; - assetBalance: CryptoAssetBalances; - balanceAsFiat?: string; + asset: AccountCryptoAssetWithDetails; + caption?: string; + fiatBalance?: string; icon: React.ReactNode; isLoading?: boolean; name: string; - onClick?(): void; + onClick?(asset: AccountCryptoAssetWithDetails): void; rightElement?: React.ReactNode; } export function CryptoAssetItemLayout({ additionalBalanceInfo, additionalBalanceInfoAsFiat, - address = '', - assetBalance, - balanceAsFiat, + asset, + caption, + fiatBalance, icon, isLoading = false, name, onClick, rightElement, }: CryptoAssetItemLayoutProps) { - const { balance, dataTestId, formattedBalance } = parseCryptoAssetBalance(assetBalance); - const title = spamFilter(capitalize(name)); + const { dataTestId, formattedBalance } = parseCryptoAssetBalance(asset.balance); + const { availableBalance } = asset.balance; + const title = spamFilter(name); const titleRight = ( @@ -48,9 +48,7 @@ export function CryptoAssetItemLayout({ ) : ( @@ -67,9 +65,7 @@ export function CryptoAssetItemLayout({ - - {balance.availableBalance.amount.toNumber() > 0 && address ? balanceAsFiat : null} - + {availableBalance.amount.toNumber() > 0 ? fiatBalance : null} {additionalBalanceInfoAsFiat} @@ -84,7 +80,7 @@ export function CryptoAssetItemLayout({ @@ -92,7 +88,7 @@ export function CryptoAssetItemLayout({ if (isInteractive) return ( - + onClick(asset)} my="space.02"> {content} ); diff --git a/src/app/components/crypto-asset-item/crypto-asset-item.layout.utils.ts b/src/app/components/crypto-asset-item/crypto-asset-item.layout.utils.ts new file mode 100644 index 00000000000..6cc1c622b67 --- /dev/null +++ b/src/app/components/crypto-asset-item/crypto-asset-item.layout.utils.ts @@ -0,0 +1,21 @@ +import type { CryptoAssetBalance } from '@leather-wallet/models'; +import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; + +import { formatBalance } from '@app/common/format-balance'; +import { ftDecimals } from '@app/common/stacks-utils'; + +export function parseCryptoAssetBalance(balance: CryptoAssetBalance) { + const { availableBalance } = balance; + + const amount = ftDecimals(availableBalance.amount, availableBalance.decimals); + const dataTestId = CryptoAssetSelectors.CryptoAssetListItem.replace( + '{symbol}', + availableBalance.symbol.toLowerCase() + ); + const formattedBalance = formatBalance(amount); + + return { + dataTestId, + formattedBalance, + }; +} diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx deleted file mode 100644 index e83635a03c3..00000000000 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import BigNumber from 'bignumber.js'; -import { styled } from 'leather-styles/jsx'; - -import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; -import { formatBalance } from '@app/common/format-balance'; -import { convertAmountToBaseUnit } from '@app/common/money/calculate-money'; -import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; -import { Brc20AvatarIcon } from '@app/ui/components/avatar/brc20-avatar-icon'; -import { ItemLayout } from '@app/ui/components/item-layout/item-layout'; -import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; -import { Pressable } from '@app/ui/pressable/pressable'; - -interface Brc20TokenAssetItemLayoutProps { - token: Brc20Token; - onClick?(): void; -} -export function Brc20TokenAssetItemLayout({ onClick, token }: Brc20TokenAssetItemLayoutProps) { - const balanceAsString = convertAmountToBaseUnit(token.balance ?? new BigNumber(0)).toString(); - const formattedBalance = formatBalance(balanceAsString); - const balanceAsFiat = convertAssetBalanceToFiat(token); - - return ( - - } - titleLeft={token.tokenData.ticker} - captionLeft="BRC-20" - titleRight={ - - - {formattedBalance.value} - - - } - captionRight={balanceAsFiat} - /> - - ); -} diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx deleted file mode 100644 index ff4c6aa7fb1..00000000000 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useNavigate } from 'react-router-dom'; - -import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; -import { Stack } from 'leather-styles/jsx'; - -import { RouteUrls } from '@shared/route-urls'; - -import { useCurrentBtcAvailableBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; -import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; - -import { Brc20TokenAssetItemLayout } from './brc20-token-asset-item.layout'; - -interface Brc20TokenAssetListProps { - tokens: Brc20Token[]; - variant?: string; -} -export function Brc20TokenAssetList({ tokens, variant }: Brc20TokenAssetListProps) { - const navigate = useNavigate(); - const { balance } = useCurrentBtcAvailableBalanceNativeSegwit(); - - const hasPositiveBtcBalanceForFees = variant === 'send' && balance.amount.isGreaterThan(0); - - function navigateToBrc20SendForm(token: Brc20Token) { - const { balance, holderAddress, marketData, tokenData } = token; - navigate(RouteUrls.SendBrc20SendForm.replace(':ticker', tokenData.ticker), { - state: { balance, ticker: tokenData.ticker, holderAddress, marketData }, - }); - } - - return ( - - {tokens.map(token => ( - navigateToBrc20SendForm(token) : undefined} - /> - ))} - - ); -} diff --git a/src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils.ts b/src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils.ts deleted file mode 100644 index 955a07cb671..00000000000 --- a/src/app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { - BtcCryptoAssetInfo, - CryptoAssetBalances, - StxCryptoAssetInfo, -} from '@leather-wallet/models'; -import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; - -import { BTC_DECIMALS, STX_DECIMALS } from '@shared/constants'; - -import { formatBalance } from '@app/common/format-balance'; -import { ftDecimals } from '@app/common/stacks-utils'; - -export const btcCryptoAssetInfo: BtcCryptoAssetInfo = { - decimals: BTC_DECIMALS, - hasMemo: true, - name: 'bitcoin', - symbol: 'BTC', -}; - -export const stxCryptoAssetInfo: StxCryptoAssetInfo = { - decimals: STX_DECIMALS, - hasMemo: true, - name: 'stacks', - symbol: 'STX', -}; - -export function parseCryptoAssetBalance(balance: CryptoAssetBalances) { - const { availableBalance } = balance; - - const amount = availableBalance.decimals - ? ftDecimals(availableBalance.amount, availableBalance.decimals) - : availableBalance.amount.toString(); - const dataTestId = CryptoAssetSelectors.CryptoAssetListItem.replace( - '{symbol}', - availableBalance.symbol.toLowerCase() - ); - const formattedBalance = formatBalance(amount); - - return { - balance, - dataTestId, - formattedBalance, - }; -} diff --git a/src/app/components/crypto-assets/stacks/fungible-token-asset/fungible-token-asset.utils.ts b/src/app/components/crypto-assets/stacks/fungible-token-asset/fungible-token-asset.utils.ts deleted file mode 100644 index 78414d38f51..00000000000 --- a/src/app/components/crypto-assets/stacks/fungible-token-asset/fungible-token-asset.utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; - -import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; - -import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; -import { getImageCanonicalUri } from '@app/common/crypto-assets/stacks-crypto-asset.utils'; -import { formatBalance } from '@app/common/format-balance'; -import { ftDecimals } from '@app/common/stacks-utils'; -import { formatContractId, getTicker } from '@app/common/utils'; -import { spamFilter } from '@app/common/utils/spam-filter'; -import { getAssetName } from '@app/ui/utils/get-asset-name'; - -export function parseStacksFungibleTokenAssetBalance( - assetBalance: StacksFungibleTokenAssetBalance -) { - const { asset, balance } = assetBalance; - const { contractAddress, contractAssetName, contractName, name, symbol } = asset; - - const amount = balance.decimals - ? ftDecimals(balance.amount, balance.decimals || 0) - : balance.amount.toString(); - const avatar = `${formatContractId(contractAddress, contractName)}::${contractAssetName}`; - const dataTestId = - symbol && CryptoAssetSelectors.CryptoAssetListItem.replace('{symbol}', symbol.toLowerCase()); - const formattedBalance = formatBalance(amount); - const friendlyName = - name || - (contractAssetName.includes('::') ? getAssetName(contractAssetName) : contractAssetName); - const imageCanonicalUri = getImageCanonicalUri(asset.imageCanonicalUri, asset.name); - const caption = symbol || getTicker(friendlyName); - const title = spamFilter(friendlyName); - const balanceAsFiat = convertAssetBalanceToFiat({ - ...assetBalance.asset, - balance: assetBalance.balance, - }); - - return { - amount, - avatar, - balanceAsFiat, - caption, - dataTestId, - formattedBalance, - imageCanonicalUri, - title, - }; -} diff --git a/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx b/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx deleted file mode 100644 index cf66b4cced8..00000000000 --- a/src/app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { styled } from 'leather-styles/jsx'; - -import { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; - -import { StacksAssetAvatar } from '@app/components/crypto-assets/stacks/components/stacks-asset-avatar'; -import { ItemLayout } from '@app/ui/components/item-layout/item-layout'; -import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip'; -import { Pressable } from '@app/ui/pressable/pressable'; - -import { parseStacksFungibleTokenAssetBalance } from './fungible-token-asset.utils'; - -interface StacksFungibleTokenAssetItemLayoutProps { - assetBalance: StacksFungibleTokenAssetBalance; - onClick?(): void; -} -export function StacksFungibleTokenAssetItemLayout({ - assetBalance, - onClick, -}: StacksFungibleTokenAssetItemLayoutProps) { - const { - amount, - avatar, - balanceAsFiat, - caption, - dataTestId, - formattedBalance, - imageCanonicalUri, - title, - } = parseStacksFungibleTokenAssetBalance(assetBalance); - - return ( - - - {title[0]} - - } - titleLeft={title} - captionLeft={caption} - titleRight={ - - - {formattedBalance.value} - - - } - captionRight={balanceAsFiat} - /> - - ); -} diff --git a/src/app/components/loaders/bitcoin-account-loader.tsx b/src/app/components/loaders/bitcoin-account-loader.tsx index d97252f68a2..42fdf1e9de0 100644 --- a/src/app/components/loaders/bitcoin-account-loader.tsx +++ b/src/app/components/loaders/bitcoin-account-loader.tsx @@ -1,17 +1,52 @@ -import type { P2Ret, P2TROut } from '@scure/btc-signer'; +import { P2Ret } from '@scure/btc-signer'; -import { ZERO_INDEX } from '@shared/constants'; +import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; +import { useCurrentAccountIndex } from '@app/store/accounts/account'; +import { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; +import { useNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; -import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +interface BitcoinAccountLoaderBaseProps { + children(account: Signer): React.ReactNode; + fallback?: React.ReactNode; +} +interface BtcAccountLoaderCurrentProps extends BitcoinAccountLoaderBaseProps { + current: true; +} +interface BtcAccountLoaderIndexProps extends BitcoinAccountLoaderBaseProps { + index: number; +} + +type BtcAccountLoaderProps = BtcAccountLoaderCurrentProps | BtcAccountLoaderIndexProps; + +export function BitcoinNativeSegwitAccountLoader({ + children, + fallback, + ...props +}: BtcAccountLoaderProps) { + const isBitcoinEnabled = useConfigBitcoinEnabled(); + + const currentAccountIndex = useCurrentAccountIndex(); + + const properIndex = 'current' in props ? currentAccountIndex : props.index; -interface CurrentBitcoinSignerLoaderProps { - children(data: { nativeSegwit: Signer; taproot: Signer }): React.ReactNode; + const signer = useNativeSegwitSigner(properIndex); + + if (!signer || !isBitcoinEnabled) return fallback; + return children(signer(0)); } -export function CurrentBitcoinSignerLoader({ children }: CurrentBitcoinSignerLoaderProps) { - const nativeSegwit = useCurrentAccountNativeSegwitSigner()?.(ZERO_INDEX); - const taproot = useCurrentAccountTaprootSigner()?.(ZERO_INDEX); - if (!taproot || !nativeSegwit) return null; - return children({ nativeSegwit, taproot }); + +export function BitcoinTaprootAccountLoader({ children, ...props }: BtcAccountLoaderProps) { + const isBitcoinEnabled = useConfigBitcoinEnabled(); + const network = useCurrentNetwork(); + + const currentAccountIndex = useCurrentAccountIndex(); + + const properIndex = 'current' in props ? currentAccountIndex : props.index; + + const signer = useTaprootSigner(properIndex, network.chain.bitcoin.bitcoinNetwork); + + if (!signer || !isBitcoinEnabled) return null; + return children(signer(0)); } diff --git a/src/app/components/loaders/brc20-tokens-loader.tsx b/src/app/components/loaders/brc20-tokens-loader.tsx index cbc9af4a770..b7f152d2e61 100644 --- a/src/app/components/loaders/brc20-tokens-loader.tsx +++ b/src/app/components/loaders/brc20-tokens-loader.tsx @@ -1,10 +1,10 @@ -import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; -import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; +import { useBrc20AccountCryptoAssetsWithDetails } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; +import type { Brc20AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; interface Brc20TokensLoaderProps { - children(tokens: Brc20Token[]): React.ReactNode; + children(tokens: Brc20AccountCryptoAssetWithDetails[]): React.ReactNode; } export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) { - const tokens = useBrc20Tokens(); + const tokens = useBrc20AccountCryptoAssetsWithDetails(); return children(tokens); } diff --git a/src/app/components/loaders/btc-balance-loader.tsx b/src/app/components/loaders/btc-balance-loader.tsx deleted file mode 100644 index 09969971af1..00000000000 --- a/src/app/components/loaders/btc-balance-loader.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { BtcCryptoAssetBalance } from '@leather-wallet/models'; - -import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; - -interface BtcBalanceLoaderProps { - address: string; - children(balance: BtcCryptoAssetBalance, isInitialLoading: boolean): React.ReactNode; -} -export function BtcBalanceLoader({ address, children }: BtcBalanceLoaderProps) { - const { btcCryptoAssetBalance, isInitialLoading } = useBtcCryptoAssetBalanceNativeSegwit(address); - return children(btcCryptoAssetBalance, isInitialLoading); -} diff --git a/src/app/components/loaders/btc-crypto-asset-loader.tsx b/src/app/components/loaders/btc-crypto-asset-loader.tsx new file mode 100644 index 00000000000..3ed06ddb8bf --- /dev/null +++ b/src/app/components/loaders/btc-crypto-asset-loader.tsx @@ -0,0 +1,11 @@ +import { useBtcAccountCryptoAssetWithDetails } from '@app/query/bitcoin/btc/btc-crypto-asset.hooks'; +import type { BtcAccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; + +interface BtcCryptoAssetLoaderProps { + address: string; + children(asset: BtcAccountCryptoAssetWithDetails, isInitialLoading: boolean): React.ReactNode; +} +export function BtcCryptoAssetLoader({ address, children }: BtcCryptoAssetLoaderProps) { + const { asset, isInitialLoading } = useBtcAccountCryptoAssetWithDetails(address); + return children(asset, isInitialLoading); +} diff --git a/src/app/components/loaders/current-bitcoin-signer-loader.tsx b/src/app/components/loaders/current-bitcoin-signer-loader.tsx new file mode 100644 index 00000000000..d97252f68a2 --- /dev/null +++ b/src/app/components/loaders/current-bitcoin-signer-loader.tsx @@ -0,0 +1,17 @@ +import type { P2Ret, P2TROut } from '@scure/btc-signer'; + +import { ZERO_INDEX } from '@shared/constants'; + +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; +import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; + +interface CurrentBitcoinSignerLoaderProps { + children(data: { nativeSegwit: Signer; taproot: Signer }): React.ReactNode; +} +export function CurrentBitcoinSignerLoader({ children }: CurrentBitcoinSignerLoaderProps) { + const nativeSegwit = useCurrentAccountNativeSegwitSigner()?.(ZERO_INDEX); + const taproot = useCurrentAccountTaprootSigner()?.(ZERO_INDEX); + if (!taproot || !nativeSegwit) return null; + return children({ nativeSegwit, taproot }); +} diff --git a/src/app/components/loaders/stx-crypto-asset-loader.tsx b/src/app/components/loaders/stx-crypto-asset-loader.tsx new file mode 100644 index 00000000000..72824eb4770 --- /dev/null +++ b/src/app/components/loaders/stx-crypto-asset-loader.tsx @@ -0,0 +1,11 @@ +import type { StxAccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; +import { useStxAccountCryptoAssetWithDetails } from '@app/query/stacks/stx/stx-crypto-asset.hooks'; + +interface StxCryptoAssetLoaderProps { + address: string; + children(asset: StxAccountCryptoAssetWithDetails, isInitialLoading: boolean): React.ReactNode; +} +export function StxCryptoAssetLoader({ address, children }: StxCryptoAssetLoaderProps) { + const { asset, isInitialLoading } = useStxAccountCryptoAssetWithDetails(address); + return children(asset, isInitialLoading); +} diff --git a/src/app/components/crypto-assets/stacks/components/stacks-asset-avatar.tsx b/src/app/components/stacks-asset-avatar.tsx similarity index 100% rename from src/app/components/crypto-assets/stacks/components/stacks-asset-avatar.tsx rename to src/app/components/stacks-asset-avatar.tsx diff --git a/src/app/components/transaction/token-transfer-icon.tsx b/src/app/components/transaction/token-transfer-icon.tsx index 3785f97c668..bcb1375623b 100644 --- a/src/app/components/transaction/token-transfer-icon.tsx +++ b/src/app/components/transaction/token-transfer-icon.tsx @@ -1,12 +1,12 @@ import { StacksTx } from '@shared/models/transactions/stacks-transaction.model'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { ArrowDownIcon } from '@app/ui/icons/arrow-down-icon'; import { ArrowUpIcon } from '@app/ui/icons/arrow-up-icon'; export function TokenTransferIcon(props: { tx: StacksTx }) { const { tx } = props; - const currentAccountStxAddress = useCurrentAccountStxAddressState(); + const currentAccountStxAddress = useCurrentStacksAccountAddress(); const isSent = tx.sender_address === currentAccountStxAddress; if (isSent) return ; diff --git a/src/app/components/tx-asset-item.tsx b/src/app/components/tx-asset-item.tsx index dfc6602366b..13a06f2f713 100644 --- a/src/app/components/tx-asset-item.tsx +++ b/src/app/components/tx-asset-item.tsx @@ -2,7 +2,7 @@ import { HStack, HstackProps, styled } from 'leather-styles/jsx'; import { isValidUrl } from '@shared/utils/validate-url'; -import { StacksAssetAvatar } from '@app/components/crypto-assets/stacks/components/stacks-asset-avatar'; +import { StacksAssetAvatar } from '@app/components/stacks-asset-avatar'; interface TxAssetItemProps extends HstackProps { iconString: string; diff --git a/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx b/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx index 592017f8c93..e9413e82f4b 100644 --- a/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx +++ b/src/app/features/activity-list/components/transaction-list/stacks-transaction/ft-transfer-item.tsx @@ -6,16 +6,16 @@ import { TxTransferDetails, } from '@shared/models/transactions/stacks-transaction.model'; -import { getImageCanonicalUri } from '@app/common/crypto-assets/stacks-crypto-asset.utils'; +import { getSafeImageCanonicalUri } from '@app/common/stacks-utils'; import { calculateTokenTransferAmount, getTxCaption, } from '@app/common/transactions/stacks/transaction.utils'; import { pullContractIdFromIdentity } from '@app/common/utils'; -import { StacksAssetAvatar } from '@app/components/crypto-assets/stacks/components/stacks-asset-avatar'; +import { StacksAssetAvatar } from '@app/components/stacks-asset-avatar'; import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item'; -import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query'; -import { isFtAsset } from '@app/query/stacks/tokens/token-metadata.utils'; +import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.query'; +import { isFtAsset } from '@app/query/stacks/token-metadata/token-metadata.utils'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { ArrowDownIcon } from '@app/ui/icons/arrow-down-icon'; import { ArrowUpIcon } from '@app/ui/icons/arrow-up-icon'; @@ -48,7 +48,7 @@ export function FtTransferItem({ ftTransfer, parentTx }: FtTransferItemProps) { const ftImageCanonicalUri = assetMetadata.image_canonical_uri && assetMetadata.name && - getImageCanonicalUri(assetMetadata.image_canonical_uri, assetMetadata.name); + getSafeImageCanonicalUri(assetMetadata.image_canonical_uri, assetMetadata.name); const icon = isOriginator ? : ; const title = `${assetMetadata.name || 'Token'} Transfer`; const value = `${isOriginator ? '-' : ''}${displayAmount.toFormat()}`; diff --git a/src/app/features/asset-list/asset-list.tsx b/src/app/features/asset-list/asset-list.tsx index c526d7e2e2c..570f48e8161 100644 --- a/src/app/features/asset-list/asset-list.tsx +++ b/src/app/features/asset-list/asset-list.tsx @@ -1,94 +1,104 @@ -import { Outlet } from 'react-router-dom'; - -import { HomePageSelectors } from '@tests/selectors/home.selectors'; import { Stack } from 'leather-styles/jsx'; import { useWalletType } from '@app/common/use-wallet-type'; +import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point'; import { BitcoinNativeSegwitAccountLoader, BitcoinTaprootAccountLoader, -} from '@app/components/account/bitcoin-account-loader'; -import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point'; -import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list'; -import { RunesAssetList } from '@app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list'; -import { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list'; -import { Stx20TokenAssetList } from '@app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list'; +} from '@app/components/loaders/bitcoin-account-loader'; import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader'; -import { BtcBalanceLoader } from '@app/components/loaders/btc-balance-loader'; +import { BtcCryptoAssetLoader } from '@app/components/loaders/btc-crypto-asset-loader'; import { RunesLoader } from '@app/components/loaders/runes-loader'; import { Src20TokensLoader } from '@app/components/loaders/src20-tokens-loader'; import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader'; import { Stx20TokensLoader } from '@app/components/loaders/stx20-tokens-loader'; -import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger'; +import { StxCryptoAssetLoader } from '@app/components/loaders/stx-crypto-asset-loader'; +import { Brc20TokenAssetList } from '@app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list'; +import { RunesAssetList } from '@app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list'; +import { Src20TokenAssetList } from '@app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list'; +import { Stx20TokenAssetList } from '@app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list'; +import { StxCryptoAssetItem } from '@app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item'; +import { StxCryptoAssetItemFallback } from '@app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item-fallback'; +import type { AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { Collectibles } from '../collectibles/collectibles'; -import { PendingBrc20TransferList } from '../pending-brc-20-transfers/pending-brc-20-transfers'; -import { AddStacksLedgerKeysItem } from './components/add-stacks-ledger-keys-item'; -import { BtcBalanceListItem } from './components/btc-balance-list-item'; -import { ConnectLedgerAssetBtn } from './components/connect-ledger-asset-button'; -import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token-asset-list'; -import { StacksUnsupportedTokenAssetList } from './components/stacks-unsupported-token-asset-list'; -import { StxBalanceListItem } from './components/stx-balance-list-item'; +import { BtcCryptoAssetItem } from './bitcoin/btc-crypto-asset-item/btc-crypto-asset-item'; +import { BtcCryptoAssetItemFallback } from './bitcoin/btc-crypto-asset-item/btc-crypto-asset-item-fallback'; +import { Sip10TokenAssetList } from './stacks/sip10-token-asset-list/sip10-token-asset-list'; +import { Sip10TokenAssetListUnsupported } from './stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported'; -export function AssetList() { - const hasBitcoinLedgerKeys = useHasBitcoinLedgerKeychain(); +export type AssetListVariant = 'interactive' | 'read-only'; + +interface AssetListProps { + onClick?(asset: AccountCryptoAssetWithDetails): void; + variant?: AssetListVariant; +} +export function AssetList({ onClick, variant = 'read-only' }: AssetListProps) { const network = useCurrentNetwork(); const { whenWallet } = useWalletType(); + const isReadOnly = variant === 'read-only'; + return ( - + {whenWallet({ software: ( {nativeSegwitAccount => ( - - {(balance, isInitialLoading) => ( - + {(asset, isInitialLoading) => ( + )} - + )} ), ledger: ( - + } + > {nativeSegwitAccount => ( - - {(balance, isInitialLoading) => ( - + {(asset, isInitialLoading) => ( + - } + onClick={onClick} /> )} - + )} ), })} {/* Temporary duplication during Ledger Bitcoin feature dev */} - {['testnet', 'regtest'].includes(network.chain.bitcoin.bitcoinNetwork) && + {isReadOnly && + ['testnet', 'regtest'].includes(network.chain.bitcoin.bitcoinNetwork) && whenWallet({ software: , ledger: null, })} - }> + }> {account => ( <> - - - - {tokens => } - + + {(asset, isInitialLoading) => ( + + )} + + + {isReadOnly && ( + + {tokens => } + + )} )} @@ -99,28 +109,29 @@ export function AssetList() { {taprootAccount => ( <> - {tokens => } + {tokens => } - - {tokens => } - - - {runes => } - + {isReadOnly && ( + <> + + {tokens => } + + + {runes => } + + + )} )} )} - - {account => } - - - - - - + {isReadOnly && ( + + {account => } + + )} ); } diff --git a/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx new file mode 100644 index 00000000000..875048d35af --- /dev/null +++ b/src/app/features/asset-list/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx @@ -0,0 +1,47 @@ +import { useNavigate } from 'react-router-dom'; + +import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; +import { Stack } from 'leather-styles/jsx'; + +import { RouteUrls } from '@shared/route-urls'; + +import { capitalize } from '@app/common/utils'; +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; +import type { AssetListVariant } from '@app/features/asset-list/asset-list'; +import { useCurrentBtcAvailableBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; +import type { Brc20AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; +import { Brc20AvatarIcon } from '@app/ui/components/avatar/brc20-avatar-icon'; + +interface Brc20TokenAssetListProps { + assets: Brc20AccountCryptoAssetWithDetails[]; + variant?: AssetListVariant; +} +export function Brc20TokenAssetList({ assets, variant }: Brc20TokenAssetListProps) { + const navigate = useNavigate(); + const { balance, isInitialLoading } = useCurrentBtcAvailableBalanceNativeSegwit(); + + const hasPositiveBtcBalanceForFees = variant === 'interactive' && balance.amount.isGreaterThan(0); + + function navigateToBrc20SendForm(asset: Brc20AccountCryptoAssetWithDetails) { + const { balance, holderAddress, info, marketData } = asset; + navigate(RouteUrls.SendBrc20SendForm.replace(':ticker', info.symbol), { + state: { balance: balance.availableBalance, holderAddress, marketData, ticker: info.symbol }, + }); + } + + return ( + + {assets.map(asset => ( + } + isLoading={isInitialLoading} + key={asset.info.symbol} + name={asset.info.symbol} + onClick={hasPositiveBtcBalanceForFees ? () => navigateToBrc20SendForm(asset) : undefined} + /> + ))} + + ); +} diff --git a/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item-fallback.tsx b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item-fallback.tsx new file mode 100644 index 00000000000..f7e7c71e797 --- /dev/null +++ b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item-fallback.tsx @@ -0,0 +1,23 @@ +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; +import { btcCryptoAssetPlaceholder } from '@app/query/bitcoin/btc/btc-crypto-asset.hooks'; +import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; +import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; + +import type { AssetListVariant } from '../../asset-list'; +import { ConnectLedgerButton } from '../../components/connect-ledger-asset-button'; + +interface StxCryptoAssetItemFallbackProps { + variant: AssetListVariant; +} +export function BtcCryptoAssetItemFallback({ variant }: StxCryptoAssetItemFallbackProps) { + const checkBlockchainAvailable = useCheckLedgerBlockchainAvailable(); + if (variant === 'interactive' && !checkBlockchainAvailable('bitcoin')) return null; + return ( + } + name={btcCryptoAssetPlaceholder.info.name} + rightElement={} + /> + ); +} diff --git a/src/app/features/asset-list/components/btc-balance-list-item.tsx b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx similarity index 54% rename from src/app/features/asset-list/components/btc-balance-list-item.tsx rename to src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx index 0832542ef1f..1128f53945e 100644 --- a/src/app/features/asset-list/components/btc-balance-list-item.tsx +++ b/src/app/features/asset-list/bitcoin/btc-crypto-asset-item/btc-crypto-asset-item.tsx @@ -1,37 +1,39 @@ -import type { BtcCryptoAssetBalance } from '@leather-wallet/models'; - import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { i18nFormatCurrency } from '@app/common/money/format-money'; -import { CryptoAssetItemLayout } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { btcCryptoAssetInfo } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils'; +import { capitalize } from '@app/common/utils'; +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; +import type { + AccountCryptoAssetWithDetails, + BtcAccountCryptoAssetWithDetails, +} from '@app/query/models/crypto-asset.model'; import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; -interface BtcBalanceListItemProps { - address: string; - balance: BtcCryptoAssetBalance; +interface BtcCryptoAssetItemProps { + asset: BtcAccountCryptoAssetWithDetails; isLoading: boolean; + onClick?(asset: AccountCryptoAssetWithDetails): void; rightElement?: React.ReactNode; } -export function BtcBalanceListItem({ - address, - balance, +export function BtcCryptoAssetItem({ + asset, isLoading, + onClick, rightElement, -}: BtcBalanceListItemProps) { +}: BtcCryptoAssetItemProps) { const marketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); const availableBalanceAsFiat = i18nFormatCurrency( - baseCurrencyAmountInQuote(balance.availableBalance, marketData) + baseCurrencyAmountInQuote(asset.balance.availableBalance, marketData) ); return ( } isLoading={isLoading} - name={btcCryptoAssetInfo.name} + name={capitalize(asset.info.name)} + onClick={onClick} rightElement={rightElement} /> ); diff --git a/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx b/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-item.layout.tsx similarity index 100% rename from src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx rename to src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-item.layout.tsx diff --git a/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx b/src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx similarity index 100% rename from src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx rename to src/app/features/asset-list/bitcoin/runes-asset-list/runes-asset-list.tsx diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx b/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx similarity index 100% rename from src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx rename to src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx b/src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx similarity index 100% rename from src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx rename to src/app/features/asset-list/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx diff --git a/src/app/features/asset-list/components/add-stacks-ledger-keys-item.tsx b/src/app/features/asset-list/components/add-stacks-ledger-keys-item.tsx deleted file mode 100644 index e3540cb461e..00000000000 --- a/src/app/features/asset-list/components/add-stacks-ledger-keys-item.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import BigNumber from 'bignumber.js'; - -import { CryptoAssetItemLayout } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { createStacksCryptoCurrencyAssetTypeWrapper } from '@app/query/stacks/balance/stacks-ft-balances.utils'; -import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; - -import { ConnectLedgerAssetBtn } from './connect-ledger-asset-button'; - -export function AddStacksLedgerKeysItem() { - return ( - } - rightElement={} - /> - ); -} diff --git a/src/app/features/asset-list/components/connect-ledger-asset-button.tsx b/src/app/features/asset-list/components/connect-ledger-asset-button.tsx index bc08367fa05..9e596ba057a 100644 --- a/src/app/features/asset-list/components/connect-ledger-asset-button.tsx +++ b/src/app/features/asset-list/components/connect-ledger-asset-button.tsx @@ -1,8 +1,8 @@ import { useNavigate } from 'react-router-dom'; +import type { Blockchains } from '@leather-wallet/models'; import { styled } from 'leather-styles/jsx'; -import { SupportedBlockchains } from '@shared/constants'; import { RouteUrls } from '@shared/route-urls'; import { capitalize } from '@app/common/utils'; @@ -10,10 +10,10 @@ import { immediatelyAttemptLedgerConnection } from '@app/features/ledger/hooks/u import { Button } from '@app/ui/components/button/button'; import { LedgerIcon } from '@app/ui/icons/ledger-icon'; -interface ConnectLedgerAssetBtnProps { - chain: SupportedBlockchains; +interface ConnectLedgerButtonProps { + chain: Blockchains; } -export function ConnectLedgerAssetBtn({ chain }: ConnectLedgerAssetBtnProps) { +export function ConnectLedgerButton({ chain }: ConnectLedgerButtonProps) { const navigate = useNavigate(); const onClick = () => { diff --git a/src/app/features/asset-list/components/stacks-fungible-token-asset-list.layout.tsx b/src/app/features/asset-list/components/stacks-fungible-token-asset-list.layout.tsx deleted file mode 100644 index a13708304f5..00000000000 --- a/src/app/features/asset-list/components/stacks-fungible-token-asset-list.layout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Stack } from 'leather-styles/jsx'; - -import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; - -import { StacksFungibleTokenAssetItemLayout } from '@app/components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout'; - -interface StacksFungibleTokenAssetListLayoutProps { - assetBalances: StacksFungibleTokenAssetBalance[]; -} -export function StacksFungibleTokenAssetListLayout({ - assetBalances, -}: StacksFungibleTokenAssetListLayoutProps) { - if (assetBalances.length === 0) return null; - return ( - - {assetBalances.map(assetBalance => ( - - ))} - - ); -} diff --git a/src/app/features/asset-list/components/stacks-fungible-token-asset-list.tsx b/src/app/features/asset-list/components/stacks-fungible-token-asset-list.tsx deleted file mode 100644 index 99e82df34a2..00000000000 --- a/src/app/features/asset-list/components/stacks-fungible-token-asset-list.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useFilteredStacksFungibleTokenList } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; - -import { StacksFungibleTokenAssetListLayout } from './stacks-fungible-token-asset-list.layout'; - -interface StacksFungibleTokenAssetListProps { - address: string; -} -export function StacksFungibleTokenAssetList({ address }: StacksFungibleTokenAssetListProps) { - const stacksFilteredFtAssetBalances = useFilteredStacksFungibleTokenList({ - address, - filter: 'supported', - }); - - return ; -} diff --git a/src/app/features/asset-list/components/stx-balance-list-item.layout.stories.tsx b/src/app/features/asset-list/components/stx-balance-list-item.layout.stories.tsx deleted file mode 100644 index f7d19d348de..00000000000 --- a/src/app/features/asset-list/components/stx-balance-list-item.layout.stories.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { TooltipProvider } from '@radix-ui/react-tooltip'; -import { Meta, StoryObj } from '@storybook/react'; -import BigNumber from 'bignumber.js'; - -import { StacksBalanceListItemLayout } from './stx-balance-list-item.layout'; - -const meta: Meta = { - component: StacksBalanceListItemLayout, - tags: ['autodocs'], - title: 'Feature/StxBalanceListItem', - argTypes: {}, - parameters: {}, - decorators: [ - Story => ( - - - - ), - ], -}; - -export default meta; - -type Story = StoryObj; - -const symbol = 'STX'; - -export const StacksBalanceItem: Story = { - args: { - address: 'ST1PQHQKV0YX2K1Z0V2VQZGZGZGZGZGZGZGZGZGZG', - stxEffectiveBalance: { - balance: { decimals: 8, amount: new BigNumber(100000000000), symbol }, - blockchain: 'stacks', - type: 'crypto-currency', - asset: { - decimals: 8, - hasMemo: true, - name: 'Stacks', - symbol, - } as const, - }, - stxEffectiveUsdBalance: '$100,000', - }, -}; - -export const StacksBalanceItemWithLockedBalance: Story = { - args: { - address: 'ST1PQHQKV0YX2K1Z0V2VQZGZGZGZGZGZGZGZGZGZG', - stxEffectiveBalance: { - balance: { decimals: 8, amount: new BigNumber(100000000000), symbol }, - blockchain: 'stacks', - type: 'crypto-currency', - asset: { - decimals: 8, - hasMemo: true, - name: 'Stacks', - symbol, - } as const, - }, - stxEffectiveUsdBalance: '$100,000', - stxUsdLockedBalance: '$1,000', - stxLockedBalance: { decimals: 8, amount: new BigNumber(1000000000), symbol }, - }, -}; diff --git a/src/app/features/asset-list/components/stx-balance-list-item.layout.tsx b/src/app/features/asset-list/components/stx-balance-list-item.layout.tsx deleted file mode 100644 index c96f31df9fc..00000000000 --- a/src/app/features/asset-list/components/stx-balance-list-item.layout.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { styled } from 'leather-styles/jsx'; - -import type { StacksCryptoCurrencyAssetBalance } from '@shared/models/crypto-asset-balance.model'; -import type { Money } from '@shared/models/money.model'; - -import { ftDecimals } from '@app/common/stacks-utils'; -import { CryptoAssetItemLayout } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; -import { BulletOperator } from '@app/ui/components/bullet-separator/bullet-separator'; -import { Caption } from '@app/ui/components/typography/caption'; - -interface StacksBalanceListItemLayoutProps { - address: string; - stxEffectiveBalance: StacksCryptoCurrencyAssetBalance; - stxEffectiveUsdBalance?: string; - stxLockedBalance?: Money; - stxUsdLockedBalance?: string; - isInitialLoading?: boolean; -} -export function StacksBalanceListItemLayout(props: StacksBalanceListItemLayoutProps) { - const { - address, - stxEffectiveBalance, - stxEffectiveUsdBalance, - stxLockedBalance, - stxUsdLockedBalance, - isInitialLoading, - } = props; - - const stxAdditionalBalanceInfo = stxLockedBalance?.amount.isGreaterThan(0) ? ( - - - {ftDecimals(stxLockedBalance.amount, stxLockedBalance.decimals || 0)} locked - - ) : undefined; - - const stxAdditionalUsdBalanceInfo = stxLockedBalance?.amount.isGreaterThan(0) ? ( - {stxUsdLockedBalance} locked - ) : undefined; - - return ( - } - isLoading={isInitialLoading} - /> - ); -} diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx new file mode 100644 index 00000000000..e7b421e56ac --- /dev/null +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.tsx @@ -0,0 +1,36 @@ +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; +import { StacksAssetAvatar } from '@app/components/stacks-asset-avatar'; +import type { + AccountCryptoAssetWithDetails, + Sip10AccountCryptoAssetWithDetails, +} from '@app/query/models/crypto-asset.model'; + +import { parseSip10TokenCryptoAssetBalance } from './sip10-token-asset-item.utils'; + +interface Sip10TokenAssetItemProps { + asset: Sip10AccountCryptoAssetWithDetails; + onClick?(asset: AccountCryptoAssetWithDetails): void; +} +export function Sip10TokenAssetItem({ asset, onClick }: Sip10TokenAssetItemProps) { + const { avatar, fiatBalance, imageCanonicalUri, title } = + parseSip10TokenCryptoAssetBalance(asset); + + return ( + + {title[0]} + + } + name={asset.info.name} + onClick={onClick} + /> + ); +} diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.utils.ts b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.utils.ts new file mode 100644 index 00000000000..e1b09175109 --- /dev/null +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-item.utils.ts @@ -0,0 +1,34 @@ +import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; + +import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; +import { formatBalance } from '@app/common/format-balance'; +import { ftDecimals, getSafeImageCanonicalUri } from '@app/common/stacks-utils'; +import { spamFilter } from '@app/common/utils/spam-filter'; +import type { Sip10AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; + +export function parseSip10TokenCryptoAssetBalance(asset: Sip10AccountCryptoAssetWithDetails) { + const { balance, info } = asset; + const { contractId, decimals, imageCanonicalUri, name, symbol } = info; + + const amount = ftDecimals(balance.availableBalance.amount, decimals); + const avatar = contractId; + const dataTestId = + symbol && CryptoAssetSelectors.CryptoAssetListItem.replace('{symbol}', symbol.toLowerCase()); + const formattedBalance = formatBalance(amount); + const safeImageCanonicalUri = getSafeImageCanonicalUri(imageCanonicalUri, name); + const title = spamFilter(name); + const fiatBalance = convertAssetBalanceToFiat({ + ...asset, + balance: asset.balance.availableBalance, + }); + + return { + amount, + avatar, + fiatBalance, + dataTestId, + formattedBalance, + imageCanonicalUri: safeImageCanonicalUri, + title, + }; +} diff --git a/src/app/features/asset-list/components/stacks-unsupported-token-asset-list.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx similarity index 56% rename from src/app/features/asset-list/components/stacks-unsupported-token-asset-list.tsx rename to src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx index ce10286792a..72b52332198 100644 --- a/src/app/features/asset-list/components/stacks-unsupported-token-asset-list.tsx +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list-unsupported.tsx @@ -1,28 +1,26 @@ import { useState } from 'react'; -import { styled } from 'leather-styles/jsx'; +import { Stack, styled } from 'leather-styles/jsx'; -import { useFilteredStacksFungibleTokenList } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; +import { useFilteredSip10AccountCryptoAssetsWithDetails } from '@app/query/stacks/sip10/sip10-tokens.hooks'; import { Accordion } from '@app/ui/components/accordion/accordion'; -import { StacksFungibleTokenAssetListLayout } from './stacks-fungible-token-asset-list.layout'; +import { Sip10TokenAssetItem } from './sip10-token-asset-item'; const accordionValue = 'accordion-unsupported-token-asset-list'; -export function StacksUnsupportedTokenAssetList({ address }: { address: string }) { - const stacksFilteredFtAssetBalances = useFilteredStacksFungibleTokenList({ +export function Sip10TokenAssetListUnsupported({ address }: { address: string }) { + const [isOpen, setIsOpen] = useState(false); + const assets = useFilteredSip10AccountCryptoAssetsWithDetails({ address, filter: 'unsupported', }); - const [isOpen, setIsOpen] = useState(false); function onValueChange(value: string) { setIsOpen(value === accordionValue); } - if (stacksFilteredFtAssetBalances.length === 0) { - return null; - } + if (!assets.length) return null; return ( @@ -31,7 +29,11 @@ export function StacksUnsupportedTokenAssetList({ address }: { address: string } View {isOpen ? 'fewer' : 'more'} - + + {assets.map(asset => ( + + ))} + diff --git a/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx new file mode 100644 index 00000000000..28e2b49c71e --- /dev/null +++ b/src/app/features/asset-list/stacks/sip10-token-asset-list/sip10-token-asset-list.tsx @@ -0,0 +1,29 @@ +import { Stack } from 'leather-styles/jsx'; + +import { isDefined } from '@shared/utils'; + +import type { AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; +import { useFilteredSip10AccountCryptoAssetsWithDetails } from '@app/query/stacks/sip10/sip10-tokens.hooks'; + +import { Sip10TokenAssetItem } from './sip10-token-asset-item'; + +interface Sip10TokenAssetListProps { + address: string; + onClick?(asset: AccountCryptoAssetWithDetails): void; +} +export function Sip10TokenAssetList({ address, onClick }: Sip10TokenAssetListProps) { + const assets = useFilteredSip10AccountCryptoAssetsWithDetails({ + address, + filter: isDefined(onClick) ? 'all' : 'supported', + }); + + if (!assets.length) return null; + + return ( + + {assets.map(asset => ( + + ))} + + ); +} diff --git a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item-fallback.tsx b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item-fallback.tsx new file mode 100644 index 00000000000..1eb4c256dcd --- /dev/null +++ b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item-fallback.tsx @@ -0,0 +1,23 @@ +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; +import { stxCryptoAssetPlaceholder } from '@app/query/stacks/stx/stx-crypto-asset.hooks'; +import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; +import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; + +import type { AssetListVariant } from '../../asset-list'; +import { ConnectLedgerButton } from '../../components/connect-ledger-asset-button'; + +interface StxCryptoAssetItemFallbackProps { + variant: AssetListVariant; +} +export function StxCryptoAssetItemFallback({ variant }: StxCryptoAssetItemFallbackProps) { + const checkBlockchainAvailable = useCheckLedgerBlockchainAvailable(); + if (variant === 'interactive' && !checkBlockchainAvailable('stacks')) return null; + return ( + } + name={stxCryptoAssetPlaceholder.info.name} + rightElement={} + /> + ); +} diff --git a/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx new file mode 100644 index 00000000000..7fe04fcfecb --- /dev/null +++ b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.stories.tsx @@ -0,0 +1,81 @@ +import { TooltipProvider } from '@radix-ui/react-tooltip'; +import { Meta, StoryObj } from '@storybook/react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; + +import { queryClient } from '@app/common/persistence'; + +import { StxCryptoAssetItem as Component } from './stx-crypto-asset-item'; + +const meta: Meta = { + component: Component, + tags: ['autodocs'], + title: 'Feature/StxCryptoAssetItem', + argTypes: {}, + parameters: {}, + decorators: [ + Story => ( + + + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +const symbol = 'STX'; + +const stxCryptoAssetBalance = { + availableBalance: { amount: new BigNumber(100000000000), decimals: 6, symbol }, + availableUnlockedBalance: { amount: new BigNumber(100000000000), decimals: 6, symbol }, + inboundBalance: { amount: new BigNumber(0), decimals: 6, symbol }, + outboundBalance: { amount: new BigNumber(0), decimals: 6, symbol }, + pendingBalance: { amount: new BigNumber(0), decimals: 6, symbol }, + totalBalance: { amount: new BigNumber(0), decimals: 6, symbol }, + unlockedBalance: { amount: new BigNumber(0), decimals: 6, symbol }, +}; + +export const StxCryptoAssetItem: Story = { + args: { + asset: { + info: { + decimals: 6, + hasMemo: true, + name: 'stacks', + symbol: 'STX', + }, + balance: { + ...stxCryptoAssetBalance, + lockedBalance: { amount: new BigNumber(0), decimals: 6, symbol }, + }, + chain: 'stacks', + marketData: null, + type: 'stx', + }, + }, +}; + +export const StxCryptoAssetItemWithLockedBalance: Story = { + args: { + asset: { + info: { + decimals: 6, + hasMemo: true, + name: 'stacks', + symbol: 'STX', + }, + balance: { + ...stxCryptoAssetBalance, + lockedBalance: { amount: new BigNumber(1000000000), decimals: 6, symbol }, + }, + chain: 'stacks', + marketData: null, + type: 'stx', + }, + }, +}; diff --git a/src/app/features/asset-list/components/stx-balance-list-item.tsx b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx similarity index 63% rename from src/app/features/asset-list/components/stx-balance-list-item.tsx rename to src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx index afb80bd5f64..55092bab5f1 100644 --- a/src/app/features/asset-list/components/stx-balance-list-item.tsx +++ b/src/app/features/asset-list/stacks/stx-crypo-asset-item/stx-crypto-asset-item.tsx @@ -3,24 +3,26 @@ import { styled } from 'leather-styles/jsx'; import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; import { i18nFormatCurrency } from '@app/common/money/format-money'; import { ftDecimals } from '@app/common/stacks-utils'; -import { CryptoAssetItemLayout } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { stxCryptoAssetInfo } from '@app/components/crypto-assets/crypto-asset-item/crypto-asset-item.utils'; +import { capitalize } from '@app/common/utils'; +import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; -import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import type { + AccountCryptoAssetWithDetails, + StxAccountCryptoAssetWithDetails, +} from '@app/query/models/crypto-asset.model'; import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; import { BulletOperator } from '@app/ui/components/bullet-separator/bullet-separator'; import { Caption } from '@app/ui/components/typography/caption'; -interface StxBalanceListItemProps { - address: string; +interface StxCryptoAssetItemProps { + asset: StxAccountCryptoAssetWithDetails; + isLoading: boolean; + onClick?(asset: AccountCryptoAssetWithDetails): void; } -export function StxBalanceListItem({ address }: StxBalanceListItemProps) { +export function StxCryptoAssetItem({ asset, isLoading, onClick }: StxCryptoAssetItemProps) { const marketData = useCryptoCurrencyMarketDataMeanAverage('STX'); - const { data: stxCryptoAssetBalance, isInitialLoading } = useStxCryptoAssetBalance(address); - if (!stxCryptoAssetBalance) return null; - - const { availableBalance, lockedBalance } = stxCryptoAssetBalance; + const { availableBalance, lockedBalance } = asset.balance; const showAdditionalInfo = lockedBalance.amount.isGreaterThan(0); const lockedBalanceAsFiat = i18nFormatCurrency( @@ -32,21 +34,21 @@ export function StxBalanceListItem({ address }: StxBalanceListItemProps) { const additionalBalanceInfo = ( - {ftDecimals(lockedBalance.amount, lockedBalance.decimals || 0)} locked + {ftDecimals(lockedBalance.amount, lockedBalance.decimals)} locked ); const additionalBalanceInfoAsFiat = {lockedBalanceAsFiat} locked; return ( } - isLoading={isInitialLoading} - name={stxCryptoAssetInfo.name} + isLoading={isLoading} + name={capitalize(asset.info.name)} + onClick={onClick} /> ); } diff --git a/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx b/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx similarity index 100% rename from src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx rename to src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-item.layout.tsx diff --git a/src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx b/src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx similarity index 100% rename from src/app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx rename to src/app/features/asset-list/stacks/stx20-token-asset-list/stx20-token-asset-list.tsx diff --git a/src/app/features/collectibles/collectibles.tsx b/src/app/features/collectibles/collectibles.tsx index 4a134de63c1..e6a34ce1c2f 100644 --- a/src/app/features/collectibles/collectibles.tsx +++ b/src/app/features/collectibles/collectibles.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { RouteUrls } from '@shared/route-urls'; import { useWalletType } from '@app/common/use-wallet-type'; -import { CurrentBitcoinSignerLoader } from '@app/components/loaders/bitcoin-account-loader'; +import { CurrentBitcoinSignerLoader } from '@app/components/loaders/current-bitcoin-signer-loader'; import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader'; import { useConfigNftMetadataEnabled } from '@app/query/common/remote-config/remote-config.query'; diff --git a/src/app/features/collectibles/components/stacks/stacks-crypto-assets.tsx b/src/app/features/collectibles/components/stacks/stacks-crypto-assets.tsx index ffbb6691fe7..265871ee7d5 100644 --- a/src/app/features/collectibles/components/stacks/stacks-crypto-assets.tsx +++ b/src/app/features/collectibles/components/stacks/stacks-crypto-assets.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { parseIfValidPunycode } from '@app/common/utils'; import { useCurrentAccountNames } from '@app/query/stacks/bns/bns.hooks'; -import { useStacksNonFungibleTokensMetadata } from '@app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.hooks'; +import { useStacksNonFungibleTokensMetadata } from '@app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-metadata.hooks'; import { StacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.models'; import { StacksBnsName } from './stacks-bns-name'; diff --git a/src/app/features/dialogs/increase-fee-dialog/increase-stx-fee-dialog.tsx b/src/app/features/dialogs/increase-fee-dialog/increase-stx-fee-dialog.tsx index 7bb2cb0c810..3142219c944 100644 --- a/src/app/features/dialogs/increase-fee-dialog/increase-stx-fee-dialog.tsx +++ b/src/app/features/dialogs/increase-fee-dialog/increase-stx-fee-dialog.tsx @@ -18,7 +18,7 @@ import { LoadingSpinner } from '@app/components/loading-spinner'; import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item'; import { useStacksBroadcastTransaction } from '@app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction'; import { useToast } from '@app/features/toasts/use-toast'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useSubmittedTransactionsActions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; import { useRawDeserializedTxState, useRawTxIdState } from '@app/store/transactions/raw.hooks'; import { Dialog } from '@app/ui/components/containers/dialog/dialog'; @@ -42,7 +42,7 @@ export function IncreaseStxFeeDialog() { const refreshAccountData = useRefreshAllAccountData(); const tx = useSelectedTx(); const [, setTxId] = useRawTxIdState(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); const submittedTransactionsActions = useSubmittedTransactionsActions(); const rawTx = useRawDeserializedTxState(); const { stacksBroadcastTransaction } = useStacksBroadcastTransaction('STX'); diff --git a/src/app/features/selectable-asset-list/crypto-asset-list-item.tsx b/src/app/features/selectable-asset-list/crypto-asset-list-item.tsx deleted file mode 100644 index 733af8c68b5..00000000000 --- a/src/app/features/selectable-asset-list/crypto-asset-list-item.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; - -import { CryptoAssetItemLayout } from '../../components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { CryptoCurrencyAssetIcon } from './crypto-currency-asset-icon'; -import { FungibleTokenAssetItem } from './fungible-token-asset-item'; - -interface CryptoAssetListItemProps { - assetBalance: AllTransferableCryptoAssetBalances; - onClick(): void; -} -export function CryptoAssetListItem(props: CryptoAssetListItemProps) { - const { assetBalance, onClick } = props; - const { blockchain, type } = assetBalance; - - switch (type) { - case 'crypto-currency': - return ( - } - onClick={onClick} - /> - ); - case 'fungible-token': - return ; - default: - return null; - } -} diff --git a/src/app/features/selectable-asset-list/crypto-asset-list.tsx b/src/app/features/selectable-asset-list/crypto-asset-list.tsx deleted file mode 100644 index 5a2c4a9d3d0..00000000000 --- a/src/app/features/selectable-asset-list/crypto-asset-list.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; -import { Stack } from 'leather-styles/jsx'; - -import type { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; -import { StacksFungibleTokenAsset } from '@shared/models/crypto-asset.model'; - -import { useWalletType } from '@app/common/use-wallet-type'; -import { BitcoinNativeSegwitAccountLoader } from '@app/components/account/bitcoin-account-loader'; -import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list'; -import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader'; -import { BtcBalanceLoader } from '@app/components/loaders/btc-balance-loader'; -import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; - -import { CryptoAssetItemLayout } from '../../components/crypto-assets/crypto-asset-item/crypto-asset-item.layout'; -import { CryptoAssetListItem } from './crypto-asset-list-item'; - -interface CryptoAssetListProps { - cryptoAssetBalances: AllTransferableCryptoAssetBalances[]; - onItemClick(cryptoAssetBalance: AllTransferableCryptoAssetBalances): void; - variant: 'send' | 'fund'; -} -export function CryptoAssetList({ - cryptoAssetBalances, - onItemClick, - variant, -}: CryptoAssetListProps) { - const { whenWallet } = useWalletType(); - - return ( - - - {signer => ( - - {(balance, isLoading) => ( - } - onClick={() => onItemClick(balance)} - isLoading={isLoading} - /> - )} - - )} - - {cryptoAssetBalances.map(cryptoAssetBalance => ( - onItemClick(cryptoAssetBalance)} - assetBalance={cryptoAssetBalance} - key={ - cryptoAssetBalance.asset.name ?? - (cryptoAssetBalance.asset as StacksFungibleTokenAsset).contractAssetName - } - /> - ))} - {variant === 'send' && - whenWallet({ - software: ( - - {() => ( - - {brc20Tokens => } - - )} - - ), - ledger: null, - })} - - ); -} diff --git a/src/app/features/selectable-asset-list/crypto-currency-asset-icon.tsx b/src/app/features/selectable-asset-list/crypto-currency-asset-icon.tsx deleted file mode 100644 index 340164184aa..00000000000 --- a/src/app/features/selectable-asset-list/crypto-currency-asset-icon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Blockchains } from '@shared/models/blockchain.model'; - -import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; -import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; - -export function CryptoCurrencyAssetIcon(props: { blockchain: Blockchains }) { - switch (props.blockchain) { - case 'bitcoin': - return ; - case 'stacks': - return ; - default: - return <>; - } -} diff --git a/src/app/features/selectable-asset-list/fungible-token-asset-item.tsx b/src/app/features/selectable-asset-list/fungible-token-asset-item.tsx deleted file mode 100644 index 61876fe5430..00000000000 --- a/src/app/features/selectable-asset-list/fungible-token-asset-item.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { FlexProps } from 'leather-styles/jsx'; - -import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; - -import { StacksFungibleTokenAssetItemLayout } from '../../components/crypto-assets/stacks/fungible-token-asset/stacks-fungible-token-asset-item.layout'; - -interface FungibleTokenAssetItemProps extends FlexProps { - assetBalance: StacksFungibleTokenAssetBalance; - onClick(): void; -} -export function FungibleTokenAssetItem({ assetBalance, onClick }: FungibleTokenAssetItemProps) { - const { blockchain } = assetBalance; - - switch (blockchain) { - case 'stacks': - return ; - default: - return null; - } -} diff --git a/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx b/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx index 8530574c866..f795222bddd 100644 --- a/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx +++ b/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx @@ -7,8 +7,8 @@ import { AttachmentRow } from '@app/features/stacks-transaction-request/attachme import { ContractPreviewLayout } from '@app/features/stacks-transaction-request/contract-preview'; import { Row } from '@app/features/stacks-transaction-request/row'; import { - useCurrentAccountStxAddressState, useCurrentStacksAccount, + useCurrentStacksAccountAddress, } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; import { CodeBlock } from '@app/ui/components/codeblock'; @@ -18,7 +18,7 @@ function ContractCodeSection() { const transactionRequest = useTransactionRequestState(); const currentAccount = useCurrentStacksAccount(); - const currentAccountStxAddress = useCurrentAccountStxAddressState(); + const currentAccountStxAddress = useCurrentStacksAccountAddress(); if ( !transactionRequest || @@ -63,7 +63,7 @@ function TabButton(props: TabButtonProps) { export function ContractDeployDetails() { const transactionRequest = useTransactionRequestState(); const currentAccount = useCurrentStacksAccount(); - const currentAccountStxAddress = useCurrentAccountStxAddressState(); + const currentAccountStxAddress = useCurrentStacksAccountAddress(); const [tab, setTab] = useState<'details' | 'code'>('details'); if ( diff --git a/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts index b4fb9e0f94f..cc0ce3116bb 100644 --- a/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts +++ b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts @@ -11,7 +11,7 @@ import { initialSearchParams } from '@app/common/initial-search-params'; import { stxToMicroStx } from '@app/common/money/unit-conversion'; import { validateStacksAddress } from '@app/common/stacks-utils'; import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useContractInterface } from '@app/query/stacks/contract/contract.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; @@ -27,7 +27,7 @@ export function useTransactionError() { const { values } = useFormikContext(); const currentAccount = useCurrentStacksAccount(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); return useMemo(() => { if (!origin) return TransactionErrorReason.ExpiredRequest; diff --git a/src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx b/src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx index 3010f23edc2..7ed1c68b3f0 100644 --- a/src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx +++ b/src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx @@ -3,8 +3,7 @@ import { Suspense } from 'react'; import { TransactionTypes } from '@stacks/connect'; import { FungiblePostCondition, addressToString } from '@stacks/transactions'; -import { getImageCanonicalUri } from '@app/common/crypto-assets/stacks-crypto-asset.utils'; -import { ftDecimals } from '@app/common/stacks-utils'; +import { ftDecimals, getSafeImageCanonicalUri } from '@app/common/stacks-utils'; import { getAmountFromPostCondition, getIconStringFromPostCondition, @@ -36,7 +35,7 @@ function FungiblePostConditionItemSuspense( const imageCanonicalUri = asset?.image_canonical_uri && asset.name && - getImageCanonicalUri(asset.image_canonical_uri, asset.name); + getSafeImageCanonicalUri(asset.image_canonical_uri, asset.name); const title = getPostConditionTitle(pc); const iconString = imageCanonicalUri ?? getIconStringFromPostCondition(pc); diff --git a/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx b/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx index eb2d2958fb5..bb0fad7f8fa 100644 --- a/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx +++ b/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx @@ -26,7 +26,7 @@ import { PostConditionModeWarning } from '@app/features/stacks-transaction-reque import { PostConditions } from '@app/features/stacks-transaction-request/post-conditions/post-conditions'; import { StxTransferDetails } from '@app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details'; import { TransactionError } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; @@ -55,7 +55,7 @@ export function StacksTransactionSigner({ const transactionRequest = useTransactionRequestState(); const { data: stxFees } = useCalculateStacksTxFees(stacksTransaction); const analytics = useAnalytics(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); const navigate = useNavigate(); const { data: nextNonce } = useNextNonce(); const { search } = useLocation(); diff --git a/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx b/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx index 0860c486752..d799820ac05 100644 --- a/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx +++ b/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx @@ -12,7 +12,7 @@ import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { stacksValue } from '@app/common/stacks-utils'; import { SwitchAccountDialog } from '@app/features/dialogs/switch-account-dialog/switch-account-dialog'; import { ErrorMessage } from '@app/features/stacks-transaction-request/transaction-error/error-message'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; import { Button } from '@app/ui/components/button/button'; @@ -59,7 +59,7 @@ export const FeeInsufficientFundsErrorMessage = memo(props => { export const StxTransferInsufficientFundsErrorMessage = memo(props => { const pendingTransaction = useTransactionRequestState(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); return ( - [stxCryptoCurrencyAssetBalance].filter(isDefined).filter(assetBalance => - whenWallet({ - ledger: checkBlockchainAvailable(assetBalance?.blockchain), - software: true, - }) - ), - [stxCryptoCurrencyAssetBalance, checkBlockchainAvailable, whenWallet] - ); - const navigateToFund = useCallback( - (cryptoAssetBalance: AllTransferableCryptoAssetBalances) => { - const { asset } = cryptoAssetBalance; - - const symbol = asset.symbol === '' ? asset.contractAssetName : asset.symbol; - navigate(RouteUrls.Fund.replace(':currency', symbol.toUpperCase())); - }, + (asset: AccountCryptoAssetWithDetails) => + navigate(RouteUrls.Fund.replace(':currency', asset.info.symbol)), [navigate] ); @@ -53,13 +36,37 @@ export function ChooseCryptoAssetToFund() { } > - - - + + + {signer => ( + + {(asset, isLoading) => ( + } + isLoading={isLoading} + name={capitalize(asset.info.name)} + onClick={() => navigateToFund(asset)} + /> + )} + + )} + + + + {account => ( + + {(asset, isInitialLoading) => ( + navigateToFund(asset)} + /> + )} + + )} + + diff --git a/src/app/pages/fund/fund.tsx b/src/app/pages/fund/fund.tsx index 438103dcf1a..d2d7f7ef77d 100644 --- a/src/app/pages/fund/fund.tsx +++ b/src/app/pages/fund/fund.tsx @@ -7,8 +7,6 @@ import type { CryptoCurrencies } from '@shared/models/currencies.model'; import { RouteUrls } from '@shared/route-urls'; import { FullPageLoadingSpinner } from '@app/components/loading-spinner'; -import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; -import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/stx-balance.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSignerNullable } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; @@ -26,36 +24,27 @@ interface FundCryptoCurrencyInfo { export function FundPage() { const currentStxAccount = useCurrentStacksAccount(); const bitcoinSigner = useCurrentAccountNativeSegwitIndexZeroSignerNullable(); - const { btcCryptoAssetBalance } = useBtcCryptoAssetBalanceNativeSegwit( - bitcoinSigner?.address ?? '' - ); - const { data: stxCryptoAssetBalance } = useStxCryptoAssetBalance( - currentStxAccount?.address ?? '' - ); const { currency = 'STX' } = useParams(); const fundCryptoCurrencyMap: Record = { BTC: { address: bitcoinSigner?.address, - balance: btcCryptoAssetBalance, - blockchain: 'Bitcoin', + blockchain: 'bitcoin', route: RouteUrls.ReceiveBtc, symbol: currency, }, STX: { address: currentStxAccount?.address, - balance: stxCryptoAssetBalance, - blockchain: 'Stacks', + blockchain: 'stacks', route: RouteUrls.ReceiveStx, symbol: currency, }, }; - const { address, balance, blockchain, route, symbol } = + const { address, blockchain, route, symbol } = fundCryptoCurrencyMap[currency as CryptoCurrencies]; - // TODO: Asset refactor: Why is the balance needed here? - if (!address || !balance) return ; + if (!address) return ; return ( <> diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx index 9fbc32d8e0c..50d5eebda05 100644 --- a/src/app/pages/home/components/account-actions.tsx +++ b/src/app/pages/home/components/account-actions.tsx @@ -51,6 +51,7 @@ export function AccountActions() { [ChainID.Mainnet]: ( } label="Swap" onClick={() => navigate(RouteUrls.Swap.replace(':base', 'STX').replace(':quote', ''))} diff --git a/src/app/pages/home/components/assets.tsx b/src/app/pages/home/components/assets.tsx new file mode 100644 index 00000000000..2568d5e08f0 --- /dev/null +++ b/src/app/pages/home/components/assets.tsx @@ -0,0 +1,19 @@ +import { Outlet } from 'react-router-dom'; + +import { HomePageSelectors } from '@tests/selectors/home.selectors'; +import { Stack } from 'leather-styles/jsx'; + +import { AssetList } from '@app/features/asset-list/asset-list'; +import { Collectibles } from '@app/features/collectibles/collectibles'; +import { PendingBrc20TransferList } from '@app/features/pending-brc-20-transfers/pending-brc-20-transfers'; + +export function Assets() { + return ( + + + + + + + ); +} diff --git a/src/app/pages/home/components/send-button.tsx b/src/app/pages/home/components/send-button.tsx index 60fbf605d12..33da083cc91 100644 --- a/src/app/pages/home/components/send-button.tsx +++ b/src/app/pages/home/components/send-button.tsx @@ -8,19 +8,25 @@ import { RouteUrls } from '@shared/route-urls'; import { useWalletType } from '@app/common/use-wallet-type'; import { whenPageMode } from '@app/common/utils'; import { openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab'; -import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; -import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/stx-balance.hooks'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; +import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/account-balance.hooks'; +import { useTransferableSip10CryptoAssetsWithDetails } from '@app/query/stacks/sip10/sip10-tokens.hooks'; +import { useCurrentAccountNativeSegwitIndexZeroSignerNullable } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { IconButton } from '@app/ui/components/icon-button/icon-button'; import { SendIcon } from '@app/ui/icons'; function SendButtonSuspense() { const navigate = useNavigate(); const { whenWallet } = useWalletType(); - const account = useCurrentStacksAccount(); - const { data: stxCryptoAssetBalance } = useStxCryptoAssetBalance(account?.address ?? ''); - const ftAssets = useTransferableStacksFungibleTokenAssetBalances(account?.address ?? ''); - const isDisabled = !stxCryptoAssetBalance && ftAssets?.length === 0; + const address = useCurrentStacksAccountAddress(); + const btcAddress = useCurrentAccountNativeSegwitIndexZeroSignerNullable()?.address; + const { btcCryptoAssetBalance } = useBtcCryptoAssetBalanceNativeSegwit(btcAddress ?? ''); + const { data: stxCryptoAssetBalance } = useStxCryptoAssetBalance(address); + const stacksFtAssets = useTransferableSip10CryptoAssetsWithDetails(address); + + const isDisabled = + !btcCryptoAssetBalance && !stxCryptoAssetBalance && stacksFtAssets?.length === 0; return ( - } /> + } /> }> {homePageModalRoutes} diff --git a/src/app/pages/receive/receive-dialog.tsx b/src/app/pages/receive/receive-dialog.tsx index c3d189dce5a..390f7463b99 100644 --- a/src/app/pages/receive/receive-dialog.tsx +++ b/src/app/pages/receive/receive-dialog.tsx @@ -11,7 +11,7 @@ import { useLocationState } from '@app/common/hooks/use-location-state'; import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; import { useZeroIndexTaprootAddress } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { Dialog } from '@app/ui/components/containers/dialog/dialog'; import { Header } from '@app/ui/components/containers/headers/header'; import { Tabs } from '@app/ui/components/tabs/tabs'; @@ -39,7 +39,7 @@ export function ReceiveDialog({ type = 'full' }: ReceiveDialogProps) { const navigate = useNavigate(); const location = useLocation(); const btcAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero(); - const stxAddress = useCurrentAccountStxAddressState(); + const stxAddress = useCurrentStacksAccountAddress(); const accountIndex = get(location.state, 'accountIndex', undefined); const btcAddressTaproot = useZeroIndexTaprootAddress(accountIndex); diff --git a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx index 0b7c3caa9ca..2cb7f107d19 100644 --- a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx +++ b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx @@ -2,44 +2,38 @@ import { useNavigate } from 'react-router-dom'; import { Box, styled } from 'leather-styles/jsx'; -import { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; import { RouteUrls } from '@shared/route-urls'; -import { useAllTransferableCryptoAssetBalances } from '@app/common/hooks/use-transferable-asset-balances.hooks'; -import { useWalletType } from '@app/common/use-wallet-type'; -import { CryptoAssetList } from '@app/features/selectable-asset-list/crypto-asset-list'; +import { AssetList } from '@app/features/asset-list/asset-list'; import { useToast } from '@app/features/toasts/use-toast'; import { useConfigBitcoinSendEnabled } from '@app/query/common/remote-config/remote-config.query'; -import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; +import type { AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; +import { getSip10InfoFromAsset } from '@app/query/stacks/sip10/sip10-tokens.hooks'; import { Card } from '@app/ui/layout/card/card'; +import { getAssetStringParts } from '@app/ui/utils/get-asset-string-parts'; export function ChooseCryptoAsset() { - const toast = useToast(); - const allTransferableCryptoAssetBalances = useAllTransferableCryptoAssetBalances(); - - const { whenWallet } = useWalletType(); const navigate = useNavigate(); const isBitcoinSendEnabled = useConfigBitcoinSendEnabled(); + const toast = useToast(); - const checkBlockchainAvailable = useCheckLedgerBlockchainAvailable(); - - function navigateToSendForm(cryptoAssetBalance: AllTransferableCryptoAssetBalances) { - const { asset } = cryptoAssetBalance; - if (asset.symbol === 'BTC' && !isBitcoinSendEnabled) { - return navigate(RouteUrls.SendBtcDisabled); - } - const symbol = asset.symbol === '' ? asset.contractAssetName : asset.symbol.toLowerCase(); - - if (cryptoAssetBalance.type === 'fungible-token') { - const asset = cryptoAssetBalance.asset; - if (!asset.contractId) { - toast.error('Unable to find contract id'); + function navigateToSendForm(asset: AccountCryptoAssetWithDetails) { + switch (asset.type) { + case 'btc': + if (!isBitcoinSendEnabled) return navigate(RouteUrls.SendBtcDisabled); + return navigate(`${RouteUrls.SendCryptoAsset}/${asset.info.symbol.toLowerCase()}`); + case 'sip-10': + const info = getSip10InfoFromAsset(asset); + if (info) { + const { assetName } = getAssetStringParts(info.contractId); + const symbol = !info.symbol ? assetName : info.symbol.toLowerCase(); + return navigate(`${RouteUrls.SendCryptoAsset}/${symbol}/${info.contractId}`); + } + toast.error('No contract id'); return navigate('..'); - } - const contractId = `${asset.contractId.split('::')[0]}`; - return navigate(`${RouteUrls.SendCryptoAsset}/${symbol}/${contractId}`); + default: + return navigate(`${RouteUrls.SendCryptoAsset}/${asset.info.symbol.toLowerCase()}`); } - navigate(`${RouteUrls.SendCryptoAsset}/${symbol}`); } return ( @@ -50,17 +44,8 @@ export function ChooseCryptoAsset() { } > - - navigateToSendForm(cryptoAssetBalance)} - cryptoAssetBalances={allTransferableCryptoAssetBalances.filter(asset => - whenWallet({ - ledger: checkBlockchainAvailable(asset.blockchain), - software: true, - }) - )} - variant="send" - /> + + ); diff --git a/src/app/features/selectable-asset-list/send-btc-disabled.tsx b/src/app/pages/send/choose-crypto-asset/send-btc-disabled.tsx similarity index 100% rename from src/app/features/selectable-asset-list/send-btc-disabled.tsx rename to src/app/pages/send/choose-crypto-asset/send-btc-disabled.tsx diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-choose-fee.tsx similarity index 100% rename from src/app/pages/send/send-crypto-asset-form/form/brc-20/brc-20-choose-fee.tsx rename to src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-choose-fee.tsx diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc20-send-form-confirmation.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-send-form-confirmation.tsx similarity index 100% rename from src/app/pages/send/send-crypto-asset-form/form/brc-20/brc20-send-form-confirmation.tsx rename to src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-send-form-confirmation.tsx diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/brc20-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-send-form.tsx similarity index 100% rename from src/app/pages/send/send-crypto-asset-form/form/brc-20/brc20-send-form.tsx rename to src/app/pages/send/send-crypto-asset-form/form/brc20/brc20-send-form.tsx diff --git a/src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/brc20/use-brc20-send-form.tsx similarity index 100% rename from src/app/pages/send/send-crypto-asset-form/form/brc-20/use-brc20-send-form.tsx rename to src/app/pages/send/send-crypto-asset-form/form/brc20/use-brc20-send-form.tsx diff --git a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx index f60b5dd0f94..57eec8dabd2 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/btc/btc-send-form.tsx @@ -9,7 +9,7 @@ import { CryptoCurrencies } from '@shared/models/currencies.model'; import { formatMoney } from '@app/common/money/format-money'; import { HighFeeDialog } from '@app/features/dialogs/high-fee-dialog/high-fee-dialog'; -import { useBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks'; +import { useBtcAccountCryptoAssetWithDetails } from '@app/query/bitcoin/btc/btc-crypto-asset.hooks'; import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; @@ -36,11 +36,9 @@ export function BtcSendForm() { const routeState = useSendFormRouteState(); const btcMarketData = useCryptoCurrencyMarketDataMeanAverage(symbol); - const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); - // TODO: Asset refactor: need asset here - const { btcCryptoAssetBalance } = useBtcCryptoAssetBalanceNativeSegwit( - nativeSegwitSigner.address - ); + const { address } = useCurrentAccountNativeSegwitIndexZeroSigner(); + const { asset } = useBtcAccountCryptoAssetWithDetails(address); + const { balance, info } = asset; const { calcMaxSpend, @@ -83,19 +81,17 @@ export function BtcSendForm() { > Continue - + } > } /> - } - name={btcBalance.asset.name} - symbol={symbol} - /> + } name={info.name} symbol={symbol} /> {currentNetwork.chain.bitcoin.bitcoinNetwork === 'testnet' && ( diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form-container.tsx b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx similarity index 83% rename from src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form-container.tsx rename to src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx index 643935c0717..fbee8ba9b37 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/sip10-token-send-form-container.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/sip10/sip10-token-send-form-container.tsx @@ -1,4 +1,5 @@ -import { StacksAssetAvatar } from '@app/components/crypto-assets/stacks/components/stacks-asset-avatar'; +import { StacksAssetAvatar } from '@app/components/stacks-asset-avatar'; +import type { Sip10AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; import { AmountField } from '../../components/amount-field'; @@ -9,13 +10,9 @@ import { StacksCommonSendForm } from '../stacks/stacks-common-send-form'; import { useSip10SendForm } from './use-sip10-send-form'; interface Sip10TokenSendFormContainerProps { - symbol: string; - contractId: string; + asset: Sip10AccountCryptoAssetWithDetails; } -export function Sip10TokenSendFormContainer({ - symbol, - contractId, -}: Sip10TokenSendFormContainerProps) { +export function Sip10TokenSendFormContainer({ asset }: Sip10TokenSendFormContainerProps) { const { availableTokenBalance, initialValues, @@ -26,7 +23,8 @@ export function Sip10TokenSendFormContainer({ avatar, marketData, decimals, - } = useSip10SendForm({ symbol, contractId }); + symbol, + } = useSip10SendForm({ asset }); const amountField = ( - {({ contractId, symbol }) => ( - - )} - - ); -} - interface Sip10TokenSendFormLoaderProps { - children(data: { symbol: string; contractId: string }): React.JSX.Element; + children(data: { asset: Sip10AccountCryptoAssetWithDetails }): React.ReactNode; } function Sip10TokenSendFormLoader({ children }: Sip10TokenSendFormLoaderProps) { - const { symbol, contractId } = useParams(); + const { contractId } = useParams(); + const asset = useSip10CryptoAssetWithDetails(contractId ?? ''); const toast = useToast(); - if (!symbol || !contractId) { - toast.error('Symbol or contract id not found'); + if (!asset) { + toast.error('Asset not found'); return ; } - return children({ symbol, contractId }); + return children({ asset }); +} + +export function Sip10TokenSendForm() { + return ( + + {({ asset }) => } + + ); } diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/sip10/use-sip10-send-form.tsx similarity index 64% rename from src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx rename to src/app/pages/send/send-crypto-asset-form/form/sip10/use-sip10-send-form.tsx index 31d63e80522..41c3b66c600 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks-sip10/use-sip10-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/sip10/use-sip10-send-form.tsx @@ -6,11 +6,10 @@ import * as yup from 'yup'; import { logger } from '@shared/logger'; import { StacksSendFormValues } from '@shared/models/form.model'; -import { getImageCanonicalUri } from '@app/common/crypto-assets/stacks-crypto-asset.utils'; import { convertAmountToBaseUnit } from '@app/common/money/calculate-money'; -import { formatContractId } from '@app/common/utils'; +import { getSafeImageCanonicalUri } from '@app/common/stacks-utils'; import { stacksFungibleTokenAmountValidator } from '@app/common/validation/forms/amount-validators'; -import { useStacksFungibleTokenAssetBalance } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; +import type { Sip10AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; import { useFtTokenTransferUnsignedTx, @@ -21,36 +20,31 @@ import { useSendFormNavigate } from '../../hooks/use-send-form-navigate'; import { useStacksCommonSendForm } from '../stacks/use-stacks-common-send-form'; interface UseSip10SendFormArgs { - symbol: string; - contractId: string; + asset: Sip10AccountCryptoAssetWithDetails; } -export function useSip10SendForm({ symbol, contractId }: UseSip10SendFormArgs) { - const assetBalance = useStacksFungibleTokenAssetBalance(contractId); - const generateTx = useGenerateFtTokenTransferUnsignedTx(assetBalance); +export function useSip10SendForm({ asset }: UseSip10SendFormArgs) { + const generateTx = useGenerateFtTokenTransferUnsignedTx(asset.info); const sendFormNavigate = useSendFormNavigate(); - const unsignedTx = useFtTokenTransferUnsignedTx(assetBalance); + const unsignedTx = useFtTokenTransferUnsignedTx(asset.info); const { data: stacksFtFees } = useCalculateStacksTxFees(unsignedTx); - const availableTokenBalance = assetBalance.balance; + const availableTokenBalance = asset.balance.availableBalance; const sendMaxBalance = useMemo( () => convertAmountToBaseUnit(availableTokenBalance), [availableTokenBalance] ); const { initialValues, checkFormValidation, recipient, memo, nonce } = useStacksCommonSendForm({ - symbol, + symbol: asset.info.symbol, availableTokenBalance, }); function createFtAvatar() { - const asset = assetBalance.asset; - - const { contractAddress, contractAssetName, contractName } = asset; return { - avatar: `${formatContractId(contractAddress, contractName)}::${contractAssetName}`, - imageCanonicalUri: getImageCanonicalUri(asset.imageCanonicalUri, asset.name), + avatar: asset.info.contractId, + imageCanonicalUri: getSafeImageCanonicalUri(asset.info.imageCanonicalUri, asset.info.name), }; } @@ -59,9 +53,9 @@ export function useSip10SendForm({ symbol, contractId }: UseSip10SendFormArgs) { initialValues, sendMaxBalance, stacksFtFees, - symbol, - decimals: assetBalance.asset.decimals, - marketData: assetBalance.asset.marketData, + symbol: asset.info.symbol, + decimals: asset.info.decimals, + marketData: asset.marketData, avatar: createFtAvatar(), validationSchema: yup.object({ amount: stacksFungibleTokenAmountValidator(availableTokenBalance), @@ -81,8 +75,8 @@ export function useSip10SendForm({ symbol, contractId }: UseSip10SendFormArgs) { if (!tx) return logger.error('Attempted to generate unsigned tx, but tx is undefined'); sendFormNavigate.toConfirmAndSignStacksSip10Transaction({ - decimals: assetBalance.balance.decimals, - name: assetBalance.asset.name, + decimals: asset.info.decimals, + name: asset.info.name, tx, }); }, diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-send-form-confirmation.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-send-form-confirmation.tsx index d91454baed6..8e0c9367c0f 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-send-form-confirmation.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-send-form-confirmation.tsx @@ -57,7 +57,7 @@ export function StacksSendFormConfirmation() { > - + diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks/use-stacks-common-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks/use-stacks-common-send-form.tsx index de916bb9069..0b8eecbe88b 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks/use-stacks-common-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stacks/use-stacks-common-send-form.tsx @@ -12,7 +12,7 @@ import { stxMemoValidator } from '@app/common/validation/forms/memo-validators'; import { stxRecipientValidator } from '@app/common/validation/forms/recipient-validators'; import { nonceValidator } from '@app/common/validation/nonce-validators'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; import { useSendFormRouteState } from '../../hooks/use-send-form-route-state'; @@ -29,7 +29,7 @@ export function useStacksCommonSendForm({ }: UseStacksCommonSendFormArgs) { const routeState = useSendFormRouteState(); const { data: nextNonce } = useNextNonce(); - const currentAccountStxAddress = useCurrentAccountStxAddressState(); + const currentAccountStxAddress = useCurrentStacksAccountAddress(); const currentNetwork = useCurrentNetworkState(); const initialValues: StacksSendFormValues = createDefaultInitialFormValues({ diff --git a/src/app/pages/send/send-crypto-asset-form/form/stx/stx-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stx/stx-send-form.tsx index f663c70878a..c13dfedc82a 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stx/stx-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stx/stx-send-form.tsx @@ -16,7 +16,7 @@ export function StxSendForm() { const stxMarketData = useCryptoCurrencyMarketDataMeanAverage(symbol); const { - availableStxBalance, + availableUnlockedBalance, initialValues, previewTransaction, sendMaxBalance, @@ -27,10 +27,13 @@ export function StxSendForm() { const amountField = ( } bottomInputOverlay={ - + } autoComplete="off" /> @@ -51,7 +54,7 @@ export function StxSendForm() { // FIXME 4370 - need to fix this as fee is actually NumberSchema; in FeeValidatorFactoryArgs // this needs to be the STX fee so it can be validated against HIGH_FEE_AMOUNT_STX fee={fee as unknown as string} - availableTokenBalance={availableStxBalance} + availableTokenBalance={availableUnlockedBalance} /> ); } diff --git a/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx index 46ecaacaa77..d69d206590b 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stx/use-stx-send-form.tsx @@ -14,7 +14,7 @@ import { } from '@app/common/validation/forms/amount-validators'; import { stxFeeValidator } from '@app/common/validation/forms/fee-validators'; import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; import { useStacksValidateFeeByNonce } from '@app/query/stacks/mempool/mempool.hooks'; import { @@ -32,7 +32,7 @@ export function useStxSendForm() { const { onFormStateChange } = useUpdatePersistedSendFormValues(); const sendFormNavigate = useSendFormNavigate(); const { changeFeeByNonce } = useStacksValidateFeeByNonce(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); const sendMaxBalance = useMemo( () => diff --git a/src/app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes.tsx b/src/app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes.tsx index d920c347341..4fdc961f637 100644 --- a/src/app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes.tsx +++ b/src/app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes.tsx @@ -8,7 +8,7 @@ import { FullPageWithHeaderLoadingSpinner } from '@app/components/loading-spinne import { EditNonceDialog } from '@app/features/dialogs/edit-nonce-dialog/edit-nonce-dialog'; import { ledgerBitcoinTxSigningRoutes } from '@app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container'; import { ledgerStacksTxSigningRoutes } from '@app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container'; -import { SendBtcDisabled } from '@app/features/selectable-asset-list/send-btc-disabled'; +import { SendBtcDisabled } from '@app/pages/send/choose-crypto-asset/send-btc-disabled'; import { AccountGate } from '@app/routes/account-gate'; import { Page } from '@app/ui/layout/page/page.layout'; @@ -19,13 +19,13 @@ import { BtcSentSummary } from '../sent-summary/btc-sent-summary'; import { StxSentSummary } from '../sent-summary/stx-sent-summary'; import { RecipientAccountsDialog } from './components/recipient-accounts-dialog/recipient-accounts-dialog'; import { SendBitcoinAssetContainer } from './family/bitcoin/components/send-bitcoin-asset-container'; -import { Brc20SendForm } from './form/brc-20/brc20-send-form'; -import { Brc20SendFormConfirmation } from './form/brc-20/brc20-send-form-confirmation'; -import { BrcChooseFee } from './form/brc-20/brc-20-choose-fee'; +import { BrcChooseFee } from './form/brc20/brc20-choose-fee'; +import { Brc20SendForm } from './form/brc20/brc20-send-form'; +import { Brc20SendFormConfirmation } from './form/brc20/brc20-send-form-confirmation'; import { BtcChooseFee } from './form/btc/btc-choose-fee'; import { BtcSendForm } from './form/btc/btc-send-form'; import { BtcSendFormConfirmation } from './form/btc/btc-send-form-confirmation'; -import { Sip10TokenSendForm } from './form/stacks-sip10/sip10-token-send-form'; +import { Sip10TokenSendForm } from './form/sip10/sip10-token-send-form'; import { StacksSendFormConfirmation } from './form/stacks/stacks-send-form-confirmation'; import { StxSendForm } from './form/stx/stx-send-form'; diff --git a/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx index 5ae67e717e5..83ccef5e333 100644 --- a/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx +++ b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx @@ -3,8 +3,8 @@ import { SwapSelectors } from '@tests/selectors/swap.selectors'; import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; import type { SwapAsset } from '@app/query/common/alex-sdk/alex-sdk.hooks'; -import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query'; -import { isFtAsset } from '@app/query/stacks/tokens/token-metadata.utils'; +import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.query'; +import { isFtAsset } from '@app/query/stacks/token-metadata/token-metadata.utils'; import { Avatar, defaultFallbackDelay, getAvatarFallback } from '@app/ui/components/avatar/avatar'; import { ItemLayout } from '@app/ui/components/item-layout/item-layout'; import { Pressable } from '@app/ui/pressable/pressable'; @@ -19,7 +19,7 @@ export function SwapAssetItem({ asset, onClick }: SwapAssetItemProps) { const ftMetadataName = ftMetadata && isFtAsset(ftMetadata) ? ftMetadata.name : asset.name; const displayName = asset.displayName ?? ftMetadataName; const fallback = getAvatarFallback(asset.name); - const balanceAsFiat = convertAssetBalanceToFiat(asset); + const fiatBalance = convertAssetBalanceToFiat(asset); return ( @@ -33,7 +33,7 @@ export function SwapAssetItem({ asset, onClick }: SwapAssetItemProps) { titleLeft={displayName} captionLeft={asset.name} titleRight={formatMoneyWithoutSymbol(asset.balance)} - captionRight={balanceAsFiat} + captionRight={fiatBalance} /> ); diff --git a/src/app/pages/transaction-request/transaction-request.tsx b/src/app/pages/transaction-request/transaction-request.tsx index 82a08a0a414..356818bec10 100644 --- a/src/app/pages/transaction-request/transaction-request.tsx +++ b/src/app/pages/transaction-request/transaction-request.tsx @@ -29,7 +29,7 @@ import { PostConditions } from '@app/features/stacks-transaction-request/post-co import { StxTransferDetails } from '@app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details'; import { SubmitAction } from '@app/features/stacks-transaction-request/submit-action'; import { TransactionError } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; @@ -47,7 +47,7 @@ function TransactionRequestBase() { const { data: stxFees } = useCalculateStacksTxFees(unsignedTx.transaction); const analytics = useAnalytics(); const generateUnsignedTx = useGenerateUnsignedStacksTransaction(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); const { data: nextNonce } = useNextNonce(); const navigate = useNavigate(); const { stacksBroadcastTransaction } = useStacksBroadcastTransaction('STX'); diff --git a/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts b/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts index ca2c0831fd9..84f43815ec3 100644 --- a/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts +++ b/src/app/query/bitcoin/balance/btc-balance-native-segwit.hooks.ts @@ -8,7 +8,7 @@ import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/account import { useGetBitcoinBalanceByAddress } from './btc-balance.hooks'; -function makeBtcCryptoAssetBalance(balance: Money): BtcCryptoAssetBalance { +function createBtcCryptoAssetBalance(balance: Money): BtcCryptoAssetBalance { return { availableBalance: balance, // TODO: Asset refactor: can we determine these here or are they nec? @@ -21,7 +21,7 @@ function makeBtcCryptoAssetBalance(balance: Money): BtcCryptoAssetBalance { export function useBtcCryptoAssetBalanceNativeSegwit(address: string) { const { balance, isInitialLoading, isLoading, isFetching } = useGetBitcoinBalanceByAddress(address); - const btcCryptoAssetBalance = useMemo(() => makeBtcCryptoAssetBalance(balance), [balance]); + const btcCryptoAssetBalance = useMemo(() => createBtcCryptoAssetBalance(balance), [balance]); return { btcCryptoAssetBalance, diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index dc729f086f5..8b9f127bed5 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -6,7 +6,6 @@ import { BESTINSLOT_API_BASE_URL_TESTNET, type BitcoinNetworkModes, } from '@shared/constants'; -import type { MarketData } from '@shared/models/market.model'; import type { Money } from '@shared/models/money.model'; import type { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; @@ -98,13 +97,6 @@ interface Brc20WalletBalancesResponse { data: Brc20Balance[]; } -export interface Brc20Token { - balance: Money | null; - holderAddress: string; - marketData: MarketData | null; - tokenData: Brc20Balance & Brc20TickerInfo; -} - /* RUNES */ export interface RuneBalance { pkscript: string; diff --git a/src/app/query/bitcoin/btc/btc-crypto-asset.hooks.ts b/src/app/query/bitcoin/btc/btc-crypto-asset.hooks.ts new file mode 100644 index 00000000000..52cc752989d --- /dev/null +++ b/src/app/query/bitcoin/btc/btc-crypto-asset.hooks.ts @@ -0,0 +1,48 @@ +import type { BtcCryptoAssetInfo } from '@leather-wallet/models'; + +import { BTC_DECIMALS } from '@shared/constants'; +import { createMoney } from '@shared/models/money.model'; + +import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; +import { createAccountCryptoAssetWithDetailsFactory } from '@app/query/models/crypto-asset.model'; + +import type { BtcAccountCryptoAssetWithDetails } from '../../models/crypto-asset.model'; +import { useBtcCryptoAssetBalanceNativeSegwit } from '../balance/btc-balance-native-segwit.hooks'; + +const btcCryptoAssetInfo: BtcCryptoAssetInfo = { + decimals: BTC_DECIMALS, + hasMemo: false, + name: 'bitcoin', + symbol: 'BTC', +}; + +const btcCryptoAssetBalancePlaceholder = { + availableBalance: createMoney(0, 'BTC'), + protectedBalance: createMoney(0, 'BTC'), + uneconomicalBalance: createMoney(0, 'BTC'), +}; + +export const btcCryptoAssetPlaceholder = + createAccountCryptoAssetWithDetailsFactory({ + balance: btcCryptoAssetBalancePlaceholder, + chain: 'bitcoin', + info: btcCryptoAssetInfo, + marketData: null, + type: 'btc', + }); + +export function useBtcAccountCryptoAssetWithDetails(address: string) { + const { btcCryptoAssetBalance, isInitialLoading } = useBtcCryptoAssetBalanceNativeSegwit(address); + const marketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); + + return { + asset: createAccountCryptoAssetWithDetailsFactory({ + balance: btcCryptoAssetBalance, + chain: 'bitcoin', + info: btcCryptoAssetInfo, + marketData, + type: 'btc', + }), + isInitialLoading, + }; +} diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts index 6e7bd527808..d0dff8e26b9 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts @@ -1,18 +1,21 @@ import BigNumber from 'bignumber.js'; import { createMarketData, createMarketPair } from '@shared/models/market.model'; -import { type Money, createMoney } from '@shared/models/money.model'; +import { createMoney } from '@shared/models/money.model'; import { unitToFractionalUnit } from '@app/common/money/unit-conversion'; import { useGetBrc20TokensQuery } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks'; import { useConfigOrdinalsbot } from '@app/query/common/remote-config/remote-config.query'; +import { + type Brc20AccountCryptoAssetWithDetails, + createAccountCryptoAssetWithDetailsFactory, +} from '@app/query/models/crypto-asset.model'; import { useAppDispatch } from '@app/store'; import { useCurrentAccountIndex } from '@app/store/accounts/account'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice'; -import type { Brc20Token } from '../../bitcoin-client'; import { useAverageBitcoinFeeRates } from '../../fees/fee-estimates.hooks'; import { useOrdinalsbotClient } from '../../ordinalsbot-client'; import { @@ -85,26 +88,7 @@ export function useBrc20Transfers(holderAddress: string) { }; } -function makeBrc20Token(priceAsFiat: Money, token: Brc20Token) { - return { - ...token, - balance: createMoney( - unitToFractionalUnit(token.tokenData.decimals)( - new BigNumber(token.tokenData.overall_balance) - ), - token.tokenData.ticker, - token.tokenData.decimals - ), - marketData: token.tokenData.min_listed_unit_price - ? createMarketData( - createMarketPair(token.tokenData.ticker, 'USD'), - createMoney(priceAsFiat.amount, 'USD') - ) - : null, - }; -} - -export function useBrc20Tokens() { +export function useBrc20AccountCryptoAssetsWithDetails() { const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue(); const { data: allBrc20TokensResponse } = useGetBrc20TokensQuery(); @@ -116,9 +100,32 @@ export function useBrc20Tokens() { return ( tokens?.map(token => { const priceAsFiat = calculateBitcoinFiatValue( - createMoney(new BigNumber(token.tokenData.min_listed_unit_price ?? 0), 'BTC') + createMoney(new BigNumber(token.balance.min_listed_unit_price ?? 0), 'BTC') ); - return makeBrc20Token(priceAsFiat, token); + return createAccountCryptoAssetWithDetailsFactory({ + balance: { + availableBalance: createMoney( + unitToFractionalUnit(token.info.decimals)(new BigNumber(token.balance.overall_balance)), + token.balance.ticker, + token.info.decimals + ), + }, + chain: 'bitcoin', + holderAddress: token.holderAddress, + info: { + decimals: token.info.decimals, + hasMemo: false, + name: 'brc-20', + symbol: token.info.ticker, + }, + marketData: token.balance.min_listed_unit_price + ? createMarketData( + createMarketPair(token.balance.ticker, 'USD'), + createMoney(priceAsFiat.amount, 'USD') + ) + : null, + type: 'brc-20', + }); }) ?? [] ); } diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts index 6440524a788..36d685caac1 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts @@ -10,8 +10,6 @@ import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/b import { useBitcoinClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { Brc20Token } from '../../bitcoin-client'; - const addressesSimultaneousFetchLimit = 3; const stopSearchAfterNumberAddressesWithoutBrc20Tokens = 3; @@ -59,18 +57,16 @@ export function useGetBrc20TokensQuery() { }) ); - // Initialize token with token data return brc20Tokens.data.map((token, index) => { return { - balance: null, + balance: token, holderAddress: address, - marketData: null, - tokenData: { ...token, ...tickerPromises[index].data }, + info: tickerPromises[index].data, }; }); }); - const brc20Tokens: Brc20Token[][] = await Promise.all(brc20TokensPromises); + const brc20Tokens = await Promise.all(brc20TokensPromises); addressesWithoutTokens += brc20Tokens.filter(tokens => tokens.length === 0).length; return { diff --git a/src/app/query/common/alex-sdk/alex-sdk.hooks.ts b/src/app/query/common/alex-sdk/alex-sdk.hooks.ts index a47479511de..6d3b95fcb92 100644 --- a/src/app/query/common/alex-sdk/alex-sdk.hooks.ts +++ b/src/app/query/common/alex-sdk/alex-sdk.hooks.ts @@ -11,9 +11,9 @@ import { isDefined } from '@shared/utils'; import { sortAssetsByName } from '@app/common/asset-utils'; import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; import { pullContractIdFromIdentity } from '@app/common/utils'; -import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; -import { useCurrentStcAvailableUnlockedBalance } from '@app/query/stacks/balance/stx-balance.hooks'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStxAvailableUnlockedBalance } from '@app/query/stacks/balance/account-balance.hooks'; +import { useTransferableSip10CryptoAssetsWithDetails } from '@app/query/stacks/sip10/sip10-tokens.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { getAvatarFallback } from '@app/ui/components/avatar/avatar'; import { useAlexSdkLatestPricesQuery } from './alex-sdk-latest-prices.query'; @@ -51,14 +51,12 @@ export function useAlexCurrencyPriceAsMarketData() { ); } -function useMakeSwapAsset() { - const account = useCurrentStacksAccount(); +function useCreateSwapAsset() { + const address = useCurrentStacksAccountAddress(); const { data: prices } = useAlexSdkLatestPricesQuery(); const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); - const availableUnlockedBalance = useCurrentStcAvailableUnlockedBalance(); - const stacksFtAssetBalances = useTransferableStacksFungibleTokenAssetBalances( - account?.address ?? '' - ); + const availableUnlockedBalance = useCurrentStxAvailableUnlockedBalance(); + const assets = useTransferableSip10CryptoAssetsWithDetails(address); return useCallback( (tokenInfo?: TokenInfo): SwapAsset | undefined => { @@ -69,17 +67,16 @@ function useMakeSwapAsset() { } const currency = tokenInfo.id as Currency; - const fungibleTokenBalance = stacksFtAssetBalances.find( - balance => tokenInfo.contractAddress === balance.asset.contractId - )?.balance; const principal = pullContractIdFromIdentity(tokenInfo.contractAddress); + const availableBalance = assets.find(a => a.info.contractId === principal)?.balance + .availableBalance; const swapAsset = { currency, fallback: getAvatarFallback(tokenInfo.name), icon: tokenInfo.icon, name: tokenInfo.name, - principal: pullContractIdFromIdentity(tokenInfo.contractAddress), + principal, }; if (currency === Currency.STX) { @@ -93,19 +90,19 @@ function useMakeSwapAsset() { return { ...swapAsset, - balance: fungibleTokenBalance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals), - marketData: fungibleTokenBalance - ? priceAsMarketData(principal, fungibleTokenBalance.symbol) + balance: availableBalance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals), + marketData: availableBalance + ? priceAsMarketData(principal, availableBalance.symbol) : priceAsMarketData(principal, tokenInfo.name), }; }, - [availableUnlockedBalance, priceAsMarketData, prices, stacksFtAssetBalances] + [assets, availableUnlockedBalance, priceAsMarketData, prices] ); } export function useAlexSwappableAssets() { - const makeSwapAsset = useMakeSwapAsset(); + const createSwapAsset = useCreateSwapAsset(); return useAlexSdkSwappableCurrencyQuery({ - select: resp => sortAssetsByName(resp.map(makeSwapAsset).filter(isDefined)), + select: resp => sortAssetsByName(resp.map(createSwapAsset).filter(isDefined)), }); } diff --git a/src/app/query/models/crypto-asset.model.ts b/src/app/query/models/crypto-asset.model.ts new file mode 100644 index 00000000000..ac2ecaa1389 --- /dev/null +++ b/src/app/query/models/crypto-asset.model.ts @@ -0,0 +1,59 @@ +import type { + BaseCryptoAssetInfo, + Blockchains, + Brc20CryptoAssetInfo, + BtcCryptoAssetBalance, + BtcCryptoAssetInfo, + CryptoAssetBalance, + CryptoAssetType, + MarketData, + Sip10CryptoAssetInfo, + StxCryptoAssetBalance, + StxCryptoAssetInfo, +} from '@leather-wallet/models'; + +interface AccountCryptoAssetDetails { + balance: CryptoAssetBalance; + chain: Blockchains; + info: BaseCryptoAssetInfo; + marketData: MarketData | null; + type: CryptoAssetType; +} + +export function createAccountCryptoAssetWithDetailsFactory( + args: T +): T { + const { balance, chain, info, marketData, type } = args; + return { + balance, + chain, + info, + marketData, + type, + } as T; +} + +export interface BtcAccountCryptoAssetWithDetails extends AccountCryptoAssetDetails { + balance: BtcCryptoAssetBalance; + info: BtcCryptoAssetInfo; +} + +export interface StxAccountCryptoAssetWithDetails extends AccountCryptoAssetDetails { + balance: StxCryptoAssetBalance; + info: StxCryptoAssetInfo; +} + +export interface Brc20AccountCryptoAssetWithDetails extends AccountCryptoAssetDetails { + holderAddress: string; + info: Brc20CryptoAssetInfo; +} + +export interface Sip10AccountCryptoAssetWithDetails extends AccountCryptoAssetDetails { + info: Sip10CryptoAssetInfo; +} + +export type AccountCryptoAssetWithDetails = + | BtcAccountCryptoAssetWithDetails + | StxAccountCryptoAssetWithDetails + | Brc20AccountCryptoAssetWithDetails + | Sip10AccountCryptoAssetWithDetails; diff --git a/src/app/query/stacks/balance/stx-balance.hooks.ts b/src/app/query/stacks/balance/account-balance.hooks.ts similarity index 67% rename from src/app/query/stacks/balance/stx-balance.hooks.ts rename to src/app/query/stacks/balance/account-balance.hooks.ts index ec382710468..0c85fa37011 100644 --- a/src/app/query/stacks/balance/stx-balance.hooks.ts +++ b/src/app/query/stacks/balance/account-balance.hooks.ts @@ -5,22 +5,22 @@ import { AccountBalanceStxKeys, type AddressBalanceResponse } from '@shared/mode import { Money, createMoney } from '@shared/models/money.model'; import { subtractMoney, sumMoney } from '@app/common/money/calculate-money'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { accountBalanceStxKeys } from '@app/store/accounts/blockchain/stacks/stacks-account.models'; import { useMempoolTxsInboundBalance, useMempoolTxsOutboundBalance, } from '../mempool/mempool.hooks'; -import { useStacksAccountBalanceQuery } from './stx-balance.query'; +import { useStacksAccountBalanceQuery } from './account-balance.query'; -function makeStxMoney(resp: AddressBalanceResponse) { +function createStxMoney(resp: AddressBalanceResponse) { return Object.fromEntries( accountBalanceStxKeys.map(key => [key, { amount: new BigNumber(resp.stx[key]), symbol: 'STX' }]) ) as Record; } -function makeStxCryptoAssetBalance( +function createStxCryptoAssetBalance( stxMoney: Record, inboundBalance: Money, outboundBalance: Money @@ -29,9 +29,6 @@ function makeStxCryptoAssetBalance( const unlockedBalance = subtractMoney(stxMoney.balance, stxMoney.locked); return { - // TODO: Asset refactor: are inbound/outbound necessary? - // - Make sure to track changes to effectiveBalance, now availableBalance - // - And, previous availableBalance is now availableUnlockedBalance availableBalance: subtractMoney(totalBalance, outboundBalance), availableUnlockedBalance: subtractMoney(unlockedBalance, outboundBalance), inboundBalance, @@ -47,13 +44,18 @@ export function useStxCryptoAssetBalance(address: string) { const inboundBalance = useMempoolTxsInboundBalance(address); const outboundBalance = useMempoolTxsOutboundBalance(address); return useStacksAccountBalanceQuery(address, { - select: resp => makeStxCryptoAssetBalance(makeStxMoney(resp), inboundBalance, outboundBalance), + select: resp => + createStxCryptoAssetBalance(createStxMoney(resp), inboundBalance, outboundBalance), }); } -export function useCurrentStcAvailableUnlockedBalance() { - const account = useCurrentStacksAccount(); - return ( - useStxCryptoAssetBalance(account?.address ?? '').data?.unlockedBalance ?? createMoney(0, 'STX') - ); +export function useCurrentStxAvailableUnlockedBalance() { + const address = useCurrentStacksAccountAddress(); + return useStxCryptoAssetBalance(address).data?.unlockedBalance ?? createMoney(0, 'STX'); +} + +export function useStacksAccountBalanceFungibleTokens(address: string) { + return useStacksAccountBalanceQuery(address, { + select: resp => resp.fungible_tokens, + }); } diff --git a/src/app/query/stacks/balance/stx-balance.query.ts b/src/app/query/stacks/balance/account-balance.query.ts similarity index 100% rename from src/app/query/stacks/balance/stx-balance.query.ts rename to src/app/query/stacks/balance/account-balance.query.ts diff --git a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts b/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts deleted file mode 100644 index 93f44728262..00000000000 --- a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import BigNumber from 'bignumber.js'; - -import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; - -import { formatContractId } from '@app/common/utils'; -import { useToast } from '@app/features/toasts/use-toast'; -import { - type SwapAsset, - useAlexCurrencyPriceAsMarketData, - useAlexSwappableAssets, -} from '@app/query/common/alex-sdk/alex-sdk.hooks'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; - -import { useGetFungibleTokenMetadataListQuery } from '../tokens/fungible-tokens/fungible-token-metadata.query'; -import { isFtAsset } from '../tokens/token-metadata.utils'; -import { - addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance, - convertFtBalancesToStacksFungibleTokenAssetBalanceType, - createStacksFtCryptoAssetBalanceTypeWrapper, -} from './stacks-ft-balances.utils'; -import { useStacksAccountBalanceQuery } from './stx-balance.query'; - -function useStacksFungibleTokenAssetBalances(address: string) { - return useStacksAccountBalanceQuery(address, { - select: resp => convertFtBalancesToStacksFungibleTokenAssetBalanceType(resp.fungible_tokens), - }); -} - -function useStacksFungibleTokenAssetBalancesWithMetadata(address: string) { - const { data: initializedAssetBalances = [] } = useStacksFungibleTokenAssetBalances(address); - const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); - - const ftAssetsMetadata = useGetFungibleTokenMetadataListQuery( - initializedAssetBalances.map(assetBalance => - formatContractId(assetBalance.asset.contractAddress, assetBalance.asset.contractName) - ) - ); - - return useMemo( - () => - initializedAssetBalances.map((assetBalance, i) => { - const metadata = ftAssetsMetadata[i].data; - if (!(metadata && isFtAsset(metadata))) return assetBalance; - return addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance( - assetBalance, - metadata, - priceAsMarketData( - formatContractId(assetBalance.asset.contractAddress, assetBalance.asset.contractName), - metadata.symbol - ) - ); - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [initializedAssetBalances] - ); -} - -export function useStacksFungibleTokenAssetBalance(contractId: string) { - const toast = useToast(); - const account = useCurrentStacksAccount(); - const navigate = useNavigate(); - const assetBalances = useStacksFungibleTokenAssetBalancesWithMetadata(account?.address ?? ''); - - return useMemo(() => { - const balance = assetBalances.find(assetBalance => - assetBalance.asset.contractId.includes(contractId) - ); - if (!balance) { - toast.error('Unable to find balance by contract id'); - navigate('..'); - } - return balance ?? createStacksFtCryptoAssetBalanceTypeWrapper(new BigNumber(0), contractId); - }, [assetBalances, contractId, navigate, toast]); -} - -export function useTransferableStacksFungibleTokenAssetBalances( - address: string -): StacksFungibleTokenAssetBalance[] { - const assetBalances = useStacksFungibleTokenAssetBalancesWithMetadata(address); - return useMemo( - () => assetBalances.filter(assetBalance => assetBalance.asset.canTransfer), - [assetBalances] - ); -} - -function filterStacksFungibleTokens( - assetBalances: StacksFungibleTokenAssetBalance[], - swapAssets: SwapAsset[], - filter: StacksFtTokensFilter -) { - if (filter === 'supported') { - return assetBalances.filter(assetBalance => - swapAssets.some(swapAsset => swapAsset.principal.includes(assetBalance.asset.contractAddress)) - ); - } - - if (filter === 'unsupported') { - return assetBalances.filter( - assetBalance => - !swapAssets.some(swapAsset => - swapAsset.principal.includes(assetBalance.asset.contractAddress) - ) - ); - } - - return assetBalances; -} - -/** - * @see https://github.com/leather-wallet/issues/issues/16 - */ -type StacksFtTokensFilter = 'all' | 'supported' | 'unsupported'; - -interface useFilteredStacksFungibleTokenListArgs { - address: string; - filter?: StacksFtTokensFilter; -} -export function useFilteredStacksFungibleTokenList({ - address, - filter = 'all', -}: useFilteredStacksFungibleTokenListArgs) { - const stacksFtAssetBalances = useStacksFungibleTokenAssetBalancesWithMetadata(address); - const { data: swapAssets = [] } = useAlexSwappableAssets(); - - return useMemo(() => { - return filterStacksFungibleTokens(stacksFtAssetBalances, swapAssets, filter); - }, [stacksFtAssetBalances, swapAssets, filter]); -} diff --git a/src/app/query/stacks/balance/stacks-ft-balances.utils.ts b/src/app/query/stacks/balance/stacks-ft-balances.utils.ts deleted file mode 100644 index 6d88699c766..00000000000 --- a/src/app/query/stacks/balance/stacks-ft-balances.utils.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { FtMetadataResponse } from '@hirosystems/token-metadata-api-client'; -import BigNumber from 'bignumber.js'; - -import { STX_DECIMALS } from '@shared/constants'; -import type { AccountBalanceResponseBigNumber } from '@shared/models/account.model'; -import type { - StacksCryptoCurrencyAssetBalance, - StacksFungibleTokenAssetBalance, -} from '@shared/models/crypto-asset-balance.model'; -import { type MarketData } from '@shared/models/market.model'; -import { createMoney } from '@shared/models/money.model'; - -import { isTransferableStacksFungibleTokenAsset } from '@app/common/crypto-assets/stacks-crypto-asset.utils'; -import { getAssetStringParts } from '@app/ui/utils/get-asset-string-parts'; - -export function createStacksCryptoCurrencyAssetTypeWrapper( - balance: BigNumber -): StacksCryptoCurrencyAssetBalance { - return { - blockchain: 'stacks', - type: 'crypto-currency', - balance: createMoney(balance, 'STX'), - asset: { - decimals: STX_DECIMALS, - hasMemo: true, - name: 'Stacks', - symbol: 'STX', - }, - }; -} - -export function createStacksFtCryptoAssetBalanceTypeWrapper( - balance: BigNumber, - contractId: string -): StacksFungibleTokenAssetBalance { - const { address, contractName, assetName } = getAssetStringParts(contractId); - return { - blockchain: 'stacks', - type: 'fungible-token', - balance: createMoney(balance, '', 0), - asset: { - contractId, - canTransfer: false, - contractAddress: address, - contractAssetName: assetName, - contractName, - decimals: 0, - hasMemo: false, - imageCanonicalUri: '', - name: '', - marketData: null, - symbol: '', - }, - }; -} - -export function convertFtBalancesToStacksFungibleTokenAssetBalanceType( - ftBalances: AccountBalanceResponseBigNumber['fungible_tokens'] -) { - return ( - Object.entries(ftBalances) - .map(([key, value]) => { - const balance = new BigNumber(value.balance); - return createStacksFtCryptoAssetBalanceTypeWrapper(balance, key); - }) - // Assets users have traded will persist in the api response - .filter(assetBalance => !assetBalance?.balance.amount.isEqualTo(0)) - ); -} - -export function addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance( - assetBalance: StacksFungibleTokenAssetBalance, - metadata: FtMetadataResponse, - marketData: MarketData | null -) { - return { - ...assetBalance, - balance: createMoney( - assetBalance.balance.amount, - metadata.symbol ?? '', - metadata.decimals ?? 0 - ), - asset: { - ...assetBalance.asset, - canTransfer: isTransferableStacksFungibleTokenAsset(assetBalance.asset), - decimals: metadata.decimals ?? 0, - hasMemo: isTransferableStacksFungibleTokenAsset(assetBalance.asset), - imageCanonicalUri: metadata.image_canonical_uri ?? '', - name: metadata.name ?? '', - marketData, - symbol: metadata.symbol ?? '', - }, - }; -} - -export function getStacksFungibleTokenCurrencyAssetBalance( - selectedAssetBalance?: StacksCryptoCurrencyAssetBalance | StacksFungibleTokenAssetBalance -) { - return selectedAssetBalance?.type === 'fungible-token' && selectedAssetBalance.asset.canTransfer - ? selectedAssetBalance - : undefined; -} diff --git a/src/app/query/stacks/bns/bns.hooks.ts b/src/app/query/stacks/bns/bns.hooks.ts index d0aea0e3fc6..1e428ee51cf 100644 --- a/src/app/query/stacks/bns/bns.hooks.ts +++ b/src/app/query/stacks/bns/bns.hooks.ts @@ -1,11 +1,11 @@ import { logger } from '@shared/logger'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useGetBnsNamesOwnedByAddress } from './bns.query'; export function useCurrentAccountNames() { - const principal = useCurrentAccountStxAddressState(); + const principal = useCurrentStacksAccountAddress(); return useGetBnsNamesOwnedByAddress(principal, { select: resp => { if (principal === '') logger.error('No principal defined'); diff --git a/src/app/query/stacks/mempool/mempool.hooks.ts b/src/app/query/stacks/mempool/mempool.hooks.ts index 5e28aaf8138..947e9050cbb 100644 --- a/src/app/query/stacks/mempool/mempool.hooks.ts +++ b/src/app/query/stacks/mempool/mempool.hooks.ts @@ -1,21 +1,16 @@ import { useMemo } from 'react'; -import { - MempoolTokenTransferTransaction, - MempoolTransaction, -} from '@stacks/stacks-blockchain-api-types'; -import BigNumber from 'bignumber.js'; - -import { createMoney } from '@shared/models/money.model'; +import { MempoolTransaction } from '@stacks/stacks-blockchain-api-types'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { increaseValueByOneMicroStx } from '@app/common/math/helpers'; import { microStxToStx } from '@app/common/money/unit-conversion'; import { useTransactionsById } from '@app/query/stacks/transactions/transactions-by-id.query'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useStacksConfirmedTransactions } from '../transactions/transactions-with-transfers.hooks'; import { useAccountMempoolQuery } from './mempool.query'; +import { calculatePendingTxsMoneyBalance } from './mempool.utils'; const droppedCache = new Map(); @@ -50,7 +45,7 @@ function useAccountMempoolTransactions(address: string) { } export function useStacksPendingTransactions() { - const address = useCurrentAccountStxAddressState(); + const address = useCurrentStacksAccountAddress(); const { query, transactions } = useAccountMempoolTransactions(address ?? ''); return useMemo(() => { const nonEmptyTransactions = transactions.filter(tx => !!tx) as MempoolTransaction[]; @@ -59,55 +54,32 @@ export function useStacksPendingTransactions() { } export function useCurrentAccountMempool() { - const address = useCurrentAccountStxAddressState(); + const address = useCurrentStacksAccountAddress(); return useAccountMempoolQuery(address ?? ''); } -// TODO: Asset refactor: combine inbound/outbound into one function? export function useMempoolTxsInboundBalance(address: string) { const { transactions: pendingTransactions } = useStacksPendingTransactions(); const confirmedTxs = useStacksConfirmedTransactions(); - const pendingInboundTxs = pendingTransactions.filter(tx => { - if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) { - return false; - } - return tx.tx_type === 'token_transfer' && tx.token_transfer.recipient_address === address; - }) as unknown as MempoolTokenTransferTransaction[]; - - const tokenTransferTxsBalance = pendingInboundTxs.reduce( - (acc, tx) => acc.plus(tx.token_transfer.amount), - new BigNumber(0) - ); - const pendingTxsFeesBalance = pendingInboundTxs.reduce( - (acc, tx) => acc.plus(tx.fee_rate), - new BigNumber(0) - ); - - return createMoney(tokenTransferTxsBalance.plus(pendingTxsFeesBalance), 'STX'); + return calculatePendingTxsMoneyBalance({ + address, + confirmedTxs, + pendingTxs: pendingTransactions, + type: 'inbound', + }); } export function useMempoolTxsOutboundBalance(address: string) { const { transactions: pendingTransactions } = useStacksPendingTransactions(); const confirmedTxs = useStacksConfirmedTransactions(); - const pendingOutboundTxs = pendingTransactions.filter(tx => { - if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) { - return false; - } - return tx.tx_type === 'token_transfer' && tx.sender_address === address; - }) as unknown as MempoolTokenTransferTransaction[]; - - const tokenTransferTxsBalance = pendingOutboundTxs.reduce( - (acc, tx) => acc.plus(tx.token_transfer.amount), - new BigNumber(0) - ); - const pendingTxsFeesBalance = pendingOutboundTxs.reduce( - (acc, tx) => acc.plus(tx.fee_rate), - new BigNumber(0) - ); - - return createMoney(tokenTransferTxsBalance.plus(pendingTxsFeesBalance), 'STX'); + return calculatePendingTxsMoneyBalance({ + address, + confirmedTxs, + pendingTxs: pendingTransactions, + type: 'outbound', + }); } export function useStacksValidateFeeByNonce() { diff --git a/src/app/query/stacks/mempool/mempool.utils.ts b/src/app/query/stacks/mempool/mempool.utils.ts new file mode 100644 index 00000000000..c2c301a1a95 --- /dev/null +++ b/src/app/query/stacks/mempool/mempool.utils.ts @@ -0,0 +1,62 @@ +import type { + MempoolTokenTransferTransaction, + MempoolTransaction, + Transaction, +} from '@stacks/stacks-blockchain-api-types'; + +import { createMoney } from '@shared/models/money.model'; + +import { sumNumbers } from '@app/common/math/helpers'; + +type PendingTransactionType = 'inbound' | 'outbound'; + +function getInboundPendingTxs( + address: string, + confirmedTxs: Transaction[], + pendingTxs: MempoolTransaction[] +) { + return pendingTxs.filter(tx => { + if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) { + return false; + } + return tx.tx_type === 'token_transfer' && tx.token_transfer.recipient_address === address; + }) as unknown as MempoolTokenTransferTransaction[]; +} + +function getOutboundPendingTxs( + address: string, + confirmedTxs: Transaction[], + pendingTxs: MempoolTransaction[] +) { + return pendingTxs.filter(tx => { + if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) { + return false; + } + return tx.tx_type === 'token_transfer' && tx.sender_address === address; + }) as unknown as MempoolTokenTransferTransaction[]; +} + +interface CalculatePendingTxsMoneyBalanceArgs { + address: string; + confirmedTxs: Transaction[]; + pendingTxs: MempoolTransaction[]; + type: PendingTransactionType; +} +export function calculatePendingTxsMoneyBalance({ + address, + confirmedTxs, + pendingTxs, + type, +}: CalculatePendingTxsMoneyBalanceArgs) { + const filteredPendingTxs = + type === 'inbound' + ? getInboundPendingTxs(address, confirmedTxs, pendingTxs) + : getOutboundPendingTxs(address, confirmedTxs, pendingTxs); + + const tokenTransferTxsBalance = sumNumbers( + filteredPendingTxs.map(tx => Number(tx.token_transfer.amount)) + ); + const pendingTxsFeesBalance = sumNumbers(filteredPendingTxs.map(tx => Number(tx.fee_rate))); + + return createMoney(tokenTransferTxsBalance.plus(pendingTxsFeesBalance), 'STX'); +} diff --git a/src/app/query/stacks/nonce/account-nonces.query.ts b/src/app/query/stacks/nonce/account-nonces.query.ts index e4ca85d3fd7..a26bb36490b 100644 --- a/src/app/query/stacks/nonce/account-nonces.query.ts +++ b/src/app/query/stacks/nonce/account-nonces.query.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import PQueue from 'p-queue'; import { AppUseQueryConfig } from '@app/query/query-config'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; @@ -36,7 +36,7 @@ type FetchAccountNoncesResp = Awaited( options?: AppUseQueryConfig ) { - const principal = useCurrentAccountStxAddressState(); + const principal = useCurrentStacksAccountAddress(); const network = useCurrentNetworkState(); const client = useStacksClient(); const limiter = useHiroApiRateLimiter(); diff --git a/src/app/query/stacks/sip10/sip10-tokens.hooks.ts b/src/app/query/stacks/sip10/sip10-tokens.hooks.ts new file mode 100644 index 00000000000..4ced7f76589 --- /dev/null +++ b/src/app/query/stacks/sip10/sip10-tokens.hooks.ts @@ -0,0 +1,124 @@ +import { useMemo } from 'react'; + +import type { FtMetadataResponse } from '@hirosystems/token-metadata-api-client'; +import type { Sip10CryptoAssetInfo } from '@leather-wallet/models'; +import BigNumber from 'bignumber.js'; + +import { createMoney } from '@shared/models/money.model'; +import { isDefined, isUndefined } from '@shared/utils'; + +import { getTicker, pullContractIdFromIdentity } from '@app/common/utils'; +import { + useAlexCurrencyPriceAsMarketData, + useAlexSwappableAssets, +} from '@app/query/common/alex-sdk/alex-sdk.hooks'; +import { + type AccountCryptoAssetWithDetails, + type Sip10AccountCryptoAssetWithDetails, + createAccountCryptoAssetWithDetailsFactory, +} from '@app/query/models/crypto-asset.model'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { getAssetStringParts } from '@app/ui/utils/get-asset-string-parts'; + +import { useStacksAccountBalanceFungibleTokens } from '../balance/account-balance.hooks'; +import { useStacksAccountFungibleTokenMetadata } from '../token-metadata/fungible-tokens/fungible-token-metadata.hooks'; +import { isFtAsset } from '../token-metadata/token-metadata.utils'; +import { + type Sip10CryptoAssetFilter, + filterSip10AccountCryptoAssetsWithDetails, +} from './sip10-tokens.utils'; + +export function isTransferableStacksFungibleTokenAsset(asset: Partial) { + return !isUndefined(asset.decimals) && !isUndefined(asset.name) && !isUndefined(asset.symbol); +} + +export function getSip10InfoFromAsset(asset: AccountCryptoAssetWithDetails) { + if ('contractId' in asset.info) return asset.info; + return; +} + +function createSip10CryptoAssetInfo( + contractId: string, + key: string, + token: FtMetadataResponse +): Sip10CryptoAssetInfo { + const { assetName, contractName } = getAssetStringParts(key); + const name = token.name ? token.name : assetName; + + return { + canTransfer: isTransferableStacksFungibleTokenAsset(token), + contractId, + contractName, + decimals: token.decimals ?? 0, + hasMemo: isTransferableStacksFungibleTokenAsset(token), + imageCanonicalUri: token.image_canonical_uri ?? '', + name, + symbol: token.symbol ?? getTicker(name), + }; +} + +function useSip10AccountCryptoAssetsWithDetails(address: string) { + const { data: tokens = {} } = useStacksAccountBalanceFungibleTokens(address); + const tokenMetadata = useStacksAccountFungibleTokenMetadata(tokens); + const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); + + return useMemo( + () => + Object.entries(tokens) + .map(([key, value], i) => { + const token = tokenMetadata[i].data; + if (!(token && isFtAsset(token))) return; + const contractId = pullContractIdFromIdentity(key); + + return createAccountCryptoAssetWithDetailsFactory({ + balance: { + availableBalance: createMoney( + new BigNumber(value.balance), + token.symbol ?? getTicker(token.name ?? ''), + token.decimals ?? 0 + ), + }, + chain: 'stacks', + info: createSip10CryptoAssetInfo(contractId, key, token), + marketData: priceAsMarketData(contractId, token.symbol), + type: 'sip-10', + }); + }) + .filter(isDefined) + .filter(asset => asset.balance.availableBalance.amount.isGreaterThan(0)), + [priceAsMarketData, tokenMetadata, tokens] + ); +} + +export function useSip10CryptoAssetWithDetails(contractId: string) { + const address = useCurrentStacksAccountAddress(); + const assets = useSip10AccountCryptoAssetsWithDetails(address); + return useMemo( + () => assets.find(asset => asset.info.contractId === contractId), + [assets, contractId] + ); +} + +interface UseFilteredSip10AccountCryptoAssetsWithDetailsArgs { + address: string; + filter?: Sip10CryptoAssetFilter; +} +export function useFilteredSip10AccountCryptoAssetsWithDetails({ + address, + filter = 'all', +}: UseFilteredSip10AccountCryptoAssetsWithDetailsArgs) { + const assets = useSip10AccountCryptoAssetsWithDetails(address); + const { data: swapAssets = [] } = useAlexSwappableAssets(); + + return useMemo( + () => filterSip10AccountCryptoAssetsWithDetails(assets, swapAssets, filter), + [assets, swapAssets, filter] + ); +} + +export function useTransferableSip10CryptoAssetsWithDetails( + address: string +): Sip10AccountCryptoAssetWithDetails[] { + const assets = useSip10AccountCryptoAssetsWithDetails(address); + return useMemo(() => assets.filter(asset => asset.info.canTransfer), [assets]); +} diff --git a/src/app/query/stacks/sip10/sip10-tokens.spec.ts b/src/app/query/stacks/sip10/sip10-tokens.spec.ts new file mode 100644 index 00000000000..c801dc7a847 --- /dev/null +++ b/src/app/query/stacks/sip10/sip10-tokens.spec.ts @@ -0,0 +1,32 @@ +import type { FtMetadataResponse } from '@hirosystems/token-metadata-api-client'; + +import { isTransferableStacksFungibleTokenAsset } from './sip10-tokens.hooks'; + +describe(isTransferableStacksFungibleTokenAsset.name, () => { + test('assets with a name, symbol and decimals are allowed to be transferred', () => { + const asset: Partial = { + decimals: 9, + name: 'SteLLa the Cat', + symbol: 'CAT', + }; + expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); + }); + + test('a token with no decimals is transferable', () => { + const asset: Partial = { + decimals: 0, + name: 'SteLLa the Cat', + symbol: 'CAT', + }; + expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); + }); + + test('assets missing either name, symbol or decimals may not be transferred', () => { + const asset: Partial = { + name: 'Test token', + symbol: 'TEST', + decimals: undefined, + }; + expect(isTransferableStacksFungibleTokenAsset(asset)).toBeFalsy(); + }); +}); diff --git a/src/app/query/stacks/sip10/sip10-tokens.utils.ts b/src/app/query/stacks/sip10/sip10-tokens.utils.ts new file mode 100644 index 00000000000..015f5154f60 --- /dev/null +++ b/src/app/query/stacks/sip10/sip10-tokens.utils.ts @@ -0,0 +1,22 @@ +import type { SwapAsset } from '@app/query/common/alex-sdk/alex-sdk.hooks'; +import type { Sip10AccountCryptoAssetWithDetails } from '@app/query/models/crypto-asset.model'; +import { getAssetStringParts } from '@app/ui/utils/get-asset-string-parts'; + +export type Sip10CryptoAssetFilter = 'all' | 'supported' | 'unsupported'; + +export function filterSip10AccountCryptoAssetsWithDetails( + assets: Sip10AccountCryptoAssetWithDetails[], + swapAssets: SwapAsset[], + filter: Sip10CryptoAssetFilter +) { + return assets.filter(asset => { + const { address: contractAddress } = getAssetStringParts(asset.info.contractId); + if (filter === 'supported') { + return swapAssets.some(swapAsset => swapAsset.principal.includes(contractAddress)); + } + if (filter === 'unsupported') { + return !swapAssets.some(swapAsset => swapAsset.principal.includes(contractAddress)); + } + return assets; + }); +} diff --git a/src/app/query/stacks/stx/stx-crypto-asset.hooks.ts b/src/app/query/stacks/stx/stx-crypto-asset.hooks.ts new file mode 100644 index 00000000000..6c89c853988 --- /dev/null +++ b/src/app/query/stacks/stx/stx-crypto-asset.hooks.ts @@ -0,0 +1,58 @@ +import type { StxCryptoAssetInfo } from '@leather-wallet/models'; + +import { STX_DECIMALS } from '@shared/constants'; +import { createMoney } from '@shared/models/money.model'; +import { isUndefined } from '@shared/utils'; + +import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; +import { + type StxAccountCryptoAssetWithDetails, + createAccountCryptoAssetWithDetailsFactory, +} from '@app/query/models/crypto-asset.model'; + +import { useStxCryptoAssetBalance } from '../balance/account-balance.hooks'; + +const stxCryptoAssetInfo: StxCryptoAssetInfo = { + decimals: STX_DECIMALS, + hasMemo: true, + name: 'stacks', + symbol: 'STX', +}; + +const stxCryptoAssetBalancePlaceholder = { + availableBalance: createMoney(0, 'STX'), + availableUnlockedBalance: createMoney(0, 'STX'), + inboundBalance: createMoney(0, 'STX'), + lockedBalance: createMoney(0, 'STX'), + outboundBalance: createMoney(0, 'STX'), + pendingBalance: createMoney(0, 'STX'), + totalBalance: createMoney(0, 'STX'), + unlockedBalance: createMoney(0, 'STX'), +}; + +export const stxCryptoAssetPlaceholder = + createAccountCryptoAssetWithDetailsFactory({ + balance: stxCryptoAssetBalancePlaceholder, + chain: 'stacks', + info: stxCryptoAssetInfo, + marketData: null, + type: 'stx', + }); + +export function useStxAccountCryptoAssetWithDetails(address: string) { + const { data: balance, isInitialLoading } = useStxCryptoAssetBalance(address); + const marketData = useCryptoCurrencyMarketDataMeanAverage('STX'); + + if (isUndefined(balance)) return { asset: stxCryptoAssetPlaceholder, isInitialLoading }; + + return { + asset: createAccountCryptoAssetWithDetailsFactory({ + balance, + chain: 'stacks', + info: stxCryptoAssetInfo, + marketData, + type: 'stx', + }), + isInitialLoading, + }; +} diff --git a/src/app/query/stacks/stx20/stx20-tokens.hooks.ts b/src/app/query/stacks/stx20/stx20-tokens.hooks.ts index f3394d4a650..ce3231a5b8b 100644 --- a/src/app/query/stacks/stx20/stx20-tokens.hooks.ts +++ b/src/app/query/stacks/stx20/stx20-tokens.hooks.ts @@ -5,7 +5,7 @@ import { createMoney } from '@shared/models/money.model'; import type { Stx20Balance, Stx20Token } from '../stacks-client'; import { useStx20BalancesQuery } from './stx20-tokens.query'; -function makeStx20Token(token: Stx20Balance): Stx20Token { +function createStx20Token(token: Stx20Balance): Stx20Token { return { balance: createMoney(new BigNumber(token.balance), token.ticker, 0), marketData: null, @@ -15,6 +15,6 @@ function makeStx20Token(token: Stx20Balance): Stx20Token { export function useStx20Tokens(address: string) { return useStx20BalancesQuery(address, { - select: resp => resp.map(balance => makeStx20Token(balance)), + select: resp => resp.map(balance => createStx20Token(balance)), }); } diff --git a/src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts b/src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts new file mode 100644 index 00000000000..ee8966d866c --- /dev/null +++ b/src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts @@ -0,0 +1,12 @@ +import type { AddressBalanceResponse } from '@shared/models/account.model'; + +import { pullContractIdFromIdentity } from '@app/common/utils'; + +import { useGetFungibleTokenMetadataListQuery } from './fungible-token-metadata.query'; + +export function useStacksAccountFungibleTokenMetadata( + tokens: AddressBalanceResponse['fungible_tokens'] +) { + const tokenContractIds = Object.keys(tokens).map(key => pullContractIdFromIdentity(key)); + return useGetFungibleTokenMetadataListQuery(tokenContractIds); +} diff --git a/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts b/src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.query.ts similarity index 100% rename from src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts rename to src/app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.query.ts diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts b/src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-holdings.query.ts similarity index 100% rename from src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts rename to src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-holdings.query.ts diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.hooks.ts b/src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-metadata.hooks.ts similarity index 100% rename from src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.hooks.ts rename to src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-metadata.hooks.ts diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts b/src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-metadata.query.ts similarity index 100% rename from src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts rename to src/app/query/stacks/token-metadata/non-fungible-tokens/non-fungible-token-metadata.query.ts diff --git a/src/app/query/stacks/tokens/token-metadata.utils.ts b/src/app/query/stacks/token-metadata/token-metadata.utils.ts similarity index 100% rename from src/app/query/stacks/tokens/token-metadata.utils.ts rename to src/app/query/stacks/token-metadata/token-metadata.utils.ts diff --git a/src/app/query/stacks/transactions/transactions-with-transfers.query.ts b/src/app/query/stacks/transactions/transactions-with-transfers.query.ts index 73659faee96..a999bbdd02b 100644 --- a/src/app/query/stacks/transactions/transactions-with-transfers.query.ts +++ b/src/app/query/stacks/transactions/transactions-with-transfers.query.ts @@ -3,7 +3,7 @@ import { UseQueryOptions, UseQueryResult, useQuery } from '@tanstack/react-query import { DEFAULT_LIST_LIMIT } from '@shared/constants'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; @@ -18,7 +18,7 @@ const queryOptions: UseQueryOptions = { }; export function useGetAccountTransactionsWithTransfersQuery() { - const principal = useCurrentAccountStxAddressState(); + const principal = useCurrentStacksAccountAddress(); const { chain } = useCurrentNetworkState(); const client = useStacksClient(); const limiter = useHiroApiRateLimiter(); diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin.ledger.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin.ledger.ts index e1647fe0bbb..425caa9fbff 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin.ledger.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin.ledger.ts @@ -8,6 +8,8 @@ import { import { selectDefaultWalletBitcoinKeys } from '@app/store/ledger/bitcoin/bitcoin-key.slice'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; +// TODO: Asset refactor: remove if determined unnecessary +// ts-unused-exports:disable-next-line export function useHasBitcoinLedgerKeychain() { const network = useCurrentNetwork(); const accounts = useSelector(selectDefaultWalletBitcoinKeys); diff --git a/src/app/store/accounts/blockchain/stacks/stacks-account.hooks.ts b/src/app/store/accounts/blockchain/stacks/stacks-account.hooks.ts index 70af3b031fe..5e1cafc412f 100644 --- a/src/app/store/accounts/blockchain/stacks/stacks-account.hooks.ts +++ b/src/app/store/accounts/blockchain/stacks/stacks-account.hooks.ts @@ -17,6 +17,9 @@ export function useStacksAccounts() { return useAtomValue(stacksAccountState); } +// TODO: Refactor, we need to use conditional empty strings everywhere +// Can we remove these atoms? + // Comment below from original atom. This pattern encourages view level // implementation details to leak into the state structure. Do not do this. // This contains the state of the current account: @@ -38,7 +41,7 @@ export function useCurrentStacksAccount() { }, [accountIndex, accounts, hasSwitched, signatureIndex, txIndex]); } -export function useCurrentAccountStxAddressState() { +export function useCurrentStacksAccountAddress() { return useCurrentStacksAccount()?.address ?? ''; } diff --git a/src/app/store/accounts/blockchain/utils.ts b/src/app/store/accounts/blockchain/utils.ts index 01b497cbab3..e1f9ca75c53 100644 --- a/src/app/store/accounts/blockchain/utils.ts +++ b/src/app/store/accounts/blockchain/utils.ts @@ -1,18 +1,22 @@ import { useCallback } from 'react'; +import type { Blockchains } from '@leather-wallet/models'; + import { useHasCurrentBitcoinAccount } from './bitcoin/bitcoin.hooks'; import { useHasStacksLedgerKeychain } from './stacks/stacks.hooks'; +// TODO: Asset refactor: remove if determined unnecessary +// ts-unused-exports:disable-next-line export function useCheckLedgerBlockchainAvailable() { const hasBitcoinLedgerKeys = useHasCurrentBitcoinAccount(); const hasStacksLedgerKeys = useHasStacksLedgerKeychain(); return useCallback( - (symbol: string) => { - if (symbol === 'bitcoin') { + (chain: Blockchains) => { + if (chain === 'bitcoin') { return hasBitcoinLedgerKeys; } - if (symbol === 'stacks') { + if (chain === 'stacks') { return hasStacksLedgerKeys; } return false; diff --git a/src/app/store/transactions/post-conditions.hooks.ts b/src/app/store/transactions/post-conditions.hooks.ts index 15133fbd322..9162e91c76b 100644 --- a/src/app/store/transactions/post-conditions.hooks.ts +++ b/src/app/store/transactions/post-conditions.hooks.ts @@ -7,9 +7,9 @@ import { getPostCondition, handlePostConditions, } from '@app/common/transactions/stacks/post-condition.utils'; -import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query'; -import { isFtAsset } from '@app/query/stacks/tokens/token-metadata.utils'; -import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useGetFungibleTokenMetadataQuery } from '@app/query/stacks/token-metadata/fungible-tokens/fungible-token-metadata.query'; +import { isFtAsset } from '@app/query/stacks/token-metadata/token-metadata.utils'; +import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useTransactionRequestState } from './requests.hooks'; @@ -34,7 +34,7 @@ export function usePostConditionModeState() { export function usePostConditionState() { const payload = useTransactionRequestState(); - const address = useCurrentAccountStxAddressState(); + const address = useCurrentStacksAccountAddress(); return useMemo(() => formatPostConditionState(payload, address), [address, payload]); } diff --git a/src/app/store/transactions/token-transfer.hooks.ts b/src/app/store/transactions/token-transfer.hooks.ts index 792518f2574..e60c6138d6b 100644 --- a/src/app/store/transactions/token-transfer.hooks.ts +++ b/src/app/store/transactions/token-transfer.hooks.ts @@ -1,6 +1,7 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useAsync } from 'react-async-hook'; +import type { Sip10CryptoAssetInfo } from '@leather-wallet/models'; import { bytesToHex } from '@stacks/common'; import { TransactionTypes } from '@stacks/connect'; import { @@ -16,7 +17,6 @@ import { uintCV, } from '@stacks/transactions'; -import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asset-balance.model'; import type { StacksSendFormValues, StacksTransactionFormValues } from '@shared/models/form.model'; import { stxToMicroStx } from '@app/common/money/unit-conversion'; @@ -25,33 +25,13 @@ import { GenerateUnsignedTransactionOptions, generateUnsignedTransaction, } from '@app/common/transactions/stacks/generate-unsigned-txs'; -import { getStacksFungibleTokenCurrencyAssetBalance } from '@app/query/stacks/balance/stacks-ft-balances.utils'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; import { useCurrentStacksNetworkState } from '@app/store/networks/networks.hooks'; import { makePostCondition } from '@app/store/transactions/transaction.hooks'; +import { getAssetStringParts } from '@app/ui/utils/get-asset-string-parts'; import { useCurrentStacksAccount } from '../accounts/blockchain/stacks/stacks-account.hooks'; -function useMakeFungibleTokenTransfer(assetBalance?: StacksFungibleTokenAssetBalance) { - const currentAccount = useCurrentStacksAccount(); - const network = useCurrentStacksNetworkState(); - - return useMemo(() => { - if (assetBalance && currentAccount && currentAccount.address) { - const { contractAddress, contractAssetName, contractName } = assetBalance.asset; - return { - assetBalance, - contractAssetName, - contractAddress, - contractName, - network, - stxAddress: currentAccount.address, - }; - } - return; - }, [assetBalance, currentAccount, network]); -} - export function useGenerateStxTokenTransferUnsignedTx() { const { data: nextNonce } = useNextNonce(); const network = useCurrentStacksNetworkState(); @@ -99,29 +79,16 @@ export function useStxTokenTransferUnsignedTxState(values?: StacksSendFormValues return tx.result; } -export function useGenerateFtTokenTransferUnsignedTx( - assetBalance: StacksFungibleTokenAssetBalance -) { +export function useGenerateFtTokenTransferUnsignedTx(assetInfo: Sip10CryptoAssetInfo) { const { data: nextNonce } = useNextNonce(); const account = useCurrentStacksAccount(); - - const tokenCurrencyAssetBalance = getStacksFungibleTokenCurrencyAssetBalance(assetBalance); - const assetTransferState = useMakeFungibleTokenTransfer(tokenCurrencyAssetBalance); + const network = useCurrentStacksNetworkState(); return useCallback( async (values?: StacksSendFormValues | StacksTransactionFormValues) => { - if (!assetTransferState || !account) return; - const { - assetBalance, - network, - contractAddress, - contractAssetName, - contractName, - stxAddress, - } = assetTransferState; + if (!account) return; const functionName = 'transfer'; - const recipient = values && 'recipient' in values ? createAddress(values.recipient || '') @@ -132,29 +99,31 @@ export function useGenerateFtTokenTransferUnsignedTx( ? someCV(bufferCVFromString(values.memo || '')) : noneCV(); - const realAmount = - assetBalance.type === 'fungible-token' - ? ftUnshiftDecimals(amount, assetBalance.asset.decimals || 0) - : amount; + const amountAsFractionalUnit = ftUnshiftDecimals(amount, assetInfo.decimals || 0); + const { + address: contractAddress, + contractName, + assetName, + } = getAssetStringParts(assetInfo.contractId); const postConditionOptions = { - amount: realAmount, + amount: amountAsFractionalUnit, contractAddress, - contractAssetName, + contractAssetName: assetName, contractName, - stxAddress, + stxAddress: account.address, }; const postConditions = [makePostCondition(postConditionOptions)]; // (transfer (uint principal principal) (response bool uint)) const functionArgs: ClarityValue[] = [ - uintCV(realAmount), - standardPrincipalCVFromAddress(createAddress(stxAddress)), + uintCV(amountAsFractionalUnit), + standardPrincipalCVFromAddress(createAddress(account.address)), standardPrincipalCVFromAddress(recipient), ]; - if (assetBalance.asset.hasMemo) { + if (assetInfo.hasMemo) { functionArgs.push(memo); } @@ -177,13 +146,20 @@ export function useGenerateFtTokenTransferUnsignedTx( return generateUnsignedTransaction(options); }, - [account, assetTransferState, nextNonce] + [ + account, + assetInfo.contractId, + assetInfo.decimals, + assetInfo.hasMemo, + network, + nextNonce?.nonce, + ] ); } -export function useFtTokenTransferUnsignedTx(assetBalance: StacksFungibleTokenAssetBalance) { - const generateTx = useGenerateFtTokenTransferUnsignedTx(assetBalance); +export function useFtTokenTransferUnsignedTx(assetInfo: Sip10CryptoAssetInfo) { const account = useCurrentStacksAccount(); + const generateTx = useGenerateFtTokenTransferUnsignedTx(assetInfo); const tx = useAsync(async () => generateTx(), [account]); return tx.result; diff --git a/src/app/store/transactions/transaction.hooks.ts b/src/app/store/transactions/transaction.hooks.ts index 245ef93f4e1..a2eaa416684 100644 --- a/src/app/store/transactions/transaction.hooks.ts +++ b/src/app/store/transactions/transaction.hooks.ts @@ -30,8 +30,8 @@ import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigat import { useToast } from '@app/features/toasts/use-toast'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; import { - useCurrentAccountStxAddressState, useCurrentStacksAccount, + useCurrentStacksAccountAddress, } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useCurrentStacksNetworkState } from '@app/store/networks/networks.hooks'; @@ -45,7 +45,7 @@ export function useTransactionPostConditions() { export function useUnsignedStacksTransactionBaseState() { const network = useCurrentStacksNetworkState(); const { data: nextNonce } = useNextNonce(); - const stxAddress = useCurrentAccountStxAddressState(); + const stxAddress = useCurrentStacksAccountAddress(); const payload = useTransactionRequestState(); const postConditions = useTransactionPostConditions(); const account = useCurrentStacksAccount(); diff --git a/src/shared/models/account.model.ts b/src/shared/models/account.model.ts index b3b5231ed87..96f12d39e57 100644 --- a/src/shared/models/account.model.ts +++ b/src/shared/models/account.model.ts @@ -1,7 +1,5 @@ import { AddressTokenOfferingLocked } from '@stacks/stacks-blockchain-api-types/generated'; -import type { Money } from './money.model'; - type SelectedKeys = | 'balance' | 'total_sent' @@ -47,17 +45,3 @@ export interface AddressBalanceResponse { >; token_offering_locked?: AddressTokenOfferingLocked; } - -export interface AccountStxBalanceBigNumber - extends Omit { - balance: Money; - total_sent: Money; - total_received: Money; - total_fees_sent: Money; - total_miner_rewards_received: Money; - locked: Money; -} - -export interface AccountBalanceResponseBigNumber extends Omit { - stx: AccountStxBalanceBigNumber; -} diff --git a/src/shared/models/crypto-asset-balance.model.ts b/src/shared/models/crypto-asset-balance.model.ts deleted file mode 100644 index e4274cbeb7c..00000000000 --- a/src/shared/models/crypto-asset-balance.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import BigNumber from 'bignumber.js'; - -import { - BitcoinCryptoCurrencyAsset, - StacksCryptoCurrencyAsset, - StacksFungibleTokenAsset, - StacksNonFungibleTokenAsset, -} from './crypto-asset.model'; -import { Money } from './money.model'; - -export interface BitcoinCryptoCurrencyAssetBalance { - readonly blockchain: 'bitcoin'; - readonly type: 'crypto-currency'; - readonly asset: BitcoinCryptoCurrencyAsset; - readonly balance: Money; -} - -export interface StacksCryptoCurrencyAssetBalance { - readonly blockchain: 'stacks'; - readonly type: 'crypto-currency'; - readonly asset: StacksCryptoCurrencyAsset; - readonly balance: Money; -} - -export interface StacksFungibleTokenAssetBalance { - readonly blockchain: 'stacks'; - readonly type: 'fungible-token'; - readonly asset: StacksFungibleTokenAsset; - readonly balance: Money; -} - -// ts-unused-exports:disable-next-line -export interface StacksNonFungibleTokenAssetBalance { - readonly blockchain: 'stacks'; - readonly type: 'non-fungible-token'; - readonly asset: StacksNonFungibleTokenAsset; - readonly count: BigNumber; -} - -export type AllCryptoCurrencyAssetBalances = - | BitcoinCryptoCurrencyAssetBalance - | StacksCryptoCurrencyAssetBalance; - -export type AllTransferableCryptoAssetBalances = - | AllCryptoCurrencyAssetBalances - | StacksFungibleTokenAssetBalance; diff --git a/src/shared/models/crypto-asset.model.ts b/src/shared/models/crypto-asset.model.ts deleted file mode 100644 index 78f75dc8adb..00000000000 --- a/src/shared/models/crypto-asset.model.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { MarketData } from './market.model'; - -export interface BitcoinCryptoCurrencyAsset { - decimals: number; - hasMemo: boolean; - name: string; - symbol: 'BTC'; -} - -export interface StacksCryptoCurrencyAsset { - decimals: number; - hasMemo: boolean; - name: string; - symbol: 'STX'; -} - -export interface StacksFungibleTokenAsset { - canTransfer: boolean; - contractId: string; - contractAddress: string; - contractAssetName: string; - contractName: string; - decimals: number; - hasMemo: boolean; - imageCanonicalUri: string; - name: string; - marketData: MarketData | null; - symbol: string; -} - -export interface StacksNonFungibleTokenAsset { - contractAddress: string; - contractAssetName: string; - contractName: string; - imageCanonicalUri: string; - name: string; -} diff --git a/tests/selectors/home.selectors.ts b/tests/selectors/home.selectors.ts index ab42923c2b9..ea28f10278d 100644 --- a/tests/selectors/home.selectors.ts +++ b/tests/selectors/home.selectors.ts @@ -1,4 +1,5 @@ export enum HomePageSelectors { + AssetList = 'asset-list', HomePageContainer = 'home-page-container', ReceiveCryptoAssetBtn = 'receive-crypto-asset-btn', ReceiveBtcNativeSegwitQrCodeBtn = 'receive-native-segwit-qr-code-btn', @@ -11,5 +12,4 @@ export enum HomePageSelectors { BalancesTabBtn = 'tab-balances', SwapBtn = 'swap-btn', FundAccountBtn = 'fund-account-btn', - BalancesList = 'balances-list', } diff --git a/tests/specs/rpc-get-addresses/get-addresses.spec.ts b/tests/specs/rpc-get-addresses/get-addresses.spec.ts index 48b13fc6b15..ac646f86d32 100644 --- a/tests/specs/rpc-get-addresses/get-addresses.spec.ts +++ b/tests/specs/rpc-get-addresses/get-addresses.spec.ts @@ -1,4 +1,3 @@ -import '@leather-wallet/types'; import type { BrowserContext, Page } from '@playwright/test'; import { TEST_PASSWORD } from '@tests/mocks/constants'; import { makeLedgerTestAccountWalletState } from '@tests/page-object-models/onboarding.page';