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

Commit

Permalink
refac: use state machine on swap
Browse files Browse the repository at this point in the history
  • Loading branch information
pedronauck committed Jun 15, 2022
1 parent 87dac27 commit b83af0d
Show file tree
Hide file tree
Showing 29 changed files with 1,387 additions and 468 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Expand Up @@ -4,4 +4,5 @@
dist
CHANGELOG.md
packages/app/src/types
contracts
contracts
**/*.typegen.ts
3 changes: 2 additions & 1 deletion .prettierignore
Expand Up @@ -11,4 +11,5 @@ yarn-lock.yaml
.github
packages/contracts
.pnpm-store
.env
.env
**/*.typegen.ts
9 changes: 7 additions & 2 deletions packages/app/package.json
Expand Up @@ -6,13 +6,16 @@
"scripts": {
"build": "tsc && vite build && pnpm create404",
"create404": "cp ./dist/index.html ./dist/404.html",
"dev": "vite",
"dev": "run-p vite xstate:typegen:watch",
"gh-preview": "sh ./scripts/gh-pages-preview.sh",
"contracts:init": "pnpm exec ts-node ./scripts/contracts-init",
"postinstall": "sh ./scripts/postinstall.sh",
"preview": "vite preview",
"test": "jest --verbose",
"test:watch": "jest --watch --detectOpenHandles"
"test:watch": "jest --watch --detectOpenHandles",
"vite": "vite",
"xstate:typegen": "xstate typegen 'src/**/*.ts?(x)'",
"xstate:typegen:watch": "xstate typegen 'src/**/*.ts?(x)' --watch"
},
"dependencies": {
"@ethersproject/bignumber": "^5.6.2",
Expand Down Expand Up @@ -62,6 +65,7 @@
"react-query": "^3.39.1",
"react-router-dom": "6",
"react-use": "^17.4.0",
"spacefold": "0.0.1-alpha.4",
"spinners-react": "^1.0.7",
"url-join-ts": "^1.0.5",
"vite-tsconfig-paths": "^3.5.0",
Expand All @@ -80,6 +84,7 @@
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-helmet": "^6.1.5",
"@vitejs/plugin-react": "^1.3.2",
"@xstate/cli": "^0.2.1",
"autoprefixer": "^10.4.7",
"dotenv": "^16.0.1",
"eslint": "^8.17.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/app/scripts/postinstall.sh
Expand Up @@ -4,3 +4,5 @@ ENV_FILE=.env
if [ ! -f "$FILE" ]; then
cp .env.example $ENV_FILE
fi

pnpm xstate:typegen
2 changes: 1 addition & 1 deletion packages/app/src/systems/Core/components/CoinInput.tsx
Expand Up @@ -57,7 +57,7 @@ export const CoinInput = forwardRef<HTMLInputElement, CoinInputProps>(
setValue(e.target.value);
}}
decimalScale={DECIMAL_UNITS}
placeholder="0"
placeholder={props.placeholder || "0"}
className="coinInput--input"
thousandSeparator={false}
onInput={onInput}
Expand Down
13 changes: 8 additions & 5 deletions packages/app/src/systems/Core/hooks/useBalances.ts
Expand Up @@ -3,15 +3,18 @@ import { useQuery } from 'react-query';

import { useWallet } from './useWallet';

import { swapEvts } from '~/systems/Swap/hooks/useSwap';
import { Queries } from '~/types';

export function useBalances(opts: UseQueryOptions = {}) {
const wallet = useWallet();

return useQuery(
Queries.UserQueryBalances,
async () => wallet?.getBalances(),
return useQuery(Queries.UserQueryBalances, async () => wallet?.getBalances(), {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
opts as any
);
...(opts as any),
onSuccess(data) {
opts.onSuccess?.(data);
swapEvts.refetchBalances.send(data);
},
});
}
9 changes: 2 additions & 7 deletions packages/app/src/systems/Pool/hooks/useUserPositions.ts
@@ -1,3 +1,5 @@
import { getPoolRatio } from '../utils/helpers';

import { usePoolInfo } from './usePoolInfo';

