feat: use multicall for argent wallets in swap and v3 add liquidity (#1387)
* use argent wallet contract in swap callback * maybe working swap callback * chore(v3): trigger a breaking release BREAKING CHANGE: trigger a major release for the uniswap interface to indicate it now supports swapping and liquidity provision against uniswap protocol v3 * fix the value * improve the error coverage * retry more frequently, couple more error nits * the is argent call was being sketchy * get it working for add liquidity * `0x0` for v2 swaps too * small nits in position page * fix import * fix compiler error
This commit is contained in:
parent
99ad4ae44c
commit
562b402293
61
src/abis/argent-wallet-contract.json
Normal file
61
src/abis/argent-wallet-contract.json
Normal file
@ -0,0 +1,61 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "_transactions",
|
||||
"type": "tuple[]"
|
||||
}
|
||||
],
|
||||
"name": "wc_multiCall",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes[]",
|
||||
"name": "",
|
||||
"type": "bytes[]"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_msgHash",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "_signature",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "isValidSignature",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
15
src/hooks/useArgentWalletContract.ts
Normal file
15
src/hooks/useArgentWalletContract.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ArgentWalletContract } from '../abis/types'
|
||||
import { useActiveWeb3React } from './web3'
|
||||
import { useContract } from './useContract'
|
||||
import useIsArgentWallet from './useIsArgentWallet'
|
||||
import ArgentWalletContractABI from '../abis/argent-wallet-contract.json'
|
||||
|
||||
export function useArgentWalletContract(): ArgentWalletContract | null {
|
||||
const { account } = useActiveWeb3React()
|
||||
const isArgentWallet = useIsArgentWallet()
|
||||
return useContract(
|
||||
isArgentWallet ? account ?? undefined : undefined,
|
||||
ArgentWalletContractABI,
|
||||
true
|
||||
) as ArgentWalletContract
|
||||
}
|
@ -41,7 +41,7 @@ import { Quoter, UniswapV3Factory, UniswapV3Pool } from 'types/v3'
|
||||
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
|
||||
import { V3Migrator } from 'types/v3/V3Migrator'
|
||||
import { getContract } from 'utils'
|
||||
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Multicall2, Weth } from '../abis/types'
|
||||
import { Erc20, ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Multicall2, Weth } from '../abis/types'
|
||||
import { UNI } from '../constants/tokens'
|
||||
import { useActiveWeb3React } from './web3'
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useActiveWeb3React } from './web3'
|
||||
import { useArgentWalletDetectorContract } from './useContract'
|
||||
@ -5,6 +6,7 @@ import { useArgentWalletDetectorContract } from './useContract'
|
||||
export default function useIsArgentWallet(): boolean {
|
||||
const { account } = useActiveWeb3React()
|
||||
const argentWalletDetector = useArgentWalletDetectorContract()
|
||||
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', [account ?? undefined], NEVER_RELOAD)
|
||||
const inputs = useMemo(() => [account ?? undefined], [account])
|
||||
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD)
|
||||
return call?.result?.[0] ?? false
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ export default function useSocksBalance(): JSBI | undefined {
|
||||
const { account } = useActiveWeb3React()
|
||||
const socksContract = useSocksController()
|
||||
|
||||
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD)
|
||||
const inputs = useMemo(() => [account ?? undefined], [account])
|
||||
const { result } = useSingleCallResult(socksContract, 'balanceOf', inputs, NEVER_RELOAD)
|
||||
const data = result?.[0]
|
||||
return data ? JSBI.BigInt(data.toString()) : undefined
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses'
|
||||
import { calculateGasMargin } from '../utils/calculateGasMargin'
|
||||
import approveAmountCalldata from '../utils/approveAmountCalldata'
|
||||
import { getTradeVersion } from '../utils/getTradeVersion'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { isAddress, shortenAddress } from '../utils'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useActiveWeb3React } from './web3'
|
||||
import { useArgentWalletContract } from './useArgentWalletContract'
|
||||
import { useV2RouterContract } from './useContract'
|
||||
import { SignatureData } from './useERC20Permit'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
@ -61,6 +63,7 @@ function useSwapCallArguments(
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
const deadline = useTransactionDeadline()
|
||||
const routerContract = useV2RouterContract()
|
||||
const argentWalletContract = useArgentWalletContract()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
|
||||
@ -68,6 +71,7 @@ function useSwapCallArguments(
|
||||
if (trade instanceof V2Trade) {
|
||||
if (!routerContract) return []
|
||||
const swapMethods = []
|
||||
|
||||
swapMethods.push(
|
||||
Router.swapCallParameters(trade, {
|
||||
feeOnTransfer: false,
|
||||
@ -87,11 +91,30 @@ function useSwapCallArguments(
|
||||
})
|
||||
)
|
||||
}
|
||||
return swapMethods.map(({ methodName, args, value }) => ({
|
||||
address: routerContract.address,
|
||||
calldata: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
value,
|
||||
}))
|
||||
return swapMethods.map(({ methodName, args, value }) => {
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return {
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
|
||||
{
|
||||
to: routerContract.address,
|
||||
value: value,
|
||||
data: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
address: routerContract.address,
|
||||
calldata: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
value,
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// trade is V3Trade
|
||||
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
|
||||
@ -122,7 +145,24 @@ function useSwapCallArguments(
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return [
|
||||
{
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
|
||||
{
|
||||
to: swapRouterAddress,
|
||||
value: value,
|
||||
data: calldata,
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
},
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
address: swapRouterAddress,
|
||||
@ -131,7 +171,18 @@ function useSwapCallArguments(
|
||||
},
|
||||
]
|
||||
}
|
||||
}, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, signatureData, trade])
|
||||
}, [
|
||||
account,
|
||||
allowedSlippage,
|
||||
argentWalletContract,
|
||||
chainId,
|
||||
deadline,
|
||||
library,
|
||||
recipient,
|
||||
routerContract,
|
||||
signatureData,
|
||||
trade,
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,6 +6,7 @@ import { AlertTriangle, AlertCircle } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { ZERO_PERCENT } from '../../constants/misc'
|
||||
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from '../../constants/addresses'
|
||||
import { useArgentWalletContract } from '../../hooks/useArgentWalletContract'
|
||||
import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
@ -18,6 +19,7 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
|
||||
import { useUSDCValue } from '../../hooks/useUSDCPrice'
|
||||
import approveAmountCalldata from '../../utils/approveAmountCalldata'
|
||||
import { calculateGasMargin } from '../../utils/calculateGasMargin'
|
||||
import Review from './Review'
|
||||
import { useActiveWeb3React } from '../../hooks/web3'
|
||||
@ -170,13 +172,15 @@ export default function AddLiquidity({
|
||||
{}
|
||||
)
|
||||
|
||||
const argentWalletContract = useArgentWalletContract()
|
||||
|
||||
// check whether the user has approved the router on the tokens
|
||||
const [approvalA, approveACallback] = useApproveCallback(
|
||||
parsedAmounts[Field.CURRENCY_A],
|
||||
argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_A],
|
||||
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
|
||||
)
|
||||
const [approvalB, approveBCallback] = useApproveCallback(
|
||||
parsedAmounts[Field.CURRENCY_B],
|
||||
argentWalletContract ? undefined : parsedAmounts[Field.CURRENCY_B],
|
||||
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
|
||||
)
|
||||
|
||||
@ -208,12 +212,36 @@ export default function AddLiquidity({
|
||||
createPool: noLiquidity,
|
||||
})
|
||||
|
||||
const txn = {
|
||||
let txn: { to: string; data: string; value: string } = {
|
||||
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId],
|
||||
data: calldata,
|
||||
value,
|
||||
}
|
||||
|
||||
if (argentWalletContract) {
|
||||
const amountA = parsedAmounts[Field.CURRENCY_A]
|
||||
const amountB = parsedAmounts[Field.CURRENCY_B]
|
||||
const batch = [
|
||||
...(amountA && amountA.currency.isToken
|
||||
? [approveAmountCalldata(amountA, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId])]
|
||||
: []),
|
||||
...(amountB && amountB.currency.isToken
|
||||
? [approveAmountCalldata(amountB, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId])]
|
||||
: []),
|
||||
{
|
||||
to: txn.to,
|
||||
data: txn.data,
|
||||
value: txn.value,
|
||||
},
|
||||
]
|
||||
const data = argentWalletContract.interface.encodeFunctionData('wc_multiCall', [batch])
|
||||
txn = {
|
||||
to: argentWalletContract.address,
|
||||
data,
|
||||
value: '0x0',
|
||||
}
|
||||
}
|
||||
|
||||
setAttemptingTxn(true)
|
||||
|
||||
library
|
||||
@ -244,6 +272,7 @@ export default function AddLiquidity({
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send transaction', error)
|
||||
setAttemptingTxn(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
@ -351,8 +380,10 @@ export default function AddLiquidity({
|
||||
)
|
||||
|
||||
// we need an existence check on parsed amounts for single-asset deposits
|
||||
const showApprovalA = approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A]
|
||||
const showApprovalB = approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
|
||||
const showApprovalA =
|
||||
!argentWalletContract && approvalA !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_A]
|
||||
const showApprovalB =
|
||||
!argentWalletContract && approvalB !== ApprovalState.APPROVED && !!parsedAmounts[Field.CURRENCY_B]
|
||||
|
||||
return (
|
||||
<ScrollablePage>
|
||||
@ -418,7 +449,7 @@ export default function AddLiquidity({
|
||||
id="add-liquidity-input-tokena"
|
||||
showCommonBases
|
||||
/>
|
||||
<div style={{ width: '12px' }}></div>
|
||||
<div style={{ width: '12px' }} />
|
||||
|
||||
<CurrencyDropdown
|
||||
value={formattedAmounts[Field.CURRENCY_B]}
|
||||
@ -571,7 +602,7 @@ export default function AddLiquidity({
|
||||
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}>
|
||||
<HoverInlineText
|
||||
maxCharacters={20}
|
||||
text={invertPrice ? price.invert().toSignificant(5) : price.toSignificant(5)}
|
||||
text={invertPrice ? price.invert().toSignificant(6) : price.toSignificant(6)}
|
||||
/>{' '}
|
||||
</TYPE.body>
|
||||
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
|
||||
@ -697,8 +728,8 @@ export default function AddLiquidity({
|
||||
}}
|
||||
disabled={
|
||||
!isValid ||
|
||||
(approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
|
||||
(approvalB !== ApprovalState.APPROVED && !depositBDisabled)
|
||||
(!argentWalletContract && approvalA !== ApprovalState.APPROVED && !depositADisabled) ||
|
||||
(!argentWalletContract && approvalB !== ApprovalState.APPROVED && !depositBDisabled)
|
||||
}
|
||||
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
|
||||
>
|
||||
|
@ -166,7 +166,7 @@ function CurrentPriceCard({
|
||||
<AutoColumn gap="8px" justify="center">
|
||||
<ExtentsText>{t('Current price')}</ExtentsText>
|
||||
<TYPE.mediumHeader textAlign="center">
|
||||
{(inverted ? pool.token1Price : pool.token0Price).toSignificant(5)}{' '}
|
||||
{(inverted ? pool.token1Price : pool.token0Price).toSignificant(6)}{' '}
|
||||
</TYPE.mediumHeader>
|
||||
<ExtentsText>{currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText>
|
||||
</AutoColumn>
|
||||
|
@ -33,6 +33,7 @@ import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useAppro
|
||||
import { V3TradeState } from '../../hooks/useBestV3Trade'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
|
||||
import useIsArgentWallet from '../../hooks/useIsArgentWallet'
|
||||
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
@ -302,9 +303,12 @@ export default function Swap({ history }: RouteComponentProps) {
|
||||
)
|
||||
}, [priceImpact, trade])
|
||||
|
||||
const isArgentWallet = useIsArgentWallet()
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
// never show if price impact is above threshold in non expert mode
|
||||
const showApproveFlow =
|
||||
!isArgentWallet &&
|
||||
!swapInputError &&
|
||||
(approvalState === ApprovalState.NOT_APPROVED ||
|
||||
approvalState === ApprovalState.PENDING ||
|
||||
|
32
src/utils/approveAmountCalldata.ts
Normal file
32
src/utils/approveAmountCalldata.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { toHex } from '@uniswap/v3-sdk'
|
||||
import { Erc20Interface } from '../abis/types/Erc20'
|
||||
|
||||
const ERC20_INTERFACE = new Interface([
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{ name: '_spender', type: 'address' },
|
||||
{ name: '_value', type: 'uint256' },
|
||||
],
|
||||
name: 'approve',
|
||||
outputs: [{ name: '', type: 'bool' }],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
]) as Erc20Interface
|
||||
|
||||
export default function approveAmountCalldata(
|
||||
amount: CurrencyAmount<Currency>,
|
||||
spender: string
|
||||
): { to: string; data: string; value: '0x0' } {
|
||||
if (!amount.currency.isToken) throw new Error('Must call with an amount of token')
|
||||
const approveData = ERC20_INTERFACE.encodeFunctionData('approve', [spender, toHex(amount.quotient)])
|
||||
return {
|
||||
to: amount.currency.address,
|
||||
data: approveData,
|
||||
value: '0x0',
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user