Skip to content
This repository has been archived by the owner on Jan 15, 2021. It is now read-only.

952/set slippage #1011

Merged
merged 17 commits into from
May 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components'
import { isElement, isFragment } from 'react-is'

// assets
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'

// components
Expand Down Expand Up @@ -194,6 +194,7 @@ interface HelpTooltipProps {
tooltip: ReactNode
placement?: Placement
offset?: number
iconSize?: FontAwesomeIconProps['size']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to do something similar. Probably better to use the type:
here https://github.com/gnosis/dex-react/pull/1025/files#diff-493194965e4afe11bf4448365069f0aeR9

}

const HelperSpan = styled.span`
Expand All @@ -212,7 +213,7 @@ export const HelpTooltipContainer = styled(LongTooltipContainer)`
color: black;
`

export const HelpTooltip: React.FC<HelpTooltipProps> = ({ tooltip, placement = 'top', offset }) => {
export const HelpTooltip: React.FC<HelpTooltipProps> = ({ tooltip, placement = 'top', offset, iconSize }) => {
const {
targetProps: { ref, onClick },
tooltipProps,
Expand All @@ -229,7 +230,7 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({ tooltip, placement = '
return (
<>
<HelperSpan ref={ref} onClick={handleClick}>
<FontAwesomeIcon icon={faQuestionCircle} />
<FontAwesomeIcon icon={faQuestionCircle} size={iconSize} />
</HelperSpan>
<Tooltip {...tooltipProps} bgColor="#bfd6ef">
{tooltip}
Expand Down
182 changes: 182 additions & 0 deletions src/components/TradeWidget/MaximumSlippage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React from 'react'
import styled from 'styled-components'

import { MEDIA, SLIPPAGE_MAP } from 'const'
import { PriceSlippageState } from 'reducers-actions/priceSlippage'

const Wrapper = styled.div`
font-size: 1.6rem;
width: 100%;

> strong {
display: flex;
align-items: center;
text-transform: capitalize;
color: var(--color-text-primary);
width: 100%;
margin: 0 0 1rem;
padding: 0;
box-sizing: border-box;
font-size: 1.5rem;

@media ${MEDIA.mobile} {
font-size: 1.3rem;
}
}