import {
Expand All @@ -10,13 +12,6 @@ import {
multiplyFn,
toFixed,
} from '~/systems/Core';
import type { PoolInfo } from '~/types/contracts/ExchangeContractAbi';

function getPoolRatio(info?: PoolInfo) {
const tokenReserve = toBigInt(info?.token_reserve || ZERO);
const ethReserve = toBigInt(info?.eth_reserve || ZERO);
return divideFnValidOnly(ethReserve, tokenReserve);
}

export function useUserPositions() {
const { data: info } = usePoolInfo();
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/systems/Pool/index.tsx
@@ -1,3 +1,4 @@
export * from "./hooks";
export * from "./pages";
export * from "./utils";
export * from "./routes";
export * from "./hooks";
18 changes: 11 additions & 7 deletions packages/app/src/systems/Pool/utils/helpers.ts
@@ -1,14 +1,18 @@
import { toNumber } from 'fuels';
import { toBigInt, toNumber } from 'fuels';

import { ZERO } from '~/systems/Core';
import { divideFnValidOnly, ZERO } from '~/systems/Core';
import type { Maybe } from '~/types';
import type { PoolInfo } from '~/types/contracts/ExchangeContractAbi';

export const calculateRatio = (
initialFromAmount?: Maybe<bigint>,
initialToAmount?: Maybe<bigint>
) => {
export function calculateRatio(initialFromAmount?: Maybe<bigint>, initialToAmount?: Maybe<bigint>) {
const fromAmount = initialFromAmount || ZERO;
const toAmount = initialToAmount || ZERO;
const ratio = toNumber(fromAmount) / toNumber(toAmount);
return Number.isNaN(ratio) || !Number.isFinite(ratio) ? 0 : ratio;
};
}

export function getPoolRatio(info?: PoolInfo) {
const tokenReserve = toBigInt(info?.token_reserve || ZERO);
const ethReserve = toBigInt(info?.eth_reserve || ZERO);
return divideFnValidOnly(ethReserve, tokenReserve);
}
1 change: 1 addition & 0 deletions packages/app/src/systems/Pool/utils/index.ts
@@ -1 +1,2 @@
export * from './helpers';
export * from './queries';
69 changes: 8 additions & 61 deletions packages/app/src/systems/Swap/components/PricePerToken.tsx
@@ -1,85 +1,32 @@
import { useEffect, useState } from "react";
import { AiOutlineSwap } from "react-icons/ai";

import { useValueIsTyping } from "../state";
import type { SwapState } from "../types";
import { SwapDirection } from "../types";
import { getPricePerToken } from "../utils/helpers";
import { usePricePerToken } from "../hooks/usePricePerToken";

import { ZERO } from "~/systems/Core";
import { Button } from "~/systems/UI";
import type { Maybe } from "~/types";

const style = {
wrapper: `flex items-center gap-3 my-4 px-2 text-sm text-gray-400`,
priceContainer: `min-w-[150px] cursor-pointer`,
};

type PricePerTokenProps = {
swapState?: Maybe<SwapState>;
previewAmount?: Maybe<bigint>;
isLoading?: boolean;
};

type Asset = {
symbol: string;
amount: bigint;
};

const createAsset = (
symbol?: Maybe<string>,
amount?: Maybe<bigint>
): Asset => ({
symbol: symbol || "",
amount: amount || ZERO,
});

export function PricePerToken({
swapState,
previewAmount,
isLoading,
}: PricePerTokenProps) {
const [[assetFrom, assetTo], setAssets] = useState<
[Maybe<Asset>, Maybe<Asset>]
>([null, null]);
const isTyping = useValueIsTyping();

useEffect(() => {
if (swapState?.direction === SwapDirection.fromTo) {
setAssets([
createAsset(swapState.coinFrom.symbol, swapState.amount),
createAsset(swapState.coinTo.symbol, previewAmount),
]);
} else if (swapState) {
setAssets([
createAsset(swapState.coinFrom.symbol, previewAmount),
createAsset(swapState.coinTo.symbol, swapState.amount),
]);
}
}, [swapState, previewAmount]);

function toggle() {
setAssets([assetTo, assetFrom]);
}

if (isTyping || isLoading) return null;
if (!assetFrom?.amount || !assetTo?.amount) return null;
const pricePerToken = getPricePerToken(assetFrom.amount, assetTo.amount);
export function PricePerToken() {
const data = usePricePerToken();

return (
<div
onClick={data.onToggleAssets}
className={style.wrapper}
onClick={toggle}
aria-label="Price per token"
>
<div className={style.priceContainer}>
<span className="text-gray-200">1</span> {assetFrom.symbol} ={" "}
<span className="text-gray-200">{pricePerToken}</span> {assetTo.symbol}
<span className="text-gray-200">1</span> {data.assetFrom.symbol} ={" "}
<span className="text-gray-200">{data.pricePerToken}</span>{" "}
{data.assetTo.symbol}
</div>
<Button
size="sm"
className="h-auto p-0 border-none"
onPress={toggle}
onPress={data.onToggleAssets}
aria-label="Invert token price"
>
<AiOutlineSwap size={20} />
Expand Down
71 changes: 17 additions & 54 deletions packages/app/src/systems/Swap/components/SwapPreview.tsx
@@ -1,58 +1,27 @@
import { BsArrowDown } from "react-icons/bs";

import { useValueIsTyping } from "../state";
import type { SwapInfo } from "../types";
import { useSwap } from "../hooks/useSwap";
import { useSwapPreview } from "../hooks/useSwapPreview";
import { SwapDirection } from "../types";
import { calculatePriceWithSlippage, calculatePriceImpact } from "../utils";

import {
PreviewItem,
PreviewTable,
useSlippage,
ZERO,
parseToFormattedNumber,
NetworkFeePreviewItem,
} from "~/systems/Core";
import type { Maybe } from "~/types";

type SwapPreviewProps = {
swapInfo: SwapInfo;
isLoading: boolean;
networkFee?: Maybe<bigint>;
};
export function SwapPreview() {
const { state } = useSwap();
const preview = useSwapPreview();

export function SwapPreview({
swapInfo,
networkFee,
isLoading,
}: SwapPreviewProps) {
const { amount, previewAmount, direction, coinFrom, coinTo } = swapInfo;
const isTyping = useValueIsTyping();
const slippage = useSlippage();
const { slippage } = preview;
const { coinTo, coinFrom, direction, txCost } = state;

if (
!coinFrom ||
!coinTo ||
!previewAmount ||
!direction ||
!amount ||
isLoading ||
isTyping
) {
return null;
}

// Expected amount of tokens to be received
const nextAmount =
direction === SwapDirection.fromTo ? previewAmount : amount || ZERO;

const outputAmount = parseToFormattedNumber(nextAmount);
const priceWithSlippage = calculatePriceWithSlippage(
previewAmount,
slippage.value,
direction
);
const inputAmountWithSlippage = parseToFormattedNumber(priceWithSlippage);
const isFrom = direction === SwapDirection.fromTo;
const inputSymbol = isFrom ? coinTo?.symbol : coinFrom?.symbol;
const inputText = isFrom
? "Min. received after slippage"
: "Max. sent after slippage";

return (
<div aria-label="preview-swap-output">
Expand All @@ -62,23 +31,17 @@ export function SwapPreview({
<PreviewTable title="Expected out:" className="my-2">
<PreviewItem
title={"You'll receive:"}
value={`${outputAmount} ${coinTo.symbol}`}
value={`${preview.outputAmount} ${coinTo?.symbol}`}
/>
<PreviewItem
title={"Price impact: "}
value={`${calculatePriceImpact(swapInfo)}%`}
value={`${preview.priceImpact}%`}
/>
<PreviewItem
title={`${
direction === SwapDirection.fromTo
? "Min. received after slippage"
: "Max. sent after slippage"
} (${slippage.formatted}):`}
value={`${inputAmountWithSlippage} ${
direction === SwapDirection.fromTo ? coinTo.symbol : coinFrom.symbol
}`}
title={`${inputText} (${slippage?.formatted}):`}
value={`${preview.inputAmount} ${inputSymbol}`}
/>
<NetworkFeePreviewItem networkFee={networkFee} />
<NetworkFeePreviewItem networkFee={txCost?.total} />
</PreviewTable>
</div>
);
Expand Down
35 changes: 35 additions & 0 deletions packages/app/src/systems/Swap/components/SwapWidget.tsx
@@ -0,0 +1,35 @@
import { useSwap } from "../hooks/useSwap";
import { useSwapCoinInput } from "../hooks/useSwapCoinInput";
import { useSwapCoinSelector } from "../hooks/useSwapCoinSelector";
import { FROM_TO, TO_FROM } from "../machines/swapMachine";

import { CoinInput, CoinSelector } from "~/systems/Core";
import { InvertButton } from "~/systems/UI";

export function SwapWidget() {
const { onInvertCoins } = useSwap();
const coinSelectorFromProps = useSwapCoinSelector(FROM_TO);
const coinSelectorToProps = useSwapCoinSelector(TO_FROM);
const coinInputFromProps = useSwapCoinInput(FROM_TO);
const coinInputToProps = useSwapCoinInput(TO_FROM);

return (
<>
<div className="mt-4">
<CoinInput
{...coinInputFromProps}
rightElement={<CoinSelector {...coinSelectorFromProps} />}
/>
</div>
<div className="flex items-center sm:justify-center -my-5">
<InvertButton onClick={onInvertCoins} />
</div>
<div className="mb-4">
<CoinInput
{...coinInputToProps}
rightElement={<CoinSelector {...coinSelectorToProps} />}
/>
</div>
</>
);
}
15 changes: 15 additions & 0 deletions packages/app/src/systems/Swap/hooks/useCoinByParam.tsx
@@ -0,0 +1,15 @@
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";

import { TOKENS } from "~/systems/Core";

export function useCoinByParam(coinDir: string) {
const [searchParams] = useSearchParams([["coinFrom", "ETH"]]);
const param = searchParams.get(coinDir);

return useMemo(() => {
if (param) {
return TOKENS.find((t) => t.assetId === param || t.symbol === param);
}
}, [param]);
}

0 comments on commit b83af0d

Please sign in to comment.