Source: blockchains/ethereum/transactions.js

'use strict';
/**
 * @module lib/blockchains/ethereum/transactions
 * @summary Whiteflag API Ethereum transaction module
 * @description Module to process Ethereum transactions for Whiteflag
 */
module.exports = {
    // Ethereum transaction functions
    init: initTransactions,
    send: sendTransaction,
    get: getTransaction,
    extractMessage
};

// Node.js core and external modules //
const EthereumTx = require('ethereumjs-tx').Transaction;
const ethereumUtil = require('ethereumjs-util');

// Whiteflag common functions and classes //
const log = require('../../common/logger');
const { hash, zeroise } = require('../../common/crypto');
const { ProcessingError } = require('../../common/errors');

// Whiteflag modules //
const wfState = require('../../protocol/state');

// Ethereum sub-modules //
const ethRpc = require('./rpc');
const { formatHexEthereum, formatHexApi, formatAddressApi, formatPubkeyApi } = require('./common');

// Module constants //
const ETHHEXPREFIX = '0x';
const WFINDENTIFIER = '5746';
const BINENCODING = 'hex';
const KEYIDLENGTH = 12;

// Module variables //
let _blockchainName;
let _ethState;
let _web3;
let _traceRawTransaction = false;

/**
 * Initialises Ethereum Transactions processing
 * @function initTransactions
 * @alias module:lib/blockchains/ethereum/transactions.init
 * @param {Object} ethConfig the Ethereum blockchain configuration
 * @param {Object} ethState the Ethereum blockchain state
 * @param {Object} web3 the Ethereum Web3 instance
 * @returns {Promise} resolve if succesfully initialised
 */
function initTransactions(ethConfig, ethState, web3) {
    _blockchainName = ethConfig.name;
    _ethState = ethState;
    _web3 = web3;
    log.trace(_blockchainName, 'Initialising Ethereum transaction processing...');

    // Trace all transactions if configured
    if (ethConfig.traceRawTransaction) _traceRawTransaction = ethConfig.traceRawTransaction;

    // Succesfully completed initialisation
    return Promise.resolve();
}

/**
 * Sends a transaction on the Ethereum blockchain
 * @function sendTransaction
 * @alias module:lib/blockchains/ethereum/transactions.send
 * @param {Object} account the account used to send the transaction
 * @param {string} toAddress the address to send the transaction to
 * @param {string} value the value to be sent in ether
 * @param {string} data the data to be sent
 * @returns {Promise} resolve to transaction hash and block number
 */
function sendTransaction(account, toAddress, value, data) {
    log.trace(_blockchainName, `Sending transaction from account: ${account.address}`);
    return new Promise((resolve, reject) => {
        // Create transaction
        createTransaction(account, toAddress, value, data)
        .then(rawTransactionObject => {
            // Get the private key to sign the transaction
            const privateKeyId = hash(_blockchainName + formatAddressApi(account.address), KEYIDLENGTH);
            wfState.getKey('blockchainKeys', privateKeyId, function ethGetKeyCb(keyErr, privateKey) {
                if (keyErr) return reject(keyErr);

                // Sign and serialise transaction
                let rawTransaction;
                try {
                    const ethTransactionObject = new EthereumTx(rawTransactionObject, { chain: _ethState.parameters.chainID });
                    const privateKeyBuffer = Buffer.from(privateKey, BINENCODING);
                    ethTransactionObject.sign(privateKeyBuffer);
                    zeroise(privateKeyBuffer);

                    const serializedTransaction = ethTransactionObject.serialize();
                    rawTransaction = formatHexEthereum(serializedTransaction.toString(BINENCODING));
                } catch(err) {
                    return reject(new Error(`Could not create raw transaction: ${err.message}`));
                }
                // Send signed transaction and process mined receipt
                _web3.eth.sendSignedTransaction(rawTransaction)
                .then(receipt => resolve(formatHexApi(receipt.transactionHash), receipt.blockNumber))
                .catch(err => {
                    if (err) return reject(new Error(`Could not send transaction from account ${account.address}: ${err.message}`));
                    return reject(new Error(`Could not send transaction from account: ${account.address}`));
                });
            });
        });
    });
}

