Commit a323a5c4 authored by Jordan Frankfurt's avatar Jordan Frankfurt Committed by GitHub

feat(widgets): convenience fee (#3231)

* feat(widgets): support convenience fee in trades (#3219)

* feat(widgets): support convenience fee in trades

* update call signature

* pr feedback

* set default convenience fee to undefined

* pr feedback
parent 43931dd6
...@@ -2,7 +2,7 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -2,7 +2,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import { SwapRouter, Trade } from '@uniswap/router-sdk' import { SwapRouter, Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk' import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
import { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' import { FeeOptions, SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses' import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react' import { useMemo } from 'react'
...@@ -36,7 +36,8 @@ export function useSwapCallArguments( ...@@ -36,7 +36,8 @@ export function useSwapCallArguments(
allowedSlippage: Percent, allowedSlippage: Percent,
recipientAddressOrName: string | null | undefined, recipientAddressOrName: string | null | undefined,
signatureData: SignatureData | null | undefined, signatureData: SignatureData | null | undefined,
deadline: BigNumber | undefined deadline: BigNumber | undefined,
feeOptions: FeeOptions | undefined
): SwapCall[] { ): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
...@@ -98,6 +99,7 @@ export function useSwapCallArguments( ...@@ -98,6 +99,7 @@ export function useSwapCallArguments(
} else { } else {
// swap options shared by v3 and v2+v3 swap routers // swap options shared by v3 and v2+v3 swap routers
const sharedSwapOptions = { const sharedSwapOptions = {
fee: feeOptions,
recipient, recipient,
slippageTolerance: allowedSlippage, slippageTolerance: allowedSlippage,
...(signatureData ...(signatureData
...@@ -167,15 +169,16 @@ export function useSwapCallArguments( ...@@ -167,15 +169,16 @@ export function useSwapCallArguments(
] ]
} }
}, [ }, [
trade,
recipient,
library,
account, account,
allowedSlippage,
argentWalletContract,
chainId, chainId,
deadline, deadline,
feeOptions,
library,
recipient,
routerContract, routerContract,
allowedSlippage,
argentWalletContract,
signatureData, signatureData,
trade,
]) ])
} }
...@@ -33,7 +33,7 @@ export function useSwapCallback( ...@@ -33,7 +33,7 @@ export function useSwapCallback(
state, state,
callback: libCallback, callback: libCallback,
error, error,
} = useLibSwapCallBack(trade, allowedSlippage, recipient, signatureData, deadline) } = useLibSwapCallBack({ trade, allowedSlippage, recipientAddressOrName: recipient, signatureData, deadline })
const callback = useMemo(() => { const callback = useMemo(() => {
if (!libCallback || !trade) { if (!libCallback || !trade) {
......
...@@ -2,8 +2,9 @@ import { t } from '@lingui/macro' ...@@ -2,8 +2,9 @@ import { t } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk' import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc' import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
import { useAtom } from 'jotai' import { useAtomValue } from 'jotai/utils'
import { integratorFeeAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings' import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import { feeOptionsAtom } from 'lib/state/swap'
import styled, { Color, ThemedText } from 'lib/theme' import styled, { Color, ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId' import { currencyId } from 'utils/currencyId'
...@@ -45,13 +46,19 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) { ...@@ -45,13 +46,19 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade]) const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const integrator = window.location.hostname const integrator = window.location.hostname
const [integratorFee] = useAtom(integratorFeeAtom) const feeOptions = useAtomValue(feeOptionsAtom)
const details = useMemo(() => { const details = useMemo(() => {
// @TODO(ianlapham): Check that provider fee is even a valid list item // @TODO(ianlapham): Check that provider fee is even a valid list item
return [ return [
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`], // [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(inputCurrency)}`], [
t`${integrator} fee`,
feeOptions &&
`${outputAmount.multiply(feeOptions.fee).toSignificant(2)} ${
outputCurrency.symbol || currencyId(outputCurrency)
}`,
],
[ [
t`Price impact`, t`Price impact`,
`${priceImpact.toFixed(2)}%`, `${priceImpact.toFixed(2)}%`,
...@@ -77,7 +84,7 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) { ...@@ -77,7 +84,7 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
function isDetail(detail: unknown[]): detail is [string, string, Color | undefined] { function isDetail(detail: unknown[]): detail is [string, string, Color | undefined] {
return Boolean(detail[1]) return Boolean(detail[1])
} }
}, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade]) }, [allowedSlippage, inputCurrency, integrator, feeOptions, outputAmount, outputCurrency, priceImpact, trade])
return ( return (
<> <>
{details.map(([label, detail, color]) => ( {details.map(([label, detail, color]) => (
......
...@@ -25,6 +25,18 @@ function Fixture() { ...@@ -25,6 +25,18 @@ function Fixture() {
} }
}, [color, setColor]) }, [color, setColor])
const [convenienceFee] = useValue('convenienceFee', { defaultValue: 100 })
const FEE_RECIPIENT_OPTIONS = [
'',
'0x1D9Cd50Dde9C19073B81303b3d930444d11552f7',
'0x0dA5533d5a9aA08c1792Ef2B6a7444E149cCB0AD',
'0xE6abE059E5e929fd17bef158902E73f0FEaCD68c',
]
const [convenienceFeeRecipient] = useSelect('convenienceFeeRecipient', {
options: FEE_RECIPIENT_OPTIONS,
defaultValue: FEE_RECIPIENT_OPTIONS[1],
})
const optionsToAddressMap: Record<string, string> = { const optionsToAddressMap: Record<string, string> = {
none: '', none: '',
Native: 'NATIVE', Native: 'NATIVE',
...@@ -52,11 +64,13 @@ function Fixture() { ...@@ -52,11 +64,13 @@ function Fixture() {
return ( return (
<Swap <Swap
tokenList={tokens} convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputAddress={optionsToAddressMap[defaultInput]} defaultInputAddress={optionsToAddressMap[defaultInput]}
defaultInputAmount={defaultInputAmount} defaultInputAmount={defaultInputAmount}
defaultOutputAddress={optionsToAddressMap[defaultOutput]} defaultOutputAddress={optionsToAddressMap[defaultOutput]}
defaultOutputAmount={defaultOutputAmount} defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
/> />
) )
} }
......
...@@ -51,6 +51,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -51,6 +51,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
currencies: { [Field.INPUT]: inputCurrency }, currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance }, currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount }, currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions,
} = useSwapInfo() } = useSwapInfo()
const independentField = useAtomValue(independentFieldAtom) const independentField = useAtomValue(independentFieldAtom)
...@@ -118,13 +119,14 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -118,13 +119,14 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline) const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap // the callback to execute the swap
const { callback: swapCallback } = useSwapCallback( const { callback: swapCallback } = useSwapCallback({
optimizedTrade, trade: optimizedTrade,
allowedSlippage, allowedSlippage,
account ?? null, recipientAddressOrName: account ?? null,
signatureData, signatureData,
deadline deadline,
) feeOptions,
})
//@TODO(ianlapham): add a loading state, process errors //@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom) const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists' import { TokenInfo } from '@uniswap/token-lists'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import useSwapDefaults from 'lib/hooks/swap/useSwapDefaults'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo' import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import useSyncConvenienceFee from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncSwapDefaults from 'lib/hooks/swap/useSyncSwapDefaults'
import { usePendingTransactions } from 'lib/hooks/transactions' import { usePendingTransactions } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTokenList from 'lib/hooks/useTokenList' import useTokenList from 'lib/hooks/useTokenList'
...@@ -47,7 +48,8 @@ export interface SwapProps { ...@@ -47,7 +48,8 @@ export interface SwapProps {
export default function Swap(props: SwapProps) { export default function Swap(props: SwapProps) {
useTokenList(props.tokenList) useTokenList(props.tokenList)
useSwapDefaults(props) useSyncSwapDefaults(props)
useSyncConvenienceFee(props)
const { active, account } = useActiveWeb3React() const { active, account } = useActiveWeb3React()
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null) const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
......
...@@ -3,6 +3,7 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -3,6 +3,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useENS from 'hooks/useENS' import useENS from 'hooks/useENS'
import { SignatureData } from 'hooks/useERC20Permit' import { SignatureData } from 'hooks/useERC20Permit'
...@@ -17,18 +18,40 @@ export enum SwapCallbackState { ...@@ -17,18 +18,40 @@ export enum SwapCallbackState {
VALID, VALID,
} }
interface UseSwapCallbackReturns {
state: SwapCallbackState
callback: null | (() => Promise<TransactionResponse>)
error: ReactNode | null
}
interface UseSwapCallbackArgs {
trade: AnyTrade | undefined // trade to execute, required
allowedSlippage: Percent // in bips
recipientAddressOrName: string | null | undefined // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
deadline: BigNumber | undefined
feeOptions?: FeeOptions
}
// returns a function that will execute a swap, if the parameters are all valid // returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade // and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback( export function useSwapCallback({
trade: AnyTrade | undefined, // trade to execute, required trade,
allowedSlippage: Percent, // in bips allowedSlippage,
recipientAddressOrName: string | null | undefined, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender recipientAddressOrName,
signatureData: SignatureData | null | undefined, signatureData,
deadline: BigNumber | undefined deadline,
): { state: SwapCallbackState; callback: null | (() => Promise<TransactionResponse>); error: ReactNode | null } { feeOptions,
}: UseSwapCallbackArgs): UseSwapCallbackReturns {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData, deadline) const swapCalls = useSwapCallArguments(
trade,
allowedSlippage,
recipientAddressOrName,
signatureData,
deadline,
feeOptions
)
const { callback } = useSendSwapTransaction(account, chainId, library, trade, swapCalls) const { callback } = useSendSwapTransaction(account, chainId, library, trade, swapCalls)
const { address: recipientAddress } = useENS(recipientAddressOrName) const { address: recipientAddress } = useENS(recipientAddressOrName)
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { atom } from 'jotai' import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance' import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import { maxSlippageAtom } from 'lib/state/settings' import { maxSlippageAtom } from 'lib/state/settings'
import { Field, swapAtom } from 'lib/state/swap' import { feeOptionsAtom, Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useEffect, useMemo } from 'react' import { ReactNode, useEffect, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
...@@ -23,6 +24,7 @@ interface SwapInfo { ...@@ -23,6 +24,7 @@ interface SwapInfo {
state: TradeState state: TradeState
} }
allowedSlippage: Percent allowedSlippage: Percent
feeOptions: FeeOptions | undefined
} }
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
...@@ -42,6 +44,8 @@ function useComputeSwapInfo(): SwapInfo { ...@@ -42,6 +44,8 @@ function useComputeSwapInfo(): SwapInfo {
[Field.OUTPUT]: outputCurrency, [Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom) } = useAtomValue(swapAtom)
const feeOptions = useAtomValue(feeOptionsAtom)
const to = account const to = account
const relevantTokenBalances = useCurrencyBalances( const relevantTokenBalances = useCurrencyBalances(
...@@ -139,8 +143,9 @@ function useComputeSwapInfo(): SwapInfo { ...@@ -139,8 +143,9 @@ function useComputeSwapInfo(): SwapInfo {
inputError, inputError,
trade, trade,
allowedSlippage, allowedSlippage,
feeOptions,
}), }),
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage] [currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions]
) )
} }
...@@ -150,6 +155,7 @@ const swapInfoAtom = atom<SwapInfo>({ ...@@ -150,6 +155,7 @@ const swapInfoAtom = atom<SwapInfo>({
currencyAmounts: {}, currencyAmounts: {},
trade: { state: TradeState.INVALID }, trade: { state: TradeState.INVALID },
allowedSlippage: new Percent(0), allowedSlippage: new Percent(0),
feeOptions: undefined,
}) })
export function SwapInfoUpdater() { export function SwapInfoUpdater() {
......
import { Percent } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useUpdateAtom } from 'jotai/utils'
import { feeOptionsAtom } from 'lib/state/swap'
import { useEffect } from 'react'
interface FeeOptionsArgs {
convenienceFee?: number
convenienceFeeRecipient?: string | string | { [chainId: number]: string }
}
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptionsArgs) {
const { chainId } = useActiveWeb3React()
const updateFeeOptions = useUpdateAtom(feeOptionsAtom)
useEffect(() => {
if (convenienceFee && convenienceFeeRecipient) {
if (typeof convenienceFeeRecipient === 'string') {
updateFeeOptions({
fee: new Percent(convenienceFee, 10_000),
recipient: convenienceFeeRecipient,
})
return
}
if (chainId && convenienceFeeRecipient[chainId]) {
updateFeeOptions({
fee: new Percent(convenienceFee, 10_000),
recipient: convenienceFeeRecipient[chainId],
})
return
}
}
updateFeeOptions(undefined)
}, [chainId, convenienceFee, convenienceFeeRecipient, updateFeeOptions])
}
...@@ -31,7 +31,7 @@ interface UseSwapDefaultsArgs { ...@@ -31,7 +31,7 @@ interface UseSwapDefaultsArgs {
defaultOutputAmount?: string defaultOutputAmount?: string
} }
export default function useSwapDefaults({ export default function useSyncSwapDefaults({
defaultInputAddress, defaultInputAddress,
defaultInputAmount, defaultInputAmount,
defaultOutputAddress, defaultOutputAddress,
......
...@@ -8,8 +8,7 @@ export const MIN_HIGH_SLIPPAGE = new Percent(1, 100) ...@@ -8,8 +8,7 @@ export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
interface Settings { interface Settings {
maxSlippage: Percent | 'auto' // auto will cause slippage to resort to default calculation maxSlippage: Percent | 'auto' // auto will cause slippage to resort to default calculation
transactionTtl: number | undefined // transaction ttl in minutes transactionTtl: number | undefined
integratorFee: number | undefined
mockTogglable: boolean mockTogglable: boolean
clientSideRouter: boolean // whether to use the client-side router or query the remote API clientSideRouter: boolean // whether to use the client-side router or query the remote API
} }
...@@ -17,7 +16,6 @@ interface Settings { ...@@ -17,7 +16,6 @@ interface Settings {
const initialSettings: Settings = { const initialSettings: Settings = {
maxSlippage: 'auto', maxSlippage: 'auto',
transactionTtl: undefined, transactionTtl: undefined,
integratorFee: undefined,
mockTogglable: true, mockTogglable: true,
clientSideRouter: false, clientSideRouter: false,
} }
...@@ -25,6 +23,5 @@ const initialSettings: Settings = { ...@@ -25,6 +23,5 @@ const initialSettings: Settings = {
export const settingsAtom = atomWithReset(initialSettings) export const settingsAtom = atomWithReset(initialSettings)
export const maxSlippageAtom = pickAtom(settingsAtom, 'maxSlippage') export const maxSlippageAtom = pickAtom(settingsAtom, 'maxSlippage')
export const transactionTtlAtom = pickAtom(settingsAtom, 'transactionTtl') export const transactionTtlAtom = pickAtom(settingsAtom, 'transactionTtl')
export const integratorFeeAtom = pickAtom(settingsAtom, 'integratorFee')
export const mockTogglableAtom = pickAtom(settingsAtom, 'mockTogglable', setTogglable) export const mockTogglableAtom = pickAtom(settingsAtom, 'mockTogglable', setTogglable)
export const clientSideRouterAtom = pickAtom(settingsAtom, 'clientSideRouter') export const clientSideRouterAtom = pickAtom(settingsAtom, 'clientSideRouter')
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { atom } from 'jotai' import { atom } from 'jotai'
...@@ -27,3 +28,5 @@ export const independentFieldAtom = pickAtom(swapAtom, 'independentField') ...@@ -27,3 +28,5 @@ export const independentFieldAtom = pickAtom(swapAtom, 'independentField')
// If set to a transaction hash, that transaction will display in a status dialog. // If set to a transaction hash, that transaction will display in a status dialog.
export const displayTxHashAtom = atom<string | undefined>(undefined) export const displayTxHashAtom = atom<string | undefined>(undefined)
export const feeOptionsAtom = atom<FeeOptions | undefined>(undefined)
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