Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
feat: add user guided states on swap (#188)
Browse files Browse the repository at this point in the history
* feat: add use guided states on swap

* fix: consider any false value and no amount

* fix: has balance check
  • Loading branch information
luizstacio committed May 23, 2022
1 parent e97ed4f commit 2277e6b
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 55 deletions.
1 change: 0 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@tailwindcss/typography": "^0.5.2",
"classnames": "^2.3.1",
"clipboard": "^2.0.11",
"decimal.js": "^10.3.1",
"ethers": "^5.6.5",
"fuels": "0.0.0-master-d5e02003",
"graphql-request": "^4.2.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export const SLIPPAGE_TOLERANCE = 0.005;
// Max value supported
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
export const MAX_U64_VALUE = 0xffff_ffff_ffff_ffff;
// Max value from Sway Contract
export const MAX_U64_STRING = '18446744073709551615';
5 changes: 5 additions & 0 deletions packages/app/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { BigNumberish } from 'fuels';
import urljoin from 'url-join';

import { MAX_U64_STRING } from '~/config';

const { PUBLIC_URL } = process.env;

export const objectId = (value: string) => ({
Expand All @@ -10,3 +13,5 @@ export const objectId = (value: string) => ({
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export const relativeUrl = (path: string) => urljoin(PUBLIC_URL || '/', path);

export const isSwayInfinity = (value: BigNumberish | null) => value?.toString() === MAX_U64_STRING;
25 changes: 12 additions & 13 deletions packages/app/src/pages/SwapPage/PricePerToken.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Decimal } from "decimal.js";
import { BigNumber } from "ethers";
import { formatUnits } from "ethers/lib/utils";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { BiRefresh } from "react-icons/bi";
Expand All @@ -7,26 +8,24 @@ import { swapIsTypingAtom } from "./jotai";
import { ActiveInput } from "./types";

import { Button } from "~/components/Button";
import { DECIMAL_UNITS, ONE_ASSET } from "~/config";

const style = {
wrapper: `flex items-center gap-3 my-4 px-2 text-sm text-gray-400`,
};

function getPricePerToken(
direction: ActiveInput,
direction?: ActiveInput,
fromAmount?: bigint | null,
toAmount?: bigint | null
) {
// TODO: remove decimal.js and use fuels instead
// we decided to use decimal.js because of we're getting some issus
// when trying to divide between bigints
if (fromAmount && toAmount) {
const from = new Decimal(fromAmount.toString());
const to = new Decimal(toAmount.toString());
const price = direction === ActiveInput.from ? from.div(to) : to.div(from);
return price.toFixed(6);
}
return "";
if (!toAmount || !fromAmount) return "";
const ratio =
direction === ActiveInput.from
? BigNumber.from(fromAmount || 0).div(toAmount || 0)
: BigNumber.from(toAmount || 0).div(fromAmount || 0);
const price = ratio.mul(ONE_ASSET);
return formatUnits(price, DECIMAL_UNITS);
}

type PricePerTokenProps = {
Expand All @@ -42,7 +41,7 @@ export function PricePerToken({
toCoin,
toAmount,
}: PricePerTokenProps) {
const [direction, setDirection] = useState<ActiveInput>(ActiveInput.from);
const [direction, setDirection] = useState<ActiveInput>(ActiveInput.to);
const isTyping = useAtomValue(swapIsTypingAtom);

const pricePerToken = getPricePerToken(direction, fromAmount, toAmount);
Expand Down
10 changes: 6 additions & 4 deletions packages/app/src/pages/SwapPage/SwapComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ export function SwapComponent({
}, []);

useEffect(() => {
const amount =
activeInput.current === ActiveInput.from
? fromInput.amount
: toInput.amount;
const currentInput =
activeInput.current === ActiveInput.from ? fromInput : toInput;
const amount = currentInput.amount;
const coin = activeInput.current === ActiveInput.from ? coinFrom : coinTo;

// This is used to reset preview amount when set first input value for null
if (activeInput.current === ActiveInput.from && amount === null) {
Expand All @@ -101,7 +101,9 @@ export function SwapComponent({
from: coinFrom.assetId,
to: coinTo.assetId,
amount,
coin,
direction: activeInput.current,
hasBalance: fromInput.hasEnoughBalance,
});
}, [fromInput.amount, toInput.amount, coinFrom, coinTo]);

Expand Down
93 changes: 62 additions & 31 deletions packages/app/src/pages/SwapPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,75 @@
import type { CoinQuantity } from "fuels";
import { useState } from "react";
import toast from "react-hot-toast";
import { MdSwapCalls } from "react-icons/md";
import { useMutation, useQuery } from "react-query";
import { useNavigate } from "react-router-dom";

import { SwapComponent } from "./SwapComponent";
import { queryPreviewAmount, swapTokens } from "./queries";
import type { SwapState } from "./types";
import { ActiveInput } from "./types";

import { Button } from "~/components/Button";
import { Card } from "~/components/Card";
import { useContract } from "~/context/AppContext";
import { useBalances } from "~/hooks/useBalances";
import useDebounce from "~/hooks/useDebounce";
import { sleep } from "~/lib/utils";
import { Pages } from "~/types/pages";
import { isSwayInfinity, sleep } from "~/lib/utils";

const getBalanceAsset = (
balances: CoinQuantity[] | undefined,
assetId: string
) => balances?.find((item) => item.assetId === assetId);
type StateParams = {
swapState: SwapState | null;
previewAmount: bigint | null;
hasLiquidity: boolean;
};

enum ValidationStateEnum {
SelectToken = 0,
EnterAmount = 1,
InsufficientBalance = 2,
InsufficientLiquidity = 3,
Swap = 4,
}

const getValidationText = (
state: ValidationStateEnum,
swapState: SwapState | null
) => {
switch (state) {
case ValidationStateEnum.SelectToken:
return "Select token";
case ValidationStateEnum.EnterAmount:
return "Enter amount";
case ValidationStateEnum.InsufficientBalance:
return `Insufficient ${swapState?.coin.symbol || ""} balance`;
case ValidationStateEnum.InsufficientLiquidity:
return "Insufficient liquidity";
default:
return "Swap";
}
};

const getValidationState = ({
swapState,
previewAmount,
hasLiquidity,
}: StateParams): ValidationStateEnum => {
if (!swapState?.to || !swapState?.from) {
return ValidationStateEnum.SelectToken;
}
if (!swapState?.amount) {
return ValidationStateEnum.EnterAmount;
}
if (!swapState.hasBalance) {
return ValidationStateEnum.InsufficientBalance;
}
if (!hasLiquidity || isSwayInfinity(previewAmount))
return ValidationStateEnum.InsufficientLiquidity;
return ValidationStateEnum.Swap;
};

export default function SwapPage() {
const contract = useContract()!;
const [previewAmount, setPreviewAmount] = useState<bigint | null>(null);
const [swapState, setSwapState] = useState<SwapState | null>(null);
const [hasLiquidity, setHasLiquidity] = useState(true);
const debouncedState = useDebounce(swapState);
const { data: balances } = useBalances();
const navigate = useNavigate();

const { isLoading } = useQuery(
[
Expand Down Expand Up @@ -66,27 +105,18 @@ export default function SwapPage() {
}
);

const hasNotBalance =
!swapState ||
!swapState.amount ||
!getBalanceAsset(
balances,
swapState.direction === ActiveInput.to ? swapState.to : swapState.from
);

const shouldDisableButton =
isLoading || isSwaping || !hasLiquidity || !previewAmount || hasNotBalance;

function getButtonText() {
if (!hasLiquidity) return "Insufficient liquidity";
if (isSwaping) return "Loading...";
return "Swap";
}

function handleSwap(state: SwapState) {
setSwapState(state);
}

const validationState = getValidationState({
swapState,
previewAmount,
hasLiquidity,
});
const shouldDisableSwap =
isLoading || validationState !== ValidationStateEnum.Swap;

return (
<Card className="min-w-[450px]">
<Card.Title>
Expand All @@ -100,12 +130,13 @@ export default function SwapPage() {
/>
<Button
isFull
isLoading={isSwaping}
size="lg"
variant="primary"
isDisabled={shouldDisableButton}
isDisabled={shouldDisableSwap}
onPress={() => swap()}
>
{getButtonText()}
{getValidationText(validationState, swapState)}
</Button>
</Card>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/pages/SwapPage/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Coin } from '~/types';

export enum ActiveInput {
'from' = 'from',
'to' = 'to',
Expand All @@ -7,5 +9,7 @@ export type SwapState = {
from: string;
to: string;
direction: ActiveInput;
coin: Coin;
amount: bigint | null;
hasBalance: boolean;
};
9 changes: 3 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 comment on commit 2277e6b

@vercel
Copy link

@vercel vercel bot commented on 2277e6b May 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

swayswap – ./

swayswap.vercel.app
swayswap-fuel-labs.vercel.app
swayswap-git-master-fuel-labs.vercel.app

Please sign in to comment.