diff --git a/.dockerignore b/.dockerignore index b2196be..cd75aae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,7 @@ npm-debug.log *.env *.env.* # except the example .env.example -!.env.example \ No newline at end of file +!.env.example + +# Gateway API files +*.pem diff --git a/.env.example b/.env.example index 07f4d81..5fa5b43 100644 --- a/.env.example +++ b/.env.example @@ -1,40 +1,48 @@ -# Configuration file - APPNAME=Hummingbot Gateway API NODE_ENV=dev PORT=5000 -PROTOCOLS=["celo", "terra", "balancer", "eth"] # use only if ip whitelist is required for local or docker instance # note that docker instance does not use 127.0.0.1 address # ipv6 format for locahost ["::ffff:127.0.0.1", "::ffff:1", "fe80::1", "::1"] IP_WHITELIST= +HUMMINGBOT_INSTANCE_ID={client_id} + # Celo # Terra -TERRA_LCD_URL=https://tequila-lcd.terra.dev -TERRA_CHAIN=tequila-0004 - -# Balancer -# - network: mainnet, kovan, etc +# - mainnet: https://lcd.terra.dev +# - mainnet chain: columbus-4 +# - testnet: https://tequila-lcd.terra.dev +# - testnet chain: tequila-0004 +TERRA_LCD_URL={testnet_lcd_url} +TERRA_CHAIN={testnet_chain_id} + +# Ethereum +# - chain: mainnet, kovan, etc # - rpc url: infura or other rpc url -BALANCER_NETWORK={network} -ETHEREUM_RPC_URL=https://{network}.infura.io/v3/{api_key} +ETHEREUM_CHAIN={chain} +ETHEREUM_RPC_URL=https://{chain}.infura.io/v3/{api_key} -# subgraph_network: +# Balancer +# subgraph_chain # Reference: https://docs.balancer.finance/sor/development#subgraph # - mainnet: balancer # - kovan: balancer-kovan # Note: REACT_APP_SUBGRAPH_URL used by @balancer-labs/sor -REACT_APP_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/balancer-labs/{subgraph_network} +REACT_APP_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/balancer-labs/{subgraph_chain} # exchange_proxy: # Reference: https://docs.balancer.finance/smart-contracts/addresses -# - kovan: 0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec # - mainnet: 0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21 +# - kovan: 0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec EXCHANGE_PROXY={exchange_proxy} +# Uniswap +# Reference: https://uniswap.org/docs/v2/smart-contracts/router02/ +UNISWAP_ROUTER=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + # cert CERT_PATH={full_path_to_certs_folder} CERT_PASSPHRASE={passphrase} diff --git a/.gitignore b/.gitignore index ff3f6d6..8a4f589 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ npm-debug.log dist/ # cert -certs/ *.pem *.srl *.key diff --git a/Dockerfile b/Dockerfile index cb27280..718af71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:10.22.0-alpine +# Set labels +LABEL application="gateway-api" + # app directory WORKDIR /usr/src/app diff --git a/README.md b/README.md index 723c745..01aa595 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ We created hummingbot to promote **decentralized market-making**: enabling membe ### Install Hummingbot -- [Installation](https://docs.hummingbot.io/installation/overview/) +- [Quickstart guide](https://docs.hummingbot.io/quickstart/) +- [All installation options](https://docs.hummingbot.io/installation/overview/) ### Get support - Chat with our support team on [Discord](https://discord.hummingbot.io) diff --git a/certs/readme.md b/certs/readme.md index 4fa6b9d..8d9f12b 100644 --- a/certs/readme.md +++ b/certs/readme.md @@ -1 +1 @@ -certs dir \ No newline at end of file +certs dir for local testing only \ No newline at end of file diff --git a/package.json b/package.json index a592d2b..e5b8380 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@uniswap/sdk": "^3.0.3", "@balancer-labs/sor": "^0.3.3", "@terra-money/terra.js": "^0.5.8", "bignumber.js": "^9.0.0", diff --git a/setup.md b/setup.md index a7d189e..3340dce 100644 --- a/setup.md +++ b/setup.md @@ -9,7 +9,7 @@ This can be used as a common API server to handle transactions that requires cus ## Development Requirements - NodeJS - - Tested on Node v10.22.0 + - Tested on Node v10.22.1 - https://docs.npmjs.com/downloading-and-installing-node-js-and-npm ```bash diff --git a/src/app.js b/src/app.js index c65bc44..910e3c9 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,7 @@ import bodyParser from 'body-parser' import express from 'express' import helmet from 'helmet' import { statusMessages } from './services/utils'; +import { validateAccess } from './services/access'; import { IpFilter } from 'express-ipfilter' // Routes @@ -11,6 +12,7 @@ import balancerRoutes from './routes/balancer.route' // import celoRoutes from './routes/celo.route' import ethRoutes from './routes/eth.route' import terraRoutes from './routes/terra.route' +import uniswapRoutes from './routes/uniswap.route' // terminate if environment not found const result = dotenv.config(); @@ -34,22 +36,18 @@ if (ipWhitelist) { app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); +app.use(validateAccess) + // mount all routes to this path -app.use('/api', apiRoutes); -app.use('/eth', ethRoutes); -// app.use('/celo', celoRoutes); -app.use('/terra', terraRoutes); -app.use('/balancer', balancerRoutes); +app.use('/uniswap', validateAccess, uniswapRoutes); +app.use('/api', validateAccess, apiRoutes); +app.use('/eth', validateAccess, ethRoutes); +// app.use('/celo', validateAccess, celoRoutes); +app.use('/terra', validateAccess, terraRoutes); +app.use('/balancer', validateAccess, balancerRoutes); app.get('/', (req, res, next) => { - const cert = req.connection.getPeerCertificate() - if (req.client.authorized) { - next() - } else if (cert.subject) { - res.status(403).send({ error: statusMessages.ssl_cert_invalid }) - } else { - res.status(401).send({ error: statusMessages.ssl_cert_required }) - } + res.send('ok') }) /** diff --git a/src/index.js b/src/index.js index 3ded6f3..a3c9aab 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,8 @@ if (result.error) { const env = process.env.NODE_ENV const port = process.env.PORT const certPassphrase = process.env.CERT_PASSPHRASE -const balancerNetwork = process.env.BALANCER_NETWORK +const ethereumChain = process.env.ETHEREUM_CHAIN +const terraChain = process.env.TERRA_CHAIN let certPath = process.env.CERT_PATH if ((typeof certPath === 'undefined' && certPath == null) || certPath === '') { @@ -79,4 +80,6 @@ server.listen(port) server.on('error', onError) server.on('listening', onListening) -console.log('server: gateway-api | port:', port, '| balancer-network:', balancerNetwork); +console.log('server: gateway-api | port:', port) +console.log(' - ethereum-chain:', ethereumChain) +console.log(' - terra-chain:', terraChain) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 8880861..1de67c9 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -3,30 +3,23 @@ import { ethers } from 'ethers'; import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; + import Balancer from '../services/balancer'; -import Ethereum from '../services/eth'; -require('dotenv').config() +// require('dotenv').config() const debug = require('debug')('router') const router = express.Router() -const balancer = new Balancer(process.env.BALANCER_NETWORK) -const eth = new Ethereum(process.env.BALANCER_NETWORK) +const balancer = new Balancer(process.env.ETHEREUM_CHAIN) const denomMultiplier = 1e18 -const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' -const swapLessThanMaxPriceError = 'Swap price lower than maxPrice' +const swapMoreThanMaxPriceError = 'Price too high' +const swapLessThanMaxPriceError = 'Price too low' -router.use((req, res, next) => { - const cert = req.connection.getPeerCertificate() - if (req.client.authorized) { - next() - } else if (cert.subject) { - res.status(403).send({ error: statusMessages.ssl_cert_invalid }) - } else { - res.status(401).send({ error: statusMessages.ssl_cert_required }) - } -}) +const estimateGasLimit = (maxswaps) => { + const gasLimit = balancer.gasBase + maxswaps * balancer.gasPerSwap + return gasLimit +} router.post('/', async (req, res) => { /* @@ -36,12 +29,31 @@ router.post('/', async (req, res) => { network: balancer.network, provider: balancer.provider.connection.url, exchangeProxy: balancer.exchangeProxy, - subgraphUrl: process.env.REACT_APP_SUBGRAPH_URL, + subgraphUrl: balancer.subgraphUrl, connection: true, timestamp: Date.now(), }) }) +router.post('/gas-limit', async (req, res) => { + /* + POST: /buy-price + x-www-form-urlencoded: { + "maxSwaps":4 + } + */ + const paramData = getParamData(req.body) + const swaps = paramData.maxSwaps + const maxSwaps = typeof swaps === 'undefined' || parseInt(swaps) === 0 ? balancer.maxSwaps : parseInt(swaps) + const gasLimit = estimateGasLimit(maxSwaps) + + res.status(200).json({ + network: balancer.network, + gasLimit: gasLimit, + timestamp: Date.now(), + }) +}) + router.post('/sell-price', async (req, res) => { /* POST: /sell-price @@ -49,6 +61,7 @@ router.post('/sell-price', async (req, res) => { "quote":"0x....." "base":"0x....." "amount":0.1 + "maxSwaps":4 } */ const initTime = Date.now() @@ -57,6 +70,10 @@ router.post('/sell-price', async (req, res) => { const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + let maxSwaps + if (paramData.maxSwaps) { + maxSwaps = parseInt(paramData.maxSwaps) + } try { // fetch the optimal pool mix from balancer-sor @@ -64,9 +81,11 @@ router.post('/sell-price', async (req, res) => { baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset amount, + maxSwaps, ) if (swaps != null && expectedOut != null) { + const gasLimit = estimateGasLimit(swaps.length) res.status(200).json({ network: balancer.network, timestamp: initTime, @@ -76,6 +95,7 @@ router.post('/sell-price', async (req, res) => { amount: parseFloat(paramData.amount), expectedOut: parseInt(expectedOut) / denomMultiplier, price: expectedOut / amount, + gasLimit: gasLimit, swaps: swaps, }) } else { // no pool available @@ -101,6 +121,7 @@ router.post('/buy-price', async (req, res) => { "quote":"0x....." "base":"0x....." "amount":0.1 + "maxSwaps":4 } */ const initTime = Date.now() @@ -109,6 +130,10 @@ router.post('/buy-price', async (req, res) => { const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + let maxSwaps + if (paramData.maxSwaps) { + maxSwaps = parseInt(paramData.maxSwaps) + } try { // fetch the optimal pool mix from balancer-sor @@ -116,8 +141,11 @@ router.post('/buy-price', async (req, res) => { quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset amount, + maxSwaps, ) + if (swaps != null && expectedIn != null) { + const gasLimit = estimateGasLimit(swaps.length) res.status(200).json({ network: balancer.network, timestamp: initTime, @@ -127,6 +155,7 @@ router.post('/buy-price', async (req, res) => { amount: parseFloat(paramData.amount), expectedIn: parseInt(expectedIn) / denomMultiplier, price: expectedIn / amount, + gasLimit: gasLimit, swaps: swaps, }) } else { // no pool available @@ -154,6 +183,7 @@ router.post('/sell', async (req, res) => { "amount":0.1 "minPrice":1 "gasPrice":10 + "maxSwaps":4 "privateKey":{{privateKey}} } */ @@ -174,6 +204,10 @@ router.post('/sell', async (req, res) => { if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) } + let maxSwaps + if (paramData.maxSwaps) { + maxSwaps = parseInt(paramData.maxSwaps) + } const minAmountOut = maxPrice / amount * denomMultiplier debug('minAmountOut', minAmountOut) @@ -184,13 +218,14 @@ router.post('/sell', async (req, res) => { baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset amount, + maxSwaps, ) - const price = expectedOut / amount + const price = expectedOut / amount debug(`Price: ${price.toString()}`) if (!maxPrice || price >= maxPrice) { // pass swaps to exchange-proxy to complete trade - const txObj = await balancer.swapExactIn( + const tx = await balancer.swapExactIn( wallet, swaps, baseTokenAddress, // tokenIn is base asset @@ -210,9 +245,7 @@ router.post('/sell', async (req, res) => { amount: parseFloat(paramData.amount), expectedOut: expectedOut / denomMultiplier, price: price, - gasUsed: parseInt(txObj.gasUsed), - txHash: txObj.transactionHash, - status: txObj.status, + txHash: tx.hash, }) } else { res.status(200).json({ @@ -240,6 +273,7 @@ router.post('/buy', async (req, res) => { "amount":0.1 "maxPrice":1 "gasPrice":10 + "maxSwaps":4 "privateKey":{{privateKey}} } */ @@ -260,6 +294,10 @@ router.post('/buy', async (req, res) => { if (paramData.gasPrice) { gasPrice = parseFloat(paramData.gasPrice) } + let maxSwaps + if (paramData.maxSwaps) { + maxSwaps = parseInt(paramData.maxSwaps) + } try { // fetch the optimal pool mix from balancer-sor @@ -267,13 +305,14 @@ router.post('/buy', async (req, res) => { quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset amount, + maxSwaps, ) const price = expectedIn / amount debug(`Price: ${price.toString()}`) if (!maxPrice || price <= maxPrice) { // pass swaps to exchange-proxy to complete trade - const txObj = await balancer.swapExactOut( + const tx = await balancer.swapExactOut( wallet, swaps, quoteTokenAddress, // tokenIn is quote asset @@ -292,9 +331,7 @@ router.post('/buy', async (req, res) => { amount: parseFloat(paramData.amount), expectedIn: expectedIn / denomMultiplier, price: price, - gasUsed: parseInt(txObj.gasUsed), - txHash: txObj.transactionHash, - status: txObj.status, + txHash: tx.hash, }) } else { res.status(200).json({ diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 4208e13..36cea3f 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -1,15 +1,15 @@ -import BigNumber from 'bignumber.js'; -import { ethers } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; import Ethereum from '../services/eth'; -import Balancer from '../services/balancer'; const router = express.Router() -const eth = new Ethereum(process.env.BALANCER_NETWORK) -const balancer = new Balancer(process.env.BALANCER_NETWORK) -const seperator = ',' +const eth = new Ethereum(process.env.ETHEREUM_CHAIN) +const spenders = { + balancer: process.env.EXCHANGE_PROXY, + uniswap: process.env.UNISWAP_ROUTER +} const debug = require('debug')('router') @@ -38,7 +38,7 @@ router.post('/balances', async (req, res) => { } let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(seperator) + tokenAddressList = JSON.parse(paramData.tokenAddressList) } debug(tokenAddressList) @@ -46,8 +46,8 @@ router.post('/balances', async (req, res) => { balances.ETH = await eth.getETHBalance(wallet, privateKey) try { Promise.all( - tokenAddressList.map(async (key) => - balances[key] = await eth.getERC20Balance(wallet, key) + Object.keys(tokenAddressList).map(async (key, index) => + balances[key] = await eth.getERC20Balance(wallet, key, tokenAddressList[key]) )).then(() => { res.status(200).json({ network: eth.network, @@ -72,11 +72,13 @@ router.post('/allowances', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddressList:{{tokenAddressList}} + connector:{{connector_name}} } */ const initTime = Date.now() const paramData = getParamData(req.body) const privateKey = paramData.privateKey + const spender = spenders[paramData.connector] let wallet try { wallet = new ethers.Wallet(privateKey, eth.provider) @@ -89,18 +91,16 @@ router.post('/allowances', async (req, res) => { }) return } - const spender = balancer.exchangeProxy let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(seperator) + tokenAddressList = JSON.parse(paramData.tokenAddressList) } - debug(tokenAddressList) const approvals = {} try { Promise.all( - tokenAddressList.map(async (key) => - approvals[key] = await eth.getERC20Allowance(wallet, spender, key) + Object.keys(tokenAddressList).map(async (key, index) => + approvals[key] = await eth.getERC20Allowance(wallet, spender, key, tokenAddressList[key]) )).then(() => { res.status(200).json({ network: eth.network, @@ -125,15 +125,17 @@ router.post('/approve', async (req, res) => { /* POST: /approve x-www-form-urlencoded: { - tokenAddress:"0x....." privateKey:{{privateKey}} + tokenAddress:"0x....." + decimals: {{token_decimals}} + connector:{{connector_name}} amount:{{amount}} } */ const initTime = Date.now() - // params: privateKey (required), tokenAddress (required), amount (optional), gasPrice (required) const paramData = getParamData(req.body) const privateKey = paramData.privateKey + const spender = spenders[paramData.connector] let wallet try { wallet = new ethers.Wallet(privateKey, eth.provider) @@ -147,10 +149,11 @@ router.post('/approve', async (req, res) => { return } const tokenAddress = paramData.tokenAddress - const spender = balancer.exchangeProxy - let amount - paramData.amount ? amount = ethers.utils.parseEther(paramData.amount) - : amount = ethers.utils.parseEther('1000000000') // approve for 1 billion units if no amount specified + let amount, decimals + paramData.decimals ? decimals = paramData.decimals + : decimals = 18 + 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) @@ -191,7 +194,6 @@ router.post('/get-weth', async (req, res) => { } */ const initTime = Date.now() - // params: primaryKey (required), amount (required), gasPrice (optional) const paramData = getParamData(req.body) const privateKey = paramData.privateKey let wallet @@ -232,16 +234,32 @@ router.post('/get-weth', async (req, res) => { message: err }) } +}) + +router.post('/get-receipt', async (req, res) => { + const initTime = Date.now() + const paramData = getParamData(req.body) + const txHash = paramData.txHash + const txReceipt = await eth.provider.getTransactionReceipt(txHash) + debug('Tx Receipt:') + const receipt = {} + const confirmed = txReceipt && txReceipt.blockNumber ? true : false + if (confirmed) { + receipt.gasUsed = BigNumber.from(txReceipt.gasUsed).toNumber() + receipt.blockNumber = txReceipt.blockNumber + receipt.confirmations = txReceipt.confirmations + receipt.status = txReceipt.status + } - // When Balancer gives us the faucet ABI, we can use this faucet to get all Kovan tokens - // const contract = new ethers.Contract(abi.KovanFaucetAddress, abi.KovanFaucetAbi, provider) - // contract.drip(wallet.address, tokenAddress).then((response) => { - // res.status(200).json({ - // network: network, - // timestamp: initTime, - // result: response - // }) - // }) + res.status(200).json({ + network: eth.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + txHash: txHash, + confirmed: confirmed, + receipt: receipt, + }) + return txReceipt }) module.exports = router; diff --git a/src/routes/index.route.js b/src/routes/index.route.js index d69541d..1ea698f 100644 --- a/src/routes/index.route.js +++ b/src/routes/index.route.js @@ -1,23 +1,14 @@ -import { statusMessages } from '../services/utils'; +import { loadConfig } from '../services/utils'; const express = require('express'); const router = express.Router(); -router.use((req, res, next) => { - const cert = req.connection.getPeerCertificate() - if (req.client.authorized) { - next() - } else if (cert.subject) { - res.status(403).send({ error: statusMessages.ssl_cert_invalid }) - } else { - res.status(401).send({ error: statusMessages.ssl_cert_required }) - } -}) - router.get('/', (req, res) => { res.status(200).json({ app: process.env.APPNAME, + image: process.env.IMAGE, + config: loadConfig(), status: 'ok', }); }) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index b29edaa..74594e9 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -1,281 +1,198 @@ 'use strict' import express from 'express' -import BigNumber from 'bignumber.js' -import { LCDClient, MnemonicKey, Coin, MsgSwap } from '@terra-money/terra.js' -import { getParamData, getSymbols, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; -const router = express.Router(); -const debug = require('debug')('router') - -const TerraTokens = { - LUNA: { denom: 'uluna' }, - UST: { denom: 'uusd' }, - KRT: { denom: 'ukrw' }, - SDT: { denom: 'usdr' }, - MNT: { denom: 'umnt' }, -} - -const getTerraSymbol = (denom) => { - let symbol - Object.keys(TerraTokens).forEach((item) => { - if (TerraTokens[item].denom === denom) { - symbol = item - } - }) - return symbol -} - -const denomUnitMultiplier = BigNumber('1e+6') - -const getTxAttributes = (attributes) => { - let attrib = {} - console.log(attributes) - attributes.forEach((item) => { - console.log(item) - attrib[item.key] = item.value - }) - return attrib -} - -// load environment config -const network = 'terra' -const lcdUrl = process.env.TERRA_LCD_URL; -const chain = process.env.TERRA_CHAIN; +import Terra from '../services/terra'; -/** - * Connect to network - */ -const connect = () => { - const terra = new LCDClient({ - URL: lcdUrl, - chainID: chain, - }) - - terra.market.parameters().catch(() => { - throw new Error('Connection error') - }) - - return terra -} +const debug = require('debug')('router') +const router = express.Router(); +const terra = new Terra() -router.use((req, res, next) => { - const cert = req.connection.getPeerCertificate() - if (req.client.authorized) { - next() - } else if (cert.subject) { - res.status(403).send({ error: statusMessages.ssl_cert_invalid }) - } else { - res.status(401).send({ error: statusMessages.ssl_cert_required }) - } -}) +// constants +const network = terra.lcd.config.chainID +const denomUnitMultiplier = terra.denomUnitMultiplier -router.get('/', async (req, res) => { +router.post('/', async (req, res) => { /* - GET / + POST / */ - const terra = connect() - - const marketParams = await terra.market.parameters().catch((err) => { - reportConnectionError(res, err) - }) - res.status(200).json({ network: network, - chain: chain, + lcdUrl: terra.lcd.config.URL, + gasPrices: terra.lcd.config.gasPrices, + gasAdjustment: terra.lcd.config.gasAdjustment, connection: true, - timestamp: Date.now(), - market_params: marketParams + timestamp: Date.now() }) }) -router.get('/price', async (req, res) => { +router.post('/balances', async (req, res) => { /* - GET /price?trading_pair=LUNA-UST&trade_type=sell&amount=1.2345 + POST: + address:{{address}} */ const initTime = Date.now() - const keyFormat = ['trading_pair', 'trade_type', 'amount'] - - const paramData = getParamData(req.query, keyFormat) - const tradingPair = paramData.trading_pair - const requestAmount = paramData.amount - const amount = parseFloat(requestAmount) * denomUnitMultiplier - debug('params', req.params) - debug('paramData', paramData) - const terra = connect() - const exchangeRates = await terra.oracle.exchangeRates().catch((err) => { - reportConnectionError(res, err) - }); + const paramData = getParamData(req.body) + const address = paramData.address + debug(paramData) - const symbols = getSymbols(tradingPair) - const symbolsKeys = Object.keys(symbols) - let price + let balances = {} - if (symbolsKeys.includes('LUNA')) { - let targetSymbol - if (symbolsKeys.includes('UST')) { - targetSymbol = TerraTokens.UST.denom - } else if (symbolsKeys.includes('KRT')) { - targetSymbol = TerraTokens.KRT.denom - } else if (symbolsKeys.includes('SDT')) { - targetSymbol = TerraTokens.SDT.denom + try { + await terra.lcd.bank.balance(address).then(bal => { + bal.toArray().forEach(async (x) => { + const item = x.toData() + const denom = item.denom + const amount = item.amount / denomUnitMultiplier + const symbol = terra.tokens[denom].symbol + balances[symbol] = amount + }) + }) + res.status(200).json({ + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + balances: balances, + }) + } catch (err) { + let message + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + const isAxiosError = err.isAxiosError + if (isAxiosError) { + reason = err.response.status + message = err.response.statusText + } else { + message = err } - price = exchangeRates.get(targetSymbol) * amount - } else { - // get the current swap rate - const baseDenom = TerraTokens[symbols.base].denom - const quoteDenom = TerraTokens[symbols.quote].denom - - const offerCoin = new Coin(baseDenom, amount); - await terra.market.swapRate(offerCoin, quoteDenom).then(swapCoin => { - price = Number(swapCoin.amount) / denomUnitMultiplier - }).catch((err) => { - reportConnectionError(res, err) + res.status(500).json({ + error: reason, + message: message }) } - - const result = Object.assign(paramData, { - price: price, - timestamp: initTime, - latency: latency(initTime, Date.now()) - }) - res.status(200).json(result) }) -router.get('/balance', async (req, res) => { +router.post('/price', async (req, res) => { /* - GET: /balance?address=0x87A4...b120 + POST: + x-www-form-urlencoded: { + "base":"UST" + "quote":"KRT" + "trade_type":"buy" or "sell" + "amount":1 + } */ - const keyFormat = ['address'] - const paramData = getParamData(req.query, keyFormat) - const address = paramData.address - debug(paramData) + const initTime = Date.now() - const terra = connect() + const paramData = getParamData(req.body) + const baseToken = paramData.base + const quoteToken = paramData.quote + const tradeType = paramData.trade_type + const amount = parseFloat(paramData.amount) - let balance = {} - let txSuccess, message + let exchangeRate try { - await terra.bank.balance(address).then(bal => { - bal.toArray().forEach((x) => { - const item = x.toData() - const denom = item.denom - const amount = item.amount / denomUnitMultiplier - const symbol = getTerraSymbol(denom) - balance[symbol] = amount - }) + await terra.getSwapRate(baseToken, quoteToken, amount, tradeType).then((rate) => { + exchangeRate = rate + }).catch((err) => { + reportConnectionError(res, err) }) + + res.status(200).json( + { + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseToken, + quote: quoteToken, + amount: amount, + tradeType: tradeType, + price: exchangeRate.price.amount, + cost: exchangeRate.cost.amount, + txFee: exchangeRate.txFee.amount, + } + ) } catch (err) { - txSuccess = false + let message + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error const isAxiosError = err.isAxiosError if (isAxiosError) { - const status = err.response.status - const statusText = err.response.statusText - message = { error: statusText, status: status, data: err.response.data } + reason = err.response.status + message = err.response.statusText } else { - message = err.status + message = err } + res.status(500).json({ + error: reason, + message: message + }) } - - res.status(200).json({ - success: txSuccess, - address: address, - balance: balance, - timestamp: Date.now(), - message: message - }) }) router.post('/trade', async (req, res) => { /* POST: /trade data: { - "trading_pair":SDT-KRT - "trade_type": "buy" - "amount": "1.01" - "address": "0x...123" + "base":"UST" + "quote":"KRT" + "trade_type":"buy" or "sell" + "amount":1 "secret": "mysupersecret" } */ - const keyFormat = ['trading_pair', 'trade_type', 'amount', 'address', 'secret'] - const paramData = getParamData(req.body, keyFormat) - const tradeType = paramData.tradeType - const secret = paramData.secret - debug(paramData) - - const terra = connect() - const mk = new MnemonicKey({ - mnemonic: secret, - }); - const wallet = terra.wallet(mk); - const address = wallet.key.accAddress - - // get the current swap rate - const symbols = getSymbols(paramData.trading_pair) - debug('symbols', symbols) - const baseDenom = TerraTokens[symbols.base].denom - const quoteDenom = TerraTokens[symbols.quote].denom - - let offerDenom, swapDenom, swapAmount - swapAmount = paramData.amount * denomUnitMultiplier - if (tradeType === 'sell') { - offerDenom = baseDenom - swapDenom = quoteDenom - } else { - offerDenom = quoteDenom - swapDenom = baseDenom - } - - const offerCoin = new Coin(offerDenom, swapAmount); - debug('base', offerDenom, 'quote', swapDenom) + const initTime = Date.now() - // Create and Sign Transaction - const swap = new MsgSwap(address, offerCoin, swapDenom); - const memo = 'tx: 0802...1520' + const paramData = getParamData(req.body) + const baseToken = paramData.base + const quoteToken = paramData.quote + const tradeType = paramData.trade_type + 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 - let txSuccess, txAttributes, message + let tokenSwaps try { - const tx = await wallet.createAndSignTx({ - msgs: [swap], - memo: memo - }).then(tx => terra.tx.broadcast(tx)).then(result => { - debug(`TX hash: ${result.txhash}`); - txSuccess = true - const txHash = result.txhash - const events = JSON.parse(result.raw_log)[0].events - console.log(events) - const swap = events.find(obj => { - return obj.type === 'swap' - }) - txAttributes = getTxAttributes(swap.attributes) - - message = { - txHash: txHash - } + await terra.swapTokens(baseToken, quoteToken, amount, tradeType, gasPrice, gasAdjustment, secret).then((swap) => { + tokenSwaps = swap + }).catch((err) => { + reportConnectionError(res, err) }) + + const swapResult = { + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseToken, + tradeType: tradeType, + quote: quoteToken, + amount: amount, + } + Object.assign(swapResult, tokenSwaps); + res.status(200).json( + swapResult + ) } catch (err) { - txSuccess = false + let message + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error const isAxiosError = err.isAxiosError if (isAxiosError) { - const status = err.response.status - const statusText = err.response.statusText - message = { error: statusText, status: status, data: err.response.data } + reason = err.response.status + message = err.response.statusText } else { - message = err.status + message = err } + res.status(500).json({ + error: reason, + message: message + }) } - - res.status(200).json({ - success: txSuccess, - timestamp: Date.now(), - buy: txAttributes.swap_coin, - sell: txAttributes.offer, - fee: txAttributes.swap_fee, - message: message - }) }) module.exports = router; diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js new file mode 100644 index 0000000..7b08aa0 --- /dev/null +++ b/src/routes/uniswap.route.js @@ -0,0 +1,319 @@ +import { ethers } from 'ethers'; +import express from 'express'; + +import { getParamData, latency, statusMessages } from '../services/utils'; +import Uniswap from '../services/uniswap'; + +require('dotenv').config() +const debug = require('debug')('router') + +const router = express.Router() +const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) + +const swapMoreThanMaxPriceError = 'Price too high' +const swapLessThanMaxPriceError = 'Price too low' + +const estimateGasLimit = () => { + return uniswap.gasLimit +} + +router.post('/', async (req, res) => { + /* + POST / + */ + res.status(200).json({ + network: uniswap.network, + provider: uniswap.provider.connection.url, + uniswap_router: uniswap.router, + connection: true, + timestamp: Date.now(), + }) +}) + +router.post('/gas-limit', async (req, res) => { + /* + POST: /buy-price + */ + const gasLimit = estimateGasLimit() + + res.status(200).json({ + network: uniswap.network, + gasLimit: gasLimit, + timestamp: Date.now(), + }) +}) + +router.post('/sell-price', async (req, res) => { + /* + POST: /sell-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 + + 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 + ) + + 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, + }) + } else { // no pool available + res.status(200).json({ + error: statusMessages.no_pool_available, + message: '' + }) + } + } catch (err) { + debug(err) + let reason + let err_code = 500 + if (Object.keys(err).includes('isInsufficientReservesError')) { + err_code = 200 + reason = statusMessages.insufficient_reserves + ' in Sell at Uniswap' + } else { + err.reason ? reason = err.reason : reason = statusMessages.operation_error + } + res.status(err_code).json({ + error: reason, + message: err + }) + } +}) + +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 + + 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) { + debug(err) + let reason + let err_code = 500 + if (Object.keys(err).includes('isInsufficientReservesError')) { + err_code = 200 + reason = statusMessages.insufficient_reserves + ' in Buy at Uniswap' + } else { + err.reason ? reason = err.reason : reason = statusMessages.operation_error + } + res.status(err_code).json({ + error: reason, + message: err + }) + } +}) + +router.post('/sell', async (req, res) => { + /* + POST: /sell + x-www-form-urlencoded: { + "quote":"0x....." + "base":"0x....." + "amount":0.1 + "minPrice":1 + "gasPrice":10 + "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, uniswap.provider) + const baseTokenAddress = paramData.base + const quoteTokenAddress = paramData.quote + const amount = paramData.amount + + let maxPrice + if (paramData.maxPrice) { + maxPrice = parseFloat(paramData.maxPrice) + } + let gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } + + 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) + 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, + ) + + // 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, + }) + } else { + res.status(200).json({ + error: swapLessThanMaxPriceError, + message: `Swap price ${price} lower than maxPrice ${maxPrice}` + }) + debug(`Swap price ${price} lower than maxPrice ${maxPrice}`) + } + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/buy', async (req, res) => { + /* + POST: /buy + x-www-form-urlencoded: { + "quote":"0x....." + "base":"0x....." + "amount":0.1 + "maxPrice":1 + "gasPrice":10 + "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, uniswap.provider) + const baseTokenAddress = paramData.base + const quoteTokenAddress = paramData.quote + const amount = paramData.amount + + let maxPrice + if (paramData.maxPrice) { + maxPrice = parseFloat(paramData.maxPrice) + } + let gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } + + 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) + debug(`Price: ${price.toString()}`) + if (!maxPrice || price <= maxPrice) { + // 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: expectedIn.toSignificant(8), + price: price, + txHash: tx.hash, + }) + } else { + res.status(200).json({ + error: swapMoreThanMaxPriceError, + message: `Swap price ${price} exceeds maxPrice ${maxPrice}` + }) + debug(`Swap price ${price} exceeds maxPrice ${maxPrice}`) + } + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +export default router; diff --git a/src/services/access.js b/src/services/access.js new file mode 100644 index 0000000..e35ff81 --- /dev/null +++ b/src/services/access.js @@ -0,0 +1,21 @@ +/* + middleware for validating mutual authentication access +*/ + +import { statusMessages } from './utils'; + +const debug = require('debug')('router') + +export const validateAccess = (req, res, next) => { + const cert = req.connection.getPeerCertificate() + if (req.client.authorized) { + debug('Access granted') + next() + } else if (cert.subject) { + console.log('Error!', statusMessages.ssl_cert_invalid) + res.status(403).send({ error: statusMessages.ssl_cert_invalid }) + } else { + console.log('Error!', statusMessages.ssl_cert_required) + res.status(401).send({ error: statusMessages.ssl_cert_required }) + } +} diff --git a/src/services/balancer.js b/src/services/balancer.js index 0698e3a..6554922 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -1,5 +1,4 @@ require('dotenv').config() // DO NOT REMOVE. needed to configure REACT_APP_SUBGRAPH_URL used by @balancer-labs/sor -const fs = require('fs'); const sor = require('@balancer-labs/sor') const BigNumber = require('bignumber.js') const ethers = require('ethers') @@ -7,30 +6,40 @@ const proxyArtifact = require('../static/ExchangeProxy.json') const debug = require('debug')('router') // constants -const MAX_UINT = ethers.constants.MaxUint256; const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; -const GAS_LIMIT = 1200000 +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; export default class Balancer { constructor (network = 'kovan') { - // network defaults to kovan const providerUrl = process.env.ETHEREUM_RPC_URL - this.network = process.env.BALANCER_NETWORK + this.network = process.env.ETHEREUM_CHAIN this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.exchangeProxy = process.env.EXCHANGE_PROXY + this.subgraphUrl = process.env.REACT_APP_SUBGRAPH_URL + this.gasBase = GAS_BASE + this.gasPerSwap = GAS_PER_SWAP + this.maxSwaps = MAX_SWAPS - if (network === 'kovan') { - // this.erc20Tokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens_kovan.json')) - // this.exchangeProxy = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec' - } else if (network === 'mainnet') { - // this.erc20Tokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens.json')) - // this.exchangeProxy = '0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21' - } else { - throw Error(`Invalid network ${network}`) + 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: + throw Error(`Invalid network ${network}`) } } - async priceSwapIn (tokenIn, tokenOut, tokenInAmount) { + async priceSwapIn (tokenIn, tokenOut, tokenInAmount, maxSwaps = MAX_SWAPS) { // Fetch all the pools that contain the tokens provided const pools = await sor.getPoolsWithTokens(tokenIn, tokenOut) if (pools.pools.length === 0) { @@ -39,20 +48,22 @@ export default class Balancer { } console.log('Pools Retrieved.', this.network); + // Get current on-chain data about the fetched pools let poolData if (this.network === 'mainnet') { - poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, MULTI, this.provider) + poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, this.multiCall, this.provider) } else { + // Kovan multicall throws an ENS error poolData = await sor.parsePoolData(pools.pools, tokenIn, tokenOut) } // Parse the pools and pass them to smart order outer to get the swaps needed const sorSwaps = sor.smartOrderRouter( - poolData, // balancers: Pool[] - 'swapExactIn', // swapType: string - tokenInAmount, // targetInputAmount: BigNumber - new BigNumber('4'), // maxBalancers: number - 0 // costOutputToken: BigNumber + poolData, // balancers: Pool[] + 'swapExactIn', // swapType: string + tokenInAmount, // targetInputAmount: BigNumber + new BigNumber(maxSwaps.toString()), // maxBalancers: number + 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountIn(sorSwaps, MAX_UINT, 0) @@ -75,7 +86,7 @@ export default class Balancer { return { swaps, expectedOut } } - async priceSwapOut (tokenIn, tokenOut, tokenOutAmount) { + async priceSwapOut (tokenIn, tokenOut, tokenOutAmount, maxSwaps = MAX_SWAPS) { // Fetch all the pools that contain the tokens provided const pools = await sor.getPoolsWithTokens(tokenIn, tokenOut) if (pools.pools.length === 0) { @@ -84,20 +95,22 @@ export default class Balancer { } console.log('Pools Retrieved.', this.network); + // Get current on-chain data about the fetched pools let poolData if (this.network === 'mainnet') { - poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, MULTI, this.provider) + poolData = await sor.parsePoolDataOnChain(pools.pools, tokenIn, tokenOut, this.multiCall, this.provider) } else { + // Kovan multicall throws an ENS error poolData = await sor.parsePoolData(pools.pools, tokenIn, tokenOut) } // Parse the pools and pass them to smart order outer to get the swaps needed const sorSwaps = sor.smartOrderRouter( - poolData, // balancers: Pool[] - 'swapExactOut', // swapType: string - tokenOutAmount, // targetInputAmount: BigNumber - new BigNumber('4'), // maxBalancers: number - 0 // costOutputToken: BigNumber + poolData, // balancers: Pool[] + 'swapExactOut', // swapType: string + tokenOutAmount, // targetInputAmount: BigNumber + new BigNumber(maxSwaps.toString()), // maxBalancers: number + 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountOut(sorSwaps, MAX_UINT, MAX_UINT) const expectedIn = sor.calcTotalInput(swapsFormatted, poolData) @@ -120,6 +133,7 @@ export default class Balancer { } async swapExactIn (wallet, swaps, tokenIn, tokenOut, amountIn, minAmountOut, gasPrice) { + debug(`Number of swaps: ${swaps.length}`); const contract = new ethers.Contract(this.exchangeProxy, proxyArtifact.abi, wallet) const tx = await contract.batchSwapExactIn( swaps, @@ -129,15 +143,15 @@ export default class Balancer { 0, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`); - const txObj = await tx.wait() - return txObj + return tx } async swapExactOut (wallet, swaps, tokenIn, tokenOut, expectedIn, gasPrice) { + debug(`Number of swaps: ${swaps.length}`); const contract = new ethers.Contract(this.exchangeProxy, proxyArtifact.abi, wallet) const tx = await contract.batchSwapExactOut( swaps, @@ -146,11 +160,10 @@ export default class Balancer { expectedIn, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`) - const txObj = await tx.wait() - return txObj + return tx } } diff --git a/src/services/eth.js b/src/services/eth.js index 4788792..2bbbe5b 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -2,13 +2,16 @@ require('dotenv').config() const fs = require('fs'); const ethers = require('ethers') const abi = require('../static/abi') +const debug = require('debug')('router') + +// constants export default class Ethereum { - constructor (network = 'kovan') { + constructor (network = 'mainnet') { // network defaults to kovan const providerUrl = process.env.ETHEREUM_RPC_URL - this.network = process.env.BALANCER_NETWORK this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.network = network if (network === 'kovan') { // for kovan testing only @@ -34,12 +37,12 @@ export default class Ethereum { } // get ERC-20 token balance - async getERC20Balance (wallet, tokenAddress) { + async getERC20Balance (wallet, tokenAddress, decimals) { // instantiate a contract and pass in provider for read-only access const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, this.provider) try { const balance = await contract.balanceOf(wallet.address) - return balance / 1e18.toString() + return balance / Math.pow(10, decimals).toString() } catch (err) { let reason err.reason ? reason = err.reason : reason = 'error balance lookup' @@ -48,12 +51,12 @@ export default class Ethereum { } // get ERC-20 token allowance - async getERC20Allowance (wallet, spender, tokenAddress) { + async getERC20Allowance (wallet, spender, tokenAddress, decimals) { // instantiate a contract and pass in provider for read-only access const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, this.provider) try { const allowance = await contract.allowance(wallet.address, spender) - return allowance / 1e18.toString() + return allowance / Math.pow(10, decimals).toString() } catch (err) { let reason err.reason ? reason = err.reason : reason = 'error allowance lookup' @@ -62,16 +65,17 @@ export default class Ethereum { } // approve a spender to transfer tokens from a wallet address - async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = process.env.GAS_PRICE) { - const GAS_LIMIT = 100000 + async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit) { try { + // fixate gas limit to prevent overwriting + const approvalGasLimit = 50000 // 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( spender, amount, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: approvalGasLimit } ) } catch (err) { @@ -81,15 +85,29 @@ export default class Ethereum { } } - async deposit (wallet, tokenAddress, amount, gasPrice = process.env.GAS_PRICE) { - const GAS_LIMIT = 100000 + // get current Gas + async getCurrentGasPrice () { + try { + this.provider.getGasPrice().then(function (gas) { + // gasPrice is a BigNumber; convert it to a decimal string + const gasPrice = gas.toString(); + return gasPrice + }) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'error gas lookup' + return reason + } + } + + async deposit (wallet, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit = this.approvalGasLimit) { // deposit ETH to a contract address try { const contract = new ethers.Contract(tokenAddress, abi.KovanWETHAbi, wallet) return await contract.deposit( { value: amount, gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: gasLimit } ) } catch (err) { diff --git a/src/services/terra.js b/src/services/terra.js new file mode 100644 index 0000000..3c6ac1b --- /dev/null +++ b/src/services/terra.js @@ -0,0 +1,315 @@ +import { LCDClient, Coin, MsgSwap, StdTx, StdFee, Dec, MnemonicKey, isTxError, Coins } from '@terra-money/terra.js' +import BigNumber from 'bignumber.js' +import { getHummingbotMemo } from './utils'; + +require('dotenv').config() +const debug = require('debug')('router') + +// constants +const TERRA_TOKENS = { + uluna: { symbol: 'LUNA' }, + uusd: { symbol: 'UST' }, + ukrw: { symbol: 'KRT' }, + usdr: { symbol: 'SDT' }, + umnt: { symbol: 'MNT' }, +} +const DENOM_UNIT = BigNumber('1e+6') +const TOBIN_TAX = 0.0025 // a Tobin Tax (set at 0.25%) for spot-converting Terra<>Terra swaps +const MIN_SPREAD = 0.02 // a minimum spread (set at 2%) for Terra<>Luna swaps +const GAS_PRICE = { uluna: 0.16 } +const GAS_ADJUSTMENT = 1.4 + +export default class Terra { + constructor () { + this.lcdUrl = process.env.TERRA_LCD_URL; + this.network = process.env.TERRA_CHAIN; + this.tokens = TERRA_TOKENS + this.denomUnitMultiplier = DENOM_UNIT + this.tobinTax = TOBIN_TAX + this.minSpread = MIN_SPREAD + this.memo = getHummingbotMemo() + + try { + this.lcd = this.connect() + + this.lcd.market.parameters().catch(() => { + throw new Error('Connection error') + }) + // set gas & fee + this.lcd.config.gasAdjustment = GAS_ADJUSTMENT + this.lcd.config.gasPrices = GAS_PRICE + } catch (err) { + throw Error(`Connection failed: ${this.network}`) + } + } + + // connect Terra LCD + connect () { + try { + const lcd = new LCDClient({ + URL: this.lcdUrl, + chainID: this.network, + }) + lcd.config.gasAdjustment = GAS_ADJUSTMENT + lcd.config.gasPrices = GAS_PRICE + return lcd + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra LCD connect' + return reason + } + } + + // get Token Denom + getTokenDenom (symbol) { + try { + let denom + Object.keys(TERRA_TOKENS).forEach((item) => { + if (TERRA_TOKENS[item].symbol === symbol) { + denom = item + } + }) + return denom + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra Denom lookup' + return reason + } + } + + // get Token Symbol + getTokenSymbol (denom) { + try { + const symbol = TERRA_TOKENS[denom].symbol + return symbol + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra Denom lookup' + return reason + } + } + + getTxAttributes (attributes) { + let attrib = {} + attributes.forEach((item) => { + attrib[item.key] = item.value + }) + return attrib + } + + async getEstimateFee (tx) { + try { + const fee = await this.lcd.tx.estimateFee(tx) + return fee + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra estimate fee lookup' + return reason + } + } + + async getExchangeRate (denom) { + try { + const exchangeRates = await this.lcd.oracle.exchangeRates() + return exchangeRates.get(denom) + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra exchange rate lookup' + return reason + } + } + + async getTxFee () { + try { + const lunaFee = GAS_PRICE.uluna * GAS_ADJUSTMENT + let feeList = { uluna: lunaFee } + await this.lcd.oracle.exchangeRates().then(rates => { + Object.keys(rates._coins).forEach(key => { + feeList[key] = rates._coins[key].amount * lunaFee + }) + }) + debug('lunaFee', lunaFee, feeList) + + return feeList + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra exchange rate lookup' + return reason + } + } + + // get Terra Swap Rate + async getSwapRate (baseToken, quoteToken, amount, tradeType) { + try { + let exchangeRate, offerCoin, offerDenom, swapDenom, cost, costAmount, offer + let swaps = {} + + if (tradeType.toLowerCase() === 'sell') { + // sell base + offerDenom = this.getTokenDenom(baseToken) + swapDenom = this.getTokenDenom(quoteToken) + + offerCoin = new Coin(offerDenom, amount * DENOM_UNIT); + await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + offer = { amount: amount } + exchangeRate = { + amount: (swapCoin.amount / DENOM_UNIT) / amount, + token: quoteToken + } + costAmount = amount * exchangeRate.amount + cost = { + amount: costAmount, + token: quoteToken + } + }) + } else { + // buy base + offerDenom = this.getTokenDenom(quoteToken) + swapDenom = this.getTokenDenom(baseToken) + + offerCoin = new Coin(offerDenom, 1 * DENOM_UNIT); + await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + exchangeRate = { + amount: (amount / parseInt(swapCoin.amount) * DENOM_UNIT) / amount, // adjusted amount + token: quoteToken + } + costAmount = amount * exchangeRate.amount + cost = { + amount: costAmount, + token: quoteToken + } + offer = { amount: cost.amount } + }) + } + + let txFee + await this.getTxFee().then(fee => { + // fee in quote + txFee = { amount: parseFloat(fee[this.getTokenDenom(quoteToken)]), token: quoteToken } + }) + + swaps.offer = offer + swaps.price = exchangeRate + swaps.cost = cost + swaps.txFee = txFee + debug('swaps', swaps) + return swaps + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error swap rate lookup' + return reason + } + } + + // Swap tokens + async swapTokens (baseToken, quoteToken, amount, tradeType, gasPrice, gasAdjustment, secret) { + let swapResult + try { + // connect to lcd + const lcd = this.connect() + + const mk = new MnemonicKey({ + mnemonic: secret, + }); + let wallet + try { + wallet = lcd.wallet(mk); + } catch (err) { + throw Error('Wallet access error') + } + + const address = wallet.key.accAddress + + // get the current swap rate + const baseDenom = this.getTokenDenom(baseToken) + const quoteDenom = this.getTokenDenom(quoteToken) + + let offerDenom, swapDenom + let swaps, txAttributes + let tokenSwap = {} + + if (tradeType.toLowerCase() === 'sell') { + offerDenom = baseDenom + swapDenom = quoteDenom + } else { + offerDenom = quoteDenom + swapDenom = baseDenom + } + + await this.getSwapRate(baseToken, quoteToken, amount, tradeType, secret).then((rate) => { + swaps = rate + }) + + 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); + + let txOptions + if (gasPrice !== null && gasPrice !== null) { // ignore gasAdjustment when gasPrice is not set + txOptions = { + msgs: [msgSwap], + gasPrices: { uluna: parseFloat(gasPrice) }, + gasAdjustment: gasAdjustment, + memo: this.memo + } + } else { + txOptions = { + msgs: [msgSwap], + memo: this.memo + } + } + + await wallet.createAndSignTx(txOptions).then(tx => lcd.tx.broadcast(tx)).then((txResult) => { + swapResult = txResult + + const swapSuccess = !isTxError(txResult) + if (swapSuccess) { + tokenSwap.txSuccess = swapSuccess + } else { + 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 => { + return obj.type === 'swap' + }) + txAttributes = this.getTxAttributes(swap.attributes) + const offer = Coin.fromString(txAttributes.offer) + const ask = Coin.fromString(txAttributes.swap_coin) + const fee = Coin.fromString(txAttributes.swap_fee) + + tokenSwap.expectedIn = { + amount: parseFloat(offer.amount) / DENOM_UNIT, + token: TERRA_TOKENS[offer.denom].symbol + } + tokenSwap.expectedOut = { + amount: parseFloat(ask.amount) / DENOM_UNIT, + token: TERRA_TOKENS[ask.denom].symbol + } + tokenSwap.fee = { + amount: parseFloat(fee.amount) / DENOM_UNIT, + token: TERRA_TOKENS[fee.denom].symbol + } + tokenSwap.txHash = txHash + }) + return tokenSwap + } catch (err) { + let reason + console.log(err) + err.reason ? reason = err.reason : reason = swapResult + return { txSuccess: false, message: reason } + } + } +} diff --git a/src/services/uniswap.js b/src/services/uniswap.js new file mode 100644 index 0000000..ced9abf --- /dev/null +++ b/src/services/uniswap.js @@ -0,0 +1,115 @@ +const uni = require('@uniswap/sdk') +const ethers = require('ethers') +const proxyArtifact = require('../static/uniswap_v2_router_abi.json') +const debug = require('debug')('router') + +// constants +const ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; +const GAS_LIMIT = 150688; +const TTL = 60; + +export default class Uniswap { + constructor (network = 'mainnet') { + const providerUrl = process.env.ETHEREUM_RPC_URL + this.network = process.env.ETHEREUM_CHAIN + this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.router = ROUTER; + this.allowedSlippage = new uni.Percent('0', '100') + this.gasLimit = GAS_LIMIT + + switch (network) { + case 'mainnet': + this.chainID = uni.ChainId.MAINNET; + break; + case 'kovan': + this.chainID = uni.ChainId.KOVAN; + break; + default: + throw Error(`Invalid network ${network}`) + } + } + + async fetch_route(tokenIn, tokenOut){ + var route, pair, pairOne, pairTwo + var tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenIn) + var tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenOut) + + try { + pair = await uni.Fetcher.fetchPairData(tIn, tOut) + route = new uni.Route([pair], tIn, tOut) + } + catch(err) { + console.log('Trying alternative/indirect route.') + pairOne = await uni.Fetcher.fetchPairData(tIn, uni.WETH[this.chainID]) + pairTwo = await uni.Fetcher.fetchPairData(tOut, uni.WETH[this.chainID]) + route = new uni.Route([pairOne, pairTwo], tIn, tOut) + } + return route + } + + async priceSwapIn (tokenIn, tokenOut, tokenInAmount) { + const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenIn) + const tokenAmountIn = new uni.TokenAmount(tIn, ethers.utils.parseUnits(tokenInAmount, tIn.decimals)) + const route = await this.fetch_route(tokenIn, tokenOut) + const trade = uni.Trade.exactIn(route, tokenAmountIn) + const expectedOut = trade.minimumAmountOut(this.allowedSlippage) + return { trade, expectedOut } + } + + async priceSwapOut (tokenIn, tokenOut, tokenOutAmount) { + const tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenOut) + const tokenAmountOut = new uni.TokenAmount(tOut, ethers.utils.parseUnits(tokenOutAmount, tOut.decimals)) + const route = await this.fetch_route(tokenIn, tokenOut) + const trade = uni.Trade.exactOut(route, tokenAmountOut) + const expectedIn = trade.maximumAmountIn(this.allowedSlippage) + return { trade, expectedIn } + } + + async swapExactIn (wallet, trade, tokenAddress, gasPrice) { + const result = uni.Router.swapCallParameters( + trade, + { + ttl: TTL, + recipient: wallet.address, + allowedSlippage: this.allowedSlippage + } + ) + + const contract = new ethers.Contract(this.router, proxyArtifact.abi, wallet) + const tx = await contract.[result.methodName]( + ...result.args, + { + gasPrice: gasPrice * 1e9, + gasLimit: GAS_LIMIT, + value: result.value + } + ) + + debug(`Tx Hash: ${tx.hash}`); + return tx + } + + async swapExactOut (wallet, trade, tokenAddress, gasPrice) { + const result = uni.Router.swapCallParameters( + trade, + { + ttl: TTL, + recipient: wallet.address, + allowedSlippage: this.allowedSlippage + } + ) + + const contract = new ethers.Contract(this.router, proxyArtifact.abi, wallet) + const tx = await contract.[result.methodName]( + ...result.args, + { + gasPrice: gasPrice * 1e9, + gasLimit: GAS_LIMIT, + value: result.value + } + ) + + debug(`Tx Hash: ${tx.hash}`); + return tx + } +} diff --git a/src/services/utils.js b/src/services/utils.js index 4095936..8b9c9af 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -8,6 +8,8 @@ export const statusMessages = { ssl_cert_invalid: 'Invalid SSL Certificate', operation_error: 'Operation Error', no_pool_available: 'No Pool Available', + invalid_token_symbol: 'Invalid Token Symbol', + insufficient_reserves: 'Insufficient Liquidity Reserves', } export const latency = (startTime, endTime) => parseFloat((endTime - startTime) / 1000) @@ -45,6 +47,11 @@ export const getParamData = (data, format = null) => { return dataObject } +export const splitParamData = (param, separator = ',') => { + const dataArray = param.split(separator) + return dataArray +} + export const getSymbols = (tradingPair) => { const symbols = tradingPair.split('-') const baseQuotePair = { @@ -62,3 +69,25 @@ export const reportConnectionError = (res, error) => { } export const strToDecimal = (str) => parseInt(str) / 100; + +export const getHummingbotMemo = () => { + const prefix = 'hbot' + const clientId = process.env.HUMMINGBOT_INSTANCE_ID + if ((typeof clientId !== 'undefined' && clientId != null) && clientId !== '') { + return [prefix, clientId].join('-') + } + return prefix +} + +export const loadConfig = () => { + const config = { + ethereum_rpc_url: process.env.ETHEREUM_RPC_URL, + ethereum_chain: process.env.ETHEREUM_CHAIN, + exchange_proxy: process.env.EXCHANGE_PROXY, + react_app_subgraph_url: process.env.REACT_APP_SUBGRAPH_URL, + uniswap_router: process.env.UNISWAP_ROUTER, + terra_lcd_url: process.env.TERRA_LCD_URL, + terra_chain: process.env.TERRA_CHAIN + } + return config +} diff --git a/src/static/uniswap_v2_router_abi.json b/src/static/uniswap_v2_router_abi.json new file mode 100644 index 0000000..e2eac42 --- /dev/null +++ b/src/static/uniswap_v2_router_abi.json @@ -0,0 +1,1924 @@ +{ + "abi": [ + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountADesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountTokenDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETHSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapETHForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETHSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "evm": { + "bytecode": { + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + }, + "deployedBytecode": { + "immutableReferences": {}, + "linkReferences": {}, + "object": "", + "opcodes": "", + "sourceMap": "" + } + }, + "interface": [ + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountADesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountTokenDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETHSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapETHForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETHSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "" +} diff --git a/test/command.md b/test/command.md new file mode 100644 index 0000000..7ddb10c --- /dev/null +++ b/test/command.md @@ -0,0 +1,3 @@ + +# test endpoint +curl --insecure --key /home/dev/bots/hbot_files/hummingbot_certs/client_key.pem --cert /home/dev/bots/hbot_files/hummingbot_certs/client_cert.pem https://localhost:5000/api diff --git a/test/postman/Gateway-Terra.postman_collection.json b/test/postman/Gateway-Terra.postman_collection.json new file mode 100644 index 0000000..40866b8 --- /dev/null +++ b/test/postman/Gateway-Terra.postman_collection.json @@ -0,0 +1,328 @@ +{ + "info": { + "_postman_id": "3cca9e73-0e1f-4e4f-8973-87c985a43219", + "name": "Gateway-Terra", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "terra", + "item": [ + { + "name": "terra/balances", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "address", + "value": "{{address}}", + "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/price", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "SDT", + "type": "text" + }, + { + "key": "quote", + "value": "KRT", + "type": "text" + }, + { + "key": "trade_type", + "value": "buy", + "type": "text" + }, + { + "key": "amount", + "value": "3", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/terra/price", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "price" + ] + } + }, + "response": [ + { + "name": "{network}/quote", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/{{network}}/quote/trading_pair/{{celo-cusd}}/amount/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "{{network}}", + "quote", + "trading_pair", + "{{celo-cusd}}", + "amount", + "1" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests" + }, + { + "key": "X-DNS-Prefetch-Control", + "value": "off" + }, + { + "key": "Expect-CT", + "value": "max-age=0" + }, + { + "key": "X-Frame-Options", + "value": "SAMEORIGIN" + }, + { + "key": "Strict-Transport-Security", + "value": "max-age=15552000; includeSubDomains" + }, + { + "key": "X-Download-Options", + "value": "noopen" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "X-Permitted-Cross-Domain-Policies", + "value": "none" + }, + { + "key": "Referrer-Policy", + "value": "no-referrer" + }, + { + "key": "X-XSS-Protection", + "value": "0" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "97" + }, + { + "key": "ETag", + "value": "W/\"61-Wemp9YmP9g/CsUFMa7Y5zK6SoLQ\"" + }, + { + "key": "Date", + "value": "Wed, 23 Sep 2020 18:07:26 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + } + ], + "cookie": [], + "body": "{\n \"timestamp\": 1600884444051,\n \"latency\": 2.542,\n \"trading_pair\": \"CELO-CUSD\",\n \"price\": 2.5435604641582747\n}" + } + ] + }, + { + "name": "terra/trade", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "base", + "value": "SDT", + "type": "text" + }, + { + "key": "quote", + "value": "KRT", + "type": "text" + }, + { + "key": "trade_type", + "value": "buy", + "type": "text" + }, + { + "key": "amount", + "value": "3", + "type": "text" + }, + { + "key": "secret", + "value": "{{secret}}", + "type": "text" + } + ] + }, + "url": { + "raw": "https://localhost:{{port}}/terra/trade", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "terra", + "trade" + ] + } + }, + "response": [ + { + "name": "{network}/quote", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/{{network}}/quote/trading_pair/{{celo-cusd}}/amount/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "{{network}}", + "quote", + "trading_pair", + "{{celo-cusd}}", + "amount", + "1" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests" + }, + { + "key": "X-DNS-Prefetch-Control", + "value": "off" + }, + { + "key": "Expect-CT", + "value": "max-age=0" + }, + { + "key": "X-Frame-Options", + "value": "SAMEORIGIN" + }, + { + "key": "Strict-Transport-Security", + "value": "max-age=15552000; includeSubDomains" + }, + { + "key": "X-Download-Options", + "value": "noopen" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "X-Permitted-Cross-Domain-Policies", + "value": "none" + }, + { + "key": "Referrer-Policy", + "value": "no-referrer" + }, + { + "key": "X-XSS-Protection", + "value": "0" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "97" + }, + { + "key": "ETag", + "value": "W/\"61-Wemp9YmP9g/CsUFMa7Y5zK6SoLQ\"" + }, + { + "key": "Date", + "value": "Wed, 23 Sep 2020 18:07:26 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + } + ], + "cookie": [], + "body": "{\n \"timestamp\": 1600884444051,\n \"latency\": 2.542,\n \"trading_pair\": \"CELO-CUSD\",\n \"price\": 2.5435604641582747\n}" + } + ] + } + ], + "protocolProfileBehavior": {} + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file diff --git a/test/postman/terra.postman_environment.json b/test/postman/terra.postman_environment.json new file mode 100644 index 0000000..735927a --- /dev/null +++ b/test/postman/terra.postman_environment.json @@ -0,0 +1,29 @@ +{ + "id": "aebe7d1f-0e85-4441-8679-2ddc38d74350", + "name": "terra", + "values": [ + { + "key": "protocol", + "value": "terra", + "enabled": true + }, + { + "key": "port", + "value": "5000", + "enabled": true + }, + { + "key": "address", + "value": "myaddresss", + "enabled": true + }, + { + "key": "secret", + "value": "mysupersecret", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2020-11-13T06:00:14.142Z", + "_postman_exported_using": "Postman/7.35.0" +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e1a4de4..f007dde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1256,6 +1256,24 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== +"@uniswap/sdk@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-3.0.3.tgz#8201c7c72215d0030cb99acc7e661eff895c18a9" + integrity sha512-t4s8bvzaCFSiqD2qfXIm3rWhbdnXp+QjD3/mRaeVDHK7zWevs6RGEb1ohMiNgOCTZANvBayb4j8p+XFdnMBadQ== + dependencies: + "@uniswap/v2-core" "^1.0.0" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.1" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + toformat "^2.0.0" + +"@uniswap/v2-core@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1429,6 +1447,11 @@ bech32@1.1.4, bech32@^1.1.4: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + bignumber.js@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" @@ -1828,6 +1851,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: dependencies: ms "2.1.2" +decimal.js-light@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" @@ -2881,6 +2909,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbi@^3.1.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.1.4.tgz#9654dd02207a66a4911b4e4bb74265bc2cbc9dd0" + integrity sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3979,6 +4012,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-invariant@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tiny-secp256k1@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-1.1.5.tgz#3dc37b9bf0fa5b4390b9fa29e953228810cebc18" @@ -3990,6 +4028,11 @@ tiny-secp256k1@^1.1.3: elliptic "^6.4.0" nan "^2.13.2" +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -4007,6 +4050,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/toformat/-/toformat-2.0.0.tgz#7a043fd2dfbe9021a4e36e508835ba32056739d8" + integrity sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ== + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"