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 25, 2023
1 parent 08b0e9c commit c7c5c3e
Show file tree
Hide file tree
Showing 17 changed files with 1,707 additions and 1,847 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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function SwapDetails() {

return (
<SwapDetailsLayout>
<SwapDetailLayout title="Protocol" value={swapSubmissionData.protocol} />
<SwapDetailLayout title="Powered by" value={swapSubmissionData.protocol} />
<SwapDetailLayout
title="Route"
value={
Expand Down
7 changes: 4 additions & 3 deletions src/app/pages/swap/components/swap-form.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Form, Formik } from 'formik';
import { Box } from 'leather-styles/jsx';

import { noop } from '@shared/utils';

import { HasChildren } from '@app/common/has-children';

import { useSwapForm } from '../hooks/use-swap-form';
import { useSwapContext } from '../swap.context';

export function SwapForm({ children }: HasChildren) {
const { initialValues, validationSchema } = useSwapForm();
const { onSubmitSwapForReview } = useSwapContext();

return (
<Formik
initialValues={initialValues}
onSubmit={noop}
onSubmit={onSubmitSwapForReview}
validateOnChange={false}
validateOnMount
validationSchema={validationSchema}
>
<Box width="100%">
Expand Down
11 changes: 8 additions & 3 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 All @@ -71,7 +76,7 @@ export function SwapSelectedAssetFrom({ onChooseAsset, title }: SwapSelectedAsse
name="swapAmountFrom"
/>
}
symbol={assetField.value.balance.symbol}
symbol={assetField.value.name}
title={title}
tooltipLabel={isSendingMax ? sendingMaxTooltip : maxAvailableTooltip}
value={formattedBalance}
Expand Down
11 changes: 9 additions & 2 deletions 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,9 +31,13 @@ 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'}
symbol={assetField.value?.name ?? 'Select asset'}
title={title}
value={assetField.value?.balance ? formatMoneyWithoutSymbol(assetField.value?.balance) : '0'}
/>
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 @@ -24,7 +24,7 @@ export function SwapAssetItem({ asset }: SwapAssetItemProps) {
<styled.span textStyle="label.01">{formatMoneyWithoutSymbol(asset.balance)}</styled.span>
</HStack>
<HStack alignItems="center" justifyContent="space-between">
<styled.span textStyle="caption.01">{asset.balance.symbol}</styled.span>
<styled.span textStyle="caption.01">{asset.name}</styled.span>
<styled.span color="accent.text-subdued" textStyle="caption.01">
{balanceAsFiat}
</styled.span>
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
6 changes: 5 additions & 1 deletion 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 @@ -137,7 +139,7 @@ export function SwapContainer() {
swapSubmissionData.router.map(x => x.currency)
);

// TODO: Add choose fee step for swaps
// TODO: Add choose fee step
const tempFormValues = {
fee: swapSubmissionData.fee,
feeCurrency: swapSubmissionData.feeCurrency,
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
14 changes: 3 additions & 11 deletions src/app/pages/swap/swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ import { SwapFormValues } from './hooks/use-swap-form';
import { useSwapContext } from './swap.context';

export function Swap() {
const { onSubmitSwapForReview, swappableAssetsFrom } = useSwapContext();
const { dirty, handleSubmit, isValid, setFieldValue, values } =
useFormikContext<SwapFormValues>();
const { isFetchingExchangeRate, swappableAssetsFrom } = useSwapContext();
const { dirty, isValid, setFieldValue, values } = useFormikContext<SwapFormValues>();

useRouteHeader(<ModalHeader defaultGoBack hideActions title="Swap" />, true);

Expand All @@ -37,14 +36,7 @@ export function Swap() {
<SwapSelectedAssets />
</SwapContentLayout>
<SwapFooterLayout>
<LeatherButton
disabled={!(dirty && isValid)}
onClick={async () => {
handleSubmit();
await onSubmitSwapForReview(values);
}}
width="100%"
>
<LeatherButton disabled={!(dirty && isValid) || isFetchingExchangeRate} width="100%">
Review and swap
</LeatherButton>
</SwapFooterLayout>
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
Loading

0 comments on commit c7c5c3e

Please sign in to comment.