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

Commit

Permalink
feat: return reserves and fee on price preview (#190)
Browse files Browse the repository at this point in the history
* feat: return reserve and fee on preview prices

* chore: prevent vercel to comment

* feat: add swap preview info

* feat: fix swap card to top

* refact: split code and improve behaviors

* feat: fix isssues and improve loading

* chore: prettify

* chore: update contract id

* fix: pass contract

* config: update fuel-core version

* refact: refactor based on PR comments

* fix: fix exchange contract tests
  • Loading branch information
luizstacio authored May 24, 2022
1 parent 2adf84d commit c2b8fa8
Show file tree
Hide file tree
Showing 20 changed files with 390 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
CI: false
PUBLIC_URL: "/${{ github.event.repository.name }}"
VITE_FUEL_PROVIDER_URL: "https://node.swayswap.io/graphql"
VITE_CONTRACT_ID: "0xad3134370511cecb84064622a87b6f7472e7d6423b8e9ea13753bed4c58fc1c2"
VITE_CONTRACT_ID: "0xac01cffc1fe91976228834078e888efcd185d6792bd2138b09afbdf833599ef6"
VITE_TOKEN_ID: "0xb72c566e5a9f69c98298a04d70a38cb32baca4d9b280da8590e0314fb00c59e0"
run: |
pnpm build
Expand Down
8 changes: 4 additions & 4 deletions contracts/exchange_contract/Forc.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ dependencies = []

[[package]]
name = 'exchange_abi'
dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a']
dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0']

[[package]]
name = 'exchange_contract'
dependencies = [
'exchange_abi',
'std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a',
'std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0',
'swayswap_helpers',
]

[[package]]
name = 'std'
source = 'git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a'
source = 'git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0'
dependencies = ['core']

[[package]]
name = 'swayswap_helpers'
dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a']
dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0']
16 changes: 9 additions & 7 deletions contracts/exchange_contract/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const TOKEN_ID = 0xb72c566e5a9f69c98298a04d70a38cb32baca4d9b280da8590e0314fb00c5

/// Minimum ETH liquidity to open a pool.
const MINIMUM_LIQUIDITY = 1; //A more realistic value would be 1000000000;
// Liquidity miner fee apply to all swaps
const LIQUIDITY_MINER_FEE = 333;

////////////////////////////////////////
// Storage declarations
Expand Down Expand Up @@ -69,7 +71,7 @@ fn remove_reserve(token_id: b256, amount: u64) {

// Calculate 0.3% fee
fn calculate_amount_with_fee(amount: u64) -> u64 {
let fee: u64 = (amount / 333);
let fee: u64 = (amount / LIQUIDITY_MINER_FEE);
amount - fee
}

Expand Down Expand Up @@ -336,7 +338,7 @@ impl Exchange for Contract {
let eth_reserve = get_current_reserve(ETH_ID);
let token_reserve = get_current_reserve(TOKEN_ID);
let mut sold = 0;
let mut has_liquidity = false;
let mut has_liquidity = true;
if (msg_asset_id().into() == ETH_ID) {
sold = get_input_price(amount, eth_reserve, token_reserve);
has_liquidity = sold < token_reserve;
Expand All @@ -346,26 +348,26 @@ impl Exchange for Contract {
}
PreviewInfo {
amount: sold,
has_liquidity: has_liquidity
has_liquidity: has_liquidity,
}
}

fn get_swap_with_maximum(amount: u64) -> PreviewInfo {
let eth_reserve = get_current_reserve(ETH_ID);
let token_reserve = get_current_reserve(TOKEN_ID);
let mut sold = 0;
let mut has_liquidity = false;
let mut has_liquidity = true;
if (msg_asset_id().into() == ETH_ID) {
sold = get_output_price(amount, eth_reserve, token_reserve);
has_liquidity = sold < token_reserve;
has_liquidity = sold < eth_reserve;

} else {
sold = get_output_price(amount, token_reserve, eth_reserve);
has_liquidity = sold < eth_reserve;
has_liquidity = sold < token_reserve;
}
PreviewInfo {
amount: sold,
has_liquidity: has_liquidity
has_liquidity: has_liquidity,
}
}
}
16 changes: 16 additions & 0 deletions contracts/exchange_contract/tests/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ async fn exchange_contract() {
// Inspect the wallet for alt tokens to be 100
assert_eq!(total_amount, 100);

// Return reserve amounts and fee
let result = exchange_instance
.get_swap_with_minimum(10)
.call()
.await
.unwrap();
assert_eq!(result.value.amount, 9);
assert!(result.value.has_liquidity);
let result = exchange_instance
.get_swap_with_maximum(10)
.call()
.await
.unwrap();
assert_eq!(result.value.amount, 12);
assert!(result.value.has_liquidity);

////////////////////
// SWAP WITH MINIMUM
////////////////////
Expand Down
2 changes: 1 addition & 1 deletion docker/fuel-core/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/fuellabs/fuel-core:v0.6.4
FROM ghcr.io/fuellabs/fuel-core:v0.7.1

ARG IP=0.0.0.0
ARG PORT=4000
Expand Down
34 changes: 34 additions & 0 deletions packages/app/src/components/PreviewTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import classNames from "classnames";
import type { ReactNode } from "react";

type TableItemProps = {
title: ReactNode;
value: ReactNode;
className?: string;
};

export const PreviewItem = ({ title, value, className }: TableItemProps) => (
<div className={classNames("flex", className)}>
<div>{title}</div>
<div className="flex-auto text-right">{value}</div>
</div>
);

type PreviewTableProps = {
title: ReactNode;
children: ReactNode;
className?: string;
};

export const PreviewTable = ({
title,
children,
className,
}: PreviewTableProps) => (
<div className={className}>
<div className="px-2 text-sm text-gray-50 mb-2">{title}</div>
<div className="flex flex-col gap-3 p-4 text-sm text-gray-100 bg-gray-700 rounded-xl">
{children}
</div>
</div>
);
1 change: 1 addition & 0 deletions packages/app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const ONE_ASSET = parseUnits('1', DECIMAL_UNITS).toBigInt();
export const RECAPTCHA_SITE_KEY = import.meta.env.VITE_RECAPTCHA_SITE_KEY!;
export const ENABLE_FAUCET_API = import.meta.env.VITE_ENABLE_FAUCET_API === 'true';
export const SLIPPAGE_TOLERANCE = 0.005;
export const NETWORK_FEE = 1;

// Max value supported
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/hooks/usePoolInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useQuery } from 'react-query';

import type { ExchangeContractAbi } from '~/types/contracts';

export function usePoolInfo(contract: ExchangeContractAbi) {
return useQuery('PoolPage-poolInfo', () => contract.callStatic.get_info());
}
8 changes: 8 additions & 0 deletions packages/app/src/hooks/useSlippage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SLIPPAGE_TOLERANCE } from '~/config';

export function useSlippage() {
return {
value: SLIPPAGE_TOLERANCE,
formatted: `${SLIPPAGE_TOLERANCE * 100}%`,
};
}
5 changes: 4 additions & 1 deletion packages/app/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { toBigInt } from 'fuels';

export const contractABI = {};
export const contractAddress = '0xF93c18172eAba6a9F145B3FB16d2bBeA2e096477';
export const LocalStorageKey = 'swayswap';
export const LocalStorageKey = 'swayswap-v2';
export const CoinETH = '0x0000000000000000000000000000000000000000000000000000000000000000';
export const ZERO = toBigInt(0);
5 changes: 5 additions & 0 deletions packages/app/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BigNumber } from 'ethers';
import type { BigNumberish } from 'fuels';
import urljoin from 'url-join';

