Source: blockchains/ethereum.js

'use strict';
/**
 * @module lib/blockchains/ethereum
 * @summary Whiteflag API Ethereum blockchain implementation
 * @description Module to use Ethereum as underlying blockchain for Whiteflag
 * @todo The Ethereum module has not been maintained and needs evaluation
 * @tutorial modules
 * @tutorial ethereum
 */
module.exports = {
    init: initEthereum,
    scanBlocks,
    sendMessage,
    getMessage,
    requestSignature,
    verifySignature,
    getBinaryAddress,
    transferFunds,
    createAccount,
    updateAccount,
    deleteAccount
};

/* Node.js core and external modules */
const { pubToAddress }  = require('ethereumjs-util');

/* Common internal functions and classes */
const log = require('../_common/logger');
const { ignore } = require('../_common/processing');
const { ProcessingError,
        ProtocolError } = require('../_common/errors');
const { hexToBuffer,
        bufferToHex } = require('../_common/encoding');
const { withHexPrefix,
        noHexPrefix,
        noAddressHexPrefix } = require('../_common/format');

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

/* Common blockchain functions */
const { getEmptyState } = require('./_common/state');
const { getPrivateKeyId } = require('./_common/keys');
const { createJWS,
        verifyJWS } = require('./_common/crypto');

/* Ethereum sub-modules */
const ethRpc = require('./ethereum/rpc');
const ethAccounts = require('./ethereum/accounts');
const ethListener = require('./ethereum/listener');
const ethTransactions = require('./ethereum/transactions');

/* Module constants */
const MODULELOG = 'ethereum';
const SIGNALGORITHM = 'ES256';
const SIGNKEYTYPE = 'secp256k1';

/* Module variables */
let _ethChain;
let _ethState = {};
let _ethApi;
let _transactionValue = '0';

/**
 * Initialises the Ethereum blockchain
 * @function initEthereum
 * @alias module:lib/blockchains/ethereum.init
 * @param {Object} ethConfig the blockchain configuration
 * @param {bcInitCb} callback function called after intitialising Ethereum
 */
function initEthereum(ethConfig, callback) {
    log.trace(MODULELOG, 'Initialising the Ethereum blockchain');
    _ethChain = ethConfig.name;

    // Get Ethereum blockchain state
    wfState.getBlockchainData(_ethChain, function blockchainsGetStateDb(err, ethState) {
        if (err) return callback(err, _ethChain);
        if (!ethState) {
            log.info(MODULELOG, `Creating new ${_ethChain} entry in internal state`);
            ethState = getEmptyState();
            wfState.updateBlockchainData(_ethChain, ethState);
        }
        _ethState = ethState;

        // Initialise sub-modules
        ethAccounts.init(ethConfig, _ethState)
        .then(() => ethRpc.init(ethConfig, _ethState))
        .then(ethApi => { _ethApi = ethApi })
        .then(() => ethTransactions.init(ethConfig, _ethState, _ethApi))
        .then(() => ethListener.init(ethConfig, _ethState))
        .then(() => {
            wfState.updateBlockchainData(_ethChain, _ethState);
            return callback(null, _ethChain);
        })
        .catch(err => callback(err, _ethChain));
    });
}

/**
 * Scans a number of blocks for Whiteflag messages
 * @function scanBlocks
 * @alias module:lib/ethereum.scanBlocks
 * @param {number} firstBlock the starting block
 * @param {number} lastBlock the ending block
 * @param {bcScanBlocksCb} callback function called on completion
 */
function scanBlocks(firstBlock, lastBlock, callback) {
    log.debug(MODULELOG, `Scanning block ${firstBlock} to ${lastBlock}`)
    ethListener.scanBlocks(firstBlock, lastBlock)
    .then(wfMessages => {
        return callback(null, wfMessages);
    })
    .catch(err => {
        return callback(err);
    });
}

