Skip to content

Commit

Permalink
feat(twap): refactor and wire price protection (#2797)
Browse files Browse the repository at this point in the history
* refactor: `ReactNode` already has `string` inside it

* feat: new optional params to ExecutionPrice

* refactor: make TradeWidgetField and TradeNumberInput more generic

* feat: calculate limit price and pass it down to price protection row

* feat: pass isInverted state onto price row

* feat: calculate buyAmount outside of twapOrderAtom

* feat: use buyAmount from its own atom

twapOrder is null when the account isn't set

* feat: show `0` rather than `-` when there's no limit price
  • Loading branch information
alfetopito authored Jul 6, 2023
1 parent 6fcf366 commit a657074
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 34 deletions.
8 changes: 6 additions & 2 deletions src/common/pure/ExecutionPrice/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,28 @@ export interface ExecutionPriceProps {
executionPrice: Price<Currency, Currency>
isInverted: boolean
showBaseCurrency?: boolean
hideSeparator?: boolean
separatorSymbol?: string
hideFiat?: boolean
}

export function ExecutionPrice({
executionPrice,
isInverted,
showBaseCurrency,
separatorSymbol = '≈',
hideSeparator,
hideFiat,
}: ExecutionPriceProps) {
const executionPriceFiat = useExecutionPriceFiat(executionPrice, isInverted)
const executionPriceFiat = useExecutionPriceFiat(hideFiat ? null : executionPrice, isInverted)
const quoteCurrency = isInverted ? executionPrice?.baseCurrency : executionPrice?.quoteCurrency
const baseCurrency = isInverted ? executionPrice?.quoteCurrency : executionPrice?.baseCurrency
const oneBaseCurrency = tryParseCurrencyAmount('1', baseCurrency)

return (
<span>
{showBaseCurrency && <TokenAmount amount={oneBaseCurrency} tokenSymbol={baseCurrency} />}
{` ${separatorSymbol} `}
{!hideSeparator && ` ${separatorSymbol} `}
<TokenAmount amount={isInverted ? executionPrice.invert() : executionPrice} tokenSymbol={quoteCurrency} />
{executionPriceFiat && (
<i>
Expand Down
2 changes: 1 addition & 1 deletion src/common/pure/RateInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface RateInfoParams {

export interface RateInfoProps {
className?: string
label?: React.ReactNode | string
label?: React.ReactNode
stylized?: boolean
noLabel?: boolean
prependSymbol?: boolean
Expand Down
9 changes: 4 additions & 5 deletions src/modules/trade/pure/TradeNumberInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ export interface TradeNumberInputProps extends TradeWidgetFieldProps {
min?: number
max?: number
placeholder?: string
inputType?: string
limitPrice?: string
prefixComponent?: React.ReactElement
}

export function TradeNumberInput(props: TradeNumberInputProps) {
const { value, suffix, onUserInput, placeholder, decimalsPlaces = 0, min, max = 0, inputType, limitPrice } = props
const { value, suffix, onUserInput, placeholder, decimalsPlaces = 0, min, max = 0, prefixComponent } = props

const [displayedValue, setDisplayedValue] = useState(value === null ? '' : value.toString())

Expand Down Expand Up @@ -57,9 +56,9 @@ export function TradeNumberInput(props: TradeNumberInputProps) {
}, [])

return (
<TradeWidgetField {...props}>
<TradeWidgetField {...props} hasPrefix={!!prefixComponent}>
<>
<em>{inputType === 'priceProtection' && limitPrice && <>{limitPrice}</>}</em>
{prefixComponent}
<span>
<NumericalInput placeholder={placeholder} value={displayedValue} onUserInput={onChange} />
{suffix && <Suffix>{suffix}</Suffix>}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/trade/pure/TradeWidgetField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Trans } from '@lingui/macro'
import QuestionHelper from 'legacy/components/QuestionHelper'
import { renderTooltip } from 'legacy/components/Tooltip'

import { TradeWidgetFieldBox, TradeWidgetFieldLabel, Content, ErrorText } from './styled'
import { Content, ErrorText, TradeWidgetFieldBox, TradeWidgetFieldLabel } from './styled'

export type TradeWidgetFieldError = { type: 'error' | 'warning'; text: string | null } | null

Expand All @@ -15,15 +15,15 @@ export interface TradeWidgetFieldProps {
tooltip?: React.ReactNode | ((params: any) => React.ReactNode)
error?: TradeWidgetFieldError
className?: string
inputType?: string
hasPrefix?: boolean
}

export function TradeWidgetField(props: TradeWidgetFieldProps) {
const { className, children, label, tooltip, error, inputType } = props
const { className, children, label, tooltip, error, hasPrefix } = props
const tooltipElement = renderTooltip(tooltip, props)

return (
<TradeWidgetFieldBox className={className} inputType={inputType}>
<TradeWidgetFieldBox className={className} hasPrefix={hasPrefix}>
<TradeWidgetFieldLabel>
<Trans>{label}</Trans>
{tooltip && <QuestionHelper text={tooltipElement} />}
Expand Down
18 changes: 9 additions & 9 deletions src/modules/trade/pure/TradeWidgetField/styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ export const ErrorText = styled.div<{ type?: 'error' | 'warning' }>`
margin-top: 5px;
`

export const TradeWidgetFieldBox = styled.div<{ inputType?: string }>`
background: ${({ theme, inputType }) => (inputType === 'priceProtection' ? 'transparent' : theme.grey1)};
border: 1px solid ${({ theme, inputType }) => (inputType === 'priceProtection' ? theme.grey1 : 0)};
export const TradeWidgetFieldBox = styled.div<{ hasPrefix?: boolean }>`
background: ${({ theme, hasPrefix }) => (hasPrefix ? 'transparent' : theme.grey1)};
border: 1px solid ${({ theme, hasPrefix }) => (hasPrefix ? theme.grey1 : 0)};
border-radius: 16px;
min-height: 45px;
font-size: 18px;
padding: 10px 16px;
padding: ${({ inputType }) => (inputType === 'priceProtection' ? '0' : '10px 16px')};
padding: ${({ hasPrefix }) => (hasPrefix ? '0' : '10px 16px')};
display: flex;
justify-content: space-between;
align-items: center;
Expand All @@ -56,11 +56,11 @@ export const TradeWidgetFieldBox = styled.div<{ inputType?: string }>`
gap: 3px;
${TradeWidgetFieldLabel} {
padding: ${({ inputType }) => (inputType === 'priceProtection' ? '10px 16px' : 'initial')};
padding: ${({ hasPrefix }) => (hasPrefix ? '10px 16px' : 'initial')};
}
${Content} {
padding: ${({ inputType }) => (inputType === 'priceProtection' ? '10px 88px 10px 16px' : 'initial')};
padding: ${({ hasPrefix }) => (hasPrefix ? '10px 88px 10px 16px' : 'initial')};
> em {
font-style: normal;
Expand All @@ -70,10 +70,10 @@ export const TradeWidgetFieldBox = styled.div<{ inputType?: string }>`
display: flex;
align-items: center;
justify-content: flex-end;
background: ${({ theme, inputType }) => (inputType === 'priceProtection' ? theme.grey1 : 'transparent')};
background: ${({ theme, hasPrefix }) => (hasPrefix ? theme.grey1 : 'transparent')};
${({ inputType }) =>
inputType === 'priceProtection' &&
${({ hasPrefix }) =>
hasPrefix &&
css`
position: absolute;
top: -1px;
Expand Down
29 changes: 24 additions & 5 deletions src/modules/twap/containers/TwapFormWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAtomValue } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'

import { renderTooltip } from 'legacy/components/Tooltip'

Expand All @@ -14,7 +14,9 @@ import { useGetTradeFormValidation } from 'modules/tradeFormValidation'
import { QuoteObserverUpdater } from 'modules/twap/updaters/QuoteObserverUpdater'
import { useIsSafeApp, useWalletInfo } from 'modules/wallet'

import { usePrice } from 'common/hooks/usePrice'
import { useRateInfoParams } from 'common/hooks/useRateInfoParams'
import { ExecutionPrice } from 'common/pure/ExecutionPrice'

import * as styledEl from './styled'
import { AMOUNT_PARTS_LABELS, LABELS_TOOLTIPS } from './tooltips'
Expand All @@ -25,7 +27,7 @@ import { useTwapFormState } from '../../hooks/useTwapFormState'
import { AmountParts } from '../../pure/AmountParts'
import { DeadlineSelector } from '../../pure/DeadlineSelector'
import { partsStateAtom } from '../../state/partsStateAtom'
import { twapTimeIntervalAtom } from '../../state/twapOrderAtom'
import { twapSlippageAdjustedBuyAmount, twapTimeIntervalAtom } from '../../state/twapOrderAtom'
import { twapOrdersSettingsAtom, updateTwapOrdersSettingsAtom } from '../../state/twapOrdersSettingsAtom'
import { FallbackHandlerVerificationUpdater } from '../../updaters/FallbackHandlerVerificationUpdater'
import { TwapOrdersUpdater } from '../../updaters/TwapOrdersUpdater'
Expand All @@ -41,6 +43,7 @@ export function TwapFormWidget() {
const isSafeApp = useIsSafeApp()
const { numberOfPartsValue, slippageValue, deadline, customDeadline, isCustomDeadline } =
useAtomValue(twapOrdersSettingsAtom)
const buyAmount = useAtomValue(twapSlippageAdjustedBuyAmount)

const { inputCurrencyAmount, outputCurrencyAmount } = useAdvancedOrdersDerivedState()
const { inputCurrencyAmount: rawInputCurrencyAmount } = useAdvancedOrdersRawState()
Expand All @@ -59,6 +62,8 @@ export function TwapFormWidget() {

const rateInfoParams = useRateInfoParams(inputCurrencyAmount, outputCurrencyAmount)

const limitPrice = usePrice(inputCurrencyAmount, buyAmount)

const deadlineState = {
deadline,
customDeadline,
Expand All @@ -78,6 +83,9 @@ export function TwapFormWidget() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const isInvertedState = useState(false)
const [isInverted] = isInvertedState

return (
<>
<QuoteObserverUpdater />
Expand All @@ -89,7 +97,11 @@ export function TwapFormWidget() {

{!isWrapOrUnwrap && (
<styledEl.Row>
<styledEl.StyledRateInfo label={LABELS_TOOLTIPS.price.label} rateInfoParams={rateInfoParams} />
<styledEl.StyledRateInfo
label={LABELS_TOOLTIPS.price.label}
rateInfoParams={rateInfoParams}
isInvertedState={isInvertedState}
/>
</styledEl.Row>
)}
<TradeNumberInput
Expand All @@ -100,8 +112,15 @@ export function TwapFormWidget() {
max={50}
label={LABELS_TOOLTIPS.slippage.label}
tooltip={renderTooltip(LABELS_TOOLTIPS.slippage.tooltip)}
inputType="priceProtection"
limitPrice={'1484.45 USDC'} // TODO: add real dynamic limit price
prefixComponent={
<em>
{limitPrice ? (
<ExecutionPrice executionPrice={limitPrice} isInverted={isInverted} hideFiat hideSeparator />
) : (
'0'
)}
</em>
}
suffix="%"
/>
<styledEl.Row>
Expand Down
2 changes: 1 addition & 1 deletion src/modules/twap/containers/TwapFormWidget/tooltips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const IconImage = styled.div`
`

export interface LabelTooltip {
label: React.ReactNode | string
label: React.ReactNode
tooltip?: React.ReactNode | ((params: any) => React.ReactNode)
}

Expand Down
27 changes: 20 additions & 7 deletions src/modules/twap/state/twapOrderAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,35 @@ export const twapTimeIntervalAtom = atom<number>((get) => {
return seconds / numberOfPartsValue
})

/**
* Get slippage adjusted buyAmount for TWAP orders
*
* Calculated independently as we don't need the user to be connected to know how much they would receive
*/
export const twapSlippageAdjustedBuyAmount = atom<CurrencyAmount<Token> | null>((get) => {
const { outputCurrencyAmount } = get(advancedOrdersDerivedStateAtom)

if (!outputCurrencyAmount) return null

const slippage = get(twapOrderSlippage)

const slippageAmount = outputCurrencyAmount.multiply(slippage)
return outputCurrencyAmount.subtract(slippageAmount) as CurrencyAmount<Token>
})

export const twapOrderAtom = atom<TWAPOrder | null>((get) => {
const appDataInfo = get(appDataInfoAtom)
const { account } = get(walletInfoAtom)
const { numberOfPartsValue } = get(twapOrdersSettingsAtom)
const timeInterval = get(twapTimeIntervalAtom)
const { inputCurrencyAmount, outputCurrencyAmount, recipient } = get(advancedOrdersDerivedStateAtom)
const slippage = get(twapOrderSlippage)
const { inputCurrencyAmount, recipient } = get(advancedOrdersDerivedStateAtom)
const buyAmount = get(twapSlippageAdjustedBuyAmount)

if (!inputCurrencyAmount || !outputCurrencyAmount || !account) return null

const slippageAmount = outputCurrencyAmount.multiply(slippage)
const buyAmountWithSlippage = outputCurrencyAmount.subtract(slippageAmount)
if (!inputCurrencyAmount || !buyAmount || !account) return null

return {
sellAmount: inputCurrencyAmount as CurrencyAmount<Token>,
buyAmount: buyAmountWithSlippage as CurrencyAmount<Token>,
buyAmount,
receiver: recipient || account, // TODO: check case with ENS name
numOfParts: numberOfPartsValue,
startTime: 0, // Will be set to a block timestamp value from CurrentBlockTimestampFactory
Expand Down

0 comments on commit a657074

Please sign in to comment.