Skip to content

Commit

Permalink
Merge pull request #3544 from Emurgo/fix/YOEXT-1193/limit-price-confi…
Browse files Browse the repository at this point in the history
…rmation-screen

fix(swap): limit price on the confirmation screen
  • Loading branch information
vsubhuman authored Jun 14, 2024
2 parents 384c492 + be900e1 commit 952680b
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 88 deletions.
6 changes: 6 additions & 0 deletions packages/yoroi-extension/app/components/swap/PriceImpact.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export function FormattedMarketPrice(): Node {
return <FormattedPrice price={marketPrice} />;
}

export function FormattedLimitPrice(): Node {
const { orderData } = useSwap();
const limitPrice = orderData.limitPrice ?? '0';
return <FormattedPrice price={limitPrice} />;
}

export function FormattedActualPrice(): Node {
const { orderData } = useSwap();
const actualPrice = orderData.selectedPoolCalculation?.prices.actualPrice ?? '0';
Expand Down
27 changes: 15 additions & 12 deletions packages/yoroi-extension/app/components/swap/SwapInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,31 @@ export default function SwapInput({
getTokenInfo,
focusState,
}: Props): Node {

const [remoteTokenLogo, setRemoteTokenLogo] = useState<?string>(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
Expand Down Expand Up @@ -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)}
Expand Down
57 changes: 33 additions & 24 deletions packages/yoroi-extension/app/components/swap/SwapPriceInput.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<Box mt="16px">
Expand Down Expand Up @@ -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)}
/>
<Box sx={{ justifySelf: 'end' }}>
<Box height="100%" width="max-content" display="flex" alignItems="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types';
import {
FormattedActualPrice,
FormattedMarketPrice,
FormattedLimitPrice,
PriceImpactBanner,
PriceImpactColored,
PriceImpactIcon,
Expand All @@ -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,
Expand Down Expand Up @@ -140,16 +154,16 @@ export default function ConfirmSwapTransaction({
<SummaryRow col1="Slippage tolerance">{slippageValue}%</SummaryRow>
<SwapPoolFullInfo defaultTokenInfo={defaultTokenInfo} showMinAda />
<SummaryRow
col1="Market price"
col1={priceStrings[orderData.type].label}
withInfo
infoText="Market price is the best price available on the market among several DEXes that lets you buy or sell an asset instantly"
infoText={priceStrings[orderData.type].info}
>
<FormattedMarketPrice />
{orderData.type === 'market' ? <FormattedMarketPrice /> : <FormattedLimitPrice />}
</SummaryRow>
<SummaryRow
col1="Price impact"
withInfo
infoText="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."
infoText="Price impact is a difference between the actual market price and your price due to trade size."
>
<PriceImpactColored priceImpactState={priceImpactState} sx={{ display: 'flex' }}>
{priceImpactState && <PriceImpactIcon isSevere={priceImpactState.isSevere} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -84,7 +91,7 @@ export const CreateSwapOrder = ({
/>

{/* Clear and switch */}
<MiddleActions swapStore={swapStore} />
<MiddleActions />

{/* To Field */}
<EditBuyAmount
Expand All @@ -94,10 +101,7 @@ export const CreateSwapOrder = ({
/>

{/* Price between assets */}
<SwapPriceInput
swapStore={swapStore}
priceImpactState={priceImpactState}
/>
<SwapPriceInput priceImpactState={priceImpactState} />

{/* Slippage settings */}
<EditSlippage
Expand All @@ -118,7 +122,7 @@ export const CreateSwapOrder = ({
store={swapStore}
onClose={() => setOpenedDialog('')}
onTokenInfoChanged={val => {
swapStore.resetLimitOrderDisplayValue();
resetLimitPrice();
sellTokenInfoChanged(val);
}}
defaultTokenInfo={defaultTokenInfo}
Expand All @@ -130,7 +134,7 @@ export const CreateSwapOrder = ({
store={swapStore}
onClose={() => setOpenedDialog('')}
onTokenInfoChanged={val => {
swapStore.resetLimitOrderDisplayValue();
resetLimitPrice();
buyTokenInfoChanged(val);
}}
defaultTokenInfo={defaultTokenInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ function SwapPage(props: StoresAndActionsProps): Node {
const defaultTokenInfo = props.stores.tokenInfoStore.getDefaultTokenInfoSummary(
network.NetworkId
);
const getTokenInfo: (string => Promise<RemoteTokenInfo>) =
id => props.stores.tokenInfoStore.getLocalOrRemoteMetadata(network, id);
const getTokenInfo: string => Promise<RemoteTokenInfo> = id =>
props.stores.tokenInfoStore.getLocalOrRemoteMetadata(network, id);

const disclaimerFlag = props.stores.substores.ada.swapStore.swapDisclaimerAcceptanceFlag;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box
sx={{ cursor: 'pointer', color: 'primary.500' }}
onClick={() => {
swapStore.resetLimitOrderDisplayValue();
onChangeLimitPrice('');
return switchTokens();
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ type Props = {|
getTokenInfo: string => Promise<RemoteTokenInfo>,
|};

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 },
Expand All @@ -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 (
<SwapInput
key={tokenId}
label="Swap to"
disabled={!isValidTickers}
handleAmountChange={handleAmountChange}
handleAmountChange={handleAmountChange()}
value={buyDisplayValue}
tokenInfo={buyTokenInfo}
defaultTokenInfo={defaultTokenInfo}
Expand Down
Loading

0 comments on commit 952680b

Please sign in to comment.