Skip to content

Commit

Permalink
fix: async fetch for exchange rate
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Oct 23, 2023
1 parent 732ab3d commit 2650c5c
Show file tree
Hide file tree
Showing 14 changed files with 76 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,7 +48,7 @@ export const BitcoinContractEntryPointLayout = forwardRefWithAs(
fontVariantNumeric="tabular-nums"
textAlign="right"
>
{isLoading ? <SmallLoadingSpinner /> : formattedBalance.value}
{isLoading ? <LoadingSpinner size="sm" /> : formattedBalance.value}
</Text>
</Tooltip>
</HStack>
Expand Down
17 changes: 6 additions & 11 deletions src/app/components/loading-spinner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex alignItems="center" flexGrow={1} justifyContent="center" width="100%" {...props}>
<Spinner color={color('text-caption')} opacity={0.5} size="lg" />
</Flex>
);
}

export function SmallLoadingSpinner(props: FlexProps) {
return (
<Flex alignItems="center" flexGrow={1} justifyContent="center" width="100%" {...props}>
<Spinner color={color('text-caption')} opacity={0.5} size="sm" />
<Spinner color={token('colors.accent.text-subdued')} size={size} />
</Flex>
);
}
Expand Down
17 changes: 10 additions & 7 deletions src/app/pages/swap/components/swap-amount-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,28 @@ interface SwapAmountFieldProps {
name: string;
}
export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFieldProps) {
const { fetchToAmount, onSetIsSendingMax } = useSwapContext();
const { setErrors, setFieldValue, values } = useFormikContext<SwapFormValues>();
const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext();
const { setFieldError, setFieldValue, values } = useFormikContext<SwapFormValues>();
const [field] = useField(name);
const showError = useShowFieldError(name) && name === 'swapAmountFrom' && values.swapAssetTo;

async function onChange(event: ChangeEvent<HTMLInputElement>) {
async function onBlur(event: ChangeEvent<HTMLInputElement>) {
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 (
Expand All @@ -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"
Expand All @@ -68,7 +71,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi
textStyle="heading.05"
width="100%"
{...field}
onChange={onChange}
onBlur={onBlur}
/>
{amountAsFiat ? (
<styled.span color={showError ? 'error' : 'accent.text-subdued'} textStyle="caption.02">
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/swap/components/swap-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function SwapForm({ children }: HasChildren) {
initialValues={initialValues}
onSubmit={noop}
validateOnChange={false}
validateOnMount={true}
validationSchema={validationSchema}
>
<Box width="100%">
Expand Down
9 changes: 7 additions & 2 deletions src/app/pages/swap/components/swap-selected-asset-from.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwapFormValues>();
const [amountField, amountFieldMeta, amountFieldHelpers] = useField('swapAmountFrom');
const showError = useShowFieldError('swapAmountFrom');
Expand All @@ -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 ?? '',
Expand Down
9 changes: 8 additions & 1 deletion src/app/pages/swap/components/swap-selected-asset-to.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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');

Expand All @@ -28,7 +31,11 @@ export function SwapSelectedAssetTo({ onChooseAsset, title }: SwapSelectedAssetT
onChooseAsset={onChooseAsset}
showToggle
swapAmountInput={
<SwapAmountField amountAsFiat={amountAsFiat} isDisabled name="swapAmountTo" />
isFetchingExchangeRate ? (
<LoadingSpinner justifyContent="flex-end" size="sm" />
) : (
<SwapAmountField amountAsFiat={amountAsFiat} isDisabled name="swapAmountTo" />
)
}
symbol={assetField.value?.balance.symbol ?? 'Select asset'}
title={title}
Expand Down
9 changes: 1 addition & 8 deletions src/app/pages/swap/components/swap-selected-asset.layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,7 @@ export function SwapSelectedAssetLayout({
</HStack>
<SelectedAssetField
contentLeft={
<LeatherButton
onClick={onChooseAsset}
p="space.02"
// pr="space.02"
// py="space.02"
type="button"
variant="ghost"
>
<LeatherButton onClick={onChooseAsset} p="space.02" type="button" variant="ghost">
<HStack>
{icon && <styled.img src={icon} width="32px" height="32px" alt="Swap asset" />}
<styled.span textStyle="label.01">{symbol}</styled.span>
Expand Down
8 changes: 6 additions & 2 deletions src/app/pages/swap/components/swap-toggle-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwapFormValues>();

async function onToggleSwapAssets() {
Expand All @@ -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));
Expand All @@ -36,7 +40,7 @@ export function SwapToggleButton() {
return (
<styled.button
alignSelf="flex-start"
disabled={isUndefined(values.swapAssetTo)}
disabled={isUndefined(values.swapAssetTo) || isFetchingExchangeRate}
onClick={onToggleSwapAssets}
>
<SwapIcon transform="rotate(90)" />
Expand Down
17 changes: 14 additions & 3 deletions src/app/pages/swap/hooks/use-alex-swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function useAlexSwap() {
const alexSDK = useState(() => new AlexSDK())[0];
const [swapSubmissionData, setSwapSubmissionData] = useState<SwapSubmissionData>();
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();
Expand Down Expand Up @@ -70,17 +71,27 @@ export function useAlexSwap() {
from: SwapAsset,
to: SwapAsset,
fromAmount: string
): Promise<string> {
): Promise<string | undefined> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 ?? '',
Expand Down
4 changes: 4 additions & 0 deletions src/app/pages/swap/swap-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export function SwapContainer() {
alexSDK,
fetchToAmount,
createSwapAssetFromAlexCurrency,
isFetchingExchangeRate,
onSetIsFetchingExchangeRate,
onSetSwapSubmissionData,
slippage,
supportedCurrencies,
Expand Down Expand Up @@ -173,7 +175,9 @@ export function SwapContainer() {

const swapContextValue: SwapContext = {
fetchToAmount,
isFetchingExchangeRate,
isSendingMax,
onSetIsFetchingExchangeRate,
onSetIsSendingMax: value => setIsSendingMax(value),
onSubmitSwapForReview,
onSubmitSwap,
Expand Down
4 changes: 3 additions & 1 deletion src/app/pages/swap/swap.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export interface SwapSubmissionData extends SwapFormValues {
}

export interface SwapContext {
fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise<string>;
fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise<string | undefined>;
isFetchingExchangeRate: boolean;
isSendingMax: boolean;
onSetIsFetchingExchangeRate(value: boolean): void;
onSetIsSendingMax(value: boolean): void;
onSubmitSwapForReview(values: SwapFormValues): Promise<void> | void;
onSubmitSwap(): Promise<void> | void;
Expand Down
4 changes: 2 additions & 2 deletions src/app/pages/swap/swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwapFormValues>();

Expand All @@ -38,7 +38,7 @@ export function Swap() {
</SwapContentLayout>
<SwapFooterLayout>
<LeatherButton
disabled={!(dirty && isValid)}
disabled={!(dirty && isValid) || isFetchingExchangeRate}
onClick={async () => {
handleSubmit();
await onSubmitSwapForReview(values);
Expand Down
7 changes: 7 additions & 0 deletions theme/recipes/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
Expand Down

0 comments on commit 2650c5c

Please sign in to comment.