/**
 * Sends an encoded message on the Ethereum blockchain
 * @function sendMessage
 * @alias module:lib/blockchains/ethereum.sendMessage
 * @param {wfMessage} wfMessage the Whiteflag message to be sent on Ethereum
 * @param {bcSendTransactionCb} callback function called after sending Whiteflag message
 */
function sendMessage(wfMessage, callback) {
    ethAccounts.get(wfMessage.MetaHeader.originatorAddress)
    .then(account => {
        const toAddress = account.address;
        const encodedMessage = wfMessage.MetaHeader.encodedMessage;
        return ethTransactions.send(account, toAddress, _transactionValue, encodedMessage);
    })
    .then((transactionHash, blockNumber) => {
        return callback(null, transactionHash, blockNumber);
    })
    .catch(err => {
        log.error(MODULELOG, `Error sending Whiteflag message: ${err.message}`);
        callback(err);
    });
}

/**
 * Performs a simple query to find a message on Ethereum by transaction hash
 * @function getMessage
 * @alias module:lib/blockchains/ethereum.getMessage
 * @param {Object} wfQuery the property of the transaction to look up
 * @param {bcGetMessageCb} callback function called after Whiteflag message lookup
 */
function getMessage(wfQuery, callback) {
    const transactionHash = wfQuery['MetaHeader.transactionHash'];
    ethTransactions.get(transactionHash)
    .then(transaction => {
        return ethTransactions.extractMessage(transaction);
    })
    .then(wfMessage => callback(null, wfMessage))
    .catch(err => {
        if (err instanceof ProcessingError) {
            log.debug(MODULELOG, `No Whiteflag message with transaction hash ${transactionHash} found: ${err.message}`);
        } else {
            log.error(MODULELOG, `Error retrieving Whiteflag message with transaction hash ${transactionHash}: ${err.message}`);
        }
        return callback(err, null);
    });
}

/**
 * Requests a Whiteflag signature for the specified Ethereum address
 * @todo Refactor to use with new JWS functions and native blockchain signature
 * @function requestSignature
 * @alias module:lib/blockchains/ethereum.requestSignature
 * @param {wfSignPayload} wfSignPayload the JWS payload for the Whiteflag signature
 * @param {authRequestSignatureCb} callback function called on completion
 */
function requestSignature(wfSignPayload, callback) {
    log.trace(MODULELOG, `Generating Whiteflag authentication signature: ${JSON.stringify(wfSignPayload)}`);

    // Get Ethereum account and address
    ethAccounts.get(wfSignPayload.addr)
    .then(account => {
        // Get ensure correct address and get private key
        wfSignPayload.addr = noAddressHexPrefix(account.address);
        const privateKeyId = getPrivateKeyId(_ethChain, account.address);
        wfState.getKey('blockchainKeys', privateKeyId, function ethGetKeyCb(err, ethPrivateKey) {
            if (err) return callback(err);
            if (!ethPrivateKey) {
                return callback(new ProcessingError(`No private key available for address ${account.address}`, null, 'WF_API_PROCESSING_ERROR'));
            }
            // Create signature
            let wfSignature;
            try {
                wfSignature = createJWS(wfSignPayload, noHexPrefix(ethPrivateKey), SIGNKEYTYPE, SIGNALGORITHM);
            } catch(err) {
                log.error(MODULELOG, `Could not create Whiteflag authentication signature for address ${account.address}: ${err.message}`);
                return callback(err);
            }
            return callback(null, wfSignature);
        });
    })
    .catch(err => callback(err));
}

/**
 * Verifies a Whiteflag signature for the specified Ethereum public key
 * @todo Refactor to use with new JWS functions and native blockchain signature
 * @function verifySignature
 * @alias module:lib/blockchains/ethereum.verifySignature
 * @param {wfSignature} wfSignature the Whiteflag authentication signature
 * @param {string} ethAddress the address of the Whiteflag signature
 * @param {string} ethPublicKey the Ethereum public key to verify against
 * @param {authVerifySignatureCb} callback function called on completion
 */