> div {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-evenly;

margin: 1rem auto 0.2rem;
height: 4.4rem;
width: 100%;

> button {
border-radius: 3rem;

> small {
font-size: x-small;
margin-left: 0.4rem;
}
}

> button,
> label > input {
display: flex;
flex: 1;
align-items: center;
justify-content: space-evenly;

background: var(--color-background-input);
color: var(--color-text-primary);
font-size: inherit;
font-weight: normal;

height: 100%;

padding: 0.65rem 1.5rem;
&:not(:last-child) {
margin-right: 1rem;
}

white-space: nowrap;

&.selected,
&.selected ~ small,
&:hover:not(input),
&:focus,
&:focus ~ small {
color: var(--color-text-button-hover);
}

&:hover:not(input):not(.selected),
&:focus {
background-color: var(--color-background-button-hover);
}

&.selected {
background: var(--color-text-active);
}

transition: all 0.2s ease-in-out;
}

> label {
position: relative;
flex: 1.6;
height: 100%;

> small {
position: absolute;
right: 2rem;
top: 0;
bottom: 0;
margin: auto;

display: flex;
align-items: center;

opacity: 0.75;
color: var(--color-text-primary);

letter-spacing: -0.05rem;
text-align: right;
font-weight: var(--font-weight-bold);

@media ${MEDIA.mobile} {
font-size: 1rem;
letter-spacing: 0.03rem;
}
}

> input {
border-radius: var(--border-radius-top);
margin: 0;
padding-right: 3.4rem;
width: 100%;

&::placeholder {
opacity: 0.6;
}
}
}
}
`

interface MaximumSlippageProps {
priceSlippage: PriceSlippageState
setNewSlippage: (customSlippage: string | React.ChangeEvent<HTMLInputElement>) => void
}

const slippagePercentages = Array.from(SLIPPAGE_MAP.keys())

const checkCustomPriceSlippage = (slippagePercentage: string): boolean =>
!!slippagePercentage && !SLIPPAGE_MAP.has(slippagePercentage)

const MaximumSlippage: React.FC<MaximumSlippageProps> = ({ setNewSlippage, priceSlippage }) => {
return (
<Wrapper>
<strong>Limit additional price slippage</strong>
<div>
{slippagePercentages.map((slippage, index) => (
<button
key={index}
type="button"
onClick={(): void => setNewSlippage(slippage)}
className={slippage === priceSlippage ? 'selected' : ''}
>
{slippage}%{SLIPPAGE_MAP.get(slippage) && <small>(suggested)</small>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SLIPPAGE_MAP.get(slippage)

is always true as you construct slippagePercentages array from that same map

</button>
))}
<label>
<input
type="number"
step="0.1"
placeholder="Custom"
value={priceSlippage}
className={checkCustomPriceSlippage(priceSlippage) ? 'selected' : ''}
onChange={(e): void => setNewSlippage(e.target.value)}
/>
<small>%</small>
</label>
</div>
</Wrapper>
)
}

/*
// PRICE SLIPPAGE
const dispatchNewSlippage = (payload: string): void => dispatch(setPriceSlippage(payload))
// to add in Price component to show current selected slippage

{priceSlippage && (
<FormMessage className="warning">
<small>{priceSlippage}% slippage</small>
</FormMessage>
)}
*/

export default MaximumSlippage
12 changes: 11 additions & 1 deletion src/components/TradeWidget/Price.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { OrderBookBtn } from 'components/OrderBookBtn'

// TradeWidget: subcomponents
import { TradeFormData } from 'components/TradeWidget'
import { FormInputError } from 'components/TradeWidget/FormMessage'
import FormMessage, { FormInputError } from 'components/TradeWidget/FormMessage'
import { useNumberInput } from 'components/TradeWidget/useNumberInput'

const Wrapper = styled.div`
Expand All @@ -25,17 +25,26 @@ const Wrapper = styled.div`

> strong {
display: flex;
align-items: center;
text-transform: capitalize;
color: var(--color-text-primary);
width: 100%;
margin: 0 0 1rem;
padding: 0;
box-sizing: border-box;
font-size: 1.5rem;

@media ${MEDIA.mobile} {
font-size: 1.3rem;
}

> ${FormMessage} {
width: min-content;
white-space: nowrap;
font-size: x-small;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever use font-size keywords? We usually do *(r)em, no? Not that it matters

margin: 0 0.5rem;
}

> button {
background: none;
border: 0;
Expand Down Expand Up @@ -235,6 +244,7 @@ const Price: React.FC<Props> = ({ sellToken, receiveToken, priceInputId, priceIn
</label>
<FormInputError errorMessage={errorPriceInverse?.message} />
</PriceInputBox>
{/* MAX SLIPPAGE CONTROL */}
</Wrapper>
)
}
Expand Down
15 changes: 14 additions & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ export {
FEE_PERCENTAGE,
DEFAULT_DECIMALS,
DEFAULT_PRECISION,
ZERO,
ONE,
TWO,
TEN,
ALLOWANCE_MAX_VALUE,
ALLOWANCE_FOR_ENABLED_TOKEN,
} from '@gnosis.pm/dex-js'
export { ZERO, ONE, TWO, TEN, ALLOWANCE_MAX_VALUE, ALLOWANCE_FOR_ENABLED_TOKEN } from '@gnosis.pm/dex-js'
import { BATCH_TIME } from '@gnosis.pm/dex-js'

export const BATCH_TIME_IN_MS = BATCH_TIME * 1000

export const ZERO_BIG_NUMBER = new BigNumber(0)
export const ONE_BIG_NUMBER = new BigNumber(1)
export const TEN_BIG_NUMBER = new BigNumber(10)
export const ONE_HUNDRED_BIG_NUMBER = new BigNumber(100)

// How much of the order needs to be matched to consider it filled
// Will divide the total sell amount by this factor.
Expand Down Expand Up @@ -143,6 +150,12 @@ export const WETH_ADDRESS_RINKEBY = '0xc778417E063141139Fce010982780140Aa0cD5Ab'
export const ORDER_BOOK_HOPS_DEFAULT = 2
export const ORDER_BOOK_HOPS_MAX = 2

export const SLIPPAGE_MAP = new Map([
['0.1', false],
['0.5', true],
['1', false],
])
export const DEFAULT_SUGGESTED_SLIPPAGE = '0.5'
// Delay disabling loading indicators, since in a normal workflow, when a transaction is mined, the spinner is stopped,
// however, the new state, that flows top down once a bock is mined, can have a small delayed
// This delay mitigates the strange effect of stopping the loading before the data is updated
Expand Down
6 changes: 6 additions & 0 deletions src/hooks/usePriceEstimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ interface Result {
isPriceLoading: boolean
}

/*
PRICE ESTIMATION
*
BACKEND APPLIES A 0.1% SAFETY MARGIN ON ALL PRICES TO ACCOUNT FOR ROUNDING BY THE SOLVER
*/

export function usePriceEstimation(params: Params): Result {
const { baseTokenId, quoteTokenId } = params
const [isPriceLoading, setIsPriceLoading] = useSafeState(true)
Expand Down
4 changes: 4 additions & 0 deletions src/reducers-actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface Actions<T, P> {
payload: P
}

export type ActionCreator<T, P> = (payload: P) => Actions<T, P>

export type ReducerCreator<S, A> = (state: S, action: A) => S
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type ReducerCreator<S, A> = (state: S, action: A) => S
export type ReducerCreator<S, A extends Actions> = (state: S, action: A) => S

maybe


export interface GlobalState {
tokens: TokenLocalState
pendingOrders: PendingOrdersState
Expand Down
30 changes: 30 additions & 0 deletions src/reducers-actions/priceSlippage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Actions, ActionCreator, ReducerCreator } from 'reducers-actions'
import { DEFAULT_SUGGESTED_SLIPPAGE } from 'const'

const ActionsList = ['SET_PRICE_SLIPPAGE'] as const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as const? Fancy 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know right!? found it while thinking why can't i infer types from an array? then i can use directly AND have them inferred/autocompleted. looks a bit weird tho


type PriceSlippageActions = typeof ActionsList[number]
type PriceSlippagePayload = string

type PriceSlippageState = PriceSlippagePayload

const setPriceSlippage: ActionCreator<PriceSlippageActions, PriceSlippagePayload> = payload => ({
type: ActionsList[0],
payload,
})

const PRICE_SLIPPAGE_INITIAL_STATE = DEFAULT_SUGGESTED_SLIPPAGE

const reducer: ReducerCreator<PriceSlippageState, Actions<PriceSlippageActions, PriceSlippagePayload>> = (
state,
action,
) => {
switch (action.type) {
case ActionsList[0]:
return action.payload
default:
return state
}
}

export { setPriceSlippage, reducer, PriceSlippageState, PRICE_SLIPPAGE_INITIAL_STATE }
1 change: 1 addition & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const variables = css`
// BORDERS
// ------------------------------
--border-radius: 0.4375rem;
--border-radius-top: 0.6rem 0.6rem 0 0;

