From 2650c5c51db9743729d5e8d07051b7533d97d4fd Mon Sep 17 00:00:00 2001 From: fbwoolf Date: Mon, 23 Oct 2023 15:01:56 -0500 Subject: [PATCH] fix: async fetch for exchange rate --- .../bitcoin-contract-entry-point-layout.tsx | 4 ++-- src/app/components/loading-spinner.tsx | 17 ++++++----------- .../pages/swap/components/swap-amount-field.tsx | 17 ++++++++++------- src/app/pages/swap/components/swap-form.tsx | 1 + .../components/swap-selected-asset-from.tsx | 9 +++++++-- .../swap/components/swap-selected-asset-to.tsx | 9 ++++++++- .../components/swap-selected-asset.layout.tsx | 9 +-------- .../swap/components/swap-toggle-button.tsx | 8 ++++++-- src/app/pages/swap/hooks/use-alex-swap.tsx | 17 ++++++++++++++--- .../components/swap-asset-list.tsx | 5 +++++ src/app/pages/swap/swap-container.tsx | 4 ++++ src/app/pages/swap/swap.context.ts | 4 +++- src/app/pages/swap/swap.tsx | 4 ++-- theme/recipes/button.ts | 7 +++++++ 14 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx b/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx index e45c3a21295..3a9dd85f6ab 100644 --- a/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx +++ b/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx @@ -11,7 +11,7 @@ import { Flag } from '@app/components/layout/flag'; import { Tooltip } from '@app/components/tooltip'; import { Caption, Text } from '@app/components/typography'; -import { SmallLoadingSpinner } from '../loading-spinner'; +import { LoadingSpinner } from '../loading-spinner'; interface BitcoinContractEntryPointLayoutProps extends StackProps { balance: Money; @@ -48,7 +48,7 @@ export const BitcoinContractEntryPointLayout = forwardRefWithAs( fontVariantNumeric="tabular-nums" textAlign="right" > - {isLoading ? : formattedBalance.value} + {isLoading ? : formattedBalance.value} diff --git a/src/app/components/loading-spinner.tsx b/src/app/components/loading-spinner.tsx index 1e195f10006..7eeb2633e1b 100644 --- a/src/app/components/loading-spinner.tsx +++ b/src/app/components/loading-spinner.tsx @@ -1,17 +1,12 @@ -import { Flex, FlexProps, Spinner, color } from '@stacks/ui'; +import { Spinner, SpinnerSize } from '@stacks/ui'; +import { Flex, FlexProps } from 'leather-styles/jsx'; +import { token } from 'leather-styles/tokens'; -export function LoadingSpinner(props: FlexProps) { +export function LoadingSpinner(props: { size?: SpinnerSize } & FlexProps) { + const { size = 'lg' } = props; return ( - - - ); -} - -export function SmallLoadingSpinner(props: FlexProps) { - return ( - - + ); } diff --git a/src/app/pages/swap/components/swap-amount-field.tsx b/src/app/pages/swap/components/swap-amount-field.tsx index 9c99884857a..50a2e44dd60 100644 --- a/src/app/pages/swap/components/swap-amount-field.tsx +++ b/src/app/pages/swap/components/swap-amount-field.tsx @@ -26,25 +26,28 @@ interface SwapAmountFieldProps { name: string; } export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFieldProps) { - const { fetchToAmount, onSetIsSendingMax } = useSwapContext(); - const { setErrors, setFieldValue, values } = useFormikContext(); + const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); + const { setFieldError, setFieldValue, values } = useFormikContext(); const [field] = useField(name); const showError = useShowFieldError(name) && name === 'swapAmountFrom' && values.swapAssetTo; - async function onChange(event: ChangeEvent) { + async function onBlur(event: ChangeEvent) { const { swapAssetFrom, swapAssetTo } = values; if (isUndefined(swapAssetFrom) || isUndefined(swapAssetTo)) return; onSetIsSendingMax(false); const value = event.currentTarget.value; const toAmount = await fetchToAmount(swapAssetFrom, swapAssetTo, value); + if (isUndefined(toAmount)) { + await setFieldValue('swapAmountTo', ''); + return; + } const toAmountAsMoney = createMoney( convertAmountToFractionalUnit(new BigNumber(toAmount), values.swapAssetTo?.balance.decimals), values.swapAssetTo?.balance.symbol ?? '', values.swapAssetTo?.balance.decimals ); await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney)); - field.onChange(event); - setErrors({}); + setFieldError('swapAmountTo', undefined); } return ( @@ -58,7 +61,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi border="none" color={showError ? 'error' : 'accent.text-primary'} display="block" - disabled={isDisabled} + disabled={isDisabled || isFetchingExchangeRate} id={name} maxLength={15} p="0px" @@ -68,7 +71,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi textStyle="heading.05" width="100%" {...field} - onChange={onChange} + onBlur={onBlur} /> {amountAsFiat ? ( diff --git a/src/app/pages/swap/components/swap-form.tsx b/src/app/pages/swap/components/swap-form.tsx index 6610dd169bd..5e1c6953bbe 100644 --- a/src/app/pages/swap/components/swap-form.tsx +++ b/src/app/pages/swap/components/swap-form.tsx @@ -15,6 +15,7 @@ export function SwapForm({ children }: HasChildren) { initialValues={initialValues} onSubmit={noop} validateOnChange={false} + validateOnMount={true} validationSchema={validationSchema} > diff --git a/src/app/pages/swap/components/swap-selected-asset-from.tsx b/src/app/pages/swap/components/swap-selected-asset-from.tsx index 9e8a9bc8098..3fc6c078879 100644 --- a/src/app/pages/swap/components/swap-selected-asset-from.tsx +++ b/src/app/pages/swap/components/swap-selected-asset-from.tsx @@ -23,7 +23,8 @@ interface SwapSelectedAssetFromProps { title: string; } export function SwapSelectedAssetFrom({ onChooseAsset, title }: SwapSelectedAssetFromProps) { - const { fetchToAmount, isSendingMax, onSetIsSendingMax } = useSwapContext(); + const { fetchToAmount, isFetchingExchangeRate, isSendingMax, onSetIsSendingMax } = + useSwapContext(); const { setFieldValue, setFieldError, values } = useFormikContext(); const [amountField, amountFieldMeta, amountFieldHelpers] = useField('swapAmountFrom'); const showError = useShowFieldError('swapAmountFrom'); @@ -40,12 +41,16 @@ export function SwapSelectedAssetFrom({ onChooseAsset, title }: SwapSelectedAsse async function onSetMaxBalanceAsAmountToSwap() { const { swapAssetFrom, swapAssetTo } = values; - if (isUndefined(swapAssetFrom)) return; + if (isFetchingExchangeRate || isUndefined(swapAssetFrom)) return; onSetIsSendingMax(!isSendingMax); await amountFieldHelpers.setValue(Number(formattedBalance)); await amountFieldHelpers.setTouched(true); if (isUndefined(swapAssetTo)) return; const toAmount = await fetchToAmount(swapAssetFrom, swapAssetTo, formattedBalance); + if (isUndefined(toAmount)) { + await setFieldValue('swapAmountTo', ''); + return; + } const toAmountAsMoney = createMoney( convertAmountToFractionalUnit(new BigNumber(toAmount), values.swapAssetTo?.balance.decimals), values.swapAssetTo?.balance.symbol ?? '', diff --git a/src/app/pages/swap/components/swap-selected-asset-to.tsx b/src/app/pages/swap/components/swap-selected-asset-to.tsx index 43ff65b2738..67f03fe1d80 100644 --- a/src/app/pages/swap/components/swap-selected-asset-to.tsx +++ b/src/app/pages/swap/components/swap-selected-asset-to.tsx @@ -1,8 +1,10 @@ import { useField } from 'formik'; import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; +import { LoadingSpinner } from '@app/components/loading-spinner'; import { useAlexSdkAmountAsFiat } from '../hooks/use-alex-sdk-fiat-price'; +import { useSwapContext } from '../swap.context'; import { SwapAmountField } from './swap-amount-field'; import { SwapSelectedAssetLayout } from './swap-selected-asset.layout'; @@ -11,6 +13,7 @@ interface SwapSelectedAssetToProps { title: string; } export function SwapSelectedAssetTo({ onChooseAsset, title }: SwapSelectedAssetToProps) { + const { isFetchingExchangeRate } = useSwapContext(); const [amountField] = useField('swapAmountTo'); const [assetField] = useField('swapAssetTo'); @@ -28,7 +31,11 @@ export function SwapSelectedAssetTo({ onChooseAsset, title }: SwapSelectedAssetT onChooseAsset={onChooseAsset} showToggle swapAmountInput={ - + isFetchingExchangeRate ? ( + + ) : ( + + ) } symbol={assetField.value?.balance.symbol ?? 'Select asset'} title={title} diff --git a/src/app/pages/swap/components/swap-selected-asset.layout.tsx b/src/app/pages/swap/components/swap-selected-asset.layout.tsx index ce224b41d3a..7def032844c 100644 --- a/src/app/pages/swap/components/swap-selected-asset.layout.tsx +++ b/src/app/pages/swap/components/swap-selected-asset.layout.tsx @@ -60,14 +60,7 @@ export function SwapSelectedAssetLayout({ + {icon && } {symbol} diff --git a/src/app/pages/swap/components/swap-toggle-button.tsx b/src/app/pages/swap/components/swap-toggle-button.tsx index e0ed5194d1c..6ffafcff1e4 100644 --- a/src/app/pages/swap/components/swap-toggle-button.tsx +++ b/src/app/pages/swap/components/swap-toggle-button.tsx @@ -9,7 +9,7 @@ import { SwapFormValues } from '../hooks/use-swap-form'; import { useSwapContext } from '../swap.context'; export function SwapToggleButton() { - const { fetchToAmount, onSetIsSendingMax } = useSwapContext(); + const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext(); const { setFieldValue, validateForm, values } = useFormikContext(); async function onToggleSwapAssets() { @@ -26,6 +26,10 @@ export function SwapToggleButton() { if (isDefined(prevAssetFrom) && isDefined(prevAssetTo)) { const toAmount = await fetchToAmount(prevAssetTo, prevAssetFrom, prevAmountTo); + if (isUndefined(toAmount)) { + await setFieldValue('swapAmountTo', ''); + return; + } await setFieldValue('swapAmountTo', Number(toAmount)); } else { await setFieldValue('swapAmountTo', Number(prevAmountFrom)); @@ -36,7 +40,7 @@ export function SwapToggleButton() { return ( diff --git a/src/app/pages/swap/hooks/use-alex-swap.tsx b/src/app/pages/swap/hooks/use-alex-swap.tsx index 48c5ad89f06..eee4fdb2a2a 100644 --- a/src/app/pages/swap/hooks/use-alex-swap.tsx +++ b/src/app/pages/swap/hooks/use-alex-swap.tsx @@ -22,6 +22,7 @@ export function useAlexSwap() { const alexSDK = useState(() => new AlexSDK())[0]; const [swapSubmissionData, setSwapSubmissionData] = useState(); const [slippage, _setSlippage] = useState(0.04); + const [isFetchingExchangeRate, setIsFetchingExchangeRate] = useState(false); const { data: supportedCurrencies = [] } = useSwappableCurrencyQuery(alexSDK); const { result: prices } = useAsync(async () => await alexSDK.getLatestPrices(), [alexSDK]); const { availableBalance: availableStxBalance } = useStxBalance(); @@ -70,17 +71,27 @@ export function useAlexSwap() { from: SwapAsset, to: SwapAsset, fromAmount: string - ): Promise { + ): Promise { const amount = new BigNumber(fromAmount).multipliedBy(oneHundredMillion).dp(0).toString(); const amountAsBigInt = isNaN(Number(amount)) ? BigInt(0) : BigInt(amount); - const result = await alexSDK.getAmountTo(from.currency, amountAsBigInt, to.currency); - return new BigNumber(Number(result)).dividedBy(oneHundredMillion).toString(); + try { + setIsFetchingExchangeRate(true); + const result = await alexSDK.getAmountTo(from.currency, amountAsBigInt, to.currency); + setIsFetchingExchangeRate(false); + return new BigNumber(Number(result)).dividedBy(oneHundredMillion).toString(); + } catch (e) { + logger.error('Error fetching exchange rate from ALEX', e); + setIsFetchingExchangeRate(false); + return; + } } return { alexSDK, fetchToAmount, createSwapAssetFromAlexCurrency, + isFetchingExchangeRate, + onSetIsFetchingExchangeRate: (value: boolean) => setIsFetchingExchangeRate(value), onSetSwapSubmissionData: (value: SwapSubmissionData) => setSwapSubmissionData(value), slippage, supportedCurrencies, diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx index a21e1ce08d4..eee860036c8 100644 --- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx +++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx @@ -5,6 +5,7 @@ import { useFormikContext } from 'formik'; import { styled } from 'leather-styles/jsx'; import { createMoney } from '@shared/models/money.model'; +import { isUndefined } from '@shared/utils'; import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; @@ -49,6 +50,10 @@ export function SwapAssetList({ assets }: SwapAssetList) { navigate(-1); if (from && to && values.swapAmountFrom) { const toAmount = await fetchToAmount(from, to, values.swapAmountFrom); + if (isUndefined(toAmount)) { + await setFieldValue('swapAmountTo', ''); + return; + } const toAmountAsMoney = createMoney( convertAmountToFractionalUnit(new BigNumber(toAmount), to?.balance.decimals), to?.balance.symbol ?? '', diff --git a/src/app/pages/swap/swap-container.tsx b/src/app/pages/swap/swap-container.tsx index 93186e326b0..56a1765c73a 100644 --- a/src/app/pages/swap/swap-container.tsx +++ b/src/app/pages/swap/swap-container.tsx @@ -47,6 +47,8 @@ export function SwapContainer() { alexSDK, fetchToAmount, createSwapAssetFromAlexCurrency, + isFetchingExchangeRate, + onSetIsFetchingExchangeRate, onSetSwapSubmissionData, slippage, supportedCurrencies, @@ -173,7 +175,9 @@ export function SwapContainer() { const swapContextValue: SwapContext = { fetchToAmount, + isFetchingExchangeRate, isSendingMax, + onSetIsFetchingExchangeRate, onSetIsSendingMax: value => setIsSendingMax(value), onSubmitSwapForReview, onSubmitSwap, diff --git a/src/app/pages/swap/swap.context.ts b/src/app/pages/swap/swap.context.ts index a74d7d12cdf..0634881a577 100644 --- a/src/app/pages/swap/swap.context.ts +++ b/src/app/pages/swap/swap.context.ts @@ -12,8 +12,10 @@ export interface SwapSubmissionData extends SwapFormValues { } export interface SwapContext { - fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise; + fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise; + isFetchingExchangeRate: boolean; isSendingMax: boolean; + onSetIsFetchingExchangeRate(value: boolean): void; onSetIsSendingMax(value: boolean): void; onSubmitSwapForReview(values: SwapFormValues): Promise | void; onSubmitSwap(): Promise | void; diff --git a/src/app/pages/swap/swap.tsx b/src/app/pages/swap/swap.tsx index a5acf3fa7e4..3ec78596442 100644 --- a/src/app/pages/swap/swap.tsx +++ b/src/app/pages/swap/swap.tsx @@ -17,7 +17,7 @@ import { SwapFormValues } from './hooks/use-swap-form'; import { useSwapContext } from './swap.context'; export function Swap() { - const { onSubmitSwapForReview, swappableAssetsFrom } = useSwapContext(); + const { isFetchingExchangeRate, onSubmitSwapForReview, swappableAssetsFrom } = useSwapContext(); const { dirty, handleSubmit, isValid, setFieldValue, values } = useFormikContext(); @@ -38,7 +38,7 @@ export function Swap() { { handleSubmit(); await onSubmitSwapForReview(values); diff --git a/theme/recipes/button.ts b/theme/recipes/button.ts index 7f80ac1234d..710c76dc6e4 100644 --- a/theme/recipes/button.ts +++ b/theme/recipes/button.ts @@ -64,6 +64,13 @@ export const buttonRecipe = defineRecipe({ color: 'brown.1', _hover: { bg: 'brown.10' }, _active: { bg: 'brown.12' }, + _disabled: { + _hover: { + bg: 'brown.6', + }, + bg: 'brown.6', + color: 'white', + }, ...focusStyles, ...loadingStyles('brown.2'), },