diff --git a/packages/yoroi-extension/app/components/swap/PriceImpact.js b/packages/yoroi-extension/app/components/swap/PriceImpact.js
index 2fe4051f8f..c3d0ffcb98 100644
--- a/packages/yoroi-extension/app/components/swap/PriceImpact.js
+++ b/packages/yoroi-extension/app/components/swap/PriceImpact.js
@@ -135,6 +135,12 @@ export function FormattedMarketPrice(): Node {
return ;
}
+export function FormattedLimitPrice(): Node {
+ const { orderData } = useSwap();
+ const limitPrice = orderData.limitPrice ?? '0';
+ return ;
+}
+
export function FormattedActualPrice(): Node {
const { orderData } = useSwap();
const actualPrice = orderData.selectedPoolCalculation?.prices.actualPrice ?? '0';
diff --git a/packages/yoroi-extension/app/components/swap/SwapInput.js b/packages/yoroi-extension/app/components/swap/SwapInput.js
index afe286ffc6..8c946a5708 100644
--- a/packages/yoroi-extension/app/components/swap/SwapInput.js
+++ b/packages/yoroi-extension/app/components/swap/SwapInput.js
@@ -37,28 +37,31 @@ export default function SwapInput({
getTokenInfo,
focusState,
}: Props): Node {
-
const [remoteTokenLogo, setRemoteTokenLogo] = useState(null);
const { id, amount: quantity = undefined, image, ticker } = tokenInfo || {};
const handleChange = e => {
- handleAmountChange(e.target.value);
+ if (!disabled && value !== quantity) {
+ handleAmountChange(e.target.value);
+ }
};
const isFocusedColor = focusState.value ? 'grayscale.max' : 'grayscale.400';
useEffect(() => {
if (id != null) {
- getTokenInfo(id).then(remoteTokenInfo => {
- if (remoteTokenInfo.logo != null) {
- setRemoteTokenLogo(`data:image/png;base64,${remoteTokenInfo.logo}`);
- }
- return null;
- }).catch(e => {
- console.warn('Failed to resolve remote info for token: ' + id, e);
- });
+ getTokenInfo(id)
+ .then(remoteTokenInfo => {
+ if (remoteTokenInfo.logo != null) {
+ setRemoteTokenLogo(`data:image/png;base64,${remoteTokenInfo.logo}`);
+ }
+ return null;
+ })
+ .catch(e => {
+ console.warn('Failed to resolve remote info for token: ' + id, e);
+ });
}
- }, [id])
+ }, [id]);
const imgSrc =
ticker === defaultTokenInfo.ticker
@@ -116,7 +119,7 @@ export default function SwapInput({
variant="body1"
color="grayscale.max"
placeholder="0"
- onChange={disabled ? () => {} : handleChange}
+ onChange={handleChange}
value={disabled ? '' : value}
onFocus={() => focusState.update(true)}
onBlur={() => focusState.update(false)}
diff --git a/packages/yoroi-extension/app/components/swap/SwapPriceInput.js b/packages/yoroi-extension/app/components/swap/SwapPriceInput.js
index 561e560550..9bd6aa22da 100644
--- a/packages/yoroi-extension/app/components/swap/SwapPriceInput.js
+++ b/packages/yoroi-extension/app/components/swap/SwapPriceInput.js
@@ -1,14 +1,13 @@
// @flow
import type { Node } from 'react';
+import type { PriceImpact } from './types';
+import { useState } from 'react';
import { Box, Typography } from '@mui/material';
import { Quantities } from '../../utils/quantities';
import { useSwap } from '@yoroi/swap';
import { PRICE_PRECISION } from './common';
import { useSwapForm } from '../../containers/swap/context/swap-form';
-import SwapStore from '../../stores/ada/SwapStore';
-import { runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import type { PriceImpact } from './types';
import {
FormattedActualPrice,
PriceImpactColored,
@@ -17,31 +16,36 @@ import {
} from './PriceImpact';
type Props = {|
- swapStore: SwapStore,
priceImpactState: ?PriceImpact,
|};
const NO_PRICE_VALUE_PLACEHOLDER = ' ';
-function SwapPriceInput({ swapStore, priceImpactState }: Props): Node {
- const { orderData, limitPriceChanged } = useSwap();
- const { sellTokenInfo, buyTokenInfo } = useSwapForm();
+function SwapPriceInput({ priceImpactState }: Props): Node {
+ const { orderData } = useSwap();
+ const {
+ sellTokenInfo,
+ buyTokenInfo,
+ limitPriceFocusState,
+ onChangeLimitPrice,
+ limitPrice,
+ } = useSwapForm();
+ const [endsWithDot, setEndsWithDot] = useState(false);
const isMarketOrder = orderData.type === 'market';
const pricePlaceholder = isMarketOrder ? NO_PRICE_VALUE_PLACEHOLDER : '0';
const marketPrice = orderData.selectedPoolCalculation?.prices.market;
- const format = s => Quantities.format(s, orderData.tokens.priceDenomination, PRICE_PRECISION) + (s.endsWith('.') ? '.' : '');
+ const format = s =>
+ Quantities.format(s, orderData.tokens.priceDenomination, PRICE_PRECISION) +
+ (s.endsWith('.') ? '.' : '');
+
const formattedPrice = marketPrice ? format(marketPrice) : pricePlaceholder;
- if (swapStore.limitOrderDisplayValue === '' && marketPrice != null) {
- runInAction(() => {
- swapStore.setLimitOrderDisplayValue(formattedPrice);
- });
- }
- const displayValue = isMarketOrder ? formattedPrice : swapStore.limitOrderDisplayValue;
+ const displayValue = isMarketOrder ? formattedPrice : limitPrice.displayValue;
const isValidTickers = sellTokenInfo?.ticker && buyTokenInfo?.ticker;
const isReadonly = !isValidTickers || isMarketOrder;
+ const valueToDisplay = endsWithDot ? displayValue + '.' : displayValue;
return (
@@ -96,19 +100,24 @@ function SwapPriceInput({ swapStore, priceImpactState }: Props): Node {
placeholder="0"
bgcolor={isReadonly ? 'grayscale.50' : 'common.white'}
readOnly={isReadonly}
- value={isValidTickers ? displayValue : NO_PRICE_VALUE_PLACEHOLDER}
- onChange={({ target: { value: val } }) => {
+ value={isValidTickers ? valueToDisplay : NO_PRICE_VALUE_PLACEHOLDER}
+ onChange={event => {
+ const val = event.target.value;
let value = val.replace(/[^\d.]+/g, '');
- if (!value) value = '0';
- if (/^\d+\.?\d*$/.test(value)) {
- runInAction(() => {
- swapStore.setLimitOrderDisplayValue(format(value));
- });
- if (!value.endsWith('.')) {
- limitPriceChanged(value);
- }
+ if (!value || value === '.') value = '';
+ if (value.endsWith('.')) {
+ setEndsWithDot(true);
+ value = value.replace('.', '');
+ onChangeLimitPrice(value);
+ return;
+ }
+ if (/^[0-9]\d*(\.\d+)?$/gi.test(value) || !value) {
+ onChangeLimitPrice(value);
+ setEndsWithDot(false);
}
}}
+ onFocus={() => !isMarketOrder && limitPriceFocusState.update(true)}
+ onBlur={() => !isMarketOrder && limitPriceFocusState.update(false)}
/>
diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js b/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js
index 32b783c5ca..69e68cde9c 100644
--- a/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js
+++ b/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js
@@ -12,6 +12,7 @@ import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types';
import {
FormattedActualPrice,
FormattedMarketPrice,
+ FormattedLimitPrice,
PriceImpactBanner,
PriceImpactColored,
PriceImpactIcon,
@@ -34,6 +35,19 @@ type Props = {|
getFormattedPairingValue: (amount: string) => string,
|};
+const priceStrings = {
+ market: {
+ label: 'Market price',
+ info:
+ 'Market price is the best price available on the market among several DEXes that lets you buy or sell an asset instantly',
+ },
+ limit: {
+ label: 'Limit price',
+ info:
+ "Limit price in a DEX is a specific pre-set price at which you can trade an asset. Unlike market orders, which execute immediately at the current market price, limit orders are set to execute only when the market reaches the trader's specified price.",
+ },
+};
+
export default function ConfirmSwapTransaction({
slippageValue,
walletAddress,
@@ -140,16 +154,16 @@ export default function ConfirmSwapTransaction({
{slippageValue}%
-
+ {orderData.type === 'market' ? : }
{priceImpactState && }
diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js
index 94d91e72d9..53636969e4 100644
--- a/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js
+++ b/packages/yoroi-extension/app/containers/swap/asset-swap/CreateSwapOrder.js
@@ -1,4 +1,6 @@
// @flow
+import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types';
+import type { PriceImpact } from '../../../components/swap/types';
import { useState } from 'react';
import { Box } from '@mui/material';
import SwapPriceInput from '../../../components/swap/SwapPriceInput';
@@ -12,12 +14,10 @@ import EditSwapPool from './edit-pool/EditPool';
import SelectSwapPoolFromList from './edit-pool/SelectPoolFromList';
import SwapStore from '../../../stores/ada/SwapStore';
import { useAsyncPools } from '../hooks';
-import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types';
-import type { PriceImpact } from '../../../components/swap/types';
-
import { TopActions } from './actions/TopActions';
import { MiddleActions } from './actions/MiddleActions';
import { EditSlippage } from './actions/EditSlippage';
+import { useSwapForm } from '../context/swap-form';
type Props = {|
slippageValue: string,
@@ -50,14 +50,21 @@ export const CreateSwapOrder = ({
buyTokenInfoChanged,
} = useSwap();
+ const { onChangeLimitPrice } = useSwapForm();
+
+ const resetLimitPrice = () => {
+ onChangeLimitPrice('');
+ };
+
if (orderType === 'market') {
const selectedPoolId = selectedPoolCalculation?.pool.poolId;
if (selectedPoolId !== prevSelectedPoolId) {
setPrevSelectedPoolId(selectedPoolId);
- swapStore.resetLimitOrderDisplayValue();
+ resetLimitPrice();
}
}
+ // TODO: refactor, this hook call will be removed and replaced with store function
useAsyncPools(sell.tokenId, buy.tokenId)
.then(() => null)
.catch(() => null);
@@ -84,7 +91,7 @@ export const CreateSwapOrder = ({
/>
{/* Clear and switch */}
-
+
{/* To Field */}
{/* Price between assets */}
-
+
{/* Slippage settings */}
setOpenedDialog('')}
onTokenInfoChanged={val => {
- swapStore.resetLimitOrderDisplayValue();
+ resetLimitPrice();
sellTokenInfoChanged(val);
}}
defaultTokenInfo={defaultTokenInfo}
@@ -130,7 +134,7 @@ export const CreateSwapOrder = ({
store={swapStore}
onClose={() => setOpenedDialog('')}
onTokenInfoChanged={val => {
- swapStore.resetLimitOrderDisplayValue();
+ resetLimitPrice();
buyTokenInfoChanged(val);
}}
defaultTokenInfo={defaultTokenInfo}
diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js
index 54f325e035..cecf998770 100644
--- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js
+++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js
@@ -88,8 +88,8 @@ function SwapPage(props: StoresAndActionsProps): Node {
const defaultTokenInfo = props.stores.tokenInfoStore.getDefaultTokenInfoSummary(
network.NetworkId
);
- const getTokenInfo: (string => Promise) =
- id => props.stores.tokenInfoStore.getLocalOrRemoteMetadata(network, id);
+ const getTokenInfo: string => Promise = id =>
+ props.stores.tokenInfoStore.getLocalOrRemoteMetadata(network, id);
const disclaimerFlag = props.stores.substores.ada.swapStore.swapDisclaimerAcceptanceFlag;
diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/actions/MiddleActions.js b/packages/yoroi-extension/app/containers/swap/asset-swap/actions/MiddleActions.js
index 3c59912c33..7f2860824c 100644
--- a/packages/yoroi-extension/app/containers/swap/asset-swap/actions/MiddleActions.js
+++ b/packages/yoroi-extension/app/containers/swap/asset-swap/actions/MiddleActions.js
@@ -2,21 +2,16 @@
import { Box, Button } from '@mui/material';
import { ReactComponent as SwitchIcon } from '../../../../assets/images/revamp/icons/switch.inline.svg';
import { useSwapForm } from '../../context/swap-form';
-import SwapStore from '../../../../stores/ada/SwapStore';
-type Props = {|
- swapStore: SwapStore,
-|};
-
-export const MiddleActions = ({ swapStore }: Props): React$Node => {
- const { clearSwapForm, switchTokens } = useSwapForm();
+export const MiddleActions = (): React$Node => {
+ const { clearSwapForm, switchTokens, onChangeLimitPrice } = useSwapForm();
return (
{
- swapStore.resetLimitOrderDisplayValue();
+ onChangeLimitPrice('');
return switchTokens();
}}
>
diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/EditBuyAmount.js b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/EditBuyAmount.js
index fea60d3497..1da54ceba9 100644
--- a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/EditBuyAmount.js
+++ b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/EditBuyAmount.js
@@ -11,7 +11,11 @@ type Props = {|
getTokenInfo: string => Promise,
|};
-export default function EditBuyAmount({ onAssetSelect, defaultTokenInfo, getTokenInfo }: Props): Node {
+export default function EditBuyAmount({
+ onAssetSelect,
+ defaultTokenInfo,
+ getTokenInfo,
+}: Props): Node {
const { orderData } = useSwap();
const {
buyQuantity: { displayValue: buyDisplayValue, error: fieldError },
@@ -27,14 +31,16 @@ export default function EditBuyAmount({ onAssetSelect, defaultTokenInfo, getToke
const error = isInvalidPair ? 'Selected pair is not available in any liquidity pool' : fieldError;
// Amount input is blocked in case invalid pair
- const handleAmountChange = isInvalidPair ? () => {} : onChangeBuyQuantity;
+ const handleAmountChange = () => {
+ return isInvalidPair ? () => {} : onChangeBuyQuantity;
+ };
return (
void,
defaultTokenInfo: RemoteTokenInfo,
-|}
+|};
export default function SelectSwapPoolFromList({ onClose, defaultTokenInfo }: Props): React$Node {
+ const {
+ orderData: { tokens, pools, selectedPoolId, amounts },
+ selectedPoolChanged,
+ } = useSwap();
- const { orderData: { tokens, pools, selectedPoolId, amounts }, selectedPoolChanged } = useSwap();
+ const { poolTouched } = useSwapForm();
- return selectedPoolChanged(poolId)}
- sellTokenId={amounts.sell.tokenId}
- denomination={tokens.priceDenomination}
- defaultTokenInfo={defaultTokenInfo}
- poolList={pools}
- currentPool={selectedPoolId}
- onClose={onClose}
- />;
+ return (
+ {
+ selectedPoolChanged(poolId);
+ poolTouched();
+ }}
+ sellTokenId={amounts.sell.tokenId}
+ denomination={tokens.priceDenomination}
+ defaultTokenInfo={defaultTokenInfo}
+ poolList={pools}
+ currentPool={selectedPoolId}
+ onClose={onClose}
+ />
+ );
}
diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js
index f0921e18ea..6442327bba 100644
--- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js
+++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js
@@ -1,15 +1,15 @@
//@flow
import type { Node } from 'react';
-import { useCallback, useEffect, useReducer, useState } from 'react';
import type { SwapFormAction, SwapFormState } from './types';
-import { StateWrap, SwapFormActionTypeValues } from './types';
import type { AssetAmount } from '../../../../components/swap/types';
+import { useCallback, useEffect, useReducer, useState } from 'react';
+import { StateWrap, SwapFormActionTypeValues } from './types';
import { useSwap } from '@yoroi/swap';
import Context from './context';
import { Quantities } from '../../../../utils/quantities';
import SwapStore from '../../../../stores/ada/SwapStore';
import { defaultSwapFormState } from './DefaultSwapFormState';
-
+import { PRICE_PRECISION } from '../../../../components/swap/common';
// const PRECISION = 14;
type Props = {|
@@ -28,6 +28,7 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node {
sellTokenInfoChanged,
switchTokens,
resetQuantities,
+ limitPriceChanged,
} = useSwap();
const { quantity: buyQuantity } = orderData.amounts.buy;
@@ -195,6 +196,10 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node {
actions.buyInputValueChanged(input);
};
+ const limitPriceUpdateHandler = ({ input }) => {
+ actions.limitPriceInputValueChanged(input);
+ };
+
const onChangeSellQuantity = useCallback(
baseSwapFieldChangeHandler(swapFormState.sellTokenInfo, sellUpdateHandler),
[sellQuantityChanged, actions, clearErrors]
@@ -205,8 +210,25 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node {
[buyQuantityChanged, actions, clearErrors]
);
+ const onChangeLimitPrice = useCallback(
+ text => {
+ const [formattedPrice, price] = Quantities.parseFromText(
+ text,
+ orderData.tokens.priceDenomination,
+ numberLocale,
+ PRICE_PRECISION
+ );
+ actions.limitPriceInputValueChanged(formattedPrice);
+ limitPriceChanged(price);
+
+ clearErrors();
+ },
+ [actions, clearErrors, orderData.tokens.priceDenomination, limitPriceChanged, numberLocale]
+ );
+
const sellFocusState = StateWrap(useState(false));
const buyFocusState = StateWrap(useState(false));
+ const limitPriceFocusState = StateWrap(useState(false));
const updateSellInput = useCallback(() => {
if (swapFormState.sellQuantity.isTouched && !sellFocusState.value) {
@@ -224,15 +246,43 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node {
}
}, [buyQuantity, swapFormState.buyTokenInfo.decimals, swapFormState.buyQuantity.isTouched]);
+ const updateLimitPrice = useCallback(() => {
+ if (orderData.type === 'limit' && !limitPriceFocusState.value) {
+ const formatted = Quantities.format(
+ orderData.limitPrice ?? Quantities.zero,
+ orderData.tokens.priceDenomination,
+ PRICE_PRECISION
+ );
+
+ limitPriceUpdateHandler({ input: formatted });
+ } else if (orderData.type === 'market') {
+ const formatted = Quantities.format(
+ orderData.selectedPoolCalculation?.prices.market ?? Quantities.zero,
+ orderData.tokens.priceDenomination,
+ PRICE_PRECISION
+ );
+
+ limitPriceUpdateHandler({ input: formatted });
+ }
+ }, [
+ orderData.tokens.priceDenomination,
+ orderData.limitPrice,
+ orderData.selectedPoolCalculation?.prices.market,
+ orderData.type,
+ ]);
+
useEffect(updateSellInput, [updateSellInput]);
useEffect(updateBuyInput, [updateBuyInput]);
+ useEffect(updateLimitPrice, [updateLimitPrice]);
const allActions = {
...actions,
sellFocusState,
buyFocusState,
+ limitPriceFocusState,
onChangeSellQuantity,
onChangeBuyQuantity,
+ onChangeLimitPrice,
};
return (
diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/context.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/context.js
index e6bfa1d7cc..ca72fbaf49 100644
--- a/packages/yoroi-extension/app/containers/swap/context/swap-form/context.js
+++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/context.js
@@ -26,6 +26,7 @@ const initialSwapFormContext: SwapFormContext = {
canSwapChanged: missingInit,
sellFocusState: ConstantState(false),
buyFocusState: ConstantState(false),
+ limitPriceFocusState: ConstantState(false),
onChangeSellQuantity: missingInit,
onChangeBuyQuantity: missingInit,
onChangeLimitPrice: missingInit,
diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/types.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/types.js
index f14a540b5e..d8e2b345e9 100644
--- a/packages/yoroi-extension/app/containers/swap/context/swap-form/types.js
+++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/types.js
@@ -85,6 +85,7 @@ export type SwapFormContext = {|
...SwapFormActions,
sellFocusState: State,
buyFocusState: State,
+ limitPriceFocusState: State,
onChangeSellQuantity: (text: string) => void,
onChangeBuyQuantity: (text: string) => void,
onChangeLimitPrice: (text: string) => void,
diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js
index e494944570..d538ac620d 100644
--- a/packages/yoroi-extension/app/stores/ada/SwapStore.js
+++ b/packages/yoroi-extension/app/stores/ada/SwapStore.js
@@ -37,7 +37,6 @@ const FRONTEND_FEE_ADDRESS_PREPROD =
'addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km';
export default class SwapStore extends Store {
- @observable limitOrderDisplayValue: string = '';
@observable orderStep: number = 0;
@observable transactionTimestamps: { [string]: Date } = {};
@@ -46,14 +45,6 @@ export default class SwapStore extends Store {
false
);
- @action setLimitOrderDisplayValue: string => void = (val: string) => {
- this.limitOrderDisplayValue = val;
- };
-
- @action resetLimitOrderDisplayValue: void => void = () => {
- this.limitOrderDisplayValue = '';
- };
-
@action setOrderStepValue: number => void = (val: number) => {
this.orderStep = val;
};