/**
* @author James Aman (j.aman@topl.me)
* @author Raul Aragonez (r.aragonez@topl.me)
* @version 1.0.0
* @date 2021.03.04
**/
"use strict";
// Dependencies
const base58 = require("bs58");
// Primary sub-modules
const Requests = require("./modules/Requests");
const KeyManager = require("./modules/KeyManager");
// Utilities
const Hash = require("./utils/Hash");
const Address = require("./utils/address-utils.js");
// Libraries
const pollTx = require("./lib/polling");
// Constants definitions
const validTxMethods = [
"createRawArbitTransfer",
"createRawAssetTransfer",
"createRawPolyTransfer"
];
/**
* Each sub-module may be initialized in one of three ways
* 1. Providing a separetly initialized {@link Requests} and {@link KeyManager} instance. Each of these instances may be initialized using the
* static methods `Requests` or `KeyManager` available in the BramblJS class.
* 2. Providing custom configuration parameters needed to create new instances of each sub-module with the specified parameters
* 3. Providing minimal inputs (i.e. calling Brambl with only a string constructor arguement). This will create new instances of
* the sub-modules with default parameters. KeyManager will create a new keyfile and Requests will target a locally running
* instance of Bifrost.
* @class
* @classdesc Creates an instance of Brambl for interacting with the Topl protocol
* @requires Requests
* @requires KeyManager
*/
class Brambl {
// private variables
#networkPrefix;
/**
* @constructor
* @param {object|string} params Constructor parameters object
* @param {string} params.networkPrefix Network Prefix
* @param {string} params.password The password used to encrpt the keyfile, same as [params.KeyManager.password]
* @param {object} params.KeyManager KeyManager object (may be either an instance or config parameters)
* @param {string} [params.KeyManager.password] The password used to encrpt the keyfile
* @param {string} [params.KeyManager.keyPath] Path to a keyfile
* @param {object} [params.KeyManager.keyFile] encrypted keyFile javascript object.
* @param {string} [params.KeyManager.constants] Parameters for encrypting the user's keyfile
* @param {object} params.Requests Request object (may be either an instance or config parameters)
* @param {string} [params.Requests.url] The chain provider to send requests to
* @param {string} [params.Requests.apikey] Api key for authorizing access to the chain provider
*/
constructor(params = {}) {
// default values for the constructor arguement
const keyManagerVar = params.KeyManager || {};
const requestsVar = params.Requests || {};
this.#networkPrefix = params.networkPrefix || "private";
// If only a string is given in the constructor, assume it is the password.
// Therefore, target a local chain provider and make a new key
if (params.constructor === String) {
keyManagerVar.password = params;
}
if (params.password && params.password.constructor === String) {
keyManagerVar.password = params.password;
}
// validate network prefix
if (!Address.isValidNetwork(this.#networkPrefix)) {
throw new Error(`Invalid Network Prefix. Must be one of: ${Address.getValidNetworksList()}`);
}
if (requestsVar instanceof Requests) {
// Request instance provided, reuse it
this.requests = requestsVar;
} else {
// create new instance and pass parameters
this.requests = new Requests(this.#networkPrefix, requestsVar.url, requestsVar.apiKey);
}
// Setup KeyManager object
if (keyManagerVar instanceof KeyManager) {
this.keyManager = keyManagerVar;
} else {
if (!keyManagerVar.password) throw new Error("An encryption password is required to open a keyfile");
// create new KeyManager
this.keyManager = new KeyManager({
password: keyManagerVar.password,
keyPath: keyManagerVar.keyPath,
keyFile: keyManagerVar.keyFile,
constants: keyManagerVar.constants,
networkPrefix: this.#networkPrefix
});
}
// If KeyManager and Requests instances were not created by Brambl class verify that both have a matching NetworkPrefix
if (this.#networkPrefix !== this.requests.networkPrefix || this.#networkPrefix !== this.keyManager.networkPrefix) {
throw new Error("Incompatible network prefixes set for Requests and KeyManager Instances.");
}
// Expose Utilities
this.utils = {Hash, Address};
}
/**
* Getter for private property #networkPrefix
* @memberof Brambl
* @returns {string} value of #networkPrefix
*/
get networkPrefix() {
return this.#networkPrefix;
}
/**
* Setter for private property #isLocked
* @memberof Brambl
* @param {any} args ignored, only necessary for setter
* @returns {void} Error is thrown to protect private variable
*/
set networkPrefix(args) {
throw new Error("Invalid private variable access.");
}
/**
* Method for creating a separate Requests instance
* @static
*
* @param {string} [networkPrefix="private"] Network Prefix, defaults to "private"
* @param {string} [url="http://localhost:9085/"] Chain provider location
* @param {string} [apiKey="topl_the_world!"] Access key for authorizing requests to the client API
* @returns {object} new Requests instance
* @memberof Brambl
*/
static Requests(networkPrefix, url, apiKey) {
return new Requests(networkPrefix, url, apiKey);
}
/**
* Method for creating a separate KeyManager instance
* @static
*
* @param {object} params constructor object for key manager or as a string password
* @param {string} [params.password] password for encrypting (decrypting) the keyfile
* @param {string} [params.keyPath] path to import keyfile
* @param {object} [params.keyFile] encrypted keyFile javascript object.
* @param {object} [params.constants] default encryption options for storing keyfiles
* @param {string} [params.networkPrefix] Network Prefix, defaults to "private"
* @returns {object} new KeyManager instance
* @memberof Brambl
*/
static KeyManager(params) {
return new KeyManager(params);
}
/**
* Method for accessing the hash utility as a static method
* @static
*
* @param {string} type type of hash to construct
* @param {object | string} msg the msg that will be hashed
* @param {object | string} encoding optional, default is "base58"
* @returns {object} Hash Instance
* @memberof Brambl
*/
static Hash(type, msg, encoding = "base58") {
const allowedTypes = ["string", "file", "any"];
if (!allowedTypes.includes(type)) throw new Error(`Invalid type specified. Must be one of ${allowedTypes}`);
return Hash[type](msg, encoding);
}
}
/**
* Add a signature to a prototype transaction using an unlocked key manager object
*
* @param {object} prototypeTx An unsigned transaction JSON object
* @param {object|object[]} userKeys A keyManager object containing the user's key (may be an array)
* @returns {object} transaction with signatures to all given key files
*/
Brambl.prototype.addSigToTx = async function(prototypeTx, userKeys) {
// function for generating a signature in the correct format
const genSig = (keys, txMsgToSign) => {
return Object.fromEntries(
keys.map(
(key) => {
const pubKeyHashByte = Buffer.from("01", "hex");
const prop = Buffer.concat([pubKeyHashByte, base58.decode(key.pk)], 33);
const sig = Buffer.concat([pubKeyHashByte, key.sign(txMsgToSign)], 65);
return [base58.encode(prop), base58.encode(sig)];
}
)
);
};
// list of Key Managers
const keys = Array.isArray(userKeys) ? userKeys : [userKeys];
return {
...prototypeTx.rawTx,
signatures: genSig(keys, prototypeTx.messageToSign)
};
};
/**
* Used to sign a prototype transaction and broadcast to a chain provider
*
* @param {object} prototypeTx An unsigned transaction JSON object
* @returns {promise} requests.broadcastTx promise
*/
Brambl.prototype.signAndBroadcast = async function(prototypeTx) {
const formattedTx = await this.addSigToTx(prototypeTx, this.keyManager);
return this.requests.broadcastTx({tx: formattedTx}).catch((e) => {
console.error(e); throw e;
});
};
/**
* Create a new transaction, then sign and broadcast
*
* @param {string} method The chain resource method to create a transaction for. Valid transaction methods are the following: "createRawArbitTransfer", "createRawAssetTransfer", "createRawPolyTransfer".
* @param {object} params Transaction parameters object
* @returns {promise} signAndBroadcast promise
*/
Brambl.prototype.transaction = async function(method, params) {
if (!validTxMethods.includes(method)) throw new Error("Invalid transaction method");
return this.requests[method](params)
.then((res) => this.signAndBroadcast(res.result));
};
/**
* A function to initiate polling of the chain provider for a specified transaction.
* This function begins by querying 'getTransactionById' which looks for confirmed transactions only.
* If the transaction is not confirmed, the mempool is checked using 'getTransactionFromMemPool' to
* ensure that the transaction is pending. The parameter 'numFailedQueries' specifies the number of consecutive
* failures (when resorting to querying the mempool) before ending the polling operation prematurely.
*
* @param {string} txId The unique transaction ID to look for
* @param {object} [options] Optional parameters to control the polling behavior
* @param {number} [options.timeout] The timeout (in seconds) before the polling operation is stopped
* @param {number} [options.interval] The interval (in seconds) between attempts
* @param {number} [options.maxFailedQueries] The maximum number of consecutive failures (to find the unconfirmed transaction) before ending the poll execution
* @returns {promise} pollTx - polling promise
*/
Brambl.prototype.pollTx = async function(txId, options) {
const opts = options || {timeout: 90, interval: 3, maxFailedQueries: 10};
return pollTx(this.requests, txId, opts);
};
/**
* A function to create an Asset Code by utilizing the Key created or imported by
* Brambl. Asset Codes are necessary to create Raw Asset transactions.
*
* @param {string} shortName name of assets, up to 8 bytes long latin-1 enconding
* @returns {string} asset code is returned if successful
*/
Brambl.prototype.createAssetCode = function(shortName) {
return this.utils.Address.createAssetCode(this.networkPrefix, this.keyManager.address, shortName);
};
module.exports = Brambl;