Source: blockchains/bitcoin.js

'use strict';
/**
 * @module lib/blockchains/bitcoin
 * @summary Whiteflag API Bitcoin blockchain implementation
 * @description Module to use Bitcoin as underlying blockchain for Whiteflag
 * @tutorial modules
 * @tutorial bitcoin
 */
module.exports = {
    // Bitcoin blockchain functionsns
    init: initBitcoin,
    sendMessage,
    getMessage,
    requestSignature,
    requestKeys,
    getBinaryAddress,
    transferFunds,
    createAccount,
    updateAccount,
    deleteAccount
};

// Node.js core and external modules //
const bitcoin = require('bitcoinjs-lib');
const bs58 = require('bs58');
const KeyEncoder = require('key-encoder').default;

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

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

// Bitcoin sub-modules //
const bcRpc = require('./bitcoin/rpc');
const bcAccounts = require('./bitcoin/accounts');
const bcListener = require('./bitcoin/listener');
const bcTransactions = require('./bitcoin/transactions');

// Module constants //
const KEYIDLENGTH = 12;
const SIGNALGORITHM = 'ES256';
const SIGNKEYTYPE = 'secp256k1';
const BINENCODING = 'hex';

// Module variables //
let _blockchainName = 'bitcoin';
let _bcState = {};
let _transactionValue = 0;

/**
 * Initialises the Bitcoin blockchain
 * @function initBitcoin
 * @alias module:lib/blockchains/bitcoin.init
 * @param {Object} bcConfig the Bitcoin blockchain configuration
 * @param {blockchainInitCb} callback function to be called after intitialising Bitcoin
 */
function initBitcoin(bcConfig, callback) {
    log.trace(_blockchainName, 'Initialising the Bitcoin blockchain...');

    // Preserve the name of the blockchain
    _blockchainName = bcConfig.name;

    // Get Bitcoin blockchain state
    wfState.getBlockchainData(_blockchainName, function blockchainsGetStateDb(err, bcState) {
        if (err) return callback(err, _blockchainName);

        // Check and preserve Bitcoin state
        if (!bcState) {
            log.info(_blockchainName, 'Creating new Bitcoin entry in internal state');
            bcState = getEmptyState();
            wfState.updateBlockchainData(_blockchainName, bcState);
        }
        _bcState = bcState;

        // Determine network parameters
        if (bcConfig.testnet) {
            _bcState.parameters.network = bitcoin.networks.testnet;
            log.info(_blockchainName, 'Configured to use the Bitcoin test network');
        } else {
            _bcState.parameters.network = bitcoin.networks.bitcoin;
            log.info(_blockchainName, 'Configured to use the Bitcoin main network');
        }
        // Initialise sub-modules
        bcRpc.init(bcConfig, _bcState)
        .then(() => bcTransactions.init(bcConfig, _bcState))
        .then(() => bcListener.init(bcConfig, _bcState))
        .then(() => bcAccounts.init(bcConfig, _bcState))
        .then(() => wfState.updateBlockchainData(_blockchainName, _bcState))
        .then(() => callback(null, _blockchainName))
        .catch(initErr => callback(initErr, _blockchainName));
    });
}

/**
 * Sends an encoded message on the Bitcoin blockchain
 * @function sendMessage
 * @alias module:lib/blockchains/bitcoin.sendMessage
 * @param {wfMessage} wfMessage the Whiteflag message to be sent on Bitcoin
 * @param {blockchainSendTransactionCb} callback function to be called after sending Whiteflag message
 * @todo Return blocknumber
 */
function sendMessage(wfMessage, callback) {
    bcAccounts.check(wfMessage.MetaHeader.originatorAddress)
    .then(account => {
        const toAddress = account.address;
        const encodedMessage = Buffer.from(wfMessage.MetaHeader.encodedMessage, 'hex');
        return bcTransactions.send(account, toAddress, _transactionValue, encodedMessage);
    })
    .then(transactionHash => callback(null, transactionHash))
    .catch(err => {
        log.error(_blockchainName, `Error sending Whiteflag message: ${err.message}`);
        callback(err);
    });
}

/**
 * Performs a simple query to find a message on Bitcoin by transaction hash
 * @function getMessage
 * @alias module:lib/blockchains/bitcoin.getMessage
 * @param {Object} wfQuery the property of the transaction to look up
 * @param {blockchainLookupMessageCb} callback function to be called after Whiteflag message lookup
 */
function getMessage(wfQuery, callback) {
    const transactionHash = wfQuery['MetaHeader.transactionHash'];
    let transaction;
    bcTransactions.get(transactionHash)
    .then(rawTransaction => {
        transaction = rawTransaction;
        return bcRpc.getBlockByHash(rawTransaction.blockhash);
    })
    .then(block => {
        return bcTransactions.extractMessage(transaction, block.height, block.time);
    })
    .then(wfMessage => callback(null, wfMessage))
    .catch(err => {
        if (err instanceof ProcessingError) {
            log.debug(_blockchainName, `No Whiteflag message with transaction hash ${transactionHash} found: ${err.message}`);
        } else {
            log.error(_blockchainName, `Error retrieving Whiteflag message with transaction hash ${transactionHash}: ${err.message}`);
        }
        return callback(err, null);
    });
}

/**
 * Requests a Whiteflag signature for a specific Bitcoin address
 * @function requestSignature
 * @alias module:lib/blockchains/bitcoin.requestSignature
 * @param {wfSignaturePayload} payload the JWS payload for the Whiteflag signature
 * @param {blockchainRequestSignatureCb} callback function to be called upon completion
 */
function requestSignature(payload, callback) {
    log.trace(_blockchainName, `Generating signature: ${JSON.stringify(payload)}`);

    // Get Ethereum account, address and private key
    bcAccounts.get(payload.addr)
    .then(account => {
        payload.addr = account.address;
        const privateKeyId = hash(_blockchainName + account.address, KEYIDLENGTH);
        wfState.getKey('blockchainKeys', privateKeyId, function bcGetKeyCb(keyErr, privateKey) {
            if (keyErr) return callback(keyErr);

            // Create JSON serialization of JWS token from array
            let wfSignature;
            try {
                wfSignature = createSignature(payload, privateKey, SIGNKEYTYPE, SIGNALGORITHM);
            } catch(err) {
                log.error(_blockchainName, `Could not not sign payload: ${err.message}`);
                return callback(err);
            }
            // Callback with any error and signature
            return callback(null, wfSignature);
        });
    })
    .catch(err => callback(err));
}

/**
 * Requests the Bitcoin address and correctly encoded pubic key of an originator
 * @function requestKeys
 * @alias module:lib/blockchains/bitcoin.requestKeys
 * @param {string} publicKey the raw hex public key of the originator
 * @param {blockchainRequestKeysCb} callback function to be called upon completion
 */
function requestKeys(publicKey, callback) {
    log.trace(_blockchainName, `Getting address and encoded keys for public key: ${publicKey}`);

    // Create data structure for requested keys
    let originatorKeys = {
        address: null,
        publicKey: {
            hex: null,
            pem: null
        }
    };
    try {
        // Bitcoin public key in HEX and PEM encoding
        const keyEncoder = new KeyEncoder(SIGNKEYTYPE);
        const publicKeyBuffer = Buffer.from(publicKey, BINENCODING);
        originatorKeys.publicKey.hex = publicKeyBuffer.toString(BINENCODING);
        originatorKeys.publicKey.pem = keyEncoder.encodePublic(originatorKeys.publicKey.hex, 'raw', 'pem');

        // Bitcoin address
        const { address } = bitcoin.payments.p2pkh({
            pubkey: publicKeyBuffer,
            network: _bcState.parameters.network
        });
        originatorKeys.address = address;
    } catch(err) {
        log.error(_blockchainName, `Could not get key and address: ${err.message}`);
        return callback(err);
    }
    return callback(null, originatorKeys);
}

/**
 * Returns a Bitcoin address in binary encoded form
 * @param {string} address the Bitcoin blockchain address
 * @param {blockchainBinaryAddressCb} callback function to be called upon completion
 */
function getBinaryAddress(address, callback) {
    let addressBuffer;
    try {
        bitcoin.address.toOutputScript(address, _bcState.parameters.network);
        addressBuffer = bs58.decode(address);
    } catch(err) {
        return callback(new ProcessingError(`Invalid Bitcoin address: ${address}`, err.message, 'WF_API_PROCESSING_ERROR'));
    }
    return callback(null, addressBuffer);
}

/**
 * Transfers bitcoin from one Bitcoin address to an other address
 * @function transferFunds
 * @alias module:lib/blockchains/bitcoin.transferFunds
 * @param {Object} transfer the transaction details for the funds transfer
 * @param {blockchainSendTransactionCb} callback function to be called upon completion
 * @todo Return blocknumber
 */
function transferFunds(transfer, callback) {
    log.trace(_blockchainName, `Transferring funds: ${JSON.stringify(transfer)}`);
    bcAccounts.check(transfer.fromAddress)
    .then(account => {
        const toAddress = transfer.toAddress;
        const amount = transfer.value;
        return bcTransactions.send(account, toAddress, amount, null, callback);
    })
    .then(txHash => callback(null, txHash))
    .catch(err => {
        log.error(_blockchainName, `Error transferring funds: ${err.message}`);
        return callback(err);
    });
}

/**
 * Creates a new Bitcoin blockchain account
 * @function createAccount
 * @alias module:lib/blockchains/bitcoin.createAccount
 * @param {string} [privateKey] hexadecimal encoded private key
 * @param {blockchainCreateAccountCb} callback function to be called upon completion
 */
function createAccount(wif, callback) {
    bcAccounts.create(wif)
    .then(account => {
        log.info(_blockchainName, `Bitcoin account created: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}

 /**
 * Updates Bitcoin blockchain account attributes
 * @function updateAccount
 * @param {Object} account the account information including address to be updated
 * @alias module:lib/blockchains/bitcoin.updateAccount
 * @param {blockchainUpdateAccountCb} callback function to be called upon completion
 */
function updateAccount(account, callback) {
    bcAccounts.update(account)
    .then(() => {
        log.debug(_blockchainName, `Bitcoin account updated: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}

/**
 * Deletes Bitcoin blockchain account
 * @function deleteAccount
 * @alias module:lib/blockchains/bitcoin.deleteAccount
 * @param {string} address the address of the account to be deleted
 * @param {blockchainDeleteAccountCb} callback function to be called upon completion
 */
function deleteAccount(address, callback) {
    bcAccounts.delete(address)
    .then(account => {
        log.info(_blockchainName, `Bitcoin account deleted: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}