function verifySignature(wfSignature, ethAddress, ethPublicKey, callback) {
    log.trace(MODULELOG, `Verifying Whiteflag authentication signature against public key ${ethPublicKey}: ${JSON.stringify(wfSignature)}`);

    // Verify the signature
    let result;
    try {
        result = verifyJWS(wfSignature, ethPublicKey, SIGNKEYTYPE);
    } catch(err) {
        if (err instanceof ProtocolError) {
            return callback(new ProtocolError(`Invalid Whiteflag authentication signature`, err.message, 'WF_SIGN_ERROR'));
        }
        log.error(MODULELOG, `Could not verify Whiteflag authentication signature: ${err.message}`);
        return callback(err);
    }
    //TODO: Check public key and address
    const publicKeyBuffer = hexToBuffer(ethPublicKey);
    const address = noAddressHexPrefix(bufferToHex(pubToAddress(publicKeyBuffer, true)));
    ignore(ethAddress, address);

    return callback(null, result);
}

/**
 * Returns an Ethereum address in binary encoded form
 * @function getBinaryAddress
 * @alias module:lib/blockchains/ethereum.getBinaryAddress
 * @param {string} ethAddress the blockchain address
 * @param {bcBinaryAddressCb} callback function called on completion
 */
function getBinaryAddress(ethAddress, callback) {
    if (_ethApi.utils.isAddress(withHexPrefix(ethAddress))) {
        return callback(null, hexToBuffer(noAddressHexPrefix(ethAddress)));
    }
    return callback(new ProcessingError(`Invalid ${_ethChain} address: ${ethAddress}`, null, 'WF_API_BAD_REQUEST'));
}

/**
 * Transfers ether from one Ethereum address to an other address
 * @function transferFunds
 * @alias module:lib/blockchains/ethereum.transferFunds
 * @param {wfTransfer} transfer the object with the transaction details to transfer funds
 * @param {bcSendTransactionCb} callback function called on completion
 */
function transferFunds(transfer, callback) {
    log.trace(MODULELOG, `Transferring funds: ${JSON.stringify(transfer)}`);
    ethAccounts.get(transfer.fromAddress)
    .then(account => {
        return ethTransactions.send(account, transfer.toAddress, transfer.value, '');
    })
    .then((transactionHash, blockNumber) => callback(null, transactionHash, blockNumber))
    .catch(err => {
        log.error(MODULELOG, `Error transferring funds: ${err.message}`);
        return callback(err);
    });
}

/**
 * Creates a new Ethereum blockchain account
 * @function createAccount
 * @alias module:lib/blockchains/ethereum.createAccount
 * @param {string} [ethPrivateKey] hexadecimal string with private key (without 0x prefix)
 * @param {bcAccountCb} callback function called on completion
 */
 function createAccount(ethPrivateKey, callback) {
    ethAccounts.create(ethPrivateKey)
    .then(account => {
        log.info(MODULELOG, `Created ${_ethChain} account: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}

 /**
 * Updates Ethereum blockchain account attributes
 * @function updateAccount
 * @alias module:lib/blockchains/ethereum.updateAccount
 * @param {wfAccount} account the account information including address to be updated
 * @param {bcAccountCb} callback function called on completion
 */
function updateAccount(account, callback) {
    ethAccounts.update(account)
    .then(updatedAccount => {
        log.info(MODULELOG, `Updated ${_ethChain} account: ${account.address}`);
        return callback(null, updatedAccount);
    })
    .catch(err => callback(err));
}

/**
 * Deletes Ethereum blockchain account
 * @function deleteAccount
 * @alias module:lib/blockchains/ethereum.deleteAccount
 * @param {string} ethAddress the address of the account to be deleted
 * @param {bcAccountCb} callback function called on completion
 */
function deleteAccount(ethAddress, callback) {
    ethAccounts.delete(ethAddress)
    .then(account => {
        log.info(MODULELOG, `Deleted ${_ethChain} account: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}