Skip to content

Commit

Permalink
feat: Update swap state structure and attach to UI (#3155)
Browse files Browse the repository at this point in the history
* refactor: mv settings state to own file

* chore: add default exports

* refactor: update swap state to match biz logic

* feat: copy biz logic to widgets

* Hook up UI to updated swap state

* fix: decimal inputs

* fix max slippage

* fix error in settings

* fix: typing errors

* revert: useBestTrade changes

* fix: use client side trade for widgets

* fix: exhaustive deps

* chore: add router-sdk

* fix: gate old web3 on widget env

* fix building errors

* update trade imports

* update hook naming for swap amount and currencies

* small changes

Co-authored-by: Zach Pomerantz <[email protected]>
  • Loading branch information
ianlapham and zzmp authored Jan 20, 2022
1 parent 053000e commit 034b3e3
Show file tree
Hide file tree
Showing 27 changed files with 538 additions and 351 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/smart-order-router": "^2.5.10",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
Expand Down Expand Up @@ -176,6 +175,7 @@
"@lingui/react": "^3.9.0",
"@popperjs/core": "^2.4.4",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useActiveWeb3React.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'

import { NetworkContextName } from '../constants/misc'

export default function useActiveWeb3React() {
const widgetsContext = useWidgetsWeb3React()
if (process.env.REACT_APP_IS_WIDGET) {
return useWidgetsWeb3React()
}

const interfaceContext = useWeb3React<Web3Provider>()
const interfaceNetworkContext = useWeb3React<Web3Provider>(
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
)

if (process.env.REACT_APP_IS_WIDGET) {
return widgetsContext
}
if (interfaceContext.active) {
return interfaceContext
}
Expand Down
30 changes: 20 additions & 10 deletions src/lib/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import JSBI from 'jsbi'
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'

Expand Down Expand Up @@ -67,23 +68,31 @@ export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(functi
})

interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
value: number | undefined
onChange: (input: number | undefined) => void
value: string
onChange: (input: string) => void
}

interface EnforcedNumericInputProps extends NumericInputProps {
// Validates nextUserInput; returns stringified value or undefined if valid, or null if invalid
enforcer: (nextUserInput: string) => string | undefined | null
// Validates nextUserInput; returns stringified value, or null if invalid
enforcer: (nextUserInput: string) => string | null
}

function isNumericallyEqual(a: string, b: string) {
const [aInteger, aDecimal] = a.split('.')
const [bInteger, bDecimal] = b.split('.')
return (
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
)
}

