From 7478945d03cc56457a4bc452bac6a9daf22f59b4 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Fri, 12 Apr 2024 12:08:21 -0500 Subject: [PATCH] refactor: price as market data --- src/app/common/asset-utils.ts | 39 +++++++ .../stacks-crypto-asset.utils.spec.ts | 5 +- src/app/common/hooks/use-brc20-tokens.ts | 11 -- src/app/common/money/calculate-money.ts | 2 +- src/app/common/money/fiat-conversion.ts | 18 --- .../money/{is-money.ts => money.utils.ts} | 7 ++ src/app/common/utils/sort-assets-by-symbol.ts | 23 ---- .../hooks/use-bitcoin-custom-fee.tsx | 2 +- ...e-bitcoin-fees-list-multiple-recipients.ts | 4 +- .../brc20-token-asset-item.layout.tsx | 23 +--- .../brc20-token-asset-list.tsx | 4 +- .../src20-token-asset-list.tsx | 4 +- .../fungible-token-asset.utils.ts | 23 +--- .../loaders/brc20-tokens-loader.tsx | 2 +- .../receive/components/receive-tokens.tsx | 18 ++- .../components/send-fiat-value.tsx | 11 +- .../form/brc-20/brc-20-choose-fee.tsx | 2 +- .../brc-20/brc20-send-form-confirmation.tsx | 2 +- .../form/brc-20/brc20-send-form.tsx | 38 ++++--- .../form/brc-20/use-brc20-send-form.tsx | 24 +--- .../sip10-token-send-form-container.tsx | 8 ++ .../form/stacks-sip10/use-sip10-send-form.tsx | 2 + src/app/pages/swap/alex-swap-container.tsx | 34 ++---- .../components/swap-asset-item.tsx | 16 +-- .../components/swap-asset-list.tsx | 37 ++++--- .../components/swap-amount-field.tsx | 4 +- .../components/swap-toggle-button.tsx | 19 ++-- .../swap-asset-select-base.tsx | 6 +- .../swap-asset-select-quote.tsx | 2 +- src/app/pages/swap/hooks/use-alex-swap.tsx | 76 ++----------- src/app/pages/swap/hooks/use-swap-form.tsx | 16 +-- src/app/pages/swap/swap.context.ts | 6 +- src/app/pages/swap/swap.tsx | 14 ++- src/app/pages/swap/swap.utils.ts | 44 +------- src/app/query/bitcoin/bitcoin-client.ts | 59 ++++------ .../{use-brc-20.ts => brc20-tokens.hooks.ts} | 28 +++++ .../ordinals/brc20/brc20-tokens.query.ts | 9 +- ...ery.ts => alex-sdk-latest-prices.query.ts} | 0 .../alex-sdk-swappable-currency.query.ts | 25 +++++ .../query/common/alex-sdk/alex-sdk.hooks.ts | 103 ++++++++++++++++-- .../alex-sdk/swappable-currency.query.ts | 13 --- .../balance/stacks-ft-balances.hooks.ts | 9 +- .../balance/stacks-ft-balances.utils.ts | 9 +- src/shared/models/crypto-asset.model.ts | 4 +- 44 files changed, 388 insertions(+), 417 deletions(-) create mode 100644 src/app/common/asset-utils.ts delete mode 100644 src/app/common/hooks/use-brc20-tokens.ts delete mode 100644 src/app/common/money/fiat-conversion.ts rename src/app/common/money/{is-money.ts => money.utils.ts} (54%) delete mode 100644 src/app/common/utils/sort-assets-by-symbol.ts rename src/app/query/bitcoin/ordinals/brc20/{use-brc-20.ts => brc20-tokens.hooks.ts} (71%) rename src/app/query/common/alex-sdk/{latest-prices.query.ts => alex-sdk-latest-prices.query.ts} (100%) create mode 100644 src/app/query/common/alex-sdk/alex-sdk-swappable-currency.query.ts delete mode 100644 src/app/query/common/alex-sdk/swappable-currency.query.ts diff --git a/src/app/common/asset-utils.ts b/src/app/common/asset-utils.ts new file mode 100644 index 00000000000..12a2534e89d --- /dev/null +++ b/src/app/common/asset-utils.ts @@ -0,0 +1,39 @@ +import type { MarketData } from '@shared/models/market.model'; +import type { Money } from '@shared/models/money.model'; + +import { baseCurrencyAmountInQuote } from './money/calculate-money'; +import { i18nFormatCurrency } from './money/format-money'; +import { isMoneyGreaterThanZero } from './money/money.utils'; + +export function sortAssetsByName(assets: T) { + return assets + .sort((a, b) => { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }) + .sort((a, b) => { + if (a.name === 'STX') return -1; + if (b.name !== 'STX') return 1; + return 0; + }) + .sort((a, b) => { + if (a.name === 'BTC') return -1; + if (b.name !== 'BTC') return 1; + return 0; + }); +} + +export function migratePositiveAssetBalancesToTop(assets: T) { + const assetsWithPositiveBalance = assets.filter(asset => asset.balance.amount.isGreaterThan(0)); + const assetsWithZeroBalance = assets.filter(asset => asset.balance.amount.isEqualTo(0)); + return [...assetsWithPositiveBalance, ...assetsWithZeroBalance] as T; +} + +export function convertAssetBalanceToFiat< + T extends { balance: Money | null; marketData: MarketData | null }, +>(asset: T) { + if (!asset.marketData || !asset.balance || !isMoneyGreaterThanZero(asset.marketData.price)) + return ''; + return i18nFormatCurrency(baseCurrencyAmountInQuote(asset.balance, asset.marketData)); +} 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 index ae948f06fa1..d8812ce8e34 100644 --- a/src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts +++ b/src/app/common/crypto-assets/stacks-crypto-asset.utils.spec.ts @@ -1,5 +1,4 @@ import { StacksFungibleTokenAsset } from '@shared/models/crypto-asset.model'; -import { createMoney } from '@shared/models/money.model'; import { isFtNameLikeStx, @@ -32,7 +31,7 @@ describe(isTransferableStacksFungibleTokenAsset.name, () => { canTransfer: true, hasMemo: true, imageCanonicalUri: '', - price: createMoney(0, 'USD'), + marketData: null, symbol: 'CAT', }; expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); @@ -49,7 +48,7 @@ describe(isTransferableStacksFungibleTokenAsset.name, () => { canTransfer: true, hasMemo: true, imageCanonicalUri: '', - price: createMoney(0, 'USD'), + marketData: null, symbol: 'CAT', }; expect(isTransferableStacksFungibleTokenAsset(asset)).toBeTruthy(); diff --git a/src/app/common/hooks/use-brc20-tokens.ts b/src/app/common/hooks/use-brc20-tokens.ts deleted file mode 100644 index b11a1c19a35..00000000000 --- a/src/app/common/hooks/use-brc20-tokens.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useGetBrc20TokensQuery } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; - -export function useBrc20Tokens() { - const { data: allBrc20TokensResponse } = useGetBrc20TokensQuery(); - const brc20Tokens = allBrc20TokensResponse?.pages - .flatMap(page => page.brc20Tokens) - .filter(token => token.length > 0) - .flatMap(token => token); - - return brc20Tokens ?? []; -} diff --git a/src/app/common/money/calculate-money.ts b/src/app/common/money/calculate-money.ts index bc332c82d70..fc44e6dee9a 100644 --- a/src/app/common/money/calculate-money.ts +++ b/src/app/common/money/calculate-money.ts @@ -6,7 +6,7 @@ import { isNumber } from '@shared/utils'; import { initBigNumber, sumNumbers } from '../math/helpers'; import { formatMoney } from './format-money'; -import { isMoney } from './is-money'; +import { isMoney } from './money.utils'; export function baseCurrencyAmountInQuote(quantity: Money, { pair, price }: MarketData) { if (quantity.symbol.toLowerCase() !== pair.base.toLowerCase()) diff --git a/src/app/common/money/fiat-conversion.ts b/src/app/common/money/fiat-conversion.ts deleted file mode 100644 index 0159e7aeffe..00000000000 --- a/src/app/common/money/fiat-conversion.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { CryptoCurrencies } from '@shared/models/currencies.model'; -import { createMarketData, createMarketPair } from '@shared/models/market.model'; -import { type Money } from '@shared/models/money.model'; - -import { baseCurrencyAmountInQuote } from './calculate-money'; - -export function checkIsMoneyAmountGreaterThanZero(money: Money) { - return !(money.amount.isNaN() || money.amount.isZero()); -} - -export function convertCryptoCurrencyMoneyToFiat( - currency: CryptoCurrencies, - price: Money, - money: Money -) { - const cryptoCurrencyMarketData = createMarketData(createMarketPair(currency, 'USD'), price); - return baseCurrencyAmountInQuote(money, cryptoCurrencyMarketData); -} diff --git a/src/app/common/money/is-money.ts b/src/app/common/money/money.utils.ts similarity index 54% rename from src/app/common/money/is-money.ts rename to src/app/common/money/money.utils.ts index 9afe70b80fd..7a83e86239a 100644 --- a/src/app/common/money/is-money.ts +++ b/src/app/common/money/money.utils.ts @@ -1,3 +1,5 @@ +import BigNumber from 'bignumber.js'; + import { Money } from '@shared/models/money.model'; import { isObject } from '@shared/utils'; @@ -5,3 +7,8 @@ export function isMoney(val: unknown): val is Money { if (!isObject(val)) return false; return 'amount' in val && 'symbol' in val && 'decimals' in val; } + +export function isMoneyGreaterThanZero(money: Money) { + if (!BigNumber.isBigNumber(money.amount)) return; + return !(money.amount.isNaN() || money.amount.isZero()); +} diff --git a/src/app/common/utils/sort-assets-by-symbol.ts b/src/app/common/utils/sort-assets-by-symbol.ts deleted file mode 100644 index 20e1baa3fa4..00000000000 --- a/src/app/common/utils/sort-assets-by-symbol.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface Asset { - name: string; - icon: string; -} - -export function sortAssetsBySymbol(assets: Asset[]) { - return assets - .sort((a, b) => { - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; - }) - .sort((a, b) => { - if (a.name === 'STX') return -1; - if (b.name !== 'STX') return 1; - return 0; - }) - .sort((a, b) => { - if (a.name === 'BTC') return -1; - if (b.name !== 'BTC') return 1; - return 0; - }); -} diff --git a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx index 2d4c757c11e..710f1e4feb3 100644 --- a/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx +++ b/src/app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee.tsx @@ -67,7 +67,7 @@ export function useBitcoinCustomFeeMultipleRecipients({ }: UseBitcoinCustomFeeArgsMultipleRecipients) { const { balance } = useCurrentNativeSegwitAddressBalance(); const { data: utxos = [] } = useCurrentNativeSegwitUtxos(); - const btcMarketData = useCryptoCurrencyMarketData('BTC'); + const btcMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); return useCallback( (feeRate: number) => { diff --git a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts index cab6be7e977..7532c67a224 100644 --- a/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts +++ b/src/app/components/bitcoin-fees-list/use-bitcoin-fees-list-multiple-recipients.ts @@ -13,7 +13,7 @@ import { } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks'; -import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks'; +import { useCryptoCurrencyMarketDataMeanAverage } from '@app/query/common/market-data/market-data.hooks'; import { FeesListItem } from './bitcoin-fees-list'; @@ -41,7 +41,7 @@ export function useBitcoinFeesListMultipleRecipients({ recipients, utxos, }: UseBitcoinFeesListArgs) { - const btcMarketData = useCryptoCurrencyMarketData('BTC'); + const btcMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC'); const { data: feeRates, isLoading } = useAverageBitcoinFeeRates(); const feesList: FeesListItem[] = useMemo(() => { 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 index 0ce58463fb0..73bc2db2e21 100644 --- 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 @@ -1,13 +1,7 @@ import { styled } from 'leather-styles/jsx'; -import { createMoney } from '@shared/models/money.model'; - +import { convertAssetBalanceToFiat } from '@app/common/asset-utils'; import { formatBalance } from '@app/common/format-balance'; -import { - checkIsMoneyAmountGreaterThanZero, - convertCryptoCurrencyMoneyToFiat, -} from '@app/common/money/fiat-conversion'; -import { i18nFormatCurrency } from '@app/common/money/format-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'; @@ -19,16 +13,9 @@ interface Brc20TokenAssetItemLayoutProps { onClick?(): void; } export function Brc20TokenAssetItemLayout({ onClick, token }: Brc20TokenAssetItemLayoutProps) { - const balanceAsMoney = createMoney(Number(token.overall_balance), token.ticker, token.decimals); - const balanceAsString = balanceAsMoney.amount.toString(); - const formattedBalance = formatBalance(balanceAsString); - const priceAsMoney = createMoney(token.min_listed_unit_price, 'USD'); - const showFiatBalance = checkIsMoneyAmountGreaterThanZero(priceAsMoney); - const balanceAsFiat = showFiatBalance - ? i18nFormatCurrency( - convertCryptoCurrencyMoneyToFiat(token.ticker, priceAsMoney, balanceAsMoney) - ) - : ''; + const balanceAsString = token.balance?.amount.toString(); + const formattedBalance = formatBalance(balanceAsString ?? '0'); + const balanceAsFiat = convertAssetBalanceToFiat(token); return ( @@ -39,7 +26,7 @@ export function Brc20TokenAssetItemLayout({ onClick, token }: Brc20TokenAssetIte titleRight={ 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 index 066739c9966..62046fc2291 100644 --- 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 @@ -25,9 +25,9 @@ export function Brc20TokenAssetList({ brc20Tokens, variant }: Brc20TokenAssetLis variant === 'send' && btcCryptoCurrencyAssetBalance.balance.amount.isGreaterThan(0); function navigateToBrc20SendForm(token: Brc20Token) { - const { ticker, available_balance, decimals, holderAddress } = token; + const { ticker, balance, holderAddress, marketData } = token; navigate(RouteUrls.SendBrc20SendForm.replace(':ticker', ticker), { - state: { balance: available_balance, ticker, decimals, holderAddress }, + state: { balance, ticker, holderAddress, marketData }, }); } diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx index 04a8983a483..022aec7a158 100644 --- a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx +++ b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx @@ -6,5 +6,7 @@ interface Src20TokenAssetListProps { src20Tokens: Src20Token[]; } export function Src20TokenAssetList({ src20Tokens }: Src20TokenAssetListProps) { - return src20Tokens.map(token => ); + return src20Tokens.map((token, i) => ( + + )); } 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 index ada3faf3059..78414d38f51 100644 --- 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 @@ -2,13 +2,9 @@ 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 { - checkIsMoneyAmountGreaterThanZero, - convertCryptoCurrencyMoneyToFiat, -} from '@app/common/money/fiat-conversion'; -import { i18nFormatCurrency } from '@app/common/money/format-money'; import { ftDecimals } from '@app/common/stacks-utils'; import { formatContractId, getTicker } from '@app/common/utils'; import { spamFilter } from '@app/common/utils/spam-filter'; @@ -33,19 +29,10 @@ export function parseStacksFungibleTokenAssetBalance( const imageCanonicalUri = getImageCanonicalUri(asset.imageCanonicalUri, asset.name); const caption = symbol || getTicker(friendlyName); const title = spamFilter(friendlyName); - - const showFiatBalance = - assetBalance.asset.price && checkIsMoneyAmountGreaterThanZero(assetBalance.asset.price); - const balanceAsFiat = showFiatBalance - ? assetBalance.asset.price && - i18nFormatCurrency( - convertCryptoCurrencyMoneyToFiat( - assetBalance.balance.symbol, - assetBalance.asset.price, - assetBalance.balance - ) - ) - : ''; + const balanceAsFiat = convertAssetBalanceToFiat({ + ...assetBalance.asset, + balance: assetBalance.balance, + }); return { amount, diff --git a/src/app/components/loaders/brc20-tokens-loader.tsx b/src/app/components/loaders/brc20-tokens-loader.tsx index ab57777495d..b5769cd74da 100644 --- a/src/app/components/loaders/brc20-tokens-loader.tsx +++ b/src/app/components/loaders/brc20-tokens-loader.tsx @@ -1,5 +1,5 @@ -import { useBrc20Tokens } from '@app/common/hooks/use-brc20-tokens'; import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; +import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; interface Brc20TokensLoaderProps { children(brc20Tokens: Brc20Token[]): React.ReactNode; diff --git a/src/app/pages/receive/components/receive-tokens.tsx b/src/app/pages/receive/components/receive-tokens.tsx index 1c8f47ee75e..62356b724db 100644 --- a/src/app/pages/receive/components/receive-tokens.tsx +++ b/src/app/pages/receive/components/receive-tokens.tsx @@ -4,15 +4,12 @@ import { HomePageSelectors } from '@tests/selectors/home.selectors'; import { css } from 'leather-styles/css'; import { Stack } from 'leather-styles/jsx'; -import { isDefined } from '@shared/utils'; - import { copyToClipboard } from '@app/common/utils/copy-to-clipboard'; -import { sortAssetsBySymbol } from '@app/common/utils/sort-assets-by-symbol'; import { useToast } from '@app/features/toasts/use-toast'; -import { useAlexSdkSwappableCurrencyQuery } from '@app/query/common/alex-sdk/swappable-currency.query'; +import { useAlexSwappableAssets } from '@app/query/common/alex-sdk/alex-sdk.hooks'; import { useConfigRunesEnabled } from '@app/query/common/remote-config/remote-config.query'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { Avatar, defaultFallbackDelay, getAvatarFallback } from '@app/ui/components/avatar/avatar'; +import { Avatar, defaultFallbackDelay } from '@app/ui/components/avatar/avatar'; import { Brc20AvatarIcon } from '@app/ui/components/avatar/brc20-avatar-icon'; import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon'; import { RunesAvatarIcon } from '@app/ui/components/avatar/runes-avatar-icon'; @@ -39,20 +36,19 @@ export function ReceiveTokens({ const toast = useToast(); const network = useCurrentNetwork(); const runesEnabled = useConfigRunesEnabled(); - const { data: supportedCurrencies = [] } = useAlexSdkSwappableCurrencyQuery(); + const { data: swapAssets = [] } = useAlexSwappableAssets(); const receivableAssets = useMemo( () => - sortAssetsBySymbol(supportedCurrencies.filter(isDefined)) + swapAssets .filter(asset => asset.name !== 'STX') .map(asset => ({ + ...asset, address: stxAddress, - fallback: getAvatarFallback(asset.name), - icon: asset.icon, - name: asset.name, })), - [stxAddress, supportedCurrencies] + [stxAddress, swapAssets] ); + return ( (createMoneyFromDecimal(0, assetSymbol)); + const [assetValue, setAssetValue] = useState( + createMoneyFromDecimal(0, assetSymbol, assetDecimals) + ); useEffect(() => { let amount = Number(field.value); @@ -26,9 +29,9 @@ export function SendFiatValue({ marketData, assetSymbol = '' }: SendFiatInputPro amount = 0; } - const assetAmount = createMoneyFromDecimal(amount, assetSymbol); + const assetAmount = createMoneyFromDecimal(amount, assetSymbol, assetDecimals); setAssetValue(assetAmount); - }, [field.value, assetSymbol]); + }, [field.value, assetSymbol, assetDecimals]); return ( 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/brc-20/brc-20-choose-fee.tsx index 20d63244dce..4568ddab1fd 100644 --- 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/brc-20/brc-20-choose-fee.tsx @@ -21,7 +21,7 @@ import { BitcoinChooseFee } from '@app/features/bitcoin-choose-fee/bitcoin-choos import { useValidateBitcoinSpend } from '@app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend'; import { useToast } from '@app/features/toasts/use-toast'; import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client'; -import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/use-brc-20'; +import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { useSendBitcoinAssetContextState } from '../../family/bitcoin/components/send-bitcoin-asset-container'; 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/brc-20/brc20-send-form-confirmation.tsx index 7fa0810da20..d6d176f3e5b 100644 --- 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/brc-20/brc20-send-form-confirmation.tsx @@ -17,7 +17,7 @@ import { InfoCardSeparator, } from '@app/components/info-card/info-card'; import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks'; -import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/use-brc-20'; +import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks'; import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction'; import { Button } from '@app/ui/components/button/button'; import { Footer } from '@app/ui/components/containers/footers/footer'; 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/brc-20/brc20-send-form.tsx index 8bfb2839be0..dffc03d66a3 100644 --- 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/brc-20/brc20-send-form.tsx @@ -5,6 +5,9 @@ import { Form, Formik } from 'formik'; import { Box, styled } from 'leather-styles/jsx'; import get from 'lodash.get'; +import type { MarketData } from '@shared/models/market.model'; +import type { Money } from '@shared/models/money.model'; + import { formatMoney } from '@app/common/money/format-money'; import { openInNewTab } from '@app/common/utils/open-in-new-tab'; import { Brc20AvatarIcon } from '@app/ui/components/avatar/brc20-avatar-icon'; @@ -18,6 +21,7 @@ import { CardContent } from '@app/ui/layout/card/card-content'; import { AmountField } from '../../components/amount-field'; import { SelectedAssetField } from '../../components/selected-asset-field'; +import { SendFiatValue } from '../../components/send-fiat-value'; import { SendMaxButton } from '../../components/send-max-button'; import { defaultSendFormFormikProps } from '../../send-form.utils'; import { useBrc20SendForm } from './use-brc20-send-form'; @@ -25,23 +29,17 @@ import { useBrc20SendForm } from './use-brc20-send-form'; function useBrc20SendFormRouteState() { const { state } = useLocation(); return { - balance: get(state, 'balance', '') as string, + balance: get(state, 'balance', '') as Money, ticker: get(state, 'ticker', '') as string, - decimals: get(state, 'decimals', '') as number, holderAddress: get(state, 'holderAddress', '') as string, + marketData: get(state, 'marketData') as MarketData, }; } export function Brc20SendForm() { - const { balance, ticker, decimals, holderAddress } = useBrc20SendFormRouteState(); - const { - initialValues, - chooseTransactionFee, - validationSchema, - formRef, - onFormStateChange, - moneyBalance, - } = useBrc20SendForm({ balance, ticker, decimals, holderAddress }); + const { balance, ticker, holderAddress, marketData } = useBrc20SendFormRouteState(); + const { initialValues, chooseTransactionFee, validationSchema, formRef, onFormStateChange } = + useBrc20SendForm({ balance, ticker, holderAddress }); return ( @@ -67,7 +65,7 @@ export function Brc20SendForm() { Continue @@ -75,14 +73,20 @@ export function Brc20SendForm() { > + } autoComplete="off" + switchableAmount={ + marketData ? ( + + ) : undefined + } /> } name={ticker} symbol={ticker} /> 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/brc-20/use-brc20-send-form.tsx index f907b5ac9b4..dbc9fee1f70 100644 --- 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/brc-20/use-brc20-send-form.tsx @@ -1,17 +1,15 @@ import { useRef } from 'react'; import { useNavigate } from 'react-router-dom'; -import BigNumber from 'bignumber.js'; import { FormikHelpers, FormikProps } from 'formik'; import * as yup from 'yup'; import { logger } from '@shared/logger'; -import { createMoney } from '@shared/models/money.model'; +import { type Money } from '@shared/models/money.model'; import { RouteUrls } from '@shared/route-urls'; import { noop } from '@shared/utils'; import { useOnMount } from '@app/common/hooks/use-on-mount'; -import { unitToFractionalUnit } from '@app/common/money/unit-conversion'; import { useWalletType } from '@app/common/use-wallet-type'; import { btcAddressNetworkValidator, @@ -33,18 +31,12 @@ interface Brc20SendFormValues { } interface UseBrc20SendFormArgs { - balance: string; + balance: Money; ticker: string; - decimals: number; holderAddress: string; } -export function useBrc20SendForm({ - balance, - ticker, - decimals, - holderAddress, -}: UseBrc20SendFormArgs) { +export function useBrc20SendForm({ balance, ticker, holderAddress }: UseBrc20SendFormArgs) { const formRef = useRef>(null); const { whenWallet } = useWalletType(); const navigate = useNavigate(); @@ -63,10 +55,7 @@ export function useBrc20SendForm({ }); const validationSchema = yup.object({ - amount: yup - .number() - .concat(currencyAmountValidator()) - .concat(tokenAmountValidator(createMoney(new BigNumber(balance), ticker, 0))), + amount: yup.number().concat(currencyAmountValidator()).concat(tokenAmountValidator(balance)), recipient: yup .string() .concat(btcAddressValidator()) @@ -96,10 +85,5 @@ export function useBrc20SendForm({ validationSchema, formRef, onFormStateChange, - moneyBalance: createMoney( - unitToFractionalUnit(decimals)(new BigNumber(balance)), - ticker, - decimals - ), }; } 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/stacks-sip10/sip10-token-send-form-container.tsx index 607a132e312..643935c0717 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/stacks-sip10/sip10-token-send-form-container.tsx @@ -3,6 +3,7 @@ import { StxAvatarIcon } from '@app/ui/components/avatar/stx-avatar-icon'; import { AmountField } from '../../components/amount-field'; import { SelectedAssetField } from '../../components/selected-asset-field'; +import { SendFiatValue } from '../../components/send-fiat-value'; import { SendMaxButton } from '../../components/send-max-button'; import { StacksCommonSendForm } from '../stacks/stacks-common-send-form'; import { useSip10SendForm } from './use-sip10-send-form'; @@ -23,6 +24,8 @@ export function Sip10TokenSendFormContainer({ stacksFtFees: fees, validationSchema, avatar, + marketData, + decimals, } = useSip10SendForm({ symbol, contractId }); const amountField = ( @@ -33,6 +36,11 @@ export function Sip10TokenSendFormContainer({ } tokenSymbol={symbol} autoComplete="off" + switchableAmount={ + marketData ? ( + + ) : undefined + } /> ); const selectedAssetField = ( 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/stacks-sip10/use-sip10-send-form.tsx index 10fe5b0a81b..31d63e80522 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/stacks-sip10/use-sip10-send-form.tsx @@ -60,6 +60,8 @@ export function useSip10SendForm({ symbol, contractId }: UseSip10SendFormArgs) { sendMaxBalance, stacksFtFees, symbol, + decimals: assetBalance.asset.decimals, + marketData: assetBalance.asset.marketData, avatar: createFtAvatar(), validationSchema: yup.object({ amount: stacksFungibleTokenAmountValidator(availableTokenBalance), diff --git a/src/app/pages/swap/alex-swap-container.tsx b/src/app/pages/swap/alex-swap-container.tsx index 0b19faf9676..d96bd033350 100644 --- a/src/app/pages/swap/alex-swap-container.tsx +++ b/src/app/pages/swap/alex-swap-container.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import { bytesToHex } from '@stacks/common'; @@ -16,9 +16,11 @@ import { RouteUrls } from '@shared/route-urls'; import { isDefined, isUndefined } from '@shared/utils'; import { alex } from '@shared/utils/alex-sdk'; +import { migratePositiveAssetBalancesToTop } from '@app/common/asset-utils'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { useWalletType } from '@app/common/use-wallet-type'; import { NonceSetter } from '@app/components/nonce-setter'; +import { defaultSwapFee } from '@app/query/common/alex-sdk/alex-sdk.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; @@ -29,14 +31,9 @@ import { generateSwapRoutes } from './generate-swap-routes'; import { useAlexBroadcastSwap } from './hooks/use-alex-broadcast-swap'; import { oneHundredMillion, useAlexSwap } from './hooks/use-alex-swap'; import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap'; -import { SwapAsset, SwapFormValues } from './hooks/use-swap-form'; +import { SwapFormValues } from './hooks/use-swap-form'; import { useSwapNavigate } from './hooks/use-swap-navigate'; import { SwapContext, SwapProvider } from './swap.context'; -import { - defaultSwapFee, - migratePositiveBalancesToTop, - sortSwappableAssetsBySymbol, -} from './swap.utils'; export const alexSwapRoutes = generateSwapRoutes(); @@ -57,27 +54,18 @@ function AlexSwapContainer() { }); const { - fetchToAmount, - createSwapAssetFromAlexCurrency, + fetchQuoteAmount, isFetchingExchangeRate, onSetIsFetchingExchangeRate, onSetSwapSubmissionData, slippage, - supportedCurrencies, + swapAssets, swapSubmissionData, } = useAlexSwap(); const broadcastAlexSwap = useAlexBroadcastSwap(); const broadcastStacksSwap = useStacksBroadcastSwap(); - const swappableAssets: SwapAsset[] = useMemo( - () => - sortSwappableAssetsBySymbol( - supportedCurrencies.map(createSwapAssetFromAlexCurrency).filter(isDefined) - ), - [createSwapAssetFromAlexCurrency, supportedCurrencies] - ); - async function onSubmitSwapForReview(values: SwapFormValues) { if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) { logger.error('Error submitting swap for review'); @@ -96,9 +84,7 @@ function AlexSwapContainer() { liquidityFee: new BigNumber(Number(lpFee)).dividedBy(oneHundredMillion).toNumber(), nonce: values.nonce, protocol: 'ALEX', - router: router - .map(x => createSwapAssetFromAlexCurrency(supportedCurrencies.find(y => y.id === x))) - .filter(isDefined), + router: router.map(x => swapAssets.find(asset => asset.currency === x)).filter(isDefined), slippage, sponsored: isSponsoredByAlex, swapAmountBase: values.swapAmountBase, @@ -191,15 +177,15 @@ function AlexSwapContainer() { } const swapContextValue: SwapContext = { - fetchToAmount, + fetchQuoteAmount, isFetchingExchangeRate, isSendingMax, onSetIsFetchingExchangeRate, onSetIsSendingMax: value => setIsSendingMax(value), onSubmitSwapForReview, onSubmitSwap, - swappableAssetsBase: migratePositiveBalancesToTop(swappableAssets), - swappableAssetsQuote: swappableAssets, + swappableAssetsBase: migratePositiveAssetBalancesToTop(swapAssets), + swappableAssetsQuote: swapAssets, swapSubmissionData, }; 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 2b191fd32bf..5ae67e717e5 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 @@ -1,11 +1,8 @@ import { SwapSelectors } from '@tests/selectors/swap.selectors'; -import { - checkIsMoneyAmountGreaterThanZero, - convertCryptoCurrencyMoneyToFiat, -} from '@app/common/money/fiat-conversion'; -import { formatMoneyWithoutSymbol, i18nFormatCurrency } from '@app/common/money/format-money'; -import type { SwapAsset } from '@app/pages/swap/hooks/use-swap-form'; +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 { Avatar, defaultFallbackDelay, getAvatarFallback } from '@app/ui/components/avatar/avatar'; @@ -22,12 +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 showFiatBalance = checkIsMoneyAmountGreaterThanZero(asset.price); - const balanceAsFiat = showFiatBalance - ? i18nFormatCurrency( - convertCryptoCurrencyMoneyToFiat(asset.balance.symbol, asset.price, asset.balance) - ) - : ''; + const balanceAsFiat = convertAssetBalanceToFiat(asset); return ( diff --git a/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx index 736c2152b17..1c2cf94a93f 100644 --- a/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx +++ b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx @@ -12,8 +12,9 @@ import { isUndefined } from '@shared/utils'; import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; import { useSwapContext } from '@app/pages/swap/swap.context'; +import type { SwapAsset } from '@app/query/common/alex-sdk/alex-sdk.hooks'; -import { SwapAsset, SwapFormValues } from '../../../hooks/use-swap-form'; +import { SwapFormValues } from '../../../hooks/use-swap-form'; import { SwapAssetItem } from './swap-asset-item'; interface SwapAssetList { @@ -21,7 +22,7 @@ interface SwapAssetList { type: string; } export function SwapAssetList({ assets, type }: SwapAssetList) { - const { fetchToAmount } = useSwapContext(); + const { fetchQuoteAmount } = useSwapContext(); const { setFieldError, setFieldValue, values } = useFormikContext(); const navigate = useNavigate(); const { base, quote } = useParams(); @@ -35,33 +36,33 @@ export function SwapAssetList({ assets, type }: SwapAssetList) { ); async function onSelectAsset(asset: SwapAsset) { - let from: SwapAsset | undefined; - let to: SwapAsset | undefined; + let baseAsset: SwapAsset | undefined; + let quoteAsset: SwapAsset | undefined; if (isBaseList) { - from = asset; - to = values.swapAssetQuote; + baseAsset = asset; + quoteAsset = values.swapAssetQuote; await setFieldValue('swapAssetBase', asset); - navigate(RouteUrls.Swap.replace(':base', from.name).replace(':quote', quote ?? '')); + navigate(RouteUrls.Swap.replace(':base', baseAsset.name).replace(':quote', quote ?? '')); } else if (isQuoteList) { - from = values.swapAssetBase; - to = asset; + baseAsset = values.swapAssetBase; + quoteAsset = asset; await setFieldValue('swapAssetQuote', asset); setFieldError('swapAssetQuote', undefined); - navigate(RouteUrls.Swap.replace(':base', base ?? '').replace(':quote', to.name)); + navigate(RouteUrls.Swap.replace(':base', base ?? '').replace(':quote', quoteAsset.name)); } - if (from && to && values.swapAmountBase) { - const toAmount = await fetchToAmount(from, to, values.swapAmountBase); - if (isUndefined(toAmount)) { + if (baseAsset && quoteAsset && values.swapAmountBase) { + const quoteAmount = await fetchQuoteAmount(baseAsset, quoteAsset, values.swapAmountBase); + if (isUndefined(quoteAmount)) { await setFieldValue('swapAmountQuote', ''); return; } - const toAmountAsMoney = createMoney( - convertAmountToFractionalUnit(new BigNumber(toAmount), to?.balance.decimals), - to?.balance.symbol ?? '', - to?.balance.decimals + const quoteAmountAsMoney = createMoney( + convertAmountToFractionalUnit(new BigNumber(quoteAmount), quoteAsset?.balance.decimals), + quoteAsset?.balance.symbol ?? '', + quoteAsset?.balance.decimals ); - await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(toAmountAsMoney)); + await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(quoteAmountAsMoney)); setFieldError('swapAmountQuote', undefined); } } diff --git a/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx b/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx index f3db4fe81d1..e47d3a5df55 100644 --- a/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx +++ b/src/app/pages/swap/components/swap-asset-select/components/swap-amount-field.tsx @@ -27,7 +27,7 @@ interface SwapAmountFieldProps { name: string; } export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFieldProps) { - const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); + const { fetchQuoteAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); const { setFieldError, setFieldValue, values } = useFormikContext(); const [field] = useField(name); const showError = useShowFieldError(name) && name === 'swapAmountBase' && values.swapAssetQuote; @@ -37,7 +37,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi if (isUndefined(swapAssetBase) || isUndefined(swapAssetQuote)) return; onSetIsSendingMax(false); const value = event.currentTarget.value; - const toAmount = await fetchToAmount(swapAssetBase, swapAssetQuote, value); + const toAmount = await fetchQuoteAmount(swapAssetBase, swapAssetQuote, value); if (isUndefined(toAmount)) { await setFieldValue('swapAmountQuote', ''); return; diff --git a/src/app/pages/swap/components/swap-asset-select/components/swap-toggle-button.tsx b/src/app/pages/swap/components/swap-asset-select/components/swap-toggle-button.tsx index e6db08a873d..dafd83d09a2 100644 --- a/src/app/pages/swap/components/swap-asset-select/components/swap-toggle-button.tsx +++ b/src/app/pages/swap/components/swap-asset-select/components/swap-toggle-button.tsx @@ -12,8 +12,8 @@ import { SwapFormValues } from '../../../hooks/use-swap-form'; import { useSwapContext } from '../../../swap.context'; export function SwapToggleButton() { - const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); - const { setFieldValue, validateForm, values } = useFormikContext(); + const { fetchQuoteAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); + const { setFieldValue, values } = useFormikContext(); const navigate = useNavigate(); async function onToggleSwapAssets() { @@ -24,21 +24,20 @@ export function SwapToggleButton() { const prevAssetBase = values.swapAssetBase; const prevAssetQuote = values.swapAssetQuote; - await setFieldValue('swapAssetBase', prevAssetQuote); - await setFieldValue('swapAssetQuote', prevAssetBase); - await setFieldValue('swapAmountBase', prevAmountQuote); + void setFieldValue('swapAssetBase', prevAssetQuote); + void setFieldValue('swapAssetQuote', prevAssetBase); + void setFieldValue('swapAmountBase', prevAmountQuote); if (isDefined(prevAssetBase) && isDefined(prevAssetQuote)) { - const quoteAmount = await fetchToAmount(prevAssetQuote, prevAssetBase, prevAmountQuote); + const quoteAmount = await fetchQuoteAmount(prevAssetQuote, prevAssetBase, prevAmountQuote); if (isUndefined(quoteAmount)) { - await setFieldValue('swapAmountQuote', ''); + void setFieldValue('swapAmountQuote', ''); return; } - await setFieldValue('swapAmountQuote', Number(quoteAmount)); + void setFieldValue('swapAmountQuote', Number(quoteAmount)); } else { - await setFieldValue('swapAmountQuote', Number(prevAmountBase)); + void setFieldValue('swapAmountQuote', Number(prevAmountBase)); } - await validateForm(); navigate( RouteUrls.Swap.replace(':base', prevAssetQuote?.name ?? '').replace( ':quote', diff --git a/src/app/pages/swap/components/swap-asset-select/swap-asset-select-base.tsx b/src/app/pages/swap/components/swap-asset-select/swap-asset-select-base.tsx index 94221abaa7b..21084aa4e94 100644 --- a/src/app/pages/swap/components/swap-asset-select/swap-asset-select-base.tsx +++ b/src/app/pages/swap/components/swap-asset-select/swap-asset-select-base.tsx @@ -22,7 +22,7 @@ const maxAvailableTooltip = const sendingMaxTooltip = 'When sending max, this amount is affected by the fee you choose.'; export function SwapAssetSelectBase() { - const { fetchToAmount, isFetchingExchangeRate, isSendingMax, onSetIsSendingMax } = + const { fetchQuoteAmount, isFetchingExchangeRate, isSendingMax, onSetIsSendingMax } = useSwapContext(); const { setFieldValue, setFieldError, values } = useFormikContext(); const [amountField, amountFieldMeta, amountFieldHelpers] = useField('swapAmountBase'); @@ -34,7 +34,7 @@ export function SwapAssetSelectBase() { isDefined(assetField.value && amountField.value) && convertInputAmountValueToFiat( assetField.value.balance, - assetField.value.price, + assetField.value.marketData, amountField.value ); const formattedBalance = formatMoneyWithoutSymbol(assetField.value.balance); @@ -48,7 +48,7 @@ export function SwapAssetSelectBase() { await amountFieldHelpers.setValue(Number(formattedBalance)); await amountFieldHelpers.setTouched(true); if (isUndefined(swapAssetQuote)) return; - const toAmount = await fetchToAmount(swapAssetBase, swapAssetQuote, formattedBalance); + const toAmount = await fetchQuoteAmount(swapAssetBase, swapAssetQuote, formattedBalance); if (isUndefined(toAmount)) { await setFieldValue('swapAmountQuote', ''); return; diff --git a/src/app/pages/swap/components/swap-asset-select/swap-asset-select-quote.tsx b/src/app/pages/swap/components/swap-asset-select/swap-asset-select-quote.tsx index 16ff6822f9c..3af59a73a89 100644 --- a/src/app/pages/swap/components/swap-asset-select/swap-asset-select-quote.tsx +++ b/src/app/pages/swap/components/swap-asset-select/swap-asset-select-quote.tsx @@ -22,7 +22,7 @@ export function SwapAssetSelectQuote() { isDefined(assetField.value && amountField.value) && convertInputAmountValueToFiat( assetField.value.balance, - assetField.value.price, + assetField.value.marketData, amountField.value ); diff --git a/src/app/pages/swap/hooks/use-alex-swap.tsx b/src/app/pages/swap/hooks/use-alex-swap.tsx index 71034ec39c5..d66bc80cd59 100644 --- a/src/app/pages/swap/hooks/use-alex-swap.tsx +++ b/src/app/pages/swap/hooks/use-alex-swap.tsx @@ -1,22 +1,13 @@ -import { useCallback, useState } from 'react'; +import { useState } from 'react'; -import { Currency, TokenInfo } from 'alex-sdk'; import BigNumber from 'bignumber.js'; import { logger } from '@shared/logger'; -import { createMoney } from '@shared/models/money.model'; import { alex } from '@shared/utils/alex-sdk'; -import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance'; -import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; -import { pullContractIdFromIdentity } from '@app/common/utils'; -import { useAlexSdkLatestPricesQuery } from '@app/query/common/alex-sdk/latest-prices.query'; -import { useAlexSdkSwappableCurrencyQuery } from '@app/query/common/alex-sdk/swappable-currency.query'; -import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { type SwapAsset, useAlexSwappableAssets } from '@app/query/common/alex-sdk/alex-sdk.hooks'; import { SwapSubmissionData } from '../swap.context'; -import { SwapAsset } from './use-swap-form'; export const oneHundredMillion = 100_000_000; @@ -24,62 +15,18 @@ export function useAlexSwap() { const [swapSubmissionData, setSwapSubmissionData] = useState(); const [slippage, _setSlippage] = useState(0.04); const [isFetchingExchangeRate, setIsFetchingExchangeRate] = useState(false); - const { data: supportedCurrencies = [] } = useAlexSdkSwappableCurrencyQuery(); - const { data: prices } = useAlexSdkLatestPricesQuery(); - const { availableBalance: availableStxBalance } = useStxBalance(); - const account = useCurrentStacksAccount(); - const stacksFtAssetBalances = useTransferableStacksFungibleTokenAssetBalances( - account?.address ?? '' - ); + const { data: swapAssets = [] } = useAlexSwappableAssets(); - const createSwapAssetFromAlexCurrency = useCallback( - (tokenInfo?: TokenInfo) => { - if (!prices) return; - if (!tokenInfo) { - logger.error('No token data found to swap'); - return; - } - - const currency = tokenInfo.id as Currency; - const price = convertAmountToFractionalUnit(new BigNumber(prices[currency] ?? 0), 2); - const swapAsset = { - currency, - icon: tokenInfo.icon, - name: tokenInfo.name, - price: createMoney(price, 'USD'), - principal: pullContractIdFromIdentity(tokenInfo.contractAddress), - }; - - if (currency === Currency.STX) { - return { - ...swapAsset, - balance: availableStxBalance, - displayName: 'Stacks', - }; - } - - const fungibleTokenBalance = - stacksFtAssetBalances.find(x => tokenInfo.contractAddress === x.asset.contractId) - ?.balance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals); - - return { - ...swapAsset, - balance: fungibleTokenBalance, - }; - }, - [availableStxBalance, prices, stacksFtAssetBalances] - ); - - async function fetchToAmount( - from: SwapAsset, - to: SwapAsset, - fromAmount: string + async function fetchQuoteAmount( + base: SwapAsset, + quote: SwapAsset, + baseAmount: string ): Promise { - const amount = new BigNumber(fromAmount).multipliedBy(oneHundredMillion).dp(0).toString(); + const amount = new BigNumber(baseAmount).multipliedBy(oneHundredMillion).dp(0).toString(); const amountAsBigInt = isNaN(Number(amount)) ? BigInt(0) : BigInt(amount); try { setIsFetchingExchangeRate(true); - const result = await alex.getAmountTo(from.currency, amountAsBigInt, to.currency); + const result = await alex.getAmountTo(base.currency, amountAsBigInt, quote.currency); setIsFetchingExchangeRate(false); return new BigNumber(Number(result)).dividedBy(oneHundredMillion).toString(); } catch (e) { @@ -90,13 +37,12 @@ export function useAlexSwap() { } return { - fetchToAmount, - createSwapAssetFromAlexCurrency, + fetchQuoteAmount, isFetchingExchangeRate, onSetIsFetchingExchangeRate: (value: boolean) => setIsFetchingExchangeRate(value), onSetSwapSubmissionData: (value: SwapSubmissionData) => setSwapSubmissionData(value), slippage, - supportedCurrencies, + swapAssets, swapSubmissionData, }; } diff --git a/src/app/pages/swap/hooks/use-swap-form.tsx b/src/app/pages/swap/hooks/use-swap-form.tsx index 0121f80b6de..e01e395495f 100644 --- a/src/app/pages/swap/hooks/use-swap-form.tsx +++ b/src/app/pages/swap/hooks/use-swap-form.tsx @@ -1,24 +1,16 @@ -import { Currency } from 'alex-sdk'; import BigNumber from 'bignumber.js'; import * as yup from 'yup'; import { FormErrorMessages } from '@shared/error-messages'; import { FeeTypes } from '@shared/models/fees/fees.model'; import { StacksTransactionFormValues } from '@shared/models/form.model'; -import { Money, createMoney } from '@shared/models/money.model'; +import { createMoney } from '@shared/models/money.model'; import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; +import type { SwapAsset } from '@app/query/common/alex-sdk/alex-sdk.hooks'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; -export interface SwapAsset { - balance: Money; - currency: Currency; - displayName?: string; - icon: string; - name: string; - price: Money; - principal: string; -} +import { useSwapContext } from '../swap.context'; export interface SwapFormValues extends StacksTransactionFormValues { swapAmountBase: string; @@ -28,6 +20,7 @@ export interface SwapFormValues extends StacksTransactionFormValues { } export function useSwapForm() { + const { isFetchingExchangeRate } = useSwapContext(); const { data: nextNonce } = useNextNonce(); const initialValues: SwapFormValues = { @@ -49,6 +42,7 @@ export function useSwapForm() { .test({ message: 'Insufficient balance', test(value) { + if (isFetchingExchangeRate) return true; const { swapAssetBase } = this.parent; const valueInFractionalUnit = convertAmountToFractionalUnit( createMoney( diff --git a/src/app/pages/swap/swap.context.ts b/src/app/pages/swap/swap.context.ts index 2b98312c02d..deec0ddbd6f 100644 --- a/src/app/pages/swap/swap.context.ts +++ b/src/app/pages/swap/swap.context.ts @@ -1,6 +1,8 @@ import { createContext, useContext } from 'react'; -import { SwapAsset, SwapFormValues } from './hooks/use-swap-form'; +import type { SwapAsset } from '@app/query/common/alex-sdk/alex-sdk.hooks'; + +import { SwapFormValues } from './hooks/use-swap-form'; export interface SwapSubmissionData extends SwapFormValues { liquidityFee: number; @@ -12,7 +14,7 @@ export interface SwapSubmissionData extends SwapFormValues { } export interface SwapContext { - fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise; + fetchQuoteAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise; isFetchingExchangeRate: boolean; isSendingMax: boolean; onSetIsFetchingExchangeRate(value: boolean): void; diff --git a/src/app/pages/swap/swap.tsx b/src/app/pages/swap/swap.tsx index 48565753c00..1a2f81fd5a3 100644 --- a/src/app/pages/swap/swap.tsx +++ b/src/app/pages/swap/swap.tsx @@ -19,7 +19,8 @@ import { useSwapContext } from './swap.context'; export function Swap() { const { isFetchingExchangeRate, swappableAssetsBase, swappableAssetsQuote } = useSwapContext(); - const { dirty, isValid, setFieldValue, values } = useFormikContext(); + const { dirty, isValid, setFieldValue, values, validateForm } = + useFormikContext(); const { base, quote } = useParams(); useEffect(() => { @@ -33,7 +34,16 @@ export function Swap() { 'swapAssetQuote', swappableAssetsQuote.find(asset => asset.name === quote) ); - }, [base, quote, setFieldValue, swappableAssetsBase, swappableAssetsQuote, values.swapAssetBase]); + void validateForm(); + }, [ + base, + quote, + setFieldValue, + swappableAssetsBase, + swappableAssetsQuote, + validateForm, + values.swapAssetBase, + ]); if (isUndefined(values.swapAssetBase)) return ; diff --git a/src/app/pages/swap/swap.utils.ts b/src/app/pages/swap/swap.utils.ts index e05a135802d..c31910c2e93 100644 --- a/src/app/pages/swap/swap.utils.ts +++ b/src/app/pages/swap/swap.utils.ts @@ -1,49 +1,17 @@ +import type { MarketData } from '@shared/models/market.model'; import { type Money, createMoney } from '@shared/models/money.model'; -import { - checkIsMoneyAmountGreaterThanZero, - convertCryptoCurrencyMoneyToFiat, -} from '@app/common/money/fiat-conversion'; +import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money'; +import { isMoneyGreaterThanZero } from '@app/common/money/money.utils'; import { unitToFractionalUnit } from '@app/common/money/unit-conversion'; -import { SwapAsset } from './hooks/use-swap-form'; - -export const defaultSwapFee = createMoney(1000000, 'STX'); - -export function sortSwappableAssetsBySymbol(swappableAssets: SwapAsset[]) { - return swappableAssets - .sort((a, b) => { - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; - }) - .sort((a, b) => { - if (a.name === 'STX') return -1; - if (b.name !== 'STX') return 1; - return 0; - }) - .sort((a, b) => { - if (a.name === 'BTC') return -1; - if (b.name !== 'BTC') return 1; - return 0; - }); -} - -export function migratePositiveBalancesToTop(swappableAssets: SwapAsset[]) { - const assetsWithPositiveBalance = swappableAssets.filter(asset => - asset.balance.amount.isGreaterThan(0) - ); - const assetsWithZeroBalance = swappableAssets.filter(asset => asset.balance.amount.isEqualTo(0)); - return [...assetsWithPositiveBalance, ...assetsWithZeroBalance]; -} - -export function convertInputAmountValueToFiat(balance: Money, price: Money, value: string) { +export function convertInputAmountValueToFiat(balance: Money, price: MarketData, value: string) { const valueAsMoney = createMoney( unitToFractionalUnit(balance.decimals)(value), balance.symbol, balance.decimals ); - if (!checkIsMoneyAmountGreaterThanZero(valueAsMoney)) return; - return convertCryptoCurrencyMoneyToFiat(balance.symbol, price, valueAsMoney); + if (!isMoneyGreaterThanZero(valueAsMoney)) return; + return baseCurrencyAmountInQuote(valueAsMoney, price); } diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index 1629779f3ba..fa5ea9c7019 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -5,9 +5,8 @@ import { BESTINSLOT_API_BASE_URL_MAINNET, BESTINSLOT_API_BASE_URL_TESTNET, type BitcoinNetworkModes, - HIRO_API_BASE_URL_MAINNET, } from '@shared/constants'; -import { Paginated } from '@shared/models/api-types'; +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'; @@ -64,21 +63,17 @@ interface BestinslotInscriptionsByTxIdResponse { blockHeight: number; } -interface Brc20TokenResponse { +/* BRC-20 */ +interface Brc20Balance { ticker: string; overall_balance: string; available_balance: string; transferrable_balance: string; image_url: string | null; - min_listed_unit_price: number; + min_listed_unit_price: number | null; } -export interface Brc20Token extends Brc20TokenResponse { - decimals: number; - holderAddress: string; -} - -interface Brc20TokenTicker { +interface Brc20TickerInfo { id: string; number: number; block_height: number; @@ -93,16 +88,23 @@ interface Brc20TokenTicker { tx_count: number; } -interface Brc20TickerResponse { - data: Brc20TokenTicker; +interface Brc20TickerInfoResponse { block_height: number; + data: Brc20TickerInfo; } -interface BestinslotBrc20AddressBalanceResponse { +interface Brc20WalletBalancesResponse { block_height: number; - data: Brc20TokenResponse[]; + data: Brc20Balance[]; } +export interface Brc20Token extends Brc20Balance, Brc20TickerInfo { + balance: Money | null; + holderAddress: string; + marketData: MarketData | null; +} + +/* RUNES */ export interface RuneBalance { pkscript: string; rune_id: string; @@ -207,8 +209,9 @@ class BestinslotApi { return resp.data; } - async getBrc20Balance(address: string) { - const resp = await axios.get( + /* BRC-20 */ + async getBrc20Balances(address: string) { + const resp = await axios.get( `${this.url}/brc20/wallet_balances?address=${address}`, { ...this.defaultOptions, @@ -217,8 +220,8 @@ class BestinslotApi { return resp.data; } - async getBrc20TickerData(ticker: string) { - const resp = await axios.get( + async getBrc20TickerInfo(ticker: string) { + const resp = await axios.get( `${this.url}/brc20/ticker_info?ticker=${ticker}`, { ...this.defaultOptions, @@ -285,24 +288,6 @@ class BestinslotApi { } } -class HiroApi { - url = HIRO_API_BASE_URL_MAINNET; - - async getBrc20Balance(address: string) { - const resp = await axios.get>( - `${this.url}/ordinals/v1/brc-20/balances/${address}` - ); - return resp.data; - } - - async getBrc20TickerData(ticker: string) { - const resp = await axios.get>( - `${this.url}/ordinals/v1/brc-20/tokens?ticker=${ticker}` - ); - return resp.data; - } -} - class AddressApi { rateLimiter: PQueue; constructor(public configuration: Configuration) { @@ -421,7 +406,6 @@ export class BitcoinClient { feeEstimatesApi: FeeEstimatesApi; transactionsApi: TransactionsApi; BestinslotApi: BestinslotApi; - HiroApi: HiroApi; constructor(basePath: string) { this.configuration = new Configuration(basePath); @@ -429,6 +413,5 @@ export class BitcoinClient { this.feeEstimatesApi = new FeeEstimatesApi(this.configuration); this.transactionsApi = new TransactionsApi(this.configuration); this.BestinslotApi = new BestinslotApi(this.configuration); - this.HiroApi = new HiroApi(); } } diff --git a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts similarity index 71% rename from src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts rename to src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts index eca1e985e3a..0b133b14a1d 100644 --- a/src/app/query/bitcoin/ordinals/brc20/use-brc-20.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts @@ -1,9 +1,16 @@ +import BigNumber from 'bignumber.js'; + +import { createMarketData, createMarketPair } from '@shared/models/market.model'; +import { createMoney } from '@shared/models/money.model'; + +import { useGetBrc20TokensQuery } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; import { useConfigOrdinalsbot } from '@app/query/common/remote-config/remote-config.query'; 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 { createBrc20TransferInscription, encodeBrc20TransferInscription } from './brc-20.utils'; @@ -72,3 +79,24 @@ export function useBrc20Transfers(holderAddress: string) { }, }; } + +function makeBrc20Token(token: Brc20Token) { + return { + ...token, + balance: createMoney(new BigNumber(token.overall_balance), token.ticker, token.decimals), + marketData: createMarketData( + createMarketPair(token.ticker, 'USD'), + createMoney(new BigNumber(token.min_listed_unit_price ?? 0), 'USD') + ), + }; +} + +export function useBrc20Tokens() { + const { data: allBrc20TokensResponse } = useGetBrc20TokensQuery(); + const brc20Tokens = allBrc20TokensResponse?.pages + .flatMap(page => page.brc20Tokens) + .filter(token => token.length > 0) + .flatMap(token => token); + + return brc20Tokens?.map(token => makeBrc20Token(token)) ?? []; +} 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 4df6782d3ca..a4a7ff2feb0 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts @@ -51,19 +51,22 @@ export function useGetBrc20TokensQuery() { } const brc20TokensPromises = addressesData.map(async address => { - const brc20Tokens = await client.BestinslotApi.getBrc20Balance(address); + const brc20Tokens = await client.BestinslotApi.getBrc20Balances(address); const tickerPromises = await Promise.all( brc20Tokens.data.map(token => { - return client.BestinslotApi.getBrc20TickerData(token.ticker); + return client.BestinslotApi.getBrc20TickerInfo(token.ticker); }) ); + // Initialize token with ticker data return brc20Tokens.data.map((token, index) => { return { ...token, - decimals: tickerPromises[index].data.decimals, + ...tickerPromises[index].data, + balance: null, holderAddress: address, + marketData: null, }; }); }); diff --git a/src/app/query/common/alex-sdk/latest-prices.query.ts b/src/app/query/common/alex-sdk/alex-sdk-latest-prices.query.ts similarity index 100% rename from src/app/query/common/alex-sdk/latest-prices.query.ts rename to src/app/query/common/alex-sdk/alex-sdk-latest-prices.query.ts diff --git a/src/app/query/common/alex-sdk/alex-sdk-swappable-currency.query.ts b/src/app/query/common/alex-sdk/alex-sdk-swappable-currency.query.ts new file mode 100644 index 00000000000..ba62c6001cb --- /dev/null +++ b/src/app/query/common/alex-sdk/alex-sdk-swappable-currency.query.ts @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; +import type { TokenInfo } from 'alex-sdk'; + +import { alex } from '@shared/utils/alex-sdk'; + +import type { AppUseQueryConfig } from '@app/query/query-config'; + +const queryOptions = { + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + retryDelay: 1000 * 60, + staleTime: 1000 * 60 * 10, +}; + +export function useAlexSdkSwappableCurrencyQuery( + options?: AppUseQueryConfig +) { + return useQuery({ + queryKey: ['alex-sdk-swappable-currencies'], + queryFn: async () => alex.fetchSwappableCurrency(), + ...queryOptions, + ...options, + }); +} 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 5add887bfbf..f343a0dd3bb 100644 --- a/src/app/query/common/alex-sdk/alex-sdk.hooks.ts +++ b/src/app/query/common/alex-sdk/alex-sdk.hooks.ts @@ -1,35 +1,114 @@ import { useCallback } from 'react'; -import { type Currency } from 'alex-sdk'; +import { Currency, type TokenInfo } from 'alex-sdk'; import BigNumber from 'bignumber.js'; import { logger } from '@shared/logger'; -import { createMoney } from '@shared/models/money.model'; +import { type MarketData, createMarketData, createMarketPair } from '@shared/models/market.model'; +import { type Money, createMoney } from '@shared/models/money.model'; import { isDefined } from '@shared/utils'; +import { sortAssetsByName } from '@app/common/asset-utils'; +import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance'; 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 { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { getAvatarFallback } from '@app/ui/components/avatar/avatar'; -import { useAlexSdkLatestPricesQuery } from './latest-prices.query'; -import { useAlexSdkSwappableCurrencyQuery } from './swappable-currency.query'; +import { useAlexSdkLatestPricesQuery } from './alex-sdk-latest-prices.query'; +import { useAlexSdkSwappableCurrencyQuery } from './alex-sdk-swappable-currency.query'; -export function useAlexSdKCurrencyPriceAsMoney() { +export interface SwapAsset { + address?: string; + balance: Money; + currency: Currency; + displayName?: string; + fallback: string; + icon: string; + name: string; + marketData: MarketData | null; + principal: string; +} + +export const defaultSwapFee = createMoney(1000000, 'STX'); + +export function useAlexCurrencyPriceAsMarketData() { const { data: supportedCurrencies = [] } = useAlexSdkSwappableCurrencyQuery(); const { data: prices } = useAlexSdkLatestPricesQuery(); return useCallback( - (principal: string) => { - if (!prices) { - logger.error('Latest prices could not be found'); - return null; - } + (principal: string, symbol?: string) => { const tokenInfo = supportedCurrencies .filter(isDefined) .find(token => pullContractIdFromIdentity(token.contractAddress) === principal); - const currency = tokenInfo?.id as Currency; + if (!symbol || !prices || !tokenInfo) { + logger.error('Could not create market data'); + return null; + } + const currency = tokenInfo.id as Currency; const price = convertAmountToFractionalUnit(new BigNumber(prices[currency] ?? 0), 2); - return createMoney(price, 'USD'); + return createMarketData(createMarketPair(symbol, 'USD'), createMoney(price, 'USD')); }, [prices, supportedCurrencies] ); } + +function useMakeSwapAsset() { + const account = useCurrentStacksAccount(); + const { data: prices } = useAlexSdkLatestPricesQuery(); + const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); + const { availableBalance: availableStxBalance } = useStxBalance(); + const stacksFtAssetBalances = useTransferableStacksFungibleTokenAssetBalances( + account?.address ?? '' + ); + + return useCallback( + (tokenInfo?: TokenInfo): SwapAsset | undefined => { + if (!prices) return; + if (!tokenInfo) { + logger.error('No token data found to swap'); + return; + } + + const currency = tokenInfo.id as Currency; + const fungibleTokenBalance = stacksFtAssetBalances.find( + balance => tokenInfo.contractAddress === balance.asset.contractId + )?.balance; + const principal = pullContractIdFromIdentity(tokenInfo.contractAddress); + + const swapAsset = { + currency, + fallback: getAvatarFallback(tokenInfo.name), + icon: tokenInfo.icon, + name: tokenInfo.name, + principal: pullContractIdFromIdentity(tokenInfo.contractAddress), + }; + + if (currency === Currency.STX) { + return { + ...swapAsset, + balance: availableStxBalance, + displayName: 'Stacks', + marketData: priceAsMarketData(principal, availableStxBalance.symbol), + }; + } + + return { + ...swapAsset, + balance: fungibleTokenBalance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals), + marketData: fungibleTokenBalance + ? priceAsMarketData(principal, fungibleTokenBalance.symbol) + : priceAsMarketData(principal, tokenInfo.name), + }; + }, + [availableStxBalance, priceAsMarketData, prices, stacksFtAssetBalances] + ); +} + +export function useAlexSwappableAssets() { + const makeSwapAsset = useMakeSwapAsset(); + return useAlexSdkSwappableCurrencyQuery({ + select: resp => sortAssetsByName(resp.map(makeSwapAsset).filter(isDefined)), + }); +} diff --git a/src/app/query/common/alex-sdk/swappable-currency.query.ts b/src/app/query/common/alex-sdk/swappable-currency.query.ts deleted file mode 100644 index 792b60b603e..00000000000 --- a/src/app/query/common/alex-sdk/swappable-currency.query.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { alex } from '@shared/utils/alex-sdk'; - -export function useAlexSdkSwappableCurrencyQuery() { - return useQuery(['alex-sdk-swappable-currencies'], async () => alex.fetchSwappableCurrency(), { - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retryDelay: 1000 * 60, - staleTime: 1000 * 60 * 10, - }); -} diff --git a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts b/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts index 1dcd2a36d37..b5feb7cec5a 100644 --- a/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts +++ b/src/app/query/stacks/balance/stacks-ft-balances.hooks.ts @@ -7,7 +7,7 @@ import type { StacksFungibleTokenAssetBalance } from '@shared/models/crypto-asse import { formatContractId } from '@app/common/utils'; import { useToast } from '@app/features/toasts/use-toast'; -import { useAlexSdKCurrencyPriceAsMoney } from '@app/query/common/alex-sdk/alex-sdk.hooks'; +import { useAlexCurrencyPriceAsMarketData } 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'; @@ -36,7 +36,7 @@ function useStacksFungibleTokenAssetBalances(address: string) { export function useStacksFungibleTokenAssetBalancesWithMetadata(address: string) { const { data: initializedAssetBalances = [] } = useStacksFungibleTokenAssetBalances(address); - const priceAsMoney = useAlexSdKCurrencyPriceAsMoney(); + const priceAsMarketData = useAlexCurrencyPriceAsMarketData(); const ftAssetsMetadata = useGetFungibleTokenMetadataListQuery( initializedAssetBalances.map(assetBalance => @@ -52,8 +52,9 @@ export function useStacksFungibleTokenAssetBalancesWithMetadata(address: string) return addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance( assetBalance, metadata, - priceAsMoney( - formatContractId(assetBalance.asset.contractAddress, assetBalance.asset.contractName) + priceAsMarketData( + formatContractId(assetBalance.asset.contractAddress, assetBalance.asset.contractName), + metadata.symbol ) ); }), diff --git a/src/app/query/stacks/balance/stacks-ft-balances.utils.ts b/src/app/query/stacks/balance/stacks-ft-balances.utils.ts index 159d0af3409..6d88699c766 100644 --- a/src/app/query/stacks/balance/stacks-ft-balances.utils.ts +++ b/src/app/query/stacks/balance/stacks-ft-balances.utils.ts @@ -7,7 +7,8 @@ import type { StacksCryptoCurrencyAssetBalance, StacksFungibleTokenAssetBalance, } from '@shared/models/crypto-asset-balance.model'; -import { type Money, createMoney } from '@shared/models/money.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'; @@ -47,7 +48,7 @@ export function createStacksFtCryptoAssetBalanceTypeWrapper( hasMemo: false, imageCanonicalUri: '', name: '', - price: null, + marketData: null, symbol: '', }, }; @@ -70,7 +71,7 @@ export function convertFtBalancesToStacksFungibleTokenAssetBalanceType( export function addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance( assetBalance: StacksFungibleTokenAssetBalance, metadata: FtMetadataResponse, - price: Money | null + marketData: MarketData | null ) { return { ...assetBalance, @@ -86,7 +87,7 @@ export function addQueriedMetadataToInitializedStacksFungibleTokenAssetBalance( hasMemo: isTransferableStacksFungibleTokenAsset(assetBalance.asset), imageCanonicalUri: metadata.image_canonical_uri ?? '', name: metadata.name ?? '', - price, + marketData, symbol: metadata.symbol ?? '', }, }; diff --git a/src/shared/models/crypto-asset.model.ts b/src/shared/models/crypto-asset.model.ts index c91dc8d6f5b..78f75dc8adb 100644 --- a/src/shared/models/crypto-asset.model.ts +++ b/src/shared/models/crypto-asset.model.ts @@ -1,4 +1,4 @@ -import type { Money } from './money.model'; +import type { MarketData } from './market.model'; export interface BitcoinCryptoCurrencyAsset { decimals: number; @@ -24,7 +24,7 @@ export interface StacksFungibleTokenAsset { hasMemo: boolean; imageCanonicalUri: string; name: string; - price: Money | null; + marketData: MarketData | null; symbol: string; }