+
{extensionLoading || activeAccountLoading ? (
-
loading...
+
loading...
) : isExtensionAvailable ? (
<>
{accountData?.name ? (
-
-
- {accountData?.nativeAssetBalance} BSX
-
- {/* TODO! Acc name / address + Icon component*/}
-
- {accountData?.name}
-
+
+
+ {accountData?.nativeAssetBalance} BSX
+ {/* TODO! Acc name / address + Icon component*/}
+
+ {accountData?.name}
+
+
) : (
-
select an account
+
+ select account
+
)}
>
) : (
-
Extension unavailable
+
+ Extension unavailable
+
)}
-
v
+
v
);
};
diff --git a/src/components/Pools/PoolsForm.scss b/src/components/Pools/PoolsForm.scss
new file mode 100644
index 00000000..8895eda0
--- /dev/null
+++ b/src/components/Pools/PoolsForm.scss
@@ -0,0 +1,334 @@
+@import './../../misc/colors.module.scss';
+@import './../../misc/misc.module.scss';
+@import '../Button/Button.scss';
+
+.pools-form-wrapper {
+ position: relative;
+ flex-basis: 350px;
+ flex-grow: 1;
+
+ padding: 22px;
+ min-width: 350px;
+ max-width: 610px;
+ margin: 0 auto;
+
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);
+ background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%);
+ overflow: hidden;
+ border-radius: 10px;
+
+ position: relative;
+
+ color: white;
+
+ .settings-button-wrapper {
+ position: absolute;
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ right: 10px;
+ top: 10px;
+ gap: 10px;
+ padding: 10px;
+
+ .pool-settings-button {
+ display: flex;
+ padding: 10px 8px;
+ width: fit-content;
+ height: fit-content;
+ border-radius: 50%;
+ background-color: rgba(162, 176, 187, 0.1);
+
+ svg {
+ width: 24px;
+ }
+
+ &:hover {
+ cursor: pointer;
+
+ svg {
+ path {
+ fill: $green1;
+ }
+ }
+ }
+ }
+
+ .pool-page-tabs {
+ display: flex;
+ flex-direction: row;
+ justify-content: right;
+ width: 90%;
+
+ .tab {
+ @extend .button--primary;
+ width: 100px;
+ border-radius: $border-radius 0px 0px $border-radius;
+ color: $gray4;
+ background-color: rgba(162, 176, 187, 0.1);
+
+ &:hover {
+ color: rgba(79, 255, 176, 1);
+ background-color: rgba(162, 176, 187, 0.15);
+ }
+
+ &:disabled {
+ color: rgba(79, 255, 176, 1);
+ background-color: rgba(162, 176, 187, 0.2);
+ }
+
+ &:first-child {
+ border-radius: $border-radius 0px 0px $border-radius;
+ }
+
+ &:last-child {
+ border-radius: 0px $border-radius $border-radius 0px;
+ }
+
+ &:not(:last-child) {
+ border-right: 1px solid rgba(162, 176, 187, 0.1);
+ }
+ }
+ }
+ }
+
+ .pools-form {
+ height: 100%;
+ min-height: 400px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 14px;
+
+ .pools-form-heading {
+ width: fit-content;
+ padding-top: 4px;
+ color: $l-gray3;
+ font-size: 22px;
+ font-weight: 500;
+ background: linear-gradient(
+ 90deg,
+ #4fffb0 1.27%,
+ #b3ff8f 48.96%,
+ #ff984e 104.14%
+ ),
+ linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%),
+ linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-fill-color: transparent;
+ }
+
+ .divider-wrapper {
+ display: flex;
+ align-items: center;
+ height: 1px;
+ width: 100%;
+ }
+
+ .divider {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ background-color: rgba(76, 243, 168, 0.12);
+ opacity: 1;
+ border: 0;
+ left: 0;
+ }
+
+ .balance-wrapper {
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: end;
+ background: rgba(162, 176, 187, 0.1);
+ padding: 12px;
+ padding-top: 24px;
+ border-radius: 10px;
+ gap: 6px;
+ padding-bottom: 16px;
+ }
+
+ .balance-wrapper-share-tokens {
+ @extend .balance-wrapper;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+
+ .submit-button {
+ background: $green1;
+ text-transform: uppercase;
+ border-radius: 36px;
+ height: 50px;
+
+ color: $d-gray4;
+
+ &:hover {
+ background-color: $green2;
+ }
+
+ &:disabled {
+ background-color: $l-gray5;
+ }
+ }
+ }
+}
+
+// SHOULD BE EXTRACTED TO COMPONENTS
+
+.balance-info {
+ display: flex;
+ align-items: center;
+ justify-content: right;
+ width: 100%;
+ gap: 4px;
+
+ height: 16px;
+ margin-top: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ position: relative;
+
+ .balance-info-type {
+ position: absolute;
+ left: 0;
+ top: -7px;
+ font-weight: 600;
+ font-size: 16px;
+ color: $green1;
+ padding: 6px;
+ }
+}
+
+.asset-switch {
+ display: flex;
+ height: 43px;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+
+ .asset-switch-icon {
+ position: absolute;
+ left: 24px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ overflow: hidden;
+ background: #192022;
+ border-radius: 50%;
+
+ transition: transform 500ms ease;
+
+ &:hover {
+ cursor: pointer;
+
+ transform: rotate(180deg);
+
+ svg {
+ path {
+ fill: $green1;
+ }
+ }
+ }
+ }
+
+ .asset-switch-price {
+ position: absolute;
+ right: 24px;
+ background: #192022;
+
+ &__wrapper {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ padding: 4px 14px;
+ font-size: 11px;
+ font-weight: 500;
+
+ background: rgba(218, 255, 238, 0.06);
+ border-radius: 7px;
+ }
+ }
+}
+
+.trade-settings-wrapper {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+
+ z-index: 1;
+
+ .disclaimer {
+ padding: 12px 24px;
+ padding-top: 0px;
+ font-size: 14px;
+ color: $gray4;
+ }
+
+ .trade-settings {
+ height: 100%;
+ }
+
+ .settings-section {
+ padding: 12px 24px;
+ background: linear-gradient(0deg, #171518, #171518), #1c1a1f;
+ }
+
+ .settings-field {
+ padding: 12px 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ &__label {
+ flex-grow: 10;
+ }
+
+ input {
+ flex-shrink: 10;
+ flex-basis: 50px;
+ width: 50px;
+ text-align: center;
+
+ border-radius: $border-radius;
+ }
+ }
+
+ &.hidden {
+ display: none;
+ }
+}
+
+.debug-box {
+ position: fixed;
+ padding: 16px;
+ right: 0;
+ top: 0;
+
+ height: 100%;
+
+ overflow-y: scroll;
+
+ background-color: rgba(0, 0, 0, 0.8);
+}
+
+.max-button {
+ font-size: 12px;
+ font-weight: 400;
+ color: $white1;
+ padding: 4px 10px;
+ background: rgba(255, 255, 255, 0.06);
+ border-radius: 12px;
+ text-transform: capitalize;
+ cursor: pointer;
+
+ &.disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+}
diff --git a/src/components/Pools/PoolsForm.tsx b/src/components/Pools/PoolsForm.tsx
new file mode 100644
index 00000000..5da745db
--- /dev/null
+++ b/src/components/Pools/PoolsForm.tsx
@@ -0,0 +1,1261 @@
+import BigNumber from 'bignumber.js';
+import classNames from 'classnames';
+import { every, find, times } from 'lodash';
+import {
+ MutableRefObject,
+ useCallback,
+ useDebugValue,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { Control, FormProvider, useForm } from 'react-hook-form';
+import { Account, Balance, Maybe, Pool } from '../../generated/graphql';
+import { fromPrecision12 } from '../../hooks/math/useFromPrecision';
+import { useMath } from '../../hooks/math/useMath';
+import { percentageChange } from '../../hooks/math/usePercentageChange';
+import { toPrecision12 } from '../../hooks/math/useToPrecision';
+import { SubmitTradeMutationVariables } from '../../hooks/pools/mutations/useSubmitTradeMutation';
+import { idToAsset, TradeAssetIds } from '../../pages/TradePage/TradePage';
+import { AssetBalanceInput } from '../Balance/AssetBalanceInput/AssetBalanceInput';
+import { PoolType } from '../Chart/shared';
+import { PoolsInfo } from './PoolsInfo/PoolsInfo';
+import './PoolsForm.scss';
+import Icon from '../Icon/Icon';
+import { useModalPortal } from '../Balance/AssetBalanceInput/hooks/useModalPortal';
+import { FormattedBalance } from '../Balance/FormattedBalance/FormattedBalance';
+import { useDebugBoxContext } from '../../pages/TradePage/hooks/useDebugBox';
+import { horizontalBar } from '../Chart/ChartHeader/ChartHeader';
+import { usePolkadotJsContext } from '../../hooks/polkadotJs/usePolkadotJs';
+import { useApolloClient } from '@apollo/client';
+import { estimateBuy } from '../../hooks/pools/xyk/buy';
+import { estimateSell } from '../../hooks/pools/xyk/sell';
+import { payment } from '@polkadot/types/interfaces/definitions';
+import { useMultiFeePaymentConversionContext } from '../../containers/MultiProvider';
+
+export interface PoolsFormSettingsProps {
+ allowedSlippage: string | null;
+ onAllowedSlippageChange: (allowedSlippage: string | null) => void;
+ closeModal: any;
+}
+
+export enum ProvisioningType {
+ Add,
+ Remove,
+}
+
+export interface PoolsFormSettingsFormFields {
+ allowedSlippage: string | null;
+ autoSlippage: boolean;
+}
+
+export const PoolsFormSettings = ({
+ allowedSlippage,
+ onAllowedSlippageChange,
+ closeModal,
+}: PoolsFormSettingsProps) => {
+ const { register, watch, getValues, setValue, handleSubmit } =
+ useForm
({
+ defaultValues: {
+ allowedSlippage,
+ autoSlippage: true,
+ },
+ });
+
+ // propagate allowed slippage to the parent
+ useEffect(() => {
+ onAllowedSlippageChange(getValues('allowedSlippage'));
+ }, watch(['allowedSlippage']));
+
+ // if you want automatic slippage, override the previous user's input
+ useEffect(() => {
+ if (getValues('autoSlippage')) {
+ // default is 3%
+ setValue('allowedSlippage', '3');
+ }
+ }, watch(['autoSlippage']));
+
+ return (
+
+ );
+};
+
+export const useModalPortalElement = ({
+ allowedSlippage,
+ setAllowedSlippage,
+}: any) => {
+ return useCallback(
+ ({ closeModal, elementRef, isModalOpen }) => {
+ return (
+
+
{
+ setAllowedSlippage(allowedSlippage);
+ }}
+ />
+
+ );
+ },
+ [allowedSlippage]
+ );
+};
+
+export interface PoolsFormProps {
+ assets?: { id: string }[];
+ assetIds: TradeAssetIds;
+ onAssetIdsChange: (assetIds: TradeAssetIds) => void;
+ isActiveAccountConnected?: boolean;
+ pool?: Pool;
+ assetInLiquidity?: string;
+ assetOutLiquidity?: string;
+ spotPrice?: {
+ outIn?: string;
+ inOut?: string;
+ };
+ isPoolLoading: boolean;
+ onSubmit: (form: PoolsFormFields & { amountBMaxLimit?: string }) => void;
+ tradeLoading: boolean;
+ activeAccountTradeBalances?: {
+ outBalance?: Balance;
+ inBalance?: Balance;
+ shareBalance?: Balance;
+ };
+ activeAccountTradeBalancesLoading: boolean;
+ activeAccount?: Maybe;
+}
+
+export interface PoolsFormFields {
+ assetIn: string | null;
+ assetOut: string | null;
+ assetInAmount: string | null;
+ assetOutAmount: string | null;
+ shareAssetAmount: string | null;
+ submit: void;
+ warnings: any;
+ provisioningType: ProvisioningType;
+}
+
+/**
+ * Trigger a state update each time the given input changes (via the `input` event)
+ * @param control
+ * @param field
+ * @returns
+ */
+export const useListenForInput = (
+ inputRef: MutableRefObject
+) => {
+ const [state, setState] = useState();
+
+ useEffect(() => {
+ if (!inputRef) return;
+ // TODO: figure out why using the 'input' broke the mask
+ // 'keydown' also doesnt work bcs its triggered by copy/paste, which then
+ // changes the trade type (which this hook is primarily)
+ const listener = inputRef.current?.addEventListener('keydown', () =>
+ setState((state) => !state)
+ );
+
+ return () =>
+ listener && inputRef.current?.removeEventListener('keydown', listener);
+ }, [inputRef]);
+
+ return state;
+};
+
+export const PoolsForm = ({
+ assetIds,
+ onAssetIdsChange,
+ isActiveAccountConnected,
+ pool,
+ isPoolLoading,
+ assetInLiquidity,
+ assetOutLiquidity,
+ spotPrice,
+ onSubmit,
+ tradeLoading,
+ assets,
+ activeAccountTradeBalances,
+ activeAccountTradeBalancesLoading,
+ activeAccount,
+}: PoolsFormProps) => {
+ // TODO: include math into loading form state
+ const { math, loading: mathLoading } = useMath();
+ const [provisioningType, setProvisioningType] = useState(
+ ProvisioningType.Add
+ );
+ const [allowedSlippage, setAllowedSlippage] = useState(null);
+
+ const form = useForm({
+ reValidateMode: 'onChange',
+ mode: 'all',
+ defaultValues: {
+ assetIn: assetIds.assetIn,
+ assetOut: assetIds.assetOut,
+ },
+ });
+ const {
+ register,
+ handleSubmit,
+ watch,
+ getValues,
+ setValue,
+ trigger,
+ control,
+ formState,
+ } = form;
+
+ const { isValid, isDirty, errors } = formState;
+
+ const assetOutAmountInputRef = useRef(null);
+ const assetInAmountInputRef = useRef(null);
+ const shareAmountInputRef = useRef(null);
+
+ // trigger form field validation right away
+ useEffect(() => {
+ trigger('submit');
+ }, []);
+
+ useEffect(() => {
+ // must provide input name otherwise it does not validate appropriately
+ trigger('submit');
+ }, [
+ isActiveAccountConnected,
+ pool,
+ isPoolLoading,
+ activeAccountTradeBalances,
+ assetInLiquidity,
+ assetOutLiquidity,
+ allowedSlippage,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ ]);
+
+ // when the assetIds change, propagate the change to the parent
+ useEffect(() => {
+ const { assetIn, assetOut } = getValues();
+ onAssetIdsChange({ assetIn, assetOut });
+ }, watch(['assetIn', 'assetOut']));
+
+ const assetInAmountInput = useListenForInput(assetInAmountInputRef);
+ const assetOutAmountInput = useListenForInput(assetOutAmountInputRef);
+ const shareAssetAmountInput = useListenForInput(shareAmountInputRef);
+
+ useEffect(
+ () => setValue('provisioningType', provisioningType),
+ [setValue, provisioningType]
+ );
+
+ const [lastAssetInteractedWith, setLastAssetInteractedWith] = useState<
+ string | null
+ >();
+
+ const calculateAssetIn = useCallback(() => {
+ setTimeout(() => {
+ const [assetOutAmount, shareAssetAmount, assetIn, assetOut] = getValues([
+ 'assetOutAmount',
+ 'shareAssetAmount',
+ 'assetIn',
+ 'assetOut',
+ ]);
+ if (
+ !pool ||
+ !math ||
+ !assetInLiquidity ||
+ !assetOutLiquidity ||
+ !activeAccountTradeBalances ||
+ !assetIn ||
+ !assetOut ||
+ !shareAssetAmount
+ )
+ return;
+ // if (provisioningType !== ProvisioningType.Add) return;
+
+ // if (!assetOutAmount) return setValue('assetInAmount', null);
+
+ // const amount = math.xyk.calculate_in_given_out(
+ // // which combination is correct?
+ // // assetOutLiquidity,
+ // // assetInLiquidity,
+ // assetInLiquidity,
+ // assetOutLiquidity,
+ // assetOutAmount
+ // );
+
+ if (provisioningType === ProvisioningType.Add && assetOutAmount) {
+ const amount = math.xyk.calculate_liquidity_in(
+ assetOutLiquidity,
+ assetInLiquidity,
+ assetOutAmount
+ );
+
+ console.log('calculateAssetIn2', {
+ assetOutLiquidity, assetInLiquidity, assetOutAmount, amount
+ })
+
+ // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in
+ if (amount === '0' && assetOutAmount !== '0') return;
+ setValue('assetInAmount', amount || null);
+ } else {
+ const amountA = math.xyk.calculate_liquidity_out_asset_a(
+ assetInLiquidity,
+ assetOutLiquidity,
+ shareAssetAmount,
+ pool.totalLiquidity
+ );
+
+ console.log('calculateAssetIn1', {
+ assetOutLiquidity, assetInLiquidity, assetOutAmount, amountA
+ })
+
+ // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in
+ // if (amountA === '0' && amountB !== '0') return;
+ setValue('assetInAmount', amountA || null);
+ }
+ }, 0);
+ }, [
+ math,
+ getValues,
+ setValue,
+ pool,
+ assetInLiquidity,
+ assetOutLiquidity,
+ provisioningType,
+ activeAccountTradeBalances,
+ ]);
+
+ const calculateAssetOut = useCallback(() => {
+ setTimeout(() => {
+ const [assetInAmount, shareAssetAmount, assetIn, assetOut] = getValues([
+ 'assetInAmount',
+ 'shareAssetAmount',
+ 'assetIn',
+ 'assetOut',
+ ]);
+
+ console.log('calculateAssetOut1', [assetInAmount, shareAssetAmount, assetIn, assetOut]);
+
+ if (
+ !pool ||
+ !math ||
+ !assetInLiquidity ||
+ !assetOutLiquidity ||
+ !activeAccountTradeBalances ||
+ !assetIn ||
+ !assetOut ||
+ !shareAssetAmount
+ )
+ return;
+ // if (provisioningType !== ProvisioningType.Remove) return;
+
+ // if (!assetInAmount) return setValue('assetOutAmount', null);
+
+ // const amount = math.xyk.calculate_out_given_in(
+ // assetInLiquidity,
+ // assetOutLiquidity,
+ // assetInAmount
+ // );
+ // if (amount === '0' && assetInAmount !== '0')
+ // return setValue('assetOutAmount', null);
+ // setValue('assetOutAmount', amount || null);
+
+ if (provisioningType === ProvisioningType.Add && assetInAmount) {
+ const amount = math.xyk.calculate_liquidity_in(
+ assetInLiquidity,
+ assetOutLiquidity,
+ assetInAmount
+ );
+
+ // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in
+ if (amount === '0' && assetInAmount !== '0' ) return;
+ setValue('assetOutAmount', amount || null);
+ } else {
+ const amountB = math.xyk.calculate_liquidity_out_asset_b(
+ assetInLiquidity,
+ assetOutLiquidity,
+ shareAssetAmount,
+ pool.totalLiquidity
+ );
+
+
+ // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in
+ // if (amountB === '0' && assetInAmount !== '0') return;
+ setValue('assetOutAmount', amountB || null);
+ }
+ }, 0);
+ }, [
+ math,
+ getValues,
+ setValue,
+ pool,
+ assetInLiquidity,
+ assetOutLiquidity,
+ provisioningType,
+ activeAccountTradeBalances,
+ ]);
+
+ useEffect(() => {
+ if (lastAssetInteractedWith === assetIds.assetIn) return;
+ calculateAssetIn();
+ }, [
+ calculateAssetIn,
+ lastAssetInteractedWith,
+ assetOutAmountInput,
+ assetIds
+ ]);
+
+ useEffect(() => {
+ if (lastAssetInteractedWith === assetIds.assetOut) return;
+ calculateAssetOut();
+ }, [
+ calculateAssetOut,
+ lastAssetInteractedWith,
+ assetInAmountInput,
+ assetIds,
+ ]);
+
+ useEffect(() => {
+ if (provisioningType === ProvisioningType.Remove) return;
+ const [assetInAmount, assetOutAmount, assetIn, assetOut] = getValues([
+ 'assetInAmount',
+ 'assetOutAmount',
+ 'assetIn',
+ 'assetOut',
+ ]);
+ if (!assetIn || !assetOut || !assetInLiquidity || !assetInAmount || !pool) return;
+
+ const shareAmount = math?.xyk.calculate_shares(assetInLiquidity, assetInAmount, pool?.totalLiquidity);
+ shareAmount && setValue('shareAssetAmount', shareAmount);
+ // assetIn > assetOut
+ // ? setValue('shareAssetAmount', assetOutAmount)
+ // : setValue('shareAssetAmount', assetInAmount);
+ }, [
+ ...watch(['assetInAmount', 'assetOutAmount', 'assetIn', 'assetOut']),
+ math,
+ assetInLiquidity,
+ provisioningType,
+ getValues,
+ pool
+ ]);
+
+ useEffect(() => {
+ setTimeout(() => {
+ if (provisioningType === ProvisioningType.Add) return;
+ const [
+ assetInAmount,
+ assetOutAmount,
+ assetIn,
+ assetOut,
+ shareAssetAmount,
+ ] = getValues([
+ 'assetInAmount',
+ 'assetOutAmount',
+ 'assetIn',
+ 'assetOut',
+ 'shareAssetAmount',
+ ]);
+ if (!assetIn || !assetOut) return;
+ console.log('calc', assetIn, assetOut)
+ calculateAssetIn();
+ calculateAssetOut();
+ }, 0);
+ }, [shareAssetAmountInput, calculateAssetIn, calculateAssetOut, provisioningType]);
+
+ const getSubmitText = useCallback(() => {
+ if (isPoolLoading) return 'loading';
+
+ // TODO: change to 'input amounts'?
+ // if (!isDirty) return 'Swap';
+
+ switch (errors.submit?.type) {
+ case 'activeAccount':
+ return 'Select account';
+ case 'poolDoesNotExist':
+ return 'Select tokens';
+ }
+
+ if (errors.assetInAmount || errors.assetOutAmount) return 'invalid amount';
+
+ return provisioningType === ProvisioningType.Add
+ ? 'Add Liquidity'
+ : 'Remove Liquidity';
+ }, [isPoolLoading, errors, isDirty, provisioningType]);
+
+ const modalContainerRef = useRef(null);
+
+ const modalPortalElement = useModalPortalElement({
+ allowedSlippage,
+ setAllowedSlippage,
+ });
+ const { toggleModal, modalPortal, toggleId } = useModalPortal(
+ modalPortalElement,
+ modalContainerRef,
+ false
+ );
+
+ const tradeLimit = useMemo(() => {
+ // convert from precision, otherwise the math doesnt work
+ const assetInAmount = fromPrecision12(
+ getValues('assetInAmount') || undefined
+ );
+ const assetOutAmount = fromPrecision12(
+ getValues('assetOutAmount') || undefined
+ );
+ const assetIn = getValues('assetIn');
+ const assetOut = getValues('assetOut');
+
+ if (
+ !assetInAmount ||
+ !assetOutAmount ||
+ !spotPrice?.inOut ||
+ !spotPrice?.outIn ||
+ !assetIn ||
+ !assetOut ||
+ !allowedSlippage
+ )
+ return;
+
+ console.log('limit', {
+ assetInAmount,
+ spotPrice,
+ allowedSlippage
+ })
+
+ switch (lastAssetInteractedWith) {
+ case assetIds.assetIn:
+ return {
+ balance: new BigNumber(assetInAmount)
+ .multipliedBy(spotPrice?.inOut)
+ .multipliedBy(new BigNumber('1').plus(allowedSlippage))
+ .toFixed(0),
+ assetId: assetOut,
+ };
+ case assetIds.assetOut:
+ return {
+ balance: new BigNumber(assetOutAmount)
+ .multipliedBy(spotPrice?.outIn)
+ .multipliedBy(new BigNumber('1').plus(allowedSlippage))
+ .toFixed(0),
+ assetId: assetIn,
+ };
+ }
+ }, [
+ spotPrice,
+ provisioningType,
+ allowedSlippage,
+ getValues,
+ assetIds,
+ lastAssetInteractedWith,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ ]);
+
+ const slippage = useMemo(() => {
+ const assetInAmount = getValues('assetInAmount');
+ const assetOutAmount = getValues('assetOutAmount');
+
+ if (!assetInAmount || !assetOutAmount || !spotPrice || !allowedSlippage)
+ return;
+
+ switch (provisioningType) {
+ case ProvisioningType.Remove:
+ return percentageChange(
+ new BigNumber(assetInAmount).multipliedBy(
+ fromPrecision12(spotPrice.inOut) || '1'
+ ),
+ assetOutAmount
+ )?.abs();
+ case ProvisioningType.Add:
+ return percentageChange(
+ new BigNumber(assetOutAmount).multipliedBy(
+ fromPrecision12(spotPrice.outIn) || '1'
+ ),
+ assetInAmount
+ )?.abs();
+ }
+ }, [
+ provisioningType,
+ getValues,
+ spotPrice,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ ]);
+
+ useEffect(() => {
+ setLastAssetInteractedWith(assetIds.assetIn);
+ }, [assetInAmountInput, assetIds.assetIn]);
+
+ useEffect(() => {
+ setLastAssetInteractedWith(assetIds.assetOut);
+ }, [assetOutAmountInput, assetIds.assetOut]);
+
+ // handle submit of the form
+ const _handleSubmit = useCallback(
+ (data: PoolsFormFields) => {
+ if (!lastAssetInteractedWith) return;
+ onSubmit({
+ ...data,
+ assetIn: lastAssetInteractedWith,
+ assetOut:
+ lastAssetInteractedWith === data.assetOut
+ ? data.assetIn
+ : data.assetOut,
+ assetInAmount:
+ lastAssetInteractedWith === data.assetOut
+ ? data.assetOutAmount
+ : data.assetInAmount,
+ assetOutAmount:
+ lastAssetInteractedWith === data.assetOut
+ ? data.assetInAmount
+ : data.assetOutAmount,
+ amountBMaxLimit: tradeLimit?.balance,
+ });
+ },
+ [
+ provisioningType,
+ tradeLimit,
+ lastAssetInteractedWith,
+ assetIds,
+ tradeLimit,
+ ]
+ );
+
+ const handleSwitchAssets = useCallback(
+ (event: any) => {
+ onAssetIdsChange({
+ assetIn: assetIds.assetOut,
+ assetOut: assetIds.assetIn,
+ });
+
+ // prevent form submit
+ event.preventDefault();
+ if (lastAssetInteractedWith === assetIds.assetOut) {
+ setLastAssetInteractedWith(assetIds.assetIn)
+ const assetOutAmount = getValues('assetOutAmount');
+ setValue('assetInAmount', assetOutAmount);
+ } else {
+ setLastAssetInteractedWith(assetIds.assetOut);
+ const assetInAmount = getValues('assetInAmount');
+ setValue('assetOutAmount', assetInAmount);
+ }
+
+ setTimeout(() => {
+
+ }, 0);
+ },
+ [assetIds, setValue, getValues, lastAssetInteractedWith]
+ );
+
+ const { apiInstance } = usePolkadotJsContext();
+ const { cache } = useApolloClient();
+ const [paymentInfo, setPaymentInfo] = useState();
+ const { convertToFeePaymentAsset } = useMultiFeePaymentConversionContext();
+ const calculatePaymentInfo = useCallback(async () => {
+ if (!apiInstance) return;
+ let [assetIn, assetOut, assetInAmount, assetOutAmount] = getValues([
+ 'assetIn',
+ 'assetOut',
+ 'assetInAmount',
+ 'assetOutAmount',
+ ]);
+
+ if (
+ !assetIn ||
+ !assetOut ||
+ !assetInAmount ||
+ !assetOutAmount ||
+ !tradeLimit
+ )
+ return;
+
+ switch (provisioningType) {
+ case ProvisioningType.Add: {
+ const estimate = await estimateBuy(
+ cache,
+ apiInstance,
+ assetOut,
+ assetIn,
+ assetOutAmount,
+ tradeLimit.balance
+ );
+ const partialFee = estimate?.partialFee.toString();
+ return convertToFeePaymentAsset(partialFee);
+ }
+ case ProvisioningType.Remove: {
+ const estimate = await estimateSell(
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut,
+ assetInAmount,
+ tradeLimit.balance
+ );
+ const partialFee = estimate?.partialFee.toString();
+ return convertToFeePaymentAsset(partialFee);
+ }
+ default:
+ return;
+ }
+ }, [
+ apiInstance,
+ cache,
+ ...watch(['assetInAmount', 'assetOutAmount', 'assetIn']),
+ tradeLimit,
+ provisioningType,
+ convertToFeePaymentAsset,
+ ]);
+
+ useEffect(() => {
+ (async () => {
+ const paymentInfo = await calculatePaymentInfo();
+ if (!paymentInfo) return;
+ setPaymentInfo(paymentInfo);
+ })();
+ }, [
+ apiInstance,
+ cache,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ tradeLimit,
+ provisioningType,
+ calculatePaymentInfo
+ ]);
+
+ useEffect(() => {
+ setValue('assetIn', assetIds.assetIn);
+ setValue('assetOut', assetIds.assetOut);
+ }, [assetIds]);
+
+ const tradeBalances = useMemo(() => {
+ const assetOutAmount = getValues('assetOutAmount');
+ const outBeforeTrade = activeAccountTradeBalances?.outBalance?.balance;
+ const outAfterTrade =
+ (outBeforeTrade &&
+ assetOutAmount &&
+ new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0)) ||
+ undefined;
+ const outTradeChange =
+ outBeforeTrade !== '0'
+ ? percentageChange(
+ fromPrecision12(outBeforeTrade),
+ fromPrecision12(outAfterTrade)
+ )?.multipliedBy(100)
+ : new BigNumber(
+ outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0'
+ );
+
+ const assetInAmount = getValues('assetInAmount');
+ const inBeforeTrade = activeAccountTradeBalances?.inBalance?.balance;
+ let inAfterTrade =
+ (inBeforeTrade &&
+ assetInAmount &&
+ new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0)) ||
+ undefined;
+
+ inAfterTrade =
+ getValues('assetIn') !== '0'
+ ? inAfterTrade
+ : paymentInfo &&
+ inAfterTrade &&
+ new BigNumber(inAfterTrade).minus(paymentInfo).toFixed(0);
+
+ const inTradeChange =
+ inBeforeTrade !== '0'
+ ? percentageChange(
+ fromPrecision12(inBeforeTrade),
+ fromPrecision12(inAfterTrade)
+ )?.multipliedBy(100)
+ : new BigNumber(
+ inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0'
+ );
+
+ return {
+ outBeforeTrade,
+ outAfterTrade,
+ outTradeChange,
+
+ inBeforeTrade,
+ inAfterTrade,
+ inTradeChange,
+ };
+ }, [
+ activeAccountTradeBalances,
+ ...watch(['assetOutAmount', 'assetInAmount', 'assetIn']),
+ paymentInfo,
+ ]);
+
+ const { debugComponent } = useDebugBoxContext();
+
+ useEffect(() => {
+ debugComponent('PoolsForm', {
+ ...getValues(),
+ spotPrice,
+ tradeLimit,
+ assetInLiquidity,
+ assetOutLiquidity,
+ tradeBalances: {
+ ...tradeBalances,
+ inTradeChange: tradeBalances.inTradeChange?.toString(),
+ outTradeChange: tradeBalances.outTradeChange?.toString(),
+ },
+ provisioningType,
+ slippage: slippage?.toString(),
+ errors: Object.keys(errors).reduce((reducedErrors, error) => {
+ return {
+ ...reducedErrors,
+ [error]: (errors as any)[error].type,
+ };
+ }, {}),
+ });
+ }, [
+ Object.values(getValues()).toString(),
+ spotPrice,
+ tradeBalances,
+ tradeBalances,
+ provisioningType,
+ errors,
+ assetInLiquidity,
+ assetOutLiquidity,
+ slippage,
+ formState.isDirty,
+ ]);
+
+ const minTradeLimitIn = useCallback(
+ (assetInAmount?: Maybe) => {
+ if (!assetInAmount || assetInAmount === '0') return false;
+ return new BigNumber(assetInLiquidity || '0')
+ .dividedBy(3)
+ .gte(assetInAmount);
+ },
+ [assetInLiquidity]
+ );
+
+ const [maxAmountInLoading, setMaxAmountInLoading] = useState(false);
+
+ const calculateMaxAmountIn = useCallback(async () => {
+ const [assetIn, assetOut] = getValues(['assetIn', 'assetOut']);
+ console.log(
+ 'calculateMaxAmountIn1',
+ tradeBalances.inBeforeTrade,
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut
+ );
+ if (
+ !tradeBalances.inBeforeTrade ||
+ !cache ||
+ !apiInstance ||
+ !assetIn ||
+ !assetOut
+ )
+ return;
+ console.log('calculateMaxAmountIn11');
+ const maxAmount = tradeBalances.inBeforeTrade;
+ const estimate = await estimateSell(
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut,
+ maxAmount,
+ '0'
+ );
+ console.log('calculateMaxAmountIn11 estimate done', estimate);
+ const paymentInfo = estimate?.partialFee.toString();
+ const maxAmountWithoutFee = new BigNumber(maxAmount).minus(
+ paymentInfo || '0'
+ );
+ console.log('calculateMaxAmountIn12', {
+ inBeforeTrade: tradeBalances.inBeforeTrade,
+ estimate,
+ paymentInfo,
+ maxAmount,
+ maxAmountWithoutFee: maxAmountWithoutFee.toFixed(10),
+ });
+
+ return getValues('assetIn') === '0'
+ ? // max amount changed when all fields are filled out since that allows
+ // us to calculate paymentInfo
+ maxAmountWithoutFee.gt('0')
+ ? maxAmountWithoutFee.toFixed(10)
+ : undefined
+ : maxAmount;
+ }, [
+ tradeBalances.inBeforeTrade,
+ paymentInfo,
+ cache,
+ apiInstance,
+ ...watch(['assetIn']),
+ ]);
+
+ const maxButtonDisabled = useMemo(() => {
+ return (
+ maxAmountInLoading || activeAccountTradeBalancesLoading || isPoolLoading
+ );
+ }, [maxAmountInLoading, activeAccountTradeBalancesLoading, isPoolLoading]);
+
+ const handleMaxButtonOnClick = useCallback(async () => {
+ setMaxAmountInLoading(true);
+ const maxAmountIn = await calculateMaxAmountIn();
+ console.log('setting max amount in', maxAmountIn);
+ if (maxAmountIn)
+ setValue('assetInAmount', maxAmountIn, {
+ shouldDirty: true,
+ shouldValidate: true,
+ });
+ setMaxAmountInLoading(false);
+ }, [calculateMaxAmountIn]);
+
+ return (
+
+
+ {modalPortal}
+
+
+
+
+
+ );
+};
diff --git a/src/components/Pools/PoolsInfo/PoolsInfo.scss b/src/components/Pools/PoolsInfo/PoolsInfo.scss
new file mode 100644
index 00000000..986bf06a
--- /dev/null
+++ b/src/components/Pools/PoolsInfo/PoolsInfo.scss
@@ -0,0 +1,77 @@
+@import './../../../misc/colors.module.scss';
+@import './../../../misc/misc.module.scss';
+
+.pools-info {
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+ gap: 4px;
+
+ font-size: 15px;
+ font-weight: 400;
+ margin-top: 4px;
+ margin-bottom: 4px;
+
+ &__data {
+ display: flex;
+ flex-direction: column;
+
+ justify-content: center;
+
+ max-height: 120px;
+ opacity: 1;
+
+ transition: max-height 0.3s ease, opacity 0.15s ease;
+
+ &.hidden {
+ max-height: 0px;
+ opacity: 0;
+ }
+
+ .data-piece {
+ padding: 2px 0 4px 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ &__label {
+ color: #9ea9b1;
+ }
+ position: relative;
+
+ &:not(:last-child):after {
+ content: ' ';
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ background-color: #26282f;
+ bottom: 0;
+ }
+ }
+ }
+
+ .validation {
+ opacity: 0;
+ line-height: 16px;
+ padding: 0 16px;
+ height: 100%;
+ max-height: 0px;
+ overflow: hidden;
+
+ transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
+ border-radius: 8px;
+
+ &.visible {
+ max-height: 80px;
+ padding: 16px;
+ opacity: 1;
+ }
+
+ &.error {
+ background: rgba(255, 104, 104, 0.3);
+ }
+
+ &.warning {
+ color: $orange1;
+ }
+ }
+}
diff --git a/src/components/Pools/PoolsInfo/PoolsInfo.tsx b/src/components/Pools/PoolsInfo/PoolsInfo.tsx
new file mode 100644
index 00000000..71b6fa49
--- /dev/null
+++ b/src/components/Pools/PoolsInfo/PoolsInfo.tsx
@@ -0,0 +1,128 @@
+import BigNumber from 'bignumber.js';
+import { debounce, delay, throttle } from 'lodash';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { FieldErrors } from 'react-hook-form';
+import { useMultiFeePaymentConversionContext } from '../../../containers/MultiProvider';
+import { Balance, Fee } from '../../../generated/graphql';
+import { FormattedBalance } from '../../Balance/FormattedBalance/FormattedBalance';
+import { horizontalBar } from '../../Chart/ChartHeader/ChartHeader';
+import { PoolsFormFields, ProvisioningType } from '../PoolsForm';
+import constants from '../../../constants';
+import './PoolsInfo.scss';
+
+export interface PoolsInfoProps {
+ transactionFee?: string;
+ tradeLimit?: Balance;
+ isDirty?: boolean;
+ errors?: FieldErrors;
+ paymentInfo?: string;
+ provisioningType: ProvisioningType;
+}
+
+export const PoolsInfo = ({
+ errors,
+ tradeLimit,
+ provisioningType,
+ isDirty,
+ paymentInfo,
+}: PoolsInfoProps) => {
+ const [displayError, setDisplayError] = useState();
+ const isError = useMemo(() => !!errors?.submit?.type, [errors?.submit]);
+ const formError = useMemo(() => {
+ switch (errors?.submit?.type) {
+ case 'slippageHigherThanTolerance':
+ return 'Slippage higher than tolerance';
+ case 'notEnoughBalanceInA':
+ return 'Insufficient Token A balance';
+ case 'notEnoughBalanceInB':
+ return 'Insufficient Token B balance';
+ case 'notEnoughBalanceInShare':
+ return 'Insufficient Share token balance';
+ case 'notEnoughFeeBalance':
+ return 'Insufficient fee balance';
+ case 'poolDoesNotExist':
+ return 'Please select valid pool';
+ case 'activeAccount':
+ return 'Please connect a wallet to continue';
+ }
+ return;
+ }, [errors?.submit]);
+
+ useEffect(() => {
+ if (formError) {
+ const timeoutId = setTimeout(() => setDisplayError(formError), 50);
+ return () => timeoutId && clearTimeout(timeoutId);
+ }
+ const timeoutId = setTimeout(() => setDisplayError(formError), 300);
+ return () => timeoutId && clearTimeout(timeoutId);
+ }, [formError]);
+
+ const { feePaymentAsset } = useMultiFeePaymentConversionContext();
+
+ return (
+
+
+ {/*
+
Current slippage
+
+ {!expectedSlippage || expectedSlippage?.isNaN()
+ ? horizontalBar
+ : `${expectedSlippage?.multipliedBy(100).toFixed(2)}%`}
+
+
*/}
+ {provisioningType === ProvisioningType.Add
+ ? (
+
+
Provisioning limit
+
+ {tradeLimit?.balance ? (
+
+ ) : (
+ <>{horizontalBar}>
+ )}
+
+
+ )
+ : <>>
+ }
+
+
Transaction fee
+
+ {paymentInfo ? (
+
+ ) : (
+ <>{horizontalBar}>
+ )}
+
+
+ {/*
+
Trade fee
+
+ {new BigNumber(tradeFee.numerator)
+ .dividedBy(tradeFee.denominator)
+ .multipliedBy(100)
+ .toFixed(2)}
+ %
+
+
*/}
+
+ {/* TODO Error message */}
+
+
+ {displayError}
+
+
+ );
+};
diff --git a/src/components/Trade/TradeForm/TradeForm.scss b/src/components/Trade/TradeForm/TradeForm.scss
index 591cb865..055a26e7 100644
--- a/src/components/Trade/TradeForm/TradeForm.scss
+++ b/src/components/Trade/TradeForm/TradeForm.scss
@@ -6,52 +6,83 @@
flex-basis: 350px;
flex-grow: 1;
- padding: 14px;
+ padding: 22px;
min-width: 350px;
+ max-width: 610px;
+ margin: 0 auto;
- background-color: $d-gray4;
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);
+ background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%);
overflow: hidden;
+ border-radius: 10px;
position: relative;
+ color: white;
+
.trade-form {
display: flex;
flex-direction: column;
justify-content: space-between;
- gap: 8px;
+ gap: 14px;
height: 100%;
min-height: 400px;
.trade-form-heading {
+ width: fit-content;
padding-top: 4px;
color: $l-gray3;
- font-size: 18px;
+ font-size: 22px;
font-weight: 500;
+ background: linear-gradient(
+ 90deg,
+ #4fffb0 1.27%,
+ #b3ff8f 48.96%,
+ #ff984e 104.14%
+ ),
+ linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%),
+ linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ text-fill-color: transparent;
}
.divider-wrapper {
display: flex;
align-items: center;
- height: 2px;
+ height: 1px;
width: 100%;
}
.divider {
position: absolute;
width: 100%;
- height: 2px;
- background-color: $d-gray5;
+ height: 1px;
+ background-color: rgba(76, 243, 168, 0.12);
opacity: 1;
border: 0;
left: 0;
}
+ .balance-wrapper {
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: end;
+ background: rgba(162, 176, 187, 0.1);
+ padding: 12px;
+ padding-top: 18px;
+ border-radius: 10px;
+ gap: 6px;
+ padding-bottom: 16px;
+ }
+
.submit-button {
background: $green1;
text-transform: uppercase;
- border-radius: $border-radius;
+ border-radius: 36px;
height: 50px;
color: $d-gray4;
@@ -72,37 +103,44 @@
.balance-info {
display: flex;
align-items: center;
+ justify-content: right;
+ width: 100%;
gap: 4px;
height: 16px;
margin-top: 4px;
- font-size: 10px;
- line-height: 10px;
+ font-size: 12px;
+ line-height: 12px;
+ position: relative;
+
+ .balance-info-type {
+ position: absolute;
+ left: 0;
+ font-weight: 600;
+ font-size: 16px;
+ color: $green1;
+ padding: 6px;
+ }
}
.asset-switch {
display: flex;
- height: 55px;
+ height: 43px;
justify-content: space-between;
align-items: center;
width: 100%;
.asset-switch-icon {
position: absolute;
- left: 16px;
+ left: 24px;
display: flex;
align-items: center;
justify-content: center;
- width: 55px;
- height: 55px;
-
- border-radius: 55px;
- border: 4px solid $d-gray3;
- background-color: $d-gray5;
-
overflow: hidden;
+ background: #192022;
+ border-radius: 50%;
transition: transform 500ms ease;
@@ -120,26 +158,37 @@
}
.asset-switch-price {
- display: flex;
- align-items: center;
- gap: 4px;
- height: 18px;
-
- right: 0;
- padding: 0 16px;
- font-size: 12px;
- font-weight: 500;
position: absolute;
+ right: 24px;
+ background: #192022;
- background-color: $d-gray5;
+ &__wrapper {
+ display: flex;
+ align-items: center;
+ gap: 4px;
- border-radius: 4px 0 0 4px;
+ padding: 4px 14px;
+ font-size: 11px;
+ font-weight: 500;
+
+ background: rgba(218, 255, 238, 0.06);
+ border-radius: 7px;
+ }
}
}
.settings-button {
position: absolute;
- right: 16px;
+ display: flex;
+ right: 24px;
+ top: 20px;
+ padding: 10px 8px;
+ border-radius: 50%;
+ background-color: rgba(162, 176, 187, 0.1);
+
+ svg {
+ width: 24px;
+ }
&:hover {
cursor: pointer;
@@ -160,12 +209,25 @@
left: 0;
z-index: 1;
+
+ .disclaimer {
+ padding: 12px 24px;
+ padding-top: 0px;
+ font-size: 14px;
+ color: $gray4;
+ }
.trade-settings {
height: 100%;
}
+ .settings-section {
+ padding: 12px 24px;
+ background: linear-gradient(0deg, #171518, #171518), #1c1a1f;
+ }
+
.settings-field {
+ padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
@@ -201,3 +263,19 @@
background-color: rgba(0, 0, 0, 0.8);
}
+
+.max-button {
+ font-size: 12px;
+ font-weight: 400;
+ color: $white1;
+ padding: 4px 10px;
+ background: rgba(255, 255, 255, 0.06);
+ border-radius: 12px;
+ text-transform: capitalize;
+ cursor: pointer;
+
+ &.disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+}
diff --git a/src/components/Trade/TradeForm/TradeForm.tsx b/src/components/Trade/TradeForm/TradeForm.tsx
index 02098da7..c4e32e6a 100644
--- a/src/components/Trade/TradeForm/TradeForm.tsx
+++ b/src/components/Trade/TradeForm/TradeForm.tsx
@@ -11,7 +11,13 @@ import {
useState,
} from 'react';
import { Control, FormProvider, useForm } from 'react-hook-form';
-import { Account, Balance, Maybe, Pool, TradeType } from '../../../generated/graphql';
+import {
+ Account,
+ Balance,
+ Maybe,
+ Pool,
+ TradeType,
+} from '../../../generated/graphql';
import { fromPrecision12 } from '../../../hooks/math/useFromPrecision';
import { useMath } from '../../../hooks/math/useMath';
import { percentageChange } from '../../../hooks/math/usePercentageChange';
@@ -31,6 +37,8 @@ import { usePolkadotJsContext } from '../../../hooks/polkadotJs/usePolkadotJs';
import { useApolloClient } from '@apollo/client';
import { estimateBuy } from '../../../hooks/pools/xyk/buy';
import { estimateSell } from '../../../hooks/pools/xyk/sell';
+import { payment } from '@polkadot/types/interfaces/definitions';
+import { useMultiFeePaymentConversionContext } from '../../../containers/MultiProvider';
export interface TradeFormSettingsProps {
allowedSlippage: string | null;
@@ -48,13 +56,14 @@ export const TradeFormSettings = ({
onAllowedSlippageChange,
closeModal,
}: TradeFormSettingsProps) => {
- const { register, watch, getValues, setValue, handleSubmit } =
- useForm({
- defaultValues: {
- allowedSlippage,
- autoSlippage: true,
- },
- });
+ const { register, watch, getValues, setValue, handleSubmit } = useForm<
+ TradeFormSettingsFormFields
+ >({
+ defaultValues: {
+ allowedSlippage,
+ autoSlippage: true,
+ },
+ });
// propagate allowed slippage to the parent
useEffect(() => {
@@ -71,32 +80,38 @@ export const TradeFormSettings = ({
return (
);
@@ -149,7 +164,7 @@ export interface TradeFormProps {
inBalance?: Balance;
};
activeAccountTradeBalancesLoading: boolean;
- activeAccount?: Maybe
+ activeAccount?: Maybe;
}
export interface TradeFormFields {
@@ -202,7 +217,7 @@ export const TradeForm = ({
assets,
activeAccountTradeBalances,
activeAccountTradeBalancesLoading,
- activeAccount
+ activeAccount,
}: TradeFormProps) => {
// TODO: include math into loading form state
const { math, loading: mathLoading } = useMath();
@@ -238,6 +253,20 @@ export const TradeForm = ({
trigger('submit');
}, []);
+ useEffect(() => {
+ // must provide input name otherwise it does not validate appropriately
+ trigger('submit');
+ }, [
+ isActiveAccountConnected,
+ pool,
+ isPoolLoading,
+ activeAccountTradeBalances,
+ assetInLiquidity,
+ assetOutLiquidity,
+ allowedSlippage,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ ]);
+
// when the assetIds change, propagate the change to the parent
useEffect(() => {
const { assetIn, assetOut } = getValues();
@@ -330,8 +359,12 @@ export const TradeForm = ({
const tradeLimit = useMemo(() => {
// convert from precision, otherwise the math doesnt work
- const assetInAmount = fromPrecision12(getValues('assetInAmount') || undefined);
- const assetOutAmount = fromPrecision12(getValues('assetOutAmount') || undefined);
+ const assetInAmount = fromPrecision12(
+ getValues('assetInAmount') || undefined
+ );
+ const assetOutAmount = fromPrecision12(
+ getValues('assetOutAmount') || undefined
+ );
const assetIn = getValues('assetIn');
const assetOut = getValues('assetOut');
@@ -436,37 +469,98 @@ export const TradeForm = ({
assetIn: assetIds.assetOut,
assetOut: assetIds.assetIn,
});
+
+ if (tradeType === TradeType.Buy) {
+ const assetOutAmount = getValues('assetOutAmount');
+ setValue('assetInAmount', assetOutAmount);
+ setTradeType(TradeType.Sell);
+ setValue('assetOutAmount', null)
+ } else {
+ const assetInAmount = getValues('assetInAmount');
+ setValue('assetOutAmount', assetInAmount);
+ setTradeType(TradeType.Buy);
+ setValue('assetInAmount', null)
+ }
},
- [assetIds]
+ [assetIds, tradeType, setValue, getValues, setTradeType]
);
- const { apiInstance } = usePolkadotJsContext()
+ const { apiInstance } = usePolkadotJsContext();
const { cache } = useApolloClient();
const [paymentInfo, setPaymentInfo] = useState();
- useEffect(() => {
+ const { convertToFeePaymentAsset, feePaymentAsset } = useMultiFeePaymentConversionContext();
+ const calculatePaymentInfo = useCallback(async () => {
if (!apiInstance) return;
- const [ assetIn, assetOut, assetInAmount, assetOutAmount ] = getValues(['assetIn', 'assetOut', 'assetInAmount', 'assetOutAmount']);
-
- if (!assetIn || !assetOut || !assetInAmount || !assetOutAmount || !tradeLimit) return;
+ let [assetIn, assetOut, assetInAmount, assetOutAmount] = getValues([
+ 'assetIn',
+ 'assetOut',
+ 'assetInAmount',
+ 'assetOutAmount',
+ ]);
- (async () => {
- switch (tradeType) {
- case TradeType.Buy: {
- const estimate = (await estimateBuy(cache, apiInstance, assetOut, assetIn, assetOutAmount, tradeLimit.balance))
- const partialFee = estimate?.partialFee.toString();
- return setPaymentInfo(partialFee);
- }
- case TradeType.Sell: {
- const estimate = (await estimateSell(cache, apiInstance, assetIn, assetOut, assetInAmount, tradeLimit.balance))
- const partialFee = estimate?.partialFee.toString();
- return setPaymentInfo(partialFee);
- }
- default:
- return;
+ if (
+ !assetIn ||
+ !assetOut ||
+ !assetInAmount ||
+ !assetOutAmount ||
+ !tradeLimit
+ )
+ return;
+
+ switch (tradeType) {
+ case TradeType.Buy: {
+ const estimate = await estimateBuy(
+ cache,
+ apiInstance,
+ assetOut,
+ assetIn,
+ assetOutAmount,
+ tradeLimit.balance
+ );
+ const partialFee = estimate?.partialFee.toString();
+ return convertToFeePaymentAsset(partialFee);
}
+ case TradeType.Sell: {
+ const estimate = await estimateSell(
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut,
+ assetInAmount,
+ tradeLimit.balance
+ );
+ const partialFee = estimate?.partialFee.toString();
+ return convertToFeePaymentAsset(partialFee);
+ }
+ default:
+ return;
+ }
+ }, [
+ apiInstance,
+ cache,
+ ...watch(['assetInAmount', 'assetOutAmount', 'assetIn']),
+ tradeLimit,
+ tradeType,
+ convertToFeePaymentAsset,
+ feePaymentAsset,
+ getValues,
+ pool
+ ]);
+
+ useEffect(() => {
+ (async () => {
+ const paymentInfo = await calculatePaymentInfo();
+ if (!paymentInfo) return;
+ setPaymentInfo(paymentInfo);
})();
-
- }, [apiInstance, cache, ...watch(['assetInAmount', 'assetOutAmount']), tradeLimit, tradeType]);
+ }, [
+ apiInstance,
+ cache,
+ ...watch(['assetInAmount', 'assetOutAmount']),
+ tradeLimit,
+ tradeType,
+ calculatePaymentInfo
+ ]);
useEffect(() => {
setValue('assetIn', assetIds.assetIn);
@@ -477,30 +571,44 @@ export const TradeForm = ({
const assetOutAmount = getValues('assetOutAmount');
const outBeforeTrade = activeAccountTradeBalances?.outBalance?.balance;
const outAfterTrade =
- outBeforeTrade &&
- assetOutAmount &&
- new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0) || undefined;
+ (outBeforeTrade &&
+ assetOutAmount &&
+ new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0)) ||
+ undefined;
const outTradeChange =
outBeforeTrade !== '0'
? percentageChange(
fromPrecision12(outBeforeTrade),
fromPrecision12(outAfterTrade)
)?.multipliedBy(100)
- : new BigNumber(outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0');
+ : new BigNumber(
+ outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0'
+ );
const assetInAmount = getValues('assetInAmount');
const inBeforeTrade = activeAccountTradeBalances?.inBalance?.balance;
- const inAfterTrade =
- inBeforeTrade &&
- assetInAmount &&
- new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0) || undefined
+ let inAfterTrade =
+ (inBeforeTrade &&
+ assetInAmount &&
+ new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0)) ||
+ undefined;
+
+ inAfterTrade =
+ getValues('assetIn') !== '0'
+ ? inAfterTrade
+ : paymentInfo &&
+ inAfterTrade &&
+ new BigNumber(inAfterTrade).minus(paymentInfo).toFixed(0);
+
const inTradeChange =
inBeforeTrade !== '0'
? percentageChange(
fromPrecision12(inBeforeTrade),
fromPrecision12(inAfterTrade)
)?.multipliedBy(100)
- : new BigNumber(inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0');
+ : new BigNumber(
+ inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0'
+ );
return {
outBeforeTrade,
@@ -513,13 +621,13 @@ export const TradeForm = ({
};
}, [
activeAccountTradeBalances,
- ...watch(['assetOutAmount', 'assetInAmount']),
+ ...watch(['assetOutAmount', 'assetInAmount', 'assetIn']),
+ paymentInfo,
]);
const { debugComponent } = useDebugBoxContext();
useEffect(() => {
- console.log('all values', getValues());
debugComponent('TradeForm', {
...getValues(),
spotPrice,
@@ -549,24 +657,102 @@ export const TradeForm = ({
errors,
assetInLiquidity,
assetOutLiquidity,
- slippage
+ slippage,
+ formState.isDirty,
]);
- useEffect(() => {
- // must provide input name otherwise it does not validate appropriately
- trigger('submit');
+ const minTradeLimitIn = useCallback(
+ (assetInAmount?: Maybe) => {
+ if (!assetInAmount || assetInAmount === '0') return false;
+ return new BigNumber(assetInLiquidity || '0')
+ .dividedBy(3)
+ .gte(assetInAmount);
+ },
+ [assetInLiquidity]
+ );
+
+ const [maxAmountInLoading, setMaxAmountInLoading] = useState(false);
+
+ const calculateMaxAmountIn = useCallback(async () => {
+ const [assetIn, assetOut] = getValues(['assetIn', 'assetOut']);
+ console.log(
+ 'calculateMaxAmountIn1',
+ tradeBalances.inBeforeTrade,
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut
+ );
+ if (
+ !tradeBalances.inBeforeTrade ||
+ !cache ||
+ !apiInstance ||
+ !assetIn ||
+ !assetOut
+ )
+ return;
+ console.log('calculateMaxAmountIn11');
+ const maxAmount = tradeBalances.inBeforeTrade;
+ const estimate = await estimateSell(
+ cache,
+ apiInstance,
+ assetIn,
+ assetOut,
+ maxAmount,
+ '0'
+ );
+ console.log('calculateMaxAmountIn11 estimate done', estimate);
+ const paymentInfo = estimate?.partialFee.toString();
+ const maxAmountWithoutFee = new BigNumber(maxAmount).minus(
+ (feePaymentAsset === getValues('assetIn')
+ ? feePaymentAsset === '0'
+ ? paymentInfo
+ : convertToFeePaymentAsset(paymentInfo)
+ : '0'
+ ) || '0'
+ );
+ console.log('calculateMaxAmountIn12', {
+ inBeforeTrade: tradeBalances.inBeforeTrade,
+ estimate,
+ paymentInfo,
+ maxAmount,
+ maxAmountWithoutFee: maxAmountWithoutFee.toFixed(10),
+ });
+
+ return getValues('assetIn') === feePaymentAsset
+ ? // max amount changed when all fields are filled out since that allows
+ // us to calculate paymentInfo
+ maxAmountWithoutFee.gt('0')
+ ? maxAmountWithoutFee.toFixed(10)
+ : undefined
+ : maxAmount;
}, [
- isActiveAccountConnected,
- pool,
- isPoolLoading,
- activeAccountTradeBalances,
- assetInLiquidity,
- assetOutLiquidity,
- allowedSlippage,
+ tradeBalances.inBeforeTrade,
paymentInfo,
- ...watch(['assetInAmount', 'assetOutAmount']),
+ cache,
+ apiInstance,
+ feePaymentAsset, convertToFeePaymentAsset,
+ ...watch(['assetIn']),
]);
+ const maxButtonDisabled = useMemo(() => {
+ return (
+ maxAmountInLoading || activeAccountTradeBalancesLoading || isPoolLoading
+ );
+ }, [maxAmountInLoading, activeAccountTradeBalancesLoading, isPoolLoading]);
+
+ const handleMaxButtonOnClick = useCallback(async () => {
+ setMaxAmountInLoading(true);
+ const maxAmountIn = await calculateMaxAmountIn();
+ console.log('setting max amount in', maxAmountIn);
+ if (maxAmountIn)
+ setValue('assetInAmount', maxAmountIn, {
+ shouldDirty: true,
+ shouldValidate: true,
+ });
+ setMaxAmountInLoading(false);
+ }, [calculateMaxAmountIn]);
+
return (
@@ -584,65 +770,49 @@ export const TradeForm = ({
- Pay with
+ Trade Tokens
!Object.values(assetIds).includes(asset.id))}
+ assets={assets?.filter(
+ (asset) => !Object.values(assetIds).includes(asset.id)
+ )}
+ maxBalanceLoading={maxAmountInLoading}
/>
- {activeAccountTradeBalancesLoading ||
- isPoolLoading
- ? (
+
Pay with
+ {activeAccountTradeBalancesLoading || isPoolLoading ? (
'Your balance: loading'
) : (
- // : `${fromPrecision12(tradeBalances.outBeforeTrade)} -> ${fromPrecision12(tradeBalances.outAfterTrade)}`
<>
Your balance:
{assetIds.assetIn ? (
- tradeBalances.inBeforeTrade !== undefined
- ? (
-
- )
- : <> {horizontalBar}>
- ) : (
- <> {horizontalBar}>
- )}
- {tradeBalances.inAfterTrade !== undefined && tradeBalances.inBeforeTrade !== undefined && assetIds.assetIn ? (
- <>
-
+ tradeBalances.inBeforeTrade !== undefined ? (
- >
+ ) : (
+ <> {horizontalBar}>
+ )
) : (
- <>>
+ <> {horizontalBar}>
)}
- {tradeBalances.inTradeChange &&
- !tradeBalances.inTradeChange.isZero() && (
-
- (
- {tradeBalances.inTradeChange?.abs().lt('0.01')
- ? `< -0.01`
- : tradeBalances.inTradeChange?.abs().gt('1000')
- ? `> -1000`
- : tradeBalances.inTradeChange.toFixed(2)}
- %)
-
- )}
>
)}
+
handleMaxButtonOnClick()}
+ >
+ Max
+
@@ -652,70 +822,71 @@ export const TradeForm = ({
- {(() => {
- const assetOut = getValues('assetOut');
- const assetIn = getValues('assetIn');
- switch (tradeType) {
- case TradeType.Sell:
- // return `1 ${
- // idToAsset(getValues('assetIn'))?.symbol ||
- // getValues('assetIn')
- // } = ${fromPrecision12(spotPrice?.inOut)} ${
- // idToAsset(getValues('assetOut'))?.symbol ||
- // getValues('assetOut')
- // }`;
- return spotPrice?.inOut && assetOut ? (
- <>
-
- =
-
- >
- ) : (
- <>->
- );
- case TradeType.Buy:
- // return `1 ${
- // idToAsset(getValues('assetOut'))?.symbol ||
- // getValues('assetOut')
- // } = ${fromPrecision12(spotPrice?.outIn)} ${
- // idToAsset(getValues('assetIn'))?.symbol ||
- // getValues('assetIn')
- // }`;
- return spotPrice?.outIn && assetIn ? (
- <>
-
- =
-
- >
- ) : (
- <>->
- );
- }
- })()}
+
+ {(() => {
+ const assetOut = getValues('assetOut');
+ const assetIn = getValues('assetIn');
+ switch (tradeType) {
+ case TradeType.Sell:
+ // return `1 ${
+ // idToAsset(getValues('assetIn'))?.symbol ||
+ // getValues('assetIn')
+ // } = ${fromPrecision12(spotPrice?.inOut)} ${
+ // idToAsset(getValues('assetOut'))?.symbol ||
+ // getValues('assetOut')
+ // }`;
+ return spotPrice?.inOut && assetOut ? (
+ <>
+
+ =
+
+ >
+ ) : (
+ <>->
+ );
+ case TradeType.Buy:
+ // return `1 ${
+ // idToAsset(getValues('assetOut'))?.symbol ||
+ // getValues('assetOut')
+ // } = ${fromPrecision12(spotPrice?.outIn)} ${
+ // idToAsset(getValues('assetIn'))?.symbol ||
+ // getValues('assetIn')
+ // }`;
+ return spotPrice?.outIn && assetIn ? (
+ <>
+
+ =
+
+ >
+ ) : (
+ <>->
+ );
+ }
+ })()}
+