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 = {
    init: initTransactions,
    send: sendTransaction,
    get: getTransaction,
    extractMessage
};

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

/* Common internal functions and classes */
const log = require('../../_common/logger');
const { zeroise } = require('../../_common/crypto');
const { ProcessingError } = require('../../_common/errors');
const { withHexPrefix,
        noHexPrefix,
        noAddressHexPrefix,
        noPubkeyHexPrefix } = require('../../_common/format');

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

/* Common blockchain functions */
const { getPrivateKeyId } = require('../_common/keys');

/* Ethereum sub-modules */
const ethRpc = require('./rpc');

/* Module constants */
const MODULELOG = 'ethereum';
const ETHHEXPREFIX = '0x';
const WFINDENTIFIER = '5746';

/* Module variables */
let _ethChain = 'ethereum';
let _ethState;
let _ethApi;
let _traceRaw = 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} ethApi the Ethereum Web3 instance
 * @returns {Promise} resolve if succesfully initialised
 */
function initTransactions(ethConfig, ethState, ethApi) {
    log.trace(MODULELOG, 'Initialising Ethereum transaction processing');
    _ethChain = ethConfig.name;
    _ethState = ethState;
    _ethApi = ethApi;

    // Trace all transactions if configured
    if (ethConfig.traceRawTransaction) _traceRaw = 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 {wfAccount} 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(MODULELOG, `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 = getPrivateKeyId(_ethChain, noAddressHexPrefix(account.address));
            wfState.getKey('blockchainKeys', privateKeyId, function ethGetKeyCb(err, privateKey) {
                if (err) return reject(err);

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

                    const serializedTransaction = ethTransactionObject.serialize();
                    rawTransaction = withHexPrefix(bufferToHex(serializedTransaction));
                } catch(err) {
                    return reject(new Error(`Could not create raw transaction: ${err.message}`));
                }
                // Send signed transaction and process mined receipt
                _ethApi.eth.sendSignedTransaction(rawTransaction)
                .then(receipt => {
                    log.debug(MODULELOG, `Transaction result: ${JSON.stringify(receipt)}`);
                    resolve(noHexPrefix(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(MODULELOG, `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 (_traceRaw) log.trace(MODULELOG, `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 orgPubkey = ethereumUtil.bufferToHex(ethTransactionObject.getSenderPublicKey());

        // Construct and return Whiteflag message object
        let wfMessage = {
            MetaHeader: {
                blockchain: _ethChain,
                blockNumber: transaction.blockNumber,
                transactionHash: noHexPrefix(transaction.hash),
                transactionTime: '',
                originatorAddress: noAddressHexPrefix(transaction.from),
                originatorPubKey: noPubkeyHexPrefix(orgPubkey),
                encodedMessage: noHexPrefix(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 {wfAccount} 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),
        _ethApi.eth.estimateGas({
            to: withHexPrefix(toAddress),
            data: withHexPrefix(data)
        }),
        _ethApi.eth.getGasPrice()
    ])
    .then(result => {
        const txCount = result[0];
        const gasLimit = result[1];
        const gasPrice = result[2];
        const rawTransactionObject = {
            nonce: _ethApi.utils.toHex(txCount),
            to: withHexPrefix(toAddress),
            value: _ethApi.utils.toHex(_ethApi.utils.toWei(value, 'ether')),
            gasLimit: _ethApi.utils.toHex(gasLimit),
            gasPrice: _ethApi.utils.toHex(gasPrice),
            data: withHexPrefix(data)
        };
        return Promise.resolve(rawTransactionObject);
    })
    .catch(err => {
        return Promise.reject(new Error(`Could not create transaction: ${err.message}`));
    });
}