214 lines
8.0 KiB
TypeScript
214 lines
8.0 KiB
TypeScript
import { BigNumber } from '@ethersproject/bignumber'
|
|
import { Contract } from '@ethersproject/contracts'
|
|
import { JSBI, Percent, Router, SwapParameters, Trade, TradeType } from '@pancakeswap/sdk'
|
|
import { useMemo } from 'react'
|
|
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
|
import { BIPS_BASE, INITIAL_ALLOWED_SLIPPAGE } from '../config/constants'
|
|
import { useTransactionAdder } from '../state/transactions/hooks'
|
|
import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
|
|
import isZero from '../utils/isZero'
|
|
import useTransactionDeadline from './useTransactionDeadline'
|
|
import useENS from './ENS/useENS'
|
|
|
|
export enum SwapCallbackState {
|
|
INVALID,
|
|
LOADING,
|
|
VALID,
|
|
}
|
|
|
|
interface SwapCall {
|
|
contract: Contract
|
|
parameters: SwapParameters
|
|
}
|
|
|
|
interface SuccessfulCall {
|
|
call: SwapCall
|
|
gasEstimate: BigNumber
|
|
}
|
|
|
|
interface FailedCall {
|
|
call: SwapCall
|
|
error: Error
|
|
}
|
|
|
|
type EstimatedSwapCall = SuccessfulCall | FailedCall
|
|
|
|
/**
|
|
* Returns the swap calls that can be used to make the trade
|
|
* @param trade trade to execute
|
|
* @param allowedSlippage user allowed slippage
|
|
* @param recipientAddressOrName
|
|
*/
|
|
function useSwapCallArguments(
|
|
trade: Trade | undefined, // trade to execute, required
|
|
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
|
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
|
): SwapCall[] {
|
|
const { account, chainId, library } = useActiveWeb3React()
|
|
|
|
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
|
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
|
const deadline = useTransactionDeadline()
|
|
|
|
return useMemo(() => {
|
|
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
|
|
|
|
const contract: Contract | null = getRouterContract(chainId, library, account)
|
|
if (!contract) {
|
|
return []
|
|
}
|
|
|
|
const swapMethods = []
|
|
|
|
swapMethods.push(
|
|
Router.swapCallParameters(trade, {
|
|
feeOnTransfer: false,
|
|
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
|
recipient,
|
|
deadline: deadline.toNumber(),
|
|
}),
|
|
)
|
|
|
|
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
|
swapMethods.push(
|
|
Router.swapCallParameters(trade, {
|
|
feeOnTransfer: true,
|
|
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
|
recipient,
|
|
deadline: deadline.toNumber(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
return swapMethods.map((parameters) => ({ parameters, contract }))
|
|
}, [account, allowedSlippage, chainId, deadline, library, recipient, trade])
|
|
}
|
|
|
|
// returns a function that will execute a swap, if the parameters are all valid
|
|
// and the user has approved the slippage adjusted input amount for the trade
|
|
export function useSwapCallback(
|
|
trade: Trade | undefined, // trade to execute, required
|
|
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
|
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
|
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {
|
|
const { account, chainId, library } = useActiveWeb3React()
|
|
|
|
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName)
|
|
|
|
const addTransaction = useTransactionAdder()
|
|
|
|
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
|
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
|
|
|
return useMemo(() => {
|
|
if (!trade || !library || !account || !chainId) {
|
|
return { state: SwapCallbackState.INVALID, callback: null, error: 'Missing dependencies' }
|
|
}
|
|
if (!recipient) {
|
|
if (recipientAddressOrName !== null) {
|
|
return { state: SwapCallbackState.INVALID, callback: null, error: 'Invalid recipient' }
|
|
}
|
|
return { state: SwapCallbackState.LOADING, callback: null, error: null }
|
|
}
|
|
|
|
return {
|
|
state: SwapCallbackState.VALID,
|
|
callback: async function onSwap(): Promise<string> {
|
|
const estimatedCalls: EstimatedSwapCall[] = await Promise.all(
|
|
swapCalls.map((call) => {
|
|
const {
|
|
parameters: { methodName, args, value },
|
|
contract,
|
|
} = call
|
|
const options = !value || isZero(value) ? {} : { value }
|
|
|
|
return contract.estimateGas[methodName](...args, options)
|
|
.then((gasEstimate) => {
|
|
return {
|
|
call,
|
|
gasEstimate,
|
|
}
|
|
})
|
|
.catch((gasError) => {
|
|
console.error('Gas estimate failed, trying eth_call to extract error', call)
|
|
|
|
return contract.callStatic[methodName](...args, options)
|
|
.then((result) => {
|
|
console.error('Unexpected successful call after failed estimate gas', call, gasError, result)
|
|
return { call, error: new Error('Unexpected issue with estimating the gas. Please try again.') }
|
|
})
|
|
.catch((callError) => {
|
|
console.error('Call threw error', call, callError)
|
|
const reason: string = callError.reason || callError.data?.message || callError.message
|
|
const errorMessage = `The transaction cannot succeed due to error: ${
|
|
reason ?? 'Unknown error, check the logs'
|
|
}.`
|
|
|
|
return { call, error: new Error(errorMessage) }
|
|
})
|
|
})
|
|
}),
|
|
)
|
|
|
|
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
|
|
const successfulEstimation = estimatedCalls.find(
|
|
(el, ix, list): el is SuccessfulCall =>
|
|
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1]),
|
|
)
|
|
|
|
if (!successfulEstimation) {
|
|
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
|
|
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
|
|
throw new Error('Unexpected error. Please contact support: none of the calls threw an error')
|
|
}
|
|
|
|
const {
|
|
call: {
|
|
contract,
|
|
parameters: { methodName, args, value },
|
|
},
|
|
gasEstimate,
|
|
} = successfulEstimation
|
|
|
|
return contract[methodName](...args, {
|
|
gasLimit: calculateGasMargin(gasEstimate),
|
|
...(value && !isZero(value) ? { value, from: account } : { from: account }),
|
|
})
|
|
.then((response: any) => {
|
|
const inputSymbol = trade.inputAmount.currency.symbol
|
|
const outputSymbol = trade.outputAmount.currency.symbol
|
|
const inputAmount = trade.inputAmount.toSignificant(3)
|
|
const outputAmount = trade.outputAmount.toSignificant(3)
|
|
|
|
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
|
const withRecipient =
|
|
recipient === account
|
|
? base
|
|
: `${base} to ${
|
|
recipientAddressOrName && isAddress(recipientAddressOrName)
|
|
? shortenAddress(recipientAddressOrName)
|
|
: recipientAddressOrName
|
|
}`
|
|
|
|
addTransaction(response, {
|
|
summary: withRecipient,
|
|
})
|
|
|
|
return response.hash
|
|
})
|
|
.catch((error: any) => {
|
|
// if the user rejected the tx, pass this along
|
|
if (error?.code === 4001) {
|
|
throw new Error('Transaction rejected.')
|
|
} else {
|
|
// otherwise, the error was unexpected and we need to convey that
|
|
console.error(`Swap failed`, error, methodName, args, value)
|
|
throw new Error(`Swap failed: ${error.message}`)
|
|
}
|
|
})
|
|
},
|
|
error: null,
|
|
}
|
|
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction])
|
|
}
|