/**
 * Gets a transaction by transaction hash and checks for Whiteflag message
 * @private
 * @param {string} transactionHash
 * @returns {Promise} resolves to a blockchain transaction
 */
function getTransaction(transactionHash) {
    log.trace(_blockchainName, `Retrieving transaction: ${transactionHash}`);
    return ethRpc.getRawTransaction(transactionHash)
    .then(transaction => {
        if (!transaction) {
            return Promise.reject(new ProcessingError(`No transaction returned by node for hash: ${transactionHash}`, null, 'WF_API_NO_DATA'));
        }
        return Promise.resolve(transaction);
    })
    .catch(err => {
        return Promise.reject(err);
    });
}

/**
 * Extracts Whiteflag message from Ethereum transaction data
 * @function extractMessage
 * @alias module:lib/blockchains/ethereum/transactions.extractMessage
 * @param {Object} transaction
 * @param {number} timestamp the block time
 * @returns {Promise} resolves to a Whiteflag message
 */
 function extractMessage(transaction, timestamp) {
    if (_traceRawTransaction) log.trace(_blockchainName, `Extracting Whiteflag message from transaction: ${transaction.hash}`);
    return new Promise((resolve, reject) => {
        if (
            !transaction ||
            !transaction.input ||
            !transaction.input.startsWith(ETHHEXPREFIX + WFINDENTIFIER)
        ) {
            return reject(new ProcessingError(`No Whiteflag message found in transaction: ${transaction.hash}`, null, 'WF_API_NO_DATA'));
        }
        // Get raw transaction data
        const rawTransactionObject = {
            nonce: transaction.nonce,
            gasPrice: ethereumUtil.bufferToHex(new ethereumUtil.BN(transaction.gasPrice)),
            gasLimit: transaction.gas,
            to: transaction.to,
            value: ethereumUtil.bufferToHex(new ethereumUtil.BN(transaction.value)),
            data: transaction.input,
            r: transaction.r,
            s: transaction.s,
            v: transaction.v
        };
        const ethTransactionObject = new EthereumTx(rawTransactionObject, { chain: _ethState.parameters.chainID });
        const originatorPubKey = ethereumUtil.bufferToHex(ethTransactionObject.getSenderPublicKey());

        // Construct and return Whiteflag message object
        let wfMessage = {
            MetaHeader: {
                blockchain: _blockchainName,
                blockNumber: transaction.blockNumber,
                transactionHash: formatHexApi(transaction.hash),
                transactionTime: '',
                originatorAddress: formatAddressApi(transaction.from),
                originatorPubKey: formatPubkeyApi(originatorPubKey),
                encodedMessage: formatHexApi(transaction.input)
            }
        };
        if (timestamp) wfMessage.MetaHeader.transactionTime = new Date(timestamp * 1000).toISOString();
        return resolve(wfMessage);
    });
}

// PRIVATE TRANSACTION FUNCTIONS //
/**
 * Creates a new Ethereum transaction to be signed
 * @private
 * @param {Object} account the account parameters used to send the transaction
 * @param {string} toAddress the address to send the transaction to
 * @param {string} value the value to be sent in ether
 * @param {string} data the data to be sent
 * @returns {Promise} resolves to a raw unsigned transaction object
 */
function createTransaction(account, toAddress, value, data) {
    return Promise.all([
        ethRpc.getTransactionCount(account.address),
        _web3.eth.estimateGas({
            to: formatHexEthereum(toAddress),
            data: formatHexEthereum(data)
        }),
        _web3.eth.getGasPrice()
    ])
    .then(result => {
        const txCount = result[0];
        const gasLimit = result[1];
        const gasPrice = result[2];
        const rawTransactionObject = {
            nonce: _web3.utils.toHex(txCount),
            to: formatHexEthereum(toAddress),
            value: _web3.utils.toHex(_web3.utils.toWei(value, 'ether')),
            gasLimit: _web3.utils.toHex(gasLimit),
            gasPrice: _web3.utils.toHex(gasPrice),
            data: formatHexEthereum(data)
        };
        return Promise.resolve(rawTransactionObject);
    })
    .catch(err => {
        return Promise.reject(new Error(`Could not create transaction: ${err.message}`));
    });
}