Expand All @@ -22,3 +23,7 @@ export function omit<T>(list: string[], props: T) {
return { ...obj, [key]: value };
}, {} as T) as T;
}

export function divideBigInt(from: BigNumberish, to: BigNumberish) {
return BigNumber.from(from).toNumber() / BigNumber.from(to).toNumber();
}
23 changes: 12 additions & 11 deletions packages/app/src/pages/SwapPage/PricePerToken.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { BigNumber } from "ethers";
import { formatUnits } from "ethers/lib/utils";
import { useAtomValue } from "jotai";
import { toNumber } from "fuels";
import { useState } from "react";
import { AiOutlineSwap } from "react-icons/ai";

import { swapIsTypingAtom } from "./jotai";
import { useValueIsTyping } from "./jotai";
import { ActiveInput } from "./types";

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

const style = {
wrapper: `flex items-center gap-3 my-4 px-2 text-sm text-gray-400`,
Expand All @@ -22,27 +21,29 @@ function getPricePerToken(
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);
? divideBigInt(fromAmount, toAmount)
: divideBigInt(toAmount, fromAmount);
const price = ratio * toNumber(ONE_ASSET);
return (price / toNumber(ONE_ASSET)).toFixed(6);
}

type PricePerTokenProps = {
fromCoin?: string;
fromAmount?: bigint | null;
toCoin?: string;
toAmount?: bigint | null;
isLoading?: boolean;
};

export function PricePerToken({
fromCoin,
fromAmount,
toCoin,
toAmount,
isLoading,
}: PricePerTokenProps) {
const [direction, setDirection] = useState<ActiveInput>(ActiveInput.to);
const isTyping = useAtomValue(swapIsTypingAtom);
const isTyping = useValueIsTyping();

const pricePerToken = getPricePerToken(direction, fromAmount, toAmount);
const from = direction === ActiveInput.from ? toCoin : fromCoin;
Expand All @@ -54,7 +55,7 @@ export function PricePerToken({
);
}

if (isTyping) return null;
if (isTyping || isLoading) return null;
if (!fromAmount || !toAmount) return null;

return (
Expand Down
36 changes: 15 additions & 21 deletions packages/app/src/pages/SwapPage/SwapComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { toBigInt } from "fuels";
import { useAtom, useAtomValue } from "jotai";
import { startTransition, useEffect } from "react";

import { PricePerToken } from "./PricePerToken";
import {
swapActiveInputAtom,
swapAmountAtom,
swapCoinsAtom,
swapIsTypingAtom,
swapHasSwappedAtom,
useSetIsTyping,
} from "./jotai";
import type { SwapState } from "./types";
import { ActiveInput } from "./types";

import { CoinInput, useCoinInput } from "~/components/CoinInput";
import { CoinSelector } from "~/components/CoinSelector";
import { InvertButton } from "~/components/InvertButton";
import { NETWORK_FEE } from "~/config";
import type { Coin } from "~/types";

const style = {
Expand All @@ -30,15 +31,16 @@ type SwapComponentProps = {
export function SwapComponent({
onChange,
isLoading,
previewAmount: previewValue,
previewAmount,
}: SwapComponentProps) {
const [initialAmount, setInitialAmount] = useAtom(swapAmountAtom);
const [activeInput, setActiveInput] = useAtom(swapActiveInputAtom);
const [[coinFrom, coinTo], setCoins] = useAtom(swapCoinsAtom);
const setTyping = useSetAtom(swapIsTypingAtom);
const hasSwapped = useAtomValue(swapHasSwappedAtom);
const setTyping = useSetIsTyping();

const handleInvertCoins = () => {
setTyping(true);
if (activeInput === ActiveInput.to) {
const from = fromInput.amount;
startTransition(() => {
Expand All @@ -60,6 +62,7 @@ export function SwapComponent({
const fromInput = useCoinInput({
coin: coinFrom,
disableWhenEth: true,
gasFee: toBigInt(NETWORK_FEE),
onChangeCoin: (coin: Coin) => {
setCoins([coin, coinTo]);
},
Expand Down Expand Up @@ -92,7 +95,6 @@ export function SwapComponent({
useEffect(() => {
const currentInput = activeInput === ActiveInput.from ? fromInput : toInput;
const amount = currentInput.amount;
const coin = activeInput === ActiveInput.from ? coinFrom : coinTo;

// This is used to reset preview amount when set first input value for null
if (activeInput === ActiveInput.from && amount === null) {
Expand All @@ -105,28 +107,26 @@ export function SwapComponent({
// Set value to hydrate
setInitialAmount(amount);

if (coin && coinFrom && coinTo) {
if (coinFrom && coinTo) {
// Call on onChange
onChange?.({
amount,
coin,
from: coinFrom?.assetId,
to: coinTo?.assetId,
amountFrom: fromInput.amount,
coinFrom,
coinTo,
direction: activeInput,
hasBalance: fromInput.hasEnoughBalance,
});
}
}, [fromInput.amount, toInput.amount, coinFrom, coinTo]);

useEffect(() => {
if (previewValue == null) return;
if (activeInput === ActiveInput.from) {
toInput.setAmount(previewValue);
toInput.setAmount(previewAmount || null);
} else {
fromInput.setAmount(previewValue);
fromInput.setAmount(previewAmount || null);
}
setTyping(false);
}, [previewValue]);
}, [previewAmount]);

useEffect(() => {
if (hasSwapped) {
Expand Down Expand Up @@ -156,12 +156,6 @@ export function SwapComponent({
rightElement={<CoinSelector {...toInput.getCoinSelectorProps()} />}
/>
</div>
<PricePerToken
fromCoin={coinFrom?.symbol}
fromAmount={fromInput.amount}
toCoin={coinTo?.symbol}
toAmount={toInput.amount}
/>
</>
);
}
Loading

0 comments on commit c2b8fa8

Please sign in to comment.