"use strict";
/*
 * Copyright 2020 NEM
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.BootstrapUtils = exports.KnownError = void 0;
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const Handlebars = require("handlebars");
const https_1 = require("https");
const _ = require("lodash");
const os_1 = require("os");
const path_1 = require("path");
const symbol_sdk_1 = require("symbol-sdk");
const util = require("util");
const CryptoUtils_1 = require("./CryptoUtils");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const version = require('../../package.json').version;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const yaml = require('js-yaml');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec);
class KnownError extends Error {
    constructor() {
        super(...arguments);
        this.known = true;
    }
}
exports.KnownError = KnownError;
class BootstrapUtils {
    static getFilesRecursively(path) {
        const isDirectory = (path) => fs_1.statSync(path).isDirectory();
        const getDirectories = (path) => fs_1.readdirSync(path)
            .map((name) => path_1.join(path, name))
            .filter(isDirectory);
        const isFile = (path) => fs_1.statSync(path).isFile();
        const getFiles = (path) => fs_1.readdirSync(path)
            .map((name) => path_1.join(path, name))
            .filter(isFile);
        const dirs = getDirectories(path);
        const files = dirs
            .map((dir) => BootstrapUtils.getFilesRecursively(dir)) // go through each directory
            .reduce((a, b) => a.concat(b), []); // map returns a 2d array (array of file arrays) so flatten
        return files.concat(getFiles(path));
    }
    static async download(logger, url, dest) {
        const destinationSize = fs_1.existsSync(dest) ? fs_1.statSync(dest).size : -1;
        const isHttpRequest = url.toLowerCase().startsWith('https:') || url.toLowerCase().startsWith('http:');
        if (!isHttpRequest) {
            const stats = fs_1.statSync(url);
            if (fs_1.existsSync(url) && !stats.isDirectory()) {
                return {
                    downloaded: false,
                    fileLocation: url,
                };
            }
            else {
                throw new Error(`Local file ${url} does not exist`);
            }
        }
        else {
            logger.info(`Checking remote file ${url}`);
            return new Promise((resolve, reject) => {
                function showDownloadingProgress(received, total) {
                    const percentage = ((received * 100) / total).toFixed(2);
                    const message = percentage + '% | ' + received + ' bytes downloaded out of ' + total + ' bytes.';
                    BootstrapUtils.logSameLineMessage(message);
                }
                const request = https_1.get(url, (response) => {
                    const total = parseInt(response.headers['content-length'] || '0', 10);
                    let received = 0;
                    if (total === destinationSize) {
                        logger.info(`File ${dest} is up to date with url ${url}. No need to download!`);
                        request.abort();
                        resolve({
                            downloaded: false,
                            fileLocation: dest,
                        });
                    }
                    else if (response.statusCode === 200) {
                        fs_1.existsSync(dest) && fs_1.unlinkSync(dest);
                        const file = fs_1.createWriteStream(dest, { flags: 'wx' });
                        logger.info(`Downloading file ${url}. This could take a while!`);
                        response.pipe(file);
                        response.on('data', function (chunk) {
                            received += chunk.length;
                            showDownloadingProgress(received, total);
                        });
                        file.on('finish', () => {
                            resolve({
                                downloaded: true,
                                fileLocation: dest,
                            });
                        });
                        file.on('error', (err) => {
                            file.close();
                            if (err.code === 'EEXIST') {
                                reject(new Error('File already exists'));
                            }
                            else {
                                fs_1.unlinkSync(dest); // Delete temp file
                                reject(err);
                            }
                        });
                    }
                    else {
                        reject(new Error(`Server responded with ${response.statusCode} ${response.statusMessage || ''}`.trim()));
                    }
                });
                request.on('error', (err) => {
                    fs_1.existsSync(dest) && fs_1.unlinkSync(dest); // Delete temp file
                    reject(err.message);
                });
            });
        }
    }
    static logSameLineMessage(message) {
        process.stdout.write(BootstrapUtils.isWindows() ? '\x1b[0G' : '\r');
        process.stdout.write(message);
    }
    static deleteFolder(logger, folder, excludeFiles = []) {
        if (fs_1.existsSync(folder)) {
            logger.info(`Deleting folder ${folder}`);
        }
        return BootstrapUtils.deleteFolderRecursive(logger, folder, excludeFiles);
    }
    static deleteFolderRecursive(logger, folder, excludeFiles = []) {
        if (fs_1.existsSync(folder)) {
            fs_1.readdirSync(folder).forEach((file) => {
                const currentPath = path_1.join(folder, file);
                if (excludeFiles.find((f) => f === currentPath)) {
                    logger.info(`File ${currentPath} excluded from deletion.`);
                    return;
                }
                if (fs_1.lstatSync(currentPath).isDirectory()) {
                    // recurse
                    this.deleteFolderRecursive(logger, currentPath, excludeFiles.map((file) => path_1.join(currentPath, file)));
                }
                else {
                    // delete file
                    fs_1.unlinkSync(currentPath);
                }
            });
            if (!fs_1.readdirSync(folder).length)
                fs_1.rmdirSync(folder);
        }
    }
    static deleteFile(file) {
        if (fs_1.existsSync(file) && fs_1.lstatSync(file).isFile()) {
            fs_1.unlinkSync(file);
        }
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static validateIsDefined(value, message) {
        if (value === undefined || value === null) {
            throw new Error(message);
        }
    }
    static validateIsTrue(value, message) {
        if (!value) {
            throw new Error(message);
        }
    }
    static async pullImage(logger, image) {
        this.validateIsDefined(image, 'Image must be provided');
        if (BootstrapUtils.pulledImages.indexOf(image) > -1) {
            return;
        }
        try {
            logger.info(`Pulling image ${image}`);
            const stdout = await this.spawn(logger, 'docker', ['pull', image], true, `${image} `);
            const outputLines = stdout.toString().split('\n');
            logger.info(`Image pulled: ${outputLines[outputLines.length - 2]}`);
            BootstrapUtils.pulledImages.push(image);
        }
        catch (e) {
            logger.warn(`Image ${image} could not be pulled!`);
        }
    }
    static resolveRootFolder() {
        const rootFolder = path_1.resolve(__dirname, '../..');
        if (!fs_1.existsSync(path_1.join(rootFolder, 'presets', 'shared.yml'))) {
            throw new Error(`Root Folder ${rootFolder} does not look right!`);
        }
        return rootFolder;
    }
    static runImageUsingExec(logger, { catapultAppFolder, image, userId, workdir, cmds, binds, }) {
        const volumes = binds.map((b) => `-v ${b}`).join(' ');
        const userParam = userId ? `-u ${userId}` : '';
        const workdirParam = workdir ? `--workdir=${workdir}` : '';
        const environmentParam = catapultAppFolder ? `--env LD_LIBRARY_PATH=${catapultAppFolder}/lib:${catapultAppFolder}/deps` : '';
        const runCommand = `docker run --rm ${userParam} ${workdirParam} ${environmentParam} ${volumes} ${image} ${cmds
            .map((a) => `"${a}"`)
            .join(' ')}`;
        logger.info(BootstrapUtils.secureString(`Running image using Exec: ${image} ${cmds.join(' ')}`));
        return this.exec(logger, runCommand);
    }
    static toAns1(privateKey) {
        const prefix = '302e020100300506032b657004220420';
        return `${prefix}${privateKey.toLowerCase()}`;
    }
    static secureString(text) {
        const regex = new RegExp('[0-9a-fA-F]{64}', 'g');
        return text.replace(regex, 'HIDDEN_KEY');
    }
    static sleep(ms) {
        // Create a promise that rejects in <ms> milliseconds
        return new Promise((resolve) => {
            setTimeout(() => {
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                //@ts-ignore
                resolve();
            }, ms);
        });
    }
    static poll(logger, promiseFunction, totalPollingTime, pollIntervalMs) {
        const startTime = new Date().getMilliseconds();
        return promiseFunction().then(async (result) => {
            if (result) {
                return true;
            }
            else {
                if (BootstrapUtils.stopProcess) {
                    return Promise.resolve(false);
                }
                const endTime = new Date().getMilliseconds();
                const newPollingTime = Math.max(totalPollingTime - pollIntervalMs - (endTime - startTime), 0);
                if (newPollingTime) {
                    logger.info(`Retrying in ${pollIntervalMs / 1000} seconds. Polling will stop in ${newPollingTime / 1000} seconds`);
                    await BootstrapUtils.sleep(pollIntervalMs);
                    return this.poll(logger, promiseFunction, newPollingTime, pollIntervalMs);
                }
                else {
                    return false;
                }
            }
        });
    }
    static async generateConfiguration(
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    templateContext, copyFrom, copyTo, excludeFiles = [], includeFiles = []) {
        // Loop through all the files in the config folder
        await fs_1.promises.mkdir(copyTo, { recursive: true });
        const files = await fs_1.promises.readdir(copyFrom);
        await Promise.all(files.map(async (file) => {
            const fromPath = path_1.join(copyFrom, file);
            const toPath = path_1.join(copyTo, file);
            // Stat the file to see if we have a file or dir
            const stat = await fs_1.promises.stat(fromPath);
            if (stat.isFile()) {
                const isMustache = file.indexOf('.mustache') > -1;
                const destinationFile = toPath.replace('.mustache', '');
                const fileName = path_1.basename(destinationFile);
                const notBlacklisted = excludeFiles.indexOf(fileName) === -1;
                const inWhitelistIfAny = includeFiles.length === 0 || includeFiles.indexOf(fileName) > -1;
                if (notBlacklisted && inWhitelistIfAny) {
                    if (isMustache) {
                        const template = await BootstrapUtils.readTextFile(fromPath);
                        const renderedTemplate = this.runTemplate(template, templateContext);
                        await fs_1.promises.writeFile(destinationFile, destinationFile.toLowerCase().endsWith('.json')
                            ? BootstrapUtils.formatJson(renderedTemplate)
                            : renderedTemplate);
                    }
                    else {
                        await fs_1.promises.copyFile(fromPath, destinationFile);
                    }
                    await fs_1.promises.chmod(destinationFile, 0o600);
                }
            }
            else if (stat.isDirectory()) {
                await fs_1.promises.mkdir(toPath, { recursive: true });
                await this.generateConfiguration(templateContext, fromPath, toPath, excludeFiles, includeFiles);
            }
        }));
    }
    static async chmodRecursive(path, mode) {
        // Loop through all the files in the config folder
        const stat = await fs_1.promises.stat(path);
        if (stat.isFile()) {
            await fs_1.promises.chmod(path, mode);
        }
        else if (stat.isDirectory()) {
            const files = await fs_1.promises.readdir(path);
            await Promise.all(files.map(async (file) => {
                await this.chmodRecursive(path_1.join(path, file), mode);
            }));
        }
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static runTemplate(template, templateContext) {
        try {
            const compiledTemplate = Handlebars.compile(template);
            return compiledTemplate(templateContext);
        }
        catch (e) {
            const securedTemplate = BootstrapUtils.secureString(template);
            const securedContext = BootstrapUtils.secureString(BootstrapUtils.toYaml(templateContext));
            const securedMessage = BootstrapUtils.secureString(e.message || 'Unknown');
            const message = `Unknown error rendering template. Error: ${securedMessage}\nTemplate:\n${securedTemplate}.`;
            throw new Error(`${message}\nContext: \n${securedContext}`);
        }
    }
    static async mkdir(path) {
        await fs_1.promises.mkdir(path, { recursive: true });
    }
    static async mkdirParentFolder(fileName) {
        const parentFolder = path_1.dirname(fileName);
        if (parentFolder) {
            await BootstrapUtils.mkdir(parentFolder);
        }
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static async writeYaml(path, object, password) {
        const yamlString = this.toYaml(password ? CryptoUtils_1.CryptoUtils.encrypt(object, BootstrapUtils.validatePassword(password)) : object);
        await BootstrapUtils.writeTextFile(path, yamlString);
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static pruneEmpty(obj) {
        return (function prune(current) {
            _.forOwn(current, (value, key) => {
                if (_.isUndefined(value) || _.isNull(value) || _.isNaN(value) || (_.isObject(value) && _.isEmpty(prune(value)))) {
                    delete current[key];
                }
            });
            // remove any leftover undefined values from the delete
            // operation on an array
            if (_.isArray(current))
                _.pull(current, undefined);
            return current;
        })(_.cloneDeep(obj)); // Do not modify the original object, create a clone instead
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static toYaml(object) {
        return yaml.safeDump(object, { skipInvalid: true, indent: 4, lineWidth: 140, noRefs: true });
    }
    static fromYaml(yamlString) {
        return yaml.safeLoad(yamlString);
    }
    static loadYaml(fileLocation, password) {
        const object = this.fromYaml(this.loadFileAsText(fileLocation));
        if (password) {
            BootstrapUtils.validatePassword(password);
            try {
                return CryptoUtils_1.CryptoUtils.decrypt(object, password);
            }
            catch (e) {
                throw new KnownError(`Cannot decrypt file ${fileLocation}. Have you used the right password?`);
            }
        }
        else {
            if (password !== false && CryptoUtils_1.CryptoUtils.encryptedCount(object) > 0) {
                throw new KnownError(`File ${fileLocation} seems to be encrypted but no password has been provided. Have you entered the right password?`);
            }
        }
        return object;
    }
    static loadFileAsText(fileLocation) {
        return fs_1.readFileSync(fileLocation, 'utf8');
    }
    static async writeTextFile(path, text) {
        await BootstrapUtils.mkdirParentFolder(path);
        await fs_1.promises.writeFile(path, text, 'utf8');
    }
    static async readTextFile(path) {
        return await fs_1.promises.readFile(path, 'utf8');
    }
    static async resolveDockerUserFromParam(logger, paramUser) {
        if (!paramUser || paramUser.trim() === '') {
            return undefined;
        }
        if (paramUser === BootstrapUtils.CURRENT_USER) {
            return BootstrapUtils.getDockerUserGroup(logger);
        }
        return paramUser;
    }
    static async createImageUsingExec(logger, targetFolder, dockerFile, tag) {
        const runCommand = `docker build -f ${dockerFile} ${targetFolder} -t ${tag}`;
        logger.info(`Creating image image '${tag}' from ${dockerFile}`);
        return (await this.exec(logger, runCommand)).stdout;
    }
    static async exec(logger, runCommand) {
        logger.debug(`Exec command: ${runCommand}`);
        const { stdout, stderr } = await exec(runCommand);
        return { stdout, stderr };
    }
    static async spawn(logger, command, args, useLogger, logPrefix = '') {
        const cmd = child_process_1.spawn(command, args);
        return new Promise((resolve, reject) => {
            logger.info(`Spawn command: ${command} ${args.join(' ')}`);
            let logText = useLogger ? '' : 'Check console for output....';
            const log = (data, isError) => {
                if (useLogger) {
                    logText = logText + `${data}\n`;
                    if (isError)
                        logger.warn(BootstrapUtils.secureString(logPrefix + data));
                    else
                        logger.info(BootstrapUtils.secureString(logPrefix + data));
                }
                else {
                    console.log(logPrefix + data);
                }
            };
            cmd.stdout.on('data', (data) => {
                log(`${data}`.trim(), false);
            });
            cmd.stderr.on('data', (data) => {
                log(`${data}`.trim(), true);
            });
            cmd.on('error', (error) => {
                log(`${error.message}`.trim(), true);
            });
            cmd.on('exit', (code, signal) => {
                if (code) {
                    log(`Process exited with code ${code} and signal ${signal}`, true);
                    reject(logText);
                }
                else {
                    resolve(logText);
                }
            });
            cmd.on('close', (code) => {
                if (code) {
                    log(`Process closed with code ${code}`, true);
                    reject(logText);
                }
                else {
                    resolve(logText);
                }
            });
            process.on('SIGINT', () => {
                resolve(logText);
            });
        });
    }
    static async getDockerUserGroup(logger) {
        const isWin = this.isWindows();
        if (isWin) {
            return '';
        }
        if (BootstrapUtils.dockerUserId !== undefined) {
            return BootstrapUtils.dockerUserId;
        }
        try {
            const userId = process === null || process === void 0 ? void 0 : process.getuid();
            const groupId = process === null || process === void 0 ? void 0 : process.getgid();
            const user = `${userId}:${groupId}`;
            logger.info(`User for docker resolved: ${user}`);
            if (userId === 0) {
                logger.error('YOU ARE RUNNING BOOTSTRAP AS ROOT!!!! THIS IS NOT RECOMMENDED!!!');
            }
            BootstrapUtils.dockerUserId = user;
            return user;
        }
        catch (e) {
            logger.info(`User for docker could not be resolved: ${e}`);
            return '';
        }
    }
    static isRoot() {
        return !this.isWindows() && (process === null || process === void 0 ? void 0 : process.getuid()) === 0;
    }
    static isWindows() {
        return process.platform === 'win32';
    }
    static validateFolder(workingDirFullPath) {
        if (!fs_1.existsSync(workingDirFullPath)) {
            throw new Error(`${workingDirFullPath} folder does not exist`);
        }
        if (!fs_1.lstatSync(workingDirFullPath).isDirectory()) {
            throw new Error(`${workingDirFullPath} is not a folder!`);
        }
    }
    static getTargetFolder(target, absolute, ...paths) {
        if (absolute) {
            return path_1.join(process.cwd(), target, ...paths);
        }
        else {
            return path_1.join(target, ...paths);
        }
    }
    static getTargetNodesFolder(target, absolute, ...paths) {
        return this.getTargetFolder(target, absolute, this.targetNodesFolder, ...paths);
    }
    static getTargetGatewayFolder(target, absolute, ...paths) {
        return this.getTargetFolder(target, absolute, this.targetGatewaysFolder, ...paths);
    }
    static getTargetNemesisFolder(target, absolute, ...paths) {
        return this.getTargetFolder(target, absolute, this.targetNemesisFolder, ...paths);
    }
    static getTargetDatabasesFolder(target, absolute, ...paths) {
        return this.getTargetFolder(target, absolute, this.targetDatabasesFolder, ...paths);
    }
    static add(a, b) {
        if (_.isNumber(a) && _.isNumber(b)) {
            return Number(a) + Number(b);
        }
        if (typeof a === 'string' && typeof b === 'string') {
            return a + b;
        }
        return '';
    }
    static minus(a, b) {
        if (!_.isNumber(a)) {
            throw new TypeError('expected the first argument to be a number');
        }
        if (!_.isNumber(b)) {
            throw new TypeError('expected the second argument to be a number');
        }
        return Number(a) - Number(b);
    }
    static computerMemory(percentage) {
        return (os_1.totalmem() * percentage) / 100;
    }
    static toAmount(renderedText) {
        const numberAsString = (renderedText + '').split("'").join('');
        if (!numberAsString.match(/^\d+$/)) {
            throw new Error(`'${renderedText}' is not a valid integer`);
        }
        return (numberAsString.match(/\d{1,3}(?=(\d{3})*$)/g) || [numberAsString]).join("'");
    }
    static toHex(renderedText) {
        if (!renderedText) {
            return '';
        }
        const numberAsString = BootstrapUtils.toSimpleHex(renderedText);
        return '0x' + (numberAsString.match(/\w{1,4}(?=(\w{4})*$)/g) || [numberAsString]).join("'");
    }
    static toSimpleHex(renderedText) {
        if (!renderedText) {
            return '';
        }
        return renderedText.toString().split("'").join('').replace(/^(0x)/, '');
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static toJson(object) {
        return JSON.stringify(object, null, 2);
    }
    static formatJson(string) {
        // Validates and format the json string.
        try {
            return JSON.stringify(JSON.parse(string), null, 2);
        }
        catch (e) {
            throw new Error(`${e.message}:JSON\n ${string}`);
        }
    }
    static splitCsv(object) {
        return (object || '')
            .split(',')
            .map((string) => string.trim())
            .filter((string) => string);
    }
    static toSeconds(serverDuration) {
        return symbol_sdk_1.DtoMapping.parseServerDuration(serverDuration).seconds();
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    static migrate(logger, entityName, versioned, migrations = []) {
        if (!versioned) {
            return versioned;
        }
        const currentVersion = migrations.length + 1;
        versioned.version = versioned.version || 1;
        if (versioned.version == currentVersion) {
            return versioned;
        }
        logger.info(`Migrating object ${entityName} from version ${versioned.version} to version ${currentVersion}`);
        if (versioned.version > currentVersion) {
            throw new Error(`Current data version is ${versioned.version} but higher version is ${currentVersion}`);
        }
        const migratedVersioned = migrations.slice(versioned.version - 1).reduce((toMigrateData, migration) => {
            if (toMigrateData === undefined) {
                logger.info(`data to migrate is undefined, ignoring migration ${migration.description}`);
                return undefined;
            }
            logger.info(`Applying migration ${migration.description}`);
            return migration.migrate(toMigrateData);
        }, versioned);
        migratedVersioned.version = currentVersion;
        logger.info(`Object ${entityName} migrated to version ${currentVersion}`);
        return migratedVersioned;
    }
    static getNetworkIdentifier(networkType) {
        return BootstrapUtils.getNetworkName(networkType);
    }
    static getNetworkName(networkType) {
        switch (networkType) {
            case symbol_sdk_1.NetworkType.MAIN_NET:
                return 'mainnet';
            case symbol_sdk_1.NetworkType.TEST_NET:
                return 'testnet';
        }
        throw new Error(`Invalid Network Type ${networkType}`);
    }
    static resolveWorkingDirPath(workingDir, path) {
        if (path_1.isAbsolute(path)) {
            return path;
        }
        else {
            return path_1.join(workingDir, path);
        }
    }
    static createDerFile(privateKey, file) {
        fs_1.writeFileSync(file, symbol_sdk_1.Convert.hexToUint8(BootstrapUtils.toAns1(privateKey)));
    }
    static isYmlFile(string) {
        return string.toLowerCase().endsWith('.yml') || string.toLowerCase().endsWith('.yaml');
    }
    static validatePassword(password) {
        const passwordMinSize = 4;
        if (password.length < passwordMinSize) {
            throw new KnownError(`Password is too short. It should have at least ${passwordMinSize} characters!`);
        }
        return password;
    }
}
exports.BootstrapUtils = BootstrapUtils;
BootstrapUtils.defaultTargetFolder = 'target';
BootstrapUtils.targetNodesFolder = 'nodes';
BootstrapUtils.targetGatewaysFolder = 'gateways';
BootstrapUtils.targetExplorersFolder = 'explorers';
BootstrapUtils.targetWalletsFolder = 'wallets';
BootstrapUtils.targetDatabasesFolder = 'databases';
BootstrapUtils.targetNemesisFolder = 'nemesis';
BootstrapUtils.defaultWorkingDir = '.';
BootstrapUtils.CURRENT_USER = 'current';
BootstrapUtils.pulledImages = [];
BootstrapUtils.VERSION = version;
/**
 * The folder where this npm module is installed. It defines where the default presets, configurations, etc are located.
 */
BootstrapUtils.ROOT_FOLDER = BootstrapUtils.resolveRootFolder();
BootstrapUtils.stopProcess = false;
BootstrapUtils.onProcessListener = (() => {
    process.on('SIGINT', () => {
        BootstrapUtils.stopProcess = true;
    });
})();
//HANDLEBARS READY FUNCTIONS:
BootstrapUtils.initialize = (() => {
    Handlebars.registerHelper('toAmount', BootstrapUtils.toAmount);
    Handlebars.registerHelper('toHex', BootstrapUtils.toHex);
    Handlebars.registerHelper('toSimpleHex', BootstrapUtils.toSimpleHex);
    Handlebars.registerHelper('toSeconds', BootstrapUtils.toSeconds);
    Handlebars.registerHelper('toJson', BootstrapUtils.toJson);
    Handlebars.registerHelper('splitCsv', BootstrapUtils.splitCsv);
    Handlebars.registerHelper('add', BootstrapUtils.add);
    Handlebars.registerHelper('minus', BootstrapUtils.minus);
    Handlebars.registerHelper('computerMemory', BootstrapUtils.computerMemory);
})();
