Skip to content

Commit

Permalink
refactor: swaps routes, closes #4317
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Apr 9, 2024
1 parent 03fe093 commit c3e3057
Show file tree
Hide file tree
Showing 28 changed files with 310 additions and 275 deletions.
4 changes: 3 additions & 1 deletion src/app/features/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ export function Container() {
const displayHeader = !isLandingPage(pathname) && !isNoHeaderPopup(pathname);
const isSessionLocked = getIsSessionLocked(pathname);

// TODO: Refactor? This is very hard to manage with dynamic routes. Temporarily
// added a fix to catch the swap route: '/swap/:base/:quote?'
function getOnGoBackLocation(pathname: RouteUrls) {
if (pathname.includes('/swap')) return navigate(RouteUrls.Home);
switch (pathname) {
case RouteUrls.Swap:
case RouteUrls.Fund.replace(':currency', 'STX'):
case RouteUrls.Fund.replace(':currency', 'BTC'):
case RouteUrls.SendCryptoAssetForm.replace(':symbol', 'stx'):
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/home/components/account-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function AccountActions() {
data-testid={HomePageSelectors.SwapBtn}
icon={<SwapIcon />}
label="Swap"
onClick={() => navigate(RouteUrls.Swap)}
onClick={() => navigate(RouteUrls.Swap.replace(':base', 'STX').replace(':quote', ''))}
/>
),
[ChainID.Testnet]: null,
Expand Down
35 changes: 18 additions & 17 deletions src/app/pages/swap/alex-swap-container.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { Outlet } from 'react-router-dom';

import { bytesToHex } from '@stacks/common';
import { ContractCallPayload, TransactionTypes } from '@stacks/connect';
Expand Down Expand Up @@ -30,6 +30,7 @@ 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 { useSwapNavigate } from './hooks/use-swap-navigate';
import { SwapContext, SwapProvider } from './swap.context';
import {
defaultSwapFee,
Expand All @@ -41,7 +42,7 @@ export const alexSwapRoutes = generateSwapRoutes(<AlexSwapContainer />);

function AlexSwapContainer() {
const [isSendingMax, setIsSendingMax] = useState(false);
const navigate = useNavigate();
const navigate = useSwapNavigate();
const { setIsLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
const currentAccount = useCurrentStacksAccount();
const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx();
Expand Down Expand Up @@ -78,14 +79,14 @@ function AlexSwapContainer() {
);

async function onSubmitSwapForReview(values: SwapFormValues) {
if (isUndefined(values.swapAssetFrom) || isUndefined(values.swapAssetTo)) {
if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) {
logger.error('Error submitting swap for review');
return;
}

const [router, lpFee] = await Promise.all([
alex.getRouter(values.swapAssetFrom.currency, values.swapAssetTo.currency),
alex.getFeeRate(values.swapAssetFrom.currency, values.swapAssetTo.currency),
alex.getRouter(values.swapAssetBase.currency, values.swapAssetQuote.currency),
alex.getFeeRate(values.swapAssetBase.currency, values.swapAssetQuote.currency),
]);

onSetSwapSubmissionData({
Expand All @@ -100,10 +101,10 @@ function AlexSwapContainer() {
.filter(isDefined),
slippage,
sponsored: isSponsoredByAlex,
swapAmountFrom: values.swapAmountFrom,
swapAmountTo: values.swapAmountTo,
swapAssetFrom: values.swapAssetFrom,
swapAssetTo: values.swapAssetTo,
swapAmountBase: values.swapAmountBase,
swapAmountQuote: values.swapAmountQuote,
swapAssetBase: values.swapAssetBase,
swapAssetQuote: values.swapAssetQuote,
timestamp: new Date().toISOString(),
});

Expand All @@ -117,8 +118,8 @@ function AlexSwapContainer() {
}

if (
isUndefined(swapSubmissionData.swapAssetFrom) ||
isUndefined(swapSubmissionData.swapAssetTo)
isUndefined(swapSubmissionData.swapAssetBase) ||
isUndefined(swapSubmissionData.swapAssetQuote)
) {
logger.error('No assets selected to perform swap');
return;
Expand All @@ -127,14 +128,14 @@ function AlexSwapContainer() {
setIsLoading();

const fromAmount = BigInt(
new BigNumber(swapSubmissionData.swapAmountFrom)
new BigNumber(swapSubmissionData.swapAmountBase)
.multipliedBy(oneHundredMillion)
.dp(0)
.toString()
);

const minToAmount = BigInt(
new BigNumber(swapSubmissionData.swapAmountTo)
new BigNumber(swapSubmissionData.swapAmountQuote)
.multipliedBy(oneHundredMillion)
.multipliedBy(new BigNumber(1).minus(slippage))
.dp(0)
Expand All @@ -143,8 +144,8 @@ function AlexSwapContainer() {

const tx = alex.runSwap(
currentAccount?.address,
swapSubmissionData.swapAssetFrom.currency,
swapSubmissionData.swapAssetTo.currency,
swapSubmissionData.swapAssetBase.currency,
swapSubmissionData.swapAssetQuote.currency,
fromAmount,
minToAmount,
swapSubmissionData.router.map(x => x.currency)
Expand Down Expand Up @@ -197,8 +198,8 @@ function AlexSwapContainer() {
onSetIsSendingMax: value => setIsSendingMax(value),
onSubmitSwapForReview,
onSubmitSwap,
swappableAssetsFrom: migratePositiveBalancesToTop(swappableAssets),
swappableAssetsTo: swappableAssets,
swappableAssetsBase: migratePositiveBalancesToTop(swappableAssets),
swappableAssetsQuote: swappableAssets,
swapSubmissionData,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function SwapAssetItem({ asset, onClick }: SwapAssetItemProps) {
const fallback = getAvatarFallback(asset.name);

return (
<Pressable data-testid={SwapSelectors.ChooseAssetListItem} onClick={onClick} my="space.02">
<Pressable data-testid={SwapSelectors.SwapAssetListItem} onClick={onClick} my="space.02">
<ItemLayout
flagImg={
<Avatar.Root>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,78 @@
import { useNavigate } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';

import { SwapSelectors } from '@tests/selectors/swap.selectors';
import BigNumber from 'bignumber.js';
import { useFormikContext } from 'formik';
import { Stack } from 'leather-styles/jsx';

import { createMoney } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
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 { SwapAsset, SwapFormValues } from '../../../hooks/use-swap-form';
import { useSwapChooseAssetState } from '../swap-choose-asset';
import { SwapAssetItem } from './swap-asset-item';

interface SwapAssetList {
assets: SwapAsset[];
type: string;
}
export function SwapAssetList({ assets }: SwapAssetList) {
export function SwapAssetList({ assets, type }: SwapAssetList) {
const { fetchToAmount } = useSwapContext();
const { swapListType } = useSwapChooseAssetState();
const { setFieldError, setFieldValue, values } = useFormikContext<SwapFormValues>();
const navigate = useNavigate();

const isFromList = swapListType === 'from';
const isToList = swapListType === 'to';
const { base, quote } = useParams();
const isBaseList = type === 'base';
const isQuoteList = type === 'quote';

const selectableAssets = assets.filter(
asset =>
(isFromList && asset.name !== values.swapAssetTo?.name) ||
(isToList && asset.name !== values.swapAssetFrom?.name)
(isBaseList && asset.name !== values.swapAssetQuote?.name) ||
(isQuoteList && asset.name !== values.swapAssetBase?.name)
);

async function onChooseAsset(asset: SwapAsset) {
async function onSelectAsset(asset: SwapAsset) {
let from: SwapAsset | undefined;
let to: SwapAsset | undefined;
if (isFromList) {
if (isBaseList) {
from = asset;
to = values.swapAssetTo;
await setFieldValue('swapAssetFrom', asset);
} else if (isToList) {
from = values.swapAssetFrom;
to = values.swapAssetQuote;
await setFieldValue('swapAssetBase', asset);
navigate(RouteUrls.Swap.replace(':base', from.name).replace(':quote', quote ?? ''));
} else if (isQuoteList) {
from = values.swapAssetBase;
to = asset;
await setFieldValue('swapAssetTo', asset);
setFieldError('swapAssetTo', undefined);
await setFieldValue('swapAssetQuote', asset);
setFieldError('swapAssetQuote', undefined);
navigate(RouteUrls.Swap.replace(':base', base ?? '').replace(':quote', to.name));
}

navigate(-1);
if (from && to && values.swapAmountFrom) {
const toAmount = await fetchToAmount(from, to, values.swapAmountFrom);
if (from && to && values.swapAmountBase) {
const toAmount = await fetchToAmount(from, to, values.swapAmountBase);
if (isUndefined(toAmount)) {
await setFieldValue('swapAmountTo', '');
await setFieldValue('swapAmountQuote', '');
return;
}
const toAmountAsMoney = createMoney(
convertAmountToFractionalUnit(new BigNumber(toAmount), to?.balance.decimals),
to?.balance.symbol ?? '',
to?.balance.decimals
);
await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney));
setFieldError('swapAmountTo', undefined);
await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(toAmountAsMoney));
setFieldError('swapAmountQuote', undefined);
}
}

return (
<Stack mb="space.05" p="space.05" width="100%" data-testid={SwapSelectors.ChooseAssetList}>
<Stack mb="space.05" p="space.05" width="100%" data-testid={SwapSelectors.SwapAssetList}>
{selectableAssets.map(asset => (
<SwapAssetItem
asset={asset}
key={asset.balance.symbol}
onClick={() => onChooseAsset(asset)}
onClick={() => onSelectAsset(asset)}
/>
))}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RouteUrls } from '@shared/route-urls';

import { Dialog } from '@app/ui/components/containers/dialog/dialog';
import { Header } from '@app/ui/components/containers/headers/header';

import { useSwapNavigate } from '../../hooks/use-swap-navigate';
import { useSwapContext } from '../../swap.context';
import { SwapAssetList } from './components/swap-asset-list';

export function SwapAssetDialogBase() {
const { swappableAssetsBase } = useSwapContext();
const navigate = useSwapNavigate();

return (
<Dialog
isShowing
onClose={() => navigate(RouteUrls.Swap)}
header={
<Header
title={
<>
Choose asset <br /> to swap
</>
}
variant="bigTitle"
onGoBack={() => navigate(RouteUrls.Swap)}
/>
}
>
<SwapAssetList assets={swappableAssetsBase} type="base" />
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RouteUrls } from '@shared/route-urls';

import { Dialog } from '@app/ui/components/containers/dialog/dialog';
import { Header } from '@app/ui/components/containers/headers/header';

import { useSwapNavigate } from '../../hooks/use-swap-navigate';
import { useSwapContext } from '../../swap.context';
import { SwapAssetList } from './components/swap-asset-list';

export function SwapAssetDialogQuote() {
const { swappableAssetsQuote } = useSwapContext();
const navigate = useSwapNavigate();

return (
<Dialog
isShowing
onClose={() => navigate(RouteUrls.Swap)}
header={
<Header
title={
<>
Choose asset <br /> to receive
</>
}
variant="bigTitle"
onGoBack={() => navigate(RouteUrls.Swap)}
/>
}
>
<SwapAssetList assets={swappableAssetsQuote} type="quote" />
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { ChevronDownIcon } from '@app/ui/icons/chevron-down-icon';
interface SelectAssetTriggerButtonProps {
icon?: string;
name: string;
onChooseAsset(): void;
onSelectAsset(): void;
symbol: string;
}
export function SelectAssetTriggerButton({
icon,
name,
onChooseAsset,
onSelectAsset,
symbol,
}: SelectAssetTriggerButtonProps) {
const [field] = useField(name);
Expand All @@ -24,7 +24,7 @@ export function SelectAssetTriggerButton({
return (
<Button
data-testid={SwapSelectors.SelectAssetTriggerBtn}
onClick={onChooseAsset}
onClick={onSelectAsset}
p="space.02"
variant="ghost"
{...field}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { useShowFieldError } from '@app/common/form-utils';
import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';

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

function getPlaceholderValue(name: string, values: SwapFormValues) {
if (name === 'swapAmountFrom' && isDefined(values.swapAssetFrom)) return '0';
if (name === 'swapAmountTo' && isDefined(values.swapAssetTo)) return '0';
if (name === 'swapAmountBase' && isDefined(values.swapAssetBase)) return '0';
if (name === 'swapAmountQuote' && isDefined(values.swapAssetQuote)) return '0';
return '-';
}

Expand All @@ -30,25 +30,28 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi
const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext();
const { setFieldError, setFieldValue, values } = useFormikContext<SwapFormValues>();
const [field] = useField(name);
const showError = useShowFieldError(name) && name === 'swapAmountFrom' && values.swapAssetTo;
const showError = useShowFieldError(name) && name === 'swapAmountBase' && values.swapAssetQuote;

async function onBlur(event: ChangeEvent<HTMLInputElement>) {
const { swapAssetFrom, swapAssetTo } = values;
if (isUndefined(swapAssetFrom) || isUndefined(swapAssetTo)) return;
const { swapAssetBase, swapAssetQuote } = values;
if (isUndefined(swapAssetBase) || isUndefined(swapAssetQuote)) return;
onSetIsSendingMax(false);
const value = event.currentTarget.value;
const toAmount = await fetchToAmount(swapAssetFrom, swapAssetTo, value);
const toAmount = await fetchToAmount(swapAssetBase, swapAssetQuote, value);
if (isUndefined(toAmount)) {
await setFieldValue('swapAmountTo', '');
await setFieldValue('swapAmountQuote', '');
return;
}
const toAmountAsMoney = createMoney(
convertAmountToFractionalUnit(new BigNumber(toAmount), values.swapAssetTo?.balance.decimals),
values.swapAssetTo?.balance.symbol ?? '',
values.swapAssetTo?.balance.decimals
convertAmountToFractionalUnit(
new BigNumber(toAmount),
values.swapAssetQuote?.balance.decimals
),
values.swapAssetQuote?.balance.symbol ?? '',
values.swapAssetQuote?.balance.decimals
);
await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney));
setFieldError('swapAmountTo', undefined);
await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(toAmountAsMoney));
setFieldError('swapAmountQuote', undefined);
}

return (
Expand Down
Loading

0 comments on commit c3e3057

Please sign in to comment.