@ethersproject/bytes#splitSignature TypeScript Examples
The following examples show how to use
@ethersproject/bytes#splitSignature.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: permits.ts From hypertext with GNU General Public License v3.0 | 6 votes |
export async function gatherPermit(
address: string,
deadline: number,
approveAmount: JSBI,
token: Token,
library: Web3Provider
): Promise<Permit> {
const { permitSelector, getPermitData, domain, ...data } = await permitGatherers[token.chainId][token.address](
address,
deadline,
approveAmount,
library
)
const signature = await library
.send('eth_signTypedData_v4', [
address,
JSON.stringify({
...data,
domain: {
...domain,
chainId: token.chainId,
verifyingContract: token.address,
},
primaryType: 'Permit',
}),
])
.then(splitSignature)
return {
permitSelector,
permitData: getPermitData(signature),
}
}
Example #2
Source File: serializeTransaction.ts From bodhi.js with Apache License 2.0 | 6 votes |
// rlp([chainId, salt, nonce, gasLimit, storageLimit, to, value, data, validUntil, tip, accessList, eip712sig])
export function serializeEip712(transaction: UnsignedAcalaEvmTX, signature?: SignatureLike): string {
const fields: any = [
formatNumber(transaction.chainId || 0, 'chainId'),
transaction.salt || '0x',
formatNumber(transaction.nonce || 0, 'nonce'),
formatNumber(transaction.gasLimit || 0, 'gasLimit'),
formatNumber(transaction.storageLimit || 0, 'storageLimit'),
transaction.to === null || transaction.to === undefined ? '0x' : getAddress(transaction.to),
formatNumber(transaction.value || 0, 'value'),
transaction.data || '0x',
formatNumber(transaction.validUntil || MAX_UINT256, 'validUntil'),
formatNumber(transaction.tip || 0, 'tip'),
formatAccessList(transaction.accessList || [])
];
if (signature) {
const sig = splitSignature(signature);
fields.push(formatNumber(sig.recoveryParam, 'recoveryParam'));
fields.push(stripZeros(sig.r));
fields.push(stripZeros(sig.s));
}
return hexConcat(['0x60', RLP.encode(fields)]);
}
Example #3
Source File: permit.ts From balancer-v2-monorepo with GNU General Public License v3.0 | 5 votes |
signPermit = async (
token: Contract,
owner: Signer & TypedDataSigner,
spender: Account,
amount: BigNumberish,
deadline: BigNumberish = MAX_DEADLINE,
nonce?: BigNumberish
): Promise<{ v: number; r: string; s: string; deadline: BigNumber; nonce: BigNumber }> => {
const { chainId } = await token.provider.getNetwork();
const ownerAddress = await owner.getAddress();
if (!nonce) nonce = (await token.nonces(ownerAddress)) as BigNumberish;
// Hack around some tokens not exposing a `version()` function.
// If they do then use it, otherwise assume that their version is "1".
let version = '1';
try {
if (token.version) {
version = await token.version();
}
} catch {
// eslint-disable-prev-line no-empty
}
const domain = {
name: await token.name(),
version,
chainId,
verifyingContract: token.address,
};
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const value = {
owner: ownerAddress,
spender: await accountToAddress(spender),
value: amount,
nonce,
deadline,
};
const signature = await owner._signTypedData(domain, types, value);
return { ...splitSignature(signature), deadline: BigNumber.from(deadline), nonce: BigNumber.from(nonce) };
}
Example #4
Source File: signatures.ts From balancer-v2-monorepo with GNU General Public License v3.0 | 5 votes |
static encodeCalldataAuthorization = (calldata: string, deadline: BigNumberish, signature: string): string => {
const encodedDeadline = hexZeroPad(hexValue(deadline), 32).slice(2);
const { v, r, s } = splitSignature(signature);
const encodedV = hexZeroPad(hexValue(v), 32).slice(2);
const encodedR = r.slice(2);
const encodedS = s.slice(2);
return `${calldata}${encodedDeadline}${encodedV}${encodedR}${encodedS}`;
};
Example #5
Source File: signatures.ts From balancer-v2-monorepo with GNU General Public License v3.0 | 5 votes |
static signSetMinterApproval = async (
minterContract: Contract,
minter: Account,
approval: boolean,
user: Signer & TypedDataSigner,
deadline: BigNumberish = MAX_DEADLINE,
nonce?: BigNumberish
): Promise<{ v: number; r: string; s: string; deadline: BigNumber }> => {
const { chainId } = await minterContract.provider.getNetwork();
if (!nonce) {
const userAddress = await user.getAddress();
nonce = (await minterContract.getNextNonce(userAddress)) as BigNumberish;
}
const domain = {
name: 'Balancer Minter',
version: '1',
chainId,
verifyingContract: minterContract.address,
};
const types = {
SetMinterApproval: [
{ name: 'minter', type: 'address' },
{ name: 'approval', type: 'bool' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const value = {
minter: await accountToAddress(minter),
approval,
nonce: nonce.toString(),
deadline: deadline.toString(),
};
const signature = await user._signTypedData(domain, types, value);
return { ...splitSignature(signature), deadline: BigNumber.from(deadline) };
};
Example #6
Source File: permit.ts From trident with GNU General Public License v3.0 | 5 votes |
export async function getPermitSignature(
wallet: Wallet,
token: ERC20Mock | ERC20PermitAllowedMock,
spender: string,
value: BigNumberish = constants.MaxUint256,
deadline = constants.MaxUint256,
permitConfig?: { nonce?: BigNumberish; name?: string; chainId?: number; version?: string }
): Promise<Signature> {
const [nonce, name, version, chainId] = await Promise.all([
permitConfig?.nonce ?? token.nonces(wallet.address),
permitConfig?.name ?? token.name(),
permitConfig?.version ?? "1",
permitConfig?.chainId ?? wallet.getChainId(),
]);
return splitSignature(
await wallet._signTypedData(
{
name,
version,
chainId,
verifyingContract: token.address,
},
{
Permit: [
{
name: "owner",
type: "address",
},
{
name: "spender",
type: "address",
},
{
name: "value",
type: "uint256",
},
{
name: "nonce",
type: "uint256",
},
{
name: "deadline",
type: "uint256",
},
],
},
{
owner: wallet.address,
spender,
value,
nonce,
deadline,
}
)
);
}
Example #7
Source File: adapter.ts From cloud-cryptographic-wallet with MIT License | 5 votes |
async signTransaction(
deferrableTransaction: Deferrable<TransactionRequest>
): Promise<string> {
const transaction = await resolveProperties(deferrableTransaction);
const address = await this.getAddress();
if (transaction.from != null) {
if (getAddress(transaction.from) !== address) {
this.logger.throwArgumentError(
"transaction from address mismatch",
"transaction.from",
transaction.from
);
}
}
const nonce = transaction.nonce
? BigNumber.from(transaction.nonce).toNumber()
: undefined;
const unsignedTransaction: UnsignedTransaction = {
to: transaction.to,
nonce,
gasLimit: transaction.gasLimit,
gasPrice: transaction.gasPrice,
data: transaction.data,
value: transaction.value,
chainId: transaction.chainId,
type: transaction.type,
accessList: transaction.accessList,
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
maxFeePerGas: transaction.maxFeePerGas,
};
(
Object.keys(unsignedTransaction) as Array<keyof UnsignedTransaction>
).forEach((key) => {
if (key in unsignedTransaction && unsignedTransaction[key] == undefined) {
delete unsignedTransaction[key];
}
});
const digest = keccak256(serialize(unsignedTransaction));
const signature = await this.signer.sign(
Buffer.from(digest.slice(2), "hex")
);
const ethersSignature = splitSignature({
v: signature.v,
r: `0x${signature.r.toString("hex")}`,
s: `0x${signature.s.toString("hex")}`,
});
return serialize(unsignedTransaction, ethersSignature);
}
Example #8
Source File: index.tsx From pancake-swap-exchange-testnet with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const TranslateString = useI18n()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small color="textSubtle" textAlign="left" padding="12px 0 0 0" style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">{TranslateString(1182, 'Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{TranslateString(1136, 'Confirm')}
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<Container>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title={TranslateString(1156, 'You will receive')}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? TranslateString(1184, 'Simple') : TranslateString(1186, 'Detailed')}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}
>
25%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}
>
50%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}
>
75%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
{TranslateString(166, 'Max')}
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{TranslateString(1188, 'Receive WBNB')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'BNB' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'BNB' : currencyIdB}`}
>
{TranslateString(1190, 'Receive BNB')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</Container>
)
}
Example #9
Source File: index.tsx From pancakeswap-testnet with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const TranslateString = useI18n()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small color="textSubtle" textAlign="left" padding="12px 0 0 0" style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">{TranslateString(1182, 'Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{TranslateString(1136, 'Confirm')}
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title={TranslateString(1156, 'You will receive')}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? TranslateString(1184, 'Simple') : TranslateString(1186, 'Detailed')}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}
>
25%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}
>
50%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}
>
75%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
{TranslateString(166, 'Max')}
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{TranslateString(1188, 'Receive WBNB')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'BNB' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'BNB' : currencyIdB}`}
>
{TranslateString(1190, 'Receive BNB')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #10
Source File: index.tsx From vvs-ui with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(
() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)],
[currencyA, currencyB, chainId],
)
const { t } = useTranslation()
const gasPrice = useGasPrice()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'VVS Finance LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber(),
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber(),
})
})
.catch((err) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (err?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, value: string) => {
setSignatureData(null)
return _onUserInput(field, value)
},
[_onUserInput],
)
const onLiquidityInput = useCallback((value: string): void => onUserInput(Field.LIQUIDITY, value), [onUserInput])
const onCurrencyAInput = useCallback((value: string): void => onUserInput(Field.CURRENCY_A, value), [onUserInput])
const onCurrencyBInput = useCallback((value: string): void => onUserInput(Field.CURRENCY_B, value), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account || !deadline) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadline.toHexString(),
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadline.toHexString(),
]
}
}
// we have a signature, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((err) => {
console.error(`estimateGas failed`, methodName, args, err)
return undefined
}),
),
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate),
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
gasPrice,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((err: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(err)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md">
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" ml="10px">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<AddIcon width="16px" />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" ml="10px">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small textAlign="left" pt="12px">
{t('Output is estimated. If the price changes by more than %slippage%% your transaction will revert.', {
slippage: allowedSlippage / 100,
})}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text>
{t('%assetA%/%assetB% Burned', { assetA: currencyA?.symbol ?? '', assetB: currencyB?.symbol ?? '' })}
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text>{t('Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{t('Confirm')}
</Button>
</>
)
}
const pendingText = t('Removing %amountA% %symbolA% and %amountB% %symbolB%', {
amountA: parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
symbolA: currencyA?.symbol ?? '',
amountB: parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
symbolB: currencyB?.symbol ?? '',
})
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput],
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB))),
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history],
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history],
)
const handleDismissConfirmation = useCallback(() => {
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback,
)
const [onPresentRemoveLiquidity] = useModal(
<TransactionConfirmationModal
title={t('You will receive')}
customOnDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => <ConfirmationModalContent topContent={modalHeader} bottomContent={modalBottom} />}
pendingText={pendingText}
/>,
true,
true,
'removeLiquidityModal',
)
return (
<Page>
<MoleTopRow />
<AppBody>
<AppHeader
backTo="/pool"
title={t('Remove %assetA%-%assetB% liquidity', {
assetA: currencyA?.symbol ?? '',
assetB: currencyB?.symbol ?? '',
})}
subtitle={t('To receive %assetA% and %assetB%', {
assetA: currencyA?.symbol ?? '',
assetB: currencyB?.symbol ?? '',
})}
noConfig
/>
<CardBody>
<AutoColumn gap="20px">
<RowBetween>
<Text>{t('Amount')}</Text>
<Button variant="text" scale="sm" onClick={() => setShowDetailed(!showDetailed)}>
{showDetailed ? t('Simple') : t('Detailed')}
</Button>
</RowBetween>
{!showDetailed && (
<BorderCard>
<Text fontSize="40px" bold mb="16px" style={{ lineHeight: 1 }}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
<Slider
name="lp-amount"
min={0}
max={100}
value={innerLiquidityPercentage}
onValueChanged={(value) => setInnerLiquidityPercentage(Math.ceil(value))}
mb="16px"
/>
<Flex flexWrap="wrap" justifyContent="space-evenly">
<Button variant="tertiary" scale="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}>
25%
</Button>
<Button variant="tertiary" scale="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}>
50%
</Button>
<Button variant="tertiary" scale="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}>
75%
</Button>
<Button variant="tertiary" scale="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}>
Max
</Button>
</Flex>
</BorderCard>
)}
</AutoColumn>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDownIcon color="textSubtle" width="24px" my="16px" />
</ColumnCenter>
<AutoColumn gap="10px">
<Text bold color="secondary" fontSize="12px" textTransform="uppercase">
{t('You will receive')}
</Text>
<LightGreyCard>
<Flex justifyContent="space-between" mb="8px">
<Flex>
<CurrencyLogo currency={currencyA} />
<Text small color="textSubtle" id="remove-liquidity-tokena-symbol" ml="4px">
{currencyA?.symbol}
</Text>
</Flex>
<Text small>{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
</Flex>
<Flex justifyContent="space-between">
<Flex>
<CurrencyLogo currency={currencyB} />
<Text small color="textSubtle" id="remove-liquidity-tokenb-symbol" ml="4px">
{currencyB?.symbol}
</Text>
</Flex>
<Text small>{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
</Flex>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end', fontSize: '14px' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{t('Receive WCRO')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'CRO' : currencyIdA}/${
currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'CRO' : currencyIdB
}`}
>
{t('Receive CRO')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</LightGreyCard>
</AutoColumn>
</>
)}
{showDetailed && (
<Box my="16px">
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
onCurrencySelect={() => null}
/>
<ColumnCenter>
<ArrowDownIcon width="24px" my="16px" />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={t('Output')}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<AddIcon width="24px" my="16px" />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={t('Output')}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</Box>
)}
{pair && (
<AutoColumn gap="10px" style={{ marginTop: '16px' }}>
<Text bold color="secondary" fontSize="12px" textTransform="uppercase">
{t('Prices')}
</Text>
<LightGreyCard>
<Flex justifyContent="space-between">
<Text small color="textSubtle">
1 {currencyA?.symbol} =
</Text>
<Text small>
{tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</Flex>
<Flex justifyContent="space-between">
<Text small color="textSubtle">
1 {currencyB?.symbol} =
</Text>
<Text small>
{tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</Flex>
</LightGreyCard>
</AutoColumn>
)}
<Box position="relative" mt="16px">
{!account ? (
<ConnectWalletButton />
) : (
<RowBetween>
<Button
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
onClick={onAttemptToApprove}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
width="100%"
mr="0.5rem"
>
{approval === ApprovalState.PENDING ? (
<Dots>{t('Enabling')}</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
t('Enabled')
) : (
t('Enable')
)}
</Button>
<Button
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
onClick={() => {
onPresentRemoveLiquidity()
}}
width="100%"
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
>
{error || t('Remove')}
</Button>
</RowBetween>
)}
</Box>
</CardBody>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '570px', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</Page>
)
}
Example #11
Source File: index.tsx From luaswap-interface with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const NATIVE_TOKEN_TEXT = getTextNativeToken(chainId)
const IsTomo = IsTomoChain(chainId)
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(
parsedAmounts[Field.LIQUIDITY],
IsTomo ? TOMO_ROUTER_ADDRESS : ROUTER_ADDRESS
)
const isArgentWallet = useIsArgentWallet()
async function onAttemptToApprove() {
if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
if (isArgentWallet) {
return approveCallback()
}
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'LuaSwap LP Token V1',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: IsTomo ? TOMO_ROUTER_ADDRESS : ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber()
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber()
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account || !deadline) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage + 10)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage + 10)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER || currencyB === TOMO
const oneCurrencyIsETH = currencyA === ETHER || currencyA === TOMO || currencyBIsETH
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadline.toHexString()
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadline.toHexString()
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<div style={{ padding: '20px 0px', fontSize: '14px' }}>
<RowBetween>
Withdraw Fee:
<div>
{formattedAmounts[Field.CURRENCY_A]
? (parseFloat(formattedAmounts[Field.CURRENCY_A]) * BASE_WITHDRAW_FEE).toFixed(6)
: 0}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
{formattedAmounts[Field.CURRENCY_B]
? (parseFloat(formattedAmounts[Field.CURRENCY_B]) * BASE_WITHDRAW_FEE).toFixed(6)
: 0}{' '}
{currencyB?.symbol}
</div>
</RowBetween>
</div>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER || currencyA === TOMO || currencyB === TOMO
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs creating={false} adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<RowRight color={theme.text3}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'}{' '}
{currencyB?.symbol}
</RowRight>
)}
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<RowRight color={theme.text3}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'}{' '}
{currencyA?.symbol}
</RowRight>
)}
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${
currencyA === ETHER || currencyA === TOMO ? WETH[chainId].address : currencyIdA
}/${currencyB === ETHER || currencyB === TOMO ? WETH[chainId].address : currencyIdB}`}
>
Receive W{NATIVE_TOKEN_TEXT}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) && IsTomo
? 'TOMO'
: currencyA && currencyEquals(currencyA, WETH[chainId])
? 'ETH'
: currencyIdA
}/${
currencyB && currencyEquals(currencyB, WETH[chainId]) && IsTomo
? 'TOMO'
: currencyB && currencyEquals(currencyB, WETH[chainId])
? 'ETH'
: currencyIdB
}`}
>
Receive {NATIVE_TOKEN_TEXT}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
{pair && (
<div
style={{
padding: '0 20px 0 20px',
display: 'flex',
fontSize: '12px',
justifyContent: 'space-between'
}}
>
<RowBetween color={theme.text3}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</RowBetween>
<RowRight color={theme.text3}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</RowRight>
</div>
)}
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px', fontSize: '14px' }}>
<RowBetween>
Withdraw Fee:
<div>
{formattedAmounts[Field.CURRENCY_A]
? (parseFloat(formattedAmounts[Field.CURRENCY_A]) * BASE_WITHDRAW_FEE).toFixed(6)
: 0}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
{formattedAmounts[Field.CURRENCY_B]
? (parseFloat(formattedAmounts[Field.CURRENCY_B]) * BASE_WITHDRAW_FEE).toFixed(6)
: 0}{' '}
{currencyB?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative', padding: '20px' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn
style={{
minWidth: '20rem',
width: '100%',
maxWidth: '400px',
marginTop: '1rem',
marginLeft: 'auto',
marginRight: 'auto'
}}
>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #12
Source File: index.tsx From sushiswap-exchange with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'SushiSwap LP Token',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #13
Source File: index.tsx From forward.swaps with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
const isArgentWallet = useIsArgentWallet()
async function onAttemptToApprove() {
if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
if (isArgentWallet) {
return approveCallback()
}
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber()
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber()
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account || !deadline) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadline.toHexString()
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadline.toHexString()
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs creating={false} adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<BlueCard>
<AutoColumn gap="10px">
<TYPE.link fontWeight={400} color={'primaryText1'}>
<b>Tip:</b> Removing pool tokens converts your position back into underlying tokens at the current
rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.
</TYPE.link>
</AutoColumn>
</BlueCard>
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #14
Source File: index.tsx From panther-frontend-dex with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'PantherSwap LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Italic fontSize={12} color={theme.colors.textSubtle} textAlign="left" padding="12px 0 0 0">
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`FLIP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">Price</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
Confirm
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title="You will receive"
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}>
25%
</Button>
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}>
50%
</Button>
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}>
75%
</Button>
<Button
variant="tertiary"
size="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
Max
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WBNB
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive BNB
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton fullWidth />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ maxWidth: '436px', width: '100%', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #15
Source File: index.tsx From pancake-swap-interface-v1 with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const TranslateString = useI18n()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small color="textSubtle" textAlign="left" padding="12px 0 0 0" style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">{TranslateString(1182, 'Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{TranslateString(1136, 'Confirm')}
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<Container>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title={TranslateString(1156, 'You will receive')}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? TranslateString(1184, 'Simple') : TranslateString(1186, 'Detailed')}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}
>
25%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}
>
50%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}
>
75%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
{TranslateString(166, 'Max')}
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{TranslateString(1188, 'Receive WBNB')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'BNB' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'BNB' : currencyIdB}`}
>
{TranslateString(1190, 'Receive BNB')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</Container>
)
}
Example #16
Source File: index.tsx From pancake-swap-testnet with MIT License | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const TranslateString = useI18n()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small color="textSubtle" textAlign="left" padding="12px 0 0 0" style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">{TranslateString(1182, 'Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{TranslateString(1136, 'Confirm')}
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title={TranslateString(1156, 'You will receive')}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? TranslateString(1184, 'Simple') : TranslateString(1186, 'Detailed')}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}
>
25%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}
>
50%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}
>
75%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
{TranslateString(166, 'Max')}
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{TranslateString(1188, 'Receive WBNB')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'BNB' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'BNB' : currencyIdB}`}
>
{TranslateString(1190, 'Receive BNB')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #17
Source File: index.tsx From mozartfinance-swap-interface with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const TranslateString = useI18n()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Text small color="textSubtle" textAlign="left" padding="12px 0 0 0" style={{ fontStyle: 'italic' }}>
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Text>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">{TranslateString(1182, 'Price')}</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
{TranslateString(1136, 'Confirm')}
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title={TranslateString(1156, 'You will receive')}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? TranslateString(1184, 'Simple') : TranslateString(1186, 'Detailed')}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}
>
25%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}
>
50%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}
>
75%
</Button>
<Button
variant="tertiary"
scale="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
{TranslateString(166, 'Max')}
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
{TranslateString(1188, 'Receive WBNB')}
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'BNB' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'BNB' : currencyIdB}`}
>
{TranslateString(1190, 'Receive BNB')}
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton width="100%" />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #18
Source File: index.tsx From goose-frontend-amm with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB },
},
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId,
])
const theme = useContext(ThemeContext)
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? '',
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
]
const domain = {
name: 'Pancake LPs',
version: '1',
chainId,
verifyingContract: pair.liquidityToken.address,
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature,
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit,
},
domain,
primaryType: 'Permit',
message,
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature,
})
})
.catch((e) => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (e?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, val: string) => {
setSignatureData(null)
return _onUserInput(field, val)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((val: string): void => onUserInput(Field.LIQUIDITY, val), [onUserInput])
const onCurrencyAInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_A, val), [onUserInput])
const onCurrencyBInput = useCallback((val: string): void => onUserInput(Field.CURRENCY_B, val), [onUserInput])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0],
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[]
let args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow,
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow,
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s,
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch((e) => {
console.error(`estimateGas failed`, index, methodName, args, e)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex((safeGasEstimate) =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate,
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary: `Remove ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(3)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(3)} ${currencyB?.symbol}`,
})
setTxHash(response.hash)
})
.catch((e: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(e)
})
}
}
function modalHeader() {
return (
<AutoColumn gap="md" style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.textSubtle} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize="24px">{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size="24px" />
<Text fontSize="24px" style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<Italic fontSize={12} color={theme.colors.textSubtle} textAlign="left" padding="12px 0 0 0">
{`Output is estimated. If the price changes by more than ${
allowedSlippage / 100
}% your transaction will revert.`}
</Italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="textSubtle">{`LP ${currencyA?.symbol}/${currencyB?.symbol}`} Burned</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin />
<Text>{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color="textSubtle">Price</Text>
<Text>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<Button disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
Confirm
</Button>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash || ''}
content={() => (
<ConfirmationModalContent
title="You will receive"
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<Body>
<OutlineCard>
<AutoColumn>
<RowBetween>
<Text>Amount</Text>
<ClickableText
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Flex justifyContent="start">
<Text fontSize="64px">{formattedAmounts[Field.LIQUIDITY_PERCENT]}%</Text>
</Flex>
{!showDetailed && (
<>
<Flex mb="8px">
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
</Flex>
<Flex justifyContent="space-around">
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')}>
25%
</Button>
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')}>
50%
</Button>
<Button variant="tertiary" size="sm" onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')}>
75%
</Button>
<Button
variant="tertiary"
size="sm"
onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
>
Max
</Button>
</Flex>
</>
)}
</AutoColumn>
</OutlineCard>
</Body>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<Body>
<OutlineCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_A] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize="24px">{formattedAmounts[Field.CURRENCY_B] || '-'}</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize="24px" id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WBNB
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive BNB
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</OutlineCard>
</Body>
</>
)}
<Body style={{ paddingBottom: '24px' }}>
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label="Output"
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.textSubtle} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label="Output"
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '24px' }}>
<Flex justifyContent="space-between" mb="8px">
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</Flex>
<Flex justifyContent="space-between">
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</Flex>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ConnectWalletButton fullWidth />
) : (
<RowBetween>
<Button
onClick={onAttemptToApprove}
variant={approval === ApprovalState.APPROVED || signatureData !== null ? 'success' : 'primary'}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="8px"
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</Button>
<Button
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
variant={
!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]
? 'danger'
: 'primary'
}
>
{error || 'Remove'}
</Button>
</RowBetween>
)}
</div>
</Body>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #19
Source File: index.tsx From dyp with Do What The F*ck You Want To Public License | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
const isArgentWallet = useIsArgentWallet()
async function onAttemptToApprove() {
if (!pairContract || !pair || !library || !deadline) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
if (isArgentWallet) {
return approveCallback()
}
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline.toNumber()
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadline.toNumber()
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account || !deadline) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadline.toHexString()
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadline.toHexString()
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs creating={false} adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #20
Source File: index.tsx From cheeseswap-interface with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'CheeseSwap LPs',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map((methodName, index) =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, index, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={700}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={700} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.colors.text1} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={700}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={700} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.colors.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.colors.text2} fontWeight={700} fontSize={16}>
{'Cheese-LP' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={700} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color={theme.colors.text2} fontWeight={700} fontSize={16}>
Price
</Text>
<Text fontWeight={700} fontSize={16} color={theme.colors.text1}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={700} fontSize={16} color={theme.colors.text1}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={700} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
liquidityPercentChangeCallback
)
return (
<>
<AppBody>
<AddRemoveTabs adding={true} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={700}>Amount</Text>
<ClickableText
fontWeight={700}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={700}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.text4} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={700}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={700} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={700}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={700} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WBNB
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive BNB
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.colors.text4} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.colors.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={700}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={700}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '27rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}
Example #21
Source File: index.tsx From cuiswap with GNU General Public License v3.0 | 4 votes |
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// burn state
const { independentField, typedValue } = useBurnState()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: pair.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then(signature => {
setSignatureData({
v: signature.v,
r: signature.r,
s: signature.s,
deadline: deadlineForSignature
})
})
.catch(error => {
// for all errors other than 4001 (EIP-1193 user rejected request), fall back to manual approve
if (error?.code !== 4001) {
approveCallback()
}
})
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
}
// removeLiquidity
else {
methodNames = ['removeLiquidity']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
}
}
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
// removeLiquidityETHWithPermit
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
signatureData.v,
signatureData.r,
signatureData.s
]
}
} else {
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
currencyB?.symbol
})
setTxHash(response.hash)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
console.error(error)
})
}
}
function modalHeader() {
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
)}
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider
value={Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0))}
onChange={liquidityPercentChangeCallback}
/>
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyA}
label={'Output'}
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
currency={currencyB}
label={'Output'}
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {currencyB?.symbol} ={' '}
{tokenB
? pair
.priceOf(tokenB)
.invert()
.toSignificant(6)
: '-'}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>
)
}