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

refactor: swap with state machines #283

Merged
merged 40 commits into from Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8642231
chore: add Maybe type
pedronauck Jun 13, 2022
10fbb4d
refac: move useCoinInput to a separate file
pedronauck Jun 13, 2022
b5543c7
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap2
pedronauck Jun 13, 2022
44f689d
refac: rename ActiveInput enum to SwapDirection
pedronauck Jun 13, 2022
8a3e634
refac: code improvements
pedronauck Jun 13, 2022
87dac27
fix: dependency cycle
pedronauck Jun 14, 2022
b83af0d
refac: use state machine on swap
pedronauck Jun 15, 2022
82ccd54
refac: improve state machine
pedronauck Jun 15, 2022
e4066c3
refactor: remove refetch balances on swap machine
pedronauck Jun 15, 2022
813ed22
fix: add insufficient eth for gas fee as first validation
pedronauck Jun 15, 2022
4df5a13
chore: rename fetch pool info state
pedronauck Jun 15, 2022
58eb87b
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 17, 2022
bb3ff36
refac: add safeBigInt method
pedronauck Jun 17, 2022
7f88aea
chore: udpate deps
pedronauck Jun 18, 2022
15a1059
fix: adapt swap to new CoinBalance component
pedronauck Jun 18, 2022
432daec
fix: keep swap service between refreshs on dev
pedronauck Jun 18, 2022
40ef08d
fix: tests
pedronauck Jun 18, 2022
5c2f3a4
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 20, 2022
1843e80
chore: some few small script changes
pedronauck Jun 20, 2022
f6bf8be
fix: swap url params for coin selected
pedronauck Jun 20, 2022
63d4673
fix: price impact calculation
pedronauck Jun 20, 2022
8cc9c5e
fix: tests
pedronauck Jun 20, 2022
2513527
fix: tests [2]
pedronauck Jun 20, 2022
965dea2
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 20, 2022
f7b9d67
fix: tests [3]
pedronauck Jun 20, 2022
036ad1f
fix: tests [4]
pedronauck Jun 20, 2022
9253a8a
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 21, 2022
bf483f8
chore: missing requested changes from review
pedronauck Jun 21, 2022
536cf09
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 21, 2022
0107f73
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 24, 2022
5a2006e
chore: add tx feedback on swap
pedronauck Jun 24, 2022
3ff2ad4
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 24, 2022
30bcac3
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 24, 2022
b3dfec3
Merge branch 'master' of github.com:FuelLabs/swayswap into refac/swap
pedronauck Jun 24, 2022
a703923
chore: requested changes
pedronauck Jun 27, 2022
6d619c5
chore: requested changes [2]
pedronauck Jun 27, 2022
efec675
fix: swap machine validations
pedronauck Jun 27, 2022
defdf7f
fix: coin balance decimals
pedronauck Jun 27, 2022
3d579ca
fix: tests
pedronauck Jun 27, 2022
429ffa0
fix: e2e tests
pedronauck Jun 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -51,6 +54,7 @@
"fuels": "0.0.0-master-3caba861",
"graphql-request": "^4.3.0",
"jotai": "^1.7.1",
"mitt": "^3.0.0",
"react": "^18.1.0",
"react-content-loader": "^6.2.0",
"react-dom": "^18.1.0",
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion packages/app/src/systems/Core/components/AssetItem.tsx
@@ -1,4 +1,6 @@
import { CoinInput, useCoinInput } from "./CoinInput";
import { useCoinInput } from "../hooks/useCoinInput";

import { CoinInput } from "./CoinInput";
import { CoinSelector } from "./CoinSelector";

import type { Coin } from "~/types";
Expand Down
174 changes: 4 additions & 170 deletions packages/app/src/systems/Core/components/CoinInput.tsx
@@ -1,183 +1,17 @@
import type { ReactNode } from "react";
import { useMemo, forwardRef, useEffect, useState } from "react";
import type { NumberFormatValues } from "react-number-format";
import { forwardRef, useEffect, useState } from "react";
import NumberFormat from "react-number-format";

import { useBalances } from "../hooks";
import {
COIN_ETH,
formatUnits,
parseUnits,
toBigInt,
MAX_U64_VALUE,
parseToFormattedNumber,
ZERO,
} from "../utils";

import type { CoinSelectorProps } from "./CoinSelector";
import type { CoinInputProps } from "../hooks/useCoinInput";
import { parseToFormattedNumber } from "../utils";

