Commit 034b3e3e authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

feat: Update swap state structure and attach to UI (#3155)

* 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: default avatarZach Pomerantz <zzmp@uniswap.org>
parent 053000e5
/* eslint-disable react-hooks/rules-of-hooks */
import { Web3Provider } from '@ethersproject/providers' import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React' import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
...@@ -5,15 +6,15 @@ import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React' ...@@ -5,15 +6,15 @@ import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
import { NetworkContextName } from '../constants/misc' import { NetworkContextName } from '../constants/misc'
export default function useActiveWeb3React() { export default function useActiveWeb3React() {
const widgetsContext = useWidgetsWeb3React() if (process.env.REACT_APP_IS_WIDGET) {
return useWidgetsWeb3React()
}
const interfaceContext = useWeb3React<Web3Provider>() const interfaceContext = useWeb3React<Web3Provider>()
const interfaceNetworkContext = useWeb3React<Web3Provider>( const interfaceNetworkContext = useWeb3React<Web3Provider>(
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
) )
if (process.env.REACT_APP_IS_WIDGET) {
return widgetsContext
}
if (interfaceContext.active) { if (interfaceContext.active) {
return interfaceContext return interfaceContext
} }
......
import JSBI from 'jsbi'
import styled, { css } from 'lib/theme' import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react' import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
...@@ -67,23 +68,31 @@ export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(functi ...@@ -67,23 +68,31 @@ export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(functi
}) })
interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> { interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
value: number | undefined value: string
onChange: (input: number | undefined) => void onChange: (input: string) => void
} }
interface EnforcedNumericInputProps extends NumericInputProps { interface EnforcedNumericInputProps extends NumericInputProps {
// Validates nextUserInput; returns stringified value or undefined if valid, or null if invalid // Validates nextUserInput; returns stringified value, or null if invalid
enforcer: (nextUserInput: string) => string | undefined | null 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( const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps, { value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref ref
) { ) {
// Allow value/onChange to use number by preventing a trailing decimal separator from triggering onChange
const [state, setState] = useState(value ?? '') const [state, setState] = useState(value ?? '')
useEffect(() => { useEffect(() => {
if (+state !== value) { if (!isNumericallyEqual(state, value)) {
setState(value ?? '') setState(value ?? '')
} }
}, [value, state, setState]) }, [value, state, setState])
...@@ -93,8 +102,8 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun ...@@ -93,8 +102,8 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
const nextInput = enforcer(event.target.value.replace(/,/g, '.')) const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) { if (nextInput !== null) {
setState(nextInput ?? '') setState(nextInput ?? '')
if (nextInput === undefined || +nextInput !== value) { if (!isNumericallyEqual(nextInput, value)) {
onChange(nextInput === undefined ? undefined : +nextInput) onChange(nextInput)
} }
} }
}, },
...@@ -114,6 +123,7 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun ...@@ -114,6 +123,7 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
pattern={pattern} pattern={pattern}
placeholder={props.placeholder || '0'} placeholder={props.placeholder || '0'}
minLength={1} minLength={1}
maxLength={79}
spellCheck="false" spellCheck="false"
ref={ref as any} ref={ref as any}
{...props} {...props}
...@@ -125,7 +135,7 @@ const integerRegexp = /^\d*$/ ...@@ -125,7 +135,7 @@ const integerRegexp = /^\d*$/
const integerEnforcer = (nextUserInput: string) => { const integerEnforcer = (nextUserInput: string) => {
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) { if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
const nextInput = parseInt(nextUserInput) const nextInput = parseInt(nextUserInput)
return isNaN(nextInput) ? undefined : nextInput.toString() return isNaN(nextInput) ? '' : nextInput.toString()
} }
return null return null
} }
...@@ -136,7 +146,7 @@ export const IntegerInput = forwardRef(function IntegerInput(props: NumericInput ...@@ -136,7 +146,7 @@ export const IntegerInput = forwardRef(function IntegerInput(props: NumericInput
const decimalRegexp = /^\d*(?:[.])?\d*$/ const decimalRegexp = /^\d*(?:[.])?\d*$/
const decimalEnforcer = (nextUserInput: string) => { const decimalEnforcer = (nextUserInput: string) => {
if (nextUserInput === '') { if (nextUserInput === '') {
return undefined return ''
} else if (nextUserInput === '.') { } else if (nextUserInput === '.') {
return '0.' return '0.'
} else if (decimalRegexp.test(nextUserInput)) { } else if (decimalRegexp.test(nextUserInput)) {
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useAtomValue } from 'jotai/utils' import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { inputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap' import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
import { useCallback } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../Column' import Column from '../Column'
import Row from '../Row' import Row from '../Row'
import TokenImg from '../TokenImg' import TokenImg from '../TokenImg'
import TokenInput from './TokenInput' 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 }>` const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em; margin: 0.75em;
position: relative; position: relative;
...@@ -27,31 +27,48 @@ interface InputProps { ...@@ -27,31 +27,48 @@ interface InputProps {
} }
export default function Input({ disabled }: InputProps) { export default function Input({ disabled }: InputProps) {
const input = useAtomValue(inputAtom) const {
const setValue = useUpdateInputValue(inputAtom) currencyBalances: { [Field.INPUT]: balance },
const setToken = useUpdateInputToken(inputAtom) currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
const balance = mockCurrencyAmount } = 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 ( return (
<InputColumn gap={0.5} approved={input.approved !== false}> <InputColumn gap={0.5} approved={mockApproved}>
<Row> <Row>
<ThemedText.Subhead2 color="secondary"> <ThemedText.Subhead2 color="secondary">
<Trans>Trading</Trans> <Trans>Trading</Trans>
</ThemedText.Subhead2> </ThemedText.Subhead2>
</Row> </Row>
<TokenInput <TokenInput
input={input} currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled} disabled={disabled}
onMax={balance ? () => setValue(1234) : undefined} onMax={onMax}
onChangeInput={setValue} onChangeInput={updateSwapInputAmount}
onChangeToken={setToken} onChangeCurrency={updateSwapInputCurrency}
> >
<ThemedText.Body2 color="secondary"> <ThemedText.Body2 color="secondary">
<Row> <Row>
{input.usdc ? `~ $${input.usdc.toLocaleString('en')}` : '-'} {inputUSDC ? `~ $${inputUSDC.toFixed(2)}` : '-'}
{balance && ( {balance && (
<ThemedText.Body2 color={input.value && balance.lessThan(input.value) ? 'error' : undefined}> <ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{balance.toExact()}</span> Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</ThemedText.Body2> </ThemedText.Body2>
)} )}
</Row> </Row>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai' import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import useCurrencyColor, { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor' import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap' import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme' 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 Column from '../Column'
import Row from '../Row' import Row from '../Row'
...@@ -33,32 +37,41 @@ interface OutputProps { ...@@ -33,32 +37,41 @@ interface OutputProps {
} }
export default function Output({ disabled, children }: OutputProps) { export default function Output({ disabled, children }: OutputProps) {
const input = useAtomValue(inputAtom) const {
const output = useAtomValue(outputAtom) currencyBalances: { [Field.OUTPUT]: balance },
const setValue = useUpdateInputValue(outputAtom) currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
const setToken = useUpdateInputToken(outputAtom) } = useSwapInfo()
const balance = 123.45
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
const overrideColor = useAtomValue(colorAtom) const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useCurrencyColor(output.token) const dynamicColor = useCurrencyColor(swapOutputCurrency)
usePrefetchCurrencyColor(input.token) // extract eagerly in case of reversal
const color = overrideColor || dynamicColor const color = overrideColor || dynamicColor
const hasColor = output.token ? Boolean(color) || null : false
const change = useMemo(() => { // different state true/null/false allow smoother color transition
if (input.usdc && output.usdc) { const hasColor = swapOutputCurrency ? Boolean(color) || null : false
const change = output.usdc / input.usdc - 1
const percent = (change * 100).toPrecision(3) const inputUSDC = useUSDCValue(inputCurrencyAmount)
return change > 0 ? ` (+${percent}%)` : `(${percent}%)` const outputUSDC = useUSDCValue(outputCurrencyAmount)
}
return '' const priceImpact = useMemo(() => {
}, [input, output]) const computedChange = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDC, outputUSDC])
const usdc = useMemo(() => { const usdc = useMemo(() => {
if (output.usdc) { if (outputUSDC) {
return `~ $${output.usdc.toLocaleString('en')}${change}` return `~ $${outputUSDC.toFixed(2)}${priceImpact}`
} }
return '-' return '-'
}, [change, output]) }, [priceImpact, outputUSDC])
const onMax = useCallback(() => {
if (balance) {
updateSwapOutputAmount(balance.toExact())
}
}, [balance, updateSwapOutputAmount])
return ( return (
<DynamicThemeProvider color={color}> <DynamicThemeProvider color={color}>
...@@ -68,13 +81,20 @@ export default function Output({ disabled, children }: OutputProps) { ...@@ -68,13 +81,20 @@ export default function Output({ disabled, children }: OutputProps) {
<Trans>For</Trans> <Trans>For</Trans>
</ThemedText.Subhead2> </ThemedText.Subhead2>
</Row> </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"> <ThemedText.Body2 color="secondary">
<Row> <Row>
{usdc} {usdc}
{balance && ( {balance && (
<span> <span>
Balance: <span style={{ userSelect: 'text' }}>{balance}</span> Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</span> </span>
)} )}
</Row> </Row>
......
import { useAtom } from 'jotai' import { useSwitchSwapCurrencies } from 'lib/hooks/swap'
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons' import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
import { stateAtom } from 'lib/state/swap'
import styled, { Layer } from 'lib/theme' import styled, { Layer } from 'lib/theme'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
...@@ -47,16 +46,12 @@ const StyledReverseButton = styled(Button)<{ turns: number }>` ...@@ -47,16 +46,12 @@ const StyledReverseButton = styled(Button)<{ turns: number }>`
` `
export default function ReverseButton({ disabled }: { disabled?: boolean }) { export default function ReverseButton({ disabled }: { disabled?: boolean }) {
const [state, setState] = useAtom(stateAtom)
const [turns, setTurns] = useState(0) const [turns, setTurns] = useState(0)
const switchCurrencies = useSwitchSwapCurrencies()
const onClick = useCallback(() => { const onClick = useCallback(() => {
const { input, output } = state switchCurrencies()
setState((state) => {
state.input = output
state.output = input
})
setTurns((turns) => ++turns) setTurns((turns) => ++turns)
}, [state, setState]) }, [switchCurrencies])
return ( return (
<ReverseRow justify="center"> <ReverseRow justify="center">
......
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { Check, LargeIcon } from 'lib/icons' 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 styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useRef } from 'react' import { ReactNode, useCallback, useRef } from 'react'
...@@ -78,8 +78,8 @@ export default function MaxSlippageSelect() { ...@@ -78,8 +78,8 @@ export default function MaxSlippageSelect() {
<InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}> <InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}>
<DecimalInput <DecimalInput
size={custom === undefined ? undefined : 5} size={custom === undefined ? undefined : 5}
value={custom} value={custom?.toString() ?? ''}
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom })} onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom: custom ? parseFloat(custom) : undefined })}
placeholder={t`Custom`} placeholder={t`Custom`}
ref={input} ref={input}
/> />
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { mockTogglableAtom } from 'lib/state/swap' import { mockTogglableAtom } from 'lib/state/settings'
import Row from '../../Row' import Row from '../../Row'
import Toggle from '../../Toggle' import Toggle from '../../Toggle'
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai' 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 styled, { ThemedText } from 'lib/theme'
import { useRef } from 'react' import { useRef } from 'react'
...@@ -25,8 +25,8 @@ export default function TransactionTtlInput() { ...@@ -25,8 +25,8 @@ export default function TransactionTtlInput() {
<Input onClick={() => input.current?.focus()}> <Input onClick={() => input.current?.focus()}>
<IntegerInput <IntegerInput
placeholder={TRANSACTION_TTL_DEFAULT.toString()} placeholder={TRANSACTION_TTL_DEFAULT.toString()}
value={transactionTtl} value={transactionTtl?.toString() ?? ''}
onChange={(value) => setTransactionTtl(value ?? 0)} onChange={(value) => setTransactionTtl(value ? parseFloat(value) : 0)}
ref={input} ref={input}
/> />
<Trans>minutes</Trans> <Trans>minutes</Trans>
......
...@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro' ...@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import { useResetAtom } from 'jotai/utils' import { useResetAtom } from 'jotai/utils'
import useScrollbar from 'lib/hooks/useScrollbar' import useScrollbar from 'lib/hooks/useScrollbar'
import { Settings as SettingsIcon } from 'lib/icons' 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 styled, { ThemedText } from 'lib/theme'
import React, { useState } from 'react' import React, { useState } from 'react'
......
import { tokens } from '@uniswap/default-token-list' import { tokens } from '@uniswap/default-token-list'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
import { transactionAtom } from 'lib/state/swap' import JSBI from 'jsbi'
import { swapTransactionAtom } from 'lib/state/swap'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSelect } from 'react-cosmos/fixture' import { useSelect } from 'react-cosmos/fixture'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
...@@ -19,15 +21,15 @@ const UNI = (function () { ...@@ -19,15 +21,15 @@ const UNI = (function () {
})() })()
function Fixture() { function Fixture() {
const setTransaction = useUpdateAtom(transactionAtom) const setTransaction = useUpdateAtom(swapTransactionAtom)
const [state] = useSelect('state', { const [state] = useSelect('state', {
options: ['PENDING', 'ERROR', 'SUCCESS'], options: ['PENDING', 'ERROR', 'SUCCESS'],
}) })
useEffect(() => { useEffect(() => {
setTransaction({ setTransaction({
input: { token: ETH, value: 1 }, input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)),
output: { token: UNI, value: 42 }, output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)),
receipt: '', receipt: '',
timestamp: Date.now(), timestamp: Date.now(),
}) })
...@@ -36,8 +38,8 @@ function Fixture() { ...@@ -36,8 +38,8 @@ function Fixture() {
switch (state) { switch (state) {
case 'PENDING': case 'PENDING':
setTransaction({ setTransaction({
input: { token: ETH, value: 1 }, input: CurrencyAmount.fromRawAmount(ETH, JSBI.BigInt(1)),
output: { token: UNI, value: 42 }, output: CurrencyAmount.fromRawAmount(UNI, JSBI.BigInt(42)),
receipt: '', receipt: '',
timestamp: Date.now(), timestamp: Date.now(),
}) })
......
...@@ -3,7 +3,7 @@ import { useAtomValue } from 'jotai/utils' ...@@ -3,7 +3,7 @@ import { useAtomValue } from 'jotai/utils'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog' import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons' import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { Transaction, transactionAtom } from 'lib/state/swap' import { SwapTransaction, swapTransactionAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
...@@ -24,7 +24,7 @@ const TransactionRow = styled(Row)` ...@@ -24,7 +24,7 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse; flex-direction: row-reverse;
` `
function ElapsedTime({ tx }: { tx: Transaction | null }) { function ElapsedTime({ tx }: { tx: SwapTransaction | null }) {
const [elapsedMs, setElapsedMs] = useState(0) const [elapsedMs, setElapsedMs] = useState(0)
useInterval( useInterval(
() => { () => {
...@@ -64,7 +64,7 @@ const EtherscanA = styled.a` ...@@ -64,7 +64,7 @@ const EtherscanA = styled.a`
` `
interface TransactionStatusProps extends StatusProps { interface TransactionStatusProps extends StatusProps {
tx: Transaction | null tx: SwapTransaction | null
} }
function TransactionStatus({ tx, onClose }: TransactionStatusProps) { function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
...@@ -100,7 +100,7 @@ interface StatusProps { ...@@ -100,7 +100,7 @@ interface StatusProps {
} }
export default function TransactionStatusDialog({ onClose }: StatusProps) { export default function TransactionStatusDialog({ onClose }: StatusProps) {
const tx = useAtomValue(transactionAtom) const tx = useAtomValue(swapTransactionAtom)
return tx?.status instanceof Error ? ( return tx?.status instanceof Error ? (
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} /> <ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
......
...@@ -2,9 +2,8 @@ import { tokens } from '@uniswap/default-token-list' ...@@ -2,9 +2,8 @@ import { tokens } from '@uniswap/default-token-list'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
import { Field, outputAtom, stateAtom } from 'lib/state/swap' import { Field, swapAtom } from 'lib/state/swap'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useValue } from 'react-cosmos/fixture'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
...@@ -19,36 +18,24 @@ const UNI = (function () { ...@@ -19,36 +18,24 @@ const UNI = (function () {
})() })()
function Fixture() { function Fixture() {
const setState = useUpdateAtom(stateAtom) const [initialized, setInitialized] = useState(false)
const [, setInitialized] = useState(false) const setState = useUpdateAtom(swapAtom)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { useEffect(() => {
setState({ setState({
activeInput: Field.INPUT, independentField: Field.INPUT,
input: { token: ETH, value: 1, usdc: 4195 }, amount: '1',
output: { token: UNI, value: 42, usdc: 42 }, [Field.INPUT]: ETH,
swap: { [Field.OUTPUT]: UNI,
lpFee: 0.0005,
integratorFee: 0.00025,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: 4190,
},
}) })
setInitialized(true) setInitialized(true)
}) })
const setOutput = useUpdateAtom(outputAtom) return initialized ? (
const [price] = useValue('output value', { defaultValue: 4200 })
useEffect(() => {
setState((state) => ({ ...state, output: { token: UNI, value: price, usdc: price } }))
}, [price, setOutput, setState])
return (
<Modal color="dialog"> <Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} /> <SummaryDialog onConfirm={() => void 0} />
</Modal> </Modal>
) ) : null
} }
export default <Fixture /> export default <Fixture />
import { t } from '@lingui/macro' import { t } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { State } from 'lib/state/swap' import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { settingsAtom } from 'lib/state/settings'
import { integratorFeeAtom } from 'lib/state/swap'
import { ThemedText } from 'lib/theme' import { ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId'
import Row from '../../Row' import Row from '../../Row'
...@@ -23,31 +27,31 @@ function Detail({ label, value }: DetailProps) { ...@@ -23,31 +27,31 @@ function Detail({ label, value }: DetailProps) {
} }
interface DetailsProps { interface DetailsProps {
swap: Required<State>['swap']
input: Currency input: Currency
output: Currency output: Currency
} }
export default function Details({ export default function Details({ input, output }: DetailsProps) {
input: { symbol: inputSymbol },
output: { symbol: outputSymbol },
swap,
}: DetailsProps) {
const integrator = window.location.hostname const integrator = window.location.hostname
const { maxSlippage } = useAtomValue(settingsAtom)
const [integratorFee] = useAtom(integratorFeeAtom)
const details = useMemo((): [string, string][] => { const details = useMemo((): [string, string][] => {
// @TODO(ianlapham) = update details to pull derived value from useDerivedSwapInfo
return [ return [
[t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`], // [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, swap.integratorFee && `${swap.integratorFee} ${inputSymbol}`], [t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(input)}`],
[t`Price impact`, `${swap.priceImpact}%`], // [t`Price impact`, `${swap.priceImpact}%`],
[t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`], // [t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`],
[t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`], // [t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`],
[t`Slippage tolerance`, `${swap.slippageTolerance}%`], [t`Slippage tolerance`, `${maxSlippage}%`],
].filter(isDetail) ].filter(isDetail)
function isDetail(detail: unknown[]): detail is [string, string] { function isDetail(detail: unknown[]): detail is [string, string] {
return Boolean(detail[1]) return Boolean(detail[1])
} }
}, [inputSymbol, outputSymbol, swap, integrator]) }, [input, integrator, integratorFee, maxSlippage])
return ( return (
<> <>
{details.map(([label, detail]) => ( {details.map(([label, detail]) => (
......
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { ArrowRight } from 'lib/icons' import { ArrowRight } from 'lib/icons'
import { Input } from 'lib/state/swap'
import styled from 'lib/theme' import styled from 'lib/theme'
import { ThemedText } from 'lib/theme' import { ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import Column from '../../Column' import Column from '../../Column'
import Row from '../../Row' import Row from '../../Row'
...@@ -13,7 +15,7 @@ const Percent = styled.span<{ gain: boolean }>` ...@@ -13,7 +15,7 @@ const Percent = styled.span<{ gain: boolean }>`
` `
interface TokenValueProps { interface TokenValueProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input input: CurrencyAmount<Currency>
usdc?: boolean usdc?: boolean
change?: number change?: number
} }
...@@ -26,18 +28,21 @@ function TokenValue({ input, usdc, change }: TokenValueProps) { ...@@ -26,18 +28,21 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
} }
return undefined return undefined
}, [change]) }, [change])
const usdcAmount = useUSDCValue(input)
return ( return (
<Column justify="flex-start"> <Column justify="flex-start">
<Row gap={0.375} justify="flex-start"> <Row gap={0.375} justify="flex-start">
<TokenImg token={input.token} /> <TokenImg token={input.currency} />
<ThemedText.Body2> <ThemedText.Body2>
{input.value} {input.token.symbol} {input.toSignificant(6)} {input.currency.symbol}
</ThemedText.Body2> </ThemedText.Body2>
</Row> </Row>
{usdc && input.usdc && ( {usdc && usdcAmount && (
<Row justify="flex-start"> <Row justify="flex-start">
<ThemedText.Caption color="secondary"> <ThemedText.Caption color="secondary">
~ ${input.usdc.toLocaleString('en')} ~ ${usdcAmount.toFixed(2)}
{change && <Percent gain={change > 0}> {percent}</Percent>} {change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption> </ThemedText.Caption>
</Row> </Row>
...@@ -47,23 +52,25 @@ function TokenValue({ input, usdc, change }: TokenValueProps) { ...@@ -47,23 +52,25 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
} }
interface SummaryProps { interface SummaryProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input input: CurrencyAmount<Currency>
output: Required<Pick<Input, 'token' | 'value'>> & Input output: CurrencyAmount<Currency>
usdc?: boolean usdc?: boolean
} }
export default function Summary({ input, output, usdc }: SummaryProps) { export default function Summary({ input, output, usdc }: SummaryProps) {
const change = useMemo(() => { const inputUSDCValue = useUSDCValue(input)
if (usdc && input.usdc && output.usdc) { const outputUSDCValue = useUSDCValue(output)
return output.usdc / input.usdc - 1
} const priceImpact = useMemo(() => {
return undefined const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
}, [usdc, input.usdc, output.usdc]) return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDCValue, outputUSDCValue])
return ( return (
<Row gap={usdc ? 1 : 0.25}> <Row gap={usdc ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} /> <TokenValue input={input} usdc={usdc} />
<ArrowRight /> <ArrowRight />
<TokenValue input={output} usdc={usdc} change={change} /> <TokenValue input={output} usdc={usdc} change={priceImpact} />
</Row> </Row>
) )
} }
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button' import { IconButton } from 'lib/components/Button'
import { useSwapInfo } from 'lib/hooks/swap'
import useScrollbar from 'lib/hooks/useScrollbar' import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando, Info } from 'lib/icons' import { Expando, Info } from 'lib/icons'
import { Input, inputAtom, outputAtom, swapAtom } from 'lib/state/swap' import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
import { useMemo, useState } from 'react' import { useState } from 'react'
import ActionButton from '../../ActionButton' import ActionButton from '../../ActionButton'
import Column from '../../Column' import Column from '../../Column'
...@@ -17,10 +17,6 @@ import Summary from './Summary' ...@@ -17,10 +17,6 @@ import Summary from './Summary'
export default Summary export default Summary
function asInput(input: Input): (Required<Pick<Input, 'token' | 'value'>> & Input) | undefined {
return input.token && input.value ? (input as Required<Pick<Input, 'token' | 'value'>>) : undefined
}
const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> } const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
const SummaryColumn = styled(Column)`` const SummaryColumn = styled(Column)``
...@@ -79,15 +75,14 @@ interface SummaryDialogProps { ...@@ -79,15 +75,14 @@ interface SummaryDialogProps {
} }
export function SummaryDialog({ onConfirm }: SummaryDialogProps) { export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
const swap = useAtomValue(swapAtom) const {
const partialInput = useAtomValue(inputAtom) trade: { trade },
const partialOutput = useAtomValue(outputAtom) currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount },
const input = asInput(partialInput) currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
const output = asInput(partialOutput) } = useSwapInfo()
const price = useMemo(() => { const price = trade?.executionPrice
return input && output ? output.value / input.value : undefined
}, [input, output])
const [confirmedPrice, confirmPrice] = useState(price) const [confirmedPrice, confirmPrice] = useState(price)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
...@@ -96,7 +91,7 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { ...@@ -96,7 +91,7 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
const scrollbar = useScrollbar(details) const scrollbar = useScrollbar(details)
if (!(input && output && swap)) { if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null return null
} }
...@@ -105,9 +100,9 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { ...@@ -105,9 +100,9 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<Header title={<Trans>Swap summary</Trans>} ruled /> <Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}> <Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center"> <SummaryColumn gap={0.75} flex justify="center">
<Summary input={input} output={output} usdc={true} /> <Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption> <ThemedText.Caption>
1 {input.token.symbol} = {price} {output.token.symbol} 1 {inputCurrency.symbol} = {price} {outputCurrency.symbol}
</ThemedText.Caption> </ThemedText.Caption>
</SummaryColumn> </SummaryColumn>
<Rule /> <Rule />
...@@ -124,12 +119,12 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { ...@@ -124,12 +119,12 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<Rule /> <Rule />
<DetailsColumn> <DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}> <Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details input={input.token} output={output.token} swap={swap} /> <Details input={inputCurrency} output={outputCurrency} />
</Column> </Column>
</DetailsColumn> </DetailsColumn>
<Estimate color="secondary"> <Estimate color="secondary">
<Trans>Output is estimated.</Trans>{' '} <Trans>Output is estimated.</Trans> {/* //@TODO(ianlapham): update with actual recieved values */}
{swap?.minimumReceived && ( {/* {swap?.minimumReceived && (
<Trans> <Trans>
You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert. You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert.
</Trans> </Trans>
...@@ -138,7 +133,7 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { ...@@ -138,7 +133,7 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<Trans> <Trans>
You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert. You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert.
</Trans> </Trans>
)} )} */}
</Estimate> </Estimate>
<ActionButton <ActionButton
onClick={onConfirm} onClick={onConfirm}
......
import { tokens } from '@uniswap/default-token-list' import { tokens } from '@uniswap/default-token-list'
import { useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useValue } from 'react-cosmos/fixture' import { useValue } from 'react-cosmos/fixture'
...@@ -18,42 +16,6 @@ const validateColor = (() => { ...@@ -18,42 +16,6 @@ const validateColor = (() => {
})() })()
function Fixture() { function Fixture() {
const [input, setInput] = useAtom(inputAtom)
const [output, setOutput] = useAtom(outputAtom)
const [swap, setSwap] = useAtom(swapAtom)
const [priceFetched] = useValue('price fetched', { defaultValue: false })
useEffect(() => {
if (priceFetched && input.token && output.token) {
const inputValue = input.value || 1
const inputUsdc = input.usdc || inputValue
const outputValue = output.value || 1
const outputUsdc = output.usdc || outputValue
if (!(inputValue === input.value && inputUsdc === input.usdc)) {
setInput({ ...input, value: inputValue, usdc: inputUsdc })
}
if (!(outputValue === output.value && outputUsdc === output.usdc)) {
setOutput({ ...output, value: outputValue, usdc: outputUsdc })
}
if (!swap || swap.minimumReceived !== outputValue * 0.995) {
setSwap({
lpFee: 0.0005,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: outputValue * 0.995,
})
}
} else if (swap) {
setSwap(undefined)
}
}, [input, output, priceFetched, setInput, setOutput, setSwap, swap])
const [tokenApproved] = useValue('token approved', { defaultValue: true })
useEffect(() => {
if (tokenApproved !== input.approved) {
setInput({ ...input, approved: tokenApproved })
}
}, [input, setInput, tokenApproved])
const setColor = useUpdateAtom(colorAtom) const setColor = useUpdateAtom(colorAtom)
const [color] = useValue('token color', { defaultValue: '' }) const [color] = useValue('token color', { defaultValue: '' })
useEffect(() => { useEffect(() => {
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import ActionButton from '../ActionButton' import ActionButton from '../ActionButton'
...@@ -9,6 +7,8 @@ import { StatusDialog } from './Status' ...@@ -9,6 +7,8 @@ import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary' import { SummaryDialog } from './Summary'
const mockBalance = 123.45 const mockBalance = 123.45
const mockInputAmount = 10
const mockApproved = true
enum Mode { enum Mode {
NONE, NONE,
...@@ -17,23 +17,21 @@ enum Mode { ...@@ -17,23 +17,21 @@ enum Mode {
} }
export default function SwapButton() { export default function SwapButton() {
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const [mode, setMode] = useState(Mode.NONE) const [mode, setMode] = useState(Mode.NONE)
//@TODO(ianlapham): update this to refer to balances and use real symbol
const actionProps = useMemo(() => { const actionProps = useMemo(() => {
if (swap && input.token && input.value && output.token && output.value && input.value <= balance) { if (mockInputAmount < mockBalance) {
if (input.approved) { if (mockApproved) {
return {} return {}
} else { } else {
return { return {
updated: { message: <Trans>Approve {input.token.symbol} first</Trans>, action: <Trans>Approve</Trans> }, updated: { message: <Trans>Approve symbol first</Trans>, action: <Trans>Approve</Trans> },
} }
} }
} }
return { disabled: true } return { disabled: true }
}, [balance, input.approved, input.token, input.value, output.token, output.value, swap]) }, [])
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {
// TODO: Send the tx to the connected wallet. // TODO: Send the tx to the connected wallet.
setMode(Mode.STATUS) setMode(Mode.STATUS)
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { Input } from 'lib/state/swap'
import styled, { keyframes, ThemedText } from 'lib/theme' import styled, { keyframes, ThemedText } from 'lib/theme'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react' import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
...@@ -45,20 +44,22 @@ const MaxButton = styled(Button)` ...@@ -45,20 +44,22 @@ const MaxButton = styled(Button)`
` `
interface TokenInputProps { interface TokenInputProps {
input: Input currency?: Currency
amount: string
disabled?: boolean disabled?: boolean
onMax?: () => void onMax?: () => void
onChangeInput: (input: number | undefined) => void onChangeInput: (input: string) => void
onChangeToken: (token: Currency) => void onChangeCurrency: (currency: Currency) => void
children: ReactNode children: ReactNode
} }
export default function TokenInput({ export default function TokenInput({
input: { value, token }, currency,
amount,
disabled, disabled,
onMax, onMax,
onChangeInput, onChangeInput,
onChangeToken, onChangeCurrency,
children, children,
}: TokenInputProps) { }: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null) const max = useRef<HTMLButtonElement>(null)
...@@ -74,10 +75,10 @@ export default function TokenInput({ ...@@ -74,10 +75,10 @@ export default function TokenInput({
<TokenInputRow gap={0.5} onBlur={onBlur}> <TokenInputRow gap={0.5} onBlur={onBlur}>
<ThemedText.H2> <ThemedText.H2>
<ValueInput <ValueInput
value={value} value={amount}
onFocus={onFocus} onFocus={onFocus}
onChange={onChangeInput} onChange={onChangeInput}
disabled={disabled || !token} disabled={disabled || !currency}
></ValueInput> ></ValueInput>
</ThemedText.H2> </ThemedText.H2>
{showMax && ( {showMax && (
...@@ -87,7 +88,7 @@ export default function TokenInput({ ...@@ -87,7 +88,7 @@ export default function TokenInput({
</ThemedText.ButtonMedium> </ThemedText.ButtonMedium>
</MaxButton> </MaxButton>
)} )}
<TokenSelect value={token} collapsed={showMax} disabled={disabled} onSelect={onChangeToken} /> <TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onChangeCurrency} />
</TokenInputRow> </TokenInputRow>
{children} {children}
</Column> </Column>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useSwapInfo } from 'lib/hooks/swap'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons' import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
import { Field, Input, inputAtom, outputAtom, stateAtom, swapAtom } from 'lib/state/swap' import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText, ThemeProvider } from 'lib/theme' import styled, { ThemedText, ThemeProvider } from 'lib/theme'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../Button' import { TextButton } from '../Button'
import Row from '../Row' import Row from '../Row'
import Rule from '../Rule' import Rule from '../Rule'
import Tooltip from '../Tooltip' import Tooltip from '../Tooltip'
const mockBalance = 123.45 const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
function RoutingTooltip() { function RoutingTooltip() {
return ( return (
...@@ -24,30 +31,37 @@ function RoutingTooltip() { ...@@ -24,30 +31,37 @@ function RoutingTooltip() {
) )
} }
type FilledInput = Input & Required<Pick<Input, 'token' | 'value'>>
function asFilledInput(input: Input): FilledInput | undefined {
return input.token && input.value ? (input as FilledInput) : undefined
}
interface LoadedStateProps { interface LoadedStateProps {
input: FilledInput inputAmount: CurrencyAmount<Currency>
output: FilledInput outputAmount: CurrencyAmount<Currency>
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
} }
function LoadedState({ input, output }: LoadedStateProps) { function LoadedState({ inputAmount, outputAmount, trade }: LoadedStateProps) {
const [flip, setFlip] = useState(true) const [flip, setFlip] = useState(true)
const executionPrice = trade?.executionPrice
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
const ratio = useMemo(() => { const ratio = useMemo(() => {
const [a, b] = flip ? [output, input] : [input, output] const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const ratio = `1 ${a.token.symbol} = ${b.value / a.value} ${b.token.symbol}`
const usdc = a.usdc && ` ($${(a.usdc / a.value).toLocaleString('en')})` const ratio = `1 ${a.currency.symbol} = ${executionPrice?.toSignificant(6)} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(2)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(2)})`
: null
return ( return (
<Row gap={0.25} style={{ userSelect: 'text' }}> <Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio} {ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>} {usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row> </Row>
) )
}, [flip, input, output]) }, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
return ( return (
<TextButton color="primary" onClick={() => setFlip(!flip)}> <TextButton color="primary" onClick={() => setFlip(!flip)}>
...@@ -56,22 +70,17 @@ function LoadedState({ input, output }: LoadedStateProps) { ...@@ -56,22 +70,17 @@ function LoadedState({ input, output }: LoadedStateProps) {
) )
} }
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) { export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { activeInput } = useAtomValue(stateAtom)
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const {
trade,
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurency },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount },
} = useSwapInfo()
const independentField = useAtomValue(independentFieldAtom)
const caption = useMemo(() => { const caption = useMemo(() => {
const filledInput = asFilledInput(input)
const filledOutput = asFilledInput(output)
if (disabled) { if (disabled) {
return ( return (
<> <>
...@@ -80,6 +89,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { ...@@ -80,6 +89,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
</> </>
) )
} }
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) { if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return ( return (
<> <>
...@@ -88,8 +98,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { ...@@ -88,8 +98,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
</> </>
) )
} }
if (activeInput === Field.INPUT ? filledInput && output.token : filledOutput && input.token) {
if (!swap) { if (independentField === Field.INPUT ? inputCurrency && inputAmount : outputCurency && outputAmount) {
if (!trade?.trade) {
return ( return (
<> <>
<Spinner color="secondary" /> <Spinner color="secondary" />
...@@ -97,19 +108,19 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { ...@@ -97,19 +108,19 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
</> </>
) )
} }
if (filledInput && filledInput.value > balance) { if (inputAmount && balance && inputAmount.greaterThan(balance)) {
return ( return (
<> <>
<AlertTriangle color="secondary" /> <AlertTriangle color="secondary" />
<Trans>Insufficient {filledInput.token.symbol}</Trans> <Trans>Insufficient {inputCurrency?.symbol}</Trans>
</> </>
) )
} }
if (filledInput && filledOutput) { if (inputCurrency && inputAmount && outputCurency && outputAmount) {
return ( return (
<> <>
<RoutingTooltip /> <RoutingTooltip />
<LoadedState input={filledInput} output={filledOutput} /> <LoadedState inputAmount={inputAmount} outputAmount={outputAmount} trade={trade?.trade} />
</> </>
) )
} }
...@@ -120,7 +131,17 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { ...@@ -120,7 +131,17 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
<Trans>Enter an amount</Trans> <Trans>Enter an amount</Trans>
</> </>
) )
}, [activeInput, balance, chainId, disabled, input, output, swap]) }, [
balance,
chainId,
disabled,
independentField,
inputAmount,
inputCurrency,
outputAmount,
outputCurency,
trade?.trade,
])
return ( return (
<> <>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists' import { TokenInfo } from '@uniswap/token-lists'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList' import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
...@@ -52,6 +53,7 @@ export default function Swap({ defaults }: SwapProps) { ...@@ -52,6 +53,7 @@ export default function Swap({ defaults }: SwapProps) {
const { active, account } = useActiveWeb3React() const { active, account } = useActiveWeb3React()
return ( return (
<> <>
<SwapInfoUpdater />
<Header logo title={<Trans>Swap</Trans>}> <Header logo title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} />} {active && <Wallet disabled={!account} />}
<Settings disabled={!active} /> <Settings disabled={!active} />
......
import { Currency } from '@uniswap/sdk-core'
import { useAtom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { pickAtom } from 'lib/state/atoms'
import { amountAtom, Field, independentFieldAtom, swapAtom } from 'lib/state/swap'
import { useCallback, useMemo } from 'react'
export { default as useSwapInfo } from './useSwapInfo'
export function useSwapCurrency(field: Field): [Currency | undefined, (currency?: Currency) => void] {
const atom = useMemo(() => pickAtom(swapAtom, field), [field])
return useAtom(atom)
}
export function useSwapAmount(field: Field): [string | undefined, (amount: string) => void] {
const amount = useAtomValue(amountAtom)
const independentField = useAtomValue(independentFieldAtom)
const value = useMemo(() => (independentField === field ? amount : undefined), [amount, independentField, field])
const updateSwap = useUpdateAtom(swapAtom)
const updateAmount = useCallback(
(amount: string) =>
updateSwap((swap) => {
swap.independentField = field
swap.amount = amount
}),
[field, updateSwap]
)
return [value, updateAmount]
}
export function useSwitchSwapCurrencies() {
const update = useUpdateAtom(swapAtom)
return useCallback(() => {
update((swap) => {
const oldOutput = swap[Field.OUTPUT]
swap[Field.OUTPUT] = swap[Field.INPUT]
swap[Field.INPUT] = oldOutput
switch (swap.independentField) {
case Field.INPUT:
swap.independentField = Field.OUTPUT
break
case Field.OUTPUT:
swap.independentField = Field.INPUT
break
}
})
}, [update])
}
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useEffect, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isAddress } from '../../../utils'
import useActiveWeb3React from '../useActiveWeb3React'
interface SwapInfo {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
currencyAmounts: { [field in Field]?: CurrencyAmount<Currency> }
trade: {
trade?: InterfaceTrade<Currency, Currency, TradeType>
state: TradeState
}
allowedSlippage: Percent
}
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory
'0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
}
// from the current swap inputs, compute the best trade and return it.
function useComputeSwapInfo(): SwapInfo {
const { account } = useActiveWeb3React()
const {
independentField,
amount,
[Field.INPUT]: inputCurrency,
[Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom)
const to = account
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount]
)
/**
* @TODO (ianlapham): eventually need a strategy for routing API here
*/
const trade = useClientSideV3Trade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
)
const currencies = useMemo(
() => ({
[Field.INPUT]: inputCurrency ?? undefined,
[Field.OUTPUT]: outputCurrency ?? undefined,
}),
[inputCurrency, outputCurrency]
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const currencyAmounts = useMemo(
() => ({
[Field.INPUT]: trade.trade?.inputAmount,
[Field.OUTPUT]: trade.trade?.outputAmount,
}),
[trade.trade?.inputAmount, trade.trade?.outputAmount]
)
// TODO(ianlapham): Fix swap slippage tolerance
// const allowedSlippage = useSwapSlippageTolerance(trade.trade ?? undefined)
const allowedSlippage = useMemo(() => new Percent(100), [])
const inputError = useMemo(() => {
let inputError: ReactNode | undefined
if (!account) {
inputError = <Trans>Connect Wallet</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
const formattedTo = isAddress(to)
if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
inputError = inputError ?? <Trans>Invalid recipient</Trans>
}
}
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
}
return inputError
}, [account, allowedSlippage, currencies, currencyBalances, parsedAmount, to, trade.trade])
return useMemo(
() => ({
currencies,
currencyBalances,
currencyAmounts,
inputError,
trade,
allowedSlippage,
}),
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage]
)
}
const swapInfoAtom = atom<SwapInfo>({
currencies: {},
currencyBalances: {},
currencyAmounts: {},
trade: { state: TradeState.INVALID },
allowedSlippage: new Percent(0),
})
export function SwapInfoUpdater() {
const setSwapInfo = useUpdateAtom(swapInfoAtom)
const swapInfo = useComputeSwapInfo()
useEffect(() => {
setSwapInfo(swapInfo)
}, [swapInfo, setSwapInfo])
return null
}
/** Requires that SwapInfoUpdater be installed in the DOM tree. */
export default function useSwapInfo(): SwapInfo {
return useAtomValue(swapInfoAtom)
}
...@@ -113,7 +113,7 @@ export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null) ...@@ -113,7 +113,7 @@ export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null)
* Returns null if currency is loading or null was passed. * Returns null if currency is loading or null was passed.
* Returns undefined if currencyId is invalid or token does not exist. * Returns undefined if currencyId is invalid or token does not exist.
*/ */
export function useCurrency(currencyId?: string | null): Currency | null | undefined { export default function useCurrency(currencyId?: string | null): Currency | null | undefined {
const tokens = useTokenMap() const tokens = useTokenMap()
return useCurrencyFromMap(tokens, currencyId) return useCurrencyFromMap(tokens, currencyId)
} }
import { atomWithReset } from 'jotai/utils'
import { Customizable, pickAtom, setCustomizable, setTogglable } from './atoms'
/** Max slippage, as a percentage. */
export enum MaxSlippage {
P01 = 0.1,
P05 = 0.5,
// Members to satisfy CustomizableEnum; see setCustomizable
CUSTOM = -1,
DEFAULT = P05,
}
export const TRANSACTION_TTL_DEFAULT = 40
interface Settings {
maxSlippage: Customizable<MaxSlippage>
transactionTtl: number | undefined
mockTogglable: boolean
clientSideRouter: boolean // wether to use
}
const initialSettings: Settings = {
maxSlippage: { value: MaxSlippage.DEFAULT },
transactionTtl: undefined,
mockTogglable: true,
clientSideRouter: false,
}
export const settingsAtom = atomWithReset(initialSettings)
export const maxSlippageAtom = pickAtom(settingsAtom, 'maxSlippage', setCustomizable(MaxSlippage))
export const transactionTtlAtom = pickAtom(settingsAtom, 'transactionTtl')
export const mockTogglableAtom = pickAtom(settingsAtom, 'mockTogglable', setTogglable)
export const clientSideRouterAtom = pickAtom(settingsAtom, 'clientSideRouter')
import { Currency } from '@uniswap/sdk-core' import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { atom, WritableAtom } from 'jotai'
import { atomWithImmer } from 'jotai/immer' import { atomWithImmer } from 'jotai/immer'
import { useUpdateAtom } from 'jotai/utils' import { pickAtom } from 'lib/state/atoms'
import { atomWithReset } from 'jotai/utils'
import { Customizable, pickAtom, setCustomizable, setTogglable } from 'lib/state/atoms'
import { useMemo } from 'react'
/** Max slippage, as a percentage. */
export enum MaxSlippage {
P01 = 0.1,
P05 = 0.5,
// Members to satisfy CustomizableEnum; see setCustomizable
CUSTOM = -1,
DEFAULT = P05,
}
export const TRANSACTION_TTL_DEFAULT = 40
export interface Settings {
maxSlippage: Customizable<MaxSlippage>
transactionTtl: number | undefined
mockTogglable: boolean
}
const initialSettings: Settings = {
maxSlippage: { value: MaxSlippage.DEFAULT },
transactionTtl: undefined,
mockTogglable: true,
}
export const settingsAtom = atomWithReset(initialSettings)
export const maxSlippageAtom = pickAtom(settingsAtom, 'maxSlippage', setCustomizable(MaxSlippage))
export const transactionTtlAtom = pickAtom(settingsAtom, 'transactionTtl')
export const mockTogglableAtom = pickAtom(settingsAtom, 'mockTogglable', setTogglable)
export enum Field { export enum Field {
INPUT = 'input', INPUT = 'INPUT',
OUTPUT = 'output', OUTPUT = 'OUTPUT',
} }
export interface Input { export interface Swap {
value?: number independentField: Field
token?: Currency readonly amount: string
usdc?: number readonly [Field.INPUT]?: Currency
readonly [Field.OUTPUT]?: Currency
integratorFee?: number
} }
export interface State { export const swapAtom = atomWithImmer<Swap>({
activeInput: Field independentField: Field.INPUT,
[Field.INPUT]: Input & { approved?: boolean } amount: '',
[Field.OUTPUT]: Input [Field.INPUT]: nativeOnChain(SupportedChainId.MAINNET),
swap?: {
lpFee: number
priceImpact: number
slippageTolerance: number
integratorFee?: number
maximumSent?: number
minimumReceived?: number
}
}
export const stateAtom = atomWithImmer<State>({
activeInput: Field.INPUT,
input: { token: nativeOnChain(SupportedChainId.MAINNET) },
output: {},
}) })
export const swapAtom = pickAtom(stateAtom, 'swap') export const independentFieldAtom = pickAtom(swapAtom, 'independentField')
export const integratorFeeAtom = pickAtom(swapAtom, 'integratorFee')
export const inputAtom = atom( export const amountAtom = pickAtom(swapAtom, 'amount')
(get) => get(stateAtom).input,
(get, set, update: Input & { approved?: boolean }) => {
set(stateAtom, (state) => {
state.activeInput = Field.INPUT
state.input = update
state.swap = undefined
})
}
)
export const outputAtom = atom(
(get) => get(stateAtom).output,
(get, set, update: Input) => {
set(stateAtom, (state) => {
state.activeInput = Field.OUTPUT
state.output = update
state.swap = undefined
})
}
)
export function useUpdateInputValue(inputAtom: WritableAtom<Input, Input>) {
return useUpdateAtom(
useMemo(
() => atom(null, (get, set, value: Input['value']) => set(inputAtom, { token: get(inputAtom).token, value })),
[inputAtom]
)
)
}
export function useUpdateInputToken(inputAtom: WritableAtom<Input, Input>) {
return useUpdateAtom(
useMemo(() => atom(null, (get, set, token: Input['token']) => set(inputAtom, { token })), [inputAtom])
)
}
export interface Transaction { export interface SwapTransaction {
input: Required<Pick<Input, 'token' | 'value'>> input: CurrencyAmount<Currency>
output: Required<Pick<Input, 'token' | 'value'>> output: CurrencyAmount<Currency>
receipt: string receipt: string
timestamp: number timestamp: number
elapsedMs?: number elapsedMs?: number
status?: true | Error status?: true | Error
} }
export const transactionAtom = atomWithImmer<Transaction | null>(null) export const swapTransactionAtom = atomWithImmer<SwapTransaction | null>(null)
...@@ -238,7 +238,10 @@ export function useUserAddedTokens(): Token[] { ...@@ -238,7 +238,10 @@ export function useUserAddedTokens(): Token[] {
return useMemo(() => { return useMemo(() => {
if (!chainId) return [] if (!chainId) return []
return Object.values(serializedTokensMap?.[chainId] ?? {}).map(deserializeToken) const tokenMap: Token[] = serializedTokensMap?.[chainId]
? Object.values(serializedTokensMap[chainId]).map(deserializeToken)
: []
return tokenMap
}, [serializedTokensMap, chainId]) }, [serializedTokensMap, chainId])
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment