amb-mediators/deploy/src/deploymentUtils.js

269 lines
7.1 KiB
JavaScript

/* eslint-disable no-param-reassign */
const BigNumber = require('bignumber.js')
const Web3 = require('web3')
const Tx = require('ethereumjs-tx')
const Web3Utils = require('web3').utils
const fetch = require('node-fetch')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const {
web3Home,
web3Foreign,
deploymentPrivateKey,
FOREIGN_RPC_URL,
HOME_RPC_URL,
GAS_LIMIT_EXTRA,
HOME_DEPLOYMENT_GAS_PRICE,
FOREIGN_DEPLOYMENT_GAS_PRICE,
GET_RECEIPT_INTERVAL_IN_MILLISECONDS,
HOME_EXPLORER_URL,
FOREIGN_EXPLORER_URL,
HOME_EXPLORER_API_KEY,
FOREIGN_EXPLORER_API_KEY
} = require('./web3')
const verifier = require('./utils/verifier')
async function deployContract(contractJson, args, { from, network, nonce }) {
let web3
let url
let gasPrice
let apiUrl
let apiKey
if (network === 'foreign') {
web3 = web3Foreign
url = FOREIGN_RPC_URL
gasPrice = FOREIGN_DEPLOYMENT_GAS_PRICE
apiUrl = FOREIGN_EXPLORER_URL
apiKey = FOREIGN_EXPLORER_API_KEY
} else {
web3 = web3Home
url = HOME_RPC_URL
gasPrice = HOME_DEPLOYMENT_GAS_PRICE
apiUrl = HOME_EXPLORER_URL
apiKey = HOME_EXPLORER_API_KEY
}
const options = {
from
}
const instance = new web3.eth.Contract(contractJson.abi, options)
const result = await instance
.deploy({
data: contractJson.bytecode,
arguments: args
})
.encodeABI()
const tx = await sendRawTx({
data: result,
nonce: Web3Utils.toHex(nonce),
to: null,
privateKey: deploymentPrivateKey,
url,
gasPrice
})
if (Web3Utils.hexToNumber(tx.status) !== 1 && !tx.contractAddress) {
throw new Error('Tx failed')
}
instance.options.address = tx.contractAddress
instance.deployedBlockNumber = tx.blockNumber
if (apiUrl) {
let constructorArguments
if (args.length) {
constructorArguments = result.substring(contractJson.bytecode.length)
}
await verifier({ artifact: contractJson, constructorArguments, address: tx.contractAddress, apiUrl, apiKey })
}
return instance
}
async function sendRawTxHome(options) {
return sendRawTx({
...options,
gasPrice: HOME_DEPLOYMENT_GAS_PRICE
})
}
async function sendRawTxForeign(options) {
return sendRawTx({
...options,
gasPrice: FOREIGN_DEPLOYMENT_GAS_PRICE
})
}
async function sendRawTx({ data, nonce, to, privateKey, url, gasPrice, value }) {
try {
const txToEstimateGas = {
from: privateKeyToAddress(Web3Utils.bytesToHex(privateKey)),
value,
to,
data
}
const estimatedGas = BigNumber(await sendNodeRequest(url, 'eth_estimateGas', txToEstimateGas))
const blockData = await sendNodeRequest(url, 'eth_getBlockByNumber', ['latest', false])
const blockGasLimit = BigNumber(blockData.gasLimit)
if (estimatedGas.isGreaterThan(blockGasLimit)) {
throw new Error(
`estimated gas greater (${estimatedGas.toString()}) than the block gas limit (${blockGasLimit.toString()})`
)
}
let gas = estimatedGas.multipliedBy(BigNumber(1 + GAS_LIMIT_EXTRA))
if (gas.isGreaterThan(blockGasLimit)) {
gas = blockGasLimit
} else {
gas = gas.toFixed(0)
}
const rawTx = {
nonce,
gasPrice: Web3Utils.toHex(gasPrice),
gasLimit: Web3Utils.toHex(gas),
to,
data,
value
}
const tx = new Tx(rawTx)
tx.sign(privateKey)
const serializedTx = tx.serialize()
const txHash = await sendNodeRequest(url, 'eth_sendRawTransaction', `0x${serializedTx.toString('hex')}`)
console.log('pending txHash', txHash)
return await getReceipt(txHash, url)
} catch (e) {
console.error(e)
}
}
async function sendNodeRequest(url, method, signedData) {
if (!Array.isArray(signedData)) {
signedData = [signedData]
}
const request = await fetch(url, {
headers: {
'Content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method,
params: signedData,
id: 1
})
})
const json = await request.json()
if (typeof json.error === 'undefined' || json.error === null) {
if (method === 'eth_sendRawTransaction') {
assert.strictEqual(json.result.length, 66, `Tx wasn't sent ${json}`)
}
return json.result
}
throw new Error(`web3 RPC failed: ${JSON.stringify(json.error)}`)
}
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function getReceipt(txHash, url) {
await timeout(GET_RECEIPT_INTERVAL_IN_MILLISECONDS)
let receipt = await sendNodeRequest(url, 'eth_getTransactionReceipt', txHash)
if (receipt === null || receipt.blockNumber === null) {
receipt = await getReceipt(txHash, url)
}
return receipt
}
function add0xPrefix(s) {
if (s.indexOf('0x') === 0) {
return s
}
return `0x${s}`
}
function privateKeyToAddress(privateKey) {
return new Web3().eth.accounts.privateKeyToAccount(add0xPrefix(privateKey)).address
}
async function upgradeProxy({ proxy, implementationAddress, version, nonce, url }) {
const data = await proxy.methods.upgradeTo(version, implementationAddress).encodeABI()
const sendTx = getSendTxMethod(url)
const result = await sendTx({
data,
nonce,
to: proxy.options.address,
privateKey: deploymentPrivateKey,
url
})
if (result.status) {
assert.strictEqual(Web3Utils.hexToNumber(result.status), 1, 'Transaction Failed')
} else {
await assertStateWithRetry(proxy.methods.implementation().call, implementationAddress)
}
}
async function transferProxyOwnership({ proxy, newOwner, nonce, url }) {
const data = await proxy.methods.transferProxyOwnership(newOwner).encodeABI()
const sendTx = getSendTxMethod(url)
const result = await sendTx({
data,
nonce,
to: proxy.options.address,
privateKey: deploymentPrivateKey,
url
})
if (result.status) {
assert.strictEqual(Web3Utils.hexToNumber(result.status), 1, 'Transaction Failed')
} else {
await assertStateWithRetry(proxy.methods.proxyOwner().call, newOwner)
}
}
async function transferOwnership({ contract, newOwner, nonce, url }) {
const data = await contract.methods.transferOwnership(newOwner).encodeABI()
const sendTx = getSendTxMethod(url)
const result = await sendTx({
data,
nonce,
to: contract.options.address,
privateKey: deploymentPrivateKey,
url
})
if (result.status) {
assert.strictEqual(Web3Utils.hexToNumber(result.status), 1, 'Transaction Failed')
} else {
await assertStateWithRetry(contract.methods.owner().call, newOwner)
}
}
async function assertStateWithRetry(fn, expected) {
return promiseRetry(async retry => {
const value = await fn()
if (value !== expected && value.toString() !== expected) {
retry(`Transaction Failed. Expected: ${expected} Actual: ${value}`)
}
})
}
function getSendTxMethod(url) {
return url === HOME_RPC_URL ? sendRawTxHome : sendRawTxForeign
}
async function isContract(web3, address) {
const code = await web3.eth.getCode(address)
return code !== '0x' && code !== '0x0'
}
module.exports = {
deployContract,
sendRawTxHome,
sendRawTxForeign,
privateKeyToAddress,
upgradeProxy,
transferProxyOwnership,
transferOwnership,
assertStateWithRetry,
isContract
}