@ocap/util
Version:
utils shared across multiple forge js libs, works in both node.js and browser
527 lines (526 loc) • 16.1 kB
JavaScript
import isBoolean from 'lodash/isBoolean';
import isString from 'lodash/isString';
import isNumber from 'lodash/isNumber';
import isObject from 'lodash/isObject';
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import isNull from 'lodash/isNull';
import rightPad from 'lodash/padEnd';
import leftPad from 'lodash/padStart';
import base58 from 'bs58';
import base64 from 'base64-url';
import BN from 'bn.js';
import * as utf8 from 'utf8';
const DID_PREFIX = 'did:abt:';
const zero = new BN(0);
const negative1 = new BN(-1);
const base58btc = {
encode: (v) => `z${base58.encode(v)}`,
decode: (v) => base58.decode(v.slice(1)),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isBase58btc = (data) => {
if (typeof data !== 'string') {
return false;
}
if (data[0] === 'z') {
try {
base58btc.decode(data);
return true;
}
catch (err) {
return false;
}
}
return false;
};
export { BN, leftPad, rightPad };
/**
* Returns a BN object, converts a number value to a BN
* @param {string|number|BN} `arg` input a string number, hex string number, number, BigNumber or BN object
* @return {BN} `output` BN object of the number
* @throws if the argument is not an array, object that isn't a bignumber, not a string number or number
*/
export const numberToBN = (arg) => {
if (typeof arg === 'string' || typeof arg === 'number') {
let multiplier = new BN(1); // eslint-disable-line
const formattedString = String(arg).toLowerCase().trim();
const isHexPrefixed = formattedString.substr(0, 2) === '0x' || formattedString.substr(0, 3) === '-0x';
let stringArg = stripHexPrefix(formattedString); // eslint-disable-line
if (stringArg.substr(0, 1) === '-') {
stringArg = stripHexPrefix(stringArg.slice(1));
multiplier = new BN(-1, 10);
}
stringArg = stringArg === '' ? '0' : stringArg;
if ((!stringArg.match(/^-?[0-9]+$/) && stringArg.match(/^[0-9A-Fa-f]+$/)) ||
stringArg.match(/^[a-fA-F]+$/) ||
(isHexPrefixed === true && stringArg.match(/^[0-9A-Fa-f]+$/))) {
return new BN(stringArg, 16).mul(multiplier);
}
if ((stringArg.match(/^-?[0-9]+$/) || stringArg === '') && isHexPrefixed === false) {
return new BN(stringArg, 10).mul(multiplier);
}
}
if (isBN(arg)) {
return new BN(arg.toString(10), 10);
}
throw new Error(`[number-to-bn] while converting number ${JSON.stringify(arg)} to BN.js instance, error: invalid number value. Value must be an integer, hex string, BN or BigNumber instance. Note, decimals are not supported.`);
};
/**
* Returns a `boolean` on whether or not the `string` starts with '0x'
*
* @public
* @static
* @param {String} str the string input value
* @return {Boolean} a boolean if it is or is not hex prefixed
* @throws if the str input is not a string
*/
export const isHexPrefixed = (str) => {
if (typeof str !== 'string') {
throw new Error('[is-hex-prefixed] value must be type string');
}
return str.slice(0, 2).toLowerCase() === '0x';
};
/**
* Removes '0x' from a given `String` if present
*
* @public
* @static
*/
export const stripHexPrefix = (str) => {
if (typeof str === 'string') {
return isHexPrefixed(str) ? str.slice(2) : str;
}
return str;
};
/**
* Returns true if object is BN, otherwise false
*
* @public
* @static
* @method isBN
* @param {Object} object
* @returns {Boolean}
*/
export const isBN = (object) => object instanceof BN || (object && object.constructor && object.constructor.name === 'BN');
/**
* Returns true if object is BigNumber, otherwise false
*
* @public
* @static
* @method isBigNumber
* @param {Object} object
* @returns {Boolean}
*/
export const isBigNumber = (object) => object && object.constructor && object.constructor.name === 'BigNumber';
/**
* Check if string is HEX, requires a 0x in front
*
* @public
* @static
* @method isHexStrict
* @param {String} hex to be checked
* @returns {Boolean}
*/
export const isHexStrict = (hex) => (isString(hex) || isNumber(hex)) && /^(-)?0x[0-9a-f]*$/i.test(hex);
/**
* Check if string is HEX
*
* @public
* @static
* @method isHex
* @param {String} hex to be checked
* @returns {Boolean}
*/
export const isHex = (hex) => (isString(hex) || isNumber(hex)) && /^(-0x|0x|0X|-0X)?[0-9a-f]*$/i.test(hex);
/**
* Takes an input and transforms it into an BN
*
* @public
* @static
* @method toBN
* @param {Number|String|BN} num, string, HEX string or BN
* @returns {BN} BN
*/
export const toBN = (num, base = 10) => {
try {
if (typeof num === 'number' && num > 0) {
const numStr = Number(num).toLocaleString('fullwide', { useGrouping: false });
return new BN(numStr, base);
}
return numberToBN(num);
}
catch (error) {
throw new Error(`${error} Given value: "${num}"`);
}
};
/**
* Should be called to get hex representation (prefixed by 0x) of utf8 string
*
* @public
* @static
* @method utf8ToHex
* @param {String} str
* @returns {String} hex representation of input string
*/
export const utf8ToHex = (str) => {
str = utf8.encode(str);
let hex = '';
/* eslint-disable no-control-regex */
// remove \u0000 padding from either side
str = str.replace(/^(?:\u0000)*/, '');
str = str.split('').reverse().join('');
str = str.replace(/^(?:\u0000)*/, '');
str = str.split('').reverse().join('');
/* eslint-enable no-control-regex */
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
const n = code.toString(16);
hex += n.length < 2 ? `0${n}` : n;
}
return `0x${hex}`;
};
/**
* Should be called to get utf8 from it's hex representation
*
* @public
* @static
* @method hexToUtf8
* @param {String} hex
* @returns {String} ascii string representation of hex value
*/
export const hexToUtf8 = (hex) => {
if (!isHexStrict(hex))
throw new Error(`The parameter "${hex}" must be a valid HEX string.`);
let str = '';
let code = 0;
hex = hex.replace(/^0x/i, '');
// remove 00 padding from either side
hex = hex.replace(/^(?:00)*/, '');
hex = hex.split('').reverse().join('');
hex = hex.replace(/^(?:00)*/, '');
hex = hex.split('').reverse().join('');
const l = hex.length;
for (let i = 0; i < l; i += 2) {
code = parseInt(hex.substr(i, 2), 16);
// if (code !== 0) {
str += String.fromCharCode(code);
// }
}
return utf8.decode(str);
};
/**
* Converts value to number representation
*
* @public
* @static
* @method hexToNumber
* @param {String|Number|BN} value
* @returns {Number}
*/
export const hexToNumber = (value) => {
if (!value) {
return 0;
}
return toBN(value).toNumber();
};
/**
* Converts value to hex representation
*
* @public
* @static
* @method numberToHex
* @param {String|Number|BN} value
* @returns {String}
*/
export const numberToHex = (value) => {
if (isNull(value) || typeof value === 'undefined') {
return value;
}
// eslint-disable-next-line no-restricted-globals
if (!isFinite(value) && !isHex(value)) {
throw new Error(`Given input "${value}" is not a number.`);
}
const num = toBN(value);
const result = num.toString(16);
return num.lt(new BN(0)) ? `-0x${result.substr(1)}` : `0x${result}`;
};
/**
* Convert a byte array to a hex string
* Note: Implementation from crypto-js
*
* @public
* @static
* @method bytesToHex
* @param {Array} bytes
* @returns {String} the hex string
*/
export const bytesToHex = (bytes) => {
const hex = [];
for (let i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xf).toString(16));
}
return `0x${hex.join('')}`;
};
/**
* Convert a hex string to a byte array
* Note: Implementation from crypto-js
*
* @public
* @static
* @method hexToBytes
* @param {String} hex
* @returns {Array} the byte array
*/
export const hexToBytes = (hex) => {
hex = hex.toString(16);
if (!isHex(hex)) {
throw new Error(`Given value "${hex}" is not a valid hex string.`);
}
hex = hex.replace(/^0x/i, '');
hex = hex.length % 2 ? `0${hex}` : hex;
const bytes = [];
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}
return bytes;
};
/**
* Auto converts any given value into it's hex representation.
* And even stringify objects before.
*
* @public
* @static
* @method toHex
* @param {String|Number|BN|Object|TypedArray|Buffer} value
* @param {Boolean} returnType
* @returns {String}
*/
export const toHex = (value, returnType = false) => {
if (isUint8Array(value) || Buffer.isBuffer(value)) {
return returnType ? 'bytes' : bytesToHex(value);
}
if (isBase58btc(value)) {
return returnType ? 'bytes' : bytesToHex(base58btc.decode(value));
}
if (isBoolean(value)) {
// eslint-disable-next-line no-nested-ternary
return returnType ? 'bool' : value ? '0x01' : '0x00';
}
if (isObject(value) && !isBigNumber(value) && !isBN(value)) {
return returnType ? 'string' : utf8ToHex(JSON.stringify(value));
}
// if its a negative number, pass it through numberToHex
if (typeof value === 'string') {
if (value.indexOf('-0x') === 0 || value.indexOf('-0X') === 0) {
return returnType ? 'int256' : numberToHex(value);
}
if (value.indexOf('0x') === 0 || value.indexOf('0X') === 0) {
return returnType ? 'bytes' : value;
}
// TODO: some edge case may be not properly handled here
return returnType ? 'string' : utf8ToHex(value);
}
// eslint-disable-next-line no-nested-ternary
return returnType ? (+value < 0 ? 'int256' : 'uint256') : numberToHex(value);
};
export const numberToString = (arg) => {
if (typeof arg === 'string') {
if (!arg.match(/^-?[0-9.]+$/)) {
throw new Error(`Invalid value '${arg}' while converting to string, should be a number matching (^-?[0-9.]+).`);
}
return arg;
}
if (typeof arg === 'number') {
if (Number.isInteger(arg) && arg > 0) {
return Number(arg).toLocaleString('fullwide', { useGrouping: false });
}
return String(arg);
}
if (typeof arg === 'object' && arg.toString && (arg.toTwos || arg.dividedToIntegerBy)) {
if (arg.toPrecision) {
return String(arg.toPrecision());
}
return arg.toString(10);
}
throw new Error(`while converting number to string, invalid number value '${arg}' type ${typeof arg}.`);
};
/**
* Format a big number to human readable number, such as 1_0000_0000_0000_000 => 1 Token
*/
export const fromUnitToToken = (input, decimal = 18, optionsInput = {}) => {
let unit = toBN(input);
const negative = unit.lt(zero);
const base = toBN(`1${'0'.repeat(decimal)}`, 10);
const baseLength = base.toString(10).length - 1 || 1;
const options = optionsInput || {};
if (negative) {
unit = unit.mul(negative1);
}
let fraction = unit.mod(base).toString(10);
while (fraction.length < baseLength) {
fraction = `0${fraction}`;
}
if (!options.pad) {
// eslint-disable-next-line prefer-destructuring
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
}
let whole = unit.div(base).toString(10);
if (options.commify) {
whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
let value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`;
if (negative) {
value = `-${value}`;
}
return value;
};
/**
* Convert human readable token number to big number instance
*/
export const fromTokenToUnit = (input, decimal = 18) => {
let token = numberToString(input);
const base = toBN(`1${'0'.repeat(decimal)}`, 10);
const baseLength = base.toString(10).length - 1 || 1;
// Is it negative?
const negative = token.substring(0, 1) === '-';
if (negative) {
token = token.substring(1);
}
if (token === '.') {
throw new Error(`error converting token ${input} to unit, invalid value`);
}
// Split it into a whole and fractional part
const comps = token.split('.');
if (comps.length > 2) {
throw new Error(`error converting token ${input} to unit, too many decimal points`);
}
let whole = comps[0];
let fraction = comps[1];
if (!whole) {
whole = '0';
}
if (!fraction) {
fraction = '0';
}
if (fraction.length > baseLength) {
throw new Error(`error converting token ${input} to unit, too many decimal places`);
}
while (fraction.length < baseLength) {
fraction += '0';
}
whole = new BN(whole);
fraction = new BN(fraction);
let unit = whole.mul(base).add(fraction);
if (negative) {
unit = unit.mul(negative1);
}
return new BN(unit.toString(10), 10);
};
/**
* Validates if a value is an Uint8Array.
*/
export function isUint8Array(value) {
return Object.prototype.toString.call(value) === '[object Uint8Array]';
}
/**
* Generate a random UUID
*/
export function UUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Check if a string is valid UUID
*/
export function isUUID(str) {
return /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(str);
}
/**
* Convert input to Uint8Array on best effort, base64 node supported
*/
export function toUint8Array(v) {
let vb = null;
if ([null, undefined, ''].includes(v)) {
vb = new Uint8Array();
}
else if (Buffer.isBuffer(v)) {
vb = new Uint8Array(v);
}
else if (isHexStrict(v)) {
vb = new Uint8Array(hexToBytes(v));
}
else if (isUint8Array(v)) {
vb = new Uint8Array(v);
}
else if (isBase58btc(v)) {
vb = new Uint8Array(base58btc.decode(v));
}
else if (typeof v === 'string') {
vb = new Uint8Array(hexToBytes(toHex(v)));
}
else {
throw new Error(`Unsupported input type ${typeof v} detected for toBuffer, only Uint8Array/Buffer/Hex/Base58 are supported`);
}
return vb;
}
/**
* Convert input to Buffer on best effort, base64 not supported
*/
export function toBuffer(v) {
return Buffer.from(toUint8Array(v));
}
/**
* Convert input to base58btc format on best effort
*/
export function toBase58(v) {
const buf = base58btc.encode(toUint8Array(v));
return Buffer.from(buf).toString('utf-8');
}
/**
* Decode base58 string
*/
export function fromBase58(v) {
if (isBase58btc(v) === false) {
throw new Error('fromBase58 expect strict base58 encoded string as input');
}
return Buffer.from(base58btc.decode(v));
}
/**
* Convert input to base64 format
*/
export function toBase64(v, escape = true) {
const encoded = base64.encode(toBuffer(v));
return escape ? base64.escape(encoded) : encoded;
}
/**
* Decode base64(base64_url) string to buffer
*/
export function fromBase64(v) {
if (typeof v !== 'string') {
throw new Error('fromBase64 requires input to be a string');
}
return Buffer.from(base64.unescape(v), 'base64');
}
/**
* Convert did to address: remove `did:abt:` prefix
*/
export function toAddress(did) {
return did.replace(DID_PREFIX, '');
}
/**
* Convert address to did: prepend `did:abt:` prefix
*/
export function toDid(address) {
return `${DID_PREFIX}${toAddress(address)}`;
}
export function isSameDid(a, b) {
return toAddress(a).toLowerCase() === toAddress(b).toLowerCase();
}
export function formatTxType(type) {
return upperFirst(camelCase(type));
}