2019-07-04 02:32:01 -07:00
|
|
|
const exec = require('child_process')
|
|
|
|
const fs = require('fs')
|
2019-07-07 12:58:35 -07:00
|
|
|
const BN = require('bignumber.js')
|
2019-07-15 12:41:02 -07:00
|
|
|
const express = require('express')
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
const logger = require('./logger')
|
2019-10-08 09:06:10 -07:00
|
|
|
const { connectRabbit, assertQueue } = require('./amqp')
|
2019-10-08 10:45:28 -07:00
|
|
|
const { publicKeyToAddress, sha256 } = require('./crypto')
|
2019-10-06 03:36:29 -07:00
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
const app = express()
|
|
|
|
app.get('/restart/:attempt', restart)
|
2019-10-11 02:39:40 -07:00
|
|
|
app.get('/start', (req, res) => {
|
|
|
|
logger.info('Ready to start')
|
|
|
|
ready = true
|
|
|
|
res.send()
|
|
|
|
})
|
2019-10-06 03:36:29 -07:00
|
|
|
app.listen(8001, () => logger.debug('Listening on 8001'))
|
2019-07-15 12:41:02 -07:00
|
|
|
|
|
|
|
const { RABBITMQ_URL, FOREIGN_URL, PROXY_URL, FOREIGN_ASSET } = process.env
|
2019-07-04 02:32:01 -07:00
|
|
|
const Transaction = require('./tx')
|
|
|
|
const axios = require('axios')
|
|
|
|
|
|
|
|
const httpClient = axios.create({ baseURL: FOREIGN_URL })
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
let attempt
|
|
|
|
let nextAttempt = null
|
|
|
|
let cancelled
|
2019-10-11 02:39:40 -07:00
|
|
|
let ready = false
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-07-04 02:32:01 -07:00
|
|
|
async function main () {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Connecting to RabbitMQ server')
|
2019-10-08 09:06:10 -07:00
|
|
|
const channel = await connectRabbit(RABBITMQ_URL)
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Connecting to signature events queue')
|
2019-10-08 09:06:10 -07:00
|
|
|
const signQueue = await assertQueue(channel, 'signQueue')
|
2019-07-04 02:32:01 -07:00
|
|
|
|
2019-10-11 02:39:40 -07:00
|
|
|
while (!ready) {
|
|
|
|
await new Promise(res => setTimeout(res, 1000))
|
|
|
|
}
|
|
|
|
|
2019-07-04 02:32:01 -07:00
|
|
|
channel.prefetch(1)
|
2019-10-08 09:06:10 -07:00
|
|
|
signQueue.consume(async msg => {
|
2019-07-04 02:32:01 -07:00
|
|
|
const data = JSON.parse(msg.content)
|
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Consumed sign event: %o', data)
|
2019-07-15 12:41:02 -07:00
|
|
|
const { recipient, value, nonce, epoch, newEpoch, parties, threshold } = data
|
|
|
|
|
|
|
|
const keysFile = `/keys/keys${epoch}.store`
|
2019-10-02 11:44:52 -07:00
|
|
|
const { address: from, publicKey } = getAccountFromFile(keysFile)
|
|
|
|
if (from === '') {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('No keys found, acking message')
|
2019-10-02 11:44:52 -07:00
|
|
|
channel.ack(msg)
|
|
|
|
return
|
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
const account = await getAccount(from)
|
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('Writing params')
|
2019-07-15 12:41:02 -07:00
|
|
|
fs.writeFileSync('./params', JSON.stringify({ parties: parties.toString(), threshold: threshold.toString() }))
|
|
|
|
|
|
|
|
attempt = 1
|
|
|
|
|
|
|
|
if (recipient && account.sequence <= nonce) {
|
|
|
|
while (true) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Building corresponding transfer transaction, nonce ${nonce}, recipient ${recipient}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
const tx = new Transaction({
|
|
|
|
from,
|
|
|
|
accountNumber: account.account_number,
|
|
|
|
sequence: nonce,
|
|
|
|
to: recipient,
|
|
|
|
tokens: value,
|
|
|
|
asset: FOREIGN_ASSET,
|
|
|
|
memo: `Attempt ${attempt}`
|
|
|
|
})
|
|
|
|
|
2019-10-08 10:45:28 -07:00
|
|
|
const hash = sha256(tx.getSignBytes())
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Starting signature generation for transaction hash ${hash}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
break
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
attempt = nextAttempt ? nextAttempt : attempt + 1
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.warn(`Sign failed, starting next attempt ${attempt}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
nextAttempt = null
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
} else if (account.sequence <= nonce) {
|
|
|
|
const newKeysFile = `/keys/keys${newEpoch}.store`
|
2019-10-02 11:44:52 -07:00
|
|
|
const { address: to } = getAccountFromFile(newKeysFile)
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-10-02 11:44:52 -07:00
|
|
|
while (to !== '') {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Building corresponding transaction for transferring all funds, nonce ${nonce}, recipient ${to}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
const tx = new Transaction({
|
|
|
|
from,
|
|
|
|
accountNumber: account.account_number,
|
|
|
|
sequence: nonce,
|
|
|
|
to,
|
|
|
|
tokens: account.balances.find(x => x.symbol === FOREIGN_ASSET).free,
|
|
|
|
asset: FOREIGN_ASSET,
|
|
|
|
bnbs: new BN(account.balances.find(x => x.symbol === 'BNB').free).minus(new BN(60000).div(10 ** 8)),
|
|
|
|
memo: `Attempt ${attempt}`
|
|
|
|
})
|
|
|
|
|
2019-10-08 10:45:28 -07:00
|
|
|
const hash = sha256(tx.getSignBytes())
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Starting signature generation for transaction hash ${hash}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
const done = await sign(keysFile, hash, tx, publicKey) && await waitForAccountNonce(from, nonce + 1)
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
await confirmFundsTransfer()
|
|
|
|
break
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
attempt = nextAttempt ? nextAttempt : attempt + 1
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.warn(`Sign failed, starting next attempt ${attempt}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
nextAttempt = null
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
|
|
}
|
2019-10-02 11:44:52 -07:00
|
|
|
} else {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('Tx has been already sent')
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Acking message')
|
2019-07-15 12:41:02 -07:00
|
|
|
channel.ack(msg)
|
2019-07-04 02:32:01 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
main()
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
function sign (keysFile, hash, tx, publicKey) {
|
|
|
|
return new Promise(resolve => {
|
2019-10-02 11:44:52 -07:00
|
|
|
const cmd = exec.execFile('./sign-entrypoint.sh', [ PROXY_URL, keysFile, hash ], async (error) => {
|
2019-07-15 12:41:02 -07:00
|
|
|
if (fs.existsSync('signature')) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Finished signature generation')
|
2019-07-15 12:41:02 -07:00
|
|
|
const signature = JSON.parse(fs.readFileSync('signature'))
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('%o', signature)
|
2019-07-15 12:41:02 -07:00
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Building signed transaction')
|
2019-07-15 12:41:02 -07:00
|
|
|
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
|
|
|
|
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Sending transaction')
|
|
|
|
logger.debug(signedTx)
|
2019-07-15 12:41:02 -07:00
|
|
|
await sendTx(signedTx)
|
|
|
|
resolve(true)
|
|
|
|
} else if (error === null || error.code === 0) {
|
|
|
|
resolve(true)
|
|
|
|
} else {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.warn('Sign failed')
|
2019-07-15 12:41:02 -07:00
|
|
|
resolve(false)
|
|
|
|
}
|
|
|
|
})
|
2019-10-06 03:36:29 -07:00
|
|
|
cmd.stdout.on('data', data => logger.debug(data.toString()))
|
|
|
|
cmd.stderr.on('data', data => logger.debug(data.toString()))
|
2019-07-15 12:41:02 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function restart (req, res) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Cancelling current sign')
|
2019-07-15 12:41:02 -07:00
|
|
|
nextAttempt = req.params.attempt
|
|
|
|
exec.execSync('pkill gg18_sign || true')
|
|
|
|
cancelled = true
|
|
|
|
res.send('Cancelled')
|
|
|
|
}
|
|
|
|
|
|
|
|
function confirmFundsTransfer () {
|
|
|
|
exec.execSync(`curl -X POST -H "Content-Type: application/json" "${PROXY_URL}/confirmFundsTransfer"`, { stdio: 'pipe' })
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
|
|
|
|
2019-10-02 11:44:52 -07:00
|
|
|
function getAccountFromFile (file) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug(`Reading ${file}`)
|
2019-10-02 11:44:52 -07:00
|
|
|
if (!fs.existsSync(file)) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('No keys found, skipping')
|
2019-10-02 11:44:52 -07:00
|
|
|
return { address: '' }
|
2019-07-04 02:32:01 -07:00
|
|
|
}
|
|
|
|
const publicKey = JSON.parse(fs.readFileSync(file))[5]
|
|
|
|
return {
|
|
|
|
address: publicKeyToAddress(publicKey),
|
|
|
|
publicKey: publicKey
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-07 12:58:35 -07:00
|
|
|
async function waitForAccountNonce (address, nonce) {
|
2019-07-15 12:41:02 -07:00
|
|
|
cancelled = false
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Waiting for account ${address} to have nonce ${nonce}`)
|
2019-07-15 12:41:02 -07:00
|
|
|
while (!cancelled) {
|
2019-07-07 12:58:35 -07:00
|
|
|
const sequence = (await getAccount(address)).sequence
|
2019-07-15 12:41:02 -07:00
|
|
|
if (sequence >= nonce)
|
2019-07-07 12:58:35 -07:00
|
|
|
break
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('Waiting for needed account nonce')
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Account nonce is OK')
|
2019-07-15 12:41:02 -07:00
|
|
|
return !cancelled
|
2019-07-07 12:58:35 -07:00
|
|
|
}
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
function getAccount (address) {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info(`Getting account ${address} data`)
|
2019-07-04 02:32:01 -07:00
|
|
|
return httpClient
|
|
|
|
.get(`/api/v1/account/${address}`)
|
|
|
|
.then(res => res.data)
|
2019-07-15 12:41:02 -07:00
|
|
|
.catch(() => {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('Retrying')
|
2019-07-15 12:41:02 -07:00
|
|
|
return getAccount(address)
|
|
|
|
})
|
2019-07-04 02:32:01 -07:00
|
|
|
}
|
|
|
|
|
2019-07-15 12:41:02 -07:00
|
|
|
function sendTx (tx) {
|
2019-07-04 02:32:01 -07:00
|
|
|
return httpClient
|
|
|
|
.post(`/api/v1/broadcast?sync=true`, tx, {
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'text/plain'
|
|
|
|
}
|
|
|
|
})
|
2019-07-15 12:41:02 -07:00
|
|
|
.catch(err => {
|
|
|
|
if (err.response.data.message.includes('Tx already exists in cache'))
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.debug('Tx already exists in cache')
|
2019-08-24 14:26:16 -07:00
|
|
|
else {
|
2019-10-06 03:36:29 -07:00
|
|
|
logger.info('Something failed, restarting: %o', err.response)
|
2019-08-24 14:26:16 -07:00
|
|
|
return new Promise(resolve => setTimeout(() => resolve(sendTx(tx)), 1000))
|
|
|
|
}
|
2019-07-15 12:41:02 -07:00
|
|
|
})
|
2019-07-04 02:32:01 -07:00
|
|
|
}
|