// ------------------------------
// BOX-SHADOW
Expand Down
26 changes: 24 additions & 2 deletions src/utils/price.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import BN from 'bn.js'
import { assert, ONE_BIG_NUMBER } from '@gnosis.pm/dex-js'
import { TEN, UNLIMITED_ORDER_AMOUNT_BIGNUMBER } from 'const'
import BigNumber from 'bignumber.js'
import { assert, ONE_BIG_NUMBER } from '@gnosis.pm/dex-js'
import { parseBigNumber } from './format'
import { TEN, UNLIMITED_ORDER_AMOUNT_BIGNUMBER, ONE_HUNDRED_BIG_NUMBER } from 'const'

interface AdjustAmountParams {
amount: BN
Expand Down Expand Up @@ -99,3 +100,24 @@ export function maxAmountsForSpread({

return { buyAmount: bigNumberToBN(buyAmount), sellAmount: bigNumberToBN(sellAmount) }
}

/**
* @name checkSlippageAgainstPrice
*
* @param slippage - user set slippage as string
* @param prePrice - pre-slippape adjusted price as BigNumber or null
* @returns [BigNumber | null] - pre-price adjusted for slippage as BigNumber or null
*/
export function checkSlippageAgainstPrice(slippage: string, prePrice: BigNumber | null): BigNumber | null {
if (!prePrice) return null
const slippageAsBigNumber = parseBigNumber(slippage)
// if price slippage is not a BigNumber e.g 'abc' return prePrice
if (!slippageAsBigNumber) return prePrice

// slippageAsBigNumber here is defined and is indeed a valid number
// convert slippage into fraction: (1 - (0.5/100)) = (1 - 0.005) = 99.995
const slippageAsFraction = ONE_BIG_NUMBER.minus(slippageAsBigNumber.div(ONE_HUNDRED_BIG_NUMBER))
const postSlippagePrice = prePrice.times(slippageAsFraction)

return postSlippagePrice
}