UNPKG

@ocap/util

Version:

utils shared across multiple forge js libs, works in both node.js and browser

450 lines (448 loc) 15.2 kB
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 };