Source: blockchains/fennel.js

'use strict';
/**
 * @module lib/blockchains/
 * @summary Whiteflag API Fennel parachain implementation
 * @description Module to use the Fennel parachain as underlying blockchain for Whiteflag
 * @tutorial modules
 * @tutorial fennel
 */
module.exports = {
    init: initFennel,
    scanBlocks,
    sendMessage,
    getMessage,
    requestSignature,
    verifySignature,
    getBinaryAddress,
    transferFunds,
    createAccount,
    updateAccount,
    deleteAccount
};

/* Node.js core and external modules */
const { base58Decode } = require('@polkadot/util-crypto');

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

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

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

/* Fennel sub-modules */
const fnlRpc = require('./fennel/rpc');
const fnlAccounts = require('./fennel/accounts');
const fnlListener = require('./fennel/listener');
const fnlTransactions =  require('./fennel/transactions');

/* Module constants */
const MODULELOG = 'fennel';
const SIGNALGORITHM = 'sr25519';

/* Module variables */
let _fnlChain;
let _fnlState = {};

/* MAIN MODULE FUNCTIONS */
/**
 * Initialises configured blockchains
 * @function initFennel
 * @alias module:lib/blockchains/fennel.init
 * @param {bcInitCb} callback function called after intitialising the blockchain
 */
function initFennel(fnConfig, callback) {
    log.trace(MODULELOG, 'Initialising the Fennel blockchain');
    _fnlChain = fnConfig.name;

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

        // Initialise sub-modules
        fnlAccounts.init(fnConfig, _fnlState)
        .then(() => fnlRpc.init(fnConfig, _fnlState))
        .then((fnlAPI) => {
            if (fnlAPI) log.info(MODULELOG, `Using ${fnlAPI.libraryInfo}`);
            return fnlListener.init(fnConfig, _fnlState, fnlAPI)
        })
        .then(() => fnlTransactions.init(fnConfig))
        .then(() => {
            wfState.updateBlockchainData(_fnlChain, _fnlState);
            return callback(null, _fnlChain);
        })
        .catch((err) => callback(err, _fnlChain));
    });
}

/**
 * Scans a number of blocks for Whiteflag messages
 * @function scanBlocks
 * @alias module:lib/fennel.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}`)
    fnlListener.scanBlocks(firstBlock, lastBlock)
    .then(wfMessages => {
        return callback(null, wfMessages);
    })
    .catch(err => {
        return callback(err);
    });
}

/**
 * Sends an encoded message on the blockchain
 * @function sendMessage
 * @alias module:lib/blockchains/fennel.sendMessage
 * @param {wfMessage} wfMessage the Whiteflag message to be sent on the blockchain
 * @param {bcSendMessageCb} callback function called after sending Whiteflag message
 */
function sendMessage(wfMessage, callback) {
    fnlAccounts.get(wfMessage.MetaHeader.originatorAddress)
    .then(account => {
        return fnlTransactions.sendSignal(account, wfMessage.MetaHeader.encodedMessage);
    })
    .then(transactionHash => {
        return callback(null, transactionHash);
    })
    .catch(err => {
        log.error(MODULELOG, `Error sending Whiteflag message: ${err.message}`);
        return callback(err);
    });
}

/**
 * Performs a simple query to find a message by transaction hash
 * @todo Implement function
 * @function getMessage
 * @alias module:lib/blockchains/fennel.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'];
    ignore(transactionHash);
    let wfMessage = null;
    return callback(new ProcessingError('Cannot look up message by transaction hash on the Fennel blockchain', null, 'WF_API_NOT_IMPLEMENTED'), wfMessage);
}

/**
 * Requests a Whiteflag signature for a specific Fennel address
 * @function requestSignature
 * @alias module:lib/blockchains/fennel.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)}`);
    const input = jws.createSignInput(wfSignPayload, SIGNALGORITHM);
    fnlAccounts.sign(input, wfSignPayload.addr)
    .then(output => {
        const signature = hexToBase64u(output);
        const wfSignature = jws.createFlattened(input, signature);
        return callback(null, wfSignature);
    })
    .catch(err => {
        log.error(MODULELOG, `Could not not create Whiteflag authentication signature for address ${wfSignPayload.addr}: ${err.message}`);
        return callback(err);
    });
}

/** 
 * Verifies a Whiteflag signature for the Fennel blockchain
 * @function verifySignature
 * @alias module:lib/blockchains/fennel.verifySignature
 * @param {wfSignature} wfSignature the Whiteflag authentication signature
 * @param {string} fnlAddress the address of the Whiteflag signature
 * @param {string} fnlPublicKey the raw hex public key of the originator
 * @param {authVerifySignatureCb} callback function called on completion
*/
function verifySignature(wfSignature, fnlAddress, fnlPublicKey, callback) {
    log.trace(MODULELOG, `Verifying Whiteflag authentication signature against public key ${fnlPublicKey}: ${JSON.stringify(wfSignature)}`);

    // Get input and signature from Whiteflag JWS object
    let input, signature;
    try {
        input = jws.serializeSignInput(wfSignature);
        signature = base64uToHex(wfSignature.signature);
    } catch(err) {
        return callback(new ProcessingError('Invalid Whiteflag authentication signature', err.message, 'WF_API_BAD_REQUEST'));
    }
    // Verify the signature
    let signErrors = [];
    fnlAccounts.verify(input, signature, fnlPublicKey)
    .then(valid => {
        if (!valid) signErrors.push('Signature does not match payload or public key');
        fnlAccounts.getAddress(noHexPrefix(fnlPublicKey))
        .then(signAddress =>{
            if (signAddress !== fnlAddress) signErrors.push(`Public key address does not correspond with address in signature`);
            if (signErrors.length > 0) {
                return callback(new ProtocolError('Invalid Whiteflag authentication signature', signErrors, 'WF_SIGN_ERROR'), null);
            }
            return callback(null);
        })
        .catch(err => callback(err));
    })
    .catch(err => callback(err));
}

/**
 * Returns a Fennel address in binary encoded form
 * @function getBinaryAddress
 * @alias module:lib/blockchains/fennel.getBinaryAddress
 * @param {string} fnlAddress the Fennel blockchain address
 * @param {bcBinaryAddressCb} callback function called on completion
 */
function getBinaryAddress(fnlAddress, callback) {
    let addressBuffer;
    try {
        addressBuffer = Buffer.from(base58Decode(fnlAddress));
    } catch(err) {
        return callback(new ProcessingError(`Invalid address: ${fnlAddress}`, err.message, 'WF_API_BAD_REQUEST'));
    }
    return callback(null, addressBuffer);
}

/**
 * Transfers fennel from one Fennel address to an other address
 * @function transferFunds
 * @alias module:lib/blockchains/fennel.transferFunds
 * @param {wfTransfer} transfer the transaction details for the funds transfer
 * @param {bcSendTransactionCb} callback function called on completion
 */
function transferFunds(transfer, callback) {
    log.trace(MODULELOG, `Transferring funds: ${JSON.stringify(transfer)}`);
    fnlAccounts.get(transfer.fromAddress)
    .then(account => {
        return fnlTransactions.sendTokens(account, transfer.toAddress, transfer.value);
    })
    .then(transactionHash => {
        return callback(null, transactionHash)
    })
    .catch(err => {
        log.error(MODULELOG, `Error transferring funds: ${err.message}`);
        return callback(err);
    });
}

/**
 * Creates a new Fennel blockchain account
 * @function createAccount
 * @alias module:lib/blockchains/fennel.createAccount
 * @param {string} [seed] hexadecimal encoded secret seed
 * @param {bcAccountCb} callback function called on completion
 */
 function createAccount(seed, callback) {
    fnlAccounts.create(noHexPrefix(seed))
    .then(account => {
        log.info(MODULELOG, `Created ${_fnlChain} account: ${account.address}`);
        return callback(null, account);
    })
    .catch(err => callback(err));
}

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

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