commit
55d26d1bf3
@ -1,5 +1,7 @@
|
||||
NET_ID=42
|
||||
RPC_URL=https://kovan.infura.io
|
||||
# ORACLE_RPC_URL should always point to mainnet
|
||||
ORACLE_RPC_URL=https://mainnet.infura.io
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
|
||||
# without 0x prefix
|
||||
|
28
abis/PriceOracle.abi.json
Normal file
28
abis/PriceOracle.abi.json
Normal file
@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract IERC20[]",
|
||||
"name": "fromTokens",
|
||||
"type": "address[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "oneUnitAmounts",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"name": "getPricesInETH",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "prices",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
141
config.js
141
config.js
@ -1,51 +1,146 @@
|
||||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
version: '1.2',
|
||||
netId: Number(process.env.NET_ID) || 42,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51',
|
||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
|
||||
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
|
||||
oracleAddress: '0xB5eE7907FF5f4c1FC9086Fc117E6c397431F39ad',
|
||||
privateKey: process.env.PRIVATE_KEY,
|
||||
mixers: {
|
||||
netId1: {
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||
'500': undefined,
|
||||
'1000': undefined,
|
||||
'5000': undefined
|
||||
},
|
||||
tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18
|
||||
},
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||
'100': undefined
|
||||
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291'
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
||||
symbol: 'DAI',
|
||||
decimals: 18
|
||||
},
|
||||
cdai: {
|
||||
mixerAddress: {
|
||||
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
||||
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8
|
||||
},
|
||||
usdc: {
|
||||
mixerAddress: {
|
||||
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
symbol: 'USDC',
|
||||
decimals: 6
|
||||
},
|
||||
cusdc: {
|
||||
mixerAddress: {
|
||||
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
||||
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
||||
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8
|
||||
},
|
||||
usdt: {
|
||||
mixerAddress: {
|
||||
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
||||
'100000': '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E'
|
||||
},
|
||||
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
symbol: 'USDT',
|
||||
decimals: 6
|
||||
}
|
||||
},
|
||||
netId42: {
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0x7832f65F4030113C007538f29AbC6C13241d4478',
|
||||
'500': undefined,
|
||||
'1000': undefined,
|
||||
'5000': undefined
|
||||
},
|
||||
tokenAddress: '0x8c158c7e57161dd4d3cb02bf1a3a97fcc78b75fd',
|
||||
decimals: 18
|
||||
},
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
||||
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
||||
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
||||
'100': undefined
|
||||
'100': '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D'
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
||||
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
||||
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
|
||||
symbol: 'DAI',
|
||||
decimals: 18
|
||||
},
|
||||
cdai: {
|
||||
mixerAddress: {
|
||||
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
||||
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
||||
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8
|
||||
},
|
||||
usdc: {
|
||||
mixerAddress: {
|
||||
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
||||
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
||||
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
|
||||
symbol: 'USDC',
|
||||
decimals: 6
|
||||
},
|
||||
cusdc: {
|
||||
mixerAddress: {
|
||||
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
||||
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
||||
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8
|
||||
},
|
||||
usdt: {
|
||||
mixerAddress: {
|
||||
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
|
||||
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
||||
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
||||
'100000': '0x14aEd24B67EaF3FF28503eB92aeb217C47514364'
|
||||
},
|
||||
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
|
||||
symbol: 'USDT',
|
||||
decimals: 6
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -12,7 +12,6 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bull": "^3.12.1",
|
||||
"coingecko-api": "^1.0.6",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"ioredis": "^4.14.1",
|
||||
|
@ -1,37 +1,46 @@
|
||||
const CoinGecko = require('coingecko-api')
|
||||
const fetch = require('node-fetch')
|
||||
const { toWei } = require('web3-utils')
|
||||
const { gasOracleUrls, defaultGasPrice } = require('../config')
|
||||
const { getMainnetTokens } = require('./utils')
|
||||
const Web3 = require('web3')
|
||||
const { gasOracleUrls, defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config')
|
||||
const { getArgsForOracle } = require('./utils')
|
||||
const { redisClient } = require('./redis')
|
||||
const priceOracleABI = require('../abis/PriceOracle.abi.json')
|
||||
|
||||
class Fetcher {
|
||||
constructor(web3) {
|
||||
this.web3 = web3
|
||||
this.oracleWeb3 = new Web3(oracleRpcUrl)
|
||||
this.oracle = new this.oracleWeb3.eth.Contract(priceOracleABI, oracleAddress)
|
||||
this.ethPrices = {
|
||||
dai: '6700000000000000' // 0.0067
|
||||
dai: '6700000000000000', // 0.0067
|
||||
cdai: '157380000000000',
|
||||
cusdc: '164630000000000',
|
||||
usdc: '7878580000000000',
|
||||
usdt: '7864940000000000'
|
||||
}
|
||||
this.tokenAddresses
|
||||
this.oneUintAmount
|
||||
this.currencyLookup
|
||||
this.gasPrices = {
|
||||
fast: defaultGasPrice
|
||||
}
|
||||
|
||||
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
|
||||
this.tokenAddresses = tokenAddresses
|
||||
this.oneUintAmount = oneUintAmount
|
||||
this.currencyLookup = currencyLookup
|
||||
}
|
||||
async fetchPrices() {
|
||||
const { tokenAddresses, currencyLookup } = getMainnetTokens()
|
||||
try {
|
||||
const CoinGeckoClient = new CoinGecko()
|
||||
const price = await CoinGeckoClient.simple.fetchTokenPrice({
|
||||
contract_addresses: tokenAddresses,
|
||||
vs_currencies: 'eth',
|
||||
assetPlatform: 'ethereum'
|
||||
})
|
||||
this.ethPrices = Object.entries(price.data).reduce((acc, token) => {
|
||||
if (token[1].eth) {
|
||||
acc[currencyLookup[token[0]]] = toWei(token[1].eth.toString())
|
||||
}
|
||||
let prices = await this.oracle.methods
|
||||
.getPricesInETH(this.tokenAddresses, this.oneUintAmount)
|
||||
.call()
|
||||
this.ethPrices = prices.reduce((acc, price, i) => {
|
||||
acc[this.currencyLookup[this.tokenAddresses[i]]] = price
|
||||
return acc
|
||||
}, {})
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
} catch(e) {
|
||||
console.error('fetchPrices', e)
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
}
|
||||
}
|
||||
|
13
src/index.js
13
src/index.js
@ -1,5 +1,5 @@
|
||||
const express = require('express')
|
||||
const { netId, port, relayerServiceFee } = require('../config')
|
||||
const { netId, port, relayerServiceFee, version } = require('../config')
|
||||
const relayController = require('./relayController')
|
||||
const { fetcher, web3 } = require('./instances')
|
||||
const { getMixers } = require('./utils')
|
||||
@ -31,7 +31,16 @@ app.get('/', function (req, res) {
|
||||
app.get('/status', async function (req, res) {
|
||||
let nonce = await redisClient.get('nonce')
|
||||
const { ethPrices, gasPrices } = fetcher
|
||||
res.json({ relayerAddress: web3.eth.defaultAccount, mixers, gasPrices, netId, ethPrices, relayerServiceFee, nonce })
|
||||
res.json({
|
||||
relayerAddress: web3.eth.defaultAccount,
|
||||
mixers,
|
||||
gasPrices,
|
||||
netId,
|
||||
ethPrices,
|
||||
relayerServiceFee,
|
||||
nonce,
|
||||
version
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/relay', relayController)
|
||||
|
@ -83,6 +83,7 @@ withdrawQueue.process(async function(job, done){
|
||||
error: 'The note has been spent.'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
|
||||
if (!isKnownRoot) {
|
||||
@ -92,6 +93,7 @@ withdrawQueue.process(async function(job, done){
|
||||
error: 'The merkle root is too old or invalid.'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({
|
||||
@ -150,7 +152,9 @@ async function sendTx(tx, done, retryAttempt = 1) {
|
||||
}).on('error', async function(e){
|
||||
console.log('error', e.message)
|
||||
if(e.message === 'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.'
|
||||
|| e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.') {
|
||||
|| e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.'
|
||||
|| e.message === 'Returned error: nonce too low'
|
||||
|| e.message === 'Returned error: replacement transaction underpriced') {
|
||||
console.log('nonce too low, retrying')
|
||||
if(retryAttempt <= 10) {
|
||||
retryAttempt++
|
||||
|
81
src/utils.js
81
src/utils.js
@ -1,4 +1,4 @@
|
||||
const { isHexStrict, toBN, toWei } = require('web3-utils')
|
||||
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
|
||||
const { netId, mixers, relayerServiceFee } = require('../config')
|
||||
|
||||
function isValidProof(proof) {
|
||||
@ -59,9 +59,62 @@ function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function fromDecimals(value, decimals) {
|
||||
value = value.toString()
|
||||
let ether = value.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points'
|
||||
)
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error(
|
||||
'[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places'
|
||||
)
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
}
|
||||
|
||||
function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
|
||||
// TODO tokens can have less then 18 decimals
|
||||
const feePercent = toBN(toWei(amount)).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
|
||||
const { decimals } = mixers[`netId${netId}`][currency]
|
||||
const feePercent = toBN(fromDecimals(amount, decimals)).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
|
||||
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
@ -69,37 +122,43 @@ function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'dai': {
|
||||
default: {
|
||||
desiredFee =
|
||||
expense.add(refund)
|
||||
.mul(toBN(10 ** 18))
|
||||
.div(toBN(ethPrices.dai))
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrices[currency]))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log('desired fee, feePercent', desiredFee.toString(), feePercent.toString())
|
||||
console.log('sent fee, desired fee, feePercent', fee.toString(), desiredFee.toString(), feePercent.toString())
|
||||
if (fee.lt(desiredFee)) {
|
||||
return { isEnough: false, reason: 'Not enough fee' }
|
||||
}
|
||||
return { isEnough: true }
|
||||
}
|
||||
|
||||
function getMainnetTokens() {
|
||||
function getArgsForOracle() {
|
||||
const tokens = mixers['netId1']
|
||||
const tokenAddresses = []
|
||||
const oneUintAmount = []
|
||||
const currencyLookup = {}
|
||||
Object.entries(tokens).map(([currency, data]) => {
|
||||
if (currency !== 'eth') {
|
||||
tokenAddresses.push(data.tokenAddress)
|
||||
currencyLookup[data.tokenAddress.toLowerCase()] = currency
|
||||
oneUintAmount.push(
|
||||
toBN('10')
|
||||
.pow(toBN(data.decimals.toString()))
|
||||
.toString()
|
||||
)
|
||||
currencyLookup[data.tokenAddress] = currency
|
||||
}
|
||||
})
|
||||
return { tokenAddresses, currencyLookup }
|
||||
return { tokenAddresses, oneUintAmount, currencyLookup }
|
||||
}
|
||||
|
||||
function getMixers() {
|
||||
return mixers[`netId${netId}`]
|
||||
}
|
||||
|
||||
module.exports = { isValidProof, isValidArgs, sleep, isKnownContract, isEnoughFee, getMixers, getMainnetTokens }
|
||||
module.exports = { isValidProof, isValidArgs, sleep, isKnownContract, isEnoughFee, getMixers, getArgsForOracle }
|
||||
|
@ -477,11 +477,6 @@ cluster-key-slot@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
||||
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
|
||||
|
||||
coingecko-api@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/coingecko-api/-/coingecko-api-1.0.6.tgz#ecc42eb96fb1cc721e319c3d06244a642394ab34"
|
||||
integrity sha512-6oJ3aB9F4AlsHQQ4F5N9753FGmMQrr12aGJl79KSfdNOFJ7wvFLSqsBoOBzgYJEab3hJ7cCWnmo2dMJs6KsA3A==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
Loading…
Reference in New Issue
Block a user