201 lines
6.8 KiB
JavaScript
201 lines
6.8 KiB
JavaScript
const exec = require('child_process')
|
|
const fs = require('fs')
|
|
const amqp = require('amqplib')
|
|
const crypto = require('crypto')
|
|
const bech32 = require('bech32')
|
|
const BN = require('bignumber.js')
|
|
|
|
const { RABBITMQ_URL, FOREIGN_URL, PROXY_URL } = process.env
|
|
const FOREIGN_ASSET = 'BNB'
|
|
const Transaction = require('./tx')
|
|
const axios = require('axios')
|
|
|
|
const httpClient = axios.create({ baseURL: FOREIGN_URL })
|
|
|
|
async function main () {
|
|
console.log('Connecting to RabbitMQ server')
|
|
const connection = await connectRabbit(RABBITMQ_URL)
|
|
console.log('Connecting to signature events queue')
|
|
const channel = await connection.createChannel()
|
|
const queue = await channel.assertQueue('signQueue')
|
|
|
|
channel.prefetch(1)
|
|
channel.consume(queue.queue, async msg => {
|
|
const data = JSON.parse(msg.content)
|
|
|
|
console.log('Consumed sign event')
|
|
console.log(data)
|
|
const { recipient, value, nonce, epoch } = data
|
|
|
|
if (recipient) {
|
|
const keysFile = `/keys/keys${epoch}.store`
|
|
|
|
const { address, publicKey } = await getAccountFromFile(keysFile)
|
|
console.log(`Tx from ${address}`)
|
|
|
|
const account = await getAccount(address)
|
|
|
|
console.log(`Building corresponding trasfer transaction, nonce ${nonce}, recipient ${recipient}`)
|
|
const tx = new Transaction(address, account.account_number, nonce, recipient, value, FOREIGN_ASSET)
|
|
|
|
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
|
|
|
|
console.log(`Starting signature generation for transaction hash ${hash}`)
|
|
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, keysFile, hash], async () => {
|
|
if (fs.existsSync('signature')) {
|
|
console.log('Finished signature generation')
|
|
const signature = JSON.parse(fs.readFileSync('signature'))
|
|
console.log(signature)
|
|
|
|
console.log('Building signed transaction')
|
|
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
|
|
|
|
console.log('Sending transaction')
|
|
console.log(signedTx)
|
|
await sendTx(signedTx)
|
|
}
|
|
await waitForAccountNonce(address, nonce + 1)
|
|
|
|
channel.ack(msg)
|
|
})
|
|
cmd.stdout.on('data', data => console.log(data.toString()))
|
|
cmd.stderr.on('data', data => console.error(data.toString()))
|
|
} else {
|
|
const accountFile = await waitLastAccountEpoch(epoch)
|
|
|
|
// If new keys with greater epoch already exists
|
|
if (accountFile === null) {
|
|
channel.ack(msg)
|
|
return
|
|
}
|
|
const to = accountFile.address
|
|
const prevEpoch = getPrevEpoch(epoch)
|
|
|
|
const prevKeysFile = `/keys/keys${prevEpoch}.store`
|
|
const { address: from, publicKey } = await getAccountFromFile(prevKeysFile)
|
|
console.log(`Tx from ${from}, to ${to}`)
|
|
|
|
const account = await getAccount(from)
|
|
|
|
const maxValue = new BN(account.balances.find(x => x.symbol === FOREIGN_ASSET).free).minus(new BN(37500).div(10 ** 8))
|
|
console.log(`Building corresponding transaction for transferring all funds, nonce ${account.sequence}, recipient ${to}`)
|
|
const tx = new Transaction(from, account.account_number, account.sequence, to, maxValue, FOREIGN_ASSET)
|
|
|
|
const hash = crypto.createHash('sha256').update(tx.getSignBytes()).digest('hex')
|
|
|
|
fs.unlinkSync('signature')
|
|
|
|
console.log(`Starting signature generation for transaction hash ${hash}`)
|
|
const cmd = exec.execFile('./sign-entrypoint.sh', [PROXY_URL, prevKeysFile, hash], async () => {
|
|
if (fs.existsSync('signature')) {
|
|
console.log('Finished signature generation')
|
|
const signature = JSON.parse(fs.readFileSync('signature'))
|
|
console.log(signature)
|
|
|
|
console.log('Building signed transaction')
|
|
const signedTx = tx.addSignature(publicKey, { r: signature[1], s: signature[3] })
|
|
|
|
console.log('Sending transaction')
|
|
console.log(signedTx)
|
|
await sendTx(signedTx)
|
|
}
|
|
await waitForAccountNonce(from, account.sequence + 1)
|
|
|
|
await confirm(`/keys/keys${epoch}.store`)
|
|
|
|
channel.ack(msg)
|
|
})
|
|
cmd.stdout.on('data', data => console.log(data.toString()))
|
|
cmd.stderr.on('data', data => console.error(data.toString()))
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
main()
|
|
|
|
async function connectRabbit (url) {
|
|
return amqp.connect(url).catch(() => {
|
|
console.log('Failed to connect, reconnecting')
|
|
return new Promise(resolve =>
|
|
setTimeout(() => resolve(connectRabbit(url)), 1000)
|
|
)
|
|
})
|
|
}
|
|
|
|
async function confirm (keysFile) {
|
|
exec.execSync(`curl -X POST -H "Content-Type: application/json" -d @"${keysFile}" "${PROXY_URL}/confirm"`, { stdio: 'pipe' })
|
|
}
|
|
|
|
async function getAccountFromFile (file) {
|
|
console.log(`Reading ${file}`)
|
|
while (!fs.existsSync(file)) {
|
|
console.log('Waiting for needed epoch key', file)
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
const publicKey = JSON.parse(fs.readFileSync(file))[5]
|
|
return {
|
|
address: publicKeyToAddress(publicKey),
|
|
publicKey: publicKey
|
|
}
|
|
}
|
|
|
|
function getPrevEpoch (epoch) {
|
|
return Math.max(0, ...fs.readdirSync('/keys').map(x => parseInt(x.split('.')[0].substr(4))).filter(x => x < epoch))
|
|
}
|
|
|
|
async function waitLastAccountEpoch (epoch) {
|
|
while (true) {
|
|
const curEpoch = Math.max(0, ...fs.readdirSync('/keys').map(x => parseInt(x.split('.')[0].substr(4))))
|
|
if (curEpoch === epoch)
|
|
return getAccountFromFile(`/keys/keys${epoch}.store`)
|
|
else if (curEpoch > epoch)
|
|
return null
|
|
else
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
}
|
|
|
|
async function waitForAccountNonce (address, nonce) {
|
|
console.log(`Waiting for account ${address} to have nonce ${nonce}`)
|
|
while (true) {
|
|
const sequence = (await getAccount(address)).sequence
|
|
if (sequence === nonce)
|
|
break
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
console.log('Waiting for needed account nonce')
|
|
}
|
|
console.log('Account nonce is OK')
|
|
}
|
|
|
|
async function getAccount (address) {
|
|
console.log(`Getting account ${address} data`)
|
|
return httpClient
|
|
.get(`/api/v1/account/${address}`)
|
|
.then(res => res.data)
|
|
}
|
|
|
|
async function sendTx (tx) {
|
|
return httpClient
|
|
.post(`/api/v1/broadcast?sync=true`, tx, {
|
|
headers: {
|
|
'Content-Type': 'text/plain'
|
|
}
|
|
})
|
|
.then(x => console.log(x.response), x => console.log(x.response))
|
|
}
|
|
|
|
function publicKeyToAddress ({ x, y }) {
|
|
const compact = (parseInt(y[y.length - 1], 16) % 2 ? '03' : '02') + padZeros(x, 64)
|
|
const sha256Hash = crypto.createHash('sha256').update(Buffer.from(compact, 'hex')).digest('hex')
|
|
const hash = crypto.createHash('ripemd160').update(Buffer.from(sha256Hash, 'hex')).digest('hex')
|
|
const words = bech32.toWords(Buffer.from(hash, 'hex'))
|
|
return bech32.encode('tbnb', words)
|
|
}
|
|
|
|
function padZeros (s, len) {
|
|
while (s.length < len)
|
|
s = '0' + s
|
|
return s
|
|
}
|