diff --git a/src/swap/spookySwap/spookyContracts.js b/src/swap/spookySwap/spookyContracts.js index 2e8f419f..8d45832a 100644 --- a/src/swap/spookySwap/spookyContracts.js +++ b/src/swap/spookySwap/spookyContracts.js @@ -8,31 +8,19 @@ import WRAPPED_FTM_ABI from '../../abi/WRAPPED_FTM_ABI' export { UNISWAP_V2_PAIR_ABI } -// Providers -// - -export const hardCodedUrls = ['https://rpc.ftm.tools'] - -// TOOD: Use FallbackProvider when it's patched https://github.com/ethers-io/ethers.js/issues/2837 -export const provider = new ethers.providers.JsonRpcProvider(hardCodedUrls[0]) - -// -// Contracts -// - const SPOOKYSWAP_ROUTER_ADDRESS = '0xF491e7B69E4244ad4002BC14e878a34207E38c29' -export const spookySwapRouter = new ethers.Contract( - SPOOKYSWAP_ROUTER_ADDRESS, - UNISWAP_V2_ROUTER_ABI, - provider -) +export const makeSpookySwapRouter = (provider: ethers.Provider) => + new ethers.Contract( + SPOOKYSWAP_ROUTER_ADDRESS, + UNISWAP_V2_ROUTER_ABI, + provider + ) const WFTM_TOKEN_ADDRESS = '0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83' -export const wrappedFtmToken = new ethers.Contract( - WFTM_TOKEN_ADDRESS, - WRAPPED_FTM_ABI, - provider -) +export const makeWrappedFtmToken = (provider: ethers.Provider) => + new ethers.Contract(WFTM_TOKEN_ADDRESS, WRAPPED_FTM_ABI, provider) -export const makeErc20Contract = (tokenAddress: string) => - new ethers.Contract(tokenAddress, UNISWAP_V2_ERC20_ABI, provider) +export const makeErc20Contract = ( + tokenAddress: string, + provider: ethers.Provider +) => new ethers.Contract(tokenAddress, UNISWAP_V2_ERC20_ABI, provider) diff --git a/src/swap/spookySwap/spookySwap.js b/src/swap/spookySwap/spookySwap.js index eedbd956..627584f5 100644 --- a/src/swap/spookySwap/spookySwap.js +++ b/src/swap/spookySwap/spookySwap.js @@ -17,9 +17,8 @@ import { type PopulatedTransaction, ethers } from 'ethers' import { round } from '../../util/biggystringplus.js' import { makeErc20Contract, - provider, - spookySwapRouter, - wrappedFtmToken + makeSpookySwapRouter, + makeWrappedFtmToken } from './spookyContracts.js' const swapInfo: EdgeSwapInfo = { @@ -32,159 +31,180 @@ const expirationMs = 1000 * 20 * 60 const SLIPPAGE = '0.05' // 5% const SLIPPAGE_MULTIPLIER = sub('1', SLIPPAGE) -export const getMetaTokenAddress = ( - metaTokens: EdgeMetaToken[], - tokenCurrencyCode: string -): string => { - const metaToken = metaTokens.find(mt => mt.currencyCode === tokenCurrencyCode) - - if (metaToken == null || metaToken?.contractAddress === undefined) - throw new Error('Could not find contract address for ' + tokenCurrencyCode) - - return metaToken.contractAddress ?? '' -} - -export const getSwapTransactions = async ( - swapRequest: EdgeSwapRequest, - amountToSwap: string, - expectedAmountOut: string, - toAddress: string, - deadline: number -): Promise => { - const { fromWallet, toWallet, fromCurrencyCode, toCurrencyCode } = swapRequest - const currencyInfo = fromWallet.currencyInfo - const fromAddress = (await fromWallet.getReceiveAddress()).publicAddress - - // Sanity check: Both wallets should be of the same chain. - if ( - fromWallet.currencyInfo.currencyCode !== toWallet.currencyInfo.currencyCode - ) - throw new Error('SpookySwap: Mismatched wallet chain') - - // TODO: Use our new denom implementation to get native amounts - const nativeCurrencyCode = currencyInfo.currencyCode - const wrappedCurrencyCode = `W${nativeCurrencyCode}` - const isFromNativeCurrency = fromCurrencyCode === nativeCurrencyCode - const isToNativeCurrency = toCurrencyCode === nativeCurrencyCode - const isFromWrappedCurrency = fromCurrencyCode === wrappedCurrencyCode - const isToWrappedCurrency = toCurrencyCode === wrappedCurrencyCode - - // TODO: Do different wallets share the same custom metaTokens? - const metaTokens: EdgeMetaToken[] = currencyInfo.metaTokens - - const fromTokenAddress = getMetaTokenAddress( - metaTokens, - isFromNativeCurrency ? wrappedCurrencyCode : fromCurrencyCode - ) - const toTokenAddress = getMetaTokenAddress( - metaTokens, - isToNativeCurrency ? wrappedCurrencyCode : toCurrencyCode +export function makeSpookySwapPlugin( + opts: EdgeCorePluginOptions +): EdgeSwapPlugin { + const { log, initOptions } = opts + const { quiknodeApiKey } = initOptions + + // TOOD: Use FallbackProvider when it's patched https://github.com/ethers-io/ethers.js/issues/2837 + const provider = new ethers.providers.JsonRpcProvider( + quiknodeApiKey + ? `https://polished-empty-cloud.fantom.quiknode.pro/${quiknodeApiKey}` + : 'https://rpc.ftm.tools' ) - // Determine router method name and params - if (isFromNativeCurrency && isToNativeCurrency) - throw new Error('Invalid swap: Cannot swap to the same native currency') - const path = [fromTokenAddress, toTokenAddress] - - const gasPrice = await provider.getGasPrice() - - const addressToApproveTxs = async ( - tokenAddress: string, - contractAddress: string - ): PopulatedTransaction | void => { - const tokenContract = makeErc20Contract(tokenAddress) - const allowence = await tokenContract.allowance( - fromAddress, - contractAddress + const spookySwapRouter = makeSpookySwapRouter(provider) + const wrappedFtmToken = makeWrappedFtmToken(provider) + + const getMetaTokenAddress = ( + metaTokens: EdgeMetaToken[], + tokenCurrencyCode: string + ): string => { + const metaToken = metaTokens.find( + mt => mt.currencyCode === tokenCurrencyCode ) - if (allowence.sub(amountToSwap).lt(0)) { - return tokenContract.populateTransaction.approve( - contractAddress, - ethers.constants.MaxUint256, - { gasLimit: '60000', gasPrice } + + if (metaToken == null || metaToken?.contractAddress === undefined) + throw new Error( + 'Could not find contract address for ' + tokenCurrencyCode ) - } + + return metaToken.contractAddress ?? '' } - const txs = await (async (): Promise< - Array | void> - > => { - // Deposit native currency for wrapped token - if (isFromNativeCurrency && isToWrappedCurrency) { - return [ - wrappedFtmToken.populateTransaction.deposit({ - gasLimit: '51000', - gasPrice, - value: amountToSwap - }) - ] - } - // Withdraw wrapped token for native currency - if (isFromWrappedCurrency && isToNativeCurrency) { - return [ - // Deposit Tx - wrappedFtmToken.populateTransaction.withdraw(amountToSwap, { - gasLimit: '51000', - gasPrice - }) - ] - } - // Swap native currency for token - if (isFromNativeCurrency && !isToNativeCurrency) { - return [ - // Swap Tx - spookySwapRouter.populateTransaction.swapExactETHForTokens( - round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), - path, - toAddress, - deadline, - { gasLimit: '250000', gasPrice, value: amountToSwap } - ) - ] - } - // Swap token for native currency - if (!isFromNativeCurrency && isToNativeCurrency) { - return [ - // Approve TX - await addressToApproveTxs(path[0], spookySwapRouter.address), - // Swap Tx - spookySwapRouter.populateTransaction.swapExactTokensForETH( - amountToSwap, - round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), - path, - toAddress, - deadline, - { gasLimit: '250000', gasPrice } - ) - ] - } - // Swap token for token - if (!isFromNativeCurrency && !isToNativeCurrency) { - return [ - // Approve TX - await addressToApproveTxs(path[0], spookySwapRouter.address), - // Swap Tx - spookySwapRouter.populateTransaction.swapExactTokensForTokens( - amountToSwap, - round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), - path, - toAddress, - deadline, - { gasLimit: '600000', gasPrice } + const getSwapTransactions = async ( + swapRequest: EdgeSwapRequest, + amountToSwap: string, + expectedAmountOut: string, + toAddress: string, + deadline: number + ): Promise => { + const { + fromWallet, + toWallet, + fromCurrencyCode, + toCurrencyCode + } = swapRequest + const currencyInfo = fromWallet.currencyInfo + const fromAddress = (await fromWallet.getReceiveAddress()).publicAddress + + // Sanity check: Both wallets should be of the same chain. + if ( + fromWallet.currencyInfo.currencyCode !== + toWallet.currencyInfo.currencyCode + ) + throw new Error('SpookySwap: Mismatched wallet chain') + + // TODO: Use our new denom implementation to get native amounts + const nativeCurrencyCode = currencyInfo.currencyCode + const wrappedCurrencyCode = `W${nativeCurrencyCode}` + const isFromNativeCurrency = fromCurrencyCode === nativeCurrencyCode + const isToNativeCurrency = toCurrencyCode === nativeCurrencyCode + const isFromWrappedCurrency = fromCurrencyCode === wrappedCurrencyCode + const isToWrappedCurrency = toCurrencyCode === wrappedCurrencyCode + + // TODO: Do different wallets share the same custom metaTokens? + const metaTokens: EdgeMetaToken[] = currencyInfo.metaTokens + + const fromTokenAddress = getMetaTokenAddress( + metaTokens, + isFromNativeCurrency ? wrappedCurrencyCode : fromCurrencyCode + ) + const toTokenAddress = getMetaTokenAddress( + metaTokens, + isToNativeCurrency ? wrappedCurrencyCode : toCurrencyCode + ) + + // Determine router method name and params + if (isFromNativeCurrency && isToNativeCurrency) + throw new Error('Invalid swap: Cannot swap to the same native currency') + const path = [fromTokenAddress, toTokenAddress] + + const gasPrice = await provider.getGasPrice() + + const addressToApproveTxs = async ( + tokenAddress: string, + contractAddress: string + ): PopulatedTransaction | void => { + const tokenContract = makeErc20Contract(tokenAddress, provider) + const allowence = await tokenContract.allowance( + fromAddress, + contractAddress + ) + if (allowence.sub(amountToSwap).lt(0)) { + return tokenContract.populateTransaction.approve( + contractAddress, + ethers.constants.MaxUint256, + { gasLimit: '60000', gasPrice } ) - ] + } } - throw new Error('Unhandled swap type') - })() + const txs = await (async (): Promise< + Array | void> + > => { + // Deposit native currency for wrapped token + if (isFromNativeCurrency && isToWrappedCurrency) { + return [ + wrappedFtmToken.populateTransaction.deposit({ + gasLimit: '51000', + gasPrice, + value: amountToSwap + }) + ] + } + // Withdraw wrapped token for native currency + if (isFromWrappedCurrency && isToNativeCurrency) { + return [ + // Deposit Tx + wrappedFtmToken.populateTransaction.withdraw(amountToSwap, { + gasLimit: '51000', + gasPrice + }) + ] + } + // Swap native currency for token + if (isFromNativeCurrency && !isToNativeCurrency) { + return [ + // Swap Tx + spookySwapRouter.populateTransaction.swapExactETHForTokens( + round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), + path, + toAddress, + deadline, + { gasLimit: '250000', gasPrice, value: amountToSwap } + ) + ] + } + // Swap token for native currency + if (!isFromNativeCurrency && isToNativeCurrency) { + return [ + // Approve TX + await addressToApproveTxs(path[0], spookySwapRouter.address), + // Swap Tx + spookySwapRouter.populateTransaction.swapExactTokensForETH( + amountToSwap, + round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), + path, + toAddress, + deadline, + { gasLimit: '250000', gasPrice } + ) + ] + } + // Swap token for token + if (!isFromNativeCurrency && !isToNativeCurrency) { + return [ + // Approve TX + await addressToApproveTxs(path[0], spookySwapRouter.address), + // Swap Tx + spookySwapRouter.populateTransaction.swapExactTokensForTokens( + amountToSwap, + round(mul(expectedAmountOut, SLIPPAGE_MULTIPLIER)), + path, + toAddress, + deadline, + { gasLimit: '600000', gasPrice } + ) + ] + } - return await Promise.all(txs.filter(tx => tx != null)) -} + throw new Error('Unhandled swap type') + })() -export function makeSpookySwapPlugin( - opts: EdgeCorePluginOptions -): EdgeSwapPlugin { - const { log } = opts + return await Promise.all(txs.filter(tx => tx != null)) + } const out: EdgeSwapPlugin = { swapInfo,