diff --git a/.env.example b/.env.example index 14a061e..8b3e860 100644 --- a/.env.example +++ b/.env.example @@ -22,8 +22,10 @@ TERRA_CHAIN={testnet_chain_id} # Ethereum # - chain: mainnet, kovan, etc # - rpc url: infura or other rpc url +# - token list: erc20 token list source (ref: https://tokenlists.org/) ETHEREUM_CHAIN={chain} ETHEREUM_RPC_URL=https://{chain}.infura.io/v3/{api_key} +ETHEREUM_TOKEN_LIST_URL=https://wispy-bird-88a7.uniswap.workers.dev/?url=http://tokens.1inch.eth.link # Balancer # subgraph_chain @@ -41,9 +43,11 @@ EXCHANGE_PROXY={exchange_proxy} # Uniswap # Reference: https://uniswap.org/docs/v2/smart-contracts/router02/ +# UniswapV2Router02 is deployed at 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D on the Ethereum mainnet, and the Ropsten, Rinkeby, Görli, and Kovan testnets. +# It was built from commit 6961711. UNISWAP_ROUTER=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D # allowed slippage for swap transactions -UNISWAP_ALLOWED_SLIPPAGE=1 +UNISWAP_ALLOWED_SLIPPAGE=1.5 # restrict updating pairs that have no reserves or failed for 5 minutes UNISWAP_NO_RESERVE_CHECK_INTERVAL=300000 # cache info about pair for 1 second @@ -57,5 +61,23 @@ CERT_PASSPHRASE={passphrase} # default to ./logs if path is not set LOG_PATH=/Users/hbot/hummingbot_files/hummingbot_logs -# GMT offset -GMT_OFFSET=-0800 +# GMT offset for logging (alpine docker image default to UTC timezone) +# -0800, -0500, +0200, +0800 +GMT_OFFSET=+0800 + +# EthGasStation +# API key for defipulse.com gas station API +# Gas level you want to use for Ethereum transactions (fast, fastest, safeLow, average) +ENABLE_ETH_GAS_STATION=true +ETH_GAS_STATION_API_KEY={apikey} +ETH_GAS_STATION_GAS_LEVEL=fast +ETH_GAS_STATION_REFRESH_TIME=60 +MANUAL_GAS_PRICE=100 + +# Balancer Config +BALANCER_MAX_SWAPS=4 + + +# Perpetual Finance Provider URL +# default: https://dai.poa.network , https://rpc.xdaichain.com, etc +XDAI_PROVIDER={providerUrl} diff --git a/Dockerfile b/Dockerfile index b81d0b6..47d8c7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,14 @@ RUN apk add --no-cache tzdata # Set labels LABEL application="gateway-api" +LABEL branch=${BRANCH} +LABEL commit=${COMMIT} +LABEL date=${BUILD_DATE} + +# Set ENV variables +ENV COMMIT_BRANCH=${BRANCH} +ENV COMMIT_SHA=${COMMIT} +ENV BUILD_DATE=${DATE} # app directory WORKDIR /usr/src/app diff --git a/package.json b/package.json index e1efda5..817ee0d 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "@perp/contract": "^1.0.6", "@balancer-labs/sor": "^0.3.3", + "@perp/contract": "^1.0.6", "@terra-money/terra.js": "^0.5.8", "@uniswap/sdk": "^3.0.3", "app-root-path": "^3.0.0", @@ -21,6 +21,7 @@ "bignumber.js": "^9.0.0", "body-parser": "^1.19.0", "capture-console": "^1.0.1", + "cross-fetch": "^3.0.6", "debug": "^4.2.0", "dotenv": "^8.2.0", "ethers": "^5.0.14", @@ -29,11 +30,11 @@ "helmet": "^4.1.1", "http-status-codes": "^2.1.3", "lodash": "^4.17.20", + "mathjs": "^9.3.0", "moment": "^2.29.1", "util": "^0.12.3", "winston": "^3.3.3", - "winston-daily-rotate-file": "^4.5.0", - "cross-fetch": "^3.0.6" + "winston-daily-rotate-file": "^4.5.0" }, "devDependencies": { "@babel/core": "^7.11.6", @@ -47,5 +48,8 @@ "eslint-plugin-standard": "^4.0.1", "nodemon": "^2.0.4" }, + "engines": { + "node": "10.x" + }, "type": "module" } diff --git a/src/app.js b/src/app.js index 6b2e7a0..647b14f 100644 --- a/src/app.js +++ b/src/app.js @@ -40,14 +40,14 @@ app.use(bodyParser.urlencoded({ extended: true })); app.use(validateAccess) -// mount all routes to this path -app.use('/uniswap', uniswapRoutes); +// mount routes to specific path app.use('/api', apiRoutes); app.use('/eth', ethRoutes); -// app.use('/celo', celoRoutes); +app.use('/eth/uniswap', uniswapRoutes); +app.use('/eth/balancer', balancerRoutes); app.use('/terra', terraRoutes); -app.use('/balancer', balancerRoutes); app.use('/perpfi', perpFiRoutes); +// app.use('/celo', celoRoutes); app.get('/', (req, res, next) => { res.send('ok') diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 11eb975..a27fe5d 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -4,11 +4,16 @@ import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import Ethereum from '../services/eth'; import Balancer from '../services/balancer'; +import Fees from '../services/fees'; import { logger } from '../services/logger'; +const debug = require('debug')('router') const router = express.Router() +const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const balancer = new Balancer(process.env.ETHEREUM_CHAIN) +const fees = new Fees() const swapMoreThanMaxPriceError = 'Price too high' const swapLessThanMaxPriceError = 'Price too low' @@ -62,219 +67,138 @@ router.post('/gas-limit', async (req, res) => { } }) -router.post('/sell-price', async (req, res) => { - /* - POST: /sell-price +router.get('/start', async (req, res) => { + /* + POST: /eth/balancer/start x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." - "amount":0.1 - "maxSwaps":4 - "base_decimals":18 - "quote_decimals":18 + "pairs":'["ETH-USDT", ...]' + "gasPrice":30 } */ const initTime = Date.now() - // params: base (required), quote (required), amount (required) - const paramData = getParamData(req.body) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const baseDenomMultiplier = 10 ** paramData.base_decimals - const quoteDenomMultiplier = 10 ** paramData.quote_decimals - const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) - let maxSwaps - if (paramData.maxSwaps) { - maxSwaps = parseInt(paramData.maxSwaps) + const paramData = getParamData(req.query) + const pairs = JSON.parse(paramData.pairs) + let gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice } - try { - // fetch the optimal pool mix from balancer-sor - const { swaps, expectedOut } = await balancer.priceSwapIn( - baseTokenAddress, // tokenIn is base asset - quoteTokenAddress, // tokenOut is quote asset - amount, - maxSwaps, - ) + // get token contract address and cache pools + for (let pair of pairs){ + pair = pair.split("-") + const baseTokenSymbol = pair[0] + const quoteTokenSymbol = pair[1] + const baseTokenContractInfo = eth.getERC20TokenAddresses(baseTokenSymbol) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(quoteTokenSymbol) - if (swaps != null && expectedOut != null) { - const gasLimit = estimateGasLimit(swaps.length) - res.status(200).json({ - network: balancer.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), - expectedOut: parseInt(expectedOut) / quoteDenomMultiplier, - price: expectedOut / amount * baseDenomMultiplier / quoteDenomMultiplier, - gasLimit: gasLimit, - swaps: swaps, - }) - } else { // no pool available - res.status(200).json({ - error: statusMessages.no_pool_available, - message: '' + // check for valid token symbols + if (baseTokenContractInfo === undefined || quoteTokenContractInfo === undefined) { + const undefinedToken = baseTokenContractInfo === undefined ? baseTokenSymbol : quoteTokenSymbol + res.status(500).json({ + error: `Token ${undefinedToken} contract address not found`, + message: `Token contract address not found for ${undefinedToken}. Check token list source`, }) + return } - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ - error: reason, - message: err - }) + await Promise.allSettled([balancer.fetchPool(baseTokenContractInfo.address, quoteTokenContractInfo.address), + balancer.fetchPool(quoteTokenContractInfo.address, baseTokenContractInfo.address)]) } -}) -router.post('/buy-price', async (req, res) => { - /* - POST: /buy-price - x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." - "amount":0.1 - "maxSwaps":4 - "base_decimals":18 - "quote_decimals":18 - } - */ - const initTime = Date.now() - // params: base (required), quote (required), amount (required) - const paramData = getParamData(req.body) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const baseDenomMultiplier = 10 ** paramData.base_decimals - const quoteDenomMultiplier = 10 ** paramData.quote_decimals - const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) - let maxSwaps - if (paramData.maxSwaps) { - maxSwaps = parseInt(paramData.maxSwaps) - } - try { - // fetch the optimal pool mix from balancer-sor - const { swaps, expectedIn } = await balancer.priceSwapOut( - quoteTokenAddress, // tokenIn is quote asset - baseTokenAddress, // tokenOut is base asset - amount, - maxSwaps, - ) + const gasLimit = estimateGasLimit(balancer.maxSwaps) + const gasCost = await fees.getGasCost(gasPrice, gasLimit) - if (swaps != null && expectedIn != null) { - const gasLimit = estimateGasLimit(swaps.length) - res.status(200).json({ - network: balancer.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), - expectedIn: parseInt(expectedIn) / quoteDenomMultiplier, - price: expectedIn / amount * baseDenomMultiplier / quoteDenomMultiplier, - gasLimit: gasLimit, - swaps: swaps, - }) - } else { // no pool available - res.status(200).json({ - error: statusMessages.no_pool_available, - message: '' - }) - } - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ - error: reason, - message: err - }) + const result = { + network: eth.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + success: true, + pairs: pairs, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, } + console.log('Initializing balancer') + res.status(200).json(result) }) -router.post('/sell', async (req, res) => { +router.post('/price', async (req, res) => { /* - POST: /sell + POST: /eth/balancer/price x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." + "quote":"BAT" + "base":"USDC" "amount":0.1 - "base_decimals":18 - "quote_decimals":18 - "minPrice":1 - "gasPrice":10 - "maxSwaps":4 - "privateKey":{{privateKey}} + "side":buy } */ const initTime = Date.now() - // params: privateKey (required), base (required), quote (required), amount (required), maxPrice (required), gasPrice (required) + // params: base (required), quote (required), amount (required) const paramData = getParamData(req.body) - const privateKey = paramData.privateKey - const wallet = new ethers.Wallet(privateKey, balancer.provider) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const baseDenomMultiplier = 10 ** paramData.base_decimals - const quoteDenomMultiplier = 10 ** paramData.quote_decimals - const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) - - let maxPrice - if (paramData.maxPrice) { - maxPrice = parseFloat(paramData.maxPrice) - } + const baseTokenContractInfo = eth.getERC20TokenAddresses(paramData.base) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(paramData.quote) + const baseTokenAddress = baseTokenContractInfo.address + const quoteTokenAddress = quoteTokenContractInfo.address + const baseDenomMultiplier = 10 ** baseTokenContractInfo.decimals + const quoteDenomMultiplier = 10 ** quoteTokenContractInfo.decimals + const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) + const maxSwaps = balancer.maxSwaps + const side = paramData.side.toUpperCase() let gasPrice if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice } - let maxSwaps - if (paramData.maxSwaps) { - maxSwaps = parseInt(paramData.maxSwaps) - } - - const minAmountOut = maxPrice / amount * baseDenomMultiplier - logger.debug('minAmountOut', minAmountOut) try { // fetch the optimal pool mix from balancer-sor - const { swaps, expectedOut } = await balancer.priceSwapIn( - baseTokenAddress, // tokenIn is base asset - quoteTokenAddress, // tokenOut is quote asset - amount, - maxSwaps, - ) - - const price = expectedOut / amount * baseDenomMultiplier / quoteDenomMultiplier - logger.info(`Price: ${price.toString()}`) - if (!maxPrice || price >= maxPrice) { - // pass swaps to exchange-proxy to complete trade - const tx = await balancer.swapExactIn( - wallet, - swaps, - baseTokenAddress, // tokenIn is base asset - quoteTokenAddress, // tokenOut is quote asset - amount.toString(), - parseInt(expectedOut) / quoteDenomMultiplier, - gasPrice, + const { swaps, expectedAmount } = side === 'BUY' + ? await balancer.priceSwapOut( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + amount, + maxSwaps, + ) + : await balancer.priceSwapIn( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + amount, + maxSwaps, ) - // submit response - res.status(200).json({ + if (swaps != null && expectedAmount != null) { + const gasLimit = estimateGasLimit(swaps.length) + const gasCost = await fees.getGasCost(gasPrice, gasLimit) + + const tradeAmount = parseFloat(amount) + const expectedTradeAmount = parseInt(expectedAmount) / quoteDenomMultiplier + const tradePrice = expectedAmount / amount * baseDenomMultiplier / quoteDenomMultiplier + + const result = { network: balancer.network, timestamp: initTime, latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), - expectedOut: expectedOut / quoteDenomMultiplier, - price: price, - txHash: tx.hash, - }) - } else { + base: baseTokenContractInfo, + quote: quoteTokenContractInfo, + amount: tradeAmount, + side: side, + expectedAmount: expectedTradeAmount, + price: tradePrice, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, + swaps: swaps, + } + debug(`Price ${side} ${baseTokenContractInfo.symbol}-${quoteTokenContractInfo.symbol} | amount:${amount} (rate:${tradePrice}) - gasPrice:${gasPrice} gasLimit:${gasLimit} estimated fee:${gasCost} ETH`) + res.status(200).json(result) + } else { // no pool available res.status(200).json({ - error: swapLessThanMaxPriceError, - message: `Swap price ${price} lower than maxPrice ${maxPrice}` + info: statusMessages.no_pool_available, + message: statusMessages.no_pool_available }) - logger.debug(`Swap price ${price} lower than maxPrice ${maxPrice}`) } } catch (err) { logger.error(req.originalUrl, { message: err }) @@ -287,85 +211,140 @@ router.post('/sell', async (req, res) => { } }) -router.post('/buy', async (req, res) => { +router.post('/trade', async (req, res) => { /* - POST: /buy + POST: /trade x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." + "quote":"BAT" + "base":"USDC" "amount":0.1 - "base_decimals":18 - "quote_decimals":18 - "maxPrice":1 + "limitPrice":1 "gasPrice":10 - "maxSwaps":4 + "side":{buy|sell} "privateKey":{{privateKey}} } */ const initTime = Date.now() - // params: privateKey (required), base (required), quote (required), amount (required), maxPrice (required), gasPrice (required) const paramData = getParamData(req.body) const privateKey = paramData.privateKey const wallet = new ethers.Wallet(privateKey, balancer.provider) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const baseDenomMultiplier = 10 ** paramData.base_decimals - const quoteDenomMultiplier = 10 ** paramData.quote_decimals - const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) - let maxPrice - if (paramData.maxPrice) { - maxPrice = parseFloat(paramData.maxPrice) + const baseTokenContractInfo = eth.getERC20TokenAddresses(paramData.base) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(paramData.quote) + const baseTokenAddress = baseTokenContractInfo.address + const quoteTokenAddress = quoteTokenContractInfo.address + const baseDenomMultiplier = 10 ** baseTokenContractInfo.decimals + const quoteDenomMultiplier = 10 ** quoteTokenContractInfo.decimals + const amount = new BigNumber(parseInt(paramData.amount * baseDenomMultiplier)) + + const maxSwaps = balancer.maxSwaps + const side = paramData.side.toUpperCase() + + let limitPrice + if (paramData.limitPrice) { + limitPrice = parseFloat(paramData.limitPrice) } let gasPrice if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) - } - let maxSwaps - if (paramData.maxSwaps) { - maxSwaps = parseInt(paramData.maxSwaps) + } else { + gasPrice = fees.ethGasPrice } try { // fetch the optimal pool mix from balancer-sor - const { swaps, expectedIn } = await balancer.priceSwapOut( - quoteTokenAddress, // tokenIn is quote asset - baseTokenAddress, // tokenOut is base asset - amount, - maxSwaps, - ) - - const price = expectedIn / amount * baseDenomMultiplier / quoteDenomMultiplier - logger.info(`Price: ${price.toString()}`) - if (!maxPrice || price <= maxPrice) { - // pass swaps to exchange-proxy to complete trade - const tx = await balancer.swapExactOut( - wallet, - swaps, - quoteTokenAddress, // tokenIn is quote asset - baseTokenAddress, // tokenOut is base asset - expectedIn.toString(), - gasPrice, + const { swaps, expectedAmount } = side === 'BUY' + ? await balancer.priceSwapOut( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + amount, + maxSwaps, + ) + : await balancer.priceSwapIn( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + amount, + maxSwaps, ) - // submit response - res.status(200).json({ - network: balancer.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), - expectedIn: expectedIn / quoteDenomMultiplier, - price: price, - txHash: tx.hash, - }) + const gasLimit = estimateGasLimit(swaps.length) + const gasCost = await fees.getGasCost(gasPrice, gasLimit) + + if (side === 'BUY') { + const price = expectedAmount / amount * baseDenomMultiplier / quoteDenomMultiplier + logger.info(`Price: ${price.toString()}`) + if (!limitPrice || price <= limitPrice) { + // pass swaps to exchange-proxy to complete trade + const tx = await balancer.swapExactOut( + wallet, + swaps, + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + expectedAmount.toString(), + gasPrice, + ) + + // submit response + res.status(200).json({ + network: balancer.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenContractInfo, + quote: quoteTokenContractInfo, + amount: parseFloat(paramData.amount), + expectedIn: expectedAmount / quoteDenomMultiplier, + price: price, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, + txHash: tx.hash, + }) + } else { + res.status(200).json({ + error: swapMoreThanMaxPriceError, + message: `Swap price ${price} exceeds limitPrice ${limitPrice}` + }) + debug(`Swap price ${price} exceeds limitPrice ${limitPrice}`) + } } else { - res.status(200).json({ - error: swapMoreThanMaxPriceError, - message: `Swap price ${price} exceeds maxPrice ${maxPrice}` - }) - logger.debug(`Swap price ${price} exceeds maxPrice ${maxPrice}`) + // sell + const minAmountOut = limitPrice / amount * baseDenomMultiplier + debug('minAmountOut', minAmountOut) + const price = expectedAmount / amount * baseDenomMultiplier / quoteDenomMultiplier + logger.info(`Price: ${price.toString()}`) + if (!limitPrice || price >= limitPrice) { + // pass swaps to exchange-proxy to complete trade + const tx = await balancer.swapExactIn( + wallet, + swaps, + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + amount.toString(), + parseInt(expectedAmount) / quoteDenomMultiplier, + gasPrice, + ) + // submit response + res.status(200).json({ + network: balancer.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenContractInfo, + quote: quoteTokenContractInfo, + amount: parseFloat(paramData.amount), + expectedOut: expectedAmount / quoteDenomMultiplier, + price: price, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, + txHash: tx.hash, + }) + } else { + res.status(200).json({ + error: swapLessThanMaxPriceError, + message: `Swap price ${price} lower than limitPrice ${limitPrice}` + }) + debug(`Swap price ${price} lower than limitPrice ${limitPrice}`) + } } } catch (err) { logger.error(req.originalUrl, { message: err }) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index c5c8798..53dfe79 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -1,23 +1,38 @@ import { ethers, BigNumber } from 'ethers'; import express from 'express'; -import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getParamData, latency, statusMessages } from '../services/utils'; import Ethereum from '../services/eth'; +import Fees from '../services/fees'; import { logger } from '../services/logger'; +const debug = require('debug')('router') const router = express.Router() const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const spenders = { balancer: process.env.EXCHANGE_PROXY, uniswap: process.env.UNISWAP_ROUTER } +const fees = new Fees() + +router.post('/', async (req, res) => { + /* + POST / + */ + res.status(200).json({ + network: eth.network, + rpcUrl: eth.provider.connection.url, + connection: true, + timestamp: Date.now(), + }) +}) router.post('/balances', async (req, res) => { /* POST: /balances x-www-form-urlencoded: { privateKey:{{privateKey}} - tokenAddressList:{{tokenAddressList}} + tokenList:{{tokenList}} } */ const initTime = Date.now() @@ -36,19 +51,32 @@ router.post('/balances', async (req, res) => { }) return } - let tokenAddressList - if (paramData.tokenAddressList) { - tokenAddressList = JSON.parse(paramData.tokenAddressList) - } + + // populate token contract info using token symbol list + const tokenContractList = [] + const tokenList = JSON.parse(paramData.tokenList) + tokenList.forEach(symbol => { + const tokenContractInfo = eth.getERC20TokenAddresses(symbol) + tokenContractList[symbol] = tokenContractInfo + }); const balances = {} balances.ETH = await eth.getETHBalance(wallet, privateKey) try { Promise.all( - Object.keys(tokenAddressList).map(async (key, index) => - balances[key] = await eth.getERC20Balance(wallet, key, tokenAddressList[key]) + Object.keys(tokenContractList).map(async (symbol, index) => { + if (tokenContractList[symbol] !== undefined) { + const address = tokenContractList[symbol].address + const decimals = tokenContractList[symbol].decimals + balances[symbol] = await eth.getERC20Balance(wallet, address, decimals) + } else { + const err = `Token contract info for ${symbol} not found` + logger.error('Token info not found', { message: err }) + debug(err) + } + } )).then(() => { - logger.info('eth.route - Get Account Balance', { message: JSON.stringify(tokenAddressList) }) + console.log('eth.route - Get Account Balance', { message: JSON.stringify(tokenList) }) res.status(200).json({ network: eth.network, timestamp: initTime, @@ -93,18 +121,25 @@ router.post('/allowances', async (req, res) => { }) return } - let tokenAddressList - if (paramData.tokenAddressList) { - tokenAddressList = JSON.parse(paramData.tokenAddressList) - } + + // populate token contract info using token symbol list + const tokenContractList = [] + const tokenList = JSON.parse(paramData.tokenList) + tokenList.forEach(symbol => { + const tokenContractInfo = eth.getERC20TokenAddresses(symbol) + tokenContractList[symbol] = tokenContractInfo + }); const approvals = {} try { Promise.all( - Object.keys(tokenAddressList).map(async (key, index) => - approvals[key] = await eth.getERC20Allowance(wallet, spender, key, tokenAddressList[key]) - )).then(() => { - logger.info('eth.route - Getting allowances', { message: JSON.stringify(tokenAddressList) }) + Object.keys(tokenContractList).map(async (symbol, index) => { + const address = tokenContractList[symbol].address + const decimals = tokenContractList[symbol].decimals + approvals[symbol] = await eth.getERC20Allowance(wallet, spender, address, decimals) + } + )).then(() => { + logger.info('eth.route - Getting allowances', { message: JSON.stringify(tokenList) }) res.status(200).json({ network: eth.network, timestamp: initTime, @@ -270,21 +305,25 @@ router.post('/approve', async (req, res) => { }) return } - const tokenAddress = paramData.tokenAddress - let amount, decimals - paramData.decimals ? decimals = paramData.decimals - : decimals = 18 + const token = paramData.token + const tokenContractInfo = eth.getERC20TokenAddresses(token) + const tokenAddress = tokenContractInfo.address + const decimals = tokenContractInfo.decimals + + let amount paramData.amount ? amount = ethers.utils.parseUnits(paramData.amount, decimals) : amount = ethers.utils.parseUnits('1000000000', decimals) // approve for 1 billion units if no amount specified let gasPrice if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice } try { // call approve function const approval = await eth.approveERC20(wallet, spender, tokenAddress, amount, gasPrice) - logger.info('eth.route - Approving allowance', { message: tokenAddress }) + // console.log('eth.route - Approving allowance', { message: approval }) // submit response res.status(200).json({ network: eth.network, @@ -306,62 +345,7 @@ router.post('/approve', async (req, res) => { } }) -// Faucet to get test tokens -router.post('/get-weth', async (req, res) => { - /* - POST: /get-weth - x-www-form-urlencoded: { - gasPrice:{gasPrice} - amount:{{amount}} - privateKey:{{privateKey}} - } - */ - const initTime = Date.now() - const paramData = getParamData(req.body) - const privateKey = paramData.privateKey - let wallet - try { - wallet = new ethers.Wallet(privateKey, eth.provider) - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - err.reason ? reason = err.reason : reason = 'Error getting wallet' - res.status(500).json({ - error: reason, - message: err - }) - return - } - const amount = ethers.utils.parseEther(paramData.amount) - const tokenAddress = eth.erc20KovanTokens['WETH'] - let gasPrice - if (paramData.gasPrice) { - gasPrice = parseFloat(paramData.gasPrice) - } - - try { - // call deposit function - const response = await eth.deposit(wallet, tokenAddress, amount, gasPrice) - - // submit response - res.status(200).json({ - network: eth.network, - timestamp: initTime, - amount: parseFloat(amount), - result: response - }) - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ - error: reason, - message: err - }) - } -}) - -router.post('/get-receipt', async (req, res) => { +router.post('/poll', async (req, res) => { const initTime = Date.now() const paramData = getParamData(req.body) const txHash = paramData.txHash @@ -386,4 +370,59 @@ router.post('/get-receipt', async (req, res) => { return txReceipt }) +// Kovan faucet to get test tokens (wip) & weth conversion +// router.post('/get-weth', async (req, res) => { +// /* +// POST: /get-weth +// x-www-form-urlencoded: { +// gasPrice:{gasPrice} +// amount:{{amount}} +// privateKey:{{privateKey}} +// } +// */ +// const initTime = Date.now() +// const paramData = getParamData(req.body) +// const privateKey = paramData.privateKey +// let wallet +// try { +// wallet = new ethers.Wallet(privateKey, eth.provider) +// } catch (err) { +// logger.error(req.originalUrl, { message: err }) +// let reason +// err.reason ? reason = err.reason : reason = 'Error getting wallet' +// res.status(500).json({ +// error: reason, +// message: err +// }) +// return +// } +// const amount = ethers.utils.parseEther(paramData.amount) +// const tokenAddress = eth.getERC20TokenAddresses('WETH').address +// let gasPrice +// if (paramData.gasPrice) { +// gasPrice = parseFloat(paramData.gasPrice) +// } + +// try { +// // call deposit function +// const response = await eth.deposit(wallet, tokenAddress, amount, gasPrice) + +// // submit response +// res.status(200).json({ +// network: eth.network, +// timestamp: initTime, +// amount: parseFloat(amount), +// result: response +// }) +// } catch (err) { +// logger.error(req.originalUrl, { message: err }) +// let reason +// err.reason ? reason = err.reason : reason = statusMessages.operation_error +// res.status(500).json({ +// error: reason, +// message: err +// }) +// } +// }) + module.exports = router; diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 96cab36..f2c64c3 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -6,6 +6,7 @@ import { logger } from '../services/logger'; import Terra from '../services/terra'; +const debug = require('debug')('router') const router = express.Router(); const terra = new Terra() @@ -49,7 +50,7 @@ router.post('/balances', async (req, res) => { balances[symbol] = amount }) }) - logger.info('terra.route - Get Account Balance', { message: address }) + logger.info('terra.route - Get Account Balance') res.status(200).json({ network: network, timestamp: initTime, @@ -75,13 +76,38 @@ router.post('/balances', async (req, res) => { } }) +router.post('/start', async (req, res) => { + /* + POST: /terra/start + x-www-form-urlencoded: { + "base":"UST" + "quote":"KRT" + "amount":1 + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const baseTokenSymbol = paramData.base + const quoteTokenSymbol = paramData.quote + + const result = { + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + success: true, + base: baseTokenSymbol, + quote: quoteTokenSymbol, + } + res.status(200).json(result) +}) + router.post('/price', async (req, res) => { /* POST: x-www-form-urlencoded: { "base":"UST" "quote":"KRT" - "trade_type":"buy" or "sell" + "side":"buy" or "sell" "amount":1 } */ @@ -90,7 +116,7 @@ router.post('/price', async (req, res) => { const paramData = getParamData(req.body) const baseToken = paramData.base const quoteToken = paramData.quote - const tradeType = paramData.trade_type + const tradeType = paramData.side.toUpperCase() const amount = parseFloat(paramData.amount) let exchangeRate @@ -141,7 +167,7 @@ router.post('/trade', async (req, res) => { data: { "base":"UST" "quote":"KRT" - "trade_type":"buy" or "sell" + "side":"buy" or "sell" "amount":1 "secret": "mysupersecret" } @@ -151,11 +177,11 @@ router.post('/trade', async (req, res) => { const paramData = getParamData(req.body) const baseToken = paramData.base const quoteToken = paramData.quote - const tradeType = paramData.trade_type + const tradeType = paramData.side.toUpperCase() const amount = parseFloat(paramData.amount) const gasPrice = parseFloat(paramData.gas_price) || terra.lcd.config.gasPrices.uluna const gasAdjustment = paramData.gas_adjustment || terra.lcd.config.gasAdjustment - const secret = paramData.secret + const secret = paramData.privateKey let tokenSwaps diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 6e76505..812e0ef 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -3,14 +3,19 @@ import express from 'express'; import { getParamData, latency, statusMessages } from '../services/utils'; import { logger } from '../services/logger'; +import Ethereum from '../services/eth'; import Uniswap from '../services/uniswap'; +import Fees from '../services/fees'; require('dotenv').config() +const debug = require('debug')('router') const router = express.Router() +const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) uniswap.generate_tokens() setTimeout(uniswap.update_pairs.bind(uniswap), 2000) +const fees = new Fees() const swapMoreThanMaxPriceError = 'Price too high' const swapLessThanMaxPriceError = 'Price too low' @@ -72,143 +77,72 @@ router.post('/gas-limit', async (req, res) => { } }) -router.post('/sell-price', async (req, res) => { +router.get('/start', async (req, res) => { /* - POST: /sell-price + POST: /eth/uniswap/start x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." - "amount":0.1 + "pairs":"[ETH-USDT, ...]" + "gasPrice":30 } */ const initTime = Date.now() - // params: base (required), quote (required), amount (required) - const paramData = getParamData(req.body) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const amount = paramData.amount + const paramData = getParamData(req.query) + const pairs = JSON.parse(paramData.pairs) + let gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice + } - try { - // fetch the optimal pool mix from uniswap - const { trade, expectedOut } = await uniswap.priceSwapIn( - baseTokenAddress, // tokenIn is base asset - quoteTokenAddress, // tokenOut is quote asset - amount - ) + // get token contract address and cache paths + for (let pair of pairs){ + pair = pair.split("-") + const baseTokenSymbol = pair[0] + const quoteTokenSymbol = pair[1] + const baseTokenContractInfo = eth.getERC20TokenAddresses(baseTokenSymbol) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(quoteTokenSymbol) - if (trade !== null && expectedOut !== null) { - res.status(200).json({ - network: uniswap.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: amount, - expectedOut: expectedOut.toSignificant(8), - price: trade.executionPrice.toSignificant(8), - trade: trade, + // check for valid token symbols + if (baseTokenContractInfo === undefined || quoteTokenContractInfo === undefined) { + const undefinedToken = baseTokenContractInfo === undefined ? baseTokenSymbol : quoteTokenSymbol + res.status(500).json({ + error: `Token ${undefinedToken} contract address not found`, + message: `Token contract address not found for ${undefinedToken}. Check token list source`, }) - } else { // no pool available - res.status(200).json({ - error: statusMessages.no_pool_available, - message: '' - }) - } - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - let errCode = 500 - if (Object.keys(err).includes('isInsufficientReservesError')) { - errCode = 200 - reason = statusMessages.insufficient_reserves + ' in Sell at Uniswap' - } else if (Object.getOwnPropertyNames(err).includes('message')) { - reason = getErrorMessage(err.message) - if (reason === statusMessages.no_pool_available) { - errCode = 200 - } - } else { - err.reason ? reason = err.reason : reason = statusMessages.operation_error + return } - res.status(errCode).json({ - error: reason, - message: err - }) + await Promise.allSettled([uniswap.extend_update_pairs([baseTokenContractInfo.address, quoteTokenContractInfo.address])]) } -}) -router.post('/buy-price', async (req, res) => { - /* - POST: /buy-price - x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." - "amount":0.1 - } - */ - const initTime = Date.now() - // params: base (required), quote (required), amount (required) - const paramData = getParamData(req.body) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote - const amount = paramData.amount + const gasLimit = estimateGasLimit() + const gasCost = await fees.getGasCost(gasPrice, gasLimit) - try { - // fetch the optimal pool mix from uniswap - const { trade, expectedIn } = await uniswap.priceSwapOut( - quoteTokenAddress, // tokenIn is quote asset - baseTokenAddress, // tokenOut is base asset - amount - ) - if (trade !== null && expectedIn !== null) { - res.status(200).json({ - network: uniswap.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: amount, - expectedIn: expectedIn.toSignificant(8), - price: trade.executionPrice.invert().toSignificant(8), - trade: trade, - }) - } else { // no pool available - res.status(200).json({ - error: statusMessages.no_pool_available, - message: '' - }) - } - } catch (err) { - logger.error(req.originalUrl, { message: err }) - let reason - let errCode = 500 - if (Object.keys(err).includes('isInsufficientReservesError')) { - errCode = 200 - reason = statusMessages.insufficient_reserves + ' in Buy at Uniswap' - } else if (Object.getOwnPropertyNames(err).includes('message')) { - reason = getErrorMessage(err.message) - if (reason === statusMessages.no_pool_available) { - errCode = 200 - } - } else { - err.reason ? reason = err.reason : reason = statusMessages.operation_error - } - res.status(errCode).json({ - error: reason, - message: err - }) + + const result = { + network: eth.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + success: true, + pairs: pairs, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, } + res.status(200).json(result) }) -router.post('/sell', async (req, res) => { +router.post('/trade', async (req, res) => { /* - POST: /sell + POST: /trade x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." + "quote":"BAT" + "base":"DAI" "amount":0.1 - "minPrice":1 + "limitPrice":1 "gasPrice":10 "privateKey":{{privateKey}} + "side":{buy|sell} } */ const initTime = Date.now() @@ -216,56 +150,108 @@ router.post('/sell', async (req, res) => { const paramData = getParamData(req.body) const privateKey = paramData.privateKey const wallet = new ethers.Wallet(privateKey, uniswap.provider) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote const amount = paramData.amount - let maxPrice - if (paramData.maxPrice) { - maxPrice = parseFloat(paramData.maxPrice) + const baseTokenContractInfo = eth.getERC20TokenAddresses(paramData.base) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(paramData.quote) + const baseTokenAddress = baseTokenContractInfo.address + const quoteTokenAddress = quoteTokenContractInfo.address + const side = paramData.side.toUpperCase() + + let limitPrice + if (paramData.limitPrice) { + limitPrice = parseFloat(paramData.limitPrice) } let gasPrice if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice } + const gasLimit = estimateGasLimit() + const gasCost = await fees.getGasCost(gasPrice, gasLimit) try { // fetch the optimal pool mix from uniswap - const { trade, expectedOut } = await uniswap.priceSwapIn( - baseTokenAddress, // tokenIn is base asset - quoteTokenAddress, // tokenOut is quote asset - amount - ) - - const price = trade.executionPrice.toSignificant(8) - logger.debug(`Price: ${price.toString()}`) - if (!maxPrice || price >= maxPrice) { - // pass swaps to exchange-proxy to complete trade - const tx = await uniswap.swapExactIn( - wallet, - trade, - baseTokenAddress, - gasPrice, + const { trade, expectedAmount } = side === 'BUY' + ? await uniswap.priceSwapOut( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + amount + ) + : await uniswap.priceSwapIn( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + amount ) - // submit response - res.status(200).json({ - network: uniswap.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseTokenAddress, - quote: quoteTokenAddress, - amount: amount, - expectedOut: expectedOut.toSignificant(8), - price: price, - txHash: tx.hash, - }) + if (side === 'BUY') { + const price = trade.executionPrice.invert().toSignificant(8) + logger.info(`uniswap.route - Price: ${price.toString()}`) + if (!limitPrice || price <= limitPrice) { + // pass swaps to exchange-proxy to complete trade + const tx = await uniswap.swapExactOut( + wallet, + trade, + baseTokenAddress, + gasPrice, + ) + // submit response + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: amount, + expectedIn: expectedAmount.toSignificant(8), + price: price, + gasPrice: gasPrice, + gasLimit, gasLimit, + gasCost, gasCost, + txHash: tx.hash, + }) + } else { + res.status(200).json({ + error: swapMoreThanMaxPriceError, + message: `Swap price ${price} exceeds limitPrice ${limitPrice}` + }) + logger.info(`uniswap.route - Swap price ${price} exceeds limitPrice ${limitPrice}`) + } } else { - res.status(200).json({ - error: swapLessThanMaxPriceError, - message: `Swap price ${price} lower than maxPrice ${maxPrice}` - }) - logger.info(`uniswap.route - Swap price ${price} lower than maxPrice ${maxPrice}`) + // sell + const price = trade.executionPrice.toSignificant(8) + logger.info(`Price: ${price.toString()}`) + if (!limitPrice || price >= limitPrice) { + // pass swaps to exchange-proxy to complete trade + const tx = await uniswap.swapExactIn( + wallet, + trade, + baseTokenAddress, + gasPrice, + ) + // submit response + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: parseFloat(paramData.amount), + expectedOut: expectedAmount.toSignificant(8), + price: parseFloat(price), + gasPrice: gasPrice, + gasLimit, gasLimit, + gasCost: gasCost, + txHash: tx.hash, + }) + } else { + res.status(200).json({ + error: swapLessThanMaxPriceError, + message: `Swap price ${price} lower than limitPrice ${limitPrice}` + }) + logger.info(`uniswap.route - Swap price ${price} lower than limitPrice ${limitPrice}`) + } } } catch (err) { logger.error(req.originalUrl, { message: err }) @@ -278,79 +264,100 @@ router.post('/sell', async (req, res) => { } }) -router.post('/buy', async (req, res) => { +router.post('/price', async (req, res) => { /* - POST: /buy + POST: /price x-www-form-urlencoded: { - "quote":"0x....." - "base":"0x....." - "amount":0.1 - "maxPrice":1 - "gasPrice":10 - "privateKey":{{privateKey}} + "quote":"BAT" + "base":"DAI" + "amount":1 } */ const initTime = Date.now() - // params: privateKey (required), base (required), quote (required), amount (required), maxPrice (required), gasPrice (required) + // params: base (required), quote (required), amount (required) const paramData = getParamData(req.body) - const privateKey = paramData.privateKey - const wallet = new ethers.Wallet(privateKey, uniswap.provider) - const baseTokenAddress = paramData.base - const quoteTokenAddress = paramData.quote const amount = paramData.amount - let maxPrice - if (paramData.maxPrice) { - maxPrice = parseFloat(paramData.maxPrice) - } + const baseTokenContractInfo = eth.getERC20TokenAddresses(paramData.base) + const quoteTokenContractInfo = eth.getERC20TokenAddresses(paramData.quote) + const baseTokenAddress = baseTokenContractInfo.address + const quoteTokenAddress = quoteTokenContractInfo.address + const side = paramData.side.toUpperCase() let gasPrice if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) + } else { + gasPrice = fees.ethGasPrice } + const gasLimit = estimateGasLimit() + const gasCost = await fees.getGasCost(gasPrice, gasLimit) + try { // fetch the optimal pool mix from uniswap - const { trade, expectedIn } = await uniswap.priceSwapOut( - quoteTokenAddress, // tokenIn is quote asset - baseTokenAddress, // tokenOut is base asset - amount, - ) - - const price = trade.executionPrice.invert().toSignificant(8) - logger.info(`uniswap.route - Price: ${price.toString()}`) - if (!maxPrice || price <= maxPrice) { - // pass swaps to exchange-proxy to complete trade - const tx = await uniswap.swapExactOut( - wallet, - trade, - baseTokenAddress, - gasPrice, + const { trade, expectedAmount } = side === 'BUY' + ? await uniswap.priceSwapOut( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + amount + ) + : await uniswap.priceSwapIn( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + amount ) - // submit response - res.status(200).json({ + if (trade !== null && expectedAmount !== null) { + const price = side === 'BUY' + ? trade.executionPrice.invert().toSignificant(8) + : trade.executionPrice.toSignificant(8) + + const tradeAmount = parseFloat(amount) + const expectedTradeAmount = parseFloat(expectedAmount.toSignificant(8)) + const tradePrice = parseFloat(price) + + const result = { network: uniswap.network, timestamp: initTime, latency: latency(initTime, Date.now()), base: baseTokenAddress, quote: quoteTokenAddress, - amount: amount, - expectedIn: expectedIn.toSignificant(8), - price: price, - txHash: tx.hash, - }) - } else { + amount: tradeAmount, + expectedAmount: expectedTradeAmount, + price: tradePrice, + gasPrice: gasPrice, + gasLimit: gasLimit, + gasCost: gasCost, + trade: trade, + } + debug(`Price ${side} ${baseTokenContractInfo.symbol}-${quoteTokenContractInfo.symbol} | amount:${amount} (rate:${tradePrice}) - gasPrice:${gasPrice} gasLimit:${gasLimit} estimated fee:${gasCost} ETH`) + res.status(200).json(result) + } else { // no pool available res.status(200).json({ - error: swapMoreThanMaxPriceError, - message: `Swap price ${price} exceeds maxPrice ${maxPrice}` + info: statusMessages.no_pool_available, + message: '' }) - logger.info(`uniswap.route - Swap price ${price} exceeds maxPrice ${maxPrice}`) } } catch (err) { logger.error(req.originalUrl, { message: err }) let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ + let errCode = 500 + if (Object.keys(err).includes('isInsufficientReservesError')) { + errCode = 200 + reason = statusMessages.insufficient_reserves + ' in ' + side + ' at Uniswap' + } else if (Object.getOwnPropertyNames(err).includes('message')) { + reason = getErrorMessage(err.message) + if (reason === statusMessages.no_pool_available) { + errCode = 200 + res.status(errCode).json({ + info: reason, + message: err + }) + } + } else { + err.reason ? reason = err.reason : reason = statusMessages.operation_error + } + res.status(errCode).json({ error: reason, message: err }) diff --git a/src/services/access.js b/src/services/access.js index bb2a75d..708f3aa 100644 --- a/src/services/access.js +++ b/src/services/access.js @@ -4,10 +4,16 @@ import { logger } from './logger'; import { statusMessages } from './utils'; +const debug = require('debug')('router') export const validateAccess = (req, res, next) => { const cert = req.connection.getPeerCertificate() if (req.client.authorized) { + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress + const method = req.method + const url = req.url + const requestInfo = 'Request from IP: ' + ip + ' ' + method + ' ' + url + console.log(requestInfo) next() } else if (cert.subject) { logger.error(statusMessages.ssl_cert_invalid) diff --git a/src/services/balancer.js b/src/services/balancer.js index d151230..dc4bb43 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -1,4 +1,5 @@ import { logger } from '../services/logger'; +const debug = require('debug')('router') require('dotenv').config() // DO NOT REMOVE. needed to configure REACT_APP_SUBGRAPH_URL used by @balancer-labs/sor const sor = require('@balancer-labs/sor') const BigNumber = require('bignumber.js') @@ -8,12 +9,9 @@ const proxyArtifact = require('../static/ExchangeProxy.json') // constants const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; const MULTI_KOVAN = ' 0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A'; -const EXCHANGE_PROXY = '0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21'; -const EXCHANGE_PROXY_KOVAN = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec'; const MAX_UINT = ethers.constants.MaxUint256; -const MAX_SWAPS = 4; -const GAS_BASE = 200688; -const GAS_PER_SWAP = 100000; +const GAS_BASE = process.env.BALANCER_GAS_BASE || 200688; +const GAS_PER_SWAP = process.env.BALANCER_GAS_PER_SWAP || 100000; export default class Balancer { constructor (network = 'kovan') { @@ -23,15 +21,15 @@ export default class Balancer { this.subgraphUrl = process.env.REACT_APP_SUBGRAPH_URL this.gasBase = GAS_BASE this.gasPerSwap = GAS_PER_SWAP - this.maxSwaps = MAX_SWAPS + this.maxSwaps = process.env.BALANCER_MAX_SWAPS || 4 + this.exchangeProxy = process.env.EXCHANGE_PROXY; + this.cachedPools = [] switch (network) { case 'mainnet': - this.exchangeProxy = EXCHANGE_PROXY; this.multiCall = MULTI; break; case 'kovan': - this.exchangeProxy = EXCHANGE_PROXY_KOVAN; this.multiCall = MULTI_KOVAN; break; default: @@ -41,23 +39,36 @@ export default class Balancer { } } - async priceSwapIn (tokenIn, tokenOut, tokenInAmount, maxSwaps = MAX_SWAPS) { + async fetchPool (tokenIn, tokenOut) { + const pools = await sor.getPoolsWithTokens(tokenIn, tokenOut) + this.cachedPools[tokenIn + tokenOut] = pools + + if (pools.pools.length === 0) { + debug('>>> No pools contain the tokens provided.', { message: this.network }); + return {}; + } + debug(`>>> ${pools.pools.length} Pools Retrieved.`, { message: this.network }) + } + + async getCachedPools (tokenIn, tokenOut) { + const cachePools = this.cachedPools[tokenIn + tokenOut].pools + debug(`>>> get cached Pools. ${tokenIn + tokenOut}`, { message: `total pools: ${cachePools.length}` }) + return cachePools + } + + async priceSwapIn (tokenIn, tokenOut, tokenInAmount, maxSwaps = this.maxSwaps) { // Fetch all the pools that contain the tokens provided try { - const pools = await sor.getPoolsWithTokens(tokenIn, tokenOut) - if (pools.pools.length === 0) { - logger.debug('No pools contain the tokens provided.', { message: this.network }) - return {}; - } - logger.debug('Pools Retrieved.', { message: this.network }) - // Get current on-chain data about the fetched pools + await this.fetchPool(tokenIn, tokenOut) + let poolData + const cachedPools = await this.getCachedPools(tokenIn, tokenOut) if (this.network === 'mainnet') { - poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, this.multiCall, this.provider) + poolData = await sor.parsePoolDataOnChain(cachedPools, tokenIn, tokenOut, this.multiCall, this.provider) } else { // Kovan multicall throws an ENS error - poolData = await sor.parsePoolData(pools.pools, tokenIn, tokenOut) + poolData = await sor.parsePoolData(cachedPools, tokenIn, tokenOut) } // Parse the pools and pass them to smart order outer to get the swaps needed @@ -70,8 +81,8 @@ export default class Balancer { ) const swapsFormatted = sor.formatSwapsExactAmountIn(sorSwaps, MAX_UINT, 0) - const expectedOut = sor.calcTotalOutput(swapsFormatted, poolData) - logger.debug(`Expected Out: ${expectedOut.toString()} (${tokenOut})`); + const expectedAmount = sor.calcTotalOutput(swapsFormatted, poolData) + debug(`Expected Out: ${expectedAmount.toString()} (${tokenOut})`); // Create correct swap format for new proxy let swaps = []; @@ -86,7 +97,7 @@ export default class Balancer { }; swaps.push(swap); } - return { swaps, expectedOut } + return { swaps, expectedAmount } } catch (err) { logger.error(err) let reason @@ -95,22 +106,19 @@ export default class Balancer { } } - async priceSwapOut (tokenIn, tokenOut, tokenOutAmount, maxSwaps = MAX_SWAPS) { + async priceSwapOut (tokenIn, tokenOut, tokenOutAmount, maxSwaps = this.maxSwaps) { // Fetch all the pools that contain the tokens provided try { - const pools = await sor.getPoolsWithTokens(tokenIn, tokenOut) - if (pools.pools.length === 0) { - logger.debug('No pools contain the tokens provided.', { message: this.network }); - return {}; - } - logger.debug('Pools Retrieved.', { message: this.network }) // Get current on-chain data about the fetched pools + await this.fetchPool(tokenIn, tokenOut) + let poolData + const cachedPools = await this.getCachedPools(tokenIn, tokenOut) if (this.network === 'mainnet') { - poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, this.multiCall, this.provider) + poolData = await sor.parsePoolDataOnChain(cachedPools, tokenIn, tokenOut, this.multiCall, this.provider) } else { // Kovan multicall throws an ENS error - poolData = await sor.parsePoolData(pools.pools, tokenIn, tokenOut) + poolData = await sor.parsePoolData(cachedPools, tokenIn, tokenOut) } // Parse the pools and pass them to smart order outer to get the swaps needed @@ -122,8 +130,8 @@ export default class Balancer { 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountOut(sorSwaps, MAX_UINT, MAX_UINT) - const expectedIn = sor.calcTotalInput(swapsFormatted, poolData) - logger.debug(`Expected In: ${expectedIn.toString()} (${tokenIn})`); + const expectedAmount = sor.calcTotalInput(swapsFormatted, poolData) + debug(`Expected In: ${expectedAmount.toString()} (${tokenIn})`); // Create correct swap format for new proxy let swaps = []; @@ -138,7 +146,7 @@ export default class Balancer { }; swaps.push(swap); } - return { swaps, expectedIn } + return { swaps, expectedAmount } } catch (err) { logger.error(err) let reason @@ -148,7 +156,7 @@ export default class Balancer { } async swapExactIn (wallet, swaps, tokenIn, tokenOut, amountIn, minAmountOut, gasPrice) { - logger.debug(`Number of swaps: ${swaps.length}`) + debug(`Number of swaps: ${swaps.length}`) try { const contract = new ethers.Contract(this.exchangeProxy, proxyArtifact.abi, wallet) const tx = await contract.batchSwapExactIn( @@ -162,7 +170,7 @@ export default class Balancer { gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) - logger.debug(`Tx Hash: ${tx.hash}`); + debug(`Tx Hash: ${tx.hash}`); return tx } catch (err) { logger.error(err) @@ -173,7 +181,7 @@ export default class Balancer { } async swapExactOut (wallet, swaps, tokenIn, tokenOut, expectedIn, gasPrice) { - logger.debug(`Number of swaps: ${swaps.length}`) + debug(`Number of swaps: ${swaps.length}`) try { const contract = new ethers.Contract(this.exchangeProxy, proxyArtifact.abi, wallet) const tx = await contract.batchSwapExactOut( @@ -186,7 +194,7 @@ export default class Balancer { gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) - logger.debug(`Tx Hash: ${tx.hash}`) + debug(`Tx Hash: ${tx.hash}`) return tx } catch (err) { logger.error(err) diff --git a/src/services/eth.js b/src/services/eth.js index 8aa280b..61a51cd 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -1,28 +1,28 @@ import { logger } from './logger'; +import axios from 'axios' +const debug = require('debug')('router') require('dotenv').config() const fs = require('fs'); const ethers = require('ethers') const abi = require('../static/abi') // constants +const APPROVAL_GAS_LIMIT = process.env.ETH_APPROVAL_GAS_LIMIT || 50000; export default class Ethereum { constructor (network = 'mainnet') { // network defaults to kovan const providerUrl = process.env.ETHEREUM_RPC_URL this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.erc20TokenListURL = process.env.ETHEREUM_TOKEN_LIST_URL this.network = network - - if (network === 'kovan') { - // for kovan testing only - this.erc20KovanTokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens_kovan.json')) - } else if (network === 'mainnet') { - // contract list no longer maintained here. changed to accept contract address via request data - // this.erc20Tokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens_hummingbot.json')) - } else { - throw Error(`Invalid network ${network}`) + this.spenders = { + balancer: process.env.EXCHANGE_PROXY, + uniswap: process.env.UNISWAP_ROUTER } + // update token list + this.getERC20TokenList() // erc20TokenList } // get ETH balance @@ -72,7 +72,7 @@ export default class Ethereum { async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit) { try { // fixate gas limit to prevent overwriting - const approvalGasLimit = 50000 + const approvalGasLimit = APPROVAL_GAS_LIMIT // instantiate a contract and pass in wallet, which act on behalf of that signer const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, wallet) return await contract.approve( @@ -123,4 +123,44 @@ export default class Ethereum { return reason } } + + // get ERC20 Token List + async getERC20TokenList () { + let tokenListSource + try { + if (this.network === 'kovan') { + tokenListSource = 'src/static/erc20_tokens_kovan.json' + this.erc20TokenList = JSON.parse(fs.readFileSync(tokenListSource)) + } else if (this.network === 'mainnet') { + tokenListSource = this.erc20TokenListURL + if (tokenListSource === undefined || tokenListSource === null) { + const errMessage = 'Token List source not found' + logger.error('ERC20 Token List Error', { message: errMessage}) + console.log('eth - Error: ', errMessage) + } + if (this.erc20TokenList === undefined || this.erc20TokenList === null || this.erc20TokenList === {}) { + const response = await axios.get(tokenListSource) + if (response.status === 200 && response.data) { + this.erc20TokenList = response.data + } + } + } else { + throw Error(`Invalid network ${this.network}`) + } + console.log('get ERC20 Token List', this.network, 'source', tokenListSource) + } catch (err) { + console.log(err); + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error ERC 20 Token List' + return reason + } + } + + getERC20TokenAddresses (tokenSymbol) { + const tokenContractAddress = this.erc20TokenList.tokens.filter(obj => { + return obj.symbol === tokenSymbol.toUpperCase() + }) + return tokenContractAddress[0] + } } diff --git a/src/services/fees.js b/src/services/fees.js new file mode 100644 index 0000000..0535897 --- /dev/null +++ b/src/services/fees.js @@ -0,0 +1,53 @@ +import { logger } from './logger'; +import axios from 'axios' +import BigNumber from 'bignumber.js' + +require('dotenv').config() + +const debug = require('debug')('router') +// constants +const ethGasStationHost = 'https://ethgasstation.info' +const ethGasStationEnabled = process.env.ENABLE_ETH_GAS_STATION || false +const ethGasStationApiKey = process.env.ETH_GAS_STATION_API_KEY +const ethManualGasPrice = parseInt(process.env.MANUAL_GAS_PRICE) +const ethGasStationURL = ethGasStationHost + '/api/ethgasAPI.json?api-key=' + ethGasStationApiKey +const defaultRefreshInterval = 120 +const denom = BigNumber('1e+9') + +export default class Fees { + constructor () { + this.ethGasStationGasLevel = process.env.ETH_GAS_STATION_GAS_LEVEL + this.ethGasStationRefreshTime = (process.env.ETH_GAS_STATION_REFRESH_TIME || defaultRefreshInterval) * 1000 + this.getETHGasStationFee(this.ethGasStationGasLevel, 0) + } + + // get ETH Gas Station + async getETHGasStationFee (gasLevel = this.ethGasStationGasLevel, interval = defaultRefreshInterval) { + try { + if (ethGasStationEnabled === true || ethGasStationEnabled.toLowerCase() === 'true') { + const response = await axios.get(ethGasStationURL) + // divite by 10 to convert it to Gwei) + this.ethGasPrice = response.data[gasLevel] / 10 + console.log(`get ETHGasStation gas price (${gasLevel}): ${this.ethGasPrice} / interval: ${this.ethGasStationRefreshTime / 1000} sec`) + } else { + this.ethGasPrice = ethManualGasPrice + console.log(`get manual fixed gas price: ${this.ethGasPrice} / interval: ${this.ethGasStationRefreshTime / 1000} sec`) + } + } catch (err) { + console.log(err); + logger.error(err) + let reason + err.reason ? reason = err.reason : reason = 'error ETH gas fee lookup' + return reason + } + if (interval > 0) { // set to '0' for one-time retrieval + setTimeout(this.getETHGasStationFee.bind(this), this.ethGasStationRefreshTime); // update every x seconds + } + } + + // get gas cost + async getGasCost (gasPrice, gasLimit, inGwei = false) { + const cost = gasPrice * gasLimit + return inGwei ? cost : cost / denom + } +} diff --git a/src/services/logger.js b/src/services/logger.js index d1c9bdf..90bf160 100644 --- a/src/services/logger.js +++ b/src/services/logger.js @@ -1,6 +1,5 @@ import { getLocalDate } from './utils' require('dotenv').config() -// const fecha = require('fecha') const appRoot = require('app-root-path') const winston = require('winston') require('winston-daily-rotate-file'); @@ -30,36 +29,15 @@ const config = { filename: `${getLogPath()}/logs_gateway_app.log.%DATE%`, datePattern: 'YYYY-MM-DD', handleExceptions: true, - }, - error: { - level: 'error', - filename: `${getLogPath()}/logs_gateway_error.log.%DATE%`, - datePattern: 'YYYY-MM-DD', - handleExceptions: false, - }, - rejection: { - level: 'error', - filename: `${getLogPath()}/logs_gateway_rejection.log.%DATE%`, - datePattern: 'YYYY-MM-DD', - handleExceptions: true, - }, - debug: { - level: 'debug', - filename: `${getLogPath()}/logs_gateway_debug.log.%DATE%`, - datePattern: 'YYYY-MM-DD', - handleExceptions: false, - }, + handleRejections: true + } } const allLogsFileTransport = new winston.transports.DailyRotateFile(config.file) -const errorLogsFileTransport = new winston.transports.DailyRotateFile(config.error) -const debugTransport = new winston.transports.DailyRotateFile(config.debug) -const rejectionTransport = new winston.transports.DailyRotateFile(config.rejection) const options = { format: logFormat, - transports: [allLogsFileTransport, errorLogsFileTransport, debugTransport], - rejectionHandlers: [rejectionTransport], + transports: [allLogsFileTransport], exitOnError: false, } diff --git a/src/services/perpetual_finance.js b/src/services/perpetual_finance.js index 7707cec..f2d482e 100644 --- a/src/services/perpetual_finance.js +++ b/src/services/perpetual_finance.js @@ -13,7 +13,7 @@ const TetherTokenArtifact = require("@perp/contract/build/contracts/TetherToken. const GAS_LIMIT = 2123456; const DEFAULT_DECIMALS = 18; const CONTRACT_ADDRESSES = 'https://metadata.perp.exchange/'; -const XDAI_PROVIDER = 'https://dai.poa.network'; +const XDAI_PROVIDER = process.env.XDAI_PROVIDER || 'https://dai.poa.network'; const PNL_OPTION_SPOT_PRICE = 0; const UPDATE_PERIOD = 60000; // stop updating prices after 30 secs from last request diff --git a/src/services/terra.js b/src/services/terra.js index 2d57d5b..38dac2e 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -3,6 +3,7 @@ import { LCDClient, Coin, MsgSwap, StdTx, StdFee, Dec, MnemonicKey, isTxError, C import BigNumber from 'bignumber.js' import { getHummingbotMemo } from './utils'; +const debug = require('debug')('router') require('dotenv').config() // constants @@ -134,7 +135,7 @@ export default class Terra { feeList[key] = rates._coins[key].amount * lunaFee }) }) - logger.debug('lunaFee', lunaFee, feeList) + debug('lunaFee', lunaFee, feeList) return feeList } catch (err) { @@ -199,7 +200,7 @@ export default class Terra { swaps.price = exchangeRate swaps.cost = cost swaps.txFee = txFee - logger.debug('swaps', swaps) + debug('swaps', swaps) return swaps } catch (err) { logger.error(err) @@ -251,7 +252,6 @@ export default class Terra { const offerAmount = parseInt((swaps.offer.amount) * DENOM_UNIT) const offerCoin = new Coin(offerDenom, offerAmount) - // debug('offerCoin', offerCoin, offerAmount, 'gasPrice', gasPrice) // Create and Sign Transaction const msgSwap = new MsgSwap(address, offerCoin, swapDenom); @@ -281,7 +281,6 @@ export default class Terra { tokenSwap.txSuccess = !swapSuccess throw new Error(`encountered an error while running the transaction: ${txResult.code} ${txResult.codespace}`); } - const txHash = txResult.txhash const events = JSON.parse(txResult.raw_log)[0].events const swap = events.find(obj => { diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 747040e..8fe7bbb 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -1,15 +1,17 @@ import { logger } from './logger'; +const debug = require('debug')('router') +const math = require('mathjs') const uni = require('@uniswap/sdk') const ethers = require('ethers') const proxyArtifact = require('../static/uniswap_v2_router_abi.json') const routeTokens = require('../static/uniswap_route_tokens.json') // constants -const ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; -const GAS_LIMIT = 150688; -const TTL = 300; -const UPDATE_PERIOD = 300000; // stop updating pair after 5 minutes from last request +const ROUTER = process.env.UNISWAP_ROUTER +const GAS_LIMIT = process.env.UNISWAP_GAS_LIMIT || 150688; +const TTL = process.env.UNISWAP_TTL || 300; +const UPDATE_PERIOD = process.env.UNISWAP_UPDATE_PERIOD || 300000; // stop updating pair after 5 minutes from last request export default class Uniswap { constructor (network = 'mainnet') { @@ -17,8 +19,8 @@ export default class Uniswap { this.network = process.env.ETHEREUM_CHAIN this.provider = new ethers.providers.JsonRpcProvider(this.providerUrl) this.router = ROUTER; - this.allowedSlippage = new uni.Percent(process.env.UNISWAP_ALLOWED_SLIPPAGE, '100') - console.log(this.allowedSlippage) + this.slippage = math.fraction(process.env.UNISWAP_ALLOWED_SLIPPAGE) + this.allowedSlippage = new uni.Percent(this.slippage.n, (this.slippage.d * 100)) this.pairsCacheTime = process.env.UNISWAP_PAIRS_CACHE_TIME this.gasLimit = GAS_LIMIT this.expireTokenPairUpdate = UPDATE_PERIOD @@ -130,17 +132,17 @@ export default class Uniswap { const route = await this.fetch_route(tIn, tOut); const trade = uni.Trade.exactIn(route, tokenAmountIn); if ( trade !== undefined ){ - const expectedOut = trade.minimumAmountOut(this.allowedSlippage); + const expectedAmount = trade.minimumAmountOut(this.allowedSlippage); this.cachedRoutes[tIn.symbol + tOut.Symbol] = trade; - return { trade, expectedOut } + return { trade, expectedAmount } } return "Can't find route to swap, kindly update " } const trade = uni.Trade.bestTradeExactIn(this.pairs, tokenAmountIn, this.tokenList[tokenOut], { maxHops: 5 })[0]; if (trade === undefined){trade = this.cachedRoutes[tIn.symbol + tOut.Symbol];} else{this.cachedRoutes[tIn.symbol + tOut.Symbol] = trade;} - const expectedOut = trade.minimumAmountOut(this.allowedSlippage); - return { trade, expectedOut } + const expectedAmount = trade.minimumAmountOut(this.allowedSlippage); + return { trade, expectedAmount } } async priceSwapOut (tokenIn, tokenOut, tokenOutAmount) { @@ -152,17 +154,17 @@ export default class Uniswap { const route = await this.fetch_route(tIn, tOut); const trade = uni.Trade.exactOut(route, tokenAmountOut); if ( trade !== undefined ){ - const expectedIn = trade.maximumAmountIn(this.allowedSlippage); + const expectedAmount = trade.maximumAmountIn(this.allowedSlippage); this.cachedRoutes[tIn.symbol + tOut.Symbol] = trade; - return { trade, expectedIn } + return { trade, expectedAmount } } return } const trade = uni.Trade.bestTradeExactOut(this.pairs, this.tokenList[tokenIn], tokenAmountOut, { maxHops: 5 })[0]; if (trade === undefined){trade = this.cachedRoutes[tIn.symbol + tOut.Symbol];} else{this.cachedRoutes[tIn.symbol + tOut.Symbol] = trade;} - const expectedIn = trade.maximumAmountIn(this.allowedSlippage); - return { trade, expectedIn } + const expectedAmount = trade.maximumAmountIn(this.allowedSlippage); + return { trade, expectedAmount } } async swapExactIn (wallet, trade, tokenAddress, gasPrice) { @@ -185,7 +187,7 @@ export default class Uniswap { } ) - logger.debug(`Tx Hash: ${tx.hash}`); + debug(`Tx Hash: ${tx.hash}`); return tx } @@ -209,7 +211,7 @@ export default class Uniswap { } ) - logger.debug(`Tx Hash: ${tx.hash}`); + debug(`Tx Hash: ${tx.hash}`); return tx } } diff --git a/src/services/utils.js b/src/services/utils.js index 8c69244..23182cc 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -86,7 +86,13 @@ export const loadConfig = () => { ethereum_rpc_url: process.env.ETHEREUM_RPC_URL, ethereum_chain: process.env.ETHEREUM_CHAIN, exchange_proxy: process.env.EXCHANGE_PROXY, + ethereum_token_list_url: process.env.ETHEREUM_TOKEN_LIST_URL, + enable_eth_gas_station: process.env.ENABLE_ETH_GAS_STATION != null ? (process.env.ENABLE_ETH_GAS_STATION.toLowerCase() == 'true') : false, + eth_gas_station_gas_level: process.env.ETH_GAS_STATION_GAS_LEVEL, + eth_gas_station_refresh_time: process.env.ETH_GAS_STATION_REFRESH_TIME != null ? parseFloat(process.env.ETH_GAS_STATION_REFRESH_TIME) : null, + manual_gas_price: process.env.MANUAL_GAS_PRICE != null ? parseFloat(process.env.MANUAL_GAS_PRICE) : null, react_app_subgraph_url: process.env.REACT_APP_SUBGRAPH_URL, + balancer_max_swaps: process.env.BALANCER_MAX_SWAPS != null ? parseInt(process.env.BALANCER_MAX_SWAPS) : null, uniswap_router: process.env.UNISWAP_ROUTER, terra_lcd_url: process.env.TERRA_LCD_URL, terra_chain: process.env.TERRA_CHAIN diff --git a/src/static/erc20_tokens_kovan.json b/src/static/erc20_tokens_kovan.json index 06590e9..d4bb4e5 100644 --- a/src/static/erc20_tokens_kovan.json +++ b/src/static/erc20_tokens_kovan.json @@ -1,12 +1,55 @@ { - "BAT": "0x1f1f156E0317167c11Aa412E3d1435ea29Dc3cCE", - "WETH": "0xd0A1E359811322d97991E03f863a0C30C2cF029C", - "DAI": "0x1528F3FCc26d13F7079325Fb78D9442607781c8C", - "MKR": "0xef13C0c8abcaf5767160018d268f9697aE4f5375", - "USDC": "0x2F375e94FC336Cdec2Dc0cCB5277FE59CBf1cAe5", - "REP": "0x8c9e6c40d3402480ACE624730524fACC5482798c", - "WBTC": "0xe0C9275E44Ea80eF17579d33c55136b7DA269aEb", - "SNX": "0x86436BcE20258a6DcfE48C9512d4d49A30C4d8c4", - "ANT": "0x37f03a12241E9FD3658ad6777d289c3fb8512Bc9", - "ZRX": "0xccb0F4Cf5D3F97f4a55bb5f5cA321C3ED033f244" + "name": "kovan", + "tokens": [ + { + "symbol": "BAT", + "address": "0x1f1f156E0317167c11Aa412E3d1435ea29Dc3cCE", + "decimals": 18 + }, + { + "symbol": "WETH", + "address": "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + "decimals": 18 + }, + { + "symbol": "DAI", + "address": "0x1528F3FCc26d13F7079325Fb78D9442607781c8C", + "decimals": 18 + }, + { + "symbol": "MKR", + "address": "0xef13C0c8abcaf5767160018d268f9697aE4f5375", + "decimals": 18 + }, + { + "symbol": "USDC", + "address": "0x2F375e94FC336Cdec2Dc0cCB5277FE59CBf1cAe5", + "decimals": 6 + }, + { + "symbol": "REP", + "address": "0x8c9e6c40d3402480ACE624730524fACC5482798c", + "decimals": 18 + }, + { + "symbol": "WBTC", + "address": "0xe0C9275E44Ea80eF17579d33c55136b7DA269aEb", + "decimals": 18 + }, + { + "symbol": "SNX", + "address": "0x86436BcE20258a6DcfE48C9512d4d49A30C4d8c4", + "decimals": 18 + }, + { + "symbol": "ANT", + "address": "0x37f03a12241E9FD3658ad6777d289c3fb8512Bc9", + "decimals": 18 + }, + { + "symbol": "ZRX", + "address": "0xccb0F4Cf5D3F97f4a55bb5f5cA321C3ED033f244", + "decimals": 18 + } + ] } diff --git a/test/postman/v2/Gateway.postman_collection.json b/test/postman/v2/Gateway.postman_collection.json new file mode 100644 index 0000000..949efe0 --- /dev/null +++ b/test/postman/v2/Gateway.postman_collection.json @@ -0,0 +1,1001 @@ +{ + "info": { + "_postman_id": "e39af94e-6095-479e-8ba0-66930b12e364", + "name": "Gateway", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "v2", + "item": [ + { + "name": "ethereum", + "item": [ + { + "name": "eth", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth" + ] + } + }, + "response": [] + }, + { + "name": "eth/balances", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "tokenList", + "value": "[\"BAT\",\"USDC\",\"DAI\",\"WETH\",\"ZRX\"]", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balances", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balances" + ] + } + }, + "response": [] + }, + { + "name": "eth/allowances", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "tokenList", + "value": "[\"BAT\",\"USDC\",\"DAI\",\"WETH\",\"ZRX\"]", + "type": "text" + }, + { + "key": "connector", + "value": "balancer", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/allowances", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "allowances" + ] + } + }, + "response": [] + }, + { + "name": "eth/approve", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "token", + "value": "ZRX", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "gasPrice", + "value": "23", + "type": "text" + }, + { + "key": "connector", + "value": "balancer", + "type": "text" + }, + { + "key": "amount", + "value": "999", + "type": "text", + "disabled": true + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/approve", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "approve" + ] + } + }, + "response": [] + }, + { + "name": "eth/get-weth", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "gasPrice", + "value": "31", + "type": "text" + }, + { + "key": "amount", + "value": "0.03", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "tokenAddress", + "value": "{{WETH}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/get-weth", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "get-weth" + ] + } + }, + "response": [] + }, + { + "name": "eth/poll", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "txHash", + "value": "{{txHash}}", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/poll", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "poll" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "balancer", + "item": [ + { + "name": "eth/balancer", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balancer", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balancer" + ] + } + }, + "response": [] + }, + { + "name": "eth/balancer/gas-limit", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balancer/gas-limit", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balancer", + "gas-limit" + ] + } + }, + "response": [] + }, + { + "name": "eth/balancer/start", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "BAT", + "type": "text" + }, + { + "key": "quote", + "value": "dai", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "approvalAmount", + "value": "1", + "type": "text", + "disabled": true + }, + { + "key": "gasPrice", + "value": "50", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balancer/start", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balancer", + "start" + ] + } + }, + "response": [] + }, + { + "name": "eth/balancer/price", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "BAT", + "type": "text" + }, + { + "key": "quote", + "value": "dai", + "type": "text" + }, + { + "key": "amount", + "value": "10", + "type": "text" + }, + { + "key": "side", + "value": "buy", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balancer/price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balancer", + "price" + ] + } + }, + "response": [] + }, + { + "name": "eth/balancer/trade", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "BAT", + "type": "text" + }, + { + "key": "quote", + "value": "USDC", + "type": "text" + }, + { + "key": "amount", + "value": "1", + "type": "text" + }, + { + "key": "limitPrice", + "value": "0.19767217120251", + "type": "text" + }, + { + "key": "gasPrice", + "value": "37", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "side", + "value": "sell", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/balancer/trade", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "balancer", + "trade" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "uniswap", + "item": [ + { + "name": "eth/uniswap", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/uniswap", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "uniswap" + ] + } + }, + "response": [] + }, + { + "name": "eth/uniswap/gas-limit", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/eth/uniswap/gas-limit", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "uniswap", + "gas-limit" + ] + } + }, + "response": [] + }, + { + "name": "eth/uniswap/start", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "WETH", + "type": "text" + }, + { + "key": "quote", + "value": "USDC", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "approvalAmount", + "value": "1", + "type": "text", + "disabled": true + }, + { + "key": "gasPrice", + "value": "50", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/uniswap/start", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "uniswap", + "start" + ] + } + }, + "response": [] + }, + { + "name": "eth/uniswap/price", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "WETH", + "type": "text" + }, + { + "key": "quote", + "value": "DAI", + "type": "text" + }, + { + "key": "amount", + "value": "1", + "type": "text" + }, + { + "key": "side", + "value": "buy", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/uniswap/price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "uniswap", + "price" + ] + } + }, + "response": [] + }, + { + "name": "eth/uniswap/trade", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "BAT", + "type": "text" + }, + { + "key": "quote", + "value": "DAI", + "type": "text" + }, + { + "key": "amount", + "value": "1", + "type": "text" + }, + { + "key": "limitPrice", + "value": "0.19767217120251", + "type": "text" + }, + { + "key": "gasPrice", + "value": "37", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{privateKey}}", + "type": "text" + }, + { + "key": "side", + "value": "sell", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/eth/uniswap/trade", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "eth", + "uniswap", + "trade" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "terra", + "item": [ + { + "name": "terra", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "address", + "value": "{{address}}", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/terra", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra" + ] + } + }, + "response": [] + }, + { + "name": "terra/balances", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "address", + "value": "{{terraWalletAddress}}", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/terra/balances", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "balances" + ] + } + }, + "response": [] + }, + { + "name": "terra/start", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "LUNA", + "type": "text" + }, + { + "key": "quote", + "value": "UST", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/terra/start", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "start" + ] + } + }, + "response": [] + }, + { + "name": "terra/price", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "UST", + "type": "text" + }, + { + "key": "quote", + "value": "KRT", + "type": "text" + }, + { + "key": "side", + "value": "buy", + "type": "text" + }, + { + "key": "amount", + "value": "1", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/terra/price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "price" + ] + } + }, + "response": [] + }, + { + "name": "terra/trade", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "UST", + "type": "text" + }, + { + "key": "quote", + "value": "KRT", + "type": "text" + }, + { + "key": "side", + "value": "buy", + "type": "text" + }, + { + "key": "amount", + "value": "10", + "type": "text" + }, + { + "key": "privateKey", + "value": "{{terraSeeds}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/terra/trade", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "trade" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "/api", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/api", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api" + ] + } + }, + "response": [] + }, + { + "name": "/", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/test/postman/v2/Gateway.postman_environment.json b/test/postman/v2/Gateway.postman_environment.json new file mode 100644 index 0000000..834026f --- /dev/null +++ b/test/postman/v2/Gateway.postman_environment.json @@ -0,0 +1,34 @@ +{ + "id": "4436d04a-e8eb-451b-b7ef-851b171508e8", + "name": "Gateway", + "values": [ + { + "key": "port", + "value": "5000", + "enabled": true + }, + { + "key": "privateKey", + "value": "myprivatekey", + "enabled": true + }, + { + "key": "txHash", + "value": "transactionhash", + "enabled": true + }, + { + "key": "terraWalletAddress", + "value": "terrawalletaddress", + "enabled": true + }, + { + "key": "terraSeeds", + "value": "terraseeds", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2021-01-30T13:04:21.323Z", + "_postman_exported_using": "Postman/8.0.3" +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b12ecfa..a3a5e0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1779,6 +1779,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +complex.js@^2.0.11: + version "2.0.12" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.12.tgz#fa4df97d8928e5f7b6a86b35bdeecc3a3eda8a22" + integrity sha512-oQX99fwL6LrTVg82gDY1dIWXy6qZRnRL35N+YhIX0N7tSwsa0KFy6IEMHTNuCW4mP7FS7MEqZ/2I/afzYwPldw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1928,6 +1933,11 @@ decimal.js@^10.2.0: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== +decimal.js@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" + integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -2129,6 +2139,11 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-latex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" + integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2518,6 +2533,11 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +fraction.js@^4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" + integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -2995,6 +3015,11 @@ isomorphic-fetch@^2.2.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" +javascript-natural-sort@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" + integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k= + js-sha3@0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" @@ -3180,6 +3205,20 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +mathjs@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-9.3.0.tgz#59cc43b536b22616197e56da303604b430daa6ac" + integrity sha512-0kYW+TXgB8lCqUj5wHR2hqAO2twSbPRelSFgRJXiwAx4nM6FrIb43Jd6XhW7sVbwYB+9HCNiyg5Kn8VYeB7ilg== + dependencies: + complex.js "^2.0.11" + decimal.js "^10.2.1" + escape-latex "^1.2.0" + fraction.js "^4.0.13" + javascript-natural-sort "^0.7.1" + seedrandom "^3.0.5" + tiny-emitter "^2.1.0" + typed-function "^2.0.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -3894,6 +3933,11 @@ secp256k1@^4.0.2: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== + semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -4171,6 +4215,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +tiny-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + tiny-invariant@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" @@ -4261,6 +4310,11 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8" + integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"