@ocap/util
Version:
utils shared across multiple forge js libs, works in both node.js and browser
450 lines (448 loc) • 15.2 kB
JavaScript
import { BN } from "./bn.mjs";
import { scopeMatch, scopeMatchAny } from "./scope-match.mjs";
import BaseBN from "bn.js";
import base64 from "base64-url";
import base58 from "bs58";
import utf8 from "utf8";
//#region src/index.ts
const isBoolean = (x) => typeof x === "boolean";
const isNull = (x) => x === null;
const isNumber = (x) => typeof x === "number";
const isString = (x) => typeof x === "string";
const isObject = (x) => x !== null && typeof x === "object";
const leftPad = (str, len, char) => String(str).padStart(len, char);
const rightPad = (str, len, char) => String(str).padEnd(len, char);
const upperFirst = (str) => str ? str[0].toUpperCase() + str.slice(1) : "";
const camelCase = (str) => {
const words = str.match(/[a-zA-Z0-9]+/g);
if (!words) return "";
return words.map((w, i) => {
const lower = w.toLowerCase();
return i === 0 ? lower : lower[0].toUpperCase() + lower.slice(1);
}).join("");
};
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))
};
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;
};
/**
* 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
*/
const numberToBN = (arg) => {
if (typeof arg === "string" || typeof arg === "number") {
let multiplier = new BN(1);
const formattedString = String(arg).toLowerCase().trim();
const isHexPrefixed$1 = formattedString.substr(0, 2) === "0x" || formattedString.substr(0, 3) === "-0x";
let stringArg = stripHexPrefix(formattedString);
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$1 === true && stringArg.match(/^[0-9A-Fa-f]+$/)) return new BN(stringArg, 16).mul(multiplier);
if ((stringArg.match(/^-?[0-9]+$/) || stringArg === "") && isHexPrefixed$1 === 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
*/
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
*/
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}
*/
const isBN = (object) => object instanceof BN || object instanceof BaseBN || object?.constructor && object.constructor.name === "BN";
/**
* Returns true if object is BigNumber, otherwise false
*
* @public
* @static
* @method isBigNumber
* @param {Object} object
* @returns {Boolean}
*/
const isBigNumber = (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}
*/
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}
*/
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
*/
const toBN = (num, base = 10) => {
try {
if (typeof num === "number" && num > 0) return new BN(Number(num).toLocaleString("fullwide", { useGrouping: false }), 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
*/
const utf8ToHex = (str) => {
str = utf8.encode(str);
let hex = "";
str = str.replace(/^(?:\u0000)*/, "");
str = str.split("").reverse().join("");
str = str.replace(/^(?:\u0000)*/, "");
str = str.split("").reverse().join("");
for (let i = 0; i < str.length; i++) {
const n = str.charCodeAt(i).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
*/
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, "");
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 = Number.parseInt(hex.substr(i, 2), 16);
str += String.fromCharCode(code);
}
return utf8.decode(str);
};
/**
* Converts value to number representation
*
* @public
* @static
* @method hexToNumber
* @param {String|Number|BN} value
* @returns {Number}
*/
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}
*/
const numberToHex = (value) => {
if (isNull(value) || typeof value === "undefined") return value;
const isNumericString = typeof value === "string" && !Number.isNaN(Number(value)) && Number.isFinite(Number(value));
if (!Number.isFinite(value) && !isHex(value) && !isNumericString && !isBN(value) && !isBigNumber(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
*/
const bytesToHex = (bytes) => {
const hex = [];
for (let i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 15).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
*/
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(Number.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}
*/
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)) return returnType ? "bool" : value ? "0x01" : "0x00";
if (isObject(value) && !isBigNumber(value) && !isBN(value)) return returnType ? "string" : utf8ToHex(JSON.stringify(value));
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;
return returnType ? "string" : utf8ToHex(value);
}
return returnType ? +value < 0 ? "int256" : "uint256" : numberToHex(value);
};
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
*/
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) {
const match = fraction.match(/^([0-9]*[1-9]|0)(0*)/);
fraction = match ? match[1] : fraction;
}
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
*/
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;
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`);
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.
*/
function isUint8Array(value) {
return Object.prototype.toString.call(value) === "[object Uint8Array]";
}
/**
* Generate a random UUID
*/
function UUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
return (c === "x" ? r : r & 3 | 8).toString(16);
});
}
/**
* Check if a string is valid UUID
*/
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
*/
function toUint8Array(v) {
let vb = null;
if ([
null,
void 0,
""
].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
*/
function toBuffer(v) {
return Buffer.from(toUint8Array(v));
}
/**
* Convert input to base58btc format on best effort
*/
function toBase58(v) {
const buf = base58btc.encode(toUint8Array(v));
return Buffer.from(buf).toString("utf-8");
}
/**
* Decode base58 string
*/
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
*/
function toBase64(v, escape = true) {
const encoded = base64.encode(toBuffer(v));
return escape ? base64.escape(encoded) : encoded;
}
/**
* Decode base64(base64_url) string to buffer
*/
function fromBase64(v) {
if (typeof v !== "string") throw new Error("fromBase64 requires input to be a string");
return Buffer.from(base64.unescape(v), "base64");
}
/** Pattern matching any did:{method}: prefix where method is [a-z0-9]+ */
const DID_PREFIX_PATTERN = /^did:[a-z0-9]+:/;
/**
* Convert did to address: remove `did:{method}:` prefix.
* Strips only the first layer prefix (anchored regex).
*/
function toAddress(did) {
return did.replace(DID_PREFIX_PATTERN, "");
}
/**
* Convert address to did: prepend `did:{method}:` prefix.
* @param address - bare address or full DID
* @param method - DID method (default: 'abt')
*/
function toDid(address, method = "abt") {
return `did:${method}:${toAddress(address)}`;
}
function isSameDid(a, b) {
return toAddress(a).toLowerCase() === toAddress(b).toLowerCase();
}
/**
* Extract the method from a DID string.
* @returns The method string, or null if no prefix found
*/
function extractMethod(did) {
if (typeof did !== "string" || !did) return null;
const match = did.match(/^did:([a-z0-9]+):/);
if (match) return match[1];
return null;
}
function formatTxType(type) {
return upperFirst(camelCase(type));
}
//#endregion
export { BN, UUID, bytesToHex, extractMethod, formatTxType, fromBase58, fromBase64, fromTokenToUnit, fromUnitToToken, hexToBytes, hexToNumber, hexToUtf8, isBN, isBase58btc, isBigNumber, isHex, isHexPrefixed, isHexStrict, isSameDid, isUUID, isUint8Array, leftPad, numberToBN, numberToHex, numberToString, rightPad, scopeMatch, scopeMatchAny, stripHexPrefix, toAddress, toBN, toBase58, toBase64, toBuffer, toDid, toHex, toUint8Array, utf8ToHex };