diff --git a/src/@types/dapp-wallet-web-bridge.d.ts b/src/@types/dapp-wallet-web-bridge.d.ts index a012068fc..ed7b55ffa 100644 --- a/src/@types/dapp-wallet-web-bridge.d.ts +++ b/src/@types/dapp-wallet-web-bridge.d.ts @@ -41,4 +41,16 @@ namespace ErgoBridge { } } +declare let ergoConnector: { + nautilus: any; +}; +declare let NautilusErgoApi: any; + +declare let cardano: Record; + declare let ergo: ErgoBridge.ErgoAPI; + +interface Window { + yoroi: ErgoBridge.ErgoAPI; + nautilus: ErgoBridge.ErgoAPI; +} diff --git a/src/App.tsx b/src/App.tsx index 46052baeb..9625f851f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,6 @@ import { AppLoadingProvider, SettingsProvider, WalletAddressesProvider, - WalletContextProvider, } from './context'; import { globalHistory } from './createBrowserHistory'; import { ContextModalProvider } from './ergodex-cdk'; @@ -32,60 +31,58 @@ const Application = withTranslation()(() => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/src/api/assets.ts b/src/api/assets.ts index b026c8984..845bb2834 100644 --- a/src/api/assets.ts +++ b/src/api/assets.ts @@ -1,18 +1,19 @@ import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import { uniqBy } from 'lodash'; -import { map, Observable, publishReplay, refCount, switchMap } from 'rxjs'; +import { map, Observable, publishReplay, refCount } from 'rxjs'; -import { selectedNetwork$ } from '../network/network'; import { ammPools$ } from './ammPools'; -export const assets$ = selectedNetwork$.pipe( - switchMap((network) => network.assets$), +export const tokenAssets$ = ammPools$.pipe( + map((pools) => pools.flatMap((p) => [p.x.asset, p.y.asset])), + map((assets) => uniqBy(assets, 'id')), publishReplay(1), refCount(), ); -export const lpAssets$ = selectedNetwork$.pipe( - switchMap((network) => network.lpAssets$), +export const lpAssets$ = ammPools$.pipe( + map((pools) => pools.map((p) => p.lp.asset)), + map((assets) => uniqBy(assets, 'id')), publishReplay(1), refCount(), ); diff --git a/src/api/wallets.ts b/src/api/wallets.ts new file mode 100644 index 000000000..bed64d251 --- /dev/null +++ b/src/api/wallets.ts @@ -0,0 +1,50 @@ +import { + filter, + first, + mapTo, + Observable, + publishReplay, + refCount, + switchMap, +} from 'rxjs'; + +import { Wallet, WalletState } from '../network/common'; +import { selectedNetwork$ } from '../network/network'; + +export const wallets$ = selectedNetwork$.pipe( + switchMap((n) => n.wallets$), + publishReplay(1), + refCount(), +); + +export const selectedWallet$ = selectedNetwork$.pipe( + switchMap((n) => n.selectedWallet$), + publishReplay(1), + refCount(), +); + +export const selectedWalletState$ = selectedNetwork$.pipe( + switchMap((n) => n.selectedWalletState$), + publishReplay(1), + refCount(), +); + +export const isWalletSetuped$ = selectedWalletState$.pipe( + filter( + (state) => + state === WalletState.CONNECTED || state === WalletState.CONNECTING, + ), + mapTo(true), + publishReplay(1), + refCount(), +); + +export const connectWallet = (wallet: Wallet): Observable => + selectedNetwork$.pipe( + switchMap((n) => n.connectWallet(wallet)), + first(), + ); + +export const disconnectWallet = (): void => { + selectedNetwork$.pipe(first()).subscribe((n) => n.disconnectWallet()); +}; diff --git a/src/applicationConfig.ts b/src/applicationConfig.ts index 328c61eb8..19f89d9b4 100644 --- a/src/applicationConfig.ts +++ b/src/applicationConfig.ts @@ -23,10 +23,12 @@ interface ApplicationConfig { readonly hiddenAssets: string[]; readonly blacklistedPools: string[]; readonly operationsRestrictions: OperationRestriction[]; + readonly requestRetryCount: number; } export const applicationConfig: ApplicationConfig = { api: 'https://api.ergodex.io/v1/', + requestRetryCount: 3, social: { twitter: 'https://twitter.com/ErgoDex', telegram: 'https://t.me/ergodex_community', diff --git a/src/assets/icons/icon-logout.svg b/src/assets/icons/icon-logout.svg new file mode 100644 index 000000000..35b89525c --- /dev/null +++ b/src/assets/icons/icon-logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/nautilus-logo-icon.svg b/src/assets/icons/nautilus-logo-icon.svg new file mode 100644 index 000000000..9f8b6acd0 --- /dev/null +++ b/src/assets/icons/nautilus-logo-icon.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/styles/styles.less b/src/assets/styles/styles.less index 0ea1e89d6..a956dff6e 100644 --- a/src/assets/styles/styles.less +++ b/src/assets/styles/styles.less @@ -16,6 +16,22 @@ body { transition: none !important; } +*::-webkit-scrollbar { + width: 6px; + height: 56px; +} + +*::-webkit-scrollbar-track { + background-color: transparent; +} + +*::-webkit-scrollbar-thumb { + border: 1px solid var(--ergo-scroll-border); + background-color: var(--ergo-scroll-bg); + border-radius: var(--ergo-border-radius-sm); + cursor: pointer; +} + .ant-click-animating-node { display: none !important; box-shadow: none !important; diff --git a/src/assets/styles/themes/dark.less b/src/assets/styles/themes/dark.less index 30f475163..408007e64 100644 --- a/src/assets/styles/themes/dark.less +++ b/src/assets/styles/themes/dark.less @@ -120,6 +120,10 @@ // -- Components + // Scroll + --ergo-scroll-bg: @dark-color-gray-1; + --ergo-scroll-border: @dark-color-gray-5; + // Glow --ergo-glow-gradient: radial-gradient(50% 50% at 50% 50%, rgba(118, 17, 1, 0.8) 0%, rgba(30, 30, 30, 0.8) 100%); diff --git a/src/common/models/AmmPool.ts b/src/common/models/AmmPool.ts index c56f39b42..42cc793e7 100644 --- a/src/common/models/AmmPool.ts +++ b/src/common/models/AmmPool.ts @@ -6,8 +6,8 @@ import { cache } from 'decorator-cache-getter'; import { evaluate } from 'mathjs'; import { math, renderFractions } from '../../utils/math'; -import { normalizeAmount } from '../utils/amount'; import { Currency } from './Currency'; +import { Ratio } from './Ratio'; export class AmmPool { constructor(private pool: BaseAmmPool) {} @@ -43,12 +43,12 @@ export class AmmPool { } @cache - get xRatio(): Currency { + get xRatio(): Ratio { return this.getRatio(this.x, this.y); } @cache - get yRatio(): Currency { + get yRatio(): Ratio { return this.getRatio(this.y, this.x); } @@ -78,40 +78,46 @@ export class AmmPool { throw new Error('unknown asset'); } - calculateOutputPrice(inputCurrency: Currency): Currency { + calculateOutputPrice(inputCurrency: Currency): Ratio { const outputCurrency = this.calculateOutputAmount(inputCurrency); + if (outputCurrency.amount === 0n) { + return outputCurrency.asset.id === this.x.asset.id + ? this.xRatio + : this.yRatio; + } + if (inputCurrency.amount === 1n) { - return outputCurrency; + return new Ratio(outputCurrency.toAmount(), outputCurrency.asset); } - const fmtInput = inputCurrency.toString({ suffix: false }); - const fmtOutput = outputCurrency.toString({ suffix: false }); + const fmtInput = inputCurrency.toAmount(); + const fmtOutput = outputCurrency.toAmount(); const p = math.evaluate!(`${fmtOutput} / ${fmtInput}`).toString(); - return new Currency( - normalizeAmount(p, outputCurrency.asset), - outputCurrency.asset, - ); + return new Ratio(p, outputCurrency.asset); } - calculateInputPrice(outputCurrency: Currency): Currency { + calculateInputPrice(outputCurrency: Currency): Ratio { const inputCurrency = this.calculateInputAmount(outputCurrency); + if (inputCurrency.amount === 0n) { + return inputCurrency.asset.id === this.x.asset.id + ? this.xRatio + : this.yRatio; + } + if (outputCurrency.amount === 1n) { - return inputCurrency; + return new Ratio(inputCurrency.toAmount(), inputCurrency.asset); } - const fmtInput = inputCurrency.toString({ suffix: false }); - const fmtOutput = outputCurrency.toString({ suffix: false }); + const fmtInput = inputCurrency.toAmount(); + const fmtOutput = outputCurrency.toAmount(); const p = math.evaluate!(`${fmtInput} / ${fmtOutput}`).toString(); - return new Currency( - normalizeAmount(p, inputCurrency.asset), - inputCurrency.asset, - ); + return new Ratio(p, inputCurrency.asset); } calculateDepositAmount(currency: Currency): Currency { @@ -127,6 +133,15 @@ export class AmmPool { new AssetAmount(currency.asset, currency.amount), ); + if (!inputAmount) { + return new Currency( + 0n, + currency.asset.id === this.pool.y.asset.id + ? this.pool.x.asset + : this.pool.y.asset, + ); + } + return new Currency(inputAmount?.amount, inputAmount?.asset); } @@ -138,15 +153,14 @@ export class AmmPool { return new Currency(outputAmount.amount, outputAmount?.asset); } - private getRatio(first: Currency, second: Currency): Currency { + private getRatio(first: Currency, second: Currency): Ratio { const firstAmount = renderFractions(first.amount, first.asset.decimals); const secondAmount = renderFractions(second.amount, second.asset.decimals); - return new Currency( - normalizeAmount( - math.evaluate!(`${firstAmount} / ${secondAmount}`).toString(), - first.asset, - ), - ); + const ratioAmount = math.evaluate!( + `${firstAmount} / ${secondAmount}`, + ).toString(); + + return new Ratio(ratioAmount, first.asset); } } diff --git a/src/common/models/Currency.ts b/src/common/models/Currency.ts index 835aaa95d..f3c093613 100644 --- a/src/common/models/Currency.ts +++ b/src/common/models/Currency.ts @@ -88,7 +88,11 @@ export class Currency { return this.amount <= currency.amount; } - plus(currency: Currency): Currency { + plus(currency: Currency | bigint): Currency { + if (typeof currency === 'bigint') { + return new Currency(this.amount + currency, this.asset); + } + if (isUnknownAsset(this.asset)) { throw new Error("can't sum unknown asset"); } @@ -99,7 +103,11 @@ export class Currency { return new Currency(this.amount + currency.amount, this.asset); } - minus(currency: Currency): Currency { + minus(currency: Currency | bigint): Currency { + if (typeof currency === 'bigint') { + return new Currency(this.amount - currency, this.asset); + } + if (isUnknownAsset(this.asset)) { throw new Error("can't subtract unknown asset"); } @@ -114,7 +122,7 @@ export class Currency { if (this.amount === 0n) { return this; } - const fmtAmount = this.toString({ suffix: false }); + const fmtAmount = this.toAmount(); const newAmount = math.evaluate!( `${fmtAmount} / 100 * ${percent}`, ).toString(); @@ -122,14 +130,14 @@ export class Currency { return new Currency(normalizeAmount(newAmount, this.asset), this.asset); } - toString(config?: { suffix: boolean }): string { - if ((!config || !!config?.suffix) && !isUnknownAsset(this.asset)) { - return `${renderFractions(this.amount, this.asset.decimals)} ${ - this.asset.name - }`; - } + toAmount(): string { + return renderFractions(this.amount, this.asset.decimals); + } - return `${renderFractions(this.amount, this.asset.decimals)}`; + toCurrencyString(): string { + return `${this.toAmount()} ${ + isUnknownAsset(this.asset) ? '' : this.asset.name + }`; } toUsd(): void {} diff --git a/src/common/models/Position.ts b/src/common/models/Position.ts index fa6c124d0..b828cfabd 100644 --- a/src/common/models/Position.ts +++ b/src/common/models/Position.ts @@ -21,8 +21,8 @@ export class Position { @cache get totalLockedPercent(): number { - const lpAmount = this.lockedLp.toString({ suffix: false }); - const poolLiquidityAmount = this.totalLp.toString({ suffix: false }); + const lpAmount = this.lockedLp.toAmount(); + const poolLiquidityAmount = this.totalLp.toAmount(); return math.evaluate!( `${lpAmount} / (${poolLiquidityAmount}) * 100`, ).toFixed(2); diff --git a/src/common/models/Ratio.ts b/src/common/models/Ratio.ts new file mode 100644 index 000000000..d2ab379a9 --- /dev/null +++ b/src/common/models/Ratio.ts @@ -0,0 +1,32 @@ +import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; + +import { parseUserInputToFractions, renderFractions } from '../../utils/math'; +export class Ratio { + readonly asset: AssetInfo; + + readonly amount: bigint; + + private readonly decimals: number; + + constructor(amount: string, asset: AssetInfo) { + this.asset = asset; + this.decimals = this.getRelevantDecimalsCount(amount); + this.amount = parseUserInputToFractions( + Number(amount).toFixed(this.decimals), + this.decimals, + ); + } + + toString(): string { + return `${renderFractions(this.amount, this.decimals)}`; + } + + private getRelevantDecimalsCount(amount: string): number { + const decimalsPart = amount.split('.')[1] || ''; + + return Math.max( + decimalsPart.split('').findIndex((symbol) => Number(symbol) > 0) + 1, + this.asset.decimals || 0, + ); + } +} diff --git a/src/components/ConfirmationModal/ConfirmationModal.tsx b/src/components/ConfirmationModal/ConfirmationModal.tsx index 0e9767a8a..abfcc4df4 100644 --- a/src/components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -37,29 +37,29 @@ const getDescriptionByData = ( switch (operation) { case Operation.ADD_LIQUIDITY: return xAsset && yAsset - ? `Adding liquidity ${xAsset.toString()} and ${yAsset.toString()}` + ? `Adding liquidity ${xAsset.toCurrencyString()} and ${yAsset.toCurrencyString()}` : ''; case Operation.REFUND: return xAsset && yAsset - ? `Refunding ${xAsset.toString()} and ${yAsset.toString()}` + ? `Refunding ${xAsset.toCurrencyString()} and ${yAsset.toCurrencyString()}` : ''; case Operation.REMOVE_LIQUIDITY: return xAsset && yAsset - ? `Removing liquidity ${xAsset.toString()} and ${yAsset.toString()}` + ? `Removing liquidity ${xAsset.toCurrencyString()} and ${yAsset.toCurrencyString()}` : ''; case Operation.SWAP: return xAsset && yAsset - ? `Swapping ${xAsset.toString()} for ${yAsset.toString()}` + ? `Swapping ${xAsset.toCurrencyString()} for ${yAsset.toCurrencyString()}` : ''; case Operation.LOCK_LIQUIDITY: return xAsset && yAsset - ? `Locking ${xAsset.toString()} and ${yAsset.toString()} (${ - lpAsset && lpAsset.toString({ suffix: false }) + ' LP-tokens' + ? `Locking ${xAsset.toCurrencyString()} and ${yAsset.toCurrencyString()} (${ + lpAsset && lpAsset.toAmount() + ' LP-tokens' }) for ${time && getLockingPeriodString(time)}` : ''; case Operation.RELOCK_LIQUIDITY: - return `Relocking ${assetLock?.x.toString()} and ${assetLock?.y.toString()} (${ - assetLock && assetLock.lp.toString({ suffix: false }) + ' LP-tokens' + return `Relocking ${assetLock?.x.toCurrencyString()} and ${assetLock?.y.toCurrencyString()} (${ + assetLock && assetLock.lp.toAmount() + ' LP-tokens' })`; } }; diff --git a/src/components/Header/ConnectWallet/ConnectWallet.tsx b/src/components/Header/ConnectWallet/ConnectWallet.tsx index e1215a90a..e8ed1294d 100644 --- a/src/components/Header/ConnectWallet/ConnectWallet.tsx +++ b/src/components/Header/ConnectWallet/ConnectWallet.tsx @@ -33,7 +33,7 @@ export const ConnectWallet: React.FC = ({ marginLeft={1} > - {balance?.toString() ?? } + {balance?.toCurrencyString() ?? } diff --git a/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.less b/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.less deleted file mode 100644 index a67efea80..000000000 --- a/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.less +++ /dev/null @@ -1,5 +0,0 @@ -.global-settings__miner-fee-wrapper { - .ant-form-item-control-input { - min-height: initial; - } -} diff --git a/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.tsx b/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.tsx index 15932142e..278bf2bab 100644 --- a/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.tsx +++ b/src/components/Header/GlobalSettingsModal/GlobalSettingsModal.tsx @@ -1,169 +1,123 @@ -import './GlobalSettingsModal.less'; - -import React, { useState } from 'react'; +import React from 'react'; import { defaultMinerFee } from '../../../common/constants/settings'; import { useSettings } from '../../../context'; import { Button, + CheckFn, Flex, Form, - Input, + FormGroup, + Messages, Modal, Typography, + useForm, } from '../../../ergodex-cdk'; import { InfoTooltip } from '../../InfoTooltip/InfoTooltip'; +import { MinerFeeInput } from './MinerFeeInput/MinerFeeInput'; interface GlobalSettingsModalProps { onClose: () => void; } interface GlobalSettingsFormModel { - readonly minerFee?: string; + readonly minerFee?: number; readonly explorerUrl?: string; } const MAX_ERG_FOR_TX = 2; const MAX_RECOMMENDED_ERG_FOR_TX = 0.3; +const MIN_ERG_FOR_TX = defaultMinerFee; -const GlobalSettingsModal: React.FC = ({ - onClose, -}): JSX.Element => { - const [form] = Form.useForm(); +const minMinerFeeCheck: CheckFn = (minerFee) => + minerFee < MIN_ERG_FOR_TX ? 'minMinerFee' : undefined; - const [settings, setSettings] = useSettings(); +const maxMinerFeeCheck: CheckFn = (minerFee) => + minerFee > MAX_ERG_FOR_TX ? 'maxMinerFee' : undefined; - const [minerFeeError, setMinerFeeError] = useState< - { type: 'error' | 'warning'; message: string } | undefined - >(); +const recommendedMinerFeeCheck: CheckFn = (minerFee) => + minerFee >= MAX_RECOMMENDED_ERG_FOR_TX && minerFee <= MAX_ERG_FOR_TX + ? 'recommendedMinerFee' + : undefined; - const initialValues = { - minerFee: settings.minerFee, - explorerUrl: settings.explorerUrl, - }; +const errorMessages: Messages = { + minerFee: { + minMinerFee: `Minimum value is ${MIN_ERG_FOR_TX} ERG`, + maxMinerFee: `The value can't be more than ${MAX_ERG_FOR_TX} ERG`, + }, +}; - const handleClickMinimal = () => { - form.setFieldsValue({ minerFee: String(defaultMinerFee) || '' }); - setMinerFeeError(undefined); - }; +const warningMessages: Messages = { + minerFee: { + recommendedMinerFee: (value: number | undefined) => + `You will spend ${value} ERG for every operation.`, + }, +}; - const submitGlobalSettings = () => { - const { minerFee } = form.getFieldsValue(); +const GlobalSettingsModal: React.FC = ({ + onClose, +}): JSX.Element => { + const [settings, setSettings] = useSettings(); - if (minerFee) { - setSettings({ ...settings, minerFee: Number(minerFee) }); - onClose(); - } - }; + const form = useForm({ + minerFee: useForm.ctrl( + settings.minerFee, + [minMinerFeeCheck, maxMinerFeeCheck], + [recommendedMinerFeeCheck], + ), + }); - const onValuesChange = (changes: GlobalSettingsFormModel) => { - if (!changes.minerFee) { - setMinerFeeError({ type: 'error', message: 'Required' }); + const submitGlobalSettings = (form: FormGroup) => { + if (form.invalid) { return; } - if (changes.minerFee) { - const val = Number(changes.minerFee); - - if (isNaN(val)) { - setMinerFeeError({ - type: 'error', - message: `Type a valid number`, - }); - return; - } - - if (val >= MAX_RECOMMENDED_ERG_FOR_TX && val <= MAX_ERG_FOR_TX) { - setMinerFeeError({ - type: 'warning', - message: `You will spend ${val} ERG for every operation. We don't recommend use such big amounts`, - }); - return; - } - - if (val > MAX_ERG_FOR_TX) { - setMinerFeeError({ - type: 'error', - message: `The value can't be more than ${MAX_ERG_FOR_TX} ERG`, - }); - return; - } - - if (val < defaultMinerFee) { - setMinerFeeError({ - type: 'error', - message: `Minimum value is ${defaultMinerFee} ERG`, - }); - } else { - setMinerFeeError(undefined); - } - } + setSettings({ ...settings, minerFee: form.value.minerFee! }); + onClose(); }; return ( <> Global Settings - - -
+ + + Miner Fee - - - - - + + + {({ value, onChange, state, message }) => ( + + )} + + + + {({ invalid }) => ( + - - + Confirm + + )} + +
+
); diff --git a/src/components/Header/GlobalSettingsModal/MinerFeeInput/MinerFeeInput.tsx b/src/components/Header/GlobalSettingsModal/MinerFeeInput/MinerFeeInput.tsx new file mode 100644 index 000000000..9eec94fc0 --- /dev/null +++ b/src/components/Header/GlobalSettingsModal/MinerFeeInput/MinerFeeInput.tsx @@ -0,0 +1,55 @@ +import React, { ChangeEvent, FC } from 'react'; + +import { defaultMinerFee } from '../../../../common/constants/settings'; +import { + Alert, + Animation, + Button, + Control, + Flex, + Input, +} from '../../../../ergodex-cdk'; + +export type MinerFeeInputProps = Control; + +export const MinerFeeInput: FC = ({ + value, + onChange, + state, + message, +}) => { + const handleMinimalBtnClick = () => onChange && onChange(defaultMinerFee); + + const handleInputChange = (event: ChangeEvent) => + onChange && onChange(event.target.valueAsNumber); + + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 41b1b58b5..e18dc314b 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -4,10 +4,12 @@ import cn from 'classnames'; import React, { useEffect, useState } from 'react'; import { isBrowser } from 'react-device-detect'; -import { networkAssetBalance$ } from '../../api/networkAssetBalance'; +import { useAssetsBalance } from '../../api/assetBalance'; +import { selectedWalletState$ } from '../../api/wallets'; import { useObservable } from '../../common/hooks/useObservable'; import { useSettings } from '../../context'; -import { WalletState, walletState$ } from '../../services/new/core'; +import { WalletState } from '../../network/common'; +import { useNetworkAsset } from '../../network/ergo/networkAsset/networkAsset'; import { AppLogo } from '../common/AppLogo/AppLogo'; import { TxHistory } from '../common/TxHistory/TxHistory'; import { AnalyticsDataTag } from './AnalyticsDataTag/AnalyticsDataTag'; @@ -24,8 +26,9 @@ const networks = [ export const Header: React.FC = () => { const [{ address }] = useSettings(); // TODO: Update with rx [EDEX-487] - const [balance] = useObservable(networkAssetBalance$); - const [walletState] = useObservable(walletState$); + const [balance, isBalanceLoading] = useAssetsBalance(); + const [networkAsset] = useNetworkAsset(); + const [walletState] = useObservable(selectedWalletState$); const [hidden, setHidden] = useState(false); useEffect(() => { @@ -64,7 +67,9 @@ export const Header: React.FC = () => { {walletState === WalletState.CONNECTED && } diff --git a/src/components/OperationForm/OperationForm.tsx b/src/components/OperationForm/OperationForm.tsx index 3516fd0cf..2db5c413d 100644 --- a/src/components/OperationForm/OperationForm.tsx +++ b/src/components/OperationForm/OperationForm.tsx @@ -3,11 +3,10 @@ import './OperationForm.less'; import React, { ReactNode, useEffect, useState } from 'react'; import { debounceTime, first, Observable } from 'rxjs'; +import { useAssetsBalance } from '../../api/assetBalance'; import { useObservable } from '../../common/hooks/useObservable'; import { isOnline$ } from '../../common/streams/networkConnection'; -import { Button, Flex } from '../../ergodex-cdk'; -import { Form, FormGroup } from '../../ergodex-cdk/components/Form/NewForm'; -import { isWalletLoading$ } from '../../services/new/core'; +import { Button, Flex, Form, FormGroup } from '../../ergodex-cdk'; import { ConnectWalletButton } from '../common/ConnectWalletButton/ConnectWalletButton'; export type OperationValidator = ( @@ -35,7 +34,7 @@ export function OperationForm({ actionCaption, }: OperationFormProps): JSX.Element { const [isOnline] = useObservable(isOnline$); - const [isWalletLoading] = useObservable(isWalletLoading$); + const [, isBalanceLoading] = useAssetsBalance(); const [value] = useObservable( form.valueChangesWithSilent$.pipe(debounceTime(100)), [form], @@ -58,7 +57,7 @@ export function OperationForm({ loading: false, caption: CHECK_INTERNET_CONNECTION_CAPTION, }); - } else if (isWalletLoading) { + } else if (isBalanceLoading) { setButtonProps({ disabled: false, loading: true, @@ -73,7 +72,7 @@ export function OperationForm({ caption: caption || actionCaption, }); } - }, [isOnline, isWalletLoading, value, validators, actionCaption]); + }, [isOnline, isBalanceLoading, value, validators, actionCaption]); const handleSubmit = () => { if (loading || disabled) { diff --git a/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx b/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx index 693ef3e2e..6b3bae8ff 100644 --- a/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx +++ b/src/components/WalletModal/TokensTab/TokenListItem/TokenListItem.tsx @@ -22,7 +22,7 @@ export const TokenListItem: React.FC = ({ currency }) => (
- {currency.toString({ suffix: false })} + {currency.toAmount()} ); diff --git a/src/components/WalletModal/WalletModal.tsx b/src/components/WalletModal/WalletModal.tsx index a5c6af4c3..2c8b3745e 100644 --- a/src/components/WalletModal/WalletModal.tsx +++ b/src/components/WalletModal/WalletModal.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { networkAssetBalance$ } from '../../api/networkAssetBalance'; import { useObservable } from '../../common/hooks/useObservable'; -import { Box, Flex, Modal, Typography } from '../../ergodex-cdk'; +import { Box, Button, Flex, Modal, Typography } from '../../ergodex-cdk'; import { Tabs } from '../../ergodex-cdk/components/Tabs/Tabs'; import { isLowBalance } from '../../utils/walletMath'; +import { ChooseWalletModal } from '../common/ConnectWalletButton/ChooseWalletModal/ChooseWalletModal'; import { AddressesTab } from './AddressesTab/AddressesTab'; import { LowBalanceWarning } from './LowBalanceWarning/LowBalanceWarning'; import { TokensTab } from './TokensTab/TokensTab'; @@ -14,6 +15,10 @@ import { WalletTotalBalance } from './WalletTotalBalance/WalletTotalBalance'; export const WalletModal: React.FC = () => { const [ergBalance] = useObservable(networkAssetBalance$); + const openChooseWalletModal = (): void => { + Modal.open(({ close }) => ); + }; + return ( <> @@ -33,7 +38,7 @@ export const WalletModal: React.FC = () => { - + @@ -49,6 +54,9 @@ export const WalletModal: React.FC = () => { + diff --git a/src/components/WalletModal/WalletTotalBalance/WalletTotalBalance.tsx b/src/components/WalletModal/WalletTotalBalance/WalletTotalBalance.tsx index a9e15be11..baace642e 100644 --- a/src/components/WalletModal/WalletTotalBalance/WalletTotalBalance.tsx +++ b/src/components/WalletModal/WalletTotalBalance/WalletTotalBalance.tsx @@ -23,7 +23,7 @@ export const WalletTotalBalance: React.FC = ({ - {balance?.toString() ?? } + {balance?.toCurrencyString() ?? } diff --git a/src/components/common/ActionForm/ActionButton/ActionButton.tsx b/src/components/common/ActionForm/ActionButton/ActionButton.tsx index 033b76705..21334b44d 100644 --- a/src/components/common/ActionForm/ActionButton/ActionButton.tsx +++ b/src/components/common/ActionForm/ActionButton/ActionButton.tsx @@ -5,6 +5,7 @@ import React, { FC, ReactNode } from 'react'; import { interval, map } from 'rxjs'; import { useObservable } from '../../../../common/hooks/useObservable'; +import { Currency } from '../../../../common/models/Currency'; import { Button, ButtonProps } from '../../../../ergodex-cdk'; import { ConnectWalletButton } from '../../ConnectWalletButton/ConnectWalletButton'; @@ -16,7 +17,8 @@ export enum ActionButtonState { INSUFFICIENT_LIQUIDITY, LOADING, CHECK_INTERNET_CONNECTION, - ANETA_SWAP_LOCK, + SWAP_LOCK, + MIN_VALUE, ACTION, } @@ -49,6 +51,12 @@ const insufficientFeeBalanceState = (token = ''): ButtonProps => ({ disabled: true, }); +const minValueState = (c?: Currency): ButtonProps => ({ + children: `Min value for ${c?.asset.name} is ${c?.toAmount()}`, + type: 'primary', + disabled: true, +}); + const insufficientLiquidityState = (): ButtonProps => ({ children: `Insufficient liquidity for this trade`, type: 'primary', @@ -76,6 +84,7 @@ const getButtonPropsByState = ( state: ActionButtonState, token?: string, nativeToken?: string, + currency?: Currency, caption?: ReactNode, ): ButtonProps => { switch (state) { @@ -91,6 +100,8 @@ const getButtonPropsByState = ( return insufficientTokenBalanceState(token); case ActionButtonState.SELECT_TOKEN: return selectTokenState(); + case ActionButtonState.MIN_VALUE: + return minValueState(currency); case ActionButtonState.LOADING: return loadingState(); case ActionButtonState.ACTION: @@ -104,6 +115,7 @@ export interface ActionButtonProps { readonly state: ActionButtonState; readonly token?: string | undefined; readonly nativeToken?: string | undefined; + readonly currency?: Currency; readonly onClick?: () => void; readonly children: ReactNode; } @@ -123,7 +135,7 @@ const timer$ = interval(1000).pipe(map(() => getDiff())); export const ActionButton: FC = (props) => { const [timer] = useObservable(timer$, [], getDiff()); - if (props.state === ActionButtonState.ANETA_SWAP_LOCK) { + if (props.state === ActionButtonState.SWAP_LOCK) { return ( - - {warningMessage && ( - - - - )} - + + + ) : ( + ); }; -interface ChooseWalletModalProps { - close: (result: boolean) => void; -} +type ChooseWalletModalProps = DialogRef; const ChooseWalletModal: React.FC = ({ close, }): JSX.Element => { - const walletCtx = useWallet(); + const [wallets] = useObservable(wallets$, [], []); + const [selectedWallet] = useObservable(selectedWallet$); - const wallets = [ - { - name: 'Yoroi Wallet', - logo: , - onClick: () => { - return connectYoroiWallet(walletCtx)().then((res) => { - connectWallet(); - notification.info({ - message: 'Yoroi Wallet tip', - description: - 'Keep Yoroi Wallet extension window open, when you use ErgoDEX. So that it will sync faster.', - duration: null, - }); - return res; - }); - }, - }, - ]; + const handleWalletClick = (wallet: Wallet) => { + connectWallet(wallet).subscribe( + () => close(true), + () => window.open(wallet.extensionLink), + ); + }; return ( <> Select a wallet - {wallets.map((wallet, index) => ( - - ))} + + {wallets.map((wallet, index) => ( + + + + ))} + {selectedWallet && ( + + )} + ); diff --git a/src/components/common/ConnectWalletButton/ConnectWalletButton.tsx b/src/components/common/ConnectWalletButton/ConnectWalletButton.tsx index d20722920..a8dff17cc 100644 --- a/src/components/common/ConnectWalletButton/ConnectWalletButton.tsx +++ b/src/components/common/ConnectWalletButton/ConnectWalletButton.tsx @@ -3,10 +3,10 @@ import './ConnectWalletButton.less'; import cn from 'classnames'; import React, { FC, ReactNode } from 'react'; +import { isWalletSetuped$ } from '../../../api/wallets'; import { useObservable } from '../../../common/hooks/useObservable'; import { useAppLoadingState } from '../../../context'; import { Button, ButtonProps, Modal } from '../../../ergodex-cdk'; -import { isWalletSetuped$ } from '../../../services/new/core'; import { ChooseWalletModal } from './ChooseWalletModal/ChooseWalletModal'; export interface ConnectWalletButtonProps { @@ -20,12 +20,7 @@ export const ConnectWalletButton: FC = ({ className, children, }) => { - // const { isWalletConnected } = useWallet(); - - // TODO: Update with rx [EDEX-487] - // const [isWalletLoading] = useObservable(isWalletLoading$); const [isWalletConnected] = useObservable(isWalletSetuped$); - const [{ isKYAAccepted }] = useAppLoadingState(); const openChooseWalletModal = (): void => { diff --git a/src/components/common/DateTimeView/DateTimeView.tsx b/src/components/common/DateTimeView/DateTimeView.tsx new file mode 100644 index 000000000..47a6b932c --- /dev/null +++ b/src/components/common/DateTimeView/DateTimeView.tsx @@ -0,0 +1,20 @@ +import { DateTime } from 'luxon'; +import React from 'react'; + +import { Typography } from '../../../ergodex-cdk'; + +interface DateTimeViewProps { + type?: 'date' | 'time' | 'datetime'; + value: DateTime; +} + +const DateTimeView: React.FC = ({ type, value }) => { + const getDate = () => { + if (type === 'time') return value.toLocaleString(DateTime.TIME_SIMPLE); + if (type === 'datetime') return value.toLocaleString(DateTime.DATETIME_MED); + return value.toLocaleString(DateTime.DATE_FULL); + }; + return {getDate()}; +}; + +export { DateTimeView }; diff --git a/src/components/common/FormView/FormFeesSection/FormFeesSection.tsx b/src/components/common/FormView/FormFeesSection/FormFeesSection.tsx index 24b28b96b..096c7cafc 100644 --- a/src/components/common/FormView/FormFeesSection/FormFeesSection.tsx +++ b/src/components/common/FormView/FormFeesSection/FormFeesSection.tsx @@ -37,7 +37,7 @@ const FormFeesSection: React.FC = ({ Execution Fee: - {minExFee.toString()} + {minExFee.toCurrencyString()} )} @@ -56,7 +56,9 @@ const FormFeesSection: React.FC = ({ - {totalFees.toString()} + + {totalFees.toCurrencyString()} + diff --git a/src/components/common/FormView/FormPairSection/FormPairSection.tsx b/src/components/common/FormView/FormPairSection/FormPairSection.tsx index 98ac3201f..86141709c 100644 --- a/src/components/common/FormView/FormPairSection/FormPairSection.tsx +++ b/src/components/common/FormView/FormPairSection/FormPairSection.tsx @@ -38,7 +38,7 @@ const FormPairSection: React.FC = ({ - {fees ? undefined : xAmount.toString({ suffix: false })} + {fees ? undefined : xAmount.toAmount()} @@ -59,7 +59,7 @@ const FormPairSection: React.FC = ({ - {fees ? undefined : yAmount.toString({ suffix: false })} + {fees ? undefined : yAmount.toAmount()} diff --git a/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx b/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx index 7112db5f8..9bb7c341f 100644 --- a/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx +++ b/src/components/common/TokenControl/TokenAmountInput/TokenAmountInput.tsx @@ -4,8 +4,7 @@ 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 { EventConfig } from '../../../../ergodex-cdk/components/Form/NewForm'; +import { Box, EventConfig, Input } from '../../../../ergodex-cdk'; import { escapeRegExp } from './format'; const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`); // match escaped "." characters via in a non-capturing group @@ -46,8 +45,8 @@ const TokenAmountInput: React.FC = ({ const [userInput, setUserInput] = useState(undefined); useEffect(() => { - if (Number(value?.toString({ suffix: false })) !== Number(userInput)) { - setUserInput(value?.toString({ suffix: false })); + if (Number(value?.toAmount()) !== Number(userInput)) { + setUserInput(value?.toAmount()); } }, [value]); @@ -55,7 +54,7 @@ const TokenAmountInput: React.FC = ({ if (value && asset) { const newValue = value?.changeAsset(asset); - setUserInput(newValue.toString({ suffix: false })); + setUserInput(newValue?.toAmount()); if (onChange && value.asset.id !== asset.id) { onChange(newValue, { emitEvent: 'silent' }); diff --git a/src/components/common/TokenControl/TokenControl.tsx b/src/components/common/TokenControl/TokenControl.tsx index 1ec73be8c..52bf82a12 100644 --- a/src/components/common/TokenControl/TokenControl.tsx +++ b/src/components/common/TokenControl/TokenControl.tsx @@ -9,12 +9,15 @@ import { Observable, of } from 'rxjs'; import { useAssetsBalance } from '../../../api/assetBalance'; import { useObservable } from '../../../common/hooks/useObservable'; import { Currency } from '../../../common/models/Currency'; -import { Animation, Box, Button, Flex, Typography } from '../../../ergodex-cdk'; import { + Animation, + Box, + Button, + Flex, Form, + Typography, useFormContext, -} from '../../../ergodex-cdk/components/Form/NewForm'; -import { isWalletLoading$ } from '../../../services/new/core'; +} from '../../../ergodex-cdk'; import { TokenAmountInput, TokenAmountInputValue, @@ -56,6 +59,7 @@ export interface NewTokenControlProps { readonly tokenName?: string; readonly label?: ReactNode; readonly maxButton?: boolean; + readonly handleMaxButtonClick?: (balance: Currency) => Currency; readonly hasBorder?: boolean; readonly assets?: AssetInfo[]; readonly assets$?: Observable; @@ -77,6 +81,7 @@ export const TokenControlFormItem: FC = ({ readonly, noBottomInfo, bordered, + handleMaxButtonClick, }) => { const { t } = useTranslation(); const { form } = useFormContext(); @@ -86,11 +91,11 @@ export const TokenControlFormItem: FC = ({ ? form.controls[tokenName].valueChangesWithSilent$ : of(undefined), ); - const [isWalletLoading] = useObservable(isWalletLoading$); - - const handleMaxButtonClick = (maxBalance: Currency) => { + const _handleMaxButtonClick = (maxBalance: Currency) => { if (amountName) { - form.controls[amountName].patchValue(maxBalance); + form.controls[amountName].patchValue( + handleMaxButtonClick ? handleMaxButtonClick(maxBalance) : maxBalance, + ); } }; @@ -165,18 +170,14 @@ export const TokenControlFormItem: FC = ({ className="token-control-bottom-panel" > {() => ( <> {t`common.tokenControl.balanceLabel`}{' '} - {balance.get(selectedAsset).toString()} + {balance.get(selectedAsset).toCurrencyString()} {!!balance.get(selectedAsset) && maxButton && ( @@ -185,7 +186,7 @@ export const TokenControlFormItem: FC = ({ type="primary" size="small" onClick={() => - handleMaxButtonClick(balance.get(selectedAsset)) + _handleMaxButtonClick(balance.get(selectedAsset)) } > {t`common.tokenControl.maxButton`} diff --git a/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx b/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx index b39e0283a..579f38ae9 100644 --- a/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx +++ b/src/components/common/TokenControl/TokenSelect/TokenSelect.tsx @@ -7,10 +7,10 @@ import { Observable } from 'rxjs'; import { Button, DownOutlined, + Form, Modal, Typography, } from '../../../../ergodex-cdk'; -import { Form } from '../../../../ergodex-cdk/components/Form/NewForm'; import { TokenIcon } from '../../../TokenIcon/TokenIcon'; import { TokenListModal } from './TokenListModal/TokenListModal'; diff --git a/src/components/common/TxHistory/InputOutputColumn/InputOutputColumn.tsx b/src/components/common/TxHistory/InputOutputColumn/InputOutputColumn.tsx index db049c8eb..0ffa08ae6 100644 --- a/src/components/common/TxHistory/InputOutputColumn/InputOutputColumn.tsx +++ b/src/components/common/TxHistory/InputOutputColumn/InputOutputColumn.tsx @@ -47,12 +47,12 @@ const InputOutputColumn: React.FC = ({ - {x.amount ? x.toString({ suffix: false }) : ''} + {x.amount ? x.toAmount() : ''} - {y.amount ? y.toString({ suffix: false }) : ''} + {y.amount ? y.toAmount() : ''} diff --git a/src/components/common/TxHistory/RefundConfirmationModal/RefundConfirmationModal.tsx b/src/components/common/TxHistory/RefundConfirmationModal/RefundConfirmationModal.tsx index 7851d4a52..001e6454f 100644 --- a/src/components/common/TxHistory/RefundConfirmationModal/RefundConfirmationModal.tsx +++ b/src/components/common/TxHistory/RefundConfirmationModal/RefundConfirmationModal.tsx @@ -8,12 +8,13 @@ import { DownOutlined, Dropdown, Flex, + Form, Menu, Modal, Typography, + useForm, } from '../../../../ergodex-cdk'; -import { Form, useForm } from '../../../../ergodex-cdk/components/Form/NewForm'; -import { utxos$ } from '../../../../services/new/core'; +import { utxos$ } from '../../../../network/ergo/common/utxos'; import { submitTx } from '../../../../services/yoroi'; import { refund } from '../../../../utils/ammOperations'; import { getShortAddress } from '../../../../utils/string/addres'; diff --git a/src/components/common/TxHistory/TxHistoryModal/TxHistoryModal.tsx b/src/components/common/TxHistory/TxHistoryModal/TxHistoryModal.tsx index addcde069..21d1b1d79 100644 --- a/src/components/common/TxHistory/TxHistoryModal/TxHistoryModal.tsx +++ b/src/components/common/TxHistory/TxHistoryModal/TxHistoryModal.tsx @@ -11,6 +11,7 @@ import { openConfirmationModal, Operation, } from '../../../ConfirmationModal/ConfirmationModal'; +import { DateTimeView } from '../../DateTimeView/DateTimeView'; import { OptionsButton } from '../../OptionsButton/OptionsButton'; import { InputOutputColumn } from '../InputOutputColumn/InputOutputColumn'; import { RefundConfirmationModal } from '../RefundConfirmationModal/RefundConfirmationModal'; @@ -75,7 +76,7 @@ const TxHistoryModal = (): JSX.Element => { Date - + Type @@ -85,42 +86,47 @@ const TxHistoryModal = (): JSX.Element => { {txs ? ( - normalizeOperations(txs).map((op, index) => { - return ( - - - - - - - - {op.timestamp} - - - - - - - - - - {renderTxActionsMenu(op)} - - - - - - ); - }) + normalizeOperations(txs) + .sort((a, b) => b.timestamp.toMillis() - a.timestamp.toMillis()) + .map((op, index) => { + return ( + + + + + + + + +
+ +
+ + + + + + + + + {renderTxActionsMenu(op)} + + +
+
+
+ ); + }) ) : ( {/*TODO:REPLACE_WITH_ORIGINAL_LOADING[EDEX-476]*/} diff --git a/src/components/common/TxHistory/types.ts b/src/components/common/TxHistory/types.ts index 0676110b6..c15ea3750 100644 --- a/src/components/common/TxHistory/types.ts +++ b/src/components/common/TxHistory/types.ts @@ -3,6 +3,7 @@ import { TxStatus, } from '@ergolabs/ergo-dex-sdk/build/main/amm/models/operations'; import { TxId } from '@ergolabs/ergo-sdk'; +import { DateTime } from 'luxon'; import { Currency } from '../../../common/models/Currency'; @@ -15,5 +16,5 @@ export type Operation = { type: OperationType; status: OperationStatus; txId: TxId; - timestamp: string; + timestamp: DateTime; }; diff --git a/src/components/common/TxHistory/utils.ts b/src/components/common/TxHistory/utils.ts index c6d8222c1..0928ce604 100644 --- a/src/components/common/TxHistory/utils.ts +++ b/src/components/common/TxHistory/utils.ts @@ -1,8 +1,8 @@ import { AmmDexOperation } from '@ergolabs/ergo-dex-sdk'; import { uniqBy } from 'lodash'; +import { DateTime } from 'luxon'; import { Currency } from '../../../common/models/Currency'; -import { getFormattedDate } from '../../../utils/date'; import { getAssetNameByMappedId } from '../../../utils/map'; import { getVerifiedPoolByName } from '../../../utils/verification'; import { Operation } from './types'; @@ -10,6 +10,8 @@ import { Operation } from './types'; export const normalizeOperations = (ops: AmmDexOperation[]): Operation[] => { return uniqBy( ops.reduce((acc, op) => { + const timestamp = DateTime.fromMillis(Number(op.timestamp)); + if (op.type === 'order') { if (op.order.type === 'swap') { return [ @@ -29,7 +31,7 @@ export const normalizeOperations = (ops: AmmDexOperation[]): Operation[] => { type: op.order.type, status: op.status, txId: op.txId, - timestamp: getFormattedDate(op.timestamp), + timestamp, }, ]; } @@ -42,7 +44,7 @@ export const normalizeOperations = (ops: AmmDexOperation[]): Operation[] => { type: op.order.type, status: op.status, txId: op.txId, - timestamp: getFormattedDate(op.timestamp), + timestamp, }, ]; } @@ -62,7 +64,7 @@ export const normalizeOperations = (ops: AmmDexOperation[]): Operation[] => { type: op.order.type, status: op.status, txId: op.txId, - timestamp: getFormattedDate(op.timestamp), + timestamp, }, ]; } diff --git a/src/context/AddressContext.tsx b/src/context/AddressContext.tsx index 996c14852..d015dece0 100644 --- a/src/context/AddressContext.tsx +++ b/src/context/AddressContext.tsx @@ -1,8 +1,11 @@ import { publicKeyFromAddress } from '@ergolabs/ergo-sdk'; import React, { createContext, useContext, useEffect, useState } from 'react'; +import { filter, mapTo } from 'rxjs'; +import { selectedWalletState$ } from '../api/wallets'; +import { useObservable } from '../common/hooks/useObservable'; +import { WalletState } from '../network/common'; import { useSettings } from './SettingsContext'; -import { WalletContext } from './WalletContext'; export type Address = string; @@ -45,7 +48,12 @@ export const WalletAddressesProvider = ({ children, }: WalletAddressesProviderProps): JSX.Element => { const [settings, setSettings] = useSettings(); - const { isWalletConnected } = useContext(WalletContext); + const [isWalletConnected] = useObservable( + selectedWalletState$.pipe( + filter((state) => state === WalletState.CONNECTED), + mapTo(true), + ), + ); const [walletAddresses, setWalletAddress] = useState( DefaultWalletAddresses, diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx deleted file mode 100644 index 50431defd..000000000 --- a/src/context/WalletContext.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ErgoBox, ergoBoxFromProxy, TokenId } from '@ergolabs/ergo-sdk'; -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; - -import { ERG_DECIMALS, ERG_TOKEN_NAME } from '../common/constants/erg'; -import { useInterval } from '../hooks/useInterval'; -import { walletCookies } from '../utils/cookies'; -import { renderFractions } from '../utils/math'; - -export enum WalletConnectionState { - NOT_CONNECTED, // initial state - CONNECTED, - DISCONNECTED, -} - -export type WalletContextType = { - isWalletConnected: boolean; // @deprecated in favour of walletConnectionState - walletConnectionState: WalletConnectionState; - utxos: ErgoBox[] | undefined; - setIsWalletConnected: (isWalletConnected: boolean) => void; - getTokenBalance: (tokenId: TokenId) => Promise; - ergBalance: string | undefined; - isWalletLoading: boolean; -}; - -function noop() { - return; -} - -export const WalletContext = createContext({ - isWalletConnected: false, - walletConnectionState: WalletConnectionState.NOT_CONNECTED, - utxos: undefined, - setIsWalletConnected: noop, - getTokenBalance: () => Promise.resolve(undefined), - ergBalance: undefined, - isWalletLoading: false, -}); - -export const useWallet = (): WalletContextType => useContext(WalletContext); - -const fetchUtxos = () => - ergo - .get_utxos() - .then((bs) => bs?.map((b) => ergoBoxFromProxy(b))) - .then((data: ErgoBox[] | undefined) => { - return data ?? []; - }); - -export const WalletContextProvider = ({ - children, -}: React.PropsWithChildren): JSX.Element => { - const [walletConnectionState, setWalletConnectionState] = useState( - WalletConnectionState.NOT_CONNECTED, - ); - const [utxos, setUtxos] = useState(); - const [ergBalance, setErgBalance] = useState(); - const [isWalletLoading, setIsWalletLoading] = useState(false); - - const setIsWalletConnected = useCallback((isConnected: boolean) => { - setWalletConnectionState( - isConnected - ? WalletConnectionState.CONNECTED - : WalletConnectionState.DISCONNECTED, - ); - }, []); - - const getTokenBalance = (tokenId: TokenId) => - ergo - .get_balance(tokenId) - .then((amount) => renderFractions(amount, ERG_DECIMALS)); - - const isWalletConnected = - walletConnectionState === WalletConnectionState.CONNECTED; - - const ctxValue = { - isWalletConnected, // TODO: replace isWalletConnected with walletConnectionState to handle initial state - walletConnectionState, - setIsWalletConnected, - getTokenBalance, - utxos, - ergBalance, - isWalletLoading, - }; - - useEffect(() => { - if (walletCookies.isSetConnected() && window.ergo_request_read_access) { - setIsWalletLoading(true); - window - .ergo_request_read_access() - .then(setIsWalletConnected) - .finally(() => setIsWalletLoading(false)); - } - }, [isWalletConnected, setIsWalletConnected]); - - useEffect(() => { - if (isWalletConnected) { - setIsWalletLoading(true); - Promise.all([ - fetchUtxos().then(setUtxos), - ergo.get_balance(ERG_TOKEN_NAME).then((balance) => { - setErgBalance(renderFractions(balance, ERG_DECIMALS)); - }), - ]).finally(() => setIsWalletLoading(false)); - } - }, [isWalletConnected]); - - useInterval(() => { - if (isWalletConnected) { - fetchUtxos().then(setUtxos); - ergo.get_balance(ERG_TOKEN_NAME).then((balance) => { - setErgBalance(renderFractions(balance, ERG_DECIMALS)); - }); - } - }, 10 * 1000); - - return ( - {children} - ); -}; diff --git a/src/context/index.ts b/src/context/index.ts index 7214404aa..24771863d 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -1,4 +1,3 @@ export * from './AddressContext'; export * from './AppLoadingContext'; export * from './SettingsContext'; -export * from './WalletContext'; diff --git a/src/ergodex-cdk/components/Flex/Flex.less b/src/ergodex-cdk/components/Flex/Flex.less index 41ab593c2..fe5460fb0 100644 --- a/src/ergodex-cdk/components/Flex/Flex.less +++ b/src/ergodex-cdk/components/Flex/Flex.less @@ -64,6 +64,24 @@ } } +.ergo-flex-align-self { + &--flex-start { + align-self: flex-start; + } + + &--flex-end { + align-self: flex-end; + } + + &--stretch { + align-self: stretch; + } + + &--center { + align-self: center; + } +} + .ergo-flex-stretch { height: 100%; } diff --git a/src/ergodex-cdk/components/Flex/Flex.tsx b/src/ergodex-cdk/components/Flex/Flex.tsx index a8bbfb00f..681827416 100644 --- a/src/ergodex-cdk/components/Flex/Flex.tsx +++ b/src/ergodex-cdk/components/Flex/Flex.tsx @@ -65,6 +65,7 @@ type ItemsProps = React.DetailedHTMLProps< > & { display?: 'flex' | 'block' | 'none'; order?: number; + alignSelf?: 'flex-start' | 'stretch' | 'flex-end' | 'center'; marginBottom?: number; marginTop?: number; marginLeft?: number; @@ -75,6 +76,7 @@ type ItemsProps = React.DetailedHTMLProps< } & FlexProps; const Item: FC = ({ + alignSelf, children, display, style, @@ -99,6 +101,7 @@ const Item: FC = ({ 'ergo-flex', `ergo-flex-direction--${(col && 'col') || (row && 'row') || direction}`, `ergo-flex-justify--${justify}`, + `ergo-flex-align-self--${alignSelf}`, `ergo-flex-align-items--${align}`, { 'ergo-flex-item__grow': grow }, ])} diff --git a/src/ergodex-cdk/components/Form/Form.less b/src/ergodex-cdk/components/Form/Form.less deleted file mode 100644 index a233f5e6d..000000000 --- a/src/ergodex-cdk/components/Form/Form.less +++ /dev/null @@ -1,7 +0,0 @@ -.ant-form-item-explain-error { - color: var(--ergo-error-color); -} - -.ant-form-item-explain-warning { - color: var(--ergo-warning-color); -} diff --git a/src/ergodex-cdk/components/Form/Form.tsx b/src/ergodex-cdk/components/Form/Form.tsx index 1eb549a8c..e462bf9ee 100644 --- a/src/ergodex-cdk/components/Form/Form.tsx +++ b/src/ergodex-cdk/components/Form/Form.tsx @@ -1,37 +1,47 @@ -import './Form.less'; - -import { Form, FormInstance, FormItemProps, FormProps } from 'antd'; -import { FieldData } from 'rc-field-form/lib/interface'; - -const oldUseForm = Form.useForm; - -// TODO: WRITE_OWN_FORMS -Form.useForm = (form?: FormInstance) => { - const [newForm] = oldUseForm(form); - - const oldSetFieldsValue = newForm.setFieldsValue; - const oldSetFields = newForm.setFields; - - newForm.setFieldsValue = function (value: any) { - oldSetFieldsValue.call(this, value); - Promise.resolve().then(() => { - if ((this as any)?.onFieldManuallyChange) { - (this as any)?.onFieldManuallyChange(newForm.getFieldsValue()); - } - }); - }; - - newForm.setFields = function (fields: FieldData[]) { - oldSetFields.call(this, fields); - Promise.resolve().then(() => { - if ((this as any)?.onFieldManuallyChange) { - (this as any)?.onFieldManuallyChange(newForm.getFieldsValue()); - } - }); - }; - - return [newForm]; -}; - -export { Form }; -export type { FormInstance, FormItemProps, FormProps }; +import React, { FormEvent, ReactNode } from 'react'; + +import { Messages } from './core'; +import { FormContext } from './FormContext'; +import { FormGroup } from './FormGroup'; +import { FormItem } from './FormItem'; +import { FormListener } from './FormListener'; + +export interface FormProps { + readonly form: FormGroup; + readonly onSubmit: (form: FormGroup) => void; + readonly children?: ReactNode | ReactNode[] | string; + readonly errorMessages?: Messages; + readonly warningMessages?: Messages; +} + +class _Form extends React.Component> { + constructor(props: FormProps) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + } + + private handleSubmit(e: FormEvent) { + e.preventDefault(); + this.props.onSubmit(this.props.form); + } + + render() { + const { children, form, errorMessages, warningMessages } = this.props; + + return ( +
+ + {children} + +
+ ); + } +} + +export const Form: typeof _Form & { + Item: typeof FormItem; + Listener: typeof FormListener; +} = _Form as any; +Form.Item = FormItem; +Form.Listener = FormListener; diff --git a/src/ergodex-cdk/components/Form/FormContext.ts b/src/ergodex-cdk/components/Form/FormContext.ts new file mode 100644 index 000000000..18704682c --- /dev/null +++ b/src/ergodex-cdk/components/Form/FormContext.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; + +import { Messages } from './core'; +import { FormGroup } from './FormGroup'; + +export const FormContext = createContext<{ + form: FormGroup; + errorMessages?: Messages; + warningMessages?: Messages; +}>({} as any); + +export const useFormContext = (): { + form: FormGroup; + errorMessages?: Messages; + warningMessages?: Messages; +} => useContext(FormContext); diff --git a/src/ergodex-cdk/components/Form/FormControl.ts b/src/ergodex-cdk/components/Form/FormControl.ts new file mode 100644 index 000000000..711d629b7 --- /dev/null +++ b/src/ergodex-cdk/components/Form/FormControl.ts @@ -0,0 +1,177 @@ +import { BehaviorSubject, Observable } from 'rxjs'; + +import { AbstractFormItem, CheckFn, EventConfig, FormItemState } from './core'; + +export type FormControlParams = + | T + | { + value: T; + errorValidators: CheckFn[]; + warningValidators: CheckFn[]; + __config: true; + }; + +export class FormControl implements AbstractFormItem { + constructor( + public name: string, + private param: FormControlParams, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + private parent: any, + ) {} + + //@ts-ignore + value: T = this.getValueFromParam(this.param); + + //@ts-ignore + errorValidators: CheckFn[] = this.getErrorValidatorsFromParam(this.param); + + //@ts-ignore + warningValidators: CheckFn[] = this.getWarningValidatorsFromParam(this.param); + + currentError: string | undefined = this.getCurrentCheckName( + this.value, + this.errorValidators, + ); + + invalid = !!this.currentError; + + valid = !this.invalid; + + currentWarning: string | undefined = this.valid + ? this.getCurrentCheckName(this.value, this.warningValidators) + : undefined; + + withWarnings = !!this.currentWarning; + + withoutWarnings = !this.withWarnings; + + touched = false; + + untouched = true; + + get state(): FormItemState { + if (this.invalid) { + return 'error'; + } + if (this.withWarnings) { + return 'warning'; + } + return undefined; + } + + get valueChanges$(): Observable { + return this._valueChanges$; + } + + get valueChangesWithSystem$(): Observable { + return this._valueChangesWithSystem$; + } + + get valueChangesWithSilent$(): Observable { + return this._valueChangesWithSilent$; + } + + private readonly _valueChanges$ = new BehaviorSubject(this.value); + + private readonly _valueChangesWithSystem$ = new BehaviorSubject( + this.value, + ); + + private readonly _valueChangesWithSilent$ = new BehaviorSubject( + this.value, + ); + + markAsTouched(): void { + this.touched = true; + this.untouched = false; + } + + markAsUntouched(): void { + this.untouched = true; + this.touched = false; + } + + internalPatchValue(value: T, config?: EventConfig): void { + this.value = value; + this.currentError = this.getCurrentCheckName( + this.value, + this.errorValidators, + ); + this.invalid = !!this.currentError; + this.valid = !this.invalid; + this.currentWarning = this.valid + ? this.getCurrentCheckName(this.value, this.warningValidators) + : undefined; + this.withWarnings = !!this.currentWarning; + this.withoutWarnings = !this.withWarnings; + this.emitEvent(config); + } + + 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 { + this.patchValue(value, config); + this.markAsUntouched(); + } + + emitEvent(config?: EventConfig): void { + if ( + config?.emitEvent === 'system' || + config?.emitEvent === 'default' || + config?.emitEvent === 'silent' || + !config?.emitEvent + ) { + this._valueChangesWithSilent$.next(this.value); + } + if ( + config?.emitEvent === 'system' || + config?.emitEvent === 'default' || + !config?.emitEvent + ) { + this._valueChangesWithSystem$.next(this.value); + } + if (config?.emitEvent === 'default' || !config?.emitEvent) { + this._valueChanges$.next(this.value); + } + } + + private getCurrentCheckName( + value: T, + checkFns: CheckFn[], + ): string | undefined { + for (let i = 0; i < checkFns.length; i++) { + const result = checkFns[i](value); + + if (result) { + return result; + } + } + + return undefined; + } + + private getValueFromParam(param: FormControlParams) { + return param instanceof Object && param.__config ? param.value : param; + } + + private getErrorValidatorsFromParam(param: FormControlParams): CheckFn[] { + return param instanceof Object && param.__config + ? param.errorValidators || [] + : []; + } + + private getWarningValidatorsFromParam( + param: FormControlParams, + ): CheckFn[] { + return param instanceof Object && param.__config + ? param.warningValidators || [] + : []; + } +} diff --git a/src/ergodex-cdk/components/Form/FormGroup.ts b/src/ergodex-cdk/components/Form/FormGroup.ts new file mode 100644 index 000000000..b66d2a2a6 --- /dev/null +++ b/src/ergodex-cdk/components/Form/FormGroup.ts @@ -0,0 +1,141 @@ +import { BehaviorSubject, Observable } from 'rxjs'; + +import { AbstractFormItem, EventConfig, FormItemState } from './core'; +import { FormControl, FormControlParams } from './FormControl'; + +export type FormGroupParams = { + [key in keyof T]: FormControlParams; +}; + +export class FormGroup implements AbstractFormItem { + //@ts-ignore + private controlsArray = Object.entries(this.params).map( + ([key, param]) => new FormControl(key, param, this), + ); + + //@ts-ignore + controls = this.controlsArray.reduce<{ + [key in keyof Required]: FormControl; + }>(this.toControlDictionary, {} as any); + + get valueChanges$(): Observable { + return this._valueChanges$; + } + + get state(): FormItemState { + if (this.controlsArray.some((c) => c.state === 'error')) { + return 'error'; + } + if (this.controlsArray.some((c) => c.state === 'warning')) { + return 'warning'; + } + return undefined; + } + + get valueChangesWithSystem$(): Observable { + return this._valueChangesWithSystem$; + } + + get valueChangesWithSilent$(): Observable { + return this._valueChangesWithSilent$; + } + + private readonly _valueChanges$ = new BehaviorSubject(this.value); + + private readonly _valueChangesWithSystem$ = new BehaviorSubject( + this.value, + ); + + private readonly _valueChangesWithSilent$ = new BehaviorSubject( + this.value, + ); + + get invalid(): boolean { + return this.controlsArray.some((c) => c.invalid); + } + + get valid(): boolean { + return this.controlsArray.every((c) => c.valid); + } + + get withWarnings(): boolean { + return this.controlsArray.some((c) => c.withWarnings); + } + + get withoutWarnings(): boolean { + return this.controlsArray.every((c) => c.withoutWarnings); + } + + get touched(): boolean { + return this.controlsArray.some((c) => c.touched); + } + + get untouched(): boolean { + return this.controlsArray.every((c) => c.untouched); + } + + get value(): T { + // @ts-ignore + return this.controlsArray.reduce((acc, ctrl) => { + // @ts-ignore + acc[ctrl.name] = ctrl.value; + + return acc; + }, {} as any); + } + + constructor(private params: FormGroupParams) {} + + private toControlDictionary( + dictionary: { [key in keyof Required]: FormControl }, + ctrl: FormControl, + ): { [key in keyof Required]: FormControl } { + //@ts-ignore + dictionary[ctrl.name] = ctrl; + + return dictionary; + } + + markAllAsTouched(): void { + this.controlsArray.forEach((c) => c.markAsTouched()); + } + + markAllAsUntouched(): void { + this.controlsArray.forEach((c) => c.markAsUntouched()); + } + + patchValue(value: Partial, config?: EventConfig): void { + Object.entries(value).forEach(([key, value]) => + this.controls[key as keyof T].internalPatchValue(value as any, config), + ); + this.emitEvent(config); + } + + 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): void { + if ( + config?.emitEvent === 'system' || + config?.emitEvent === 'default' || + config?.emitEvent === 'silent' || + !config?.emitEvent + ) { + this._valueChangesWithSilent$.next(this.value); + } + if ( + config?.emitEvent === 'system' || + config?.emitEvent === 'default' || + !config?.emitEvent + ) { + this._valueChangesWithSystem$.next(this.value); + } + if (config?.emitEvent === 'default' || !config?.emitEvent) { + this._valueChanges$.next(this.value); + } + } +} diff --git a/src/ergodex-cdk/components/Form/FormItem.tsx b/src/ergodex-cdk/components/Form/FormItem.tsx new file mode 100644 index 000000000..7940fbc39 --- /dev/null +++ b/src/ergodex-cdk/components/Form/FormItem.tsx @@ -0,0 +1,92 @@ +import React, { ReactNode } from 'react'; +import { Subscription } from 'rxjs'; + +import { EventConfig, FormItemState } from './core'; +import { FormContext } from './FormContext'; +import { FormControl } from './FormControl'; + +interface FormItemFnParams { + readonly value: T; + readonly onChange: (value: T, config?: EventConfig) => void; + readonly touched: boolean; + readonly untouched: boolean; + readonly invalid: boolean; + readonly valid: boolean; + readonly state: FormItemState; + readonly withWarnings?: boolean; + readonly withoutWarnings?: boolean; + readonly message?: string; +} + +export type Control = Omit>, 'children'>; + +export interface FormItemProps { + readonly name: string; + readonly children?: ( + params: FormItemFnParams, + ) => ReactNode | ReactNode[] | string; +} + +export class FormItem extends React.Component> { + //@ts-ignore + private subscription: Subscription; + + componentWillUnmount(): void { + this.subscription?.unsubscribe(); + } + + onChange(ctrl: FormControl, value: T, config?: EventConfig): void { + ctrl.onChange(value, config); + } + + render(): ReactNode { + const { name, children } = this.props; + + return ( + + {({ form, errorMessages, warningMessages }) => { + const control = form.controls[name]; + if (!this.subscription && control) { + this.subscription = control.valueChangesWithSilent$.subscribe(() => + this.forceUpdate(), + ); + } + + let message = undefined; + + if (control.invalid) { + message = + control.currentError && + errorMessages && + errorMessages[name] && + errorMessages[name][control.currentError]; + } else if (control.withWarnings) { + message = + control.currentWarning && + warningMessages && + warningMessages[name] && + warningMessages[name][control.currentWarning]; + } + + if (message instanceof Function) { + message = message(control.value); + } + return children && control + ? children({ + onChange: this.onChange.bind(this, control), + value: control.value, + touched: control.touched, + untouched: control.untouched, + state: control.state, + invalid: control.invalid, + valid: control.valid, + withWarnings: control.withWarnings, + withoutWarnings: control.withoutWarnings, + message, + }) + : undefined; + }} + + ); + } +} diff --git a/src/ergodex-cdk/components/Form/FormListener.tsx b/src/ergodex-cdk/components/Form/FormListener.tsx new file mode 100644 index 000000000..5e537e484 --- /dev/null +++ b/src/ergodex-cdk/components/Form/FormListener.tsx @@ -0,0 +1,91 @@ +import React, { ReactNode } from 'react'; +import { Subscription } from 'rxjs'; + +import { FormItemState } from './core'; +import { FormContext } from './FormContext'; +import { FormGroup } from './FormGroup'; + +interface FormListenerFnParams { + readonly value: T; + readonly touched: boolean; + readonly untouched: boolean; + readonly invalid: boolean; + readonly valid: boolean; + readonly state: FormItemState; + readonly withWarnings?: boolean; + readonly withoutWarnings?: boolean; + readonly message?: string; +} + +export type Listener = Omit>, 'children'>; + +export interface FormListenerProps { + readonly name?: string; + readonly children?: ( + params: FormListenerFnParams, + ) => ReactNode | ReactNode[] | string; +} + +export class FormListener extends React.Component< + FormListenerProps +> { + //@ts-ignore + private subscription: Subscription; + + componentWillUnmount(): void { + this.subscription?.unsubscribe(); + } + + render(): ReactNode { + const { name, children } = this.props; + + return ( + + {({ form, errorMessages, warningMessages }) => { + const item = name ? form.controls[name] : form; + if (!this.subscription && item) { + this.subscription = item.valueChangesWithSilent$.subscribe(() => + this.forceUpdate(), + ); + } + + let message = undefined; + + if (item instanceof FormGroup) { + message = undefined; + } else if (item.invalid && name) { + message = + item.currentError && + errorMessages && + errorMessages[name] && + errorMessages[name][item.currentError]; + } else if (item.withWarnings && name) { + message = + item.currentWarning && + warningMessages && + warningMessages[name] && + warningMessages[name][item.currentWarning]; + } + + if (message instanceof Function) { + message = message(item.value); + } + + return children && item + ? children({ + value: item.value, + touched: item.touched, + untouched: item.untouched, + invalid: item.invalid, + valid: item.valid, + state: item.state, + withWarnings: item.withWarnings, + withoutWarnings: item.withoutWarnings, + message, + }) + : undefined; + }} + + ); + } +} diff --git a/src/ergodex-cdk/components/Form/NewForm.tsx b/src/ergodex-cdk/components/Form/NewForm.tsx deleted file mode 100644 index 3d9329355..000000000 --- a/src/ergodex-cdk/components/Form/NewForm.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import React, { - createContext, - FormEvent, - ReactNode, - useContext, - useState, -} from 'react'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; - -export type CheckFn = (t: T) => string | undefined; - -export function _useForm(param: FormGroupParams): FormGroup { - const [form] = useState(new FormGroup(param)); - - return form; -} - -function ctrl( - value: T, - errorValidators?: CheckFn[], - warningValidators?: CheckFn[], -): { - value: T; - errorValidators: CheckFn[]; - warningValidators: CheckFn[]; - __config: true; -} { - return { - value, - errorValidators: errorValidators || [], - warningValidators: warningValidators || [], - __config: true, - }; -} - -(_useForm as any).ctrl = ctrl; - -export interface EventConfig { - readonly emitEvent: 'default' | 'system' | 'silent'; -} - -export type Messages = { - [key in keyof Partial]: { - [key: string]: string | ((value: T[key]) => string); - }; -}; - -interface AbstractFormItem { - readonly value: T; - - readonly invalid: boolean; - - readonly valid: boolean; - - readonly touched: boolean; - - readonly untouched: boolean; - - readonly valueChanges$: Observable; - - readonly valueChangesWithSystem$: Observable; -} - -export type FormControlParams = - | T - | { - value: T; - errorValidators: CheckFn[]; - warningValidators: CheckFn[]; - __config: true; - }; - -export class FormControl implements AbstractFormItem { - constructor( - public name: string, - private param: FormControlParams, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - private parent: any, - ) {} - - //@ts-ignore - value: T = this.getValueFromParam(this.param); - - //@ts-ignore - errorValidators: CheckFn[] = this.getErrorValidatorsFromParam(this.param); - - //@ts-ignore - warningValidators: CheckFn[] = this.getWarningValidatorsFromParam(this.param); - - currentError: string | undefined = this.getCurrentCheckName( - this.value, - this.errorValidators, - ); - - invalid = !!this.currentError; - - valid = !this.invalid; - - currentWarning: string | undefined = this.getCurrentCheckName( - this.value, - this.warningValidators, - ); - - withWarnings = !!this.currentWarning; - - withoutWarnings = !this.withWarnings; - - touched = false; - - untouched = true; - - get valueChanges$(): Observable { - return this._valueChanges$; - } - - get valueChangesWithSystem$(): Observable { - return this._valueChangesWithSystem$; - } - - get valueChangesWithSilent$(): Observable { - return this._valueChangesWithSilent$; - } - - private readonly _valueChanges$ = new BehaviorSubject(this.value); - - private readonly _valueChangesWithSystem$ = new BehaviorSubject( - this.value, - ); - - private readonly _valueChangesWithSilent$ = new BehaviorSubject( - this.value, - ); - - markAsTouched(): void { - this.touched = true; - this.untouched = false; - } - - markAsUntouched(): void { - this.untouched = true; - this.touched = false; - } - - internalPatchValue(value: T, config?: EventConfig): void { - this.value = value; - this.currentError = this.getCurrentCheckName( - this.value, - this.errorValidators, - ); - this.invalid = !!this.currentError; - this.valid = !this.invalid; - this.currentWarning = this.getCurrentCheckName( - this.value, - this.warningValidators, - ); - this.withWarnings = !!this.currentWarning; - this.withoutWarnings = !this.withWarnings; - this.emitEvent(config); - } - - 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 { - this.patchValue(value, config); - this.markAsUntouched(); - } - - emitEvent(config?: EventConfig): void { - if ( - config?.emitEvent === 'system' || - config?.emitEvent === 'default' || - config?.emitEvent === 'silent' || - !config?.emitEvent - ) { - this._valueChangesWithSilent$.next(this.value); - } - if ( - config?.emitEvent === 'system' || - config?.emitEvent === 'default' || - !config?.emitEvent - ) { - this._valueChangesWithSystem$.next(this.value); - } - if (config?.emitEvent === 'default' || !config?.emitEvent) { - this._valueChanges$.next(this.value); - } - } - - private getCurrentCheckName( - value: T, - checkFns: CheckFn[], - ): string | undefined { - for (let i = 0; i < checkFns.length; i++) { - const result = checkFns[i](value); - - if (result) { - return result; - } - } - - return undefined; - } - - private getValueFromParam(param: FormControlParams) { - return param instanceof Object && param.__config ? param.value : param; - } - - private getErrorValidatorsFromParam(param: FormControlParams): CheckFn[] { - return param instanceof Object && param.__config - ? param.errorValidators || [] - : []; - } - - private getWarningValidatorsFromParam( - param: FormControlParams, - ): CheckFn[] { - return param instanceof Object && param.__config - ? param.warningValidators || [] - : []; - } -} - -export type FormGroupParams = { - [key in keyof T]: FormControlParams; -}; - -export class FormGroup implements AbstractFormItem { - //@ts-ignore - private controlsArray = Object.entries(this.params).map( - ([key, param]) => new FormControl(key, param, this), - ); - - //@ts-ignore - controls = this.controlsArray.reduce<{ - [key in keyof Required]: FormControl; - }>(this.toControlDictionary, {} as any); - - get valueChanges$(): Observable { - return this._valueChanges$; - } - - get valueChangesWithSystem$(): Observable { - return this._valueChangesWithSystem$; - } - - get valueChangesWithSilent$(): Observable { - return this._valueChangesWithSilent$; - } - - private readonly _valueChanges$ = new BehaviorSubject(this.value); - - private readonly _valueChangesWithSystem$ = new BehaviorSubject( - this.value, - ); - - private readonly _valueChangesWithSilent$ = new BehaviorSubject( - this.value, - ); - - get invalid(): boolean { - return this.controlsArray.some((c) => c.invalid); - } - - get valid(): boolean { - return this.controlsArray.every((c) => c.valid); - } - - get touched(): boolean { - return this.controlsArray.some((c) => c.touched); - } - - get untouched(): boolean { - return this.controlsArray.every((c) => c.untouched); - } - - get value(): T { - // @ts-ignore - return this.controlsArray.reduce((acc, ctrl) => { - // @ts-ignore - acc[ctrl.name] = ctrl.value; - - return acc; - }, {} as any); - } - - constructor(private params: FormGroupParams) {} - - private toControlDictionary( - dictionary: { [key in keyof Required]: FormControl }, - ctrl: FormControl, - ): { [key in keyof Required]: FormControl } { - //@ts-ignore - dictionary[ctrl.name] = ctrl; - - return dictionary; - } - - markAllAsTouched(): void { - this.controlsArray.forEach((c) => c.markAsTouched()); - } - - markAllAsUntouched(): void { - this.controlsArray.forEach((c) => c.markAsUntouched()); - } - - patchValue(value: Partial, config?: EventConfig): void { - Object.entries(value).forEach(([key, value]) => - this.controls[key as keyof T].internalPatchValue(value as any, config), - ); - this.emitEvent(config); - } - - 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): void { - if ( - config?.emitEvent === 'system' || - config?.emitEvent === 'default' || - config?.emitEvent === 'silent' || - !config?.emitEvent - ) { - this._valueChangesWithSilent$.next(this.value); - } - if ( - config?.emitEvent === 'system' || - config?.emitEvent === 'default' || - !config?.emitEvent - ) { - this._valueChangesWithSystem$.next(this.value); - } - if (config?.emitEvent === 'default' || !config?.emitEvent) { - this._valueChanges$.next(this.value); - } - } -} - -export interface FormProps { - readonly form: FormGroup; - readonly onSubmit: (form: FormGroup) => void; - readonly children?: ReactNode | ReactNode[] | string; - readonly errorMessages?: Messages; - readonly warningMessages?: Messages; -} - -export const FormContext = createContext<{ - form: FormGroup; - errorMessages?: Messages; - warningMessages?: Messages; -}>({} as any); - -export const useFormContext = (): { - form: FormGroup; - errorMessages?: Messages; - warningMessages?: Messages; -} => useContext(FormContext); - -class _Form extends React.Component> { - constructor(props: FormProps) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - } - - private handleSubmit(e: FormEvent) { - e.preventDefault(); - this.props.onSubmit(this.props.form); - } - - render() { - const { children, form, errorMessages, warningMessages } = this.props; - - return ( -
- - {children} - -
- ); - } -} - -interface FormItemFnParams { - readonly value: T; - readonly onChange: (value: T, config?: EventConfig) => void; - readonly touched: boolean; - readonly untouched: boolean; - readonly invalid: boolean; - readonly valid: boolean; - readonly warningMessage?: string; - readonly withWarnings?: boolean; - readonly withoutWarnings?: boolean; - readonly errorMessage?: string; -} - -export type Control = Omit>, 'children'>; - -export interface FormItemProps { - readonly name: string; - readonly children?: ( - params: FormItemFnParams, - ) => ReactNode | ReactNode[] | string; -} - -class _FormItem extends React.Component> { - //@ts-ignore - private subscription: Subscription; - - componentWillUnmount() { - this.subscription?.unsubscribe(); - } - - onChange(ctrl: FormControl, value: T, config?: EventConfig) { - ctrl.onChange(value, config); - } - - render() { - const { name, children } = this.props; - - return ( - - {({ form, errorMessages, warningMessages }) => { - const control = form.controls[name]; - if (!this.subscription && control) { - this.subscription = control.valueChangesWithSilent$.subscribe(() => - this.forceUpdate(), - ); - } - - let warningMessage = - control.currentWarning && - warningMessages && - warningMessages[name] && - warningMessages[name][control.currentWarning]; - - if (warningMessage instanceof Function) { - warningMessage = warningMessage(control.value); - } - - let errorMessage = - control.currentError && - errorMessages && - errorMessages[name] && - errorMessages[name][control.currentError]; - - if (errorMessage instanceof Function) { - errorMessage = errorMessage(control.value); - } - return children && control - ? children({ - onChange: this.onChange.bind(this, control), - value: control.value, - touched: control.touched, - untouched: control.untouched, - invalid: control.invalid, - valid: control.valid, - withWarnings: control.withWarnings, - withoutWarnings: control.withoutWarnings, - errorMessage, - warningMessage, - }) - : undefined; - }} - - ); - } -} - -export const Form: typeof _Form & { Item: typeof _FormItem } = _Form as any; -Form.Item = _FormItem; - -export const useForm: typeof _useForm & { ctrl: typeof ctrl } = _useForm as any; diff --git a/src/ergodex-cdk/components/Form/core.ts b/src/ergodex-cdk/components/Form/core.ts new file mode 100644 index 000000000..faeef9ceb --- /dev/null +++ b/src/ergodex-cdk/components/Form/core.ts @@ -0,0 +1,37 @@ +import { Observable } from 'rxjs'; + +export type CheckFn = (t: T) => string | undefined; + +export interface EventConfig { + readonly emitEvent: 'default' | 'system' | 'silent'; +} + +export type Messages = { + [key in keyof Partial]: { + [key: string]: string | ((value: T[key]) => string); + }; +}; + +export type FormItemState = 'error' | 'warning' | undefined; + +export interface AbstractFormItem { + readonly value: T; + + readonly invalid: boolean; + + readonly valid: boolean; + + readonly withWarnings: boolean; + + readonly withoutWarnings: boolean; + + readonly state: FormItemState; + + readonly touched: boolean; + + readonly untouched: boolean; + + readonly valueChanges$: Observable; + + readonly valueChangesWithSystem$: Observable; +} diff --git a/src/ergodex-cdk/components/Form/hooks.ts b/src/ergodex-cdk/components/Form/hooks.ts new file mode 100644 index 000000000..d7e10f1ee --- /dev/null +++ b/src/ergodex-cdk/components/Form/hooks.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +import { CheckFn } from './core'; +import { FormGroup, FormGroupParams } from './FormGroup'; + +function _useForm(param: FormGroupParams): FormGroup { + const [form] = useState(new FormGroup(param)); + + return form; +} + +function ctrl( + value: T, + errorValidators?: CheckFn[], + warningValidators?: CheckFn[], +): { + value: T; + errorValidators: CheckFn[]; + warningValidators: CheckFn[]; + __config: true; +} { + return { + value, + errorValidators: errorValidators || [], + warningValidators: warningValidators || [], + __config: true, + }; +} + +(_useForm as any).ctrl = ctrl; + +export const useForm: typeof _useForm & { ctrl: typeof ctrl } = _useForm as any; diff --git a/src/ergodex-cdk/components/Form/index.ts b/src/ergodex-cdk/components/Form/index.ts new file mode 100644 index 000000000..59c9d4d8b --- /dev/null +++ b/src/ergodex-cdk/components/Form/index.ts @@ -0,0 +1,8 @@ +export * from './core'; +export * from './Form'; +export { useFormContext } from './FormContext'; +export * from './FormControl'; +export * from './FormGroup'; +export * from './FormItem'; +export * from './FormListener'; +export * from './hooks'; diff --git a/src/ergodex-cdk/components/Input/Input.less b/src/ergodex-cdk/components/Input/Input.less index dd892c21a..5a2128132 100644 --- a/src/ergodex-cdk/components/Input/Input.less +++ b/src/ergodex-cdk/components/Input/Input.less @@ -49,6 +49,17 @@ font-size: 16px; line-height: 24px; } + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + margin: 0; + /* stylelint-disable-next-line */ + -webkit-appearance: none; + } + &[type="number"] { + /* stylelint-disable-next-line */ + -moz-appearance: textfield; + } } .ant-input-affix-wrapper { diff --git a/src/ergodex-cdk/components/index.ts b/src/ergodex-cdk/components/index.ts index 7eeb30d51..e784e8da9 100644 --- a/src/ergodex-cdk/components/index.ts +++ b/src/ergodex-cdk/components/index.ts @@ -8,7 +8,7 @@ export * from './Collapse/Collapse'; export * from './DatePicker/DatePicker'; export * from './Dropdown/Dropdown'; export * from './Flex/Flex'; -export * from './Form/Form'; +export * from './Form'; export * from './Icon/Icon'; export * from './Input/Input'; export * from './List/List'; diff --git a/src/ergodex-cdk/services/notification/index.ts b/src/ergodex-cdk/services/notification/index.ts index 3744e9973..588dc2e91 100644 --- a/src/ergodex-cdk/services/notification/index.ts +++ b/src/ergodex-cdk/services/notification/index.ts @@ -1,3 +1,5 @@ import { notification } from 'antd'; +import { ArgsProps } from 'antd/es/notification'; export { notification }; +export type { ArgsProps }; diff --git a/src/ergodex-cdk/styles/variables.less b/src/ergodex-cdk/styles/variables.less index 4995dd648..69dbb3feb 100644 --- a/src/ergodex-cdk/styles/variables.less +++ b/src/ergodex-cdk/styles/variables.less @@ -163,6 +163,10 @@ // -- Components -- + // Scroll + --ergo-scroll-bg: @light-color-gray-5; + --ergo-scroll-border: @light-color-gray-1; + // Glow --ergo-glow-gradient: radial-gradient(50% 50% at 50% 50%, rgba(255, 115, 92, 0.8) 0%, rgba(240, 242, 245, 0.8) 100%); @@ -289,7 +293,7 @@ // Tabs --ergo-tabs-nav-list-color: var(--ergo-default-card-background); --ergo-tabs-nav-list-border-radius: var(--ergo-border-radius-md); - --ergo-tab-border-radius: var(--ergo-border-radius-md); + --ergo-tab-border-radius: var(--ergo-border-radius-sm); --ergo-tab-text-contrast: var(--ergo-primary-text); --ergo-tab-color-active: var(--ergo-primary-color); --ergo-tab-text-contrast-active: var(--ergo-primary-text-contrast); diff --git a/src/network/common.ts b/src/network/common.ts index 72fb4994a..d165ec09f 100644 --- a/src/network/common.ts +++ b/src/network/common.ts @@ -1,6 +1,7 @@ import { AmmDexOperation } from '@ergolabs/ergo-dex-sdk'; -import { Address } from '@ergolabs/ergo-sdk'; +import { Address, ErgoBox } from '@ergolabs/ergo-sdk'; import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; +import { ReactNode } from 'react'; import { Observable } from 'rxjs'; import { AmmPool } from '../common/models/AmmPool'; @@ -8,20 +9,40 @@ import { AssetLock } from '../common/models/AssetLock'; import { Balance } from '../common/models/Balance'; import { Currency } from '../common/models/Currency'; import { Position } from '../common/models/Position'; +import { ArgsProps } from '../ergodex-cdk'; export interface Network { readonly networkAsset$: Observable; readonly networkAssetBalance$: Observable; readonly assetBalance$: Observable; readonly lpBalance$: Observable; - readonly lpAssets$: Observable; - readonly assets$: Observable; readonly addresses$: Observable; readonly locks$: Observable; readonly ammPools$: Observable; readonly positions$: Observable; readonly pendingTransactionsCount$: Observable; readonly getTxHistory: (limit: number) => Observable; - + readonly wallets$: Observable; + readonly selectedWallet$: Observable; + readonly selectedWalletState$: Observable; + readonly connectWallet: (w: Wallet) => Observable; + readonly disconnectWallet: () => void; readonly useNetworkAsset: () => [AssetInfo, boolean, Error]; } + +export enum WalletState { + NOT_CONNECTED, + CONNECTING, + CONNECTED, +} + +export interface Wallet { + readonly name: string; + readonly icon: ReactNode; + readonly experimental: boolean; + readonly extensionLink: string; + readonly connectWallet: () => Observable; + readonly getUtxos: () => Observable; + readonly getNotification?: () => ArgsProps | undefined; + readonly onDisconnect?: () => void; +} diff --git a/src/network/ergo/addresses/addresses.ts b/src/network/ergo/addresses/addresses.ts index 9a397d8d8..2bef9eb19 100644 --- a/src/network/ergo/addresses/addresses.ts +++ b/src/network/ergo/addresses/addresses.ts @@ -1,36 +1,31 @@ import { - combineLatest, - debounceTime, + filter, from, map, + Observable, publishReplay, refCount, switchMap, + zip, } from 'rxjs'; import { appTick$ } from '../../../common/streams/appTick'; -import { isWalletConnected$ } from '../../../services/new/core'; +import { WalletState } from '../../common'; +import { selectedWalletState$ } from '../wallets'; -const usedAddresses$ = isWalletConnected$.pipe( - switchMap(() => appTick$), - switchMap(() => from(ergo.get_used_addresses())), - publishReplay(1), - refCount(), -); +const getUsedAddresses = () => from(ergo.get_used_addresses()); -const unusedAddresses$ = isWalletConnected$.pipe( - switchMap(() => appTick$), - switchMap(() => from(ergo.get_unused_addresses())), - publishReplay(1), - refCount(), -); +const getUnusedAddresses = () => from(ergo.get_unused_addresses()); + +export const getAddresses = (): Observable => + selectedWalletState$.pipe( + filter((state) => state === WalletState.CONNECTED), + switchMap(() => zip(getUsedAddresses(), getUnusedAddresses())), + map(([usedAddrs, unusedAddrs]) => unusedAddrs.concat(usedAddrs)), + ); -export const addresses$ = combineLatest([ - usedAddresses$, - unusedAddresses$, -]).pipe( - debounceTime(200), - map(([usedAddrs, unusedAddrs]) => unusedAddrs.concat(usedAddrs)), +export const addresses$: Observable = appTick$.pipe( + switchMap(() => getAddresses()), publishReplay(1), refCount(), ); diff --git a/src/network/ergo/ammPools/ammPools.ts b/src/network/ergo/ammPools/ammPools.ts index 0ac386a23..39433e551 100644 --- a/src/network/ergo/ammPools/ammPools.ts +++ b/src/network/ergo/ammPools/ammPools.ts @@ -1,49 +1,40 @@ import { - combineLatest, - debounceTime, - defer, + catchError, + filter, from, map, + of, publishReplay, refCount, retry, switchMap, + zip, } from 'rxjs'; +import { applicationConfig } from '../../../applicationConfig'; import { AmmPool } from '../../../common/models/AmmPool'; -import { appTick$ } from '../../../common/streams/appTick'; +import { networkContext$ } from '../networkContext/networkContext'; import { nativeNetworkPools, networkPools } from './common'; -const nativeNetworkAmmPools$ = appTick$.pipe( - switchMap(() => - defer(() => - from(nativeNetworkPools().getAll({ limit: 100, offset: 0 })), - ).pipe(retry(3)), - ), - map(([pools]) => pools), - publishReplay(1), - refCount(), -); +const getNativeNetworkAmmPools = () => + from(nativeNetworkPools().getAll({ limit: 100, offset: 0 })).pipe( + map(([pools]) => pools), + retry(applicationConfig.requestRetryCount), + ); -const networkAmmPools$ = appTick$.pipe( - switchMap(() => - defer(() => from(networkPools().getAll({ limit: 100, offset: 0 }))).pipe( - retry(3), - ), - ), - map(([pools]) => pools), - publishReplay(1), - refCount(), -); +const getNetworkAmmPools = () => + from(networkPools().getAll({ limit: 100, offset: 0 })).pipe( + map(([pools]) => pools), + retry(applicationConfig.requestRetryCount), + ); -export const ammPools$ = combineLatest([ - nativeNetworkAmmPools$, - networkAmmPools$, -]).pipe( - debounceTime(200), +export const ammPools$ = networkContext$.pipe( + switchMap(() => zip([getNativeNetworkAmmPools(), getNetworkAmmPools()])), map(([nativeNetworkPools, networkPools]) => nativeNetworkPools.concat(networkPools), ), + catchError(() => of(undefined)), + filter(Boolean), map((pools) => pools.map((p) => new AmmPool(p))), publishReplay(1), refCount(), diff --git a/src/network/ergo/assets/assets.ts b/src/network/ergo/assets/assets.ts deleted file mode 100644 index c98448211..000000000 --- a/src/network/ergo/assets/assets.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { uniqBy } from 'lodash'; -import { map, publishReplay, refCount } from 'rxjs'; - -import { applicationConfig } from '../../../applicationConfig'; -import { ammPools$ } from '../ammPools/ammPools'; - -export const assets$ = ammPools$.pipe( - map((pools) => - pools.filter( - (p) => - !applicationConfig.hiddenAssets.includes(p.x.asset.id) && - !applicationConfig.hiddenAssets.includes(p.y.asset.id) && - !applicationConfig.blacklistedPools.includes(p.id), - ), - ), - map((pools) => pools.flatMap((p) => [p.x.asset, p.y.asset])), - map((assets) => uniqBy(assets, 'id')), - publishReplay(1), - refCount(), -); diff --git a/src/network/ergo/assets/lpAssets.ts b/src/network/ergo/assets/lpAssets.ts deleted file mode 100644 index cc8a623d2..000000000 --- a/src/network/ergo/assets/lpAssets.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { uniqBy } from 'lodash'; -import { map, publishReplay, refCount } from 'rxjs'; - -import { applicationConfig } from '../../../applicationConfig'; -import { ammPools$ } from '../ammPools/ammPools'; - -export const lpAssets$ = ammPools$.pipe( - map((pools) => - pools.filter( - (p) => - !applicationConfig.hiddenAssets.includes(p.x.asset.id) && - !applicationConfig.hiddenAssets.includes(p.y.asset.id) && - !applicationConfig.blacklistedPools.includes(p.id), - ), - ), - map((pools) => pools.map((p) => p.lp.asset)), - map((assets) => uniqBy(assets, 'id')), - publishReplay(1), - refCount(), -); diff --git a/src/network/ergo/balance/assetBalance.ts b/src/network/ergo/balance/assetBalance.ts index 2bc43ae0b..085a340ac 100644 --- a/src/network/ergo/balance/assetBalance.ts +++ b/src/network/ergo/balance/assetBalance.ts @@ -1,22 +1,15 @@ -import { - combineLatest, - debounceTime, - map, - publishReplay, - refCount, -} from 'rxjs'; +import { map, publishReplay, refCount, zip } from 'rxjs'; import { Balance } from '../../../common/models/Balance'; import { ammPools$ } from '../ammPools/ammPools'; import { availableTokensData$ } from './common'; import { networkAssetBalance$ } from './networkAssetBalance'; -export const assetBalance$ = combineLatest([ +export const assetBalance$ = zip([ networkAssetBalance$, ammPools$, availableTokensData$, ]).pipe( - debounceTime(200), map(([networkAssetBalance, pools, availableTokensData]) => availableTokensData .filter(([, info]) => !pools.some((p) => p.lp.asset.id === info.id)) diff --git a/src/network/ergo/balance/common.ts b/src/network/ergo/balance/common.ts index 1ec56ffbc..308b16da7 100644 --- a/src/network/ergo/balance/common.ts +++ b/src/network/ergo/balance/common.ts @@ -2,7 +2,6 @@ import { ErgoBox } from '@ergolabs/ergo-sdk'; import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; import { combineLatest, - debounceTime, defaultIfEmpty, from, map, @@ -13,26 +12,23 @@ import { } from 'rxjs'; import { explorer } from '../../../services/explorer'; -import { utxos$ } from '../../../services/new/core'; import { Asset, getListAvailableTokens, } from '../../../utils/getListAvailableTokens'; +import { utxos$ } from '../common/utxos'; const toListAvailableTokens = (utxos: ErgoBox[]): Asset[] => Object.values(getListAvailableTokens(utxos)); -export const availableTokensData$: Observable<[bigint, AssetInfo][]> = utxos$ - .pipe(map(toListAvailableTokens)) - .pipe( - debounceTime(200), +export const availableTokensData$: Observable<[bigint, AssetInfo][]> = + utxos$.pipe( + map(toListAvailableTokens), switchMap((boxAssets) => combineLatest<[bigint, AssetInfo][]>( boxAssets.map((ba) => from(explorer.getFullTokenInfo(ba.tokenId)).pipe( - map((assetInfo) => { - return [ba.amount, assetInfo as AssetInfo]; - }), + map((assetInfo) => [ba.amount, assetInfo as AssetInfo]), ), ), ).pipe(defaultIfEmpty([])), diff --git a/src/network/ergo/balance/lpBalance.ts b/src/network/ergo/balance/lpBalance.ts index 7b1939b9f..b3ab376af 100644 --- a/src/network/ergo/balance/lpBalance.ts +++ b/src/network/ergo/balance/lpBalance.ts @@ -1,17 +1,10 @@ -import { - combineLatest, - debounceTime, - map, - publishReplay, - refCount, -} from 'rxjs'; +import { map, publishReplay, refCount, zip } from 'rxjs'; import { Balance } from '../../../common/models/Balance'; import { ammPools$ } from '../ammPools/ammPools'; import { availableTokensData$ } from './common'; -export const lpBalance$ = combineLatest([ammPools$, availableTokensData$]).pipe( - debounceTime(200), +export const lpBalance$ = zip([ammPools$, availableTokensData$]).pipe( map(([pools, availableTokensData]) => availableTokensData.filter(([, info]) => pools.some((p) => p.lp.asset.id === info.id), diff --git a/src/network/ergo/balance/networkAssetBalance.ts b/src/network/ergo/balance/networkAssetBalance.ts index 30a0d724f..7ec663e24 100644 --- a/src/network/ergo/balance/networkAssetBalance.ts +++ b/src/network/ergo/balance/networkAssetBalance.ts @@ -9,10 +9,12 @@ import { import { Currency } from '../../../common/models/Currency'; import { explorer } from '../../../services/explorer'; -import { addresses$ } from '../addresses/addresses'; +import { getAddresses } from '../addresses/addresses'; import { networkAsset } from '../networkAsset/networkAsset'; +import { networkContext$ } from '../networkContext/networkContext'; -export const networkAssetBalance$ = addresses$.pipe( +export const networkAssetBalance$ = networkContext$.pipe( + switchMap(() => getAddresses()), switchMap((addresses) => combineLatest( addresses.map((address) => from(explorer.getBalanceByAddress(address))), diff --git a/src/network/ergo/common/tokenLocks.ts b/src/network/ergo/common/tokenLocks.ts index ef0fd082a..3ecb46c26 100644 --- a/src/network/ergo/common/tokenLocks.ts +++ b/src/network/ergo/common/tokenLocks.ts @@ -3,11 +3,13 @@ import { TokenLock } from '@ergolabs/ergo-dex-sdk/build/main/security/entities'; import { map, Observable, publishReplay, refCount, switchMap } from 'rxjs'; import { explorer } from '../../../services/explorer'; -import { addresses$ } from '../addresses/addresses'; +import { getAddresses } from '../addresses/addresses'; +import { networkContext$ } from '../networkContext/networkContext'; export const locksHistory = mkLocksHistory(explorer, mkLockParser()); -export const tokenLocks$: Observable = addresses$.pipe( +export const tokenLocks$: Observable = networkContext$.pipe( + switchMap(() => getAddresses()), switchMap((addresses) => locksHistory.getAllByAddresses(addresses)), map((locks) => locks.filter((l) => l.active)), publishReplay(1), diff --git a/src/network/ergo/common/utxos.ts b/src/network/ergo/common/utxos.ts new file mode 100644 index 000000000..54a3c5cec --- /dev/null +++ b/src/network/ergo/common/utxos.ts @@ -0,0 +1,19 @@ +import { filter, first, publishReplay, refCount, switchMap } from 'rxjs'; + +import { WalletState } from '../../common'; +import { networkContext$ } from '../networkContext/networkContext'; +import { selectedWallet$, selectedWalletState$ } from '../wallets'; + +export const utxos$ = selectedWalletState$.pipe( + filter((state) => state === WalletState.CONNECTED), + switchMap(() => networkContext$), + switchMap(() => + selectedWallet$.pipe( + first(), + filter(Boolean), + switchMap((w) => w.getUtxos()), + ), + ), + publishReplay(1), + refCount(), +); diff --git a/src/network/ergo/index.ts b/src/network/ergo/index.ts index f582afde1..2f391f3f0 100644 --- a/src/network/ergo/index.ts +++ b/src/network/ergo/index.ts @@ -1,8 +1,6 @@ import { Network } from '../common'; import { addresses$ } from './addresses/addresses'; import { ammPools$ } from './ammPools/ammPools'; -import { assets$ } from './assets/assets'; -import { lpAssets$ } from './assets/lpAssets'; import { assetBalance$ } from './balance/assetBalance'; import { lpBalance$ } from './balance/lpBalance'; import { networkAssetBalance$ } from './balance/networkAssetBalance'; @@ -11,13 +9,18 @@ import { networkAsset$, useNetworkAsset } from './networkAsset/networkAsset'; import { positions$ } from './positions/positions'; import { pendingTransactionsCount$ } from './transactions/pendingTransactions'; import { getTxHistory } from './transactions/transactionsHistory'; +import { + connectWallet, + disconnectWallet, + selectedWallet$, + selectedWalletState$, + wallets$, +} from './wallets'; export const ergoNetwork: Network = { addresses$, pendingTransactionsCount$, networkAsset$, - assets$, - lpAssets$, networkAssetBalance$, assetBalance$, lpBalance$, @@ -26,4 +29,9 @@ export const ergoNetwork: Network = { ammPools$, getTxHistory, useNetworkAsset, + connectWallet, + wallets$, + selectedWallet$, + selectedWalletState$, + disconnectWallet, }; diff --git a/src/network/ergo/networkContext/networkContext.ts b/src/network/ergo/networkContext/networkContext.ts index 3609cbd59..493b1bc19 100644 --- a/src/network/ergo/networkContext/networkContext.ts +++ b/src/network/ergo/networkContext/networkContext.ts @@ -1,4 +1,5 @@ import { + distinctUntilKeyChanged, from, map, Observable, @@ -12,11 +13,12 @@ import { explorer } from '../../../services/explorer'; //@ts-ignore export const networkContext$: Observable<{ - height: number; - lastBlockId: number; + readonly height: number; + readonly lastBlockId: number; }> = appTick$.pipe( switchMap(() => from(explorer.getNetworkContext())), map((ctx) => ctx), + distinctUntilKeyChanged('height'), publishReplay(1), refCount(), ); diff --git a/src/network/ergo/positions/positions.ts b/src/network/ergo/positions/positions.ts index 09573058f..3d3b2ef07 100644 --- a/src/network/ergo/positions/positions.ts +++ b/src/network/ergo/positions/positions.ts @@ -1,10 +1,4 @@ -import { - combineLatest, - debounceTime, - map, - publishReplay, - refCount, -} from 'rxjs'; +import { map, publishReplay, refCount, zip } from 'rxjs'; import { Position } from '../../../common/models/Position'; import { ammPools$ } from '../ammPools/ammPools'; @@ -12,13 +6,12 @@ import { lpBalance$ } from '../balance/lpBalance'; import { tokenLocksGroupedByLpAsset$ } from '../common/tokenLocks'; import { networkContext$ } from '../networkContext/networkContext'; -export const positions$ = combineLatest([ +export const positions$ = zip([ ammPools$, lpBalance$, tokenLocksGroupedByLpAsset$, networkContext$, ]).pipe( - debounceTime(200), map( ([ammPools, lpWalletBalance, tokenLocksGroupedByLpAsset, networkContext]) => ammPools diff --git a/src/network/ergo/settings/walletSettings.ts b/src/network/ergo/settings/walletSettings.ts new file mode 100644 index 000000000..3284768e1 --- /dev/null +++ b/src/network/ergo/settings/walletSettings.ts @@ -0,0 +1,25 @@ +import Cookies from 'js-cookie'; + +const WALLET_COOKIE = `ergo-wallet-connected`; + +class WalletSettings { + setConnected(name: string) { + Cookies.set(WALLET_COOKIE, name, { expires: 1 }); + } + + removeConnected() { + Cookies.remove(WALLET_COOKIE); + } + + getConnected() { + return Cookies.get(WALLET_COOKIE); + } + + isConnected(name: string) { + return Cookies.get(WALLET_COOKIE) === name; + } +} + +const walletSettings = new WalletSettings(); + +export { walletSettings }; diff --git a/src/network/ergo/wallets/NautilusWallet.tsx b/src/network/ergo/wallets/NautilusWallet.tsx new file mode 100644 index 000000000..178f32cde --- /dev/null +++ b/src/network/ergo/wallets/NautilusWallet.tsx @@ -0,0 +1,32 @@ +import { ErgoBox, ergoBoxFromProxy } from '@ergolabs/ergo-sdk'; +import React from 'react'; +import { catchError, from, map, Observable, of, tap, throwError } from 'rxjs'; + +import { ReactComponent as NautilusLogo } from '../../../assets/icons/nautilus-logo-icon.svg'; +import { Wallet } from '../../common'; + +const connectWallet = (): Observable => { + if (!ergoConnector?.nautilus) { + return throwError(() => new Error('EXTENSION_NOT_FOUND')); + } + return from(ergoConnector.nautilus.connect()).pipe( + tap(() => (window.nautilus = Object.freeze(new NautilusErgoApi()))), + catchError(() => of(true)), + ); +}; + +const getUtxos = (): Observable => + from(window.nautilus.get_utxos()).pipe( + map((bs) => bs?.map((b) => ergoBoxFromProxy(b))), + map((data) => data ?? []), + ); + +export const NautilusWallet: Wallet = { + name: 'Nautilus', + icon: , + experimental: true, + extensionLink: + 'https://chrome.google.com/webstore/detail/nautilus-wallet/gjlmehlldlphhljhpnlddaodbjjcchai', + connectWallet, + getUtxos, +}; diff --git a/src/network/ergo/wallets/YoroiWallet.tsx b/src/network/ergo/wallets/YoroiWallet.tsx new file mode 100644 index 000000000..df914ba64 --- /dev/null +++ b/src/network/ergo/wallets/YoroiWallet.tsx @@ -0,0 +1,51 @@ +import { ErgoBox, ergoBoxFromProxy } from '@ergolabs/ergo-sdk'; +import Cookies from 'js-cookie'; +import React from 'react'; +import { from, map, Observable, tap, throwError } from 'rxjs'; + +import { ReactComponent as YoroiLogo } from '../../../assets/icons/yoroi-logo-icon.svg'; +import { ArgsProps } from '../../../ergodex-cdk'; +import { Wallet } from '../../common'; + +const MESSAGE_COOKIE = 'YOROI_MESSAGE_COOKIE'; + +const getNotification = (): ArgsProps | undefined => { + if (Cookies.get(MESSAGE_COOKIE)) { + return undefined; + } + Cookies.set(MESSAGE_COOKIE, 'true', { expires: 1 }); + return { + message: 'Yoroi Wallet tip', + description: + 'Keep Yoroi Wallet extension window open, when you use ErgoDEX. So that it will sync faster.', + }; +}; + +const onDisconnect = (): void => Cookies.remove(MESSAGE_COOKIE); + +const connectWallet = (): Observable => { + if (!cardano) { + return throwError(() => new Error('EXTENSION_NOT_FOUND')); + } + return from(window.ergo_request_read_access()).pipe( + tap(() => (window.yoroi = ergo)), + ); +}; + +const getUtxos = (): Observable => + from(window.yoroi.get_utxos()).pipe( + map((bs) => bs?.map((b) => ergoBoxFromProxy(b))), + map((data) => data ?? []), + ); + +export const YoroiWallet: Wallet = { + name: 'Yoroi', + icon: , + experimental: false, + extensionLink: + 'https://chrome.google.com/webstore/detail/yoroi/ffnbelfdoeiohenkjibnmadjiehjhajb', + connectWallet, + getUtxos, + getNotification, + onDisconnect, +}; diff --git a/src/network/ergo/wallets/index.ts b/src/network/ergo/wallets/index.ts new file mode 100644 index 000000000..bfb5c6d5b --- /dev/null +++ b/src/network/ergo/wallets/index.ts @@ -0,0 +1,104 @@ +import { + catchError, + combineLatest, + first, + map, + Observable, + of, + publishReplay, + refCount, + startWith, + Subject, + switchMap, + tap, +} from 'rxjs'; + +import { notification } from '../../../ergodex-cdk'; +import { Wallet, WalletState } from '../../common'; +import { walletSettings } from '../settings/walletSettings'; +import { NautilusWallet } from './NautilusWallet'; +import { YoroiWallet } from './YoroiWallet'; + +const updateSelectedWallet$ = new Subject(); + +export const wallets$ = of([YoroiWallet, NautilusWallet]); + +export const disconnectWallet = (): void => { + selectedWallet$.pipe(first()).subscribe((wallet) => { + if (wallet?.onDisconnect) { + wallet.onDisconnect(); + } + walletSettings.removeConnected(); + location.reload(); + }); +}; + +export const connectWallet = (wallet: Wallet): Observable => { + const connectedWalletName = walletSettings.getConnected(); + walletSettings.removeConnected(); + + if (connectedWalletName) { + return combineLatest([wallet.connectWallet(), selectedWallet$]).pipe( + tap(([, selectedWallet]) => { + if (selectedWallet?.onDisconnect) { + selectedWallet.onDisconnect(); + } + walletSettings.setConnected(wallet.name); + location.reload(); + }), + map(([isConnected]) => isConnected), + ); + } + + updateSelectedWallet$.next(undefined); + return wallet.connectWallet().pipe( + tap(() => { + walletSettings.setConnected(wallet.name); + updateSelectedWallet$.next(wallet.name); + }), + ); +}; + +export const selectedWallet$: Observable = + updateSelectedWallet$.pipe( + startWith(walletSettings.getConnected()), + switchMap((walletName: string | undefined) => { + if (!walletName) { + return of(undefined); + } + return wallets$.pipe( + map((wallets) => wallets.find((w) => w.name === walletName)), + ); + }), + tap((wallet) => { + if (!wallet?.getNotification) { + return; + } + + const notificationArgs = wallet.getNotification(); + if (notificationArgs) { + notification.info(notificationArgs); + } + }), + publishReplay(1), + refCount(), + ); + +export const selectedWalletState$: Observable = + selectedWallet$.pipe( + switchMap((wallet) => { + if (!wallet) { + return of(WalletState.NOT_CONNECTED); + } + + return wallet.connectWallet().pipe( + map((isConnected) => + isConnected ? WalletState.CONNECTED : WalletState.NOT_CONNECTED, + ), + startWith(WalletState.CONNECTING), + catchError(() => of(WalletState.NOT_CONNECTED)), + ); + }), + publishReplay(1), + refCount(), + ); diff --git a/src/pages/Pool/AddLiquidity/AddLiquidity.tsx b/src/pages/Pool/AddLiquidity/AddLiquidity.tsx index 888631874..5b0578354 100644 --- a/src/pages/Pool/AddLiquidity/AddLiquidity.tsx +++ b/src/pages/Pool/AddLiquidity/AddLiquidity.tsx @@ -20,12 +20,13 @@ import { import { getAmmPoolById, getAmmPoolsByAssetPair } from '../../../api/ammPools'; import { useAssetsBalance } from '../../../api/assetBalance'; -import { assets$, getAvailableAssetFor } from '../../../api/assets'; +import { getAvailableAssetFor, tokenAssets$ } from '../../../api/assets'; import { useObservable, useSubject, useSubscription, } from '../../../common/hooks/useObservable'; +import { Currency } from '../../../common/models/Currency'; import { ActionForm } from '../../../components/common/ActionForm/ActionForm'; import { PoolSelect } from '../../../components/common/PoolSelect/PoolSelect'; import { TokenControlFormItem } from '../../../components/common/TokenControl/TokenControl'; @@ -35,8 +36,7 @@ import { Operation, } from '../../../components/ConfirmationModal/ConfirmationModal'; import { Page } from '../../../components/Page/Page'; -import { Flex, Typography } from '../../../ergodex-cdk'; -import { Form, useForm } from '../../../ergodex-cdk/components/Form/NewForm'; +import { Flex, Form, Typography, useForm } from '../../../ergodex-cdk'; import { useMaxTotalFees, useNetworkAsset } from '../../../services/new/core'; import { AddLiquidityConfirmationModal } from './AddLiquidityConfirmationModal/AddLiquidityConfirmationModal'; import { AddLiquidityFormModel } from './FormModel'; @@ -177,8 +177,34 @@ const AddLiquidity = (): JSX.Element => { return undefined; }; - const isAmountNotEntered = (value: AddLiquidityFormModel): boolean => { - return !value.xAmount?.isPositive() || !value.yAmount?.isPositive(); + const isAmountNotEntered = ({ + xAmount, + yAmount, + }: AddLiquidityFormModel): boolean => { + if ( + (!xAmount?.isPositive() && yAmount?.isPositive()) || + (!yAmount?.isPositive() && xAmount?.isPositive()) + ) { + return false; + } + + return !xAmount?.isPositive() || !yAmount?.isPositive(); + }; + + const getMinValueForToken = ({ + xAmount, + yAmount, + x, + y, + pool, + }: AddLiquidityFormModel): Currency | undefined => { + if (!xAmount?.isPositive() && yAmount?.isPositive() && pool) { + return pool.calculateDepositAmount(new Currency(1n, x)).plus(1n); + } + if (!yAmount?.isPositive() && xAmount?.isPositive() && pool) { + return pool.calculateDepositAmount(new Currency(1n, y)); + } + return undefined; }; const isTokensNotSelected = (value: AddLiquidityFormModel): boolean => { @@ -188,7 +214,19 @@ const AddLiquidity = (): JSX.Element => { const addLiquidityAction = (value: Required) => { openConfirmationModal( (next) => { - return ; + return ( + ) => + next( + request.then((tx) => { + resetForm(); + return tx; + }), + ) + } + /> + ); }, Operation.ADD_LIQUIDITY, { @@ -198,12 +236,22 @@ const AddLiquidity = (): JSX.Element => { ); }; + const resetForm = () => + form.patchValue( + { + xAmount: undefined, + yAmount: undefined, + }, + { emitEvent: 'silent' }, + ); + return ( {!poolId || !poolsLoading ? ( { Select Pair - + @@ -249,7 +297,7 @@ const AddLiquidity = (): JSX.Element => { disabled={!isPairSelected} amountName="xAmount" tokenName="x" - assets$={assets$} + assets$={tokenAssets$} /> diff --git a/src/pages/Pool/AddLiquidity/AddLiquidityConfirmationModal/AddLiquidityConfirmationModal.tsx b/src/pages/Pool/AddLiquidity/AddLiquidityConfirmationModal/AddLiquidityConfirmationModal.tsx index c90667321..701e90939 100644 --- a/src/pages/Pool/AddLiquidity/AddLiquidityConfirmationModal/AddLiquidityConfirmationModal.tsx +++ b/src/pages/Pool/AddLiquidity/AddLiquidityConfirmationModal/AddLiquidityConfirmationModal.tsx @@ -9,12 +9,9 @@ import { InfoTooltip } from '../../../../components/InfoTooltip/InfoTooltip'; import { PageSection } from '../../../../components/Page/PageSection/PageSection'; import { useSettings } from '../../../../context'; import { Button, Flex, Modal, Typography } from '../../../../ergodex-cdk'; +import { utxos$ } from '../../../../network/ergo/common/utxos'; import { explorer } from '../../../../services/explorer'; -import { - useMinExFee, - useMinTotalFees, - utxos$, -} from '../../../../services/new/core'; +import { useMinExFee, useMinTotalFees } from '../../../../services/new/core'; import { poolActions } from '../../../../services/poolActions'; import { submitTx } from '../../../../services/yoroi'; import { makeTarget } from '../../../../utils/ammMath'; @@ -47,8 +44,12 @@ const AddLiquidityConfirmationModal: FC = ({ const actions = poolActions(pool['pool']); - const inputX = pool['pool'].x.withAmount(xAmount.amount); - const inputY = pool['pool'].y.withAmount(yAmount.amount); + const inputX = pool['pool'].x.withAmount( + xAmount.asset.id === pool.x.asset.id ? xAmount.amount : yAmount.amount, + ); + const inputY = pool['pool'].y.withAmount( + yAmount.asset.id === pool.y.asset.id ? yAmount.amount : xAmount.amount, + ); const target = makeTarget( [inputX, inputY], @@ -115,7 +116,7 @@ const AddLiquidityConfirmationModal: FC = ({ Execution Fee: - {minExFee.toString()} + {minExFee.toCurrencyString()}
@@ -133,7 +134,7 @@ const AddLiquidityConfirmationModal: FC = ({ - {totalFees.toString()} + {totalFees.toCurrencyString()} diff --git a/src/pages/Pool/LockLiquidity/LockLiquidity.tsx b/src/pages/Pool/LockLiquidity/LockLiquidity.tsx index fdbf4fdbd..863303dbc 100644 --- a/src/pages/Pool/LockLiquidity/LockLiquidity.tsx +++ b/src/pages/Pool/LockLiquidity/LockLiquidity.tsx @@ -21,12 +21,15 @@ import { Page } from '../../../components/Page/Page'; import { PageHeader } from '../../../components/Page/PageHeader/PageHeader'; import { PageSection } from '../../../components/Page/PageSection/PageSection'; import { SubmitButton } from '../../../components/SubmitButton/SubmitButton'; -import { Alert, Animation, Flex, LockOutlined } from '../../../ergodex-cdk'; import { + Alert, + Animation, + Flex, Form, FormGroup, + LockOutlined, useForm, -} from '../../../ergodex-cdk/components/Form/NewForm'; +} from '../../../ergodex-cdk'; import { LiquidityDatePicker } from '../components/LockLiquidityDatePicker/LiquidityDatePicker'; import { LockLiquidityConfirmationModal } from './LockLiquidityConfirmationModal/LockLiquidityConfirmationModal'; import { LockLiquidityModel } from './LockLiquidityModel'; diff --git a/src/pages/Pool/LockLiquidity/LockLiquidityConfirmationModal/LockLiquidityConfirmationModal.tsx b/src/pages/Pool/LockLiquidity/LockLiquidityConfirmationModal/LockLiquidityConfirmationModal.tsx index 4b17543ff..00d1c5cbd 100644 --- a/src/pages/Pool/LockLiquidity/LockLiquidityConfirmationModal/LockLiquidityConfirmationModal.tsx +++ b/src/pages/Pool/LockLiquidity/LockLiquidityConfirmationModal/LockLiquidityConfirmationModal.tsx @@ -22,9 +22,10 @@ import { FormFeesSection } from '../../../../components/common/FormView/FormFees import { FormPairSection } from '../../../../components/common/FormView/FormPairSection/FormPairSection'; import { useSettings } from '../../../../context'; import { Button, Checkbox, Flex, Modal } from '../../../../ergodex-cdk'; +import { utxos$ } from '../../../../network/ergo/common/utxos'; import { mainnetTxAssembler } from '../../../../services/defaultTxAssembler'; import { explorer } from '../../../../services/explorer'; -import { useNetworkAsset, utxos$ } from '../../../../services/new/core'; +import { useNetworkAsset } from '../../../../services/new/core'; import { submitTx } from '../../../../services/yoroi'; import yoroiProver from '../../../../services/yoroi/prover'; import { makeTarget } from '../../../../utils/ammMath'; @@ -122,9 +123,8 @@ const LockLiquidityConfirmationModal: React.FC - I understand that I'm locking{' '} - {lpAsset.toString({ suffix: false })} LP-tokens, which - is {percent}% of my{' '} + I understand that I'm locking {lpAsset.toAmount()}{' '} + LP-tokens, which is {percent}% of my{' '} {`${xAsset.asset.name}/${yAsset.asset.name}`} liquidity position, for a period of{' '} {getLockingPeriodString(timelock)} (until{' '} diff --git a/src/pages/Pool/Pool.tsx b/src/pages/Pool/Pool.tsx index d44cbe02e..5f0aa88b4 100644 --- a/src/pages/Pool/Pool.tsx +++ b/src/pages/Pool/Pool.tsx @@ -4,13 +4,14 @@ import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { ammPools$ } from '../../api/ammPools'; +import { useAssetsBalance } from '../../api/assetBalance'; import { positions$ } from '../../api/positions'; +import { isWalletSetuped$ } from '../../api/wallets'; import { useObservable } from '../../common/hooks/useObservable'; import { ConnectWalletButton } from '../../components/common/ConnectWalletButton/ConnectWalletButton'; import { Page } from '../../components/Page/Page'; import { DownOutlined, Dropdown, Flex, Menu, Tabs } from '../../ergodex-cdk'; import { useQuery } from '../../hooks/useQuery'; -import { isWalletLoading$, isWalletSetuped$ } from '../../services/new/core'; import { EmptyPositionsWrapper } from './components/EmptyPositionsWrapper/EmptyPositionsWrapper'; import { LiquidityPositionsList } from './components/LiquidityPositionsList/LiquidityPositionsList'; import { LockListView } from './components/LocksList/LockListView'; @@ -68,7 +69,7 @@ const PoolPageWrapper: React.FC = ({ const Pool = (): JSX.Element => { const [isWalletConnected] = useObservable(isWalletSetuped$, [], false); - const [isWalletLoading] = useObservable(isWalletLoading$); + const [, isBalanceLoading] = useAssetsBalance(); const history = useHistory(); const query = useQuery(); @@ -106,7 +107,7 @@ const Pool = (): JSX.Element => { {isWalletConnected ? ( p.pool)} - loading={isWalletLoading || isPositionLoading} + loading={isBalanceLoading || isPositionLoading} /> ) : ( diff --git a/src/pages/Pool/RelockLiquidity/RelockLiquidity.tsx b/src/pages/Pool/RelockLiquidity/RelockLiquidity.tsx index 52d70b834..34cbffac2 100644 --- a/src/pages/Pool/RelockLiquidity/RelockLiquidity.tsx +++ b/src/pages/Pool/RelockLiquidity/RelockLiquidity.tsx @@ -22,15 +22,13 @@ import { PageSection } from '../../../components/Page/PageSection/PageSection'; import { Animation, Flex, + Form, + FormGroup, List, Skeleton, Typography, -} from '../../../ergodex-cdk'; -import { - Form, - FormGroup, useForm, -} from '../../../ergodex-cdk/components/Form/NewForm'; +} from '../../../ergodex-cdk'; import { LockedPositionItem } from '../components/LockedPositionItem/LockedPositionItem'; import { LiquidityDatePicker } from '../components/LockLiquidityDatePicker/LiquidityDatePicker'; import { RelockLiquidityConfirmationModal } from './RelockLiquidityConfirmationModal/RelockLiquidityConfirmationModal'; diff --git a/src/pages/Pool/RelockLiquidity/RelockLiquidityConfirmationModal/RelockLiquidityConfirmationModal.tsx b/src/pages/Pool/RelockLiquidity/RelockLiquidityConfirmationModal/RelockLiquidityConfirmationModal.tsx index 5340a8cb4..74c87dd25 100644 --- a/src/pages/Pool/RelockLiquidity/RelockLiquidityConfirmationModal/RelockLiquidityConfirmationModal.tsx +++ b/src/pages/Pool/RelockLiquidity/RelockLiquidityConfirmationModal/RelockLiquidityConfirmationModal.tsx @@ -28,10 +28,11 @@ import { Modal, Typography, } from '../../../../ergodex-cdk'; +import { utxos$ } from '../../../../network/ergo/common/utxos'; import { mainnetTxAssembler } from '../../../../services/defaultTxAssembler'; import { explorer } from '../../../../services/explorer'; import { lockParser } from '../../../../services/locker/parser'; -import { useNetworkAsset, utxos$ } from '../../../../services/new/core'; +import { useNetworkAsset } from '../../../../services/new/core'; import { formatToInt } from '../../../../services/number'; import { submitTx } from '../../../../services/yoroi'; import yoroiProver from '../../../../services/yoroi/prover'; @@ -169,10 +170,8 @@ const RelockLiquidityConfirmationModal: FC I understand that I’m relocking{' '} - - {formatToInt(lockedPosition.lp.toString({ suffix: false }))} - {' '} - LP-tokens of my{' '} + {formatToInt(lockedPosition.lp.toAmount())} LP-tokens + of my{' '} {`${lockedPosition.x.asset.name}/${lockedPosition.y.asset.name}`}{' '} position until{' '} {relocktime.toLocaleString(DateTime.DATE_FULL)} (or{' '} diff --git a/src/pages/Pool/RemoveLiquidity/RemoveLiquidity.tsx b/src/pages/Pool/RemoveLiquidity/RemoveLiquidity.tsx index 8a74fa238..3b3f708ad 100644 --- a/src/pages/Pool/RemoveLiquidity/RemoveLiquidity.tsx +++ b/src/pages/Pool/RemoveLiquidity/RemoveLiquidity.tsx @@ -21,12 +21,7 @@ import { Page } from '../../../components/Page/Page'; import { PageHeader } from '../../../components/Page/PageHeader/PageHeader'; import { PageSection } from '../../../components/Page/PageSection/PageSection'; import { SubmitButton } from '../../../components/SubmitButton/SubmitButton'; -import { Flex, Skeleton } from '../../../ergodex-cdk'; -import { - Form, - FormGroup, - useForm, -} from '../../../ergodex-cdk/components/Form/NewForm'; +import { Flex, Form, FormGroup, Skeleton, useForm } from '../../../ergodex-cdk'; import { RemoveLiquidityConfirmationModal } from './RemoveLiquidityConfirmationModal/RemoveLiquidityConfirmationModal'; interface RemoveFormModel { diff --git a/src/pages/Pool/RemoveLiquidity/RemoveLiquidityConfirmationModal/RemoveLiquidityConfirmationModal.tsx b/src/pages/Pool/RemoveLiquidity/RemoveLiquidityConfirmationModal/RemoveLiquidityConfirmationModal.tsx index 0f7ef0df1..329b9edea 100644 --- a/src/pages/Pool/RemoveLiquidity/RemoveLiquidityConfirmationModal/RemoveLiquidityConfirmationModal.tsx +++ b/src/pages/Pool/RemoveLiquidity/RemoveLiquidityConfirmationModal/RemoveLiquidityConfirmationModal.tsx @@ -10,12 +10,9 @@ import { FormFeesSection } from '../../../../components/common/FormView/FormFees import { FormPairSection } from '../../../../components/common/FormView/FormPairSection/FormPairSection'; import { useSettings } from '../../../../context'; import { Box, Button, Flex, Modal } from '../../../../ergodex-cdk'; +import { utxos$ } from '../../../../network/ergo/common/utxos'; import { explorer } from '../../../../services/explorer'; -import { - useMinExFee, - useMinTotalFees, - utxos$, -} from '../../../../services/new/core'; +import { useMinExFee, useMinTotalFees } from '../../../../services/new/core'; import { poolActions } from '../../../../services/poolActions'; import { submitTx } from '../../../../services/yoroi'; import { makeTarget } from '../../../../utils/ammMath'; diff --git a/src/pages/Pool/WithdrawalLiquidity/WithdrawalLiquidity.tsx b/src/pages/Pool/WithdrawalLiquidity/WithdrawalLiquidity.tsx index 71f3eda09..e79c4d0d0 100644 --- a/src/pages/Pool/WithdrawalLiquidity/WithdrawalLiquidity.tsx +++ b/src/pages/Pool/WithdrawalLiquidity/WithdrawalLiquidity.tsx @@ -16,12 +16,15 @@ import { } from '../../../components/OperationForm/OperationForm'; import { Page } from '../../../components/Page/Page'; import { PageHeader } from '../../../components/Page/PageHeader/PageHeader'; -import { Flex, List, Skeleton, Typography } from '../../../ergodex-cdk'; import { + Flex, Form, FormGroup, + List, + Skeleton, + Typography, useForm, -} from '../../../ergodex-cdk/components/Form/NewForm'; +} from '../../../ergodex-cdk'; import { LockedPositionItem } from '../components/LockedPositionItem/LockedPositionItem'; import { WithdrawalLiquidityConfirmationModal } from './WithdrawalLiquidityConfirmationModal/WithdrawalLiquidityConfirmationModal'; diff --git a/src/pages/Pool/components/LiquidityPositionsList/LiquidityPositionsList.tsx b/src/pages/Pool/components/LiquidityPositionsList/LiquidityPositionsList.tsx index 129d484b6..28ef24bc8 100644 --- a/src/pages/Pool/components/LiquidityPositionsList/LiquidityPositionsList.tsx +++ b/src/pages/Pool/components/LiquidityPositionsList/LiquidityPositionsList.tsx @@ -3,10 +3,10 @@ import { isEmpty } from 'lodash'; import React, { FC } from 'react'; import { useHistory } from 'react-router-dom'; +import { isWalletSetuped$ } from '../../../../api/wallets'; import { useObservable } from '../../../../common/hooks/useObservable'; import { AmmPool } from '../../../../common/models/AmmPool'; import { Button, Flex, List, PlusOutlined } from '../../../../ergodex-cdk'; -import { isWalletSetuped$ } from '../../../../services/new/core'; import { EmptyPositionsWrapper } from '../EmptyPositionsWrapper/EmptyPositionsWrapper'; import { PositionListLoader } from '../PositionListLoader/PositionListLoader'; import { LiquidityPositionsItem } from './LiquidityPositionsItem/LiquidityPositionsItem'; diff --git a/src/pages/Pool/components/LockedPositionItem/LockedPositionItem.tsx b/src/pages/Pool/components/LockedPositionItem/LockedPositionItem.tsx index 9948fb8bd..5771f3b23 100644 --- a/src/pages/Pool/components/LockedPositionItem/LockedPositionItem.tsx +++ b/src/pages/Pool/components/LockedPositionItem/LockedPositionItem.tsx @@ -46,7 +46,7 @@ export const LockedPositionItem: FC = ({ - {xAssetAmount.toString({ suffix: false })} + {xAssetAmount.toAmount()} @@ -65,7 +65,7 @@ export const LockedPositionItem: FC = ({ - {yAssetAmount.toString({ suffix: false })} + {yAssetAmount.toAmount()} diff --git a/src/pages/Pool/components/LocksList/LockListView.tsx b/src/pages/Pool/components/LocksList/LockListView.tsx index e830997ab..c241545e6 100644 --- a/src/pages/Pool/components/LocksList/LockListView.tsx +++ b/src/pages/Pool/components/LocksList/LockListView.tsx @@ -38,7 +38,7 @@ const LockItemView: FC = ({ position }) => { - {position.lockedX.toString({ suffix: false })} + {position.lockedX.toAmount()} @@ -57,7 +57,7 @@ const LockItemView: FC = ({ position }) => { - {position.lockedY.toString({ suffix: false })} + {position.lockedY.toAmount()} @@ -85,9 +85,7 @@ const LockItemView: FC = ({ position }) => { - {position.withdrawableLockedX.toString({ - suffix: false, - })} + {position.withdrawableLockedX.toAmount()} @@ -106,9 +104,7 @@ const LockItemView: FC = ({ position }) => { - {position.withdrawableLockedY.toString({ - suffix: false, - })} + {position.withdrawableLockedY.toAmount()} diff --git a/src/pages/PoolOverview/LocksAnalytic.ts b/src/pages/PoolOverview/AmmPoolConfidenceAnalytic.ts similarity index 81% rename from src/pages/PoolOverview/LocksAnalytic.ts rename to src/pages/PoolOverview/AmmPoolConfidenceAnalytic.ts index 93aa77cb4..5762ff664 100644 --- a/src/pages/PoolOverview/LocksAnalytic.ts +++ b/src/pages/PoolOverview/AmmPoolConfidenceAnalytic.ts @@ -12,7 +12,6 @@ import { AmmPoolLocksAnalytic, getPoolLocksAnalyticsById, } from '../../services/new/analytics'; -import { math } from '../../utils/math'; const MIN_RELEVANT_PCT_VALUE = 0.01; @@ -58,15 +57,6 @@ export class LocksGroup { }); } - @cache - get share(): Currency { - const lpAmount = this.lockedLp.toString({ suffix: false }); - const poolLiquidityAmount = this.pool.lp.toString({ suffix: false }); - return math.evaluate!( - `${lpAmount} / (${poolLiquidityAmount}) * 100`, - ).toFixed(2); - } - constructor( public readonly pool: AmmPool, private locksAnalytic: AmmPoolLocksAnalytic[], @@ -90,15 +80,6 @@ export class AmmPoolConfidenceAnalytic { ); } - @cache - get share(): Currency { - const lpAmount = this.lockedLp.toString({ suffix: false }); - const poolLiquidityAmount = this.pool.lp.toString({ suffix: false }); - return math.evaluate!( - `${lpAmount} / (${poolLiquidityAmount}) * 100`, - ).toFixed(2); - } - @cache get lockedX(): Currency { const [lockedX] = this.pool.shares(this.lockedLp); @@ -120,20 +101,16 @@ export class AmmPoolConfidenceAnalytic { ) { this.locksGroups = Object.values( locksAnalytic.reduce(this.groupByDeadline, {}), - ).map( - (items) => - new LocksGroup( - this.pool, - items.filter((item) => item.percent >= MIN_RELEVANT_PCT_VALUE), - networkHeight, - ), - ); + ).map((items) => new LocksGroup(this.pool, items, networkHeight)); } private groupByDeadline( acc: Dictionary, lockAnalytic: AmmPoolLocksAnalytic, ) { + if (lockAnalytic.percent < MIN_RELEVANT_PCT_VALUE) { + return acc; + } if (!acc[lockAnalytic.deadline]) { acc[lockAnalytic.deadline] = []; } diff --git a/src/pages/PoolOverview/LockLiquidityChart/AnalyticOverview/AnalyticOverview.tsx b/src/pages/PoolOverview/LockLiquidityChart/AnalyticOverview/AnalyticOverview.tsx index e07880a53..3957b3e14 100644 --- a/src/pages/PoolOverview/LockLiquidityChart/AnalyticOverview/AnalyticOverview.tsx +++ b/src/pages/PoolOverview/LockLiquidityChart/AnalyticOverview/AnalyticOverview.tsx @@ -4,7 +4,11 @@ import React, { FC } from 'react'; import { Currency } from '../../../../common/models/Currency'; import { TokenIcon } from '../../../../components/TokenIcon/TokenIcon'; import { Flex, Typography } from '../../../../ergodex-cdk'; -import { AmmPoolConfidenceAnalytic, LocksGroup } from '../../LocksAnalytic'; +import { formatToPercent } from '../../../../services/number'; +import { + AmmPoolConfidenceAnalytic, + LocksGroup, +} from '../../AmmPoolConfidenceAnalytic'; export interface AnalyticOverviewProps { data: LocksGroup | AmmPoolConfidenceAnalytic; @@ -19,7 +23,7 @@ const AmountOverview: FC<{ currency: Currency }> = ({ currency }) => ( {currency.asset.name} - {currency.toString({ suffix: false })} + {currency.toAmount()} ); @@ -47,7 +51,9 @@ export const AnalyticOverview: FC = ({ data }) => { Share - {data.share} + + {formatToPercent(data.lockedPercent)} + diff --git a/src/pages/PoolOverview/LockLiquidityChart/LockLiquidityChart.tsx b/src/pages/PoolOverview/LockLiquidityChart/LockLiquidityChart.tsx index 0e1713d77..936598c3d 100644 --- a/src/pages/PoolOverview/LockLiquidityChart/LockLiquidityChart.tsx +++ b/src/pages/PoolOverview/LockLiquidityChart/LockLiquidityChart.tsx @@ -4,7 +4,10 @@ import React, { FC, useState } from 'react'; import { Bar, BarChart, Label, Tooltip, XAxis, YAxis } from 'recharts'; import { Flex, Progress } from '../../../ergodex-cdk'; -import { AmmPoolConfidenceAnalytic, LocksGroup } from '../LocksAnalytic'; +import { + AmmPoolConfidenceAnalytic, + LocksGroup, +} from '../AmmPoolConfidenceAnalytic'; import { AnalyticOverview } from './AnalyticOverview/AnalyticOverview'; import { ChartContainer } from './ChartContainer/ChartContainer'; diff --git a/src/pages/PoolOverview/PoolOverview.tsx b/src/pages/PoolOverview/PoolOverview.tsx index 70102d4f3..9d1142db5 100644 --- a/src/pages/PoolOverview/PoolOverview.tsx +++ b/src/pages/PoolOverview/PoolOverview.tsx @@ -21,8 +21,8 @@ import { Skeleton, Typography, } from '../../ergodex-cdk'; +import { getAmmPoolConfidenceAnalyticByAmmPoolId } from './AmmPoolConfidenceAnalytic'; import { LockLiquidityChart } from './LockLiquidityChart/LockLiquidityChart'; -import { getAmmPoolConfidenceAnalyticByAmmPoolId } from './LocksAnalytic'; import { PoolFeeTag } from './PoolFeeTag/PoolFeeTag'; import { PoolRatio } from './PoolRatio/PoolRatio'; diff --git a/src/pages/PoolOverview/PoolRatio/PoolRatio.tsx b/src/pages/PoolOverview/PoolRatio/PoolRatio.tsx index 5b84f07f1..af04c12bd 100644 --- a/src/pages/PoolOverview/PoolRatio/PoolRatio.tsx +++ b/src/pages/PoolOverview/PoolRatio/PoolRatio.tsx @@ -19,9 +19,7 @@ export const PoolRatio: FC = ({ ammPool, ratioOf }) => { - - {price.toString({ suffix: false })} - + {price.toString()} diff --git a/src/pages/Swap/OperationSettings/NitroInput/NitroInput.less b/src/pages/Swap/OperationSettings/NitroInput/NitroInput.less new file mode 100644 index 000000000..2eb35d1a3 --- /dev/null +++ b/src/pages/Swap/OperationSettings/NitroInput/NitroInput.less @@ -0,0 +1,4 @@ +.nitro-execution-fee { + font-size: 8px !important; + line-height: 12px !important; +} diff --git a/src/pages/Swap/OperationSettings/NitroInput/NitroInput.tsx b/src/pages/Swap/OperationSettings/NitroInput/NitroInput.tsx index 53e75a7e6..8e20f4b70 100644 --- a/src/pages/Swap/OperationSettings/NitroInput/NitroInput.tsx +++ b/src/pages/Swap/OperationSettings/NitroInput/NitroInput.tsx @@ -1,17 +1,29 @@ +import './NitroInput.less'; + import React, { ChangeEvent, FC } from 'react'; import { MIN_NITRO } from '../../../../common/constants/erg'; -import { Alert, Button, Flex, Input } from '../../../../ergodex-cdk'; -import { Control } from '../../../../ergodex-cdk/components/Form/NewForm'; +import { + Alert, + Animation, + Button, + Control, + Flex, + Input, + Typography, +} from '../../../../ergodex-cdk'; +import { useMaxExFee, useMinExFee } from '../../../../services/new/core'; export type NitroInputProps = Control; export const NitroInput: FC = ({ onChange, value, - errorMessage, - invalid, + message, + state, }) => { + const minExFee = useMinExFee(); + const maxExFee = useMaxExFee(); const isMinimumNitro = value === MIN_NITRO; const handleClickNitroAuto = () => { @@ -28,9 +40,9 @@ export const NitroInput: FC = ({ return ( - - - + + + - + + + + Execution Fee Range + + + {minExFee.toAmount()} - {maxExFee.toAmount()}{' '} + {maxExFee.asset.name} + + - {errorMessage && } + + + ); }; diff --git a/src/pages/Swap/OperationSettings/OperationSettings.tsx b/src/pages/Swap/OperationSettings/OperationSettings.tsx index df7d1b808..edb4e1f60 100644 --- a/src/pages/Swap/OperationSettings/OperationSettings.tsx +++ b/src/pages/Swap/OperationSettings/OperationSettings.tsx @@ -9,17 +9,15 @@ import { useSettings } from '../../../context'; import { Box, Button, + CheckFn, Flex, + Form, + Messages, Popover, SettingOutlined, Typography, -} from '../../../ergodex-cdk'; -import { - CheckFn, - Form, - Messages, useForm, -} from '../../../ergodex-cdk/components/Form/NewForm'; +} from '../../../ergodex-cdk'; import { NitroInput } from './NitroInput/NitroInput'; import { SlippageInput } from './SlippageInput/SlippageInput'; @@ -41,17 +39,14 @@ const errorMessages: Messages = { }, }; -const slippageCheck: CheckFn = (value) => { - return value > 10 ? 'transactionFrontrun' : undefined; -}; +const slippageCheck: CheckFn = (value) => + value > 10 ? 'transactionFrontrun' : undefined; -const slippageTxFailCheck: CheckFn = (value) => { - return value < defaultSlippage ? 'transactionMayFail' : undefined; -}; +const slippageTxFailCheck: CheckFn = (value) => + value < defaultSlippage ? 'transactionMayFail' : undefined; -const nitroCheck: CheckFn = (value) => { - return value < MIN_NITRO ? 'minNitro' : undefined; -}; +const nitroCheck: CheckFn = (value) => + isNaN(value) || value < MIN_NITRO ? 'minNitro' : undefined; const OperationSettings = (): JSX.Element => { const [settings, setSettings] = useSettings(); @@ -118,10 +113,10 @@ const OperationSettings = (): JSX.Element => { - {({ onChange, value, withWarnings, warningMessage }) => ( + {({ onChange, value, state, message }) => ( @@ -147,10 +142,10 @@ const OperationSettings = (): JSX.Element => { - {({ onChange, value, invalid, errorMessage }) => ( + {({ onChange, value, state, message }) => ( diff --git a/src/pages/Swap/OperationSettings/SlippageInput/SlippageInput.tsx b/src/pages/Swap/OperationSettings/SlippageInput/SlippageInput.tsx index 634a12eee..ce73f1be5 100644 --- a/src/pages/Swap/OperationSettings/SlippageInput/SlippageInput.tsx +++ b/src/pages/Swap/OperationSettings/SlippageInput/SlippageInput.tsx @@ -7,26 +7,27 @@ import { SlippageMax, SlippageMin, } from '../../../../common/constants/settings'; -import { Alert, Button, Flex, Input } from '../../../../ergodex-cdk'; -import { Control } from '../../../../ergodex-cdk/components/Form/NewForm'; +import { + Alert, + Animation, + Box, + Button, + Control, + Flex, + Input, +} from '../../../../ergodex-cdk'; export type NitroInputProps = Control; -const SLIPPAGE_OPTIONS = { - '1': 1, - '3': defaultSlippage, - '7': 7, -}; +const SLIPPAGE_OPTIONS = [1, defaultSlippage, 7]; export const SlippageInput: FC = ({ value, onChange, - warningMessage, - withWarnings, + state, + message, }) => { - const isCustomSlippage = !Object.values(SLIPPAGE_OPTIONS).some( - (val) => val === value, - ); + const isCustomSlippage = !SLIPPAGE_OPTIONS.some((val) => val === value); const handleClickSlippage = (percentage: number) => { if (onChange) { @@ -42,15 +43,14 @@ export const SlippageInput: FC = ({ return ( - - - {Object.values(SLIPPAGE_OPTIONS) - .sort() - .map((val, index) => ( + + + + {SLIPPAGE_OPTIONS.sort().map((val, index) => ( ))} - - - - + + + + + - {warningMessage && ( - - )} + + + ); }; diff --git a/src/pages/Swap/Ratio/Ratio.less b/src/pages/Swap/RatioView/Ratio.less similarity index 100% rename from src/pages/Swap/Ratio/Ratio.less rename to src/pages/Swap/RatioView/Ratio.less diff --git a/src/pages/Swap/Ratio/Ratio.tsx b/src/pages/Swap/RatioView/RatioView.tsx similarity index 81% rename from src/pages/Swap/Ratio/Ratio.tsx rename to src/pages/Swap/RatioView/RatioView.tsx index 47d90c3dd..92a9775cc 100644 --- a/src/pages/Swap/Ratio/Ratio.tsx +++ b/src/pages/Swap/RatioView/RatioView.tsx @@ -5,15 +5,15 @@ import { debounceTime, map } from 'rxjs'; import { useObservable } from '../../../common/hooks/useObservable'; import { Currency } from '../../../common/models/Currency'; -import { Animation, Typography } from '../../../ergodex-cdk'; -import { FormGroup } from '../../../ergodex-cdk/components/Form/NewForm'; +import { Ratio } from '../../../common/models/Ratio'; +import { Animation, FormGroup, Typography } from '../../../ergodex-cdk'; import { SwapFormModel } from '../SwapFormModel'; const calculateOutputPrice = ({ fromAmount, fromAsset, pool, -}: Required): Currency => { +}: Required): Ratio => { if (fromAmount?.isPositive()) { return pool.calculateOutputPrice(fromAmount); } else { @@ -25,7 +25,7 @@ const calculateInputPrice = ({ toAmount, toAsset, pool, -}: Required): Currency => { +}: Required): Ratio => { if (toAmount?.isPositive()) { return pool.calculateInputPrice(toAmount); } else { @@ -33,7 +33,7 @@ const calculateInputPrice = ({ } }; -export const Ratio: FC<{ form: FormGroup }> = ({ form }) => { +export const RatioView: FC<{ form: FormGroup }> = ({ form }) => { const [reversedRatio, setReversedRatio] = useState(false); const [ratio] = useObservable( form.valueChangesWithSilent$.pipe( @@ -42,6 +42,7 @@ export const Ratio: FC<{ form: FormGroup }> = ({ form }) => { if (!value.pool || !value.fromAsset) { return undefined; } + if (reversedRatio) { return calculateInputPrice(value as Required); } else { @@ -50,8 +51,12 @@ export const Ratio: FC<{ form: FormGroup }> = ({ form }) => { }), map((price) => reversedRatio - ? `1 ${form.value.toAsset?.name} = ${price?.toString()}` - : `1 ${form.value.fromAsset?.name} = ${price?.toString()}`, + ? `1 ${form.value.toAsset?.name} = ${price?.toString()} ${ + price?.asset.name + }` + : `1 ${form.value.fromAsset?.name} = ${price?.toString()} ${ + price?.asset.name + }`, ), ), [form, reversedRatio], diff --git a/src/pages/Swap/Swap.tsx b/src/pages/Swap/Swap.tsx index c188b0a0b..0a862e5bd 100644 --- a/src/pages/Swap/Swap.tsx +++ b/src/pages/Swap/Swap.tsx @@ -10,7 +10,6 @@ import { combineLatest, debounceTime, distinctUntilChanged, - filter, map, Observable, of, @@ -21,9 +20,10 @@ import { import { getAmmPoolsByAssetPair } from '../../api/ammPools'; import { useAssetsBalance } from '../../api/assetBalance'; -import { assets$, getAvailableAssetFor } from '../../api/assets'; +import { getAvailableAssetFor, tokenAssets$ } from '../../api/assets'; import { useSubscription } from '../../common/hooks/useObservable'; import { AmmPool } from '../../common/models/AmmPool'; +import { Currency } from '../../common/models/Currency'; import { END_TIMER_DATE, LOCKED_TOKEN_ID, @@ -35,17 +35,22 @@ import { Operation, } from '../../components/ConfirmationModal/ConfirmationModal'; import { Page } from '../../components/Page/Page'; -import { Button, Flex, SwapOutlined, Typography } from '../../ergodex-cdk'; -import { useForm } from '../../ergodex-cdk/components/Form/NewForm'; +import { + Button, + Flex, + SwapOutlined, + Typography, + useForm, +} from '../../ergodex-cdk'; import { useMaxTotalFees, useNetworkAsset } from '../../services/new/core'; import { OperationSettings } from './OperationSettings/OperationSettings'; -import { Ratio } from './Ratio/Ratio'; +import { RatioView } from './RatioView/RatioView'; import { SwapConfirmationModal } from './SwapConfirmationModal/SwapConfirmationModal'; import { SwapFormModel } from './SwapFormModel'; import { SwapTooltip } from './SwapTooltip/SwapTooltip'; const getToAssets = (fromAsset?: string) => - fromAsset ? getAvailableAssetFor(fromAsset) : assets$; + fromAsset ? getAvailableAssetFor(fromAsset) : tokenAssets$; const isAssetsPairEquals = ( [prevFrom, prevTo]: [AssetInfo | undefined, AssetInfo | undefined], @@ -109,8 +114,43 @@ export const Swap = (): JSX.Element => { return undefined; }; - const isAmountNotEntered = ({ toAmount, fromAmount }: SwapFormModel) => - !fromAmount?.isPositive() || !toAmount?.isPositive(); + const isAmountNotEntered = ({ toAmount, fromAmount }: SwapFormModel) => { + if ( + (!fromAmount?.isPositive() && toAmount?.isPositive()) || + (!toAmount?.isPositive() && fromAmount?.isPositive()) + ) { + return false; + } + + return !fromAmount?.isPositive() || !toAmount?.isPositive(); + }; + + const getMinValueForToken = ({ + toAmount, + fromAmount, + fromAsset, + toAsset, + pool, + }: SwapFormModel): Currency | undefined => { + if ( + !fromAmount?.isPositive() && + toAmount && + toAmount.isPositive() && + pool && + toAmount.gt(pool.getAssetAmount(toAmount.asset)) + ) { + return undefined; + } + + if (!fromAmount?.isPositive() && toAmount?.isPositive() && pool) { + // TODO: FIX_ERGOLABS_SDK_COMPUTING + return pool.calculateOutputAmount(new Currency(1n, fromAsset)).plus(1n); + } + if (!toAmount?.isPositive() && fromAmount?.isPositive() && pool) { + return pool.calculateInputAmount(new Currency(1n, toAsset)); + } + return undefined; + }; const isTokensNotSelected = ({ toAsset, fromAsset }: SwapFormModel) => !toAsset || !fromAsset; @@ -125,7 +165,19 @@ export const Swap = (): JSX.Element => { const submitSwap = (value: Required) => { openConfirmationModal( (next) => { - return ; + return ( + ) => + next( + request.then((tx) => { + resetForm(); + return tx; + }), + ) + } + /> + ); }, Operation.SWAP, { @@ -135,6 +187,15 @@ export const Swap = (): JSX.Element => { ); }; + const resetForm = () => + form.patchValue( + { fromAmount: undefined, toAmount: undefined }, + { emitEvent: 'silent' }, + ); + + const handleMaxButtonClick = (balance: Currency) => + balance.asset.id === networkAsset.id ? balance.minus(totalFees) : balance; + const isLiquidityInsufficient = ({ toAmount, pool }: SwapFormModel) => { if (!toAmount?.isPositive() || !pool) { return false; @@ -149,11 +210,9 @@ export const Swap = (): JSX.Element => { useSubscription( combineLatest([ form.controls.fromAsset.valueChangesWithSilent$.pipe( - filter(Boolean), distinctUntilChanged(), ), form.controls.toAsset.valueChangesWithSilent$.pipe( - filter(Boolean), distinctUntilChanged(), ), ]).pipe( @@ -217,11 +276,7 @@ export const Swap = (): JSX.Element => { ); useSubscription( - combineLatest([ - form.controls.toAsset.valueChanges$, - form.controls.fromAsset.valueChanges$, - form.controls.pool.valueChanges$, - ]).pipe(debounceTime(200)), + form.controls.pool.valueChanges$, () => { const { fromAmount, toAmount, pool } = form.value; @@ -268,6 +323,7 @@ export const Swap = (): JSX.Element => { getInsufficientTokenNameForFee={getInsufficientTokenNameForFee} getInsufficientTokenNameForTx={getInsufficientTokenNameForTx} isLoading={isPoolLoading} + getMinValueForToken={getMinValueForToken} isAmountNotEntered={isAmountNotEntered} isTokensNotSelected={isTokensNotSelected} isLiquidityInsufficient={isLiquidityInsufficient} @@ -287,7 +343,8 @@ export const Swap = (): JSX.Element => { { - + diff --git a/src/pages/Swap/SwapConfirmationModal/SwapConfirmationModal.tsx b/src/pages/Swap/SwapConfirmationModal/SwapConfirmationModal.tsx index 2431b19c9..92f7bacd6 100644 --- a/src/pages/Swap/SwapConfirmationModal/SwapConfirmationModal.tsx +++ b/src/pages/Swap/SwapConfirmationModal/SwapConfirmationModal.tsx @@ -13,10 +13,18 @@ import { useObservable } from '../../../common/hooks/useObservable'; import { TokenControlFormItem } from '../../../components/common/TokenControl/TokenControl'; import { InfoTooltip } from '../../../components/InfoTooltip/InfoTooltip'; import { useSettings } from '../../../context'; -import { Box, Button, Flex, Modal, Typography } from '../../../ergodex-cdk'; -import { Form, useForm } from '../../../ergodex-cdk/components/Form/NewForm'; +import { + Box, + Button, + Flex, + Form, + Modal, + Typography, + useForm, +} from '../../../ergodex-cdk'; +import { utxos$ } from '../../../network/ergo/common/utxos'; import { explorer } from '../../../services/explorer'; -import { useMinExFee, utxos$ } from '../../../services/new/core'; +import { useMinExFee } from '../../../services/new/core'; import { poolActions } from '../../../services/poolActions'; import { submitTx } from '../../../services/yoroi'; import { makeTarget } from '../../../utils/ammMath'; @@ -67,7 +75,7 @@ export const SwapConfirmationModal: FC = ({ if (value.pool && value.fromAsset && value.fromAmount) { setBaseParams( getBaseInputParameters(value.pool['pool'], { - inputAmount: value.fromAmount.toString({ suffix: false }), + inputAmount: value.fromAmount.toAmount(), inputAsset: value.fromAsset, slippage, }), diff --git a/src/pages/Swap/SwapTooltip/SwapTooltip.tsx b/src/pages/Swap/SwapTooltip/SwapTooltip.tsx index 13e310bda..977650b96 100644 --- a/src/pages/Swap/SwapTooltip/SwapTooltip.tsx +++ b/src/pages/Swap/SwapTooltip/SwapTooltip.tsx @@ -5,8 +5,7 @@ import React, { FC } from 'react'; import { useObservable } from '../../../common/hooks/useObservable'; import { InfoTooltip } from '../../../components/InfoTooltip/InfoTooltip'; import { useSettings } from '../../../context'; -import { Flex } from '../../../ergodex-cdk'; -import { FormGroup } from '../../../ergodex-cdk/components/Form/NewForm'; +import { Flex, FormGroup } from '../../../ergodex-cdk'; import { useMaxTotalFees, useMinExFee } from '../../../services/new/core'; import { renderFractions } from '../../../utils/math'; import { getBaseInputParameters } from '../../../utils/walletMath'; @@ -23,7 +22,7 @@ const TxInfoTooltipContent: FC<{ value: SwapFormModel }> = ({ value }) => { minExFee.amount, nitro, getBaseInputParameters(value.pool['pool']!, { - inputAmount: value.fromAmount?.toString({ suffix: false })!, + inputAmount: value.fromAmount?.toAmount()!, inputAsset: value.fromAsset!, slippage, }).minOutput, @@ -57,7 +56,7 @@ const TxInfoTooltipContent: FC<{ value: SwapFormModel }> = ({ value }) => { Total Fees: - {totalFees.toString()} + {totalFees.toCurrencyString()} diff --git a/src/services/new/core.ts b/src/services/new/core.ts index a77ba61f4..9ca690d1b 100644 --- a/src/services/new/core.ts +++ b/src/services/new/core.ts @@ -1,128 +1,18 @@ -import { ergoBoxFromProxy } from '@ergolabs/ergo-sdk'; import { AssetInfo } from '@ergolabs/ergo-sdk/build/main/entities/assetInfo'; -import { - combineLatest, - distinctUntilChanged, - exhaustMap, - filter, - from, - interval, - map, - mapTo, - Observable, - of, - publishReplay, - refCount, - startWith, - Subject, - switchMap, -} from 'rxjs'; - -import { ERG_DECIMALS, ERG_TOKEN_NAME } from '../../common/constants/erg'; +import { map, Observable, of, publishReplay, refCount } from 'rxjs'; + +import { ERG_DECIMALS } from '../../common/constants/erg'; import { defaultExFee } from '../../common/constants/settings'; import { useObservable } from '../../common/hooks/useObservable'; import { Currency } from '../../common/models/Currency'; import { normalizeAmount } from '../../common/utils/amount'; import { useSettings } from '../../context'; -import { walletCookies } from '../../utils/cookies'; -import { renderFractions } from '../../utils/math'; import { calculateTotalFee } from '../../utils/transactions'; export const UPDATE_TIME = 5 * 1000; const ERGO_ID = '0000000000000000000000000000000000000000000000000000000000000000'; -export enum WalletState { - NOT_CONNECTED, - CONNECTING, - CONNECTED, -} - -const updateWalletState = new Subject(); - -export const walletState$ = updateWalletState.pipe( - startWith(undefined), - switchMap(() => - walletCookies.isSetConnected() && !!window.ergo_request_read_access - ? from(window.ergo_request_read_access()).pipe( - map((value) => - value ? WalletState.CONNECTED : WalletState.CONNECTING, - ), - startWith(WalletState.CONNECTING), - ) - : of(WalletState.NOT_CONNECTED), - ), - distinctUntilChanged(), - publishReplay(1), - refCount(), -); - -export const connectWallet = (): void => { - updateWalletState.next(undefined); -}; - -export const isWalletSetuped$ = walletState$.pipe( - filter( - (state) => - state === WalletState.CONNECTED || state === WalletState.CONNECTING, - ), - mapTo(true), - publishReplay(1), - refCount(), -); - -export const isWalletConnected$ = walletState$.pipe( - filter((state) => state === WalletState.CONNECTED), - mapTo(true), - publishReplay(1), - refCount(), -); - -export const appTick$ = walletState$.pipe( - filter((state) => state === WalletState.CONNECTED), - switchMap(() => interval(UPDATE_TIME).pipe(startWith(0))), - publishReplay(1), - refCount(), -); - -export const utxos$ = appTick$.pipe( - exhaustMap(() => from(ergo.get_utxos())), - map((bs) => bs?.map((b) => ergoBoxFromProxy(b))), - map((data) => data ?? []), - publishReplay(1), - refCount(), -); - -export const nativeTokenBalance$ = appTick$.pipe( - exhaustMap(() => from(ergo.get_balance(ERG_TOKEN_NAME))), - map((balance) => renderFractions(balance, ERG_DECIMALS)), - publishReplay(1), - refCount(), -); - -export const isWalletLoading$ = combineLatest([ - utxos$, - nativeTokenBalance$, -]).pipe( - map(() => false), - startWith(true), - publishReplay(1), - refCount(), -); - -export const getTokenBalance = (tokenId: string): Observable => - isWalletSetuped$.pipe( - filter(Boolean), - switchMap(() => - from( - tokenId === ERGO_ID - ? ergo.get_balance(ERG_TOKEN_NAME) - : ergo.get_balance(tokenId), - ), - ), - map((amount) => +renderFractions(amount, ERG_DECIMALS)), - ); - export const networkAsset = { name: 'ERG', id: ERGO_ID, @@ -155,6 +45,18 @@ export const useMinExFee = (): Currency => { return new Currency(calculateTotalFee([exFee], ERG_DECIMALS), networkAsset); }; +export const useMaxExFee = (): Currency => { + const [{ minerFee, nitro }] = useSettings(); + const networkAsset = useNetworkAsset(); + + const exFee = +normalizeAmount( + (minerFee * 3 * nitro).toString(), + networkAsset, + ); + + return new Currency(calculateTotalFee([exFee], ERG_DECIMALS), networkAsset); +}; + export const useMaxTotalFees = (): Currency => { const [{ minerFee, nitro }] = useSettings(); const networkAsset = useNetworkAsset(); diff --git a/src/services/number.ts b/src/services/number.ts index f6c5ffb7e..4291dae1e 100644 --- a/src/services/number.ts +++ b/src/services/number.ts @@ -20,7 +20,7 @@ export const formatToUSD = (amount: number | string, type?: 'abbr'): string => { }; export const formatToPercent = (amount: number | string): string => { - return numeral(amount).format(PERCENT_FORMAT); + return numeral(Number(amount) / 100).format(PERCENT_FORMAT); }; export const formatToInt = (amount: number | string): string => diff --git a/src/utils/date.ts b/src/utils/date.ts deleted file mode 100644 index 326bf10ae..000000000 --- a/src/utils/date.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DateTime } from 'luxon'; - -export const getFormattedDate = (timestamp?: bigint): string => - timestamp - ? DateTime.fromMillis(Number(timestamp)).toLocaleString( - DateTime.DATETIME_MED, - { locale: 'en' }, - ) - : ''; diff --git a/src/utils/wallets/yoroi.tsx b/src/utils/wallets/yoroi.tsx deleted file mode 100644 index b2bc6fae4..000000000 --- a/src/utils/wallets/yoroi.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; - -import { applicationConfig } from '../../applicationConfig'; -import { YOROI_LINK } from '../../common/constants/env'; -import { WalletContextType } from '../../context'; -import { Typography } from '../../ergodex-cdk'; -import { walletCookies } from '../cookies'; - -export const connectYoroiWallet = - (ctx: WalletContextType) => (): Promise => { - if (!window.ergo_request_read_access) { - return Promise.reject( - <> - To use ErgoDEX install{' '} - - Yoroi Wallet - {' '} - . - , - ); - } - - return window.ergo_request_read_access().then((isConnected) => { - if (isConnected) { - ctx.setIsWalletConnected(isConnected); - walletCookies.setConnected(); - } else { - walletCookies.removeConnected(); - return Promise.reject( - <> - Seems like an issue on Yoroi side. Get help in{' '} - - Discord - {' '} - or{' '} - - Telegram - - . - , - ); - } - }); - };