diff --git a/src/abis/argent-wallet-contract.json b/src/abis/argent-wallet-contract.json new file mode 100644 index 0000000000..e5e3ab323b --- /dev/null +++ b/src/abis/argent-wallet-contract.json @@ -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" + } +] diff --git a/src/hooks/useArgentWalletContract.ts b/src/hooks/useArgentWalletContract.ts new file mode 100644 index 0000000000..018d523b23 --- /dev/null +++ b/src/hooks/useArgentWalletContract.ts @@ -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 +} diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index 631a52c2a1..9a1e4caecb 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -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' diff --git a/src/hooks/useIsArgentWallet.ts b/src/hooks/useIsArgentWallet.ts index f42533dcc7..db43333156 100644 --- a/src/hooks/useIsArgentWallet.ts +++ b/src/hooks/useIsArgentWallet.ts @@ -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 } diff --git a/src/hooks/useSocksBalance.ts b/src/hooks/useSocksBalance.ts index 61804592ea..48d03b4ad9 100644 --- a/src/hooks/useSocksBalance.ts +++ b/src/hooks/useSocksBalance.ts @@ -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 } diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index b85bad3d59..437cf2e8f2 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -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, + ]) } /** diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index d84fe7e653..980f4c174e 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -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 ( @@ -418,7 +449,7 @@ export default function AddLiquidity({ id="add-liquidity-input-tokena" showCommonBases /> -
+
{' '} @@ -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]} > diff --git a/src/pages/Pool/PositionPage.tsx b/src/pages/Pool/PositionPage.tsx index 33263d360b..4e2b5a08ce 100644 --- a/src/pages/Pool/PositionPage.tsx +++ b/src/pages/Pool/PositionPage.tsx @@ -166,7 +166,7 @@ function CurrentPriceCard({ {t('Current price')} - {(inverted ? pool.token1Price : pool.token0Price).toSignificant(5)}{' '} + {(inverted ? pool.token1Price : pool.token0Price).toSignificant(6)}{' '} {currencyQuote?.symbol + ' per ' + currencyBase?.symbol} diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 206dde21e1..8048880199 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -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 || diff --git a/src/utils/approveAmountCalldata.ts b/src/utils/approveAmountCalldata.ts new file mode 100644 index 0000000000..40a9ae07e9 --- /dev/null +++ b/src/utils/approveAmountCalldata.ts @@ -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, + 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', + } +}