From 6833f5ec5706ede41e256ba4fbe816ed73c30884 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 9 Nov 2020 10:08:31 +0800 Subject: [PATCH 01/41] refactor common variables across routes --- .dockerignore | 5 ++++- .env.example | 3 +++ certs/readme.md | 2 +- src/routes/balancer.route.js | 12 +++++++----- src/routes/eth.route.js | 10 +++++++--- src/services/balancer.js | 12 +++++++----- src/services/config.js | 21 ++++++++++++++++++++ src/services/eth.js | 37 ++++++++++++++++++++++++++++-------- test/command.md | 3 +++ 9 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 src/services/config.js create mode 100644 test/command.md 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..6cdac29 100644 --- a/.env.example +++ b/.env.example @@ -38,3 +38,6 @@ EXCHANGE_PROXY={exchange_proxy} # cert CERT_PATH={full_path_to_certs_folder} CERT_PASSPHRASE={passphrase} + +# Gas Limit +GAS_LIMIT=750000 \ No newline at end of file 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/src/routes/balancer.route.js b/src/routes/balancer.route.js index 8880861..5b101aa 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -3,15 +3,16 @@ import { ethers } from 'ethers'; import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getConfig } from '../services/config'; + 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 envConfig = getConfig() +const balancer = new Balancer(envConfig.balancer.BALANCER_NETWORK) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' @@ -36,7 +37,8 @@ router.post('/', async (req, res) => { network: balancer.network, provider: balancer.provider.connection.url, exchangeProxy: balancer.exchangeProxy, - subgraphUrl: process.env.REACT_APP_SUBGRAPH_URL, + subgraphUrl: envConfig.balancer.REACT_APP_SUBGRAPH_URL, + gasLimit: envConfig.balancer.GAS_LIMIT, connection: true, timestamp: Date.now(), }) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 4208e13..67216d6 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -3,12 +3,14 @@ import { ethers } from 'ethers'; import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getConfig } from '../services/config'; 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 envConfig = getConfig() +const eth = new Ethereum(envConfig.balancer.BALANCER_NETWORK) +const balancer = new Balancer(envConfig.balancer.BALANCER_NETWORK) const seperator = ',' const debug = require('debug')('router') @@ -24,9 +26,10 @@ router.post('/balances', async (req, res) => { const initTime = Date.now() const paramData = getParamData(req.body) const privateKey = paramData.privateKey - let wallet + let wallet, currentGasPrice try { wallet = new ethers.Wallet(privateKey, eth.provider) + currentGasPrice = await eth.getCurrentGasPrice() } catch (err) { let reason err.reason ? reason = err.reason : reason = 'Error getting wallet' @@ -53,6 +56,7 @@ router.post('/balances', async (req, res) => { network: eth.network, timestamp: initTime, latency: latency(initTime, Date.now()), + currentGasPrice: currentGasPrice, balances: balances }) }) diff --git a/src/services/balancer.js b/src/services/balancer.js index 0698e3a..63e5e78 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -5,19 +5,21 @@ const BigNumber = require('bignumber.js') const ethers = require('ethers') const proxyArtifact = require('../static/ExchangeProxy.json') const debug = require('debug')('router') +const config = require('../services/config') // constants +const ENV_CONFIG = config.getConfig() const MAX_UINT = ethers.constants.MaxUint256; const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; -const GAS_LIMIT = 1200000 export default class Balancer { constructor (network = 'kovan') { // network defaults to kovan - const providerUrl = process.env.ETHEREUM_RPC_URL - this.network = process.env.BALANCER_NETWORK + const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL + this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.exchangeProxy = process.env.EXCHANGE_PROXY + this.exchangeProxy = ENV_CONFIG.balancer.EXCHANGE_PROXY + this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT if (network === 'kovan') { // this.erc20Tokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens_kovan.json')) @@ -129,7 +131,7 @@ export default class Balancer { 0, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: this.gasLimit } ) debug(`Tx Hash: ${tx.hash}`); diff --git a/src/services/config.js b/src/services/config.js new file mode 100644 index 0000000..1275dca --- /dev/null +++ b/src/services/config.js @@ -0,0 +1,21 @@ +require('dotenv').config() + +export const getConfig = () => { + const env = { + terra: { + TERRA_LCD_URL: process.env.TERRA_LCD_URL, + TERRA_CHAIN: process.env.TERRA_LCD_URL, + }, + balancer: { + BALANCER_NETWORK: process.env.BALANCER_NETWORK, + REACT_APP_SUBGRAPH_URL: process.env.REACT_APP_SUBGRAPH_URL, + EXCHANGE_PROXY: process.env.EXCHANGE_PROXY, + GAS_PRICE: parseInt(process.env.GAS_PRICE), + GAS_LIMIT: parseInt(process.env.GAS_LIMIT) || 1200000 + }, + ethereum: { + ETHEREUM_RPC_URL: process.env.ETHEREUM_RPC_URL, + } + } + return env +} diff --git a/src/services/eth.js b/src/services/eth.js index 4788792..caac63f 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -2,13 +2,19 @@ require('dotenv').config() const fs = require('fs'); const ethers = require('ethers') const abi = require('../static/abi') +const debug = require('debug')('router') +const config = require('../services/config') + +// constants +const ENV_CONFIG = config.getConfig() export default class Ethereum { constructor (network = 'kovan') { // network defaults to kovan - const providerUrl = process.env.ETHEREUM_RPC_URL - this.network = process.env.BALANCER_NETWORK + const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL + this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT if (network === 'kovan') { // for kovan testing only @@ -62,8 +68,7 @@ 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 = ENV_CONFIG.balancer.GAS_PRICE, gasLimit = this.gasLimit) { try { // instantiate a contract and pass in wallet, which act on behalf of that signer const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, wallet) @@ -71,7 +76,7 @@ export default class Ethereum { spender, amount, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: gasLimit } ) } catch (err) { @@ -81,15 +86,31 @@ 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(); + debug('gas obj', gas) + debug('Current gas price: ', gasPrice / 1e9) + return gasPrice + }) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'error gas lookup' + return reason + } + } + + async deposit (wallet, tokenAddress, amount, gasPrice = ENV_CONFIG.balancer.GAS_PRICE, gasLimit = this.gasLimit) { // 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/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 From b34e57005deeb89bb9ccb6c76b87aa6191477a87 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 9 Nov 2020 23:49:23 +0800 Subject: [PATCH 02/41] Update input param and readme --- README.md | 3 +-- setup.md | 2 +- src/routes/eth.route.js | 3 +-- src/services/balancer.js | 2 +- src/services/eth.js | 7 ++++--- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8b49cff..01aa595 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ We created hummingbot to promote **decentralized market-making**: enabling membe ### Install Hummingbot - [Quickstart guide](https://docs.hummingbot.io/quickstart/) -- [All installation options](https://docs.hummingbot.io/installation/) -- [Installation scripts](./installation/) +- [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/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/routes/eth.route.js b/src/routes/eth.route.js index 67216d6..6722016 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -26,10 +26,9 @@ router.post('/balances', async (req, res) => { const initTime = Date.now() const paramData = getParamData(req.body) const privateKey = paramData.privateKey - let wallet, currentGasPrice + let wallet try { wallet = new ethers.Wallet(privateKey, eth.provider) - currentGasPrice = await eth.getCurrentGasPrice() } catch (err) { let reason err.reason ? reason = err.reason : reason = 'Error getting wallet' diff --git a/src/services/balancer.js b/src/services/balancer.js index 63e5e78..561b8a2 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -16,8 +16,8 @@ export default class Balancer { constructor (network = 'kovan') { // network defaults to kovan const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL - this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.exchangeProxy = ENV_CONFIG.balancer.EXCHANGE_PROXY this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT diff --git a/src/services/eth.js b/src/services/eth.js index caac63f..fe9b29c 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -12,9 +12,10 @@ export default class Ethereum { constructor (network = 'kovan') { // network defaults to kovan const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL - this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.network = ENV_CONFIG.balancer.BALANCER_NETWORK this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT + this.approvalGasLimit = ENV_CONFIG.balancer.APPROVAL_GAS_LIMIT if (network === 'kovan') { // for kovan testing only @@ -68,7 +69,7 @@ export default class Ethereum { } // approve a spender to transfer tokens from a wallet address - async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = ENV_CONFIG.balancer.GAS_PRICE, gasLimit = this.gasLimit) { + async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit = this.approvalGasLimit) { try { // instantiate a contract and pass in wallet, which act on behalf of that signer const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, wallet) @@ -103,7 +104,7 @@ export default class Ethereum { } } - async deposit (wallet, tokenAddress, amount, gasPrice = ENV_CONFIG.balancer.GAS_PRICE, gasLimit = this.gasLimit) { + 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) From 519dcab01c47820ee8d7f88595286ea098f19afd Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 10 Nov 2020 00:11:32 +0800 Subject: [PATCH 03/41] (fix) Remove undefined current gas price param --- src/routes/eth.route.js | 1 - src/services/eth.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 6722016..d3200f9 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -55,7 +55,6 @@ router.post('/balances', async (req, res) => { network: eth.network, timestamp: initTime, latency: latency(initTime, Date.now()), - currentGasPrice: currentGasPrice, balances: balances }) }) diff --git a/src/services/eth.js b/src/services/eth.js index fe9b29c..0cafdd3 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -93,8 +93,6 @@ export default class Ethereum { this.provider.getGasPrice().then(function (gas) { // gasPrice is a BigNumber; convert it to a decimal string const gasPrice = gas.toString(); - debug('gas obj', gas) - debug('Current gas price: ', gasPrice / 1e9) return gasPrice }) } catch (err) { From ce04a694095b43b72da025c10015a6e0e439161d Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 11 Nov 2020 08:40:55 +0800 Subject: [PATCH 04/41] (feat) Refactor routes to use middleware peer connection check --- src/app.js | 22 +++++++++------------- src/routes/balancer.route.js | 11 ----------- src/routes/index.route.js | 12 +----------- src/routes/terra.route.js | 11 ----------- src/services/access.js | 21 +++++++++++++++++++++ src/services/config.js | 3 ++- 6 files changed, 33 insertions(+), 47 deletions(-) create mode 100644 src/services/access.js diff --git a/src/app.js b/src/app.js index c65bc44..e1ac9a4 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 @@ -34,22 +35,17 @@ 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('/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/routes/balancer.route.js b/src/routes/balancer.route.js index 5b101aa..a2faabf 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -18,17 +18,6 @@ const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' const swapLessThanMaxPriceError = 'Swap price lower than maxPrice' -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.post('/', async (req, res) => { /* POST / diff --git a/src/routes/index.route.js b/src/routes/index.route.js index d69541d..ac1cc57 100644 --- a/src/routes/index.route.js +++ b/src/routes/index.route.js @@ -4,20 +4,10 @@ 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, status: 'ok', }); }) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index b29edaa..1831848 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -59,17 +59,6 @@ const connect = () => { return 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 }) - } -}) - router.get('/', async (req, res) => { /* GET / 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/config.js b/src/services/config.js index 1275dca..16543ad 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -11,7 +11,8 @@ export const getConfig = () => { REACT_APP_SUBGRAPH_URL: process.env.REACT_APP_SUBGRAPH_URL, EXCHANGE_PROXY: process.env.EXCHANGE_PROXY, GAS_PRICE: parseInt(process.env.GAS_PRICE), - GAS_LIMIT: parseInt(process.env.GAS_LIMIT) || 1200000 + GAS_LIMIT: parseInt(process.env.GAS_LIMIT) || 1200000, + APPROVAL_GAS_LIMIT: parseInt(process.env.APPROVAL_GAS_LIMIT) || 100000 }, ethereum: { ETHEREUM_RPC_URL: process.env.ETHEREUM_RPC_URL, From 647b867af2166634d09a4992f2dae22a9de554d3 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 12 Nov 2020 16:07:53 -0800 Subject: [PATCH 05/41] (feat) fixes #2557: makes ETH routes non Balancer specific --- src/index.js | 4 ++-- src/routes/eth.route.js | 29 ++++++++--------------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index 3ded6f3..ab5b30d 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ 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.BALANCER_NETWORK let certPath = process.env.CERT_PATH if ((typeof certPath === 'undefined' && certPath == null) || certPath === '') { @@ -79,4 +79,4 @@ 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, '| ethereum-chain:', ethereumChain); diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 4208e13..4c378d2 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -1,15 +1,12 @@ -import BigNumber from 'bignumber.js'; import { ethers } 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 separator = ',' const debug = require('debug')('router') @@ -38,7 +35,7 @@ router.post('/balances', async (req, res) => { } let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(seperator) + tokenAddressList = paramData.tokenAddressList.split(separator) } debug(tokenAddressList) @@ -72,6 +69,7 @@ router.post('/allowances', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddressList:{{tokenAddressList}} + spenderAddress:"0x....." } */ const initTime = Date.now() @@ -89,12 +87,12 @@ router.post('/allowances', async (req, res) => { }) return } - const spender = balancer.exchangeProxy let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(seperator) + tokenAddressList = paramData.tokenAddressList.split(separator) } debug(tokenAddressList) + const spender = paramData.spenderAddress const approvals = {} try { @@ -125,13 +123,13 @@ router.post('/approve', async (req, res) => { /* POST: /approve x-www-form-urlencoded: { - tokenAddress:"0x....." privateKey:{{privateKey}} + tokenAddress:"0x....." + spenderAddress:"0x....." amount:{{amount}} } */ const initTime = Date.now() - // params: privateKey (required), tokenAddress (required), amount (optional), gasPrice (required) const paramData = getParamData(req.body) const privateKey = paramData.privateKey let wallet @@ -147,7 +145,7 @@ router.post('/approve', async (req, res) => { return } const tokenAddress = paramData.tokenAddress - const spender = balancer.exchangeProxy + const spender = paramData.spenderAddress let amount paramData.amount ? amount = ethers.utils.parseEther(paramData.amount) : amount = ethers.utils.parseEther('1000000000') // approve for 1 billion units if no amount specified @@ -191,7 +189,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 +229,6 @@ router.post('/get-weth', async (req, res) => { message: err }) } - - // 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 - // }) - // }) }) module.exports = router; From 11375842ea085339cf64ebe7394fddb03db76fe3 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Fri, 13 Nov 2020 13:02:24 +0800 Subject: [PATCH 06/41] Update global config setting; Refactor Terra endpoints --- .env.example | 2 +- src/index.js | 5 +- src/routes/balancer.route.js | 8 +- src/routes/eth.route.js | 4 +- src/routes/terra.route.js | 363 +++++++++++++++++------------------ src/services/balancer.js | 9 +- src/services/config.js | 18 +- src/services/eth.js | 8 +- src/services/terra.js | 90 +++++++++ src/services/utils.js | 6 + 10 files changed, 308 insertions(+), 205 deletions(-) create mode 100644 src/services/terra.js diff --git a/.env.example b/.env.example index 6cdac29..958a19a 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,7 @@ IP_WHITELIST= # Terra TERRA_LCD_URL=https://tequila-lcd.terra.dev -TERRA_CHAIN=tequila-0004 +TERRA_NETWORK=tequila-0004 # Balancer # - network: mainnet, kovan, etc diff --git a/src/index.js b/src/index.js index 3ded6f3..e628b92 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ const env = process.env.NODE_ENV const port = process.env.PORT const certPassphrase = process.env.CERT_PASSPHRASE const balancerNetwork = process.env.BALANCER_NETWORK +const terraNetwork = process.env.TERRA_NETWORK 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(' - balancer-network:', balancerNetwork) +console.log(' - terra-network:', terraNetwork) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index a2faabf..c06d45d 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -11,8 +11,8 @@ import Balancer from '../services/balancer'; const debug = require('debug')('router') const router = express.Router() -const envConfig = getConfig() -const balancer = new Balancer(envConfig.balancer.BALANCER_NETWORK) +const ENV_CONFIG = getConfig() +const balancer = new Balancer(ENV_CONFIG.BALANCER.NETWORK) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' @@ -26,8 +26,8 @@ router.post('/', async (req, res) => { network: balancer.network, provider: balancer.provider.connection.url, exchangeProxy: balancer.exchangeProxy, - subgraphUrl: envConfig.balancer.REACT_APP_SUBGRAPH_URL, - gasLimit: envConfig.balancer.GAS_LIMIT, + subgraphUrl: balancer.subgraphUrl, + gasLimit: balancer.gasLimit, connection: true, timestamp: Date.now(), }) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index d3200f9..f5a745b 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -9,8 +9,8 @@ import Balancer from '../services/balancer'; const router = express.Router() const envConfig = getConfig() -const eth = new Ethereum(envConfig.balancer.BALANCER_NETWORK) -const balancer = new Balancer(envConfig.balancer.BALANCER_NETWORK) +const eth = new Ethereum(envConfig.BALANCER.NETWORK) +const balancer = new Balancer(envConfig.BALANCER.NETWORK) const seperator = ',' const debug = require('debug')('router') diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 1831848..d39878f 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -2,269 +2,268 @@ 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 { MnemonicKey, Coin, MsgSwap } from '@terra-money/terra.js' +import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getConfig } from '../services/config'; -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' }, -} +import Terra from '../services/terra'; -const getTerraSymbol = (denom) => { - let symbol - Object.keys(TerraTokens).forEach((item) => { - if (TerraTokens[item].denom === denom) { - symbol = item - } - }) - return symbol -} +const debug = require('debug')('router') +const router = express.Router(); +const ENV_CONFIG = getConfig() +const terra = new Terra(ENV_CONFIG.TERRA) +// constants +const network = terra.lcd.config.chainID 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; - -/** - * Connect to network - */ -const connect = () => { - const terra = new LCDClient({ - URL: lcdUrl, - chainID: chain, - }) - - terra.market.parameters().catch(() => { - throw new Error('Connection error') - }) - - return terra -} - -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" + "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 amount = parseFloat(paramData.amount) + debug('paramData', paramData) - let balance = {} - let txSuccess, message + const symbols = [baseToken, quoteToken] + let exchangeRate, price 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 + if (symbols.includes('LUNA')) { + const target = baseToken !== 'LUNA' ? baseToken : quoteToken + const denom = terra.getTokenDenom(target) + await terra.getExchangeRates(denom).then((rate) => { + price = exchangeRate * amount + }).catch((err) => { + reportConnectionError(res, err) }) - }) + } else { + // get the current swap rate + const offerDenom = terra.getTokenDenom(baseToken) + const swapDenom = terra.getTokenDenom(quoteToken) + + if ((typeof offerDenom === 'undefined' && offerDenom == null) || (typeof swapDenom === 'undefined' && swapDenom == null)) { + res.status(500).json({ + error: statusMessages.invalid_token_symbol, + message: { + base: baseToken, + quote: quoteToken + } + }) + return + } + + console.log('offerDenom, swapDenom', offerDenom, swapDenom) + + const offerCoin = new Coin(offerDenom, amount * denomUnitMultiplier); + await terra.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + price = parseFloat(swapCoin.amount) / denomUnitMultiplier + debug('price', price) + }).catch((err) => { + reportConnectionError(res, err) + }) + } + debug('price', price) + + res.status(200).json( + { + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseToken, + quote: quoteToken, + amount: amount, + exchangeRate: exchangeRate, + price: price + } + ) } 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" - "secret": "mysupersecret" + "base":"UST" + "quote":"KRT" + "trade_type":"buy" or "sell" + "amount":1 + "seeds": "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 initTime = Date.now() + + const paramData = getParamData(req.body) + const baseToken = paramData.base + const quoteToken = paramData.quote + const tradeType = paramData.trade_type + const amount = parseFloat(paramData.amount) + const seeds = paramData.seeds + // debug(paramData) - const terra = connect() const mk = new MnemonicKey({ - mnemonic: secret, + mnemonic: seeds, }); - const wallet = terra.wallet(mk); + const wallet = terra.lcd.wallet(mk); const address = wallet.key.accAddress + debug(address) // 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 + const baseDenom = terra.getTokenDenom(baseToken) + const quoteDenom = terra.getTokenDenom(quoteToken) - let offerDenom, swapDenom, swapAmount - swapAmount = paramData.amount * denomUnitMultiplier + let offerDenom, swapDenom if (tradeType === 'sell') { offerDenom = baseDenom swapDenom = quoteDenom } else { + // get equivalent of amount in return offerDenom = quoteDenom swapDenom = baseDenom } - const offerCoin = new Coin(offerDenom, swapAmount); - debug('base', offerDenom, 'quote', swapDenom) + const swapAmount = amount * denomUnitMultiplier + const offerCoin = new Coin(offerDenom, swapAmount) // Create and Sign Transaction const swap = new MsgSwap(address, offerCoin, swapDenom); - const memo = 'tx: 0802...1520' - - let txSuccess, txAttributes, message + const testnetMemo = 'tx: 0xhb034' + const memo = network.toLowerCase().includes('columbus') ? '' : testnetMemo + let txAttributes try { const tx = await wallet.createAndSignTx({ msgs: [swap], memo: memo - }).then(tx => terra.tx.broadcast(tx)).then(result => { + }).then(tx => terra.lcd.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 - } + txAttributes = terra.getTxAttributes(swap.attributes) + const buyCoin = Coin.fromString(txAttributes.swap_coin).toDecCoin() + const sellCoin = Coin.fromString(txAttributes.offer) + // const feeCoin = Coin.fromString(txAttributes.swap_fee) + + res.status(200).json( + { + network: network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseToken, + quote: quoteToken, + tradeType: tradeType, + amount: amount, + buy: buyCoin.amount / denomUnitMultiplier, + sell: sellCoin.amount / denomUnitMultiplier, + // fee: feeCoin.amount / denomUnitMultiplier, + txHash: txHash + } + ) }) } 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/services/balancer.js b/src/services/balancer.js index 561b8a2..7d88571 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -15,11 +15,12 @@ const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; export default class Balancer { constructor (network = 'kovan') { // network defaults to kovan - const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL + const providerUrl = ENV_CONFIG.ETHEREUM.RPC_URL this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.network = ENV_CONFIG.balancer.BALANCER_NETWORK - this.exchangeProxy = ENV_CONFIG.balancer.EXCHANGE_PROXY - this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT + this.network = ENV_CONFIG.BALANCER.NETWORK + this.subgraphUrl = ENV_CONFIG.BALANCER.REACT_APP_SUBGRAPH_URL + this.exchangeProxy = ENV_CONFIG.BALANCER.EXCHANGE_PROXY + this.gasLimit = ENV_CONFIG.BALANCER.GAS_LIMIT if (network === 'kovan') { // this.erc20Tokens = JSON.parse(fs.readFileSync('src/static/erc20_tokens_kovan.json')) diff --git a/src/services/config.js b/src/services/config.js index 16543ad..32cbc90 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -2,20 +2,24 @@ require('dotenv').config() export const getConfig = () => { const env = { - terra: { - TERRA_LCD_URL: process.env.TERRA_LCD_URL, - TERRA_CHAIN: process.env.TERRA_LCD_URL, + TERRA: { + LCD_URL: process.env.TERRA_LCD_URL, + NETWORK: process.env.TERRA_NETWORK, }, - balancer: { - BALANCER_NETWORK: process.env.BALANCER_NETWORK, + BALANCER: { + NETWORK: process.env.BALANCER_NETWORK, REACT_APP_SUBGRAPH_URL: process.env.REACT_APP_SUBGRAPH_URL, EXCHANGE_PROXY: process.env.EXCHANGE_PROXY, GAS_PRICE: parseInt(process.env.GAS_PRICE), GAS_LIMIT: parseInt(process.env.GAS_LIMIT) || 1200000, APPROVAL_GAS_LIMIT: parseInt(process.env.APPROVAL_GAS_LIMIT) || 100000 }, - ethereum: { - ETHEREUM_RPC_URL: process.env.ETHEREUM_RPC_URL, + ETHEREUM: { + RPC_URL: process.env.ETHEREUM_RPC_URL, + }, + uniswap: { + }, + celo: { } } return env diff --git a/src/services/eth.js b/src/services/eth.js index 0cafdd3..cd40e45 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -11,11 +11,11 @@ const ENV_CONFIG = config.getConfig() export default class Ethereum { constructor (network = 'kovan') { // network defaults to kovan - const providerUrl = ENV_CONFIG.ethereum.ETHEREUM_RPC_URL + const providerUrl = ENV_CONFIG.ETHEREUM.RPC_URL this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.network = ENV_CONFIG.balancer.BALANCER_NETWORK - this.gasLimit = ENV_CONFIG.balancer.GAS_LIMIT - this.approvalGasLimit = ENV_CONFIG.balancer.APPROVAL_GAS_LIMIT + this.network = ENV_CONFIG.BALANCER.NETWORK + this.gasLimit = ENV_CONFIG.BALANCER.GAS_LIMIT + this.approvalGasLimit = ENV_CONFIG.BALANCER.APPROVAL_GAS_LIMIT if (network === 'kovan') { // for kovan testing only diff --git a/src/services/terra.js b/src/services/terra.js new file mode 100644 index 0000000..d1c3b40 --- /dev/null +++ b/src/services/terra.js @@ -0,0 +1,90 @@ +import { LCDClient } from '@terra-money/terra.js' +import { reportConnectionError, statusMessages } from '../services/utils'; + +require('dotenv').config() +const debug = require('debug')('router') +const config = require('../services/config') + +// constants +const ENV_CONFIG = config.getConfig() +const TERRA_TOKENS = { + uluna: { symbol: 'LUNA' }, + uusd: { symbol: 'UST' }, + ukrw: { symbol: 'KRT' }, + usdr: { symbol: 'SDT' }, + umnt: { symbol: 'MNT' }, +} + +export default class Terra { + constructor () { + const lcdUrl = ENV_CONFIG.TERRA.LCD_URL; + const network = ENV_CONFIG.TERRA.NETWORK; + + this.tokens = TERRA_TOKENS + + try { + this.lcd = new LCDClient({ + URL: lcdUrl, + chainID: network, + }) + + this.lcd.market.parameters().catch(() => { + throw new Error('Connection error') + }) + } catch (err) { + throw Error(`Connection failed: ${network}`) + } + } + + // 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 getExchangeRates (denom) { + try { + const exchangeRates = await this.lcd.oracle.exchangeRates() + debug('exchangeRates', 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 + } + } +} diff --git a/src/services/utils.js b/src/services/utils.js index 4095936..58883de 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -8,6 +8,7 @@ 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', } export const latency = (startTime, endTime) => parseFloat((endTime - startTime) / 1000) @@ -45,6 +46,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 = { From 463440580936cb38f6973387f2283ac619672304 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Fri, 13 Nov 2020 14:02:31 +0800 Subject: [PATCH 07/41] Update secret param, Add postman collection & environment --- .gitignore | 1 - src/routes/terra.route.js | 6 +- .../Gateway-Terra.postman_collection.json | 323 ++++++++++++++++++ test/postman/terra.postman_environment.json | 29 ++ 4 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 test/postman/Gateway-Terra.postman_collection.json create mode 100644 test/postman/terra.postman_environment.json 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/src/routes/terra.route.js b/src/routes/terra.route.js index d39878f..e085c0a 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -173,7 +173,7 @@ router.post('/trade', async (req, res) => { "quote":"KRT" "trade_type":"buy" or "sell" "amount":1 - "seeds": "mysupersecret" + "secret": "mysupersecret" } */ const initTime = Date.now() @@ -183,11 +183,11 @@ router.post('/trade', async (req, res) => { const quoteToken = paramData.quote const tradeType = paramData.trade_type const amount = parseFloat(paramData.amount) - const seeds = paramData.seeds + const secret = paramData.secret // debug(paramData) const mk = new MnemonicKey({ - mnemonic: seeds, + mnemonic: secret, }); const wallet = terra.lcd.wallet(mk); const address = wallet.key.accAddress diff --git a/test/postman/Gateway-Terra.postman_collection.json b/test/postman/Gateway-Terra.postman_collection.json new file mode 100644 index 0000000..9436113 --- /dev/null +++ b/test/postman/Gateway-Terra.postman_collection.json @@ -0,0 +1,323 @@ +{ + "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": "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 From f3d508ab7515d0b391f8cc995c2c388ca9229f8f Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 14 Nov 2020 18:46:49 +0800 Subject: [PATCH 08/41] Return token swap info in price & trade. Refactor route functions --- src/routes/terra.route.js | 137 ++++++++--------------------- src/services/terra.js | 178 +++++++++++++++++++++++++++++++++++--- 2 files changed, 203 insertions(+), 112 deletions(-) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index e085c0a..580a906 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -1,12 +1,12 @@ 'use strict' import express from 'express' -import BigNumber from 'bignumber.js' -import { MnemonicKey, Coin, MsgSwap } from '@terra-money/terra.js' +import { MnemonicKey, Coin, MsgSwap, isTxError } from '@terra-money/terra.js' import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; import { getConfig } from '../services/config'; import Terra from '../services/terra'; +import e from 'express'; const debug = require('debug')('router') const router = express.Router(); @@ -15,7 +15,7 @@ const terra = new Terra(ENV_CONFIG.TERRA) // constants const network = terra.lcd.config.chainID -const denomUnitMultiplier = BigNumber('1e+6') +const denomUnitMultiplier = terra.denomUnitMultiplier router.post('/', async (req, res) => { /* @@ -84,6 +84,7 @@ router.post('/price', async (req, res) => { x-www-form-urlencoded: { "base":"UST" "quote":"KRT" + "trade_type":"buy" or "sell" "amount":1 } */ @@ -92,48 +93,21 @@ router.post('/price', async (req, res) => { const paramData = getParamData(req.body) const baseToken = paramData.base const quoteToken = paramData.quote + const tradeType = paramData.trade_type const amount = parseFloat(paramData.amount) debug('paramData', paramData) const symbols = [baseToken, quoteToken] - let exchangeRate, price + let exchangeRate try { - if (symbols.includes('LUNA')) { - const target = baseToken !== 'LUNA' ? baseToken : quoteToken - const denom = terra.getTokenDenom(target) - await terra.getExchangeRates(denom).then((rate) => { - price = exchangeRate * amount - }).catch((err) => { - reportConnectionError(res, err) - }) - } else { - // get the current swap rate - const offerDenom = terra.getTokenDenom(baseToken) - const swapDenom = terra.getTokenDenom(quoteToken) - - if ((typeof offerDenom === 'undefined' && offerDenom == null) || (typeof swapDenom === 'undefined' && swapDenom == null)) { - res.status(500).json({ - error: statusMessages.invalid_token_symbol, - message: { - base: baseToken, - quote: quoteToken - } - }) - return - } - - console.log('offerDenom, swapDenom', offerDenom, swapDenom) + await terra.getSwapRate(baseToken, quoteToken, amount, tradeType).then((rate) => { + exchangeRate = rate + }).catch((err) => { + reportConnectionError(res, err) + }) - const offerCoin = new Coin(offerDenom, amount * denomUnitMultiplier); - await terra.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { - price = parseFloat(swapCoin.amount) / denomUnitMultiplier - debug('price', price) - }).catch((err) => { - reportConnectionError(res, err) - }) - } - debug('price', price) + debug('exchangeRate', exchangeRate) res.status(200).json( { @@ -143,8 +117,11 @@ router.post('/price', async (req, res) => { base: baseToken, quote: quoteToken, amount: amount, - exchangeRate: exchangeRate, - price: price + tradeType: tradeType, + swapIn: exchangeRate.swapIn, + swapOut: exchangeRate.swapOut, + tobinTax: terra.tobinTax, + minSpread: terra.minSpread } ) } catch (err) { @@ -184,70 +161,30 @@ router.post('/trade', async (req, res) => { const tradeType = paramData.trade_type const amount = parseFloat(paramData.amount) const secret = paramData.secret - // debug(paramData) - const mk = new MnemonicKey({ - mnemonic: secret, - }); - const wallet = terra.lcd.wallet(mk); - const address = wallet.key.accAddress - debug(address) - - // get the current swap rate - const baseDenom = terra.getTokenDenom(baseToken) - const quoteDenom = terra.getTokenDenom(quoteToken) - - let offerDenom, swapDenom - if (tradeType === 'sell') { - offerDenom = baseDenom - swapDenom = quoteDenom - } else { - // get equivalent of amount in return - offerDenom = quoteDenom - swapDenom = baseDenom - } - - const swapAmount = amount * denomUnitMultiplier - const offerCoin = new Coin(offerDenom, swapAmount) - - // Create and Sign Transaction - const swap = new MsgSwap(address, offerCoin, swapDenom); - const testnetMemo = 'tx: 0xhb034' - const memo = network.toLowerCase().includes('columbus') ? '' : testnetMemo - let txAttributes + let tokenSwaps try { - const tx = await wallet.createAndSignTx({ - msgs: [swap], - memo: memo - }).then(tx => terra.lcd.tx.broadcast(tx)).then(result => { - debug(`TX hash: ${result.txhash}`); - const txHash = result.txhash - const events = JSON.parse(result.raw_log)[0].events - const swap = events.find(obj => { - return obj.type === 'swap' - }) - txAttributes = terra.getTxAttributes(swap.attributes) - const buyCoin = Coin.fromString(txAttributes.swap_coin).toDecCoin() - const sellCoin = Coin.fromString(txAttributes.offer) - // const feeCoin = Coin.fromString(txAttributes.swap_fee) - - res.status(200).json( - { - network: network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - base: baseToken, - quote: quoteToken, - tradeType: tradeType, - amount: amount, - buy: buyCoin.amount / denomUnitMultiplier, - sell: sellCoin.amount / denomUnitMultiplier, - // fee: feeCoin.amount / denomUnitMultiplier, - txHash: txHash - } - ) + await terra.swapTokens(baseToken, quoteToken, amount, tradeType, 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 + } + debug('tokenSwaps', tokenSwaps) + Object.assign(swapResult, tokenSwaps); + res.status(200).json( + swapResult + ) } catch (err) { let message let reason diff --git a/src/services/terra.js b/src/services/terra.js index d1c3b40..14dbf8d 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -1,5 +1,5 @@ -import { LCDClient } from '@terra-money/terra.js' -import { reportConnectionError, statusMessages } from '../services/utils'; +import { LCDClient, Coin, MsgSwap, MnemonicKey, isTxError } from '@terra-money/terra.js' +import BigNumber from 'bignumber.js' require('dotenv').config() const debug = require('debug')('router') @@ -14,25 +14,44 @@ const TERRA_TOKENS = { usdr: { symbol: 'SDT' }, umnt: { symbol: 'MNT' }, } +const DENOM_UNIT = BigNumber('1e+6') +const TOBIN_TAX = 0.25 +const MIN_SPREAD = 2.0 +const TESTNET_MEMO = '' export default class Terra { constructor () { - const lcdUrl = ENV_CONFIG.TERRA.LCD_URL; - const network = ENV_CONFIG.TERRA.NETWORK; - + this.lcdUrl = ENV_CONFIG.TERRA.LCD_URL; + this.network = ENV_CONFIG.TERRA.NETWORK; this.tokens = TERRA_TOKENS + this.denomUnitMultiplier = DENOM_UNIT + this.tobinTax = TOBIN_TAX + this.minSpread = MIN_SPREAD try { - this.lcd = new LCDClient({ - URL: lcdUrl, - chainID: network, - }) + this.lcd = this.connect() this.lcd.market.parameters().catch(() => { throw new Error('Connection error') }) } catch (err) { - throw Error(`Connection failed: ${network}`) + throw Error(`Connection failed: ${this.network}`) + } + } + + // connect Terra LCD + connect () { + try { + const lcd = new LCDClient({ + URL: this.lcdUrl, + chainID: this.network, + }) + return lcd + } catch (err) { + let reason + console.log(reason) + err.reason ? reason = err.reason : reason = 'error Terra LCD connect' + return reason } } @@ -75,10 +94,9 @@ export default class Terra { return attrib } - async getExchangeRates (denom) { + async getExchangeRate (denom) { try { const exchangeRates = await this.lcd.oracle.exchangeRates() - debug('exchangeRates', exchangeRates) return exchangeRates.get(denom) } catch (err) { let reason @@ -87,4 +105,140 @@ export default class Terra { return reason } } + + // get Terra Swap Rate + async getSwapRate (baseToken, quoteToken, amount, tradeType) { + try { + let exchangeRate + let swaps = {} + const offerDenom = this.getTokenDenom(baseToken) + const swapDenom = this.getTokenDenom(quoteToken) + + const offerCoin = new Coin(offerDenom, amount * DENOM_UNIT); + await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + exchangeRate = { + amount: parseFloat(swapCoin.amount) / DENOM_UNIT, + token: quoteToken + } + }) + + if (tradeType.toLowerCase() === 'buy') { + swaps.swapIn = { + amount: amount, + token: baseToken + } + swaps.swapOut = exchangeRate + } else { + // sell + swaps.swapIn = exchangeRate + swaps.swapOut = { + amount: amount, + token: baseToken + } + } + return swaps + } catch (err) { + debug() + 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, secret) { + 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 + let 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 = swaps.swapOut.amount * DENOM_UNIT + const offerCoin = new Coin(offerDenom, offerAmount) + + // Create and Sign Transaction + const msgSwap = new MsgSwap(address, offerCoin, swapDenom); + const memo = lcd.config.chainID.toLowerCase().includes('columbus') ? '' : TESTNET_MEMO + + const tx = await wallet.createAndSignTx({ + msgs: [msgSwap], + memo: memo + }) + + await lcd.tx.broadcast(tx).then((txResult) => { + const swapSucces = !isTxError(txResult) + if (swapSucces) { + tokenSwap.txSuccess = swapSucces + } else { + tokenSwap.txSuccess = !swapSucces + throw new Error(`encountered an error while running the transaction: ${txResult.code} ${txResult.codespace}`); + } + + // check for events from the first message + // debug('event first message', txResult.logs[0].eventsByType.store_code) + + 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.swapIn = { + amount: parseFloat(ask.amount) / DENOM_UNIT, + token: TERRA_TOKENS[ask.denom].symbol + } + tokenSwap.swapOut = { + amount: parseFloat(offer.amount) / DENOM_UNIT, + token: TERRA_TOKENS[offer.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(reason) + err.reason ? reason = err.reason : reason = 'error Terra tokens swap' + return { txSuccess: false, message: reason } + } + } } From bf51849072617de6ee7bf449b24c99bf09eeff4b Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 14 Nov 2020 19:01:45 +0800 Subject: [PATCH 09/41] Add trade_type to price endpoint param --- test/postman/Gateway-Terra.postman_collection.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/postman/Gateway-Terra.postman_collection.json b/test/postman/Gateway-Terra.postman_collection.json index 9436113..40866b8 100644 --- a/test/postman/Gateway-Terra.postman_collection.json +++ b/test/postman/Gateway-Terra.postman_collection.json @@ -61,6 +61,11 @@ "value": "KRT", "type": "text" }, + { + "key": "trade_type", + "value": "buy", + "type": "text" + }, { "key": "amount", "value": "3", From a43ef33ce1d3842e7aa89e41d6dd782a03dba77c Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sun, 15 Nov 2020 23:13:33 +0800 Subject: [PATCH 10/41] (fix) price look up error. --- src/routes/terra.route.js | 23 ++++-- src/services/terra.js | 170 ++++++++++++++++++++++++++------------ 2 files changed, 136 insertions(+), 57 deletions(-) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 580a906..04a5951 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -118,10 +118,18 @@ router.post('/price', async (req, res) => { quote: quoteToken, amount: amount, tradeType: tradeType, - swapIn: exchangeRate.swapIn, - swapOut: exchangeRate.swapOut, - tobinTax: terra.tobinTax, - minSpread: terra.minSpread + price: { + amount: exchangeRate.price.amount, + token: exchangeRate.price.token + }, + cost: { + amount: exchangeRate.cost.amount, + token: exchangeRate.cost.token + } + // swapIn: exchangeRate.swapIn, + // swapOut: exchangeRate.swapOut, + // tobinTax: terra.tobinTax, + // minSpread: terra.minSpread } ) } catch (err) { @@ -160,12 +168,14 @@ router.post('/trade', async (req, res) => { const quoteToken = paramData.quote const tradeType = paramData.trade_type const amount = parseFloat(paramData.amount) + const gasPrice = parseFloat(paramData.gas_price) || null + const gasAdjustment = paramData.gas_adjustment || null const secret = paramData.secret let tokenSwaps try { - await terra.swapTokens(baseToken, quoteToken, amount, tradeType, secret).then((swap) => { + await terra.swapTokens(baseToken, quoteToken, amount, tradeType, gasPrice, gasAdjustment, secret).then((swap) => { tokenSwaps = swap }).catch((err) => { reportConnectionError(res, err) @@ -178,7 +188,8 @@ router.post('/trade', async (req, res) => { base: baseToken, tradeType: tradeType, quote: quoteToken, - amount: amount + amount: amount, + // gasPrice: gasPrice } debug('tokenSwaps', tokenSwaps) Object.assign(swapResult, tokenSwaps); diff --git a/src/services/terra.js b/src/services/terra.js index 14dbf8d..e9630f0 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -15,9 +15,10 @@ const TERRA_TOKENS = { umnt: { symbol: 'MNT' }, } const DENOM_UNIT = BigNumber('1e+6') -const TOBIN_TAX = 0.25 -const MIN_SPREAD = 2.0 -const TESTNET_MEMO = '' +const TOBIN_TAX = 0.25 // a Tobin Tax (set at 0.25%) for spot-converting Terra<>Terra swaps +const MIN_SPREAD = 2.0 // a minimum spread (set at 2%) for Terra<>Luna swaps +const GAS_ADJUSTMENT = 1.5 +const TESTNET_MEMO = '0xhb34' export default class Terra { constructor () { @@ -85,7 +86,7 @@ export default class Terra { return reason } } - + getTxAttributes (attributes) { let attrib = {} attributes.forEach((item) => { @@ -94,6 +95,18 @@ export default class Terra { 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() @@ -109,36 +122,48 @@ export default class Terra { // get Terra Swap Rate async getSwapRate (baseToken, quoteToken, amount, tradeType) { try { - let exchangeRate + let exchangeRate, offerDenom, swapDenom, cost let swaps = {} - const offerDenom = this.getTokenDenom(baseToken) - const swapDenom = this.getTokenDenom(quoteToken) - - const offerCoin = new Coin(offerDenom, amount * DENOM_UNIT); - await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { - exchangeRate = { - amount: parseFloat(swapCoin.amount) / DENOM_UNIT, - token: quoteToken - } - }) - if (tradeType.toLowerCase() === 'buy') { - swaps.swapIn = { - amount: amount, - token: baseToken - } - swaps.swapOut = exchangeRate + if (tradeType.toLowerCase() === 'sell') { + // sell base + offerDenom = this.getTokenDenom(baseToken) + swapDenom = this.getTokenDenom(quoteToken) + + const offerCoin = new Coin(offerDenom, amount * DENOM_UNIT); + await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + exchangeRate = { + amount: (swapCoin.amount / DENOM_UNIT) / amount, + token: quoteToken + } + cost = { + amount: amount * exchangeRate.amount, + token: quoteToken + } + }) } else { - // sell - swaps.swapIn = exchangeRate - swaps.swapOut = { - amount: amount, - token: baseToken - } + // buy base + offerDenom = this.getTokenDenom(quoteToken) + swapDenom = this.getTokenDenom(baseToken) + + const offerCoin = new Coin(offerDenom, 1 * DENOM_UNIT); + await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { + exchangeRate = { + amount: (amount / swapCoin.amount * DENOM_UNIT) / amount, // adjusted amount + token: quoteToken + } + cost = { + amount: amount * exchangeRate.amount, + token: quoteToken + } + }) } + + swaps.price = exchangeRate + swaps.cost = cost + debug('swaps', swaps) return swaps } catch (err) { - debug() let reason console.log(reason) err.reason ? reason = err.reason : reason = 'error swap rate lookup' @@ -147,7 +172,8 @@ export default class Terra { } // Swap tokens - async swapTokens (baseToken, quoteToken, amount, tradeType, secret) { + async swapTokens (baseToken, quoteToken, amount, tradeType, gasPrice, gasAdjustment, secret) { + let swapResult try { // connect to lcd const lcd = this.connect() @@ -169,8 +195,7 @@ export default class Terra { const quoteDenom = this.getTokenDenom(quoteToken) let offerDenom, swapDenom - let swaps - let txAttributes + let swaps, txAttributes let tokenSwap = {} if (tradeType.toLowerCase() === 'sell') { @@ -185,30 +210,56 @@ export default class Terra { swaps = rate }) - const offerAmount = swaps.swapOut.amount * DENOM_UNIT - const offerCoin = new Coin(offerDenom, offerAmount) + const offerAmount = parseInt(swaps.cost.amount * DENOM_UNIT) + const offerCoin = new Coin(offerDenom, offerAmount) + debug('offerCoin', offerCoin, offerAmount) // Create and Sign Transaction const msgSwap = new MsgSwap(address, offerCoin, swapDenom); const memo = lcd.config.chainID.toLowerCase().includes('columbus') ? '' : TESTNET_MEMO - const tx = await wallet.createAndSignTx({ - msgs: [msgSwap], - memo: memo - }) + debug('msgSwap', msgSwap) + let txOptions + if (gasPrice !== null && gasPrice !== null) { // ignore gasAdjustment when gasPrice is not set + txOptions = { + msgs: [msgSwap], + gasPrices: { [offerDenom]: gasPrice }, + gasAdjustment: gasAdjustment || GAS_ADJUSTMENT, + memo: memo + } + } else { + txOptions = { + msgs: [msgSwap], + memo: memo + } + } + + // const fee = await this.getEstimateFee(tx) + // const estimatedGas = fee.gas + // const estimatedFee = fee.amount._coins[offerDenom] + // const estimatedFeeDenom = estimatedFee.denom + // const estimatedFeeAmount = estimatedFee.amount * DENOM_UNIT + // debug('estimatedGas/estimatedFeeDenom/estimatedFeeAmount', estimatedGas, estimatedFee) + // debug(estimatedFeeDenom, estimatedFeeAmount) + + // debug('offerDenom', offerDenom, 'gasPrice', gasPrice, 'gasAdjustment', gasAdjustment, 'options', txOptions) + + await wallet.createAndSignTx(txOptions).then(tx => lcd.tx.broadcast(tx)).then((txResult) => { + swapResult = txResult + // debug('txResult', txResult) - await lcd.tx.broadcast(tx).then((txResult) => { - const swapSucces = !isTxError(txResult) - if (swapSucces) { - tokenSwap.txSuccess = swapSucces + const swapSuccess = !isTxError(txResult) + if (swapSuccess) { + tokenSwap.txSuccess = swapSuccess } else { - tokenSwap.txSuccess = !swapSucces + tokenSwap.txSuccess = !swapSuccess throw new Error(`encountered an error while running the transaction: ${txResult.code} ${txResult.codespace}`); } // check for events from the first message // debug('event first message', txResult.logs[0].eventsByType.store_code) + // debug('swapSuccess', swapSuccess) const txHash = txResult.txhash const events = JSON.parse(txResult.raw_log)[0].events const swap = events.find(obj => { @@ -219,25 +270,42 @@ export default class Terra { const ask = Coin.fromString(txAttributes.swap_coin) const fee = Coin.fromString(txAttributes.swap_fee) - tokenSwap.swapIn = { - amount: parseFloat(ask.amount) / DENOM_UNIT, - token: TERRA_TOKENS[ask.denom].symbol - } - tokenSwap.swapOut = { - amount: parseFloat(offer.amount) / DENOM_UNIT, - token: TERRA_TOKENS[offer.denom].symbol + debug('txAttributes', txAttributes) + + if (tradeType.toLowerCase() === 'sell') { + tokenSwap.expectedOut = { + amount: parseFloat(ask.amount) / DENOM_UNIT, + token: TERRA_TOKENS[ask.denom].symbol + } + tokenSwap.price = { + amount: parseFloat(offer.amount) / DENOM_UNIT, + token: TERRA_TOKENS[offer.denom].symbol + } + } else { + tokenSwap.expectedIn = { + amount: parseFloat(ask.amount) / DENOM_UNIT, + token: TERRA_TOKENS[ask.denom].symbol + } + tokenSwap.price = { + amount: parseFloat(offer.amount) / DENOM_UNIT, + token: TERRA_TOKENS[offer.denom].symbol + } } tokenSwap.fee = { amount: parseFloat(fee.amount) / DENOM_UNIT, token: TERRA_TOKENS[fee.denom].symbol } + // tokenSwap.estimatedFee = { + // amount: estimatedFeeAmount, + // token: + // }, tokenSwap.txHash = txHash }) return tokenSwap } catch (err) { let reason - console.log(reason) - err.reason ? reason = err.reason : reason = 'error Terra tokens swap' + console.log(reason, typeof result) + err.reason ? reason = err.reason : reason = swapResult return { txSuccess: false, message: reason } } } From 2f7eaefd6e41e80781d189b4b44da92574b44975 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sun, 15 Nov 2020 23:20:40 +0800 Subject: [PATCH 11/41] (fix) Remove token info from price lookup --- src/routes/terra.route.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 04a5951..a6184b0 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -118,14 +118,8 @@ router.post('/price', async (req, res) => { quote: quoteToken, amount: amount, tradeType: tradeType, - price: { - amount: exchangeRate.price.amount, - token: exchangeRate.price.token - }, - cost: { - amount: exchangeRate.cost.amount, - token: exchangeRate.cost.token - } + price: exchangeRate.price.amount, + cost: exchangeRate.cost.amount // swapIn: exchangeRate.swapIn, // swapOut: exchangeRate.swapOut, // tobinTax: terra.tobinTax, From 8fcc13d444c4df0aa18d6470c1348c2c2d1b4bee Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 16 Nov 2020 01:50:01 +0800 Subject: [PATCH 12/41] (fix) incorrect sell trade offer amount --- src/routes/terra.route.js | 8 ++------ src/services/terra.js | 38 +++++++++++++++----------------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index a6184b0..62285e1 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -107,7 +107,7 @@ router.post('/price', async (req, res) => { reportConnectionError(res, err) }) - debug('exchangeRate', exchangeRate) + // debug('exchangeRate', exchangeRate) res.status(200).json( { @@ -120,10 +120,6 @@ router.post('/price', async (req, res) => { tradeType: tradeType, price: exchangeRate.price.amount, cost: exchangeRate.cost.amount - // swapIn: exchangeRate.swapIn, - // swapOut: exchangeRate.swapOut, - // tobinTax: terra.tobinTax, - // minSpread: terra.minSpread } ) } catch (err) { @@ -185,7 +181,7 @@ router.post('/trade', async (req, res) => { amount: amount, // gasPrice: gasPrice } - debug('tokenSwaps', tokenSwaps) + // debug('tokenSwaps', tokenSwaps) Object.assign(swapResult, tokenSwaps); res.status(200).json( swapResult diff --git a/src/services/terra.js b/src/services/terra.js index e9630f0..6b313a0 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -122,7 +122,7 @@ export default class Terra { // get Terra Swap Rate async getSwapRate (baseToken, quoteToken, amount, tradeType) { try { - let exchangeRate, offerDenom, swapDenom, cost + let exchangeRate, offerDenom, swapDenom, cost, offer let swaps = {} if (tradeType.toLowerCase() === 'sell') { @@ -132,6 +132,7 @@ export default class Terra { const 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 @@ -156,12 +157,14 @@ export default class Terra { amount: amount * exchangeRate.amount, token: quoteToken } + offer = { amount: cost.amount } }) } + swaps.offer = offer swaps.price = exchangeRate swaps.cost = cost - debug('swaps', swaps) + // debug('swaps', swaps) return swaps } catch (err) { let reason @@ -210,7 +213,7 @@ export default class Terra { swaps = rate }) - const offerAmount = parseInt(swaps.cost.amount * DENOM_UNIT) + const offerAmount = parseInt(swaps.offer.amount * DENOM_UNIT) const offerCoin = new Coin(offerDenom, offerAmount) debug('offerCoin', offerCoin, offerAmount) @@ -218,7 +221,7 @@ export default class Terra { const msgSwap = new MsgSwap(address, offerCoin, swapDenom); const memo = lcd.config.chainID.toLowerCase().includes('columbus') ? '' : TESTNET_MEMO - debug('msgSwap', msgSwap) + // debug('msgSwap', msgSwap) let txOptions if (gasPrice !== null && gasPrice !== null) { // ignore gasAdjustment when gasPrice is not set txOptions = { @@ -270,26 +273,15 @@ export default class Terra { const ask = Coin.fromString(txAttributes.swap_coin) const fee = Coin.fromString(txAttributes.swap_fee) - debug('txAttributes', txAttributes) + // debug('txAttributes', txAttributes) - if (tradeType.toLowerCase() === 'sell') { - tokenSwap.expectedOut = { - amount: parseFloat(ask.amount) / DENOM_UNIT, - token: TERRA_TOKENS[ask.denom].symbol - } - tokenSwap.price = { - amount: parseFloat(offer.amount) / DENOM_UNIT, - token: TERRA_TOKENS[offer.denom].symbol - } - } else { - tokenSwap.expectedIn = { - amount: parseFloat(ask.amount) / DENOM_UNIT, - token: TERRA_TOKENS[ask.denom].symbol - } - tokenSwap.price = { - amount: parseFloat(offer.amount) / DENOM_UNIT, - token: TERRA_TOKENS[offer.denom].symbol - } + 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, From 991f0201013ca0ae9771c3038e4ce101e0e8a152 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Sun, 15 Nov 2020 19:55:54 -0800 Subject: [PATCH 13/41] (fix) add approve and allowances to balancer routes --- src/routes/balancer.route.js | 114 +++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 8880861..4a1ef10 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -16,6 +16,7 @@ const eth = new Ethereum(process.env.BALANCER_NETWORK) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' const swapLessThanMaxPriceError = 'Swap price lower than maxPrice' +const separator = ',' router.use((req, res, next) => { const cert = req.connection.getPeerCertificate() @@ -313,4 +314,117 @@ router.post('/buy', async (req, res) => { } }) +router.post('/allowances', async (req, res) => { + /* + POST: /allowances + x-www-form-urlencoded: { + privateKey:{{privateKey}} + tokenAddressList:{{tokenAddressList}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, eth.provider) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + return + } + let tokenAddressList + if (paramData.tokenAddressList) { + tokenAddressList = paramData.tokenAddressList.split(separator) + } + debug(tokenAddressList) + const spender = balancer.exchangeProxy + + const approvals = {} + try { + Promise.all( + tokenAddressList.map(async (key) => + approvals[key] = await eth.getERC20Allowance(wallet, spender, key) + )).then(() => { + res.status(200).json({ + network: eth.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + spender: spender, + approvals: approvals, + }) + } + ) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + +router.post('/approve', async (req, res) => { + /* + POST: /approve + x-www-form-urlencoded: { + privateKey:{{privateKey}} + tokenAddress:"0x....." + amount:{{amount}} + } + */ + const initTime = Date.now() + const paramData = getParamData(req.body) + const privateKey = paramData.privateKey + let wallet + try { + wallet = new ethers.Wallet(privateKey, eth.provider) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = 'Error getting wallet' + res.status(500).json({ + error: reason, + message: err + }) + 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 gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } + + try { + // call approve function + const approval = await eth.approveERC20(wallet, spender, tokenAddress, amount, gasPrice) + + // submit response + res.status(200).json({ + network: eth.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + tokenAddress: tokenAddress, + spender: spender, + amount: amount / 1e18.toString(), + approval: approval + }) + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).json({ + error: reason, + message: err + }) + } +}) + export default router; From 23c30b3fd4a23d483350efd724428f1ddee86422 Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 16 Nov 2020 10:19:45 +0100 Subject: [PATCH 14/41] (feat) add uniswap gateway --- package.json | 1 + src/app.js | 2 + src/index.js | 3 +- src/routes/uniswap.route.js | 308 ++++ src/services/uniswap.js | 72 + src/static/uniswap_v2_router_abi.json | 1924 +++++++++++++++++++++++++ 6 files changed, 2309 insertions(+), 1 deletion(-) create mode 100644 src/routes/uniswap.route.js create mode 100644 src/services/uniswap.js create mode 100644 src/static/uniswap_v2_router_abi.json diff --git a/package.json b/package.json index aa299db..f13c24f 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/src/app.js b/src/app.js index c65bc44..a98af7b 100644 --- a/src/app.js +++ b/src/app.js @@ -11,6 +11,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(); @@ -40,6 +41,7 @@ app.use('/eth', ethRoutes); // app.use('/celo', celoRoutes); app.use('/terra', terraRoutes); app.use('/balancer', balancerRoutes); +app.use('/uniswap', uniswapRoutes); app.get('/', (req, res, next) => { const cert = req.connection.getPeerCertificate() diff --git a/src/index.js b/src/index.js index 3ded6f3..ffb6abe 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ const env = process.env.NODE_ENV const port = process.env.PORT const certPassphrase = process.env.CERT_PASSPHRASE const balancerNetwork = process.env.BALANCER_NETWORK +const uniswapNetwork = process.env.UNISWAP_NETWORK let certPath = process.env.CERT_PATH if ((typeof certPath === 'undefined' && certPath == null) || certPath === '') { @@ -79,4 +80,4 @@ 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, '| balancer-network:', balancerNetwork, '| uniswap-network:', uniswapNetwork); diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js new file mode 100644 index 0000000..ff59fce --- /dev/null +++ b/src/routes/uniswap.route.js @@ -0,0 +1,308 @@ +import BigNumber from 'bignumber.js'; +import { ethers } from 'ethers'; +import express from 'express'; + +import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import Uniswap from '../services/uniswap'; +import Ethereum from '../services/eth'; + +require('dotenv').config() +const debug = require('debug')('router') + +const router = express.Router() +const uniswap = new Uniswap(process.env.UNISWAP_NETWORK) +const eth = new Ethereum(process.env.UNISWAP_NETWORK) + +const denomMultiplier = 1e18 +const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' +const swapLessThanMaxPriceError = 'Swap price lower than maxPrice' + +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.post('/', async (req, res) => { + /* + POST / + */ + res.status(200).json({ + network: uniswap.network, + provider: uniswap.provider.connection.url, + exchangeProxy: uniswap.exchangeProxy, + subgraphUrl: process.env.REACT_APP_SUBGRAPH_URL, // would most likely replace with uniswap's when working on the client connector + connection: true, + 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 + + try { + // fetch the optimal pool mix from uniswap + const route = await uniswap.fetch_route( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + ) + + if (route != null) { + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: parseFloat(paramData.amount), + expectedOut: parseFloat(paramData.amount * route.midPrice.toSignificant(8)), + price: route.midPrice.toSignificant(8), + swaps: route.path, + }) + } else { // no pool available + res.status(200).json({ + error: statusMessages.no_pool_available, + message: '' + }) + } + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).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 + + try { + // fetch the optimal pool mix from uniswap + const route = await uniswap.fetch_route( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + ) + if (route != null) { + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: parseFloat(paramData.amount), + expectedIn: parseFloat(paramData.amount * route.midPrice.invert().toSignificant(8)), + price: route.midPrice.invert().toSignificant(8), + swaps: route.path, + }) + } else { // no pool available + res.status(200).json({ + error: statusMessages.no_pool_available, + message: '' + }) + } + } catch (err) { + let reason + err.reason ? reason = err.reason : reason = statusMessages.operation_error + res.status(500).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 = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + + let maxPrice + if (paramData.maxPrice) { + maxPrice = parseFloat(paramData.maxPrice) + } + let gasPrice + if (paramData.gasPrice) { + gasPrice = parseFloat(paramData.gasPrice) + } + + const minAmountOut = maxPrice / amount + debug('minAmountOut', minAmountOut) + + try { + // fetch the optimal pool mix from uniswap + const route = await uniswap.fetch_route( + baseTokenAddress, // tokenIn is base asset + quoteTokenAddress, // tokenOut is quote asset + ) + + const price = route.midPrice.toSignificant(8) + debug(`Price: ${price.toString()}`) + if (!maxPrice || price >= maxPrice) { + // pass swaps to exchange-proxy to complete trade + const txObj = await uniswap.swapExactIn( + wallet, + route, + baseTokenAddress, + amount, + gasPrice, + ) + + // submit response + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: parseFloat(paramData.amount), + expectedOut: parseFloat(paramData.amount * route.midPrice.toSignificant(8)), + price: price, + gasUsed: parseInt(txObj.gasUsed), + txHash: txObj.transactionHash, + status: txObj.status, + }) + } 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 = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + + 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 route = await uniswap.fetch_route( + quoteTokenAddress, // tokenIn is quote asset + baseTokenAddress, // tokenOut is base asset + // amount, + ) + + const price = route.midPrice.invert().toSignificant(8) + debug(`Price: ${price.toString()}`) + if (!maxPrice || price <= maxPrice) { + // pass swaps to exchange-proxy to complete trade + const txObj = await uniswap.swapExactIn( + wallet, + route, + quoteTokenAddress, + amount, + gasPrice, + ) + + // submit response + res.status(200).json({ + network: uniswap.network, + timestamp: initTime, + latency: latency(initTime, Date.now()), + base: baseTokenAddress, + quote: quoteTokenAddress, + amount: parseFloat(paramData.amount), + expectedIn: parseFloat(paramData.amount * route.midPrice.invert().toSignificant(8)), + price: price, + gasUsed: parseInt(txObj.gasUsed), + txHash: txObj.transactionHash, + status: txObj.status, + }) + } 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/uniswap.js b/src/services/uniswap.js new file mode 100644 index 0000000..945b251 --- /dev/null +++ b/src/services/uniswap.js @@ -0,0 +1,72 @@ +import Ethereum from '../services/eth'; +const uni = require('@uniswap/sdk') +const ethers = require('ethers') +const proxyArtifact = require('../static/uniswap_v2_router_abi.json') +const debug = require('debug')('router') + +const GAS_LIMIT = 1200000 //120000000 + +export default class Uniswap { + constructor (network = 'kovan') { + // network defaults to KOVAN + const providerUrl = process.env.ETHEREUM_RPC_URL + this.network = process.env.UNISWAP_NETWORK + this.chainID = uni.ChainId.KOVAN + this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.exchangeProxy = process.env.UNISWAP_PROXY + + if (network === 'kovan') { + this.chainID = uni.ChainId.KOVAN + } else if (network === 'mainnet') { + this.chainID = uni.ChainId.MAINNET + } else { + 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 swapExactIn (wallet, route, tokenAddress, amountIn, gasPrice) { + const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) + const tokenAmountIn = new uni.TokenAmount(tIn, amountIn) + const result = uni.Router.swapCallParameters( + uni.Trade.exactIn(route, tokenAmountIn), + { ttl: 50, recipient: wallet.address, allowedSlippage: new uni.Percent('1', '100') } + ) + + const erc = new Ethereum(this.network) + const approval = await erc.approveERC20(wallet, this.exchangeProxy, tokenAddress, ethers.utils.parseUnits(tokenAmountIn.toExact(), tIn.decimals), gasPrice) + let confirmApproval = await approval.wait() // Wait to ensure that approval transaction is mined + debug(`Approval Hash: ${approval.hash}`); + + const contract = new ethers.Contract(this.exchangeProxy, 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}`); + const txObj = await tx.wait() + return txObj + } +} 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": "" +} From 63478843c9404a210c0e145008b687649d814c1a Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 18 Nov 2020 14:15:11 +0100 Subject: [PATCH 15/41] (feat) update uniswap and modified eth route to be non balancer specific --- src/index.js | 3 +-- src/routes/eth.route.js | 14 +++++++++----- src/routes/uniswap.route.js | 8 ++++---- src/services/uniswap.js | 36 ++++++++++++++++++++++++++++-------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/index.js b/src/index.js index ffb6abe..3ded6f3 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,6 @@ const env = process.env.NODE_ENV const port = process.env.PORT const certPassphrase = process.env.CERT_PASSPHRASE const balancerNetwork = process.env.BALANCER_NETWORK -const uniswapNetwork = process.env.UNISWAP_NETWORK let certPath = process.env.CERT_PATH if ((typeof certPath === 'undefined' && certPath == null) || certPath === '') { @@ -80,4 +79,4 @@ server.listen(port) server.on('error', onError) server.on('listening', onListening) -console.log('server: gateway-api | port:', port, '| balancer-network:', balancerNetwork, '| uniswap-network:', uniswapNetwork); +console.log('server: gateway-api | port:', port, '| balancer-network:', balancerNetwork); diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 4208e13..f500d7c 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -10,6 +10,8 @@ const router = express.Router() const eth = new Ethereum(process.env.BALANCER_NETWORK) const balancer = new Balancer(process.env.BALANCER_NETWORK) const seperator = ',' +const spenders = { balancer: process.env.EXCHANGE_PROXY, + uniswap: process.env.UNISWAP_ROUTER } const debug = require('debug')('router') @@ -77,6 +79,7 @@ router.post('/allowances', async (req, res) => { 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,7 +92,6 @@ router.post('/allowances', async (req, res) => { }) return } - const spender = balancer.exchangeProxy let tokenAddressList if (paramData.tokenAddressList) { tokenAddressList = paramData.tokenAddressList.split(seperator) @@ -134,6 +136,7 @@ router.post('/approve', async (req, res) => { // 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 +150,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) diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index ff59fce..9aab609 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -10,8 +10,8 @@ require('dotenv').config() const debug = require('debug')('router') const router = express.Router() -const uniswap = new Uniswap(process.env.UNISWAP_NETWORK) -const eth = new Ethereum(process.env.UNISWAP_NETWORK) +const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) +const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' @@ -35,7 +35,7 @@ router.post('/', async (req, res) => { res.status(200).json({ network: uniswap.network, provider: uniswap.provider.connection.url, - exchangeProxy: uniswap.exchangeProxy, + exchangeProxy: uniswap.router, subgraphUrl: process.env.REACT_APP_SUBGRAPH_URL, // would most likely replace with uniswap's when working on the client connector connection: true, timestamp: Date.now(), @@ -266,7 +266,7 @@ router.post('/buy', async (req, res) => { debug(`Price: ${price.toString()}`) if (!maxPrice || price <= maxPrice) { // pass swaps to exchange-proxy to complete trade - const txObj = await uniswap.swapExactIn( + const txObj = await uniswap.swapExactOut( wallet, route, quoteTokenAddress, diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 945b251..690cfa3 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -5,15 +5,17 @@ const proxyArtifact = require('../static/uniswap_v2_router_abi.json') const debug = require('debug')('router') const GAS_LIMIT = 1200000 //120000000 +const TTL = 60 export default class Uniswap { constructor (network = 'kovan') { // network defaults to KOVAN const providerUrl = process.env.ETHEREUM_RPC_URL - this.network = process.env.UNISWAP_NETWORK + this.network = process.env.ETHEREUM_CHAIN this.chainID = uni.ChainId.KOVAN this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.exchangeProxy = process.env.UNISWAP_PROXY + this.router = process.env.UNISWAP_ROUTER + this.allowedSlippage = new uni.Percent('0', '100') if (network === 'kovan') { this.chainID = uni.ChainId.KOVAN @@ -47,15 +49,33 @@ export default class Uniswap { const tokenAmountIn = new uni.TokenAmount(tIn, amountIn) const result = uni.Router.swapCallParameters( uni.Trade.exactIn(route, tokenAmountIn), - { ttl: 50, recipient: wallet.address, allowedSlippage: new uni.Percent('1', '100') } + { ttl: TTL, recipient: wallet.address, allowedSlippage: this.allowedSlippage } ) - const erc = new Ethereum(this.network) - const approval = await erc.approveERC20(wallet, this.exchangeProxy, tokenAddress, ethers.utils.parseUnits(tokenAmountIn.toExact(), tIn.decimals), gasPrice) - let confirmApproval = await approval.wait() // Wait to ensure that approval transaction is mined - debug(`Approval Hash: ${approval.hash}`); + 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}`); + const txObj = await tx.wait() + return txObj + } + + async swapExactOut (wallet, route, tokenAddress, amountOut, gasPrice) { + const tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) + const tokenAmountOut = new uni.TokenAmount(tOut, amountOut) + const result = uni.Router.swapCallParameters( + uni.Trade.exactOut(route, tokenAmountOut), + { ttl: TTL, recipient: wallet.address, allowedSlippage: this.allowedSlippage } + ) - const contract = new ethers.Contract(this.exchangeProxy, proxyArtifact.abi, wallet) + const contract = new ethers.Contract(this.router, proxyArtifact.abi, wallet) const tx = await contract.[result.methodName]( ...result.args, { From 32f886304464146a3b158f754d723b94f1ec8441 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 18 Nov 2020 14:30:26 +0100 Subject: [PATCH 16/41] (cleanup) remove additions to balancer.routes --- src/routes/balancer.route.js | 116 +---------------------------------- 1 file changed, 1 insertion(+), 115 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 4a1ef10..593d7ee 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -16,7 +16,6 @@ const eth = new Ethereum(process.env.BALANCER_NETWORK) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' const swapLessThanMaxPriceError = 'Swap price lower than maxPrice' -const separator = ',' router.use((req, res, next) => { const cert = req.connection.getPeerCertificate() @@ -187,7 +186,7 @@ router.post('/sell', async (req, res) => { amount, ) - const price = expectedOut / amount + const price = expectedOut / amount debug(`Price: ${price.toString()}`) if (!maxPrice || price >= maxPrice) { // pass swaps to exchange-proxy to complete trade @@ -314,117 +313,4 @@ router.post('/buy', async (req, res) => { } }) -router.post('/allowances', async (req, res) => { - /* - POST: /allowances - x-www-form-urlencoded: { - privateKey:{{privateKey}} - tokenAddressList:{{tokenAddressList}} - } - */ - const initTime = Date.now() - const paramData = getParamData(req.body) - const privateKey = paramData.privateKey - let wallet - try { - wallet = new ethers.Wallet(privateKey, eth.provider) - } catch (err) { - let reason - err.reason ? reason = err.reason : reason = 'Error getting wallet' - res.status(500).json({ - error: reason, - message: err - }) - return - } - let tokenAddressList - if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(separator) - } - debug(tokenAddressList) - const spender = balancer.exchangeProxy - - const approvals = {} - try { - Promise.all( - tokenAddressList.map(async (key) => - approvals[key] = await eth.getERC20Allowance(wallet, spender, key) - )).then(() => { - res.status(200).json({ - network: eth.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - spender: spender, - approvals: approvals, - }) - } - ) - } catch (err) { - let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ - error: reason, - message: err - }) - } -}) - -router.post('/approve', async (req, res) => { - /* - POST: /approve - x-www-form-urlencoded: { - privateKey:{{privateKey}} - tokenAddress:"0x....." - amount:{{amount}} - } - */ - const initTime = Date.now() - const paramData = getParamData(req.body) - const privateKey = paramData.privateKey - let wallet - try { - wallet = new ethers.Wallet(privateKey, eth.provider) - } catch (err) { - let reason - err.reason ? reason = err.reason : reason = 'Error getting wallet' - res.status(500).json({ - error: reason, - message: err - }) - 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 gasPrice - if (paramData.gasPrice) { - gasPrice = parseFloat(paramData.gasPrice) - } - - try { - // call approve function - const approval = await eth.approveERC20(wallet, spender, tokenAddress, amount, gasPrice) - - // submit response - res.status(200).json({ - network: eth.network, - timestamp: initTime, - latency: latency(initTime, Date.now()), - tokenAddress: tokenAddress, - spender: spender, - amount: amount / 1e18.toString(), - approval: approval - }) - } catch (err) { - let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ - error: reason, - message: err - }) - } -}) - export default router; From 90ddf3b6194c52e61740d855e09f556970160093 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 18 Nov 2020 14:42:39 +0100 Subject: [PATCH 17/41] (feat) rename BALANCER_NETWORK to ETHEREUM_CHAIN and make eth routes none balancer specific --- src/index.js | 2 +- src/routes/eth.route.js | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index ab5b30d..083fbb5 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ if (result.error) { const env = process.env.NODE_ENV const port = process.env.PORT const certPassphrase = process.env.CERT_PASSPHRASE -const ethereumChain = process.env.BALANCER_NETWORK +const ethereumChain = process.env.ETHEREUM_CHAIN let certPath = process.env.CERT_PATH if ((typeof certPath === 'undefined' && certPath == null) || certPath === '') { diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 4c378d2..abcf6e6 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -5,8 +5,10 @@ import { getParamData, latency, reportConnectionError, statusMessages } from '.. import Ethereum from '../services/eth'; const router = express.Router() -const eth = new Ethereum(process.env.BALANCER_NETWORK) +const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const separator = ',' +const spenders = { balancer: process.env.EXCHANGE_PROXY, + uniswap: process.env.UNISWAP_ROUTER } const debug = require('debug')('router') @@ -69,12 +71,13 @@ router.post('/allowances', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddressList:{{tokenAddressList}} - spenderAddress:"0x....." + spenderAddress:"0x....." } */ 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) @@ -91,8 +94,6 @@ router.post('/allowances', async (req, res) => { if (paramData.tokenAddressList) { tokenAddressList = paramData.tokenAddressList.split(separator) } - debug(tokenAddressList) - const spender = paramData.spenderAddress const approvals = {} try { @@ -125,13 +126,14 @@ router.post('/approve', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddress:"0x....." - spenderAddress:"0x....." + spenderAddress:"0x....." amount:{{amount}} } */ 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) @@ -145,10 +147,11 @@ router.post('/approve', async (req, res) => { return } const tokenAddress = paramData.tokenAddress - const spender = paramData.spenderAddress - 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) From feee7b6a4b77d33dfbec809b319aeb3a5f2ff0c0 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 18 Nov 2020 14:48:39 +0100 Subject: [PATCH 18/41] (feat) update argument description for approval related routes --- src/routes/eth.route.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index abcf6e6..939be62 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -71,7 +71,7 @@ router.post('/allowances', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddressList:{{tokenAddressList}} - spenderAddress:"0x....." + connector:{{connector_name}}" } */ const initTime = Date.now() @@ -126,7 +126,7 @@ router.post('/approve', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddress:"0x....." - spenderAddress:"0x....." + connector:{{connector_name}}" amount:{{amount}} } */ From b192023b819bb5e85781eade0da1ff0604f7c972 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 18 Nov 2020 14:51:48 +0100 Subject: [PATCH 19/41] (feat) update argument description for approval related routes 2 --- src/routes/eth.route.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 939be62..ba72c37 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -71,7 +71,7 @@ router.post('/allowances', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddressList:{{tokenAddressList}} - connector:{{connector_name}}" + connector:{{connector_name}} } */ const initTime = Date.now() @@ -126,7 +126,8 @@ router.post('/approve', async (req, res) => { x-www-form-urlencoded: { privateKey:{{privateKey}} tokenAddress:"0x....." - connector:{{connector_name}}" + decimals: {{token_decimals}} + connector:{{connector_name}} amount:{{amount}} } */ From b53f354fec8acebf5dcdad64067be01e49f79137 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 19 Nov 2020 16:03:46 -0800 Subject: [PATCH 20/41] (fix) minor fixes --- .env.example | 16 ++++++------ src/routes/balancer.route.js | 4 +-- src/routes/eth.route.js | 6 +++-- src/routes/uniswap.route.js | 7 ++---- src/services/balancer.js | 2 +- src/services/uniswap.js | 35 +++++++++++++++----------- yarn.lock | 48 ++++++++++++++++++++++++++++++++++++ 7 files changed, 87 insertions(+), 31 deletions(-) diff --git a/.env.example b/.env.example index 07f4d81..259452b 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,6 @@ -# 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 @@ -16,13 +13,14 @@ IP_WHITELIST= TERRA_LCD_URL=https://tequila-lcd.terra.dev TERRA_CHAIN=tequila-0004 -# Balancer +# Ethereum # - network: mainnet, kovan, etc # - rpc url: infura or other rpc url -BALANCER_NETWORK={network} +ETHEREUM CHAIN={network} ETHEREUM_RPC_URL=https://{network}.infura.io/v3/{api_key} -# subgraph_network: +# Balancer +# subgraph_network # Reference: https://docs.balancer.finance/sor/development#subgraph # - mainnet: balancer # - kovan: balancer-kovan @@ -31,10 +29,14 @@ REACT_APP_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/balancer-labs/{su # 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/src/routes/balancer.route.js b/src/routes/balancer.route.js index 593d7ee..82d8d92 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -10,8 +10,8 @@ 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 eth = new Ethereum(process.env.ETHEREUM_CHAIN) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index ba72c37..77619ef 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -7,8 +7,10 @@ import Ethereum from '../services/eth'; const router = express.Router() const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const separator = ',' -const spenders = { balancer: process.env.EXCHANGE_PROXY, - uniswap: process.env.UNISWAP_ROUTER } +const spenders = { + balancer: process.env.EXCHANGE_PROXY, + uniswap: process.env.UNISWAP_ROUTER +} const debug = require('debug')('router') diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 9aab609..27eff55 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -2,16 +2,14 @@ import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; import express from 'express'; -import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; +import { getParamData, latency, statusMessages } from '../services/utils'; import Uniswap from '../services/uniswap'; -import Ethereum from '../services/eth'; require('dotenv').config() const debug = require('debug')('router') const router = express.Router() const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) -const eth = new Ethereum(process.env.ETHEREUM_CHAIN) const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Swap price exceeds maxPrice' @@ -35,8 +33,7 @@ router.post('/', async (req, res) => { res.status(200).json({ network: uniswap.network, provider: uniswap.provider.connection.url, - exchangeProxy: uniswap.router, - subgraphUrl: process.env.REACT_APP_SUBGRAPH_URL, // would most likely replace with uniswap's when working on the client connector + uniswap_router: uniswap.router, connection: true, timestamp: Date.now(), }) diff --git a/src/services/balancer.js b/src/services/balancer.js index 0698e3a..5477e22 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -15,7 +15,7 @@ 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 diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 690cfa3..0063cb3 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -12,7 +12,6 @@ export default class Uniswap { // network defaults to KOVAN const providerUrl = process.env.ETHEREUM_RPC_URL this.network = process.env.ETHEREUM_CHAIN - this.chainID = uni.ChainId.KOVAN this.provider = new ethers.providers.JsonRpcProvider(providerUrl) this.router = process.env.UNISWAP_ROUTER this.allowedSlippage = new uni.Percent('0', '100') @@ -32,14 +31,14 @@ export default class Uniswap { var tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenOut) try { - pair = await uni.Fetcher.fetchPairData(tIn, tOut) - route = new uni.Route([pair], tIn, tOut) + 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) + 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 } @@ -48,9 +47,13 @@ export default class Uniswap { const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) const tokenAmountIn = new uni.TokenAmount(tIn, amountIn) const result = uni.Router.swapCallParameters( - uni.Trade.exactIn(route, tokenAmountIn), - { ttl: TTL, recipient: wallet.address, allowedSlippage: this.allowedSlippage } - ) + uni.Trade.exactIn(route, tokenAmountIn), + { + ttl: TTL, + recipient: wallet.address, + allowedSlippage: this.allowedSlippage + } + ) const contract = new ethers.Contract(this.router, proxyArtifact.abi, wallet) const tx = await contract.[result.methodName]( @@ -71,9 +74,13 @@ export default class Uniswap { const tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) const tokenAmountOut = new uni.TokenAmount(tOut, amountOut) const result = uni.Router.swapCallParameters( - uni.Trade.exactOut(route, tokenAmountOut), - { ttl: TTL, recipient: wallet.address, allowedSlippage: this.allowedSlippage } - ) + uni.Trade.exactOut(route, tokenAmountOut), + { + ttl: TTL, + recipient: wallet.address, + allowedSlippage: this.allowedSlippage + } + ) const contract = new ethers.Contract(this.router, proxyArtifact.abi, wallet) const tx = await contract.[result.methodName]( 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" From d2a6efad47b61fdb14db7d6c03cda2c299c94a11 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 19 Nov 2020 16:58:10 -0800 Subject: [PATCH 21/41] (fix) clean up code and add balancer/uniswap addresses --- src/routes/balancer.route.js | 6 ++-- src/routes/uniswap.route.js | 4 +-- src/services/balancer.js | 54 +++++++++++++++++++----------------- src/services/uniswap.js | 27 ++++++++++-------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 82d8d92..9423de5 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -4,18 +4,16 @@ 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() const debug = require('debug')('router') const router = express.Router() const balancer = new Balancer(process.env.ETHEREUM_CHAIN) -const eth = new Ethereum(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() diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 27eff55..3719825 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -12,8 +12,8 @@ const router = express.Router() const uniswap = new Uniswap(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() diff --git a/src/services/balancer.js b/src/services/balancer.js index 5477e22..26a2877 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,26 +6,29 @@ 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 EXCHANGE_PROXY = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec'; +const EXCHANGE_PROXY_KOVAN = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; +const MAX_UINT = ethers.constants.MaxUint256; +const MAX_SWAPS = 2; +const GAS_BASE = 100000; +const GAS_PER_SWAP = 100000; export default class Balancer { - constructor (network = 'kovan') { - // network defaults to kovan + constructor (network = 'mainnet') { const providerUrl = process.env.ETHEREUM_RPC_URL this.network = process.env.ETHEREUM_CHAIN this.provider = new ethers.providers.JsonRpcProvider(providerUrl) - this.exchangeProxy = process.env.EXCHANGE_PROXY - 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; + break; + case 'kovan': + this.exchangeProxy = EXCHANGE_PROXY_KOVAN; + break; + default: + throw Error(`Invalid network ${network}`) } } @@ -48,11 +50,11 @@ export default class Balancer { // 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(MAX_SWAPS.toString()), // maxBalancers: number + 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountIn(sorSwaps, MAX_UINT, 0) @@ -93,11 +95,11 @@ export default class Balancer { // 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(MAX_SWAPS.toString()), // maxBalancers: number + 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountOut(sorSwaps, MAX_UINT, MAX_UINT) const expectedIn = sor.calcTotalInput(swapsFormatted, poolData) @@ -129,7 +131,7 @@ export default class Balancer { 0, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: GAS_BASE + MAX_SWAPS * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`); @@ -146,7 +148,7 @@ export default class Balancer { expectedIn, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_LIMIT + gasLimit: GAS_BASE + MAX_SWAPS * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`) diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 0063cb3..11ebde3 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -1,27 +1,30 @@ -import Ethereum from '../services/eth'; const uni = require('@uniswap/sdk') const ethers = require('ethers') const proxyArtifact = require('../static/uniswap_v2_router_abi.json') const debug = require('debug')('router') -const GAS_LIMIT = 1200000 //120000000 -const TTL = 60 +// constants +const ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; +const GAS_LIMIT = 1200000; +const TTL = 60; export default class Uniswap { - constructor (network = 'kovan') { - // network defaults to KOVAN + 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 = process.env.UNISWAP_ROUTER + this.router = ROUTER; this.allowedSlippage = new uni.Percent('0', '100') - if (network === 'kovan') { - this.chainID = uni.ChainId.KOVAN - } else if (network === 'mainnet') { - this.chainID = uni.ChainId.MAINNET - } else { - throw Error(`Invalid network ${network}`) + switch (network) { + case 'mainnet': + this.chainID = uni.ChainId.MAINNET; + break; + case 'kovan': + this.chainID = uni.ChainId.KOVAN; + break; + default: + throw Error(`Invalid network ${network}`) } } From 4d8810dbbb9fa3c7823cbe2c9b3093131236d432 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 19 Nov 2020 20:06:03 -0800 Subject: [PATCH 22/41] (feat) balancer dynamic gas --- src/routes/balancer.route.js | 23 +++++++++++++++++++++++ src/services/balancer.js | 22 ++++++++++++---------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 9423de5..cd213e8 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -47,6 +47,7 @@ router.post('/sell-price', async (req, res) => { "quote":"0x....." "base":"0x....." "amount":0.1 + "swaps": 4 (optional) } */ const initTime = Date.now() @@ -55,6 +56,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 @@ -62,6 +67,7 @@ router.post('/sell-price', async (req, res) => { baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset amount, + maxSwaps, ) if (swaps != null && expectedOut != null) { @@ -107,6 +113,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 @@ -114,6 +124,7 @@ router.post('/buy-price', async (req, res) => { quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset amount, + maxSwaps, ) if (swaps != null && expectedIn != null) { res.status(200).json({ @@ -172,6 +183,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) @@ -182,6 +197,7 @@ router.post('/sell', async (req, res) => { baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset amount, + maxSwaps, ) const price = expectedOut / amount @@ -198,6 +214,8 @@ router.post('/sell', async (req, res) => { gasPrice, ) + debug(txObj) + // submit response res.status(200).json({ network: balancer.network, @@ -258,6 +276,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 @@ -265,6 +287,7 @@ router.post('/buy', async (req, res) => { quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset amount, + maxSwaps, ) const price = expectedIn / amount diff --git a/src/services/balancer.js b/src/services/balancer.js index 26a2877..9aa3593 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -7,11 +7,11 @@ const debug = require('debug')('router') // constants const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; -const EXCHANGE_PROXY = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec'; -const EXCHANGE_PROXY_KOVAN = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; +const EXCHANGE_PROXY = '0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21'; +const EXCHANGE_PROXY_KOVAN = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec'; const MAX_UINT = ethers.constants.MaxUint256; -const MAX_SWAPS = 2; -const GAS_BASE = 100000; +const MAX_SWAPS = 4; +const GAS_BASE = 200000; const GAS_PER_SWAP = 100000; export default class Balancer { @@ -32,7 +32,7 @@ export default class Balancer { } } - 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) { @@ -53,7 +53,7 @@ export default class Balancer { poolData, // balancers: Pool[] 'swapExactIn', // swapType: string tokenInAmount, // targetInputAmount: BigNumber - new BigNumber(MAX_SWAPS.toString()), // maxBalancers: number + new BigNumber(maxSwaps.toString()), // maxBalancers: number 0 // costOutputToken: BigNumber ) @@ -77,7 +77,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) { @@ -98,7 +98,7 @@ export default class Balancer { poolData, // balancers: Pool[] 'swapExactOut', // swapType: string tokenOutAmount, // targetInputAmount: BigNumber - new BigNumber(MAX_SWAPS.toString()), // maxBalancers: number + new BigNumber(maxSwaps.toString()), // maxBalancers: number 0 // costOutputToken: BigNumber ) const swapsFormatted = sor.formatSwapsExactAmountOut(sorSwaps, MAX_UINT, MAX_UINT) @@ -122,6 +122,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, @@ -131,7 +132,7 @@ export default class Balancer { 0, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_BASE + MAX_SWAPS * GAS_PER_SWAP, + gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`); @@ -140,6 +141,7 @@ export default class Balancer { } 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, @@ -148,7 +150,7 @@ export default class Balancer { expectedIn, { gasPrice: gasPrice * 1e9, - gasLimit: GAS_BASE + MAX_SWAPS * GAS_PER_SWAP, + gasLimit: GAS_BASE + swaps.length * GAS_PER_SWAP, } ) debug(`Tx Hash: ${tx.hash}`) From e28db41688439c8d895a06cb2a831eccf8300b68 Mon Sep 17 00:00:00 2001 From: vic-en Date: Fri, 20 Nov 2020 15:53:18 +0100 Subject: [PATCH 23/41] (fix) provide appropraite token address when swapping out --- src/routes/uniswap.route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 9aab609..1886998 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -269,7 +269,7 @@ router.post('/buy', async (req, res) => { const txObj = await uniswap.swapExactOut( wallet, route, - quoteTokenAddress, + baseTokenAddress, amount, gasPrice, ) From 9037859dfae4e6444e35ae5b43880e6ebe30f8c8 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 21 Nov 2020 00:36:19 +0800 Subject: [PATCH 24/41] Add gas price preset, return tx fee in price --- .env.example | 8 +++- src/routes/terra.route.js | 5 +-- src/services/config.js | 7 ++- src/services/terra.js | 92 ++++++++++++++++++++++----------------- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/.env.example b/.env.example index 958a19a..0790df5 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,12 @@ IP_WHITELIST= # Celo # Terra -TERRA_LCD_URL=https://tequila-lcd.terra.dev -TERRA_NETWORK=tequila-0004 +# - 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_NETWORK={testnet_chain_id} # Balancer # - network: mainnet, kovan, etc diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 62285e1..5f73ba1 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -1,12 +1,10 @@ 'use strict' import express from 'express' -import { MnemonicKey, Coin, MsgSwap, isTxError } from '@terra-money/terra.js' import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; import { getConfig } from '../services/config'; import Terra from '../services/terra'; -import e from 'express'; const debug = require('debug')('router') const router = express.Router(); @@ -119,7 +117,8 @@ router.post('/price', async (req, res) => { amount: amount, tradeType: tradeType, price: exchangeRate.price.amount, - cost: exchangeRate.cost.amount + cost: exchangeRate.cost.amount, + txFee: exchangeRate.txFee.amount, } ) } catch (err) { diff --git a/src/services/config.js b/src/services/config.js index 32cbc90..e7ffe2e 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -1,10 +1,13 @@ require('dotenv').config() +const defaultMemo = '' + export const getConfig = () => { const env = { TERRA: { LCD_URL: process.env.TERRA_LCD_URL, NETWORK: process.env.TERRA_NETWORK, + MEMO: process.env.TERRA_MEMO || defaultMemo }, BALANCER: { NETWORK: process.env.BALANCER_NETWORK, @@ -12,12 +15,14 @@ export const getConfig = () => { EXCHANGE_PROXY: process.env.EXCHANGE_PROXY, GAS_PRICE: parseInt(process.env.GAS_PRICE), GAS_LIMIT: parseInt(process.env.GAS_LIMIT) || 1200000, - APPROVAL_GAS_LIMIT: parseInt(process.env.APPROVAL_GAS_LIMIT) || 100000 + APPROVAL_GAS_LIMIT: parseInt(process.env.APPROVAL_GAS_LIMIT) || 100000, + MEMO: process.env.BALANCER_MEMO || defaultMemo }, ETHEREUM: { RPC_URL: process.env.ETHEREUM_RPC_URL, }, uniswap: { + }, celo: { } diff --git a/src/services/terra.js b/src/services/terra.js index 6b313a0..4733add 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -1,9 +1,10 @@ -import { LCDClient, Coin, MsgSwap, MnemonicKey, isTxError } from '@terra-money/terra.js' +import { LCDClient, Coin, MsgSwap, StdTx, StdFee, Dec, MnemonicKey, isTxError, Coins } from '@terra-money/terra.js' import BigNumber from 'bignumber.js' require('dotenv').config() const debug = require('debug')('router') const config = require('../services/config') +const dummyAccount = require('../static/terraTestAccount') // constants const ENV_CONFIG = config.getConfig() @@ -15,10 +16,11 @@ const TERRA_TOKENS = { umnt: { symbol: 'MNT' }, } const DENOM_UNIT = BigNumber('1e+6') -const TOBIN_TAX = 0.25 // a Tobin Tax (set at 0.25%) for spot-converting Terra<>Terra swaps -const MIN_SPREAD = 2.0 // a minimum spread (set at 2%) for Terra<>Luna swaps +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 = 0.02 const GAS_ADJUSTMENT = 1.5 -const TESTNET_MEMO = '0xhb34' +const MEMO = ENV_CONFIG.TERRA.MEMO export default class Terra { constructor () { @@ -35,6 +37,9 @@ export default class Terra { this.lcd.market.parameters().catch(() => { throw new Error('Connection error') }) + // set gas & fee + this.lcd.config.gasAdjustment = GAS_ADJUSTMENT + this.lcd.config.gasPrice = GAS_PRICE } catch (err) { throw Error(`Connection failed: ${this.network}`) } @@ -47,6 +52,8 @@ export default class Terra { URL: this.lcdUrl, chainID: this.network, }) + lcd.config.gasAdjustment = GAS_ADJUSTMENT + lcd.config.gasPrice = GAS_PRICE return lcd } catch (err) { let reason @@ -119,10 +126,30 @@ export default class Terra { } } + async getTxFee () { + try { + const lunaFee = GAS_PRICE * 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, offerDenom, swapDenom, cost, offer + let exchangeRate, offerCoin, offerDenom, swapDenom, cost, costAmount, offer let swaps = {} if (tradeType.toLowerCase() === 'sell') { @@ -130,15 +157,16 @@ export default class Terra { offerDenom = this.getTokenDenom(baseToken) swapDenom = this.getTokenDenom(quoteToken) - const offerCoin = new Coin(offerDenom, amount * DENOM_UNIT); + 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: amount * exchangeRate.amount, + amount: costAmount, token: quoteToken } }) @@ -147,24 +175,32 @@ export default class Terra { offerDenom = this.getTokenDenom(quoteToken) swapDenom = this.getTokenDenom(baseToken) - const offerCoin = new Coin(offerDenom, 1 * DENOM_UNIT); + offerCoin = new Coin(offerDenom, 1 * DENOM_UNIT); await this.lcd.market.swapRate(offerCoin, swapDenom).then(swapCoin => { exchangeRate = { - amount: (amount / swapCoin.amount * DENOM_UNIT) / amount, // adjusted amount + amount: (amount / parseInt(swapCoin.amount) * DENOM_UNIT) / amount, // adjusted amount token: quoteToken } + costAmount = amount * exchangeRate.amount cost = { - amount: amount * exchangeRate.amount, + 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 - // debug('swaps', swaps) + swaps.txFee = txFee + debug('swaps', swaps) return swaps } catch (err) { let reason @@ -213,13 +249,12 @@ export default class Terra { swaps = rate }) - const offerAmount = parseInt(swaps.offer.amount * DENOM_UNIT) - + const offerAmount = parseInt((swaps.offer.amount) * DENOM_UNIT) const offerCoin = new Coin(offerDenom, offerAmount) debug('offerCoin', offerCoin, offerAmount) + // Create and Sign Transaction const msgSwap = new MsgSwap(address, offerCoin, swapDenom); - const memo = lcd.config.chainID.toLowerCase().includes('columbus') ? '' : TESTNET_MEMO // debug('msgSwap', msgSwap) let txOptions @@ -227,29 +262,18 @@ export default class Terra { txOptions = { msgs: [msgSwap], gasPrices: { [offerDenom]: gasPrice }, - gasAdjustment: gasAdjustment || GAS_ADJUSTMENT, - memo: memo + gasAdjustment: gasAdjustment, + memo: MEMO } } else { txOptions = { msgs: [msgSwap], - memo: memo + memo: MEMO } } - // const fee = await this.getEstimateFee(tx) - // const estimatedGas = fee.gas - // const estimatedFee = fee.amount._coins[offerDenom] - // const estimatedFeeDenom = estimatedFee.denom - // const estimatedFeeAmount = estimatedFee.amount * DENOM_UNIT - // debug('estimatedGas/estimatedFeeDenom/estimatedFeeAmount', estimatedGas, estimatedFee) - // debug(estimatedFeeDenom, estimatedFeeAmount) - - // debug('offerDenom', offerDenom, 'gasPrice', gasPrice, 'gasAdjustment', gasAdjustment, 'options', txOptions) - await wallet.createAndSignTx(txOptions).then(tx => lcd.tx.broadcast(tx)).then((txResult) => { swapResult = txResult - // debug('txResult', txResult) const swapSuccess = !isTxError(txResult) if (swapSuccess) { @@ -259,10 +283,6 @@ export default class Terra { throw new Error(`encountered an error while running the transaction: ${txResult.code} ${txResult.codespace}`); } - // check for events from the first message - // debug('event first message', txResult.logs[0].eventsByType.store_code) - - // debug('swapSuccess', swapSuccess) const txHash = txResult.txhash const events = JSON.parse(txResult.raw_log)[0].events const swap = events.find(obj => { @@ -273,8 +293,6 @@ export default class Terra { const ask = Coin.fromString(txAttributes.swap_coin) const fee = Coin.fromString(txAttributes.swap_fee) - // debug('txAttributes', txAttributes) - tokenSwap.expectedIn = { amount: parseFloat(offer.amount) / DENOM_UNIT, token: TERRA_TOKENS[offer.denom].symbol @@ -287,16 +305,12 @@ export default class Terra { amount: parseFloat(fee.amount) / DENOM_UNIT, token: TERRA_TOKENS[fee.denom].symbol } - // tokenSwap.estimatedFee = { - // amount: estimatedFeeAmount, - // token: - // }, tokenSwap.txHash = txHash }) return tokenSwap } catch (err) { let reason - console.log(reason, typeof result) + console.log(err) err.reason ? reason = err.reason : reason = swapResult return { txSuccess: false, message: reason } } From f2e8f7333a180389e43906816211c108bec8bd01 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 21 Nov 2020 01:05:11 +0800 Subject: [PATCH 25/41] (fix) remove unused test address config --- src/services/terra.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/terra.js b/src/services/terra.js index 4733add..32ec3f2 100644 --- a/src/services/terra.js +++ b/src/services/terra.js @@ -4,7 +4,6 @@ import BigNumber from 'bignumber.js' require('dotenv').config() const debug = require('debug')('router') const config = require('../services/config') -const dummyAccount = require('../static/terraTestAccount') // constants const ENV_CONFIG = config.getConfig() From 68ceced11176a54f390df2ea8bee0da10ea2877a Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 23 Nov 2020 23:12:14 +0100 Subject: [PATCH 26/41] (refactor) refactor price routes to use execution price from Uniswap's trade class --- src/routes/uniswap.route.js | 46 ++++++++++++++++++++----------------- src/services/uniswap.js | 40 +++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 992fb6c..ae3017c 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -53,15 +53,17 @@ router.post('/sell-price', async (req, res) => { const paramData = getParamData(req.body) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote + const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) try { // fetch the optimal pool mix from uniswap - const route = await uniswap.fetch_route( + const { trade, expectedOut} = await uniswap.priceSwapIn( baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset + amount ) - if (route != null) { + if (trade != null && expectedOut != null) { res.status(200).json({ network: uniswap.network, timestamp: initTime, @@ -69,9 +71,9 @@ router.post('/sell-price', async (req, res) => { base: baseTokenAddress, quote: quoteTokenAddress, amount: parseFloat(paramData.amount), - expectedOut: parseFloat(paramData.amount * route.midPrice.toSignificant(8)), - price: route.midPrice.toSignificant(8), - swaps: route.path, + expectedOut: expectedOut.toSignificant(8), + price: trade.executionPrice.toSignificant(8), + swaps: trade, }) } else { // no pool available res.status(200).json({ @@ -103,14 +105,16 @@ router.post('/buy-price', async (req, res) => { const paramData = getParamData(req.body) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote + const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) try { // fetch the optimal pool mix from uniswap - const route = await uniswap.fetch_route( + const { trade, expectedIn } = await uniswap.priceSwapOut( quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset + amount ) - if (route != null) { + if (trade != null && expectedIn != null) { res.status(200).json({ network: uniswap.network, timestamp: initTime, @@ -118,9 +122,9 @@ router.post('/buy-price', async (req, res) => { base: baseTokenAddress, quote: quoteTokenAddress, amount: parseFloat(paramData.amount), - expectedIn: parseFloat(paramData.amount * route.midPrice.invert().toSignificant(8)), - price: route.midPrice.invert().toSignificant(8), - swaps: route.path, + expectedIn: expectedIn.toSignificant(8), + price: trade.executionPrice.invert().toSignificant(8), + swaps: trade, }) } else { // no pool available res.status(200).json({ @@ -129,6 +133,7 @@ router.post('/buy-price', async (req, res) => { }) } } catch (err) { + console.log(err) let reason err.reason ? reason = err.reason : reason = statusMessages.operation_error res.status(500).json({ @@ -173,20 +178,20 @@ router.post('/sell', async (req, res) => { try { // fetch the optimal pool mix from uniswap - const route = await uniswap.fetch_route( + const { trade, expectedOut} = await uniswap.priceSwapIn( baseTokenAddress, // tokenIn is base asset quoteTokenAddress, // tokenOut is quote asset + amount ) - const price = route.midPrice.toSignificant(8) + const price = trade.executionPrice.toSignificant(8) debug(`Price: ${price.toString()}`) if (!maxPrice || price >= maxPrice) { // pass swaps to exchange-proxy to complete trade const txObj = await uniswap.swapExactIn( wallet, - route, + trade, baseTokenAddress, - amount, gasPrice, ) @@ -198,7 +203,7 @@ router.post('/sell', async (req, res) => { base: baseTokenAddress, quote: quoteTokenAddress, amount: parseFloat(paramData.amount), - expectedOut: parseFloat(paramData.amount * route.midPrice.toSignificant(8)), + expectedOut: expectedOut.toSignificant(8), price: price, gasUsed: parseInt(txObj.gasUsed), txHash: txObj.transactionHash, @@ -253,21 +258,20 @@ router.post('/buy', async (req, res) => { try { // fetch the optimal pool mix from uniswap - const route = await uniswap.fetch_route( + const { trade, expectedIn} = await uniswap.priceSwapOut( quoteTokenAddress, // tokenIn is quote asset baseTokenAddress, // tokenOut is base asset - // amount, + amount, ) - const price = route.midPrice.invert().toSignificant(8) + const price = trade.executionPrice.invert().toSignificant(8) debug(`Price: ${price.toString()}`) if (!maxPrice || price <= maxPrice) { // pass swaps to exchange-proxy to complete trade const txObj = await uniswap.swapExactOut( wallet, - route, + trade, baseTokenAddress, - amount, gasPrice, ) @@ -279,7 +283,7 @@ router.post('/buy', async (req, res) => { base: baseTokenAddress, quote: quoteTokenAddress, amount: parseFloat(paramData.amount), - expectedIn: parseFloat(paramData.amount * route.midPrice.invert().toSignificant(8)), + expectedIn: expectedIn.toSignificant(8), price: price, gasUsed: parseInt(txObj.gasUsed), txHash: txObj.transactionHash, diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 11ebde3..a2863b3 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -46,12 +46,28 @@ export default class Uniswap { return route } - async swapExactIn (wallet, route, tokenAddress, amountIn, gasPrice) { - const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) - const tokenAmountIn = new uni.TokenAmount(tIn, amountIn) + async priceSwapIn (tokenIn, tokenOut, tokenInAmount) { + const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenIn) + const tokenAmountIn = new uni.TokenAmount(tIn, tokenInAmount) + 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, tokenOutAmount) + 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( - uni.Trade.exactIn(route, tokenAmountIn), - { + trade, + { ttl: TTL, recipient: wallet.address, allowedSlippage: this.allowedSlippage @@ -73,15 +89,13 @@ export default class Uniswap { return txObj } - async swapExactOut (wallet, route, tokenAddress, amountOut, gasPrice) { - const tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenAddress) - const tokenAmountOut = new uni.TokenAmount(tOut, amountOut) + async swapExactOut (wallet, trade, tokenAddress, gasPrice) { const result = uni.Router.swapCallParameters( - uni.Trade.exactOut(route, tokenAmountOut), - { - ttl: TTL, - recipient: wallet.address, - allowedSlippage: this.allowedSlippage + trade, + { + ttl: TTL, + recipient: wallet.address, + allowedSlippage: this.allowedSlippage } ) From 2168cb7af12940166d416dcf8731f91f54fa85c1 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Nov 2020 17:55:21 -0800 Subject: [PATCH 27/41] (feat) balancer dynamic gas calc --- src/services/balancer.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/services/balancer.js b/src/services/balancer.js index 9aa3593..cee470b 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -7,11 +7,12 @@ const debug = require('debug')('router') // constants const MULTI = '0xeefba1e63905ef1d7acba5a8513c70307c1ce441'; +const MULTI_KOVAN = ' 0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A'; const EXCHANGE_PROXY = '0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21'; const EXCHANGE_PROXY_KOVAN = '0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec'; const MAX_UINT = ethers.constants.MaxUint256; const MAX_SWAPS = 4; -const GAS_BASE = 200000; +const GAS_BASE = 200688; const GAS_PER_SWAP = 100000; export default class Balancer { @@ -23,9 +24,11 @@ export default class Balancer { 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}`) @@ -41,10 +44,12 @@ 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) } @@ -86,10 +91,12 @@ 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) } From 840f9f8670e731c1ca3d10f2d7a8135e3b6de88f Mon Sep 17 00:00:00 2001 From: vic-en Date: Tue, 24 Nov 2020 11:51:00 +0100 Subject: [PATCH 28/41] (fix) parse amout correctly --- src/routes/uniswap.route.js | 21 ++++++++------------- src/services/uniswap.js | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index ae3017c..c82c319 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -1,4 +1,3 @@ -import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; import express from 'express'; @@ -11,7 +10,6 @@ const debug = require('debug')('router') const router = express.Router() const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) -const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Price too high' const swapLessThanMaxPriceError = 'Price too low' @@ -53,7 +51,7 @@ router.post('/sell-price', async (req, res) => { const paramData = getParamData(req.body) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote - const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + const amount = paramData.amount try { // fetch the optimal pool mix from uniswap @@ -70,7 +68,7 @@ router.post('/sell-price', async (req, res) => { latency: latency(initTime, Date.now()), base: baseTokenAddress, quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), + amount: amount, expectedOut: expectedOut.toSignificant(8), price: trade.executionPrice.toSignificant(8), swaps: trade, @@ -105,7 +103,7 @@ router.post('/buy-price', async (req, res) => { const paramData = getParamData(req.body) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote - const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + const amount = paramData.amount try { // fetch the optimal pool mix from uniswap @@ -121,7 +119,7 @@ router.post('/buy-price', async (req, res) => { latency: latency(initTime, Date.now()), base: baseTokenAddress, quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), + amount: amount, expectedIn: expectedIn.toSignificant(8), price: trade.executionPrice.invert().toSignificant(8), swaps: trade, @@ -162,7 +160,7 @@ router.post('/sell', async (req, res) => { const wallet = new ethers.Wallet(privateKey, uniswap.provider) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote - const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + const amount = paramData.amount let maxPrice if (paramData.maxPrice) { @@ -173,9 +171,6 @@ router.post('/sell', async (req, res) => { gasPrice = parseFloat(paramData.gasPrice) } - const minAmountOut = maxPrice / amount - debug('minAmountOut', minAmountOut) - try { // fetch the optimal pool mix from uniswap const { trade, expectedOut} = await uniswap.priceSwapIn( @@ -202,7 +197,7 @@ router.post('/sell', async (req, res) => { latency: latency(initTime, Date.now()), base: baseTokenAddress, quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), + amount: amount, expectedOut: expectedOut.toSignificant(8), price: price, gasUsed: parseInt(txObj.gasUsed), @@ -245,7 +240,7 @@ router.post('/buy', async (req, res) => { const wallet = new ethers.Wallet(privateKey, uniswap.provider) const baseTokenAddress = paramData.base const quoteTokenAddress = paramData.quote - const amount = new BigNumber(parseInt(paramData.amount * denomMultiplier)) + const amount = paramData.amount let maxPrice if (paramData.maxPrice) { @@ -282,7 +277,7 @@ router.post('/buy', async (req, res) => { latency: latency(initTime, Date.now()), base: baseTokenAddress, quote: quoteTokenAddress, - amount: parseFloat(paramData.amount), + amount: amount, expectedIn: expectedIn.toSignificant(8), price: price, gasUsed: parseInt(txObj.gasUsed), diff --git a/src/services/uniswap.js b/src/services/uniswap.js index a2863b3..2d41e80 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -48,7 +48,7 @@ export default class Uniswap { async priceSwapIn (tokenIn, tokenOut, tokenInAmount) { const tIn = await uni.Fetcher.fetchTokenData(this.chainID, tokenIn) - const tokenAmountIn = new uni.TokenAmount(tIn, tokenInAmount) + 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) @@ -57,7 +57,7 @@ export default class Uniswap { async priceSwapOut (tokenIn, tokenOut, tokenOutAmount) { const tOut = await uni.Fetcher.fetchTokenData(this.chainID, tokenOut) - const tokenAmountOut = new uni.TokenAmount(tOut, tokenOutAmount) + 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) From 36ae4b4465ba102044e11cfc1fbfaa14065f34c5 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 24 Nov 2020 15:28:42 -0800 Subject: [PATCH 29/41] (fix) minor fixes + changed gasLimit to 150k --- src/routes/uniswap.route.js | 11 ++++++----- src/services/uniswap.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index c82c319..129d97d 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -61,7 +61,7 @@ router.post('/sell-price', async (req, res) => { amount ) - if (trade != null && expectedOut != null) { + if (trade !== null && expectedOut !== null) { res.status(200).json({ network: uniswap.network, timestamp: initTime, @@ -71,7 +71,7 @@ router.post('/sell-price', async (req, res) => { amount: amount, expectedOut: expectedOut.toSignificant(8), price: trade.executionPrice.toSignificant(8), - swaps: trade, + trade: trade, }) } else { // no pool available res.status(200).json({ @@ -80,6 +80,7 @@ router.post('/sell-price', async (req, res) => { }) } } catch (err) { + debug(err) let reason err.reason ? reason = err.reason : reason = statusMessages.operation_error res.status(500).json({ @@ -112,7 +113,7 @@ router.post('/buy-price', async (req, res) => { baseTokenAddress, // tokenOut is base asset amount ) - if (trade != null && expectedIn != null) { + if (trade !== null && expectedIn !== null) { res.status(200).json({ network: uniswap.network, timestamp: initTime, @@ -122,7 +123,7 @@ router.post('/buy-price', async (req, res) => { amount: amount, expectedIn: expectedIn.toSignificant(8), price: trade.executionPrice.invert().toSignificant(8), - swaps: trade, + trade: trade, }) } else { // no pool available res.status(200).json({ @@ -131,7 +132,7 @@ router.post('/buy-price', async (req, res) => { }) } } catch (err) { - console.log(err) + debug(err) let reason err.reason ? reason = err.reason : reason = statusMessages.operation_error res.status(500).json({ diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 2d41e80..39bca1f 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -5,7 +5,7 @@ const debug = require('debug')('router') // constants const ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; -const GAS_LIMIT = 1200000; +const GAS_LIMIT = 150000; const TTL = 60; export default class Uniswap { From 05aae210adcfda20e70560f43c0fda0d2887ad36 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 24 Nov 2020 16:20:58 -0800 Subject: [PATCH 30/41] (cleanup) remove debug statement --- src/routes/balancer.route.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index cd213e8..65dcef2 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -47,7 +47,7 @@ router.post('/sell-price', async (req, res) => { "quote":"0x....." "base":"0x....." "amount":0.1 - "swaps": 4 (optional) + "maxSwaps":4 } */ const initTime = Date.now() @@ -105,6 +105,7 @@ router.post('/buy-price', async (req, res) => { "quote":"0x....." "base":"0x....." "amount":0.1 + "maxSwaps":4 } */ const initTime = Date.now() @@ -163,6 +164,7 @@ router.post('/sell', async (req, res) => { "amount":0.1 "minPrice":1 "gasPrice":10 + "maxSwaps":4 "privateKey":{{privateKey}} } */ @@ -214,8 +216,6 @@ router.post('/sell', async (req, res) => { gasPrice, ) - debug(txObj) - // submit response res.status(200).json({ network: balancer.network, @@ -256,6 +256,7 @@ router.post('/buy', async (req, res) => { "amount":0.1 "maxPrice":1 "gasPrice":10 + "maxSwaps":4 "privateKey":{{privateKey}} } */ From cf789cfa90f499c9981ed7b7e806671e09bac865 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 24 Nov 2020 16:43:42 -0800 Subject: [PATCH 31/41] use custom gasLimit for Uniswap --- src/services/uniswap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 39bca1f..2216074 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -5,7 +5,7 @@ const debug = require('debug')('router') // constants const ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; -const GAS_LIMIT = 150000; +const GAS_LIMIT = 150688; const TTL = 60; export default class Uniswap { From 55b2746685603bb2046e0f42e618a24d2f5dea2c Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 1 Dec 2020 14:34:40 +0800 Subject: [PATCH 32/41] (fix) Remove comments & unused variables --- src/routes/terra.route.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/routes/terra.route.js b/src/routes/terra.route.js index 1ce7c07..74594e9 100644 --- a/src/routes/terra.route.js +++ b/src/routes/terra.route.js @@ -91,9 +91,7 @@ router.post('/price', async (req, res) => { const quoteToken = paramData.quote const tradeType = paramData.trade_type const amount = parseFloat(paramData.amount) - debug('paramData', paramData) - const symbols = [baseToken, quoteToken] let exchangeRate try { @@ -103,8 +101,6 @@ router.post('/price', async (req, res) => { reportConnectionError(res, err) }) - // debug('exchangeRate', exchangeRate) - res.status(200).json( { network: network, @@ -176,9 +172,7 @@ router.post('/trade', async (req, res) => { tradeType: tradeType, quote: quoteToken, amount: amount, - // gasPrice: gasPrice } - // debug('tokenSwaps', tokenSwaps) Object.assign(swapResult, tokenSwaps); res.status(200).json( swapResult From d87bbae858d30b00befe6e93098d2f1260c4e199 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 1 Dec 2020 15:28:45 +0800 Subject: [PATCH 33/41] (feat) Add application label to dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) 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 From d0ecd2f18a84718d1a91278e9c093087a83004af Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 2 Dec 2020 15:26:58 +0100 Subject: [PATCH 34/41] (feat) update eth routes to handle token decimals properlly --- src/routes/eth.route.js | 12 ++++++------ src/services/eth.js | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 77619ef..452bcc3 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -39,7 +39,7 @@ router.post('/balances', async (req, res) => { } let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(separator) + tokenAddressList = JSON.parse(paramData.tokenAddressList) } debug(tokenAddressList) @@ -47,8 +47,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, @@ -94,14 +94,14 @@ router.post('/allowances', async (req, res) => { } let tokenAddressList if (paramData.tokenAddressList) { - tokenAddressList = paramData.tokenAddressList.split(separator) + tokenAddressList = JSON.parse(paramData.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, diff --git a/src/services/eth.js b/src/services/eth.js index 50cdede..b91b420 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -37,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' @@ -51,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' From d5c8914c64e17d10715da2306b227077856be09f Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 3 Dec 2020 20:41:40 -0800 Subject: [PATCH 35/41] (feat) poll for eth tx --- src/routes/balancer.route.js | 12 ++++-------- src/routes/eth.route.js | 30 +++++++++++++++++++++++++++++- src/routes/uniswap.route.js | 12 ++++-------- src/services/balancer.js | 6 ++---- src/services/uniswap.js | 6 ++---- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index f5bb4eb..d38dbdf 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -197,7 +197,7 @@ router.post('/sell', async (req, res) => { 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 @@ -217,9 +217,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({ @@ -286,7 +284,7 @@ router.post('/buy', async (req, res) => { 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 @@ -305,9 +303,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 77619ef..d55f916 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; import express from 'express'; import { getParamData, latency, reportConnectionError, statusMessages } from '../services/utils'; @@ -237,4 +237,32 @@ router.post('/get-weth', async (req, res) => { } }) +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:') + debug(txReceipt) + + 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 + } + + res.status(500).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/uniswap.route.js b/src/routes/uniswap.route.js index 129d97d..d58b6b6 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -184,7 +184,7 @@ router.post('/sell', async (req, res) => { debug(`Price: ${price.toString()}`) if (!maxPrice || price >= maxPrice) { // pass swaps to exchange-proxy to complete trade - const txObj = await uniswap.swapExactIn( + const tx = await uniswap.swapExactIn( wallet, trade, baseTokenAddress, @@ -201,9 +201,7 @@ router.post('/sell', async (req, res) => { amount: amount, expectedOut: expectedOut.toSignificant(8), price: price, - gasUsed: parseInt(txObj.gasUsed), - txHash: txObj.transactionHash, - status: txObj.status, + txHash: tx.hash, }) } else { res.status(200).json({ @@ -264,7 +262,7 @@ router.post('/buy', async (req, res) => { debug(`Price: ${price.toString()}`) if (!maxPrice || price <= maxPrice) { // pass swaps to exchange-proxy to complete trade - const txObj = await uniswap.swapExactOut( + const tx = await uniswap.swapExactOut( wallet, trade, baseTokenAddress, @@ -281,9 +279,7 @@ router.post('/buy', async (req, res) => { amount: amount, expectedIn: expectedIn.toSignificant(8), price: price, - gasUsed: parseInt(txObj.gasUsed), - txHash: txObj.transactionHash, - status: txObj.status, + txHash: tx.hash, }) } else { res.status(200).json({ diff --git a/src/services/balancer.js b/src/services/balancer.js index cee470b..eca16fd 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -143,8 +143,7 @@ export default class Balancer { } ) debug(`Tx Hash: ${tx.hash}`); - const txObj = await tx.wait() - return txObj + return tx } async swapExactOut (wallet, swaps, tokenIn, tokenOut, expectedIn, gasPrice) { @@ -161,7 +160,6 @@ export default class Balancer { } ) debug(`Tx Hash: ${tx.hash}`) - const txObj = await tx.wait() - return txObj + return tx } } diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 2216074..a4bbab3 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -85,8 +85,7 @@ export default class Uniswap { ) debug(`Tx Hash: ${tx.hash}`); - const txObj = await tx.wait() - return txObj + return tx } async swapExactOut (wallet, trade, tokenAddress, gasPrice) { @@ -110,7 +109,6 @@ export default class Uniswap { ) debug(`Tx Hash: ${tx.hash}`); - const txObj = await tx.wait() - return txObj + return tx } } From 638c1da28ce0ce14668887e804da05cd03a40951 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 7 Dec 2020 21:07:03 +0800 Subject: [PATCH 36/41] (feat) Dynamic gas limit based-on max swaps. * Return gas Limit calculation using the maximum swap from request * Add balancer info to /api endpoint --- src/routes/balancer.route.js | 27 ++++++++++++++++++++++++++- src/routes/index.route.js | 3 ++- src/services/balancer.js | 6 +++++- src/services/utils.js | 13 +++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index f5bb4eb..9bbdbe1 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -25,12 +25,30 @@ router.post('/', async (req, res) => { provider: balancer.provider.connection.url, exchangeProxy: balancer.exchangeProxy, subgraphUrl: balancer.subgraphUrl, - gasLimit: balancer.gasLimit, 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 = balancer.gasBase + maxSwaps * balancer.gasPerSwap + + res.status(200).json({ + network: balancer.network, + gasLimit: gasLimit, + timestamp: Date.now(), + }) +}) + router.post('/sell-price', async (req, res) => { /* POST: /sell-price @@ -61,6 +79,8 @@ router.post('/sell-price', async (req, res) => { maxSwaps, ) + const gasLimit = balancer.gasBase + swaps.length * balancer.gasPerSwap + if (swaps != null && expectedOut != null) { res.status(200).json({ network: balancer.network, @@ -71,6 +91,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 @@ -118,6 +139,9 @@ router.post('/buy-price', async (req, res) => { amount, maxSwaps, ) + + const gasLimit = balancer.gasBase + swaps.length * balancer.gasPerSwap + if (swaps != null && expectedIn != null) { res.status(200).json({ network: balancer.network, @@ -128,6 +152,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 diff --git a/src/routes/index.route.js b/src/routes/index.route.js index ac1cc57..1ea698f 100644 --- a/src/routes/index.route.js +++ b/src/routes/index.route.js @@ -1,4 +1,4 @@ -import { statusMessages } from '../services/utils'; +import { loadConfig } from '../services/utils'; const express = require('express'); @@ -8,6 +8,7 @@ router.get('/', (req, res) => { res.status(200).json({ app: process.env.APPNAME, image: process.env.IMAGE, + config: loadConfig(), status: 'ok', }); }) diff --git a/src/services/balancer.js b/src/services/balancer.js index cee470b..d24456e 100644 --- a/src/services/balancer.js +++ b/src/services/balancer.js @@ -16,10 +16,14 @@ const GAS_BASE = 200688; const GAS_PER_SWAP = 100000; export default class Balancer { - constructor (network = 'mainnet') { + constructor (network = 'kovan') { const providerUrl = process.env.ETHEREUM_RPC_URL this.network = process.env.ETHEREUM_CHAIN this.provider = new ethers.providers.JsonRpcProvider(providerUrl) + this.subgraphUrl = process.env.REACT_APP_SUBGRAPH_URL + this.gasBase = GAS_BASE + this.gasPerSwap = GAS_PER_SWAP + this.maxSwaps = MAX_SWAPS switch (network) { case 'mainnet': diff --git a/src/services/utils.js b/src/services/utils.js index 133d34d..03f98df 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -74,3 +74,16 @@ export const getHummingbotMemo = () => { const clientId = process.env.HUMMINGBOT_CLIENT_ID || '' return [prefix, clientId].join('-') } + +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 +} From cd2e744404da18e6db317fe6a436993a861fface Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 8 Dec 2020 17:45:07 +0800 Subject: [PATCH 37/41] (feat) Add gasLimit to uniswap. Default all network to mainnet - Refactor gasLimit function for Balancer --- src/routes/balancer.route.js | 11 ++++++++--- src/routes/uniswap.route.js | 26 ++++++++++++++++---------- src/services/eth.js | 2 +- src/services/uniswap.js | 1 + 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index 9bbdbe1..d707f4b 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -16,6 +16,11 @@ const denomMultiplier = 1e18 const swapMoreThanMaxPriceError = 'Price too high' const swapLessThanMaxPriceError = 'Price too low' +const estimateGasLimit = (maxswaps) => { + const gasLimit = balancer.gasBase + maxswaps * balancer.gasPerSwap + return gasLimit +} + router.post('/', async (req, res) => { /* POST / @@ -40,7 +45,7 @@ router.post('/gas-limit', async (req, res) => { const paramData = getParamData(req.body) const swaps = paramData.maxSwaps const maxSwaps = typeof swaps === 'undefined' || parseInt(swaps) === 0 ? balancer.maxSwaps : parseInt(swaps) - const gasLimit = balancer.gasBase + maxSwaps * balancer.gasPerSwap + const gasLimit = estimateGasLimit(maxSwaps) res.status(200).json({ network: balancer.network, @@ -79,7 +84,7 @@ router.post('/sell-price', async (req, res) => { maxSwaps, ) - const gasLimit = balancer.gasBase + swaps.length * balancer.gasPerSwap + const gasLimit = estimateGasLimit(swaps.length) if (swaps != null && expectedOut != null) { res.status(200).json({ @@ -140,7 +145,7 @@ router.post('/buy-price', async (req, res) => { maxSwaps, ) - const gasLimit = balancer.gasBase + swaps.length * balancer.gasPerSwap + const gasLimit = estimateGasLimit(swaps.length) if (swaps != null && expectedIn != null) { res.status(200).json({ diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 129d97d..004dd8b 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -13,16 +13,9 @@ const uniswap = new Uniswap(process.env.ETHEREUM_CHAIN) 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 = () => { + return uniswap.gasLimit +} router.post('/', async (req, res) => { /* @@ -37,6 +30,19 @@ router.post('/', async (req, res) => { }) }) +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 diff --git a/src/services/eth.js b/src/services/eth.js index 50cdede..bfa9692 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -7,7 +7,7 @@ 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.provider = new ethers.providers.JsonRpcProvider(providerUrl) diff --git a/src/services/uniswap.js b/src/services/uniswap.js index 2216074..b8c870f 100644 --- a/src/services/uniswap.js +++ b/src/services/uniswap.js @@ -15,6 +15,7 @@ export default class Uniswap { 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': From 16c5b22532f07db44f22c665149b37b66f80c4d4 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 10 Dec 2020 23:00:32 +0800 Subject: [PATCH 38/41] (fix) Incorrect http response code 500 on successful tx --- src/routes/eth.route.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/eth.route.js b/src/routes/eth.route.js index 62e499b..36cea3f 100644 --- a/src/routes/eth.route.js +++ b/src/routes/eth.route.js @@ -6,7 +6,6 @@ import Ethereum from '../services/eth'; const router = express.Router() const eth = new Ethereum(process.env.ETHEREUM_CHAIN) -const separator = ',' const spenders = { balancer: process.env.EXCHANGE_PROXY, uniswap: process.env.UNISWAP_ROUTER @@ -243,8 +242,6 @@ router.post('/get-receipt', async (req, res) => { const txHash = paramData.txHash const txReceipt = await eth.provider.getTransactionReceipt(txHash) debug('Tx Receipt:') - debug(txReceipt) - const receipt = {} const confirmed = txReceipt && txReceipt.blockNumber ? true : false if (confirmed) { @@ -254,7 +251,7 @@ router.post('/get-receipt', async (req, res) => { receipt.status = txReceipt.status } - res.status(500).json({ + res.status(200).json({ network: eth.network, timestamp: initTime, latency: latency(initTime, Date.now()), From e269a20c43979a710ec0ea08b961cb64991c7402 Mon Sep 17 00:00:00 2001 From: "Alt.D" Date: Thu, 17 Dec 2020 11:00:45 +0800 Subject: [PATCH 39/41] (feat) Set approval gas limit to 50000 --- src/services/eth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/eth.js b/src/services/eth.js index 1c1f286..cb3151a 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -65,7 +65,7 @@ export default class Ethereum { } // approve a spender to transfer tokens from a wallet address - async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit = this.approvalGasLimit) { + async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit = 50000) { try { // instantiate a contract and pass in wallet, which act on behalf of that signer const contract = new ethers.Contract(tokenAddress, abi.ERC20Abi, wallet) From 871a6f584812316fa6671d95ec972ce59178e76c Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 17 Dec 2020 12:03:22 +0800 Subject: [PATCH 40/41] (fix) No pool http 500 error code. Fixate gas to prevent default value --- src/routes/balancer.route.js | 6 ++---- src/services/eth.js | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/routes/balancer.route.js b/src/routes/balancer.route.js index ae8347f..1de67c9 100644 --- a/src/routes/balancer.route.js +++ b/src/routes/balancer.route.js @@ -84,9 +84,8 @@ router.post('/sell-price', async (req, res) => { maxSwaps, ) - const gasLimit = estimateGasLimit(swaps.length) - if (swaps != null && expectedOut != null) { + const gasLimit = estimateGasLimit(swaps.length) res.status(200).json({ network: balancer.network, timestamp: initTime, @@ -145,9 +144,8 @@ router.post('/buy-price', async (req, res) => { maxSwaps, ) - const gasLimit = estimateGasLimit(swaps.length) - if (swaps != null && expectedIn != null) { + const gasLimit = estimateGasLimit(swaps.length) res.status(200).json({ network: balancer.network, timestamp: initTime, diff --git a/src/services/eth.js b/src/services/eth.js index cb3151a..2bbbe5b 100644 --- a/src/services/eth.js +++ b/src/services/eth.js @@ -65,15 +65,17 @@ export default class Ethereum { } // approve a spender to transfer tokens from a wallet address - async approveERC20 (wallet, spender, tokenAddress, amount, gasPrice = this.gasPrice, gasLimit = 50000) { + 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: gasLimit + gasLimit: approvalGasLimit } ) } catch (err) { From c9543f7eb1d405ff0b80009ea596b38fb65b5c42 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 17 Dec 2020 16:03:10 +0800 Subject: [PATCH 41/41] (refactor) Use HB instance ID memo. Add uniswap insufficient pool msg --- .env.example | 2 +- src/routes/uniswap.route.js | 20 ++++++++++++++++---- src/services/utils.js | 8 ++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index b7a96c6..5fa5b43 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ PORT=5000 # ipv6 format for locahost ["::ffff:127.0.0.1", "::ffff:1", "fe80::1", "::1"] IP_WHITELIST= -HUMMINGBOT_CLIENT_ID={client_id} +HUMMINGBOT_INSTANCE_ID={client_id} # Celo diff --git a/src/routes/uniswap.route.js b/src/routes/uniswap.route.js index 61c721f..7b08aa0 100644 --- a/src/routes/uniswap.route.js +++ b/src/routes/uniswap.route.js @@ -88,8 +88,14 @@ router.post('/sell-price', async (req, res) => { } catch (err) { debug(err) let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ + 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 }) @@ -140,8 +146,14 @@ router.post('/buy-price', async (req, res) => { } catch (err) { debug(err) let reason - err.reason ? reason = err.reason : reason = statusMessages.operation_error - res.status(500).json({ + 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 }) diff --git a/src/services/utils.js b/src/services/utils.js index 03f98df..8b9c9af 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -9,6 +9,7 @@ export const statusMessages = { 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) @@ -71,8 +72,11 @@ export const strToDecimal = (str) => parseInt(str) / 100; export const getHummingbotMemo = () => { const prefix = 'hbot' - const clientId = process.env.HUMMINGBOT_CLIENT_ID || '' - return [prefix, clientId].join('-') + const clientId = process.env.HUMMINGBOT_INSTANCE_ID + if ((typeof clientId !== 'undefined' && clientId != null) && clientId !== '') { + return [prefix, clientId].join('-') + } + return prefix } export const loadConfig = () => {