const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref
) {
// Allow value/onChange to use number by preventing a trailing decimal separator from triggering onChange
const [state, setState] = useState(value ?? '')
useEffect(() => {
if (+state !== value) {
if (!isNumericallyEqual(state, value)) {
setState(value ?? '')
}
}, [value, state, setState])
Expand All @@ -93,8 +102,8 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) {
setState(nextInput ?? '')
if (nextInput === undefined || +nextInput !== value) {
onChange(nextInput === undefined ? undefined : +nextInput)
if (!isNumericallyEqual(nextInput, value)) {
onChange(nextInput)
}
}
},
Expand All @@ -114,6 +123,7 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
pattern={pattern}
placeholder={props.placeholder || '0'}
minLength={1}
maxLength={79}
spellCheck="false"
ref={ref as any}
{...props}
Expand All @@ -125,7 +135,7 @@ const integerRegexp = /^\d*$/
const integerEnforcer = (nextUserInput: string) => {
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
const nextInput = parseInt(nextUserInput)
return isNaN(nextInput) ? undefined : nextInput.toString()
return isNaN(nextInput) ? '' : nextInput.toString()
}
return null
}
Expand All @@ -136,7 +146,7 @@ export const IntegerInput = forwardRef(function IntegerInput(props: NumericInput
const decimalRegexp = /^\d*(?:[.])?\d*$/
const decimalEnforcer = (nextUserInput: string) => {
if (nextUserInput === '') {
return undefined
return ''
} else if (nextUserInput === '.') {
return '0.'
} else if (decimalRegexp.test(nextUserInput)) {
Expand Down
53 changes: 35 additions & 18 deletions src/lib/components/Swap/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useAtomValue } from 'jotai/utils'
import { inputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useCallback } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'

import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'

const mockToken = new Token(1, '0x8b3192f5eebd8579568a2ed41e6feb402f93f73f', 9, 'STM', 'Saitama')
const mockCurrencyAmount = CurrencyAmount.fromRawAmount(mockToken, '134108514895957704114061')

const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em;
position: relative;
Expand All @@ -27,31 +27,48 @@ interface InputProps {
}

export default function Input({ disabled }: InputProps) {
const input = useAtomValue(inputAtom)
const setValue = useUpdateInputValue(inputAtom)
const setToken = useUpdateInputToken(inputAtom)
const balance = mockCurrencyAmount
const {
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(inputCurrencyAmount)

const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)

// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)

//TODO(ianlapham): mimic logic from app swap page
const mockApproved = true

const onMax = useCallback(() => {
if (balance) {
updateSwapInputAmount(balance.toExact())
}
}, [balance, updateSwapInputAmount])

return (
<InputColumn gap={0.5} approved={input.approved !== false}>
<InputColumn gap={0.5} approved={mockApproved}>
<Row>
<ThemedText.Subhead2 color="secondary">
<Trans>Trading</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput
input={input}
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onMax={balance ? () => setValue(1234) : undefined}
onChangeInput={setValue}
onChangeToken={setToken}
onMax={onMax}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
>
<ThemedText.Body2 color="secondary">
<Row>
{input.usdc ? `~ $${input.usdc.toLocaleString('en')}` : '-'}
{inputUSDC ? `~ $${inputUSDC.toFixed(2)}` : '-'}
{balance && (
<ThemedText.Body2 color={input.value && balance.lessThan(input.value) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{balance.toExact()}</span>
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</ThemedText.Body2>
)}
</Row>
Expand Down
68 changes: 44 additions & 24 deletions src/lib/components/Swap/Output.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Trans } from '@lingui/macro'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useCurrencyColor, { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
import { ReactNode, useCallback, useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'

import Column from '../Column'
import Row from '../Row'
Expand Down Expand Up @@ -33,32 +37,41 @@ interface OutputProps {
}

export default function Output({ disabled, children }: OutputProps) {
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const setValue = useUpdateInputValue(outputAtom)
const setToken = useUpdateInputToken(outputAtom)
const balance = 123.45
const {
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo()

const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)

const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useCurrencyColor(output.token)
usePrefetchCurrencyColor(input.token) // extract eagerly in case of reversal
const dynamicColor = useCurrencyColor(swapOutputCurrency)
const color = overrideColor || dynamicColor
const hasColor = output.token ? Boolean(color) || null : false

const change = useMemo(() => {
if (input.usdc && output.usdc) {
const change = output.usdc / input.usdc - 1
const percent = (change * 100).toPrecision(3)
return change > 0 ? ` (+${percent}%)` : `(${percent}%)`
}
return ''
}, [input, output])
// different state true/null/false allow smoother color transition
const hasColor = swapOutputCurrency ? Boolean(color) || null : false

const inputUSDC = useUSDCValue(inputCurrencyAmount)
const outputUSDC = useUSDCValue(outputCurrencyAmount)

const priceImpact = useMemo(() => {
const computedChange = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDC, outputUSDC])

const usdc = useMemo(() => {
if (output.usdc) {
return `~ $${output.usdc.toLocaleString('en')}${change}`
if (outputUSDC) {
return `~ $${outputUSDC.toFixed(2)}${priceImpact}`
}
return '-'
}, [change, output])
}, [priceImpact, outputUSDC])

const onMax = useCallback(() => {
if (balance) {
updateSwapOutputAmount(balance.toExact())
}
}, [balance, updateSwapOutputAmount])

return (
<DynamicThemeProvider color={color}>
Expand All @@ -68,13 +81,20 @@ export default function Output({ disabled, children }: OutputProps) {
<Trans>For</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput input={output} disabled={disabled} onChangeInput={setValue} onChangeToken={setToken}>
<TokenInput
currency={swapOutputCurrency}
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onMax={onMax}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
>
<ThemedText.Body2 color="secondary">
<Row>
{usdc}
{balance && (
<span>
Balance: <span style={{ userSelect: 'text' }}>{balance}</span>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</span>
)}
</Row>
Expand Down
13 changes: 4 additions & 9 deletions src/lib/components/Swap/ReverseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useAtom } from 'jotai'
import { useSwitchSwapCurrencies } from 'lib/hooks/swap'
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
import { stateAtom } from 'lib/state/swap'
import styled, { Layer } from 'lib/theme'
import { useCallback, useState } from 'react'

Expand Down Expand Up @@ -47,16 +46,12 @@ const StyledReverseButton = styled(Button)<{ turns: number }>`
`

export default function ReverseButton({ disabled }: { disabled?: boolean }) {
const [state, setState] = useAtom(stateAtom)
const [turns, setTurns] = useState(0)
const switchCurrencies = useSwitchSwapCurrencies()
const onClick = useCallback(() => {
const { input, output } = state
setState((state) => {
state.input = output
state.output = input
})
switchCurrencies()
setTurns((turns) => ++turns)
}, [state, setState])
}, [switchCurrencies])

return (
<ReverseRow justify="center">
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/Swap/Settings/MaxSlippageSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { t, Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { Check, LargeIcon } from 'lib/icons'
import { MaxSlippage, maxSlippageAtom } from 'lib/state/swap'
import { MaxSlippage, maxSlippageAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useRef } from 'react'

Expand Down Expand Up @@ -78,8 +78,8 @@ export default function MaxSlippageSelect() {
<InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}>
<DecimalInput
size={custom === undefined ? undefined : 5}
value={custom}
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom })}
value={custom?.toString() ?? ''}
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom: custom ? parseFloat(custom) : undefined })}
placeholder={t`Custom`}
ref={input}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/Swap/Settings/MockToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { mockTogglableAtom } from 'lib/state/swap'
import { mockTogglableAtom } from 'lib/state/settings'

import Row from '../../Row'
import Toggle from '../../Toggle'
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/Swap/Settings/TransactionTtlInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/swap'
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import { useRef } from 'react'

Expand All @@ -25,8 +25,8 @@ export default function TransactionTtlInput() {
<Input onClick={() => input.current?.focus()}>
<IntegerInput
placeholder={TRANSACTION_TTL_DEFAULT.toString()}
value={transactionTtl}
onChange={(value) => setTransactionTtl(value ?? 0)}
value={transactionTtl?.toString() ?? ''}
onChange={(value) => setTransactionTtl(value ? parseFloat(value) : 0)}
ref={input}
/>
<Trans>minutes</Trans>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/Swap/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import { useResetAtom } from 'jotai/utils'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Settings as SettingsIcon } from 'lib/icons'
import { settingsAtom } from 'lib/state/swap'
import { settingsAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import React, { useState } from 'react'

Expand Down
Loading

0 comments on commit 034b3e3

Please sign in to comment.