import { DECIMAL_UNITS } from "~/config";
import type { NumberInputProps } from "~/systems/UI";
import { Spinner } from "~/systems/UI";
import type { Coin } from "~/types";

type UseCoinParams = {
/**
* Props for <CoinInput />
*/
amount?: bigint | null;
coin?: Coin | null;
gasFee?: bigint | null;
isReadOnly?: boolean;
onInput?: (...args: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
onChange?: (val: bigint | null) => void;
/**
* Coins for <CoinSelector />
*/
showBalance?: boolean;
showMaxButton?: boolean;
onChangeCoin?: (coin: Coin) => void;
disableWhenEth?: boolean;
};

export type UseCoinInput = {
amount: bigint | null;
setAmount: React.Dispatch<React.SetStateAction<bigint | null>>;
setGasFee: React.Dispatch<React.SetStateAction<bigint | null>>;
getInputProps: () => CoinInputProps;
getCoinSelectorProps: () => CoinSelectorProps;
formatted: string;
hasEnoughBalance: boolean;
};

type DisplayType = "input" | "text";

const parseValue = (value: string) => (value === "." ? "0." : value);

const parseValueBigInt = (value: string) => {
if (value !== "") {
const nextValue = parseValue(value);
return toBigInt(parseUnits(nextValue));
}
return ZERO;
};

const formatValue = (amount: bigint | null | undefined) => {
if (amount != null) {
return formatUnits(amount);
}
// If amount is null return empty string
return "";
};

export function useCoinInput({
amount: initialAmount,
onChange,
coin,
gasFee: initialGasFee,
isReadOnly,
onInput,
showBalance,
showMaxButton,
onChangeCoin,
disableWhenEth,
...params
}: UseCoinParams): UseCoinInput {
const [amount, setAmount] = useState<bigint | null>(null);
const [gasFee, setGasFee] = useState<bigint | null>(initialGasFee || ZERO);
const { data: balances } = useBalances();
const coinBalance = balances?.find((item) => item.assetId === coin?.assetId);
const isEth = useMemo(() => coin?.assetId === COIN_ETH, [coin?.assetId]);

useEffect(() => {
if (initialGasFee) setGasFee(initialGasFee);
}, [initialGasFee]);

// TODO: consider real gas fee, replacing GAS_FEE variable.
// For now we need to keep 1 unit in the wallet(it's not spent) in order to complete "create pool" transaction.
function getSafeMaxBalance() {
const next = coinBalance?.amount || ZERO;
const value = next > ZERO ? next - (gasFee || ZERO) : next;
if (value < ZERO) return ZERO;
return value;
}

const handleInputPropsChange = (val: string) => {
if (isReadOnly) return;
const next = val !== "" ? parseValueBigInt(val) : null;
if (typeof onChange === "function") {
onChange(next);
} else {
setAmount(next);
}
};

const isAllowed = ({ value }: NumberFormatValues) =>
parseValueBigInt(value) <= MAX_U64_VALUE;

function getInputProps() {
return {
...params,
value: formatValue(amount),
displayType: (isReadOnly ? "text" : "input") as DisplayType,
onInput,
onChange: handleInputPropsChange,
balance: formatValue(coinBalance?.amount || ZERO),
isAllowed,
} as CoinInputProps;
}

function getCoinSelectorProps() {
return {
coin,
isReadOnly,
showBalance,
showMaxButton,
onChange: onChangeCoin,
onSetMaxBalance: () => {
onInput?.();
handleInputPropsChange(formatValue(getSafeMaxBalance()));
},
...(disableWhenEth &&
isEth && {
isReadOnly: true,
tooltip: "Currently, we only support ETH to TOKEN.",
}),
} as CoinSelectorProps;
}

useEffect(() => {
// Enable value initialAmount to be null
if (initialAmount !== undefined) setAmount(initialAmount);
}, [initialAmount]);

return {
amount,
setAmount,
setGasFee,
getInputProps,
getCoinSelectorProps,
formatted: formatValue(amount),
hasEnoughBalance: getSafeMaxBalance() >= (amount || ZERO),
};
}

function getRightValue(value: string, displayType: string) {
if (displayType === "text") return parseToFormattedNumber(value);
return value === "0.0" ? "0" : value;
}

type CoinInputProps = Omit<UseCoinParams, "onChange"> &
NumberInputProps & {
value: string;
displayType: DisplayType;
autoFocus?: boolean;
isLoading?: boolean;
rightElement?: ReactNode;
isAllowed?: (values: NumberFormatValues) => boolean;
onChange?: (val: string) => void;
};

export const CoinInput = forwardRef<HTMLInputElement, CoinInputProps>(
(
{
Expand Down Expand Up @@ -223,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
20 changes: 5 additions & 15 deletions packages/app/src/systems/Core/components/CoinSelector.tsx
@@ -1,26 +1,16 @@
import cx from "classnames";
import type { ReactNode } from "react";
import { useState, useEffect, forwardRef, useMemo } from "react";
import { FiChevronDown } from "react-icons/fi";

import { useBalances } from "../hooks";
import { TOKENS, parseToFormattedNumber, COIN_ETH } from "../utils";
import type { CoinSelectorProps } from "../hooks/useCoinInput";
import { TOKENS, parseToFormattedNumber, isCoinEth } from "../utils";

import { CoinsListDialog } from "./CoinsListDialog";
import { TokenIcon } from "./TokenIcon";

import { Button, Dialog, Tooltip, useDialog } from "~/systems/UI";
import type { Coin } from "~/types";

export type CoinSelectorProps = {
coin?: Coin | null;
isReadOnly?: boolean;
showBalance?: boolean;
showMaxButton?: boolean;
onChange?: (coin: Coin) => void;
onSetMaxBalance?: () => void;
tooltip?: ReactNode;
};
import type { Coin, Maybe } from "~/types";

export const CoinSelector = forwardRef<HTMLDivElement, CoinSelectorProps>(
(
Expand All @@ -35,7 +25,7 @@ export const CoinSelector = forwardRef<HTMLDivElement, CoinSelectorProps>(
},
ref
) => {
const [selected, setSelected] = useState<Coin | null>(null);
const [selected, setSelected] = useState<Maybe<Coin>>(null);
const dialog = useDialog();
const { data: balances } = useBalances({ enabled: showBalance });
const coinBalance = balances?.find(
Expand Down Expand Up @@ -112,7 +102,7 @@ export const CoinSelector = forwardRef<HTMLDivElement, CoinSelectorProps>(
{showMaxButton && (
<Tooltip
content={`Set: max ${coin?.symbol} balance ${
coin?.assetId === COIN_ETH ? " - network fee" : ""
isCoinEth(coin) ? " - network fee" : ""
}`}
>
<Button
Expand Down
Expand Up @@ -2,10 +2,12 @@ import { parseToFormattedNumber } from "../utils";

import { PreviewItem } from "./PreviewTable";

import type { Maybe } from "~/types";

export function NetworkFeePreviewItem({
networkFee,
}: {
networkFee?: bigint | null;
networkFee?: Maybe<bigint>;
}) {
if (!networkFee) return null;

Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/systems/Core/components/TokenIcon.tsx
@@ -1,15 +1,15 @@
import cx from "classnames";

import type { Coin } from "~/types";
import type { Coin, Maybe } from "~/types";

const style = {
icon: `inline-flex rounded-full border-2 border-transparent`,
iconLast: `last:ml-[-10px] last:z-10 border-gray-800`,
};

type TokenIconProps = {
coinFrom?: Coin | null;
coinTo?: Coin | null;
coinFrom?: Maybe<Coin>;
coinTo?: Maybe<Coin>;
size?: number;
};

Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/systems/Core/context.tsx
Expand Up @@ -7,19 +7,20 @@ import React, { useContext, useMemo } from "react";
import { LocalStorageKey } from "./utils";

import { FUEL_PROVIDER_URL } from "~/config";
import type { Maybe } from "~/types";

interface AppContextValue {
justContent?: boolean;
wallet: Wallet | null;
wallet: Maybe<Wallet>;
createWallet: () => void;
}

const walletPrivateKeyState = atomWithStorage<string | null>(
const walletPrivateKeyState = atomWithStorage<Maybe<string>>(
`${LocalStorageKey}-state`,
null
);

export const AppContext = React.createContext<AppContextValue | null>(null);
export const AppContext = React.createContext<Maybe<AppContextValue>>(null);

export const useAppContext = () => useContext(AppContext)!;

Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/systems/Core/hooks/index.ts
@@ -1,11 +1,14 @@
export * from './useAssets';
export * from './useBalances';
export * from './useBreakpoint';
export * from './useCoinInput';
export * from './useCoinMetadata';
export * from './useContract';
export * from './useCreateWallet';
export * from './useDebounce';
export * from './useEthBalance';
export * from './useSlippage';
export * from './usePubSub';
export * from './useTokensMethods';
export * from './useTransactionCost';
export * from './useWallet';