diff --git a/.eslintrc.json b/.eslintrc.json index c66fd61ca..c23015c07 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -35,7 +35,7 @@ "simple-import-sort/exports": "error", "prettier/prettier": "error", "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", + "react-hooks/exhaustive-deps": "off", "no-console": ["warn", { "allow": ["warn", "error"] }], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/indent": "off", diff --git a/src/@types/asset.d.ts b/src/@types/asset.d.ts index 1f3335bd8..fd8feca50 100644 --- a/src/@types/asset.d.ts +++ b/src/@types/asset.d.ts @@ -1,7 +1,10 @@ +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; + type Asset = { name?: string; amount?: number; earnedFees?: number; + asset?: AssetInfo; }; type AssetPair = { diff --git a/src/assets/styles/styles.less b/src/assets/styles/styles.less index 6f05ce1d4..1d952c540 100644 --- a/src/assets/styles/styles.less +++ b/src/assets/styles/styles.less @@ -7,7 +7,11 @@ body { background-color: var(--ergo-body-bg) !important; } -*, *::before, *::after { +* { + box-sizing: border-box !important; +} + +*::before, *::after { box-sizing: border-box !important; transition: none !important; } diff --git a/src/common/models/AmmPool.ts b/src/common/models/AmmPool.ts new file mode 100644 index 000000000..fc06107b0 --- /dev/null +++ b/src/common/models/AmmPool.ts @@ -0,0 +1,106 @@ +import { PoolId } from '@ergolabs/ergo-dex-sdk'; +import { AmmPool as BaseAmmPool } from '@ergolabs/ergo-dex-sdk/build/main/amm/entities/ammPool'; +import { AssetAmount } from '@ergolabs/ergo-sdk'; +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; + +import { math } from '../../utils/math'; +import { normalizeAmount } from '../utils/amount'; +import { Currency } from './Currency'; + +export class AmmPool { + constructor(private pool: BaseAmmPool) {} + + get id(): PoolId { + return this.pool.id; + } + + get poolFeeNum(): number { + return this.pool.poolFeeNum; + } + + get feeNum(): bigint { + return this.pool.feeNum; + } + + get lp(): Currency { + return new Currency(this.pool.lp.amount, this.pool.lp.asset); + } + + get y(): Currency { + return new Currency(this.pool.y.amount, this.pool.y.asset); + } + + get x(): Currency { + return new Currency(this.pool.x.amount, this.pool.x.asset); + } + + getAssetAmount(asset: AssetInfo): Currency { + if (this.pool.x.asset.id === asset.id) { + return this.x; + } + if (this.pool.y.asset.id === asset.id) { + return this.y; + } + throw new Error('unknown asset'); + } + + calculateOutputPrice(inputCurrency: Currency): Currency { + const outputCurrency = this.calculateOutputAmount(inputCurrency); + + if (inputCurrency.amount === 1n) { + return outputCurrency; + } + + const fmtInput = inputCurrency.toString({ suffix: false }); + const fmtOutput = outputCurrency.toString({ suffix: false }); + + const p = math.evaluate!(`${fmtOutput} / ${fmtInput}`).toString(); + + return new Currency( + normalizeAmount(p, outputCurrency.asset), + outputCurrency.asset, + ); + } + + calculateInputPrice(outputCurrency: Currency): Currency { + const inputCurrency = this.calculateInputAmount(outputCurrency); + + if (outputCurrency.amount === 1n) { + return inputCurrency; + } + + const fmtInput = inputCurrency.toString({ suffix: false }); + const fmtOutput = outputCurrency.toString({ suffix: false }); + + const p = math.evaluate!(`${fmtInput} / ${fmtOutput}`).toString(); + + return new Currency( + normalizeAmount(p, inputCurrency.asset), + inputCurrency.asset, + ); + } + + calculateDepositAmount(currency: Currency): Currency { + const depositAmount = this.pool.depositAmount( + new AssetAmount(currency.asset, currency.amount), + ); + + return new Currency(depositAmount?.amount, depositAmount?.asset); + } + + calculateInputAmount(currency: Currency): Currency { + const inputAmount = this.pool.inputAmount( + new AssetAmount(currency.asset, currency.amount), + ); + + return new Currency(inputAmount?.amount, inputAmount?.asset); + } + + calculateOutputAmount(currency: Currency): Currency { + const outputAmount = this.pool.outputAmount( + new AssetAmount(currency.asset, currency.amount), + ); + + return new Currency(outputAmount.amount, outputAmount?.asset); + } +} diff --git a/src/common/models/Currency.ts b/src/common/models/Currency.ts new file mode 100644 index 000000000..0f0a71677 --- /dev/null +++ b/src/common/models/Currency.ts @@ -0,0 +1,151 @@ +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; + +import { parseUserInputToFractions, renderFractions } from '../../utils/math'; +import { getDecimalsCount, normalizeAmount } from '../utils/amount'; + +const createUnknownAsset = (decimals = 0): AssetInfo => ({ + id: '-1', + name: 'unknown', + decimals, +}); + +const isUnknownAsset = (asset: AssetInfo): boolean => asset.name === 'unknown'; + +export class Currency { + private _amount = 0n; + + private _asset: AssetInfo = createUnknownAsset(0); + + constructor(amount?: bigint | string, asset?: AssetInfo) { + if (!!asset) { + this._asset = asset; + } + if (typeof amount === 'bigint') { + this._amount = amount; + } + if (typeof amount === 'string') { + this.checkAmountErrors(amount, this._asset); + this._amount = parseUserInputToFractions(amount, this._asset.decimals); + } + } + + get amount(): bigint { + return this._amount; + } + + get asset(): AssetInfo { + return this._asset; + } + + fromAmount(amount: bigint | string): Currency { + return this.changeAmount(amount); + } + + isUnknownAsset(): boolean { + return isUnknownAsset(this.asset); + } + + isAssetEquals(a: AssetInfo): boolean { + return a.id === this.asset.id; + } + + isPositive(): boolean { + return this.amount > 0n; + } + + changeAmount(amount: bigint | string): Currency { + return new Currency(amount, this.asset); + } + + changeAsset(asset: AssetInfo): Currency { + return new Currency( + this.normalizeAmount(this.amount, this.asset, asset), + asset, + ); + } + + gt(currency: Currency): boolean { + this.checkComparisonErrors(currency); + return this.amount > currency.amount; + } + + lt(currency: Currency): boolean { + this.checkComparisonErrors(currency); + return this.amount < currency.amount; + } + + gte(currency: Currency): boolean { + this.checkComparisonErrors(currency); + return this.amount >= currency.amount; + } + + lte(currency: Currency): boolean { + this.checkComparisonErrors(currency); + return this.amount <= currency.amount; + } + + plus(currency: Currency): Currency { + if (isUnknownAsset(this.asset)) { + throw new Error("can't sum unknown asset"); + } + if (this.asset.id !== currency.asset.id) { + throw new Error("can't sum currencies with different assets"); + } + + return new Currency(this.amount + currency.amount, this.asset); + } + + minus(currency: Currency): Currency { + if (isUnknownAsset(this.asset)) { + throw new Error("can't subtract unknown asset"); + } + if (this.asset.id !== currency.asset.id) { + throw new Error("can't subtract currencies with different assets"); + } + + return new Currency(this.amount - currency.amount, this.asset); + } + + toString(config?: { suffix: boolean }): string { + if ((!config || !!config?.suffix) && !isUnknownAsset(this.asset)) { + return `${renderFractions(this.amount, this.asset.decimals)} ${ + this.asset.name + }`; + } + + return `${renderFractions(this.amount, this.asset.decimals)}`; + } + + toUsd(): void {} + + private checkComparisonErrors(currency: Currency): void { + if (isUnknownAsset(this.asset)) { + throw new Error("can't compare unknown asset"); + } + if (this.asset.id !== currency.asset.id) { + throw new Error("can't compare currencies with different assets"); + } + } + + private checkAmountErrors(amount: string, asset: AssetInfo): void { + const decimalsCount = getDecimalsCount(amount); + + if (isUnknownAsset(asset)) { + this._asset = createUnknownAsset(decimalsCount); + return; + } + if (decimalsCount > (asset?.decimals || 0)) { + throw new Error('amount has to many fractions'); + } + } + + private normalizeAmount( + amount: bigint, + currentAsset: AssetInfo, + newAsset: AssetInfo, + ): string { + const amountString = renderFractions(amount, currentAsset.decimals); + + return normalizeAmount(amountString, newAsset); + } +} diff --git a/src/common/utils/amount.ts b/src/common/utils/amount.ts new file mode 100644 index 000000000..ae8e72df2 --- /dev/null +++ b/src/common/utils/amount.ts @@ -0,0 +1,23 @@ +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; + +export const getDecimalsCount = (amount: string): number => { + const decimals = amount.split('.')[1]; + + if (decimals) { + return decimals.length; + } + return 0; +}; + +export const normalizeAmount = (amount: string, asset: AssetInfo): string => { + const currentDecimalsCount = getDecimalsCount(amount); + + if (currentDecimalsCount <= (asset.decimals || 0)) { + return amount; + } + + return amount.slice( + 0, + amount.length - currentDecimalsCount + (asset.decimals || 0), + ); +}; diff --git a/src/components/ConfirmationModal/ConfirmationModal.tsx b/src/components/ConfirmationModal/ConfirmationModal.tsx index 039d91a9c..a78cfd649 100644 --- a/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -1,10 +1,9 @@ import { TxId } from '@ergolabs/ergo-sdk'; -import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import React, { ReactNode } from 'react'; -import { Flex, Modal, Typography } from '../../ergodex-cdk'; +import { Currency } from '../../common/models/Currency'; +import { DialogRef, Flex, Modal, Typography } from '../../ergodex-cdk'; import { RequestProps } from '../../ergodex-cdk/components/Modal/presets/Request'; -import { renderFractions } from '../../utils/math'; import { exploreTx } from '../../utils/redirect'; export enum Operation { @@ -14,44 +13,27 @@ export enum Operation { REFUND, } -export interface ConfirmationAssetAmount { - readonly amount: number; - readonly asset: AssetInfo; -} - const getDescriptionByData = ( operation: Operation, - xAsset: ConfirmationAssetAmount, - yAsset: ConfirmationAssetAmount, + xAsset: Currency, + yAsset: Currency, ): ReactNode => { switch (operation) { case Operation.ADD_LIQUIDITY: - return `Adding liquidity ${xAsset.amount} ${xAsset.asset.name} and ${yAsset.amount} ${yAsset.asset.name}`; + return `Adding liquidity ${xAsset.toString()} and ${yAsset.toString()}`; case Operation.REFUND: - return `Refunding ${renderFractions( - xAsset.amount, - xAsset.asset.decimals, - )} ${xAsset.asset.name} and ${renderFractions( - yAsset.amount, - yAsset.asset.decimals, - )} ${yAsset.asset.name}`; + return `Refunding ${xAsset.toString()} and ${yAsset.toString()}`; case Operation.REMOVE_LIQUIDITY: - return `Removing liquidity ${renderFractions( - xAsset.amount, - xAsset.asset.decimals, - )} ${xAsset.asset.name} and ${renderFractions( - yAsset.amount, - yAsset.asset.decimals, - )} ${yAsset.asset.name}`; + return `Removing liquidity ${xAsset.toString()} and ${yAsset.toString()}`; case Operation.SWAP: - return `Swapping ${xAsset.amount} ${xAsset.asset.name} for ${yAsset.amount} ${yAsset.asset.name}`; + return `Swapping ${xAsset.toString()} for ${yAsset.toString()}`; } }; const ProgressModalContent = ( operation: Operation, - xAsset: ConfirmationAssetAmount, - yAsset: ConfirmationAssetAmount, + xAsset: Currency, + yAsset: Currency, ) => { return ( @@ -74,8 +56,8 @@ const ProgressModalContent = ( const ErrorModalContent = ( operation: Operation, - xAsset: ConfirmationAssetAmount, - yAsset: ConfirmationAssetAmount, + xAsset: Currency, + yAsset: Currency, ) => ( @@ -114,9 +96,9 @@ const SuccessModalContent = (txId: TxId) => ( export const openConfirmationModal = ( actionContent: RequestProps['actionContent'], operation: Operation, - xAsset: ConfirmationAssetAmount, - yAsset: ConfirmationAssetAmount, -) => { + xAsset: Currency, + yAsset: Currency, +): DialogRef => { return Modal.request({ actionContent, errorContent: ErrorModalContent(operation, xAsset, yAsset), diff --git a/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx b/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx index ee9dbcb4c..693ef3e2e 100644 --- a/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx +++ b/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx @@ -1,34 +1,28 @@ -import { AssetInfo } from '@ergolabs/ergo-sdk'; -import React, { useEffect } from 'react'; +import React from 'react'; +import { Currency } from '../../../../common/models/Currency'; import { Box, Flex, Typography } from '../../../../ergodex-cdk'; import { TokenIcon } from '../../../TokenIcon/TokenIcon'; interface TokenListItemProps { - readonly asset: AssetInfo; - readonly balance: number; + readonly currency: Currency; } -export const TokenListItem: React.FC = ({ - asset, - balance, -}) => ( +export const TokenListItem: React.FC = ({ currency }) => ( - + - {asset.name} + {currency.asset.name} {/*{asset.name}*/} - - {balance} {asset.name} - + {currency.toString({ suffix: false })} ); diff --git a/src/components/WalletModal/TokensTab/TokensTab.tsx b/src/components/WalletModal/TokensTab/TokensTab.tsx index c4bf9956a..548ae4b17 100644 --- a/src/components/WalletModal/TokensTab/TokensTab.tsx +++ b/src/components/WalletModal/TokensTab/TokensTab.tsx @@ -1,32 +1,23 @@ -import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import React from 'react'; -import { combineLatest, map } from 'rxjs'; import { Flex, List } from '../../../ergodex-cdk'; -import { useObservable } from '../../../hooks/useObservable'; -import { assets$ } from '../../../services/new/assets'; -import { Balance, walletBalance$ } from '../../../services/new/balance'; +import { useWalletBalance } from '../../../services/new/balance'; import { TokenListItem } from './TokenListItem/TokenListItem'; -const userAssets$ = combineLatest([assets$, walletBalance$]).pipe( - map<[AssetInfo[], Balance], { asset: AssetInfo; balance: number }[]>( - ([assets, balance]) => - assets - .filter((a) => balance.get(a.id) > 0) - .map((a) => ({ asset: a, balance: balance.get(a.id) })), - ), -); - export const TokensTab: React.FC = () => { - const [assets] = useObservable(userAssets$); + const [balance] = useWalletBalance(); return ( - - {(item) => ( - - )} + + {(item) => } diff --git a/src/components/common/ActionForm/ActionButton/ActionButton.tsx b/src/components/common/ActionForm/ActionButton/ActionButton.tsx index 669880c70..e97b2e8cc 100644 --- a/src/components/common/ActionForm/ActionButton/ActionButton.tsx +++ b/src/components/common/ActionForm/ActionButton/ActionButton.tsx @@ -107,12 +107,6 @@ export const ActionButton: FC = (props) => { props.children, ); - const handleClick = () => { - if (props.state === ActionButtonState.ACTION && props.onClick) { - props.onClick(); - } - }; - return ( > = ({ }) => { const [isOnline] = useObservable(isOnline$); const [isWalletLoading] = useObservable(isWalletLoading$); - const [value] = useObservable(form.valueChangesWithSilent$, { - deps: [form], - }); + const [value] = useObservable( + form.valueChangesWithSilent$.pipe(debounceTime(100)), + { + deps: [form], + defaultValue: {}, + }, + ); const [buttonData, setButtonData] = useState<{ state: ActionButtonState; data?: any; @@ -70,7 +74,7 @@ export const ActionForm: FC> = ({ setButtonData({ state: ActionButtonState.INSUFFICIENT_FEE_BALANCE, data: { - token: getInsufficientTokenNameForFee(value), + nativeToken: getInsufficientTokenNameForFee(value), }, }); } else if (isLiquidityInsufficient && isLiquidityInsufficient(value)) { diff --git a/src/components/common/PoolSelect/PoolSelect.tsx b/src/components/common/PoolSelect/PoolSelect.tsx index 3f14ab16a..fb332c096 100644 --- a/src/components/common/PoolSelect/PoolSelect.tsx +++ b/src/components/common/PoolSelect/PoolSelect.tsx @@ -1,9 +1,9 @@ import './PoolSelect.less'; -import { AmmPool } from '@ergolabs/ergo-dex-sdk'; import { maxBy } from 'lodash'; import React, { useEffect } from 'react'; +import { AmmPool } from '../../../common/models/AmmPool'; import { Button, DownOutlined, diff --git a/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx b/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx index a89034a58..7112db5f8 100644 --- a/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx +++ b/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx @@ -1,9 +1,11 @@ import './TokenAmountInput.less'; -import React from 'react'; +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; +import React, { useEffect, useState } from 'react'; +import { Currency } from '../../../../common/models/Currency'; import { Box, Input } from '../../../../ergodex-cdk'; -import { toFloat } from '../../../../utils/string/string'; +import { EventConfig } from '../../../../ergodex-cdk/components/Form/NewForm'; import { escapeRegExp } from './format'; const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`); // match escaped "." characters via in a non-capturing group @@ -14,40 +16,70 @@ export interface TokenAmountInputValue { } export interface TokenAmountInputProps { - value?: TokenAmountInputValue | number; - onChange?: (data: TokenAmountInputValue) => void; + value?: Currency; + onChange?: (data: Currency | undefined, config?: EventConfig) => void; disabled?: boolean; readonly?: boolean; - decimals?: number; + asset?: AssetInfo; } +const isValidAmount = ( + value: string, + asset: AssetInfo | undefined, +): boolean => { + if (!asset) { + return true; + } + if (!asset.decimals && value.indexOf('.') !== -1) { + return false; + } + return (value.split('.')[1]?.length || 0) <= (asset?.decimals || 0); +}; + const TokenAmountInput: React.FC = ({ value, onChange, disabled, readonly, - decimals, + asset, }) => { - const normalizeViewValue = ( - value: TokenAmountInputValue | number | undefined, - ): string | undefined => { - if (typeof value === 'number') { - return toFloat(value.toString(), decimals); + const [userInput, setUserInput] = useState(undefined); + + useEffect(() => { + if (Number(value?.toString({ suffix: false })) !== Number(userInput)) { + setUserInput(value?.toString({ suffix: false })); } + }, [value]); - return toFloat(value?.viewValue || '', decimals); - }; + useEffect(() => { + if (value && asset) { + const newValue = value?.changeAsset(asset); + + setUserInput(newValue.toString({ suffix: false })); + + if (onChange && value.asset.id !== asset.id) { + onChange(newValue, { emitEvent: 'silent' }); + } + } + }, [asset?.id]); const enforcer = (nextUserInput: string) => { if (nextUserInput.startsWith('.')) { nextUserInput = nextUserInput.replace('.', '0.'); } - if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { + if (nextUserInput === '' && onChange) { + setUserInput(''); + onChange(undefined); + return; + } + if ( + inputRegex.test(escapeRegExp(nextUserInput)) && onChange && - onChange({ - viewValue: nextUserInput, - value: nextUserInput !== undefined ? +nextUserInput : undefined, - }); + isValidAmount(nextUserInput, asset) + ) { + setUserInput(nextUserInput); + onChange(new Currency(nextUserInput, asset)); + return; } }; @@ -55,7 +87,7 @@ const TokenAmountInput: React.FC = ({ { enforcer(event.target.value.replace(/,/g, '.')); }} diff --git a/src/components/common/TokenControl/TokenControl.less b/src/components/common/TokenControl/TokenControl.less index b0cc2786d..9ee362967 100644 --- a/src/components/common/TokenControl/TokenControl.less +++ b/src/components/common/TokenControl/TokenControl.less @@ -8,10 +8,6 @@ margin-bottom: 0; } -.token-control-bottom-panel { - min-height: 32px; -} - .token-control--bordered { border: 1px solid var(--ergo-box-bg-contrast); } diff --git a/src/components/common/TokenControl/TokenControl.tsx b/src/components/common/TokenControl/TokenControl.tsx index 8d6091843..72a72db14 100644 --- a/src/components/common/TokenControl/TokenControl.tsx +++ b/src/components/common/TokenControl/TokenControl.tsx @@ -2,20 +2,19 @@ import './TokenControl.less'; import { AssetInfo } from '@ergolabs/ergo-sdk'; import cn from 'classnames'; -import React, { FC, ReactNode, useEffect } from 'react'; +import React, { FC, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Observable, of } from 'rxjs'; -import { Box, Button, Flex, Typography } from '../../../ergodex-cdk'; +import { Currency } from '../../../common/models/Currency'; +import { Animation, Box, Button, Flex, Typography } from '../../../ergodex-cdk'; import { Form, useFormContext, } from '../../../ergodex-cdk/components/Form/NewForm'; -import { useObservable, useSubject } from '../../../hooks/useObservable'; -import { - getBalanceByTokenId, - useWalletBalance, -} from '../../../services/new/balance'; +import { useObservable } from '../../../hooks/useObservable'; +import { useWalletBalance } from '../../../services/new/balance'; +import { isWalletLoading$ } from '../../../services/new/core'; import { TokenAmountInput, TokenAmountInputValue, @@ -40,121 +39,6 @@ export interface TokenControlProps { readonly bordered?: boolean; } -const getTokenBalanceByTokenName = (tokenName: string | undefined) => - tokenName ? getBalanceByTokenId(tokenName) : of(undefined); - -export const TokenControl: FC = ({ - label, - value, - onChange, - maxButton, - assets, - hasBorder, - disabled, - readonly, - noBottomInfo, - bordered, -}) => { - const { t } = useTranslation(); - const [balance, updateBalance] = useSubject(getTokenBalanceByTokenName); - - useEffect(() => { - if (value?.asset) { - updateBalance(value?.asset?.id); - } else { - updateBalance(undefined); - } - }, [value, updateBalance]); - - const onAmountChange = (amount: TokenAmountInputValue) => { - if (onChange) { - onChange({ ...value, amount }); - } - }; - - const onTokenChange = (asset: AssetInfo) => { - if (onChange) { - onChange({ ...value, asset }); - } - }; - - const onMaxButtonClick = () => { - if (onChange) { - onChange({ - asset: value?.asset, - amount: { value: +(balance as any), viewValue: balance?.toString() }, - }); - } - }; - - return ( - - - - {label} - - - - - - - - - - - - - {!noBottomInfo && ( - - {balance !== undefined && ( - - - {t`common.tokenControl.balanceLabel`} {balance}{' '} - {value?.asset?.name} - - - )} - {balance !== undefined && maxButton && ( - - )} - - )} - - - ); -}; - export interface TokenControlFormItemProps { readonly name: string; readonly label?: ReactNode; @@ -196,19 +80,17 @@ export const TokenControlFormItem: FC = ({ }) => { const { t } = useTranslation(); const { form } = useFormContext(); - const [balance] = useWalletBalance(); + const [balance, balanceLoading] = useWalletBalance(); const [selectedAsset] = useObservable( tokenName ? form.controls[tokenName].valueChangesWithSilent$ : of(undefined), ); + const [isWalletLoading] = useObservable(isWalletLoading$); - const handleMaxButtonClick = (maxBalance: number) => { + const handleMaxButtonClick = (maxBalance: Currency) => { if (amountName) { - form.controls[amountName].patchValue({ - value: maxBalance, - viewValue: maxBalance.toString(), - }); + form.controls[amountName].patchValue(maxBalance); } }; @@ -250,6 +132,7 @@ export const TokenControlFormItem: FC = ({ @@ -274,35 +157,47 @@ export const TokenControlFormItem: FC = ({ )} - - {!noBottomInfo && ( - - {selectedAsset !== undefined && ( - - - {t`common.tokenControl.balanceLabel`}{' '} - {balance.get(selectedAsset)} {selectedAsset?.name} - - - )} - {selectedAsset !== undefined && - !!balance.get(selectedAsset) && - maxButton && ( - - )} - - )} + {() => ( + <> + + + {t`common.tokenControl.balanceLabel`}{' '} + {balance.get(selectedAsset).toString()} + + + {!!balance.get(selectedAsset) && maxButton && ( + + )} + + )} + + + + )} + ); }; diff --git a/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx b/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx index 521990d4f..a992c346b 100644 --- a/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx +++ b/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx @@ -31,6 +31,12 @@ const TokenSelect: React.FC = ({ readonly, assets$, }) => { + const handleSelectChange = (newValue: AssetInfo): void => { + if (value?.id !== newValue?.id && onChange) { + onChange(newValue); + } + }; + const openTokenModal = () => { if (readonly) { return; @@ -40,7 +46,7 @@ const TokenSelect: React.FC = ({ assets$={assets$} assets={assets} close={close} - onSelectChanged={onChange} + onSelectChanged={handleSelectChange} /> )); }; diff --git a/src/constants/erg.ts b/src/constants/erg.ts index f7abfbbb8..d69af79ec 100644 --- a/src/constants/erg.ts +++ b/src/constants/erg.ts @@ -1,6 +1,4 @@ export const ERG_TOKEN_NAME = 'ERG'; -export const ERG_TOKEN_ID = - '0000000000000000000000000000000000000000000000000000000000000000'; export const DEFAULT_MINER_FEE = BigInt(2_000_000); export const ERG_DECIMALS = 9; diff --git a/src/context/ConnectionContext.tsx b/src/context/ConnectionContext.tsx index 9fc2d99e4..fd909118e 100644 --- a/src/context/ConnectionContext.tsx +++ b/src/context/ConnectionContext.tsx @@ -14,7 +14,8 @@ const ConnectionContext = createContext({ online: true, }); -export const useConnection = () => useContext(ConnectionContext); +export const useConnection = (): ConnectionContextType => + useContext(ConnectionContext); export const ConnectionContextProvider: FC> = ({ children }) => { diff --git a/src/ergodex-cdk/components/Animation/Expand/Expand.tsx b/src/ergodex-cdk/components/Animation/Expand/Expand.tsx new file mode 100644 index 000000000..d80bba505 --- /dev/null +++ b/src/ergodex-cdk/components/Animation/Expand/Expand.tsx @@ -0,0 +1,44 @@ +import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'; + +const calculateHeight = (elt: HTMLDivElement) => + parseFloat(window.getComputedStyle(elt).height); + +export interface ExpandProps { + children?: + | ReactNode + | ReactNode[] + | string + | (() => ReactNode | ReactNode[] | string); + duration?: number; + expanded?: boolean; +} + +export const Expand: FC = ({ duration, children, expanded }) => { + const containerRef = useRef(); + const [height, setHeight] = useState(0); + + useEffect(() => { + if (containerRef.current) { + setHeight(calculateHeight(containerRef.current)); + } else { + setHeight(0); + } + }, [expanded]); + + return ( +
+ {expanded && ( +
+ {children instanceof Function ? children() : children} +
+ )} +
+ ); +}; diff --git a/src/ergodex-cdk/components/Animation/index.ts b/src/ergodex-cdk/components/Animation/index.ts new file mode 100644 index 000000000..292f79429 --- /dev/null +++ b/src/ergodex-cdk/components/Animation/index.ts @@ -0,0 +1,5 @@ +import { Expand } from './Expand/Expand'; + +export const Animation = { + Expand, +}; diff --git a/src/ergodex-cdk/components/Button/Button.less b/src/ergodex-cdk/components/Button/Button.less index d3a14089a..6756ac1e4 100644 --- a/src/ergodex-cdk/components/Button/Button.less +++ b/src/ergodex-cdk/components/Button/Button.less @@ -6,6 +6,14 @@ } } +.ant-btn { + transition: none !important; + + > * { + transition: none !important; + } +} + .ant-btn.ant-btn-lg { border-radius: var(--ergo-border-radius-md); diff --git a/src/ergodex-cdk/components/Form/NewForm.tsx b/src/ergodex-cdk/components/Form/NewForm.tsx index df9dbd401..812dbf7dd 100644 --- a/src/ergodex-cdk/components/Form/NewForm.tsx +++ b/src/ergodex-cdk/components/Form/NewForm.tsx @@ -35,7 +35,7 @@ function ctrl( (_useForm as any).ctrl = ctrl; -interface EventConfig { +export interface EventConfig { readonly emitEvent: 'default' | 'system' | 'silent'; } @@ -140,7 +140,7 @@ export class FormControl implements AbstractFormItem { this.touched = false; } - patchValue(value: T, config?: EventConfig): void { + internalPatchValue(value: T, config?: EventConfig): void { this.value = value; this.currentError = this.getCurrentCheckName( this.value, @@ -155,11 +155,15 @@ export class FormControl implements AbstractFormItem { this.withWarnings = !!this.currentWarning; this.withoutWarnings = !this.withWarnings; this.emitEvent(config); - this.parent.emitEvent(); } - onChange(value: T): void { - this.patchValue(value); + patchValue(value: T, config?: EventConfig): void { + this.internalPatchValue(value, config); + this.parent.emitEvent(config); + } + + onChange(value: T, config?: EventConfig): void { + this.patchValue(value, config); } reset(value: T, config?: EventConfig): void { @@ -167,7 +171,7 @@ export class FormControl implements AbstractFormItem { this.markAsUntouched(); } - emitEvent(config?: EventConfig) { + emitEvent(config?: EventConfig): void { if ( config?.emitEvent === 'system' || config?.emitEvent === 'default' || @@ -297,29 +301,29 @@ export class FormGroup implements AbstractFormItem { return dictionary; } - markAllAsTouched() { + markAllAsTouched(): void { this.controlsArray.forEach((c) => c.markAsTouched()); } - markAllAsUntouched() { + markAllAsUntouched(): void { this.controlsArray.forEach((c) => c.markAsUntouched()); } - patchValue(value: Partial, config?: EventConfig) { + patchValue(value: Partial, config?: EventConfig): void { Object.entries(value).forEach(([key, value]) => - this.controls[key as keyof T].patchValue(value as any, config), + this.controls[key as keyof T].internalPatchValue(value as any, config), ); this.emitEvent(config); } - reset(value: Partial, config?: EventConfig) { + reset(value: Partial, config?: EventConfig): void { Object.entries(value).forEach(([key, value]) => this.controls[key as keyof T].reset(value as any, config), ); this.emitEvent(config); } - private emitEvent(config?: EventConfig) { + private emitEvent(config?: EventConfig): void { if ( config?.emitEvent === 'system' || config?.emitEvent === 'default' || @@ -384,7 +388,7 @@ class _Form extends React.Component> { interface FormItemFnParams { readonly value: T; - readonly onChange: (value: T) => void; + readonly onChange: (value: T, config?: EventConfig) => void; readonly touched: boolean; readonly untouched: boolean; readonly invalid: boolean; @@ -412,8 +416,8 @@ class _FormItem extends React.Component> { this.subscription?.unsubscribe(); } - onChange(ctrl: FormControl, value: T) { - ctrl.onChange(value); + onChange(ctrl: FormControl, value: T, config?: EventConfig) { + ctrl.onChange(value, config); } render() { diff --git a/src/ergodex-cdk/components/Modal/Modal.tsx b/src/ergodex-cdk/components/Modal/Modal.tsx index 6fb0c3c82..dab2bd67e 100644 --- a/src/ergodex-cdk/components/Modal/Modal.tsx +++ b/src/ergodex-cdk/components/Modal/Modal.tsx @@ -1,6 +1,6 @@ import './Modal.less'; -import { Modal as BaseModal, Typography } from 'antd'; +import { Modal as BaseModal } from 'antd'; import React, { ReactElement } from 'react'; import { ReactNode } from 'react'; import ReactDOM from 'react-dom'; @@ -9,7 +9,6 @@ import { ModalContent } from './ModalContent/ModalContent'; import { ModalInnerTitle, ModalTitle, - ModalTitleContext, ModalTitleContextProvider, } from './ModalTitle/ModalTitle'; import { Error } from './presets/Error'; @@ -26,7 +25,7 @@ export interface ModalParams { readonly width?: number; } -interface DialogRef { +export interface DialogRef { close: (result?: T) => void; } @@ -192,11 +191,11 @@ export class ContextModalProvider private modals = new Map(); - componentDidMount() { + componentDidMount(): void { Modal.provider = this; } - componentWillUnmount() { + componentWillUnmount(): void { Modal.provider = new BaseModalProvider(); } @@ -267,7 +266,7 @@ export class ContextModalProvider return { close }; } - render() { + render(): ReactNode | ReactNode[] | string { return ( <> {Array.from(this.modals.values()).map((modal) => ( @@ -280,7 +279,7 @@ export class ContextModalProvider ); } - private createDialogId() { + private createDialogId(): number { return dialogId++; } } diff --git a/src/ergodex-cdk/components/Modal/presets/Progress.tsx b/src/ergodex-cdk/components/Modal/presets/Progress.tsx index 9822d571b..a67332db9 100644 --- a/src/ergodex-cdk/components/Modal/presets/Progress.tsx +++ b/src/ergodex-cdk/components/Modal/presets/Progress.tsx @@ -2,7 +2,6 @@ import { LoadingOutlined } from '@ant-design/icons'; import React, { FC, ReactNode } from 'react'; import { Flex } from '../../Flex/Flex'; -import { Row } from '../../Row/Row'; import { Spin } from '../../Spin/Spin'; import { ModalContent } from '../ModalContent/ModalContent'; import { ModalTitle } from '../ModalTitle/ModalTitle'; diff --git a/src/ergodex-cdk/components/Modal/presets/Warning.tsx b/src/ergodex-cdk/components/Modal/presets/Warning.tsx index 29f52428b..fa25fc835 100644 --- a/src/ergodex-cdk/components/Modal/presets/Warning.tsx +++ b/src/ergodex-cdk/components/Modal/presets/Warning.tsx @@ -2,7 +2,6 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import React, { FC, ReactNode } from 'react'; import { Flex } from '../../Flex/Flex'; -import { Row } from '../../Row/Row'; import { ModalContent } from '../ModalContent/ModalContent'; import { ModalTitle } from '../ModalTitle/ModalTitle'; import { INFO_DIALOG_WIDTH } from './core'; diff --git a/src/ergodex-cdk/components/index.ts b/src/ergodex-cdk/components/index.ts index 7f398ddaa..bdd2abc8f 100644 --- a/src/ergodex-cdk/components/index.ts +++ b/src/ergodex-cdk/components/index.ts @@ -1,4 +1,5 @@ export * from './Alert/Alert'; +export * from './Animation'; export * from './Box/Box'; export * from './Button/Button'; export * from './Col/Col'; diff --git a/src/ergodex-cdk/utils/gutter.ts b/src/ergodex-cdk/utils/gutter.ts index 6590e389b..96ee16878 100644 --- a/src/ergodex-cdk/utils/gutter.ts +++ b/src/ergodex-cdk/utils/gutter.ts @@ -5,7 +5,7 @@ export type Gutter = number | GutterTwoNumbers | GutterFourNumbers; export const calcGutter = (n: number): string => `calc(var(--ergo-base-gutter) * ${n})`; -export const getGutter = (p: Gutter) => { +export const getGutter = (p: Gutter): string => { if (p instanceof Array && p.length === 2) { return `${calcGutter(p[0])} ${calcGutter(p[1])}`; } diff --git a/src/hooks/useObservable.ts b/src/hooks/useObservable.ts index f3f18144a..1215f8b6d 100644 --- a/src/hooks/useObservable.ts +++ b/src/hooks/useObservable.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { map, Observable, Subject, Subscription, switchMap } from 'rxjs'; +import { Observable, Subject, Subscription, switchMap } from 'rxjs'; import { Unpacked } from '../utils/unpacked'; @@ -15,20 +15,25 @@ export function useObservable( observable: Observable, config?: { defaultValue?: T; deps?: any[] }, ): [T | undefined, boolean, Error | undefined] { - const [data, setData] = useState(config?.defaultValue); - const [error, setError] = useState(); - const [loading, setLoading] = useState(true); + const [{ data, error, loading }, setParams] = useState<{ + data: T | undefined; + error: Error | undefined; + loading: boolean; + }>({ + data: config?.defaultValue, + error: undefined, + loading: false, + }); useEffect(() => { - setLoading(true); + setParams((params) => ({ ...params, loading: true })); + const subscription = observable.subscribe({ next: (value: T) => { - setData(() => value); - setLoading(false); + setParams((params) => ({ ...params, data: value, loading: false })); }, error: (error: Error) => { - setError(error); - setLoading(false); + setParams((params) => ({ ...params, error, loading: false })); }, }); @@ -65,12 +70,16 @@ export function useSubject Observable>( boolean, Error | undefined, ] { - const [data, setData] = useState> | undefined>( - config?.defaultValue, - ); - const [error, setError] = useState(); - const [loading, setLoading] = useState(true); - const [nextData, setNextData] = useState<{ + const [{ data, error, loading }, setParams] = useState<{ + data: any | undefined; + error: Error | undefined; + loading: boolean; + }>({ + data: config?.defaultValue, + error: undefined, + loading: false, + }); + const [nextData] = useState<{ subject: Subject>; next: (...args: Parameters) => void; //@ts-ignore @@ -84,17 +93,15 @@ export function useSubject Observable>( }); useEffect(() => { - setLoading(true); + setParams((params) => ({ ...params, loading: true })); const subscription = nextData.subject .pipe(switchMap((args) => observableAction(...args))) .subscribe({ next: (value: Unpacked>) => { - setData(() => value); - setLoading(false); + setParams((params) => ({ ...params, loading: false, data: value })); }, error: (error: Error) => { - setError(error); - setLoading(false); + setParams((params) => ({ ...params, error, loading: false })); }, }); diff --git a/src/hooks/usePair.ts b/src/hooks/usePair.ts index 9b151271b..4bf9e46d8 100644 --- a/src/hooks/usePair.ts +++ b/src/hooks/usePair.ts @@ -2,6 +2,7 @@ import { AmmPool } from '@ergolabs/ergo-dex-sdk'; import { AssetAmount } from '@ergolabs/ergo-sdk'; import { useEffect, useState } from 'react'; +import { AssetPair } from '../@types/asset'; import { parseUserInputToFractions, renderFractions } from '../utils/math'; interface Pair { @@ -33,12 +34,14 @@ const usePair = (pool: AmmPool | undefined): Pair => { amount: Number( renderFractions(sharedPair[0].amount, sharedPair[0].asset.decimals), ), + asset: sharedPair[0].asset, }, assetY: { name: sharedPair[1].asset.name || '', amount: Number( renderFractions(sharedPair[1].amount, sharedPair[1].asset.decimals), ), + asset: sharedPair[1].asset, }, }; diff --git a/src/pages/Pool/AddLiquidity/AddLiquidity.tsx b/src/pages/Pool/AddLiquidity/AddLiquidity.tsx index 6c08b1bc1..0a313576b 100644 --- a/src/pages/Pool/AddLiquidity/AddLiquidity.tsx +++ b/src/pages/Pool/AddLiquidity/AddLiquidity.tsx @@ -2,10 +2,9 @@ import './AddLiquidity.less'; import { PoolId } from '@ergolabs/ergo-dex-sdk'; -import { AssetAmount } from '@ergolabs/ergo-sdk'; import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import { Skeleton } from 'antd'; -import React, { useCallback, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useParams } from 'react-router'; import { BehaviorSubject, @@ -27,14 +26,6 @@ import { Operation, } from '../../../components/ConfirmationModal/ConfirmationModal'; import { FormPageWrapper } from '../../../components/FormPageWrapper/FormPageWrapper'; -import { - ERG_DECIMALS, - ERG_TOKEN_ID, - ERG_TOKEN_NAME, - UI_FEE, -} from '../../../constants/erg'; -import { defaultExFee } from '../../../constants/settings'; -import { useSettings } from '../../../context'; import { Flex, Typography } from '../../../ergodex-cdk'; import { Form, useForm } from '../../../ergodex-cdk/components/Form/NewForm'; import { @@ -44,12 +35,8 @@ import { } from '../../../hooks/useObservable'; import { assets$, getAvailableAssetFor } from '../../../services/new/assets'; import { useWalletBalance } from '../../../services/new/balance'; +import { useNetworkAsset, useTotalFees } from '../../../services/new/core'; import { getPoolById, getPoolByPair } from '../../../services/new/pools'; -import { - parseUserInputToFractions, - renderFractions, -} from '../../../utils/math'; -import { calculateTotalFee } from '../../../utils/transactions'; import { AddLiquidityConfirmationModal } from './AddLiquidityConfirmationModal/AddLiquidityConfirmationModal'; import { AddLiquidityFormModel } from './FormModel'; @@ -62,16 +49,13 @@ const getAvailablePools = (xId?: string, yId?: string) => const AddLiquidity = (): JSX.Element => { const [balance] = useWalletBalance(); - const [{ minerFee }] = useSettings(); + const totalFees = useTotalFees(); + const networkAsset = useNetworkAsset(); const { poolId } = useParams<{ poolId?: PoolId }>(); const form = useForm({ - x: { - name: 'ERG', - id: '0000000000000000000000000000000000000000000000000000000000000000', - decimals: ERG_DECIMALS, - }, + x: undefined, y: undefined, - activePool: undefined, + pool: undefined, xAmount: undefined, yAmount: undefined, }); @@ -86,6 +70,12 @@ const AddLiquidity = (): JSX.Element => { ), ); + useEffect(() => { + if (!poolId) { + form.patchValue({ x: networkAsset }); + } + }, [networkAsset]); + const updateYAssets$ = useMemo( () => new BehaviorSubject(undefined), [], @@ -95,87 +85,23 @@ const AddLiquidity = (): JSX.Element => { [], ); - const getInsufficientTokenNameForFee = useCallback( - (value: AddLiquidityFormModel): string | undefined => { - const { xAmount, x } = value; - - let totalFees = +calculateTotalFee( - [minerFee, UI_FEE, defaultExFee], - ERG_DECIMALS, - ); - - totalFees = - x?.id === ERG_TOKEN_ID ? totalFees + xAmount?.value! : totalFees; - - return +totalFees > balance.get(ERG_TOKEN_ID) - ? ERG_TOKEN_NAME - : undefined; - }, - [balance, minerFee], - ); - - const getInsufficientTokenNameForTx = useCallback( - (value: AddLiquidityFormModel): string | undefined => { - const { x, y, xAmount, yAmount } = value; - const xAmountValue = xAmount?.value; - const yAmountValue = yAmount?.value; - - if (x && xAmount && xAmountValue! > balance.get(x?.id)) { - return x?.name; - } - - if (y && yAmount && yAmountValue! > balance.get(y?.id)) { - return y?.name; - } - - return undefined; - }, - [balance], - ); - - const isAmountNotEntered = useCallback( - (value: AddLiquidityFormModel): boolean => { - return !value.xAmount?.value || !value.yAmount?.value; - }, - [], + useSubscription( + form.controls.x.valueChanges$, + (token: AssetInfo | undefined) => updateYAssets$.next(token?.id), ); - const isTokensNotSelected = useCallback( - (value: AddLiquidityFormModel): boolean => { - return !value.activePool; - }, - [], + useSubscription(form.controls.x.valueChanges$, () => + form.patchValue({ y: undefined, pool: undefined }), ); - const addLiquidityAction = useCallback((value: AddLiquidityFormModel) => { - openConfirmationModal( - (next) => { - return ( - - ); - }, - Operation.ADD_LIQUIDITY, - { asset: value.x!, amount: value?.xAmount?.value! }, - { asset: value.y!, amount: value?.yAmount?.value! }, - ); - }, []); - useSubscription( - form.controls.x.valueChanges$, - (token: AssetInfo | undefined) => updateYAssets$.next(token?.id), + combineLatest([ + form.controls.x.valueChangesWithSystem$, + form.controls.y.valueChangesWithSystem$, + ]).pipe(debounceTime(100)), + ([x, y]) => { + updatePools(x?.id, y?.id); + }, ); useSubscription( @@ -194,71 +120,75 @@ const AddLiquidity = (): JSX.Element => { }, ); - useSubscription( - combineLatest([ - form.controls.x.valueChangesWithSystem$, - form.controls.y.valueChangesWithSystem$, - ]).pipe(debounceTime(100)), - ([x, y]) => { - updatePools(x?.id, y?.id); - }, - ); - - useSubscription(form.controls.x.valueChanges$, () => - form.patchValue({ y: undefined, activePool: undefined }), - ); - useSubscription( combineLatest([ form.controls.xAmount.valueChanges$.pipe(skip(1)), - form.controls.activePool.valueChanges$, + form.controls.pool.valueChanges$, ]).pipe(debounceTime(100)), - ([amount]) => { - const newYAmount = form.value.activePool!.depositAmount( - new AssetAmount( - form.value.x!, - parseUserInputToFractions(amount?.value ?? 0, form.value.x!.decimals), - ), - ); - - const value = Number( - renderFractions(newYAmount.amount, newYAmount.asset.decimals), - ); - + ([amount]) => form.controls.yAmount.patchValue( - { - value, - viewValue: value.toString(), - }, - { emitEvent: 'system' }, - ); - }, + amount ? form.value.pool!.calculateDepositAmount(amount) : undefined, + { emitEvent: 'silent' }, + ), ); useSubscription( form.controls.yAmount.valueChanges$.pipe(skip(1)), (amount) => { - const newXAmount = form.value.activePool!.depositAmount( - new AssetAmount( - form.value.y!, - parseUserInputToFractions(amount?.value ?? 0, form.value.y!.decimals), - ), - ); - - const value = Number( - renderFractions(newXAmount.amount, newXAmount.asset.decimals), - ); - form.controls.xAmount.patchValue( - { - value, - viewValue: value.toString(), - }, + amount ? form.value.pool!.calculateDepositAmount(amount) : undefined, { emitEvent: 'system' }, ); }, + [], ); + const getInsufficientTokenNameForFee = ({ + xAmount, + }: Required): string | undefined => { + const totalFeesWithAmount = xAmount.isAssetEquals(networkAsset) + ? xAmount.plus(totalFees) + : totalFees; + + return totalFeesWithAmount.gt(balance.get(networkAsset)) + ? networkAsset.name + : undefined; + }; + + const getInsufficientTokenNameForTx = ({ + xAmount, + yAmount, + }: Required): string | undefined => { + if (xAmount.gt(balance.get(xAmount.asset))) { + return xAmount.asset.name; + } + + if (yAmount.gt(balance.get(yAmount.asset))) { + return yAmount.asset.name; + } + + return undefined; + }; + + const isAmountNotEntered = (value: AddLiquidityFormModel): boolean => { + return !value.xAmount?.isPositive() || !value.yAmount?.isPositive(); + }; + + const isTokensNotSelected = (value: AddLiquidityFormModel): boolean => { + return !value.pool; + }; + + const addLiquidityAction = (value: Required) => { + openConfirmationModal( + (next) => { + return ; + }, + Operation.ADD_LIQUIDITY, + value.xAmount!, + value.yAmount!, + ); + }; + return ( { style={{ opacity: isPairSelected ? '' : '0.3' }} > Select Pool - + {({ value, onChange }) => ( ; onClose: (r: Promise) => void; } -const AddLiquidityConfirmationModal: React.FC = ({ - position, - pair, +const AddLiquidityConfirmationModal: FC = ({ + value, onClose, }) => { const [{ minerFee, address, pk }] = useSettings(); const [utxos] = useObservable(utxos$); + const totalFees = useTotalFees(); const uiFeeNErg = parseUserInputToFractions(UI_FEE, ERG_DECIMALS); const exFeeNErg = parseUserInputToFractions(defaultExFee, ERG_DECIMALS); const minerFeeNErgs = parseUserInputToFractions(minerFee, ERG_DECIMALS); - const totalFees = calculateTotalFee( - [minerFee, UI_FEE, defaultExFee], - ERG_DECIMALS, - ); - const addLiquidityOperation = async () => { - if (position && pk && address && utxos) { - const poolId = position.id; + const { pool, yAmount, xAmount } = value; - const actions = poolActions(position); + if (pool && pk && address && utxos) { + const poolId = pool.id; - const inputX = position.x.withAmount( - parseUserInputToFractions( - String(pair.assetX.amount), - position.x.asset.decimals, - ), - ); - const inputY = position.y.withAmount( - parseUserInputToFractions( - String(pair.assetY.amount), - position.y.asset.decimals, - ), - ); + const actions = poolActions(pool['pool']); + + const inputX = pool['pool'].x.withAmount(xAmount.amount); + const inputY = pool['pool'].y.withAmount(yAmount.amount); const target = makeTarget( [inputX, inputY], @@ -99,7 +85,11 @@ const AddLiquidityConfirmationModal: React.FC = ({ - + @@ -125,6 +115,7 @@ const AddLiquidityConfirmationModal: React.FC = ({ {defaultExFee} ERG + {!!UI_FEE && ( diff --git a/src/pages/Pool/AddLiquidity/FormModel.ts b/src/pages/Pool/AddLiquidity/FormModel.ts index 749314c6a..6d52d990d 100644 --- a/src/pages/Pool/AddLiquidity/FormModel.ts +++ b/src/pages/Pool/AddLiquidity/FormModel.ts @@ -1,12 +1,12 @@ -import { AmmPool } from '@ergolabs/ergo-dex-sdk'; +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; -import { TokenAmountInputValue } from '../../../components/common/TokenControl/TokenAmountInput/TokenAmountInput'; -import { TokenControlValue } from '../../../components/common/TokenControl/TokenControl'; +import { AmmPool } from '../../../common/models/AmmPool'; +import { Currency } from '../../../common/models/Currency'; export interface AddLiquidityFormModel { - readonly x?: TokenControlValue['asset']; - readonly y?: TokenControlValue['asset']; - readonly xAmount?: TokenAmountInputValue; - readonly yAmount?: TokenAmountInputValue; - readonly activePool?: AmmPool; + readonly x?: AssetInfo; + readonly y?: AssetInfo; + readonly xAmount?: Currency; + readonly yAmount?: Currency; + readonly pool?: AmmPool; } diff --git a/src/pages/Pool/Pool.tsx b/src/pages/Pool/Pool.tsx index 2afc211ea..c66b73280 100644 --- a/src/pages/Pool/Pool.tsx +++ b/src/pages/Pool/Pool.tsx @@ -1,17 +1,13 @@ -import { isEmpty } from 'lodash'; -import React, { useContext } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { ConnectWalletButton } from '../../components/common/ConnectWalletButton/ConnectWalletButton'; import { FormPageWrapper } from '../../components/FormPageWrapper/FormPageWrapper'; -import { WalletContext } from '../../context'; -import { Button, Flex, PlusOutlined, Typography } from '../../ergodex-cdk'; +import { Button, Flex, PlusOutlined } from '../../ergodex-cdk'; import { useObservable } from '../../hooks/useObservable'; import { isWalletSetuped$ } from '../../services/new/core'; -import { availablePools$ } from '../../services/new/pools'; import { EmptyPositionsWrapper } from './components/EmptyPositionsWrapper/EmptyPositionsWrapper'; import { LiquidityPositionsList } from './components/LiquidityPositionsList/LiquidityPositionsList'; -import { PositionListLoader } from './components/PositionListLoader/PositionListLoader'; // import { LPGuide } from './LPGuide/LPGuide'; diff --git a/src/pages/Remove/ConfirmRemoveModal/ConfirmRemoveModal.tsx b/src/pages/Remove/ConfirmRemoveModal/ConfirmRemoveModal.tsx index b160f75b9..48fc5efdc 100644 --- a/src/pages/Remove/ConfirmRemoveModal/ConfirmRemoveModal.tsx +++ b/src/pages/Remove/ConfirmRemoveModal/ConfirmRemoveModal.tsx @@ -1,7 +1,9 @@ -import { AmmPool, minValueForOrder } from '@ergolabs/ergo-dex-sdk'; +import { minValueForOrder } from '@ergolabs/ergo-dex-sdk'; import { BoxSelection, DefaultBoxSelector } from '@ergolabs/ergo-sdk'; -import React, { useCallback } from 'react'; +import React from 'react'; +import { AmmPool } from '../../../common/models/AmmPool'; +import { Currency } from '../../../common/models/Currency'; import { InfoTooltip } from '../../../components/InfoTooltip/InfoTooltip'; import { ERG_DECIMALS, UI_FEE } from '../../../constants/erg'; import { defaultExFee } from '../../../constants/settings'; @@ -26,15 +28,17 @@ import { RemoveFormSpaceWrapper } from '../RemoveFormSpaceWrapper/RemoveFormSpac interface ConfirmRemoveModalProps { onClose: (p: Promise) => void; - position: AmmPool; + pool: AmmPool; lpToRemove: number; - pair: AssetPair; + xAmount: Currency; + yAmount: Currency; } const ConfirmRemoveModal: React.FC = ({ - position, + pool, lpToRemove, - pair, + xAmount, + yAmount, onClose, }) => { const UTXOs = useUTXOs(); @@ -49,61 +53,46 @@ const ConfirmRemoveModal: React.FC = ({ ERG_DECIMALS, ); - const removeOperation = useCallback( - async (position: AmmPool) => { - const actions = poolActions(position); - const lp = position.lp.withAmount(BigInt(lpToRemove.toFixed(0))); + const removeOperation = async (pool: AmmPool) => { + const actions = poolActions(pool['pool']); + const lp = pool['pool'].lp.withAmount(BigInt(lpToRemove.toFixed(0))); - const poolId = position.id; + const poolId = pool.id; - try { - const network = await explorer.getNetworkContext(); + try { + const network = await explorer.getNetworkContext(); - const inputs = DefaultBoxSelector.select( - UTXOs, - makeTarget( - [lp], - minValueForOrder(minerFeeNErgs, uiFeeNErg, exFeeNErg), - ), - ) as BoxSelection; + const inputs = DefaultBoxSelector.select( + UTXOs, + makeTarget([lp], minValueForOrder(minerFeeNErgs, uiFeeNErg, exFeeNErg)), + ) as BoxSelection; - if (address && pk) { - onClose( - actions - .redeem( - { - poolId, - pk, - lp, - exFee: exFeeNErg, - uiFee: uiFeeNErg, - }, - { - inputs, - changeAddress: address, - selfAddress: address, - feeNErgs: minerFeeNErgs, - network, - }, - ) - .then((tx) => submitTx(tx)), - ); - } - } catch (err) { - message.error('Network connection issue'); + if (address && pk) { + onClose( + actions + .redeem( + { + poolId, + pk, + lp, + exFee: exFeeNErg, + uiFee: uiFeeNErg, + }, + { + inputs, + changeAddress: address, + selfAddress: address, + feeNErgs: minerFeeNErgs, + network, + }, + ) + .then((tx) => submitTx(tx)), + ); } - }, - [ - UTXOs, - address, - exFeeNErg, - lpToRemove, - minerFeeNErgs, - onClose, - pk, - uiFeeNErg, - ], - ); + } catch (err) { + message.error('Network connection issue'); + } + }; return ( <> @@ -112,7 +101,11 @@ const ConfirmRemoveModal: React.FC = ({ - + @@ -148,6 +141,7 @@ const ConfirmRemoveModal: React.FC = ({ )} + } /> @@ -164,7 +158,7 @@ const ConfirmRemoveModal: React.FC = ({ block type="primary" size="large" - onClick={() => removeOperation(position)} + onClick={() => removeOperation(pool)} > Remove Liquidity diff --git a/src/pages/Remove/PairSpace/PairSpace.tsx b/src/pages/Remove/PairSpace/PairSpace.tsx index 39f1428ed..c1dfedfd2 100644 --- a/src/pages/Remove/PairSpace/PairSpace.tsx +++ b/src/pages/Remove/PairSpace/PairSpace.tsx @@ -1,18 +1,21 @@ import React from 'react'; +import { Currency } from '../../../common/models/Currency'; import { TokenIcon } from '../../../components/TokenIcon/TokenIcon'; import { Box, Flex, Typography } from '../../../ergodex-cdk'; import { RemoveFormSpaceWrapper } from '../RemoveFormSpaceWrapper/RemoveFormSpaceWrapper'; interface PairSpaceProps { - title: string; - pair: AssetPair; - fees?: boolean; + readonly title: string; + readonly xAmount: Currency; + readonly yAmount: Currency; + readonly fees?: boolean; } const PairSpace: React.FC = ({ title, - pair, + xAmount, + yAmount, fees, }): JSX.Element => { return ( @@ -24,17 +27,19 @@ const PairSpace: React.FC = ({ - + - {pair.assetX.name} + + {xAmount.asset.name} + - {fees ? pair.assetX.earnedFees : pair.assetX.amount} + {fees ? undefined : xAmount.toString({ suffix: false })} @@ -45,17 +50,19 @@ const PairSpace: React.FC = ({ - + - {pair.assetY.name} + + {yAmount.asset.name} + - {fees ? pair.assetY.earnedFees : pair.assetY.amount} + {fees ? undefined : yAmount.toString({ suffix: false })} diff --git a/src/pages/Remove/Remove.tsx b/src/pages/Remove/Remove.tsx index 3816a4610..ddb7c8d92 100644 --- a/src/pages/Remove/Remove.tsx +++ b/src/pages/Remove/Remove.tsx @@ -4,6 +4,9 @@ import { evaluate } from 'mathjs'; import React, { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router'; +import { AssetPair } from '../../@types/asset'; +import { AmmPool } from '../../common/models/AmmPool'; +import { Currency } from '../../common/models/Currency'; import { openConfirmationModal, Operation, @@ -14,7 +17,6 @@ import { TokenIconPair } from '../../components/TokenIconPair/TokenIconPair'; import { Flex, Skeleton, Typography } from '../../ergodex-cdk'; import { usePair } from '../../hooks/usePair'; import { usePosition } from '../../hooks/usePosition'; -import { parseUserInputToFractions } from '../../utils/math'; import { ConfirmRemoveModal } from './ConfirmRemoveModal/ConfirmRemoveModal'; import { PairSpace } from './PairSpace/PairSpace'; import { RemoveFormSpaceWrapper } from './RemoveFormSpaceWrapper/RemoveFormSpaceWrapper'; @@ -23,7 +25,7 @@ import { RemovePositionSlider } from './RemovePositionSlider/RemovePositionSlide const getPercent = (val: number | undefined, percent: string): number => Number(evaluate(`${val} * ${percent}%`)); -const Remove = (): JSX.Element => { +export const Remove = (): JSX.Element => { const { poolId } = useParams<{ poolId: PoolId }>(); const DEFAULT_SLIDER_PERCENTAGE = '100'; @@ -52,12 +54,18 @@ const Remove = (): JSX.Element => { setPair({ assetX: { name: pair.assetX.name, - amount: getPercent(initialPair.assetX.amount, percentage), + asset: pair.assetX.asset, + amount: +getPercent(initialPair.assetX.amount, percentage).toFixed( + pair.assetX.asset?.decimals, + ), earnedFees: pair.assetX?.earnedFees, }, assetY: { name: pair.assetY.name, - amount: getPercent(initialPair.assetY.amount, percentage), + asset: pair.assetY.asset, + amount: +getPercent(initialPair.assetY.amount, percentage).toFixed( + pair.assetY.asset?.decimals, + ), earnedFees: pair.assetY?.earnedFees, }, }); @@ -68,36 +76,30 @@ const Remove = (): JSX.Element => { const handleRemove = () => { if (pair && position && lpToRemove) { + const xAmount = new Currency( + pair.assetX.amount?.toString(), + position.x.asset, + ); + const yAmount = new Currency( + pair.assetY.amount?.toString(), + position.y.asset, + ); + openConfirmationModal( (next) => { return ( ); }, Operation.REMOVE_LIQUIDITY, - { - asset: position?.x.asset, - amount: Number( - parseUserInputToFractions( - pair.assetX.amount!, - position?.x.asset.decimals, - ), - ), - }, - { - asset: position?.y.asset, - amount: Number( - parseUserInputToFractions( - pair.assetY.amount!, - position?.y.asset.decimals, - ), - ), - }, + xAmount, + yAmount, ); } }; @@ -138,7 +140,21 @@ const Remove = (): JSX.Element => { - + {/*TODO: ADD_FEES_DISPLAY_AFTER_SDK_UPDATE[EDEX-468]*/} @@ -157,5 +173,3 @@ const Remove = (): JSX.Element => { ); }; - -export { Remove }; diff --git a/src/pages/Swap/Ratio/Ratio.less b/src/pages/Swap/Ratio/Ratio.less new file mode 100644 index 000000000..c745bb6a3 --- /dev/null +++ b/src/pages/Swap/Ratio/Ratio.less @@ -0,0 +1,8 @@ +.ratio { + cursor: pointer; + user-select: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/src/pages/Swap/Ratio/Ratio.tsx b/src/pages/Swap/Ratio/Ratio.tsx index f1b1cb6d2..3e1976cb0 100644 --- a/src/pages/Swap/Ratio/Ratio.tsx +++ b/src/pages/Swap/Ratio/Ratio.tsx @@ -1,76 +1,70 @@ -/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ -import { AmmPool } from '@ergolabs/ergo-dex-sdk'; -import { AssetAmount } from '@ergolabs/ergo-sdk'; -import React, { useEffect, useMemo } from 'react'; -import { - distinctUntilChanged, - filter, - interval, - map, - mapTo, - of, - startWith, - tap, -} from 'rxjs'; +import './Ratio.less'; -import { TokenControlValue } from '../../../components/common/TokenControl/TokenControl'; -import { FormInstance, Typography } from '../../../ergodex-cdk'; +import React, { FC, useState } from 'react'; +import { debounceTime, map } from 'rxjs'; + +import { Currency } from '../../../common/models/Currency'; +import { Animation, Typography } from '../../../ergodex-cdk'; import { FormGroup } from '../../../ergodex-cdk/components/Form/NewForm'; -import { useObservable, useSubject } from '../../../hooks/useObservable'; -import { - math, - parseUserInputToFractions, - renderFractions, -} from '../../../utils/math'; -import { SwapFormModel } from '../SwapModel'; +import { useObservable } from '../../../hooks/useObservable'; +import { SwapFormModel } from '../SwapFormModel'; -export function renderPrice(x: AssetAmount, y: AssetAmount): string { - const nameX = x.asset.name ?? x.asset.id.slice(0, 8); - const nameY = y.asset.name ?? y.asset.id.slice(0, 8); - const fmtX = renderFractions(x.amount, x.asset.decimals); - const fmtY = renderFractions(y.amount, y.asset.decimals); - const p = math.evaluate!(`${fmtY} / ${fmtX}`).toFixed(y.asset.decimals ?? 0); - return `1 ${nameX} - ${p} ${nameY}`; -} +const calculateOutputPrice = ({ + fromAmount, + fromAsset, + pool, +}: Required): Currency => { + if (fromAmount?.isPositive()) { + return pool.calculateOutputPrice(fromAmount); + } else { + return pool.calculateOutputPrice(new Currency('1', fromAsset)); + } +}; -const calculateRatio = (value: SwapFormModel) => { - return renderPrice( - new AssetAmount( - value?.fromAsset!, - parseUserInputToFractions( - value?.fromAmount?.value!, - value?.fromAsset?.decimals!, - ), - ), - new AssetAmount( - value?.toAsset!, - parseUserInputToFractions( - value?.toAmount?.value!, - value?.toAsset?.decimals!, - ), - ), - ); +const calculateInputPrice = ({ + toAmount, + toAsset, + pool, +}: Required): Currency => { + if (toAmount?.isPositive()) { + return pool.calculateInputPrice(toAmount); + } else { + return pool.calculateInputPrice(new Currency('1', toAsset)); + } }; -export const Ratio = ({ form }: { form: FormGroup }) => { +export const Ratio: FC<{ form: FormGroup }> = ({ form }) => { + const [reversedRatio, setReversedRatio] = useState(false); const [ratio] = useObservable( form.valueChangesWithSilent$.pipe( + debounceTime(100), map((value) => { - if ( - value.fromAmount?.value && - value.fromAsset && - value.toAmount?.value && - value.toAsset && - value.pool - ) { - return calculateRatio(value); - } else { + if (!value.pool || !value.fromAsset) { return undefined; } + if (reversedRatio) { + return calculateInputPrice(value as Required); + } else { + return calculateOutputPrice(value as Required); + } }), + map((price) => + reversedRatio + ? `1 ${form.value.toAsset?.name} = ${price?.toString()}` + : `1 ${form.value.fromAsset?.name} = ${price?.toString()}`, + ), ), - { deps: [form] }, + { deps: [form, reversedRatio] }, ); - return {ratio}; + const toggleReversedRatio = () => + setReversedRatio((reversedRatio) => !reversedRatio); + + return ( + + + {ratio} + + + ); }; diff --git a/src/pages/Swap/Swap.tsx b/src/pages/Swap/Swap.tsx index d36967888..1e92e2927 100644 --- a/src/pages/Swap/Swap.tsx +++ b/src/pages/Swap/Swap.tsx @@ -1,93 +1,43 @@ -/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import './Swap.less'; -import { AmmPool } from '@ergolabs/ergo-dex-sdk'; -import { AssetAmount } from '@ergolabs/ergo-sdk'; import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import { maxBy } from 'lodash'; -import React, { useCallback, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { BehaviorSubject, combineLatest, debounceTime, + distinctUntilChanged, filter, map, Observable, of, switchMap, + tap, } from 'rxjs'; +import { AmmPool } from '../../common/models/AmmPool'; import { ActionForm } from '../../components/common/ActionForm/ActionForm'; -import { TokenAmountInputValue } from '../../components/common/TokenControl/TokenAmountInput/TokenAmountInput'; import { TokenControlFormItem } from '../../components/common/TokenControl/TokenControl'; import { openConfirmationModal, Operation, } from '../../components/ConfirmationModal/ConfirmationModal'; import { FormPageWrapper } from '../../components/FormPageWrapper/FormPageWrapper'; -import { - ERG_DECIMALS, - ERG_TOKEN_ID, - ERG_TOKEN_NAME, - UI_FEE, -} from '../../constants/erg'; -import { defaultExFee } from '../../constants/settings'; -import { useSettings } from '../../context'; import { Button, Flex, SwapOutlined, Typography } from '../../ergodex-cdk'; import { useForm } from '../../ergodex-cdk/components/Form/NewForm'; import { useSubscription } from '../../hooks/useObservable'; import { assets$, getAvailableAssetFor } from '../../services/new/assets'; import { useWalletBalance } from '../../services/new/balance'; +import { useNetworkAsset, useTotalFees } from '../../services/new/core'; import { getPoolByPair } from '../../services/new/pools'; -import { fractionsToNum, parseUserInputToFractions } from '../../utils/math'; -import { calculateTotalFee } from '../../utils/transactions'; import { OperationSettings } from './OperationSettings/OperationSettings'; import { Ratio } from './Ratio/Ratio'; import { SwapConfirmationModal } from './SwapConfirmationModal/SwapConfirmationModal'; -import { SwapFormModel } from './SwapModel'; +import { SwapFormModel } from './SwapFormModel'; import { SwapTooltip } from './SwapTooltip/SwapTooltip'; -const convertToTo = ( - fromAmount: TokenAmountInputValue | undefined, - fromAsset: AssetInfo, - pool: AmmPool, -): number | undefined => { - if (!fromAmount) { - return undefined; - } - - const toAmount = pool.outputAmount( - new AssetAmount( - fromAsset, - parseUserInputToFractions(fromAmount.value!, fromAsset.decimals), - ), - ); - - return fractionsToNum(toAmount.amount, toAmount.asset?.decimals); -}; - -const convertToFrom = ( - toAmount: TokenAmountInputValue | undefined, - toAsset: AssetInfo, - pool: AmmPool, -): number | undefined => { - if (!toAmount) { - return undefined; - } - - const fromAmount = pool.inputAmount( - new AssetAmount( - toAsset, - parseUserInputToFractions(toAmount.value!, toAsset.decimals), - ), - ); - - return fromAmount - ? fractionsToNum(fromAmount.amount, fromAmount.asset?.decimals) - : undefined; -}; - const getToAssets = (fromAsset?: string) => fromAsset ? getAvailableAssetFor(fromAsset) : assets$; @@ -105,16 +55,13 @@ export const Swap = (): JSX.Element => { const form = useForm({ fromAmount: undefined, toAmount: undefined, - fromAsset: { - name: 'ERG', - id: '0000000000000000000000000000000000000000000000000000000000000000', - decimals: ERG_DECIMALS, - }, + fromAsset: undefined, toAsset: undefined, pool: undefined, }); + const networkAsset = useNetworkAsset(); const [balance] = useWalletBalance(); - const [{ minerFee }] = useSettings(); + const totalFees = useTotalFees(); const updateToAssets$ = useMemo( () => new BehaviorSubject(undefined), [], @@ -124,73 +71,55 @@ export const Swap = (): JSX.Element => { [], ); - const getInsufficientTokenNameForFee = useCallback( - (value: SwapFormModel) => { - const { fromAmount, fromAsset } = value; - let totalFees = +calculateTotalFee( - [minerFee, UI_FEE, defaultExFee], - ERG_DECIMALS, - ); - totalFees = - fromAsset?.id === ERG_TOKEN_ID - ? totalFees + fromAmount?.value! - : totalFees; + useEffect(() => form.patchValue({ fromAsset: networkAsset }), [networkAsset]); - return +totalFees > balance.get(ERG_TOKEN_ID) - ? ERG_TOKEN_NAME - : undefined; - }, - [minerFee, balance], - ); - - const getInsufficientTokenNameForTx = useCallback( - (value: SwapFormModel) => { - const { fromAmount, fromAsset } = value; - const asset = fromAsset; - const amount = fromAmount?.value; + const getInsufficientTokenNameForFee = ({ + fromAmount, + }: Required) => { + const totalFeesWithAmount = fromAmount.isAssetEquals(networkAsset) + ? fromAmount.plus(totalFees) + : totalFees; - if (asset && amount && amount > balance.get(asset)) { - return asset.name; - } + return totalFeesWithAmount.gt(balance.get(networkAsset)) + ? networkAsset.name + : undefined; + }; - return undefined; - }, - [balance], - ); + const getInsufficientTokenNameForTx = ({ + fromAsset, + fromAmount, + }: SwapFormModel) => { + if (fromAsset && fromAmount && fromAmount.gt(balance.get(fromAsset))) { + return fromAsset.name; + } + return undefined; + }; - const isAmountNotEntered = useCallback( - (value: SwapFormModel) => - !value.fromAmount?.value || !value.toAmount?.value, - [], - ); + const isAmountNotEntered = ({ toAmount, fromAmount }: SwapFormModel) => + !fromAmount?.isPositive() || !toAmount?.isPositive(); - const isTokensNotSelected = useCallback( - (value: SwapFormModel) => !value.toAsset || !value.fromAsset, - [], - ); + const isTokensNotSelected = ({ toAsset, fromAsset }: SwapFormModel) => + !toAsset || !fromAsset; - const submitSwap = useCallback((value: SwapFormModel) => { + const submitSwap = (value: Required) => { openConfirmationModal( (next) => { return ; }, Operation.SWAP, - { asset: value.fromAsset!, amount: value?.fromAmount?.value! }, - { asset: value.toAsset!, amount: value?.toAmount?.value! }, + value.fromAmount!, + value.toAmount!, ); - }, []); - - const isLiquidityInsufficient = useCallback((value: SwapFormModel) => { - const { toAmount, pool } = value; + }; - if (!toAmount?.value || !pool) { + const isLiquidityInsufficient = ({ toAmount, pool }: SwapFormModel) => { + if (!toAmount?.isPositive() || !pool) { return false; } + return toAmount?.gt(pool.getAssetAmount(toAmount?.asset)); + }; - return ( - toAmount.value > fractionsToNum(pool?.y.amount, pool?.y.asset.decimals) - ); - }, []); + useSubscription(form.valueChangesWithSilent$, (value) => console.log(value)); useSubscription( form.controls.fromAsset.valueChangesWithSilent$, @@ -207,10 +136,21 @@ export const Swap = (): JSX.Element => { useSubscription( combineLatest([ - form.controls.fromAsset.valueChanges$, - form.controls.toAsset.valueChanges$, + form.controls.fromAsset.valueChangesWithSilent$.pipe( + distinctUntilChanged(), + ), + form.controls.toAsset.valueChangesWithSilent$.pipe( + distinctUntilChanged(), + ), ]).pipe( debounceTime(100), + distinctUntilChanged(([prevFrom, prevTo], [nextFrom, nextTo]) => { + return ( + (prevFrom?.id === nextFrom?.id && prevTo?.id === nextTo?.id) || + (prevFrom?.id === nextTo?.id && prevTo?.id === nextFrom?.id) + ); + }), + tap(() => form.patchValue({ pool: undefined })), switchMap(([fromAsset, toAsset]) => getSelectedPool(fromAsset?.id, toAsset?.id), ), @@ -220,56 +160,45 @@ export const Swap = (): JSX.Element => { useSubscription( combineLatest([ - form.controls.fromAmount.valueChanges$, + form.controls.fromAmount.valueChangesWithSystem$, form.controls.pool.valueChanges$, ]).pipe( debounceTime(100), - filter(([amount, pool]) => !!amount && !!form.value.fromAsset && !!pool), + filter(([_, pool]) => !!form.value.fromAsset && !!pool), ), ([amount, pool]) => { - const toAmount = convertToTo(amount!, form.value.fromAsset!, pool!); form.patchValue( - { - toAmount: toAmount - ? { value: toAmount, viewValue: toAmount.toString() } - : undefined, - }, - { emitEvent: 'system' }, + { toAmount: amount ? pool?.calculateOutputAmount(amount) : undefined }, + { emitEvent: 'silent' }, ); }, ); useSubscription( - combineLatest([ - form.controls.toAmount.valueChanges$, - form.controls.pool.valueChanges$, - ]).pipe( + form.controls.toAmount.valueChanges$.pipe( debounceTime(100), - filter(([amount, pool]) => !!amount && !!form.value.toAsset && !!pool), + filter(() => !!form.value.toAsset && !!form.value.pool), ), - ([amount, pool]) => { - const fromAmount = convertToFrom(amount!, form.value.toAsset!, pool!); - + (amount) => { form.patchValue( { - fromAmount: fromAmount - ? { value: fromAmount, viewValue: fromAmount.toString() } + fromAmount: amount + ? form.value.pool?.calculateInputAmount(amount) : undefined, }, - { emitEvent: 'system' }, + { emitEvent: 'silent' }, ); }, ); - const swapTokens = () => { + const switchAssets = () => { form.patchValue( { fromAsset: form.value.toAsset, fromAmount: form.value.toAmount, toAsset: form.value.fromAsset, - toAmount: form.value.fromAmount, }, - { emitEvent: 'silent' }, + { emitEvent: 'system' }, ); }; @@ -308,9 +237,9 @@ export const Swap = (): JSX.Element => {