Source: icon-js/tx.js

/**
 * Provides transaction related functions for ICON
 * @module stayge-wallet/icon-js/tx
 */

'use strict'

const secp256k1 = require('secp256k1');
const sha3_256 = require('js-sha3').sha3_256;
const BigNumber = require('bignumber.js');
const utils = require('./utils.js');
const config = require('./config.js');

var nonce = 1;

/**
 * Generate a hash key for transaction
 * @param  {Object} obj
 * @return {String}
 */
function generateHashKey(obj){
    let resultStrReplaced = ''
    let resultStr = serializeObj(obj);
    resultStrReplaced = resultStr
                        .substring(1)
                        .slice(0, -1);
    //console.log(`obj=${JSON.stringify(obj)}`);
    //console.log('resultStr        :' + resultStr);
    //console.log('resultStrReplaced:' + resultStrReplaced);
    const result = 'icx_sendTransaction.' + resultStrReplaced;
    return result;
}

/**
 * Serialize a object for transaction
 * @param  {Object} obj
 * @return {String}
 */
function serializeObj(obj) {
    //console.log(`obj=${JSON.stringify(obj)}`)
    let result = '';
    result += '{';
    let keys;
    keys = Object.keys(obj);
    keys.sort();
    for(let i=0;i<keys.length;i++){
        const key = keys[i]
        const value = obj[key];
        switch(true) {
            case (value === null) : {
                result +=`${key}.`;
                result += String.raw`\0`;
                break;
            }
            case (typeof value === 'string') : {
                result += `${key}.`
                result += escapeString(value)
                break;
            }
            case (Array.isArray(value)) : {
                result+= `${key}.`
                result += serializeArray(value)
                break;
            }
            case (typeof value === 'object') : {
                result+= `${key}.`
                result += serializeObj(value);
                break;
            }
            default:
                break;
        }

        result += '.'
    }

    result = result.slice(0, -1);
    result += '}';
    return result;
}


/**
 * Serialize a array for transaction
 * @param  {Array} arr
 * @return {String}
 */
function serializeArray(arr) {
    let result = '';
    result += '[';
    for(let j=0;j<arr.length;j++) {
        const value = arr[j];
        switch(true) {
            case (value === null) : {
                result += String.raw`\0`;
                break;
            }
            case (typeof value === 'string') : {
                result += escapeString(value)
                break;
            }
            case (Array.isArray(value)) : {
                result += serializeArray(value)
                break;
            }
            case (typeof value === 'object') : {
                result += serializieObj(value);
                break;
            }
            default:
                break;
        }

        result += '.'
    }

    result = result.slice(0, -1);
    result += ']';
    return result;
}

/**
 * Escape a string for transaction
 * @param  {String} value
 * @return {String}
 */
function escapeString(value) {
    let newString = String.raw`${value}`;
    newString = newString.replace(/\\/g, '\\\\');
    newString = newString.replace(/\./g, '\\.');
    newString = newString.replace(/{/g, '\\{');
    newString = newString.replace(/}/g, '\\}');
    newString = newString.replace(/\[/g, '\\[');
    newString = newString.replace(/]/g, '\\]');
    return newString
}

/**
 * Concatenate two typed arrays
 * @param  {TypedArray} a
 * @param  {TypedArray} b
 * @return {Buffer}
 */
function concatTypedArrays(a, b) { // a, b TypedArray of same type
    const c = Buffer.alloc(a.length + b.length);
    //var c = new (a.constructor)(a.length + b.length);
    c.set(a, 0);
    c.set(b, a.length);
    return c;
}


module.exports = {

     /**
      * Make a raw transaction for ICX
      * @param  {Object} data
      * @param  {String} nid
      * @return {Object}
      */
    makeIcxRawTx(data, nid) {
        const rawTx = {
            from: data.from,
            to: data.to,
            version: utils.toHexString(config.apiVersion),
            nid: nid,
            stepLimit: utils.toHexString(
                new BigNumber(data.stepLimit).toString(16)
            ),
            timestamp: utils.toHexString(
                ((new Date()).getTime() * 1000).toString(16)
            ),
            nonce: utils.toHexString(nonce++)
        };

        if (data.value) {
            const sendAmount = utils.convertToLoop(data.value);
            rawTx.value = utils.toHexString(sendAmount);
        }

        if (data.dataType) {
            rawTx.dataType = data.dataType;
            rawTx.data = data.data;
        }

        return rawTx;
    },

    /**
     * Sign a raw transaction
     * @param  {Buffer} privateKey
     * @param  {Object} rawTx
     * @return {Object}
     */
    signRawTx(privateKey, rawTx) {
        //console.log(`rawTx=${JSON.stringify(rawTx)}`)
        const phraseToSign = generateHashKey(rawTx);
        //console.log(`serialized=${phraseToSign}`)
        const hashcode = sha3_256.update(phraseToSign).hex();
        const message = Buffer.from(hashcode, 'hex');
        //console.log(`hashcode=${hashcode}`)
        const sign = secp256k1.sign(message, privateKey);
        const recovery = new Uint8Array(1);
        recovery[0] = sign.recovery;
        const signature = concatTypedArrays(sign.signature, recovery);
        const b64encoded = signature.toString('base64');
        //console.log(`b64 sig=${b64encoded}`)

        /*
        const b64encoded = Buffer.from(
          String.fromCharCode.apply(null, signature)
        ).toString('base64');
        */

        const newRawTx = {
            ...rawTx,
            signature: b64encoded
        };

        //console.log('newRawTx: ' + JSON.stringify(newRawTx));

        return newRawTx
    },

};