diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..e358124 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,82 @@ +import js from "@eslint/js"; +import globals from "globals"; +import {defineConfig} from "eslint/config"; + +const jestGlobals = { + beforeAll: "readonly", + beforeEach: "readonly", + afterAll: "readonly", + afterEach: "readonly", + describe: "readonly", + expect: "readonly", + fit: "readonly", + it: "readonly", + jest: "readonly", + test: "readonly", + xdescribe: "readonly", + xit: "readonly", + xtest: "readonly" +}; + +export default defineConfig([ + { + ignores: [ + "dist/**", + "coverage/**", + ".npm-cache/**", + "test/browser/.npm-cache/**" + ] + }, + { + files: ["**/*.{js,mjs,cjs}"], + plugins: {js}, + extends: ["js/recommended"], + languageOptions: {globals: {...globals.browser, ...globals.node}}, + rules: { + indent: ["warn", 4, { SwitchCase: 1 }], + "no-const-assign": "error", + "no-constant-condition": ["error", { checkLoops: false }], + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-pattern": "error", + "no-ex-assign": "error", + "no-func-assign": "error", + "no-import-assign": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-new-native-nonconstructor": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "off", + "no-redeclare": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-undef": "warn", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-private-class-members": "warn", + "no-unused-vars": ["warn", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }], + "no-use-before-define": ["warn", { functions: false, classes: true, variables: true }], + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + } + }, + { + files: ["test/browser/**/*.js"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...jestGlobals, + fail: "readonly" + } + } + }, +]); diff --git a/index.js b/index.js index 02fa084..0c93ed1 100644 --- a/index.js +++ b/index.js @@ -29,16 +29,8 @@ const operation = require('./modules/operation.js'); const constants = require('./constants.js'); // ===== OPTIONAL (browser/RN) ===== -let b4a; -let sodium; - -try { - b4a = require('b4a'); -} catch {} - -try { - sodium = require('sodium-universal'); -} catch {} +const b4a = require('b4a'); +const sodium = require('sodium-universal'); // ===== BASE ===== const sign = signature.sign; @@ -63,8 +55,8 @@ const exported = { // ===== BROWSER / RN EXTENSIONS ===== if (isBrowser || isRN) { - if (b4a) exported.b4a = b4a; - if (sodium) exported.sodium = sodium; + exported.b4a = b4a; + exported.sodium = sodium; } -module.exports = exported; \ No newline at end of file +module.exports = exported; diff --git a/modules/address.js b/modules/address.js index 419abc6..b857f42 100644 --- a/modules/address.js +++ b/modules/address.js @@ -19,30 +19,31 @@ const DEFAULT_DERIVATION_PATH = MAINNET_DERIVATION_PATH; let SLIP10Node; function loadSLIP10Node() { - if (SLIP10Node) return SLIP10Node; - - try { - if (runtime.isBare && runtime.isBare()) { - // Use eval to hide from bundler (webpack react-native) - const req = eval("require"); - const keyTree = req("@metamask/key-tree", { - with: { imports: "../package.json" }, - }); - SLIP10Node = keyTree.SLIP10Node; - - // Polyfill - const util = req("util"); - globalThis.TextEncoder = util.TextEncoder; - globalThis.TextDecoder = util.TextDecoder; - return SLIP10Node; + if (SLIP10Node) return SLIP10Node; + + try { + if (runtime.isBare && runtime.isBare()) { + // Use eval to hide from bundler (webpack react-native) + const req = eval("require"); + const keyTree = req("@metamask/key-tree", { + with: { imports: "../package.json" }, + }); + SLIP10Node = keyTree.SLIP10Node; + + // Polyfill + const util = req("util"); + globalThis.TextEncoder = util.TextEncoder; + globalThis.TextDecoder = util.TextDecoder; + return SLIP10Node; + } + + // Browser / RN / Node + SLIP10Node = require("@metamask/key-tree").SLIP10Node; + return SLIP10Node; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`[trac-crypto-api] Failed to load @metamask/key-tree: ${message}`, { cause: err }); } - - // Browser / RN / Node - SLIP10Node = require("@metamask/key-tree").SLIP10Node; - return SLIP10Node; - } catch (err) { - throw new Error(`[trac-crypto-api] Failed to load @metamask/key-tree: ${err.message}`); - } } /** @@ -51,25 +52,25 @@ function loadSLIP10Node() { * @returns {boolean} True if the HRP is valid, false otherwise. */ function _isValidHrp(hrp) { - // HRP must be a non-empty string with length between 1 and HRP_SIZE_LIMIT characters - if (typeof hrp !== 'string' || hrp.length < 1 || hrp.length > HRP_SIZE_LIMIT) { - return false; - } - // HRP must consist of printable lower-case ASCII characters (33-126) - for (let i = 0; i < hrp.length; i++) { - const charCode = hrp.charCodeAt(i); - // Only allow lower-case letters a-z - if (charCode < 97 || charCode > 122) { - return false; + // HRP must be a non-empty string with length between 1 and HRP_SIZE_LIMIT characters + if (typeof hrp !== 'string' || hrp.length < 1 || hrp.length > HRP_SIZE_LIMIT) { + return false; + } + // HRP must consist of printable lower-case ASCII characters (33-126) + for (let i = 0; i < hrp.length; i++) { + const charCode = hrp.charCodeAt(i); + // Only allow lower-case letters a-z + if (charCode < 97 || charCode > 122) { + return false; + } } - } - return true; + return true; } function _validateHrp(hrp) { - if (!_isValidHrp(hrp)) { - throw new Error(`Invalid HRP. It must be a non-empty string with length between 1 and ${HRP_SIZE_LIMIT} characters, consisting of lowercase characters a-z.`); - } + if (!_isValidHrp(hrp)) { + throw new Error(`Invalid HRP. It must be a non-empty string with length between 1 and ${HRP_SIZE_LIMIT} characters, consisting of lowercase characters a-z.`); + } } /** @@ -80,38 +81,38 @@ function _validateHrp(hrp) { * @throws {TypeError|Error} If the path is invalid. */ function _sanitizeDerivationPath(path) { - // Validate input type - if (typeof path !== 'string') { - throw new TypeError('Derivation path must be a string'); - } - // Remove all spaces from the input path - path = path.replace(/\s+/g, ''); - - // Must start with 'm/' - // Observation: Although 'm' is not necessary for SLIP-10, - // enforce it for clarity and consistency with BIP32 - if (!path.startsWith('m/')) { - throw new Error("Derivation path must start with 'm/'"); - } - - // Disallow empty and invalid segments - const segments = path.split('/').map(seg => seg.trim()); - segments.shift(); // Remove initial 'm' and process segments separately - - if (segments.length < 1) { - throw new Error('Derivation path must have at least one child segment'); - } - - const segmentRegex = /^\d+'$/; - let slip10Segments = []; - for (const seg of segments) { - if (!segmentRegex.test(seg)) { - throw new Error(`Invalid segment: '${seg}'. Only hardened segments (e.g. 0') are supported.`); + // Validate input type + if (typeof path !== 'string') { + throw new TypeError('Derivation path must be a string'); + } + // Remove all spaces from the input path + path = path.replace(/\s+/g, ''); + + // Must start with 'm/' + // Observation: Although 'm' is not necessary for SLIP-10, + // enforce it for clarity and consistency with BIP32 + if (!path.startsWith('m/')) { + throw new Error("Derivation path must start with 'm/'"); } - slip10Segments.push(`slip10:${seg}`); - } - return { safePath: path, slip10Segments }; + // Disallow empty and invalid segments + const segments = path.split('/').map(seg => seg.trim()); + segments.shift(); // Remove initial 'm' and process segments separately + + if (segments.length < 1) { + throw new Error('Derivation path must have at least one child segment'); + } + + const segmentRegex = /^\d+'$/; + let slip10Segments = []; + for (const seg of segments) { + if (!segmentRegex.test(seg)) { + throw new Error(`Invalid segment: '${seg}'. Only hardened segments (e.g. 0') are supported.`); + } + slip10Segments.push(`slip10:${seg}`); + } + + return { safePath: path, slip10Segments }; } /** @@ -123,55 +124,55 @@ function _sanitizeDerivationPath(path) { * @returns {Promise<{publicKey: Buffer, secretKey: Buffer, mnemonic: string}>} Resolves to an object containing the public key, secret key, and mnemonic used. */ async function _generateKeyPair(masterPathSegments, mnemonic = null, path = null) { - const node = loadSLIP10Node(); - - let safeMnemonic; - if (mnemonic === null) { - safeMnemonic = mnemonicUtils.generate(); - } else { - safeMnemonic = mnemonicUtils.sanitize(mnemonic); // Will throw if the mnemonic is invalid - } - - if (path === null) { - path = DEFAULT_DERIVATION_PATH; - } - - let masterPath = [`bip39:${safeMnemonic}`]; - for (let i = 0; i < masterPathSegments.length; i++) { - masterPath.push(`slip10:${masterPathSegments[i]}'`); - } - - const masterNode = await node.fromDerivationPath({ - curve: 'ed25519', - derivationPath: masterPath, - }); - - const { safePath, slip10Segments } = _sanitizeDerivationPath(path); - const childNode = await masterNode.derive(slip10Segments); - - // Observation: - // libsodium uses a 64-byte secret key (32-byte seed + 32-byte public key) - // @metamask/key-tree, in turn, uses a 32-byte private key - // In order to keep consistency between key derivation and signing, we concatenate the private key with the public key here. - // More info here: - // https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures#generating-a-new-signing-key-pair - const publicKey = b4a.from(childNode.publicKeyBytes.subarray(1)); // Remove compressed public key prefix byte (always 0x00) - const secretKey = b4a.concat([b4a.from(childNode.privateKeyBytes), publicKey]); - - // Sanity checks. Maybe not necessary, but better safe than sorry. - if (publicKey.length !== TRAC_PUB_KEY_SIZE) { - throw new Error(`Derived public key has invalid length. Expected ${TRAC_PUB_KEY_SIZE}, got ${publicKey.length}`); - } - if (secretKey.length !== TRAC_PRIV_KEY_SIZE) { - throw new Error(`Derived secret key has invalid length. Expected ${TRAC_PRIV_KEY_SIZE}, got ${secretKey.length}`); - } - - return { - publicKey, - secretKey, - mnemonic: safeMnemonic, - derivationPath: safePath, - }; + const node = loadSLIP10Node(); + + let safeMnemonic; + if (mnemonic === null) { + safeMnemonic = mnemonicUtils.generate(); + } else { + safeMnemonic = mnemonicUtils.sanitize(mnemonic); // Will throw if the mnemonic is invalid + } + + if (path === null) { + path = DEFAULT_DERIVATION_PATH; + } + + let masterPath = [`bip39:${safeMnemonic}`]; + for (let i = 0; i < masterPathSegments.length; i++) { + masterPath.push(`slip10:${masterPathSegments[i]}'`); + } + + const masterNode = await node.fromDerivationPath({ + curve: 'ed25519', + derivationPath: masterPath, + }); + + const { safePath, slip10Segments } = _sanitizeDerivationPath(path); + const childNode = await masterNode.derive(slip10Segments); + + // Observation: + // libsodium uses a 64-byte secret key (32-byte seed + 32-byte public key) + // @metamask/key-tree, in turn, uses a 32-byte private key + // In order to keep consistency between key derivation and signing, we concatenate the private key with the public key here. + // More info here: + // https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures#generating-a-new-signing-key-pair + const publicKey = b4a.from(childNode.publicKeyBytes.subarray(1)); // Remove compressed public key prefix byte (always 0x00) + const secretKey = b4a.concat([b4a.from(childNode.privateKeyBytes), publicKey]); + + // Sanity checks. Maybe not necessary, but better safe than sorry. + if (publicKey.length !== TRAC_PUB_KEY_SIZE) { + throw new Error(`Derived public key has invalid length. Expected ${TRAC_PUB_KEY_SIZE}, got ${publicKey.length}`); + } + if (secretKey.length !== TRAC_PRIV_KEY_SIZE) { + throw new Error(`Derived secret key has invalid length. Expected ${TRAC_PRIV_KEY_SIZE}, got ${secretKey.length}`); + } + + return { + publicKey, + secretKey, + mnemonic: safeMnemonic, + derivationPath: safePath, + }; } /** @@ -183,23 +184,23 @@ async function _generateKeyPair(masterPathSegments, mnemonic = null, path = null * @returns {boolean} True if the address is valid, false otherwise. */ function isValid(address) { - const _separateHrp = (address) => { - let ret = { prefix: null, suffix: null }; - if (typeof address === 'string') { - const separatorIndex = address.indexOf('1'); - if (separatorIndex > -1) { - ret.prefix = address.slice(0, separatorIndex); - ret.suffix = address.slice(separatorIndex + 1); - } - } - return ret; - }; - - const bech32Chars = /^[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$/; - const { prefix, suffix } = _separateHrp(address); - const suffixLength = Math.ceil((TRAC_PUB_KEY_SIZE * 8) / 5) + 6; // Data part + checksum - - return typeof prefix === 'string' && + const _separateHrp = (address) => { + let ret = { prefix: null, suffix: null }; + if (typeof address === 'string') { + const separatorIndex = address.indexOf('1'); + if (separatorIndex > -1) { + ret.prefix = address.slice(0, separatorIndex); + ret.suffix = address.slice(separatorIndex + 1); + } + } + return ret; + }; + + const bech32Chars = /^[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$/; + const { prefix, suffix } = _separateHrp(address); + const suffixLength = Math.ceil((TRAC_PUB_KEY_SIZE * 8) / 5) + 6; // Data part + checksum + + return typeof prefix === 'string' && typeof suffix === 'string' && _isValidHrp(prefix) && bech32Chars.test(suffix) && @@ -212,18 +213,18 @@ function isValid(address) { * @returns {Buffer} The buffer representation of the address. */ function toBuffer(address) { - if (!isValid(address)) { - throw new Error('Invalid address'); - } - return b4a.from(address, 'ascii'); + if (!isValid(address)) { + throw new Error('Invalid address'); + } + return b4a.from(address, 'ascii'); } function fromBuffer(buffer) { - if (!b4a.isBuffer(buffer)) { - throw new Error('Invalid input: buffer must be a Buffer'); - } + if (!b4a.isBuffer(buffer)) { + throw new Error('Invalid input: buffer must be a Buffer'); + } - return b4a.toString(buffer, 'ascii'); + return b4a.toString(buffer, 'ascii'); } /** @@ -234,14 +235,14 @@ function fromBuffer(buffer) { * @throws {Error} If the publicKey is not a Buffer or has incorrect length. */ function encode(hrp, publicKey) { - _validateHrp(hrp); - if (!b4a.isBuffer(publicKey) || publicKey.length !== TRAC_PUB_KEY_SIZE) { - throw new Error( - `Invalid public key. Expected a Buffer of length ${TRAC_PUB_KEY_SIZE}, got ${publicKey.length}` - ); - } - const words = bech32m.toWords(publicKey); - return bech32m.encode(hrp, words); + _validateHrp(hrp); + if (!b4a.isBuffer(publicKey) || publicKey.length !== TRAC_PUB_KEY_SIZE) { + throw new Error( + `Invalid public key. Expected a Buffer of length ${TRAC_PUB_KEY_SIZE}, got ${publicKey.length}` + ); + } + const words = bech32m.toWords(publicKey); + return bech32m.encode(hrp, words); } /** @@ -251,14 +252,14 @@ function encode(hrp, publicKey) { * @throws {Error} If the decoded buffer has incorrect length. */ function decode(address) { - const { words } = bech32m.decode(address); - const buffer = b4a.from(bech32m.fromWords(words)); - if (buffer.length !== TRAC_PUB_KEY_SIZE) { - throw new Error( - `Decoded buffer is invalid. Expected ${TRAC_PUB_KEY_SIZE} bytes, got ${buffer.length} bytes` - ); - } - return buffer; + const { words } = bech32m.decode(address); + const buffer = b4a.from(bech32m.fromWords(words)); + if (buffer.length !== TRAC_PUB_KEY_SIZE) { + throw new Error( + `Decoded buffer is invalid. Expected ${TRAC_PUB_KEY_SIZE} bytes, got ${buffer.length} bytes` + ); + } + return buffer; } /** @@ -267,11 +268,11 @@ function decode(address) { * @returns {Buffer|null} The decoded public key buffer, or null if decoding fails. */ function decodeSafe(address) { - try { - return decode(address); - } catch (err) { - return NULL_BUFFER; - } + try { + return decode(address); + } catch { + return NULL_BUFFER; + } } /** @@ -280,12 +281,12 @@ function decodeSafe(address) { * @returns {boolean} True if the address can be decoded, false otherwise. */ function canDecode(address) { - try { - decode(address); - return true; - } catch (err) { - return false; - } + try { + decode(address); + return true; + } catch { + return false; + } } @@ -298,17 +299,17 @@ function canDecode(address) { * @returns {Promise<{address: string, publicKey: Buffer, secretKey: Buffer, mnemonic: string, derivationPath: string}>} Resolves to an object containing the address, public key, secret key, mnemonic and derivationPath used. */ async function generate(hrp, mnemonic = null, derivationPath = null) { - _validateHrp(hrp); - const masterPathSegments = b4a.from(hrp, 'utf8'); // The master path segments used in address generation are derived from the HRP - const keypair = await _generateKeyPair(masterPathSegments, mnemonic, derivationPath); - const address = encode(hrp, keypair.publicKey); - return { - address, - publicKey: keypair.publicKey, - secretKey: keypair.secretKey, - mnemonic: keypair.mnemonic, - derivationPath: keypair.derivationPath, - }; + _validateHrp(hrp); + const masterPathSegments = b4a.from(hrp, 'utf8'); // The master path segments used in address generation are derived from the HRP + const keypair = await _generateKeyPair(masterPathSegments, mnemonic, derivationPath); + const address = encode(hrp, keypair.publicKey); + return { + address, + publicKey: keypair.publicKey, + secretKey: keypair.secretKey, + mnemonic: keypair.mnemonic, + derivationPath: keypair.derivationPath, + }; } /** @@ -319,47 +320,47 @@ async function generate(hrp, mnemonic = null, derivationPath = null) { * @throws {Error} If the secretKey is not a Buffer or has incorrect length. */ function fromSecretKey(hrp, secretKey) { - if (!b4a.isBuffer(secretKey) || secretKey.length !== TRAC_PRIV_KEY_SIZE) { - throw new Error( - `Invalid secret key. Expected a Buffer of length ${TRAC_PRIV_KEY_SIZE}, got ${secretKey.length}` - ); - } - const publicKey = secretKey.subarray(32); // The public key is the last 32 bytes of the 64-byte secret key - const address = encode(hrp, publicKey); - return { - address, - publicKey, - secretKey, - }; + if (!b4a.isBuffer(secretKey) || secretKey.length !== TRAC_PRIV_KEY_SIZE) { + throw new Error( + `Invalid secret key. Expected a Buffer of length ${TRAC_PRIV_KEY_SIZE}, got ${secretKey.length}` + ); + } + const publicKey = secretKey.subarray(32); // The public key is the last 32 bytes of the 64-byte secret key + const address = encode(hrp, publicKey); + return { + address, + publicKey, + secretKey, + }; } function size(hrp) { - if (!_isValidHrp(hrp)) { - throw new Error('Invalid HRP. It must be a non-empty string with length between 1 and 83 characters, consisting of printable ASCII characters.'); - } - const hrpSize = hrp.length; - const separatorSize = 1; // The '1' character separating HRP and data part - // Each byte is represented by 8 bits, and bech32m encodes 5 bits per character - const dataPartSize = Math.ceil((TRAC_PUB_KEY_SIZE * 8) / 5); - const checksumSize = 6; // Bech32m checksum is always 6 characters - - return hrpSize + separatorSize + dataPartSize + checksumSize; + if (!_isValidHrp(hrp)) { + throw new Error('Invalid HRP. It must be a non-empty string with length between 1 and 83 characters, consisting of printable ASCII characters.'); + } + const hrpSize = hrp.length; + const separatorSize = 1; // The '1' character separating HRP and data part + // Each byte is represented by 8 bits, and bech32m encodes 5 bits per character + const dataPartSize = Math.ceil((TRAC_PUB_KEY_SIZE * 8) / 5); + const checksumSize = 6; // Bech32m checksum is always 6 characters + + return hrpSize + separatorSize + dataPartSize + checksumSize; } module.exports = { - generate, - encode, - decode, - decodeSafe, - canDecode, - isValid, - size, - toBuffer, - fromBuffer, - fromSecretKey, - PUB_KEY_SIZE: TRAC_PUB_KEY_SIZE, - PRIV_KEY_SIZE: TRAC_PRIV_KEY_SIZE, - MAINNET_DERIVATION_PATH, - TESNET_DERIVATION_PATH, - DEFAULT_DERIVATION_PATH -}; \ No newline at end of file + generate, + encode, + decode, + decodeSafe, + canDecode, + isValid, + size, + toBuffer, + fromBuffer, + fromSecretKey, + PUB_KEY_SIZE: TRAC_PUB_KEY_SIZE, + PRIV_KEY_SIZE: TRAC_PRIV_KEY_SIZE, + MAINNET_DERIVATION_PATH, + TESNET_DERIVATION_PATH, + DEFAULT_DERIVATION_PATH +}; diff --git a/package-lock.json b/package-lock.json index 14bed41..b7ed25d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/preset-env": "^7.28.3", + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", @@ -34,6 +35,8 @@ "baseline-browser-mapping": "^2.10.20", "brittle": "^3.18.0", "c8": "^11.0.0", + "eslint": "^10.2.0", + "globals": "^17.5.0", "ignore-loader": "^0.1.2", "rollup": "^4.50.2", "webpack": "^5.102.1", @@ -1593,6 +1596,134 @@ "node": ">=14.17.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@ethereumjs/common": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", @@ -1644,6 +1775,58 @@ "node": ">=14" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2387,6 +2570,13 @@ "@types/estree": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2660,9 +2850,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, @@ -2686,6 +2876,16 @@ "acorn": "^8.14.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -3711,6 +3911,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3789,6 +3996,76 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -3803,6 +4080,113 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -3921,6 +4305,20 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -3966,6 +4364,19 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3993,6 +4404,27 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4073,6 +4505,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -4080,6 +4525,19 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globbie": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz", @@ -4130,6 +4588,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-loader": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", @@ -4156,6 +4624,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -4182,6 +4660,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4192,6 +4680,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -4326,6 +4827,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4340,6 +4848,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4353,6 +4868,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4363,6 +4888,20 @@ "node": ">=0.10.0" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -4529,6 +5068,13 @@ "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", "license": "ISC" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -4543,6 +5089,24 @@ "dev": true, "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4745,6 +5309,26 @@ "node": ">=12.0.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5368,6 +5952,19 @@ "node": ">=8" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -5450,6 +6047,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -5650,6 +6257,16 @@ "dev": true, "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 12a5af5..1a5fae3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "coverage": "c8 --all --include=constants.js --include=index.js --include=modules/**/*.js --exclude=test/** --exclude=dist/** --exclude=scripts/** --reporter=text --reporter=json-summary --reporter=lcov --reporter=html npm run test:node", "ci:clean": "node scripts/ci/clean.cjs && npm ci --cache ./.npm-cache", "ci:full": "npm run ci:clean && npm run test:node && npm run test:bare && npm run build:browser && npm run test:browser && npm run build:rn", - "pack:test": "npm pack --pack-destination test/browser" + "pack:test": "npm pack --pack-destination test/browser", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "keywords": [], "author": "", @@ -75,6 +77,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/preset-env": "^7.28.3", + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", @@ -83,6 +86,8 @@ "baseline-browser-mapping": "^2.10.20", "brittle": "^3.18.0", "c8": "^11.0.0", + "eslint": "^10.2.0", + "globals": "^17.5.0", "ignore-loader": "^0.1.2", "rollup": "^4.50.2", "webpack": "^5.102.1", diff --git a/rollup.config.js b/rollup.config.js index a256392..a463b5a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,38 +4,38 @@ import json from "@rollup/plugin-json"; import replace from "@rollup/plugin-replace"; export default { - input: "index.js", // entrypoint - output: [ - { - file: "dist/trac-crypto-api.browser.js", - format: "umd", - name: "TracCryptoApi", - sourcemap: true, + input: "index.js", // entrypoint + output: [ + { + file: "dist/trac-crypto-api.browser.js", + format: "umd", + name: "TracCryptoApi", + sourcemap: true, + }, + { + file: "dist/trac-crypto-api.browser.esm.js", + format: "es", + sourcemap: true, + }, + ], + plugins: [ + json(), + resolve({ + browser: true, + preferBuiltins: false, + }), + commonjs(), + replace({ + preventAssignment: true, + values: { + __filename: JSON.stringify(""), + __dirname: JSON.stringify(""), + }, + }), + ], + // evita puxar sodium-native + onwarn(warning, warn) { + if (warning.code === "THIS_IS_UNDEFINED") return; + warn(warning); }, - { - file: "dist/trac-crypto-api.browser.esm.js", - format: "es", - sourcemap: true, - }, - ], - plugins: [ - json(), - resolve({ - browser: true, - preferBuiltins: false, - }), - commonjs(), - replace({ - preventAssignment: true, - values: { - __filename: JSON.stringify(""), - __dirname: JSON.stringify(""), - }, - }), - ], - // evita puxar sodium-native - onwarn(warning, warn) { - if (warning.code === "THIS_IS_UNDEFINED") return; - warn(warning); - }, }; diff --git a/scripts/ci/clean.cjs b/scripts/ci/clean.cjs index 9e3e932..667642c 100644 --- a/scripts/ci/clean.cjs +++ b/scripts/ci/clean.cjs @@ -1,13 +1,13 @@ const { rmSync } = require('node:fs'); const paths = [ - 'node_modules', - 'dist', - '.npm-cache', - 'test/browser/node_modules', - 'test/browser/.npm-cache' + 'node_modules', + 'dist', + '.npm-cache', + 'test/browser/node_modules', + 'test/browser/.npm-cache' ]; for (const path of paths) { - rmSync(path, { recursive: true, force: true }); + rmSync(path, { recursive: true, force: true }); } diff --git a/scripts/ci/comment-coverage.cjs b/scripts/ci/comment-coverage.cjs index 9dd39cb..e8ed486 100644 --- a/scripts/ci/comment-coverage.cjs +++ b/scripts/ci/comment-coverage.cjs @@ -1,45 +1,45 @@ module.exports = async function commentCoverage({ github, context }) { - const marker = ''; - const legacyMarker = ''; - const body = [ - marker, - '## Coverage', - '', - '| Metric | Coverage | Covered/Total |', - '| --- | ---: | ---: |', - `| Lines | ${process.env.LINES_PCT}% | ${process.env.LINES_COV}/${process.env.LINES_TOTAL} |`, - `| Statements | ${process.env.STATEMENTS_PCT}% | ${process.env.STATEMENTS_COV}/${process.env.STATEMENTS_TOTAL} |`, - `| Functions | ${process.env.FUNCTIONS_PCT}% | ${process.env.FUNCTIONS_COV}/${process.env.FUNCTIONS_TOTAL} |`, - `| Branches | ${process.env.BRANCHES_PCT}% | ${process.env.BRANCHES_COV}/${process.env.BRANCHES_TOTAL} |` - ].join('\n'); + const marker = ''; + const legacyMarker = ''; + const body = [ + marker, + '## Coverage', + '', + '| Metric | Coverage | Covered/Total |', + '| --- | ---: | ---: |', + `| Lines | ${process.env.LINES_PCT}% | ${process.env.LINES_COV}/${process.env.LINES_TOTAL} |`, + `| Statements | ${process.env.STATEMENTS_PCT}% | ${process.env.STATEMENTS_COV}/${process.env.STATEMENTS_TOTAL} |`, + `| Functions | ${process.env.FUNCTIONS_PCT}% | ${process.env.FUNCTIONS_COV}/${process.env.FUNCTIONS_TOTAL} |`, + `| Branches | ${process.env.BRANCHES_PCT}% | ${process.env.BRANCHES_COV}/${process.env.BRANCHES_TOTAL} |` + ].join('\n'); - const { owner, repo } = context.repo; - const issueNumber = context.payload.pull_request.number; - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: issueNumber, - per_page: 100 - }); - const existing = comments.find((comment) => - comment.user.type === 'Bot' && + const { owner, repo } = context.repo; + const issueNumber = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issueNumber, + per_page: 100 + }); + const existing = comments.find((comment) => + comment.user.type === 'Bot' && (comment.body.includes(marker) || comment.body.includes(legacyMarker)) - ); + ); - if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body - }); - return; - } + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body + }); + return; + } - await github.rest.issues.createComment({ - owner, - repo, - issue_number: issueNumber, - body - }); + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body + }); }; diff --git a/scripts/ci/export-coverage-totals.mjs b/scripts/ci/export-coverage-totals.mjs index dde6c6a..9576eed 100644 --- a/scripts/ci/export-coverage-totals.mjs +++ b/scripts/ci/export-coverage-totals.mjs @@ -3,27 +3,27 @@ import { readFileSync, appendFileSync } from 'node:fs'; const output = process.env.GITHUB_OUTPUT; if (!output) { - throw new Error('GITHUB_OUTPUT is not set'); + throw new Error('GITHUB_OUTPUT is not set'); } const summaryPath = process.argv[2] ?? 'coverage/coverage-summary.json'; const summary = JSON.parse(readFileSync(summaryPath, 'utf8')).total; const values = { - lines_pct: summary.lines.pct, - lines_cov: summary.lines.covered, - lines_total: summary.lines.total, - statements_pct: summary.statements.pct, - statements_cov: summary.statements.covered, - statements_total: summary.statements.total, - functions_pct: summary.functions.pct, - functions_cov: summary.functions.covered, - functions_total: summary.functions.total, - branches_pct: summary.branches.pct, - branches_cov: summary.branches.covered, - branches_total: summary.branches.total + lines_pct: summary.lines.pct, + lines_cov: summary.lines.covered, + lines_total: summary.lines.total, + statements_pct: summary.statements.pct, + statements_cov: summary.statements.covered, + statements_total: summary.statements.total, + functions_pct: summary.functions.pct, + functions_cov: summary.functions.covered, + functions_total: summary.functions.total, + branches_pct: summary.branches.pct, + branches_cov: summary.branches.covered, + branches_total: summary.branches.total }; for (const [key, value] of Object.entries(values)) { - appendFileSync(output, `${key}=${value}\n`); + appendFileSync(output, `${key}=${value}\n`); } diff --git a/scripts/ci/setup-browser.cjs b/scripts/ci/setup-browser.cjs index f70c32d..c2066e9 100644 --- a/scripts/ci/setup-browser.cjs +++ b/scripts/ci/setup-browser.cjs @@ -6,15 +6,15 @@ const browserDir = path.join(__dirname, "../../test/browser"); // clean rmSync(path.join(browserDir, "node_modules/trac-crypto-api"), { - recursive: true, - force: true, + recursive: true, + force: true, }); // remove old tgz for (const file of readdirSync(browserDir)) { - if (file.endsWith(".tgz")) { - rmSync(path.join(browserDir, file), { force: true }); - } + if (file.endsWith(".tgz")) { + rmSync(path.join(browserDir, file), { force: true }); + } } // pack @@ -24,11 +24,11 @@ execSync("npm pack --pack-destination test/browser", { stdio: "inherit" }); const tgz = readdirSync(browserDir).find((f) => f.endsWith(".tgz")); if (!tgz) { - throw new Error("No .tgz found"); + throw new Error("No .tgz found"); } // install execSync(`npm install ./${tgz}`, { - cwd: browserDir, - stdio: "inherit", + cwd: browserDir, + stdio: "inherit", }); diff --git a/test/browser/address/address.test.js b/test/browser/address/address.test.js index 01b86d1..91fb485 100644 --- a/test/browser/address/address.test.js +++ b/test/browser/address/address.test.js @@ -30,7 +30,7 @@ test("address.encode: should throw on invalid public key", () => { "not a buffer", b4a.alloc(10), // Too short b4a.alloc(50) // Too long -]; + ]; for (const key of invalidKeys) { expect(() => api.address.encode(HRP, key)).toThrow(); @@ -199,7 +199,7 @@ test("address.generate: should return an error for invalid mnemonic", async () = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about extra', ]; - for (const mnemonic of invalidMnemonics) { + for (const mnemonic of invalidMnemonics) { await expect(api.address.generate(HRP, mnemonic)).rejects.toThrow(); } }); @@ -211,7 +211,7 @@ test("address.generate: should accept valid hrp", async () => { 'a'.repeat(31) ]; - for (const hrp of validHrps) { + for (const hrp of validHrps) { const result = await api.address.generate(hrp); expect(typeof result.address).toBe("string"); expect(b4a.isBuffer(result.publicKey)).toBe(true); diff --git a/test/browser/jest.config.js b/test/browser/jest.config.js index 8b98496..bd56873 100644 --- a/test/browser/jest.config.js +++ b/test/browser/jest.config.js @@ -1,10 +1,10 @@ export default { - testEnvironment: 'jsdom', - setupFiles: ['./polyfills.js'], - setupFilesAfterEnv: ['./setupTests.js'], - testMatch: ['**/*.test.js'], + testEnvironment: 'jsdom', + setupFiles: ['./polyfills.js'], + setupFilesAfterEnv: ['./setupTests.js'], + testMatch: ['**/*.test.js'], - moduleNameMapper: { - 'b4a': 'b4a/browser.js' - } + moduleNameMapper: { + 'b4a': 'b4a/browser.js' + } }; \ No newline at end of file diff --git a/test/browser/mnemonic/mnemonic.test.js b/test/browser/mnemonic/mnemonic.test.js index 0fd2fa0..5511c5d 100644 --- a/test/browser/mnemonic/mnemonic.test.js +++ b/test/browser/mnemonic/mnemonic.test.js @@ -1,3 +1,4 @@ +const b4a = require('b4a'); const api = window.TracCryptoApi; const mnemonic = api.mnemonic; @@ -78,4 +79,4 @@ test('mnemonic.toSeed: should convert mnemonic to seed', async () => { test('mnemonic.toSeed: should throw on invalid mnemonic', async () => { await expect(mnemonic.toSeed('invalid mnemonic phrase')).rejects.toThrow(); -}); \ No newline at end of file +}); diff --git a/test/browser/operation/operation.test.js b/test/browser/operation/operation.test.js index ac53370..ff6fd61 100644 --- a/test/browser/operation/operation.test.js +++ b/test/browser/operation/operation.test.js @@ -96,7 +96,9 @@ test('operation.build: should produce valid payload', async () => { try { // NOTE: payload is not guaranteed to be JSON in browser build parsed = JSON.parse(decoded.toString('utf-8')); - } catch (_) {} + } catch { + // Ignore parse failures: browser payloads are not guaranteed to be JSON. + } if (parsed && parsed.txo) { expect(parsed.type).toBe(OP_TYPE_TX); @@ -128,4 +130,4 @@ test('operation.build: should produce valid payload', async () => { expect(isValid).toBe(true); } -}); \ No newline at end of file +}); diff --git a/test/browser/package-lock.json b/test/browser/package-lock.json index 3f9b5c7..a335ba6 100644 --- a/test/browser/package-lock.json +++ b/test/browser/package-lock.json @@ -5275,7 +5275,7 @@ "node_modules/trac-crypto-api": { "version": "0.1.5", "resolved": "file:trac-crypto-api-0.1.5.tgz", - "integrity": "sha512-wnymgvqSHO6cr0qUsnmg+Py5yp/iH4MH0WkH4SJZFQYsy+nGo025HtfSJMjpYvg7ZYwiPKzInDlPWrUXllYO4A==", + "integrity": "sha512-tQjEgn8jDDO/y1rucaxrbLa1F6sMiQT/l8NKg57pHuII3UtkZBavKxnbh0URSS7fWccyUE2gb7P8Gb5KINgiKg==", "license": "ISC", "dependencies": { "@metamask/key-tree": "10.1.1", diff --git a/test/browser/setupTests.js b/test/browser/setupTests.js index dd756b5..88a022e 100644 --- a/test/browser/setupTests.js +++ b/test/browser/setupTests.js @@ -7,9 +7,7 @@ window.sodium = sodium; const TracCryptoApi = require('trac-crypto-api'); window.TracCryptoApi = TracCryptoApi; - -const original = global.process; Object.defineProperty(global, 'process', { - value: undefined, - configurable: true + value: undefined, + configurable: true }); diff --git a/test/unit/address/address.test.js b/test/unit/address/address.test.js index 9932812..e1163c6 100644 --- a/test/unit/address/address.test.js +++ b/test/unit/address/address.test.js @@ -1,12 +1,12 @@ const test = require('brittle'); function runTests() { - test.pause(); + test.pause(); - require('./encoding.test.js'); - require('./generate.test.js'); + require('./encoding.test.js'); + require('./generate.test.js'); - test.resume(); + test.resume(); } runTests(); \ No newline at end of file diff --git a/test/unit/address/encoding.test.js b/test/unit/address/encoding.test.js index dc085dc..9cc3751 100644 --- a/test/unit/address/encoding.test.js +++ b/test/unit/address/encoding.test.js @@ -1,120 +1,119 @@ const test = require("brittle"); const api = require("../../../index.js"); -const { TRAC_PUB_KEY_SIZE, TRAC_PRIV_KEY_SIZE } = require("../../../constants.js"); const b4a = require("b4a"); const HRP = "trac"; test("address.encode/decode: should encode and decode public key correctly", async (t) => { - const { publicKey } = await api.address.generate(HRP); - const encoded = api.address.encode(HRP, publicKey); - const decoded = api.address.decode(encoded); - t.ok(decoded.equals(publicKey)); + const {publicKey} = await api.address.generate(HRP); + const encoded = api.address.encode(HRP, publicKey); + const decoded = api.address.decode(encoded); + t.ok(decoded.equals(publicKey)); }); test("address.encode: should throw on invalid public key", (t) => { - const invalidKeys = [ - null, - undefined, - "not a buffer", - b4a.alloc(10), // Too short - b4a.alloc(50) // Too long - ]; - for (const key of invalidKeys) { - try { - api.address.encode(HRP, key); - t.fail(`Did not throw for key: ${key}`); - } catch (e) { - t.pass(); + const invalidKeys = [ + null, + undefined, + "not a buffer", + b4a.alloc(10), // Too short + b4a.alloc(50) // Too long + ]; + for (const key of invalidKeys) { + try { + api.address.encode(HRP, key); + t.fail(`Did not throw for key: ${key}`); + } catch { + t.pass(); + } } - } }); test("address.encode: should throw on invalid hrp", async (t) => { - const invalidHrps = [ - null, - undefined, - "", - "ThisHRPIsWayTooLongToBeValid", - "invalid*char" - ]; - for (const hrp of invalidHrps) { - try { - const { publicKey } = await api.address.generate("trac"); - api.address.encode(hrp, publicKey); - t.fail(`Did not throw for HRP: ${hrp}`); - } catch (e) { - t.pass(); + const invalidHrps = [ + null, + undefined, + "", + "ThisHRPIsWayTooLongToBeValid", + "invalid*char" + ]; + for (const hrp of invalidHrps) { + try { + const {publicKey} = await api.address.generate("trac"); + api.address.encode(hrp, publicKey); + t.fail(`Did not throw for HRP: ${hrp}`); + } catch { + t.pass(); + } } - } }); test("address.decode: should throw on invalid address", (t) => { - const invalidAddresses = [ - null, - undefined, - "not a buffer", - b4a.alloc(10), // Too short - b4a.alloc(50) // Too long - ]; - for (const address of invalidAddresses) { - try { - api.address.decode(address); - t.fail(`Did not throw for address: ${address}`); - } catch (e) { - t.pass(); + const invalidAddresses = [ + null, + undefined, + "not a buffer", + b4a.alloc(10), // Too short + b4a.alloc(50) // Too long + ]; + for (const address of invalidAddresses) { + try { + api.address.decode(address); + t.fail(`Did not throw for address: ${address}`); + } catch { + t.pass(); + } } - } }); test("address.toBuffer/fromBuffer: should convert address to and from buffer correctly", async (t) => { - const { address } = await api.address.generate(HRP); - const buf = api.address.toBuffer(address); - const addrFromBuf = api.address.fromBuffer(buf); - t.ok(b4a.isBuffer(buf)); - t.is(addrFromBuf, address); - t.ok(api.address.isValid(addrFromBuf)); + const {address} = await api.address.generate(HRP); + const buf = api.address.toBuffer(address); + const addrFromBuf = api.address.fromBuffer(buf); + t.ok(b4a.isBuffer(buf)); + t.is(addrFromBuf, address); + t.ok(api.address.isValid(addrFromBuf)); }); test("address.toBuffer: should throw on invalid address", (t) => { - const invalidAddresses = [ - null, - undefined, - "", - "not a valid address", - "trac1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", // Too short - "trac1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzo" // Invalid char - ]; - for (const address of invalidAddresses) { - try { - api.address.toBuffer(address); - t.fail(`Did not throw for address: ${address}`); - } catch (e) { - t.pass(); + const invalidAddresses = [ + null, + undefined, + "", + "not a valid address", + "trac1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", // Too short + "trac1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzo" // Invalid char + ]; + for (const address of invalidAddresses) { + try { + api.address.toBuffer(address); + t.fail(`Did not throw for address: ${address}`); + } catch { + t.pass(); + } } - } }); test("address.fromBuffer: should throw on invalid buffer", (t) => { - const invalidBuffers = [ - null, - undefined, - "not a buffer", - b4a.alloc(10), // Too short - b4a.alloc(50) // Too long - ]; - for (const buf of invalidBuffers) { - try { - api.address.fromBuffer(HRP, buf); - t.fail(`Did not throw for buffer: ${buf}`); - } catch (e) { - t.pass(); + const invalidBuffers = [ + null, + undefined, + "not a buffer", + b4a.alloc(10), // Too short + b4a.alloc(50) // Too long + ]; + for (const buf of invalidBuffers) { + try { + api.address.fromBuffer(HRP, buf); + t.fail(`Did not throw for buffer: ${buf}`); + } catch { + t.pass(); + } } - } }); test("address.size: should return correct address size", async (t) => { - const { address } = await api.address.generate(HRP); - const size = api.address.size(HRP); - t.is(size, address.length); + const {address} = await api.address.generate(HRP); + const size = api.address.size(HRP); + t.is(size, address.length); }); \ No newline at end of file diff --git a/test/unit/address/generate.test.js b/test/unit/address/generate.test.js index 6ca3c22..d46a0bd 100644 --- a/test/unit/address/generate.test.js +++ b/test/unit/address/generate.test.js @@ -13,240 +13,240 @@ const mnemonic12Words = "abandon abandon abandon abandon abandon abandon abandon const mnemonic24Words = api.mnemonic.generate(); test("address.generate: should generate a valid address and keypair with no mnemonic input", async (t) => { - const result = await api.address.generate(HRP); - t.is(typeof result.address, "string"); - t.ok(b4a.isBuffer(result.publicKey)); - t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); - t.ok(b4a.isBuffer(result.secretKey)); - t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); - t.is(typeof result.mnemonic, "string"); - t.is(typeof result.derivationPath, "string"); - t.is(result.derivationPath, DEFAULT_PATH); + const result = await api.address.generate(HRP); + t.is(typeof result.address, "string"); + t.ok(b4a.isBuffer(result.publicKey)); + t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); + t.ok(b4a.isBuffer(result.secretKey)); + t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); + t.is(typeof result.mnemonic, "string"); + t.is(typeof result.derivationPath, "string"); + t.is(result.derivationPath, DEFAULT_PATH); }); test("address.generate: should generate keypair for 12 words mnemonic", async (t) => { - const mnemonic = mnemonic12Words; - const result = await api.address.generate(HRP, mnemonic, PATH1); - t.ok(b4a.isBuffer(result.publicKey)); - t.ok(b4a.isBuffer(result.secretKey)); - t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); - t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); - t.is(result.mnemonic, mnemonic); - t.ok(api.address.isValid(result.address)); - t.is(result.derivationPath, PATH1); + const mnemonic = mnemonic12Words; + const result = await api.address.generate(HRP, mnemonic, PATH1); + t.ok(b4a.isBuffer(result.publicKey)); + t.ok(b4a.isBuffer(result.secretKey)); + t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); + t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); + t.is(result.mnemonic, mnemonic); + t.ok(api.address.isValid(result.address)); + t.is(result.derivationPath, PATH1); }); test("address.generate: should generate keypair for 24 words mnemonic", async (t) => { - const mnemonic = mnemonic24Words; - const result = await api.address.generate(HRP, mnemonic, PATH1); - t.ok(b4a.isBuffer(result.publicKey)); - t.ok(b4a.isBuffer(result.secretKey)); - t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); - t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); - t.is(result.mnemonic, mnemonic); - t.ok(api.address.isValid(result.address)); - t.is(result.derivationPath, PATH1); + const mnemonic = mnemonic24Words; + const result = await api.address.generate(HRP, mnemonic, PATH1); + t.ok(b4a.isBuffer(result.publicKey)); + t.ok(b4a.isBuffer(result.secretKey)); + t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE); + t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE); + t.is(result.mnemonic, mnemonic); + t.ok(api.address.isValid(result.address)); + t.is(result.derivationPath, PATH1); }); test("address.generate: should generate the same keypair for the same mnemonic and derivation path", async (t) => { - const mnemonic = api.mnemonic.generate(); - const result1 = await api.address.generate(HRP, mnemonic, PATH1); - const result2 = await api.address.generate(HRP, mnemonic, PATH1); - t.ok(b4a.equals(result1.publicKey, result2.publicKey)); - t.ok(b4a.equals(result1.secretKey, result2.secretKey)); - t.is(result1.mnemonic, result2.mnemonic); - t.is(result1.address, result2.address); - t.is(result1.derivationPath, result2.derivationPath); - t.is(result1.derivationPath, PATH1); + const mnemonic = api.mnemonic.generate(); + const result1 = await api.address.generate(HRP, mnemonic, PATH1); + const result2 = await api.address.generate(HRP, mnemonic, PATH1); + t.ok(b4a.equals(result1.publicKey, result2.publicKey)); + t.ok(b4a.equals(result1.secretKey, result2.secretKey)); + t.is(result1.mnemonic, result2.mnemonic); + t.is(result1.address, result2.address); + t.is(result1.derivationPath, result2.derivationPath); + t.is(result1.derivationPath, PATH1); }); test("address.generate: should generate different keypairs for different mnemonic and same derivation path", async (t) => { - const mnemonic1 = api.mnemonic.generate(); - const mnemonic2 = api.mnemonic.generate(); - const result1 = await api.address.generate(HRP, mnemonic1, PATH1); - const result2 = await api.address.generate(HRP, mnemonic2, PATH1); - t.not(b4a.equals(result1.publicKey, result2.publicKey)); - t.not(b4a.equals(result1.secretKey, result2.secretKey)); - t.not(result1.address, result2.address); - t.is(result1.derivationPath, result2.derivationPath); - t.is(result1.derivationPath, PATH1); + const mnemonic1 = api.mnemonic.generate(); + const mnemonic2 = api.mnemonic.generate(); + const result1 = await api.address.generate(HRP, mnemonic1, PATH1); + const result2 = await api.address.generate(HRP, mnemonic2, PATH1); + t.not(b4a.equals(result1.publicKey, result2.publicKey)); + t.not(b4a.equals(result1.secretKey, result2.secretKey)); + t.not(result1.address, result2.address); + t.is(result1.derivationPath, result2.derivationPath); + t.is(result1.derivationPath, PATH1); }); test("address.generate: should generate different keypairs for the same mnemonic and different derivation path", async (t) => { - const mnemonic = api.mnemonic.generate(); - const result1 = await api.address.generate(HRP, mnemonic, PATH1); - const result2 = await api.address.generate(HRP, mnemonic, PATH2); - t.not(b4a.equals(result1.publicKey, result2.publicKey)); - t.not(b4a.equals(result1.secretKey, result2.secretKey)); - t.not(result1.address, result2.address); - t.is(result1.derivationPath, PATH1); - t.is(result2.derivationPath, PATH2); + const mnemonic = api.mnemonic.generate(); + const result1 = await api.address.generate(HRP, mnemonic, PATH1); + const result2 = await api.address.generate(HRP, mnemonic, PATH2); + t.not(b4a.equals(result1.publicKey, result2.publicKey)); + t.not(b4a.equals(result1.secretKey, result2.secretKey)); + t.not(result1.address, result2.address); + t.is(result1.derivationPath, PATH1); + t.is(result2.derivationPath, PATH2); }); test("address.generate: should return an error for invalid mnemonic", async (t) => { - const invalidMnemonics = [ - "", // Empty string - " ", // Spaces only - "invalid mnemonic", // Invalid words - "abandon abandon abandon", // Too few words - "abandon ".repeat(25).trim(), // Too many words (25) - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about extra", // 13 words - ]; - - for (const mnemonic of invalidMnemonics) { - try { - const result = await api.address.generate(HRP, mnemonic); - t.fail(`Should have thrown error for invalid mnemonic: "${mnemonic}"`); - } catch (error) { - t.pass(`Has thrown error for invalid mnemonic "${mnemonic}": ${error.message}`); // TODO: In the future, assert the error message + const invalidMnemonics = [ + "", // Empty string + " ", // Spaces only + "invalid mnemonic", // Invalid words + "abandon abandon abandon", // Too few words + "abandon ".repeat(25).trim(), // Too many words (25) + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about extra", // 13 words + ]; + + for (const mnemonic of invalidMnemonics) { + try { + await api.address.generate(HRP, mnemonic); + t.fail(`Should have thrown error for invalid mnemonic: "${mnemonic}"`); + } catch (error) { + t.pass(`Has thrown error for invalid mnemonic "${mnemonic}": ${error.message}`); // TODO: In the future, assert the error message + } } - } }); test("should accept valid hrp", async (t) => { - const validHrps = [ - "t", // Min length - "trac", - "a".repeat(31), // Max length - ]; - - for (const hrp of validHrps) { - try { - const result = await api.address.generate(hrp); - t.is(typeof result.address, "string", `Address is string for HRP: ${hrp}`); - t.ok(b4a.isBuffer(result.publicKey), `Public key is buffer for HRP: ${hrp}`); - t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE, `Public key length is valid for HRP: ${hrp}`); - t.ok(b4a.isBuffer(result.secretKey), `Secret key is buffer for HRP: ${hrp}`); - t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE, `Secret key length is valid for HRP: ${hrp}`); - t.is(typeof result.mnemonic, "string", `Mnemonic is string for HRP: ${hrp}`); - t.is(typeof result.derivationPath, "string", `Derivation path is string for HRP: ${hrp}`); + const validHrps = [ + "t", // Min length + "trac", + "a".repeat(31), // Max length + ]; + + for (const hrp of validHrps) { + try { + const result = await api.address.generate(hrp); + t.is(typeof result.address, "string", `Address is string for HRP: ${hrp}`); + t.ok(b4a.isBuffer(result.publicKey), `Public key is buffer for HRP: ${hrp}`); + t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE, `Public key length is valid for HRP: ${hrp}`); + t.ok(b4a.isBuffer(result.secretKey), `Secret key is buffer for HRP: ${hrp}`); + t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE, `Secret key length is valid for HRP: ${hrp}`); + t.is(typeof result.mnemonic, "string", `Mnemonic is string for HRP: ${hrp}`); + t.is(typeof result.derivationPath, "string", `Derivation path is string for HRP: ${hrp}`); + } + catch (error) { + t.fail(`Should not have thrown error for valid HRP ${hrp}: ${error.message}`); + } } - catch (error) { - t.fail(`Should not have thrown error for valid HRP ${hrp}: ${error.message}`); - } - } }); test("address.generate: should return an error for invalid hrp", async (t) => { - const invalidHrps = [ - {}, // Not a string - "", // Empty string - " ", // Spaces only - "a".repeat(32), // Too long - "Trac", // Uppercase letters - "tr@c", // Special characters - "trac-hrp", // Contains hyphen - "trac_hrp", // Contains underscore - "тест", // Non-ASCII characters - "hrp1", // Contains special separator '1' - "hrp2", // Contains number - ]; - - for (const hrp of invalidHrps) { - try { - const result = await api.address.generate(hrp); - t.fail(); - } catch (error) { - t.pass(`Has thrown error for invalid hrp: ${error.message}`); // TODO: In the future, assert the error message + const invalidHrps = [ + {}, // Not a string + "", // Empty string + " ", // Spaces only + "a".repeat(32), // Too long + "Trac", // Uppercase letters + "tr@c", // Special characters + "trac-hrp", // Contains hyphen + "trac_hrp", // Contains underscore + "тест", // Non-ASCII characters + "hrp1", // Contains special separator '1' + "hrp2", // Contains number + ]; + + for (const hrp of invalidHrps) { + try { + await api.address.generate(hrp); + t.fail(); + } catch (error) { + t.pass(`Has thrown error for invalid hrp: ${error.message}`); // TODO: In the future, assert the error message + } } - } }); test("address.generate: should accept valid derivation paths", async (t) => { - const mnemonic = api.mnemonic.generate(); - const paths = [ - undefined, // No path provided, should use default - null, // No path provided, should use default - "m/0'", - "m/0'/1'", - "m/0' /1'", // spaces are trimmed - "m/44'/60'" + "/0'".repeat(256), // Accepts big paths - "m/2147483647'/2147483646'/2147483645'", // Max hardened index values - ]; - - // Paths are accepted as valid - for (const path of paths) { - const result = await api.address.generate(HRP, mnemonic, path); - console.log("Testing valid path:", path); - t.is(typeof result.address, "string", "Address is string"); - t.ok(b4a.isBuffer(result.publicKey), "Public key is buffer"); - t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE, "Public key length is valid"); - t.ok(b4a.isBuffer(result.secretKey), "Secret key is buffer"); - t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE, "Secret key length is valid"); - t.is(typeof result.mnemonic, "string", "Mnemonic is string"); - t.is(typeof result.derivationPath, "string", "Derivation path is string"); - if (typeof path !== "string") { - t.is(result.derivationPath, DEFAULT_PATH, "Derivation path is default"); - } else { - t.is(result.derivationPath, path.replace(/\s+/g, ''), "Spaces were trimmed"); // Path with spaces trimmed + const mnemonic = api.mnemonic.generate(); + const paths = [ + undefined, // No path provided, should use default + null, // No path provided, should use default + "m/0'", + "m/0'/1'", + "m/0' /1'", // spaces are trimmed + "m/44'/60'" + "/0'".repeat(256), // Accepts big paths + "m/2147483647'/2147483646'/2147483645'", // Max hardened index values + ]; + + // Paths are accepted as valid + for (const path of paths) { + const result = await api.address.generate(HRP, mnemonic, path); + console.log("Testing valid path:", path); + t.is(typeof result.address, "string", "Address is string"); + t.ok(b4a.isBuffer(result.publicKey), "Public key is buffer"); + t.is(result.publicKey.length, TRAC_PUB_KEY_SIZE, "Public key length is valid"); + t.ok(b4a.isBuffer(result.secretKey), "Secret key is buffer"); + t.is(result.secretKey.length, TRAC_PRIV_KEY_SIZE, "Secret key length is valid"); + t.is(typeof result.mnemonic, "string", "Mnemonic is string"); + t.is(typeof result.derivationPath, "string", "Derivation path is string"); + if (typeof path !== "string") { + t.is(result.derivationPath, DEFAULT_PATH, "Derivation path is default"); + } else { + t.is(result.derivationPath, path.replace(/\s+/g, ''), "Spaces were trimmed"); // Path with spaces trimmed + } + // Verify address checksum + t.ok(api.address.isValid(result.address), "Address is valid"); + t.ok(bech32m.decode(result.address).prefix === HRP, "Address could be decoded and has correct HRP"); } - // Verify address checksum - t.ok(api.address.isValid(result.address), "Address is valid"); - t.ok(bech32m.decode(result.address).prefix === HRP, "Address could be decoded and has correct HRP"); - } }); test("address.generate: equivalent paths should produce the same result", async (t) => { - const mnemonic = api.mnemonic.generate(); - const result1 = await api.address.generate(HRP, mnemonic, "m/0'/1'"); - const result2 = await api.address.generate(HRP, mnemonic, "m/0' /1'"); - t.ok(b4a.equals(result1.publicKey, result2.publicKey)); - t.ok(b4a.equals(result1.secretKey, result2.secretKey)); - t.is(result1.mnemonic, result2.mnemonic); - t.is(result1.address, result2.address); - t.is(result1.derivationPath, result2.derivationPath); + const mnemonic = api.mnemonic.generate(); + const result1 = await api.address.generate(HRP, mnemonic, "m/0'/1'"); + const result2 = await api.address.generate(HRP, mnemonic, "m/0' /1'"); + t.ok(b4a.equals(result1.publicKey, result2.publicKey)); + t.ok(b4a.equals(result1.secretKey, result2.secretKey)); + t.is(result1.mnemonic, result2.mnemonic); + t.is(result1.address, result2.address); + t.is(result1.derivationPath, result2.derivationPath); }); test("address.generate: should throw an error for invalid derivation paths", async (t) => { - const paths = [ - 1234, // Not a string - { valid: false }, // Not a string - "invalid_path", - "m/", // No segments - "m/a'", // invalid segment - "m/0/1", // Non-hardened segments - "m/0'/1/2'", // Non-hardened segments - "m/2147483648'/2147483647'/2147483646'", // index bigger than max hardened value - "n/0'/1'", // Doesn't start with m/ - "", // Empty string - " ", // Spaces only - ]; - const mnemonic = api.mnemonic.generate(); - - for (const path of paths) { - try { - const result = await api.address.generate(HRP, mnemonic, path); - t.fail(`Should have thrown error for invalid path: ${path}`); - } catch (error) { - t.pass(`Has thrown error for invalid path ${path}: ${error.message}`); // TODO: In the future, assert the error message + const paths = [ + 1234, // Not a string + { valid: false }, // Not a string + "invalid_path", + "m/", // No segments + "m/a'", // invalid segment + "m/0/1", // Non-hardened segments + "m/0'/1/2'", // Non-hardened segments + "m/2147483648'/2147483647'/2147483646'", // index bigger than max hardened value + "n/0'/1'", // Doesn't start with m/ + "", // Empty string + " ", // Spaces only + ]; + const mnemonic = api.mnemonic.generate(); + + for (const path of paths) { + try { + await api.address.generate(HRP, mnemonic, path); + t.fail(`Should have thrown error for invalid path: ${path}`); + } catch (error) { + t.pass(`Has thrown error for invalid path ${path}: ${error.message}`); // TODO: In the future, assert the error message + } } - } }); test("address.fromSecretKey: should derive the correct address and public key from a given secret key", async (t) => { - const original = await api.address.generate(HRP); - const derived = api.address.fromSecretKey(HRP, original.secretKey); - t.is(derived.address, original.address); - t.ok(b4a.equals(derived.publicKey, original.publicKey)); - t.ok(b4a.equals(derived.secretKey, original.secretKey)); + const original = await api.address.generate(HRP); + const derived = api.address.fromSecretKey(HRP, original.secretKey); + t.is(derived.address, original.address); + t.ok(b4a.equals(derived.publicKey, original.publicKey)); + t.ok(b4a.equals(derived.secretKey, original.secretKey)); }); test("address.fromSecretKey: should throw on invalid secret key", async (t) => { - const invalidKeys = [ - Buffer.alloc(10), // Too short - Buffer.alloc(100), // Too long - "not a buffer", // Not a buffer - 12345, // Not a buffer - null, // Not a buffer - undefined, // Not a buffer - ]; - - for (const key of invalidKeys) { - try { - const result = await api.address.fromSecretKey(key, HRP); - t.fail("Should have thrown error for invalid secret key"); - } catch (error) { - t.pass(`Has thrown error for invalid secret key: ${error.message}`); // TODO: In the future, assert the error message + const invalidKeys = [ + Buffer.alloc(10), // Too short + Buffer.alloc(100), // Too long + "not a buffer", // Not a buffer + 12345, // Not a buffer + null, // Not a buffer + undefined, // Not a buffer + ]; + + for (const key of invalidKeys) { + try { + await api.address.fromSecretKey(key, HRP); + t.fail("Should have thrown error for invalid secret key"); + } catch (error) { + t.pass(`Has thrown error for invalid secret key: ${error.message}`); // TODO: In the future, assert the error message + } } - } }); \ No newline at end of file diff --git a/test/unit/all.test.js b/test/unit/all.test.js index d235485..71a4eac 100644 --- a/test/unit/all.test.js +++ b/test/unit/all.test.js @@ -1,20 +1,22 @@ const test = require("brittle"); function runTests() { - test.pause(); + test.pause(); - require("./mnemonic/mnemonic.test.js"); - require("./hash/hash.test.js"); - require("./address/address.test.js"); - require("./nonce/nonce.test.js"); - require("./signature/signature.test.js"); - require("./data/data.test.js"); - require("./utils/utils.test.js"); - require("./transaction/transaction.test.js"); - require("./operation/operation.test.js"); - require("./runtime/runtime.test.js"); + require("./mnemonic/mnemonic.test.js"); + require("./hash/hash.test.js"); + require("./address/address.test.js"); + require("./nonce/nonce.test.js"); + require("./signature/signature.test.js"); + require("./data/data.test.js"); + require("./utils/utils.test.js"); + require("./compat/dom.test.js"); + require("./compat/sodium.test.js"); + require("./transaction/transaction.test.js"); + require("./operation/operation.test.js"); + require("./runtime/runtime.test.js"); - test.resume(); + test.resume(); } -runTests(); \ No newline at end of file +runTests(); diff --git a/test/unit/compat/dom.test.js b/test/unit/compat/dom.test.js index bea672c..4f8e722 100644 --- a/test/unit/compat/dom.test.js +++ b/test/unit/compat/dom.test.js @@ -1,10 +1,53 @@ -test('ensureDOMCompat: should define document and location if missing', () => { - delete global.document; - delete global.location; +const test = require('brittle'); + +const { ensureDOMCompat } = require('../../../modules/compat/dom'); + +test('ensureDOMCompat: should define document and location if missing', (t) => { + const originalDocument = globalThis.document; + const originalLocation = globalThis.location; + + delete globalThis.document; + delete globalThis.location; + + ensureDOMCompat(); + + t.ok(globalThis.document, 'document should be defined'); + t.ok(globalThis.location, 'location should be defined'); + + if (originalDocument === undefined) { + delete globalThis.document; + } else { + globalThis.document = originalDocument; + } + + if (originalLocation === undefined) { + delete globalThis.location; + } else { + globalThis.location = originalLocation; + } +}); + +test('ensureDOMCompat: should not override existing globals', (t) => { + const originalDocument = globalThis.document; + const originalLocation = globalThis.location; + + globalThis.document = { test: true }; + globalThis.location = { href: 'keep-me' }; - const { ensureDOMCompat } = require('../../../modules/compat/dom'); ensureDOMCompat(); - expect(global.document).toBeDefined(); - expect(global.location).toBeDefined(); -}); \ No newline at end of file + t.is(globalThis.document.test, true, 'existing document should be preserved'); + t.is(globalThis.location.href, 'keep-me', 'existing location should be preserved'); + + if (originalDocument === undefined) { + delete globalThis.document; + } else { + globalThis.document = originalDocument; + } + + if (originalLocation === undefined) { + delete globalThis.location; + } else { + globalThis.location = originalLocation; + } +}); diff --git a/test/unit/compat/sodium.test.js b/test/unit/compat/sodium.test.js index bbee2d2..a71c8e7 100644 --- a/test/unit/compat/sodium.test.js +++ b/test/unit/compat/sodium.test.js @@ -1,19 +1,20 @@ -test('ensureSodiumCompat: should inject pbkdf2 if missing', () => { - const sodium = require('sodium-universal'); +const test = require('brittle'); +const sodium = require('sodium-universal'); - delete sodium.extension_pbkdf2_sha512_async; +const { ensureSodiumCompat } = require('../../../modules/compat/sodium'); - const { ensureSodiumCompat } = require('../../../modules/compat/sodium'); - ensureSodiumCompat(); +test('ensureSodiumCompat: should inject pbkdf2 if missing', (t) => { + const originalPbkdf2 = sodium.extension_pbkdf2_sha512_async; - expect(typeof sodium.extension_pbkdf2_sha512_async).toBe('function'); -}); + delete sodium.extension_pbkdf2_sha512_async; -test('ensureDOMCompat: should not override existing globals', () => { - global.document = { test: true }; + ensureSodiumCompat(); - const { ensureDOMCompat } = require('../../../modules/compat/dom'); - ensureDOMCompat(); + t.is(typeof sodium.extension_pbkdf2_sha512_async, 'function', 'pbkdf2 async compat should be injected'); - expect(global.document.test).toBe(true); -}); \ No newline at end of file + if (originalPbkdf2) { + sodium.extension_pbkdf2_sha512_async = originalPbkdf2; + } else { + delete sodium.extension_pbkdf2_sha512_async; + } +}); diff --git a/test/unit/data/data.test.js b/test/unit/data/data.test.js index d40bae0..cfba1c4 100644 --- a/test/unit/data/data.test.js +++ b/test/unit/data/data.test.js @@ -3,61 +3,61 @@ const api = require("../../../index.js"); const b4a = require("b4a"); test("encrypt and decrypt: should encrypt and decrypt a message", (t) => { - const password = b4a.from("someP@ssw0rd"); - const message = b4a.from("hello world!"); - const encrypted = api.data.encrypt(message, password); - t.ok(b4a.isBuffer(encrypted.nonce)); - t.ok(b4a.isBuffer(encrypted.salt)); - t.ok(b4a.isBuffer(encrypted.ciphertext)); - const decrypted = api.data.decrypt(encrypted, password); - t.ok(b4a.equals(decrypted, message)); + const password = b4a.from("someP@ssw0rd"); + const message = b4a.from("hello world!"); + const encrypted = api.data.encrypt(message, password); + t.ok(b4a.isBuffer(encrypted.nonce)); + t.ok(b4a.isBuffer(encrypted.salt)); + t.ok(b4a.isBuffer(encrypted.ciphertext)); + const decrypted = api.data.decrypt(encrypted, password); + t.ok(b4a.equals(decrypted, message)); }); test("encrypt: should throw if password is not a buffer", (t) => { - const message = b4a.from("hello world"); - try { - api.data.encrypt(message, "notabuffer"); - t.fail("Should have thrown an error"); - } catch (err) { - t.ok(err); - } + const message = b4a.from("hello world"); + try { + api.data.encrypt(message, "notabuffer"); + t.fail("Should have thrown an error"); + } catch (err) { + t.ok(err); + } }); test("encrypt: should throw if message is not a buffer", (t) => { - const password = b4a.from("someP@ssw0rd"); - try { - api.data.encrypt("notabuffer", password); - } catch (err) { - t.ok(err); - } + const password = b4a.from("someP@ssw0rd"); + try { + api.data.encrypt("notabuffer", password); + } catch (err) { + t.ok(err); + } }); test("decrypt: should throw if data format is invalid", (t) => { - const password = b4a.from("someP@ssw0rd"); - try { - api.data.decrypt({}, password); - } catch (err) { - t.ok(err); - } - try { - api.data.decrypt( - { nonce: "notabuffer", salt: "notabuffer", ciphertext: "notabuffer" }, - password - ); - } catch (err) { - t.ok(err); - } + const password = b4a.from("someP@ssw0rd"); + try { + api.data.decrypt({}, password); + } catch (err) { + t.ok(err); + } + try { + api.data.decrypt( + { nonce: "notabuffer", salt: "notabuffer", ciphertext: "notabuffer" }, + password + ); + } catch (err) { + t.ok(err); + } }); test("decrypt: should throw if decryption fails", (t) => { - const password = b4a.from("someP@ssw0rd"); - const wrongPassword = b4a.from("someOtherP@ssw0rd"); - const message = b4a.from("hello world"); - const encrypted = api.data.encrypt(message, password); - try { - api.data.decrypt(encrypted, wrongPassword); - t.fail("Decryption should have failed"); - } catch (err) { - t.ok(err); - } + const password = b4a.from("someP@ssw0rd"); + const wrongPassword = b4a.from("someOtherP@ssw0rd"); + const message = b4a.from("hello world"); + const encrypted = api.data.encrypt(message, password); + try { + api.data.decrypt(encrypted, wrongPassword); + t.fail("Decryption should have failed"); + } catch (err) { + t.ok(err); + } }); diff --git a/test/unit/hash/hash.test.js b/test/unit/hash/hash.test.js index 77029bf..5fa5cf3 100644 --- a/test/unit/hash/hash.test.js +++ b/test/unit/hash/hash.test.js @@ -13,61 +13,61 @@ const SHA256_DIGEST = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; test("hash.blake3: should compute Blake3 hash of a Buffer", async (t) => { - const digest = await hash.blake3(MESSAGE_BUFFER); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 32); - t.ok(b4a.equals(digest, b4a.from(BLAKE3_DIGEST, "hex"))); + const digest = await hash.blake3(MESSAGE_BUFFER); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 32); + t.ok(b4a.equals(digest, b4a.from(BLAKE3_DIGEST, "hex"))); }); test("hash.blake3: should compute Blake3 hash of a Uint8Array", async (t) => { - const digest = await hash.blake3(MESSAGE_ARRAY); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 32); - t.ok(b4a.equals(digest, b4a.from(BLAKE3_DIGEST, "hex"))); + const digest = await hash.blake3(MESSAGE_ARRAY); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 32); + t.ok(b4a.equals(digest, b4a.from(BLAKE3_DIGEST, "hex"))); }); test("hash.blake3: should throw for invalid input", async (t) => { - try { - const digest = await hash.blake3(MESSAGE); - t.fail("Expected error for invalid input"); - } catch (err) { - t.is(err.message, "Invalid input: must be a Buffer or Uint8Array"); - } + try { + await hash.blake3(MESSAGE); + t.fail("Expected error for invalid input"); + } catch (err) { + t.is(err.message, "Invalid input: must be a Buffer or Uint8Array"); + } }); test("hash.blake3Safe: should not throw for invalid input", async (t) => { - const digest = await hash.blake3Safe(MESSAGE); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 0); + const digest = await hash.blake3Safe(MESSAGE); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 0); }); test("hash.sha256: should compute SHA-256 hash of a Buffer", (t) => { - const msg = b4a.from(MESSAGE); - const digest = hash.sha256(msg); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 32); - t.ok(b4a.equals(digest, b4a.from(SHA256_DIGEST, "hex"))); + const msg = b4a.from(MESSAGE); + const digest = hash.sha256(msg); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 32); + t.ok(b4a.equals(digest, b4a.from(SHA256_DIGEST, "hex"))); }); test("hash.sha256: should compute SHA-256 hash of a Uint8Array", (t) => { - const msg = new Uint8Array(b4a.from(MESSAGE)); - const digest = hash.sha256(msg); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 32); - t.ok(b4a.equals(digest, b4a.from(SHA256_DIGEST, "hex"))); + const msg = new Uint8Array(b4a.from(MESSAGE)); + const digest = hash.sha256(msg); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 32); + t.ok(b4a.equals(digest, b4a.from(SHA256_DIGEST, "hex"))); }); test("hash.sha256: should throw for invalid input", async (t) => { - try { - const digest = await hash.sha256(MESSAGE); - t.fail("Expected error for invalid input."); - } catch (err) { - t.is(err.message, "Invalid input: must be a Buffer or Uint8Array"); - } + try { + await hash.sha256(MESSAGE); + t.fail("Expected error for invalid input."); + } catch (err) { + t.is(err.message, "Invalid input: must be a Buffer or Uint8Array"); + } }); test("hash.sha256Safe: should not throw for invalid input", async (t) => { - const digest = await hash.sha256Safe(MESSAGE); - t.ok(b4a.isBuffer(digest)); - t.is(digest.length, 0); + const digest = await hash.sha256Safe(MESSAGE); + t.ok(b4a.isBuffer(digest)); + t.is(digest.length, 0); }); diff --git a/test/unit/mnemonic/mnemonic.test.js b/test/unit/mnemonic/mnemonic.test.js index c9e4284..66c552e 100644 --- a/test/unit/mnemonic/mnemonic.test.js +++ b/test/unit/mnemonic/mnemonic.test.js @@ -7,70 +7,70 @@ const mnemonic11Words = "abandon abandon abandon abandon abandon abandon abandon const mnemonic24Words = mnemonic.generate(); test("mnemonic.generate: should generate a valid mnemonic", (t) => { - const phrase = mnemonic.generate(); - t.is(typeof phrase, "string"); - t.not(phrase, ""); - t.ok(TRAC_MNEMONIC_WORD_COUNT.includes(phrase.split(" ").length)); + const phrase = mnemonic.generate(); + t.is(typeof phrase, "string"); + t.not(phrase, ""); + t.ok(TRAC_MNEMONIC_WORD_COUNT.includes(phrase.split(" ").length)); }); test("mnemonic.generate: should generate different mnemonics", (t) => { - const phrase1 = mnemonic.generate(); - const phrase2 = mnemonic.generate(); - t.not(phrase1, phrase2); + const phrase1 = mnemonic.generate(); + const phrase2 = mnemonic.generate(); + t.not(phrase1, phrase2); }); test("mnemonic.isValid: should return true for a valid mnemonic", (t) => { - t.is(mnemonic.isValid(mnemonic24Words), true); - t.is(mnemonic.isValid(mnemonic12Words), true); + t.is(mnemonic.isValid(mnemonic24Words), true); + t.is(mnemonic.isValid(mnemonic12Words), true); }); test("mnemonic.isValid: should return false for an invalid mnemonic", (t) => { - t.is(mnemonic.isValid("invalid mnemonic phrase"), false); + t.is(mnemonic.isValid("invalid mnemonic phrase"), false); }); test("mnemonic.isValid: should return false for empty input", (t) => { - t.is(mnemonic.isValid(""), false); - t.is(mnemonic.isValid(null), false); + t.is(mnemonic.isValid(""), false); + t.is(mnemonic.isValid(null), false); }); test("mnemonic.isValid: should return false for wrong word count", (t) => { - t.is(mnemonic.isValid(mnemonic11Words), false); + t.is(mnemonic.isValid(mnemonic11Words), false); }); test("mnemonic.sanitize: should sanitize and validate mnemonic", (t) => { - const phrase = mnemonic.generate(); - const sanitized = mnemonic.sanitize(phrase); - t.is(sanitized, phrase); + const phrase = mnemonic.generate(); + const sanitized = mnemonic.sanitize(phrase); + t.is(sanitized, phrase); }); test("mnemonic.sanitize: should return null for invalid input", (t) => { - t.is(mnemonic.sanitize(""), null); - t.is(mnemonic.sanitize(undefined), null); - t.is(mnemonic.sanitize({}), null); - t.is(mnemonic.sanitize(1234), null); - t.is(mnemonic.sanitize(null), null); + t.is(mnemonic.sanitize(""), null); + t.is(mnemonic.sanitize(undefined), null); + t.is(mnemonic.sanitize({}), null); + t.is(mnemonic.sanitize(1234), null); + t.is(mnemonic.sanitize(null), null); }); test("mnemonic.sanitize: should return null for wrong word count", (t) => { - t.is(mnemonic.sanitize(mnemonic11Words), null); + t.is(mnemonic.sanitize(mnemonic11Words), null); }); test("mnemonic.sanitize: should return null on invalid mnemonic", (t) => { - t.is(mnemonic.sanitize("invalid mnemonic phrase"), null); + t.is(mnemonic.sanitize("invalid mnemonic phrase"), null); }); test("mnemonic.toSeed: should convert mnemonic to seed", async (t) => { - const phrase = mnemonic.generate(); - const seed = await mnemonic.toSeed(phrase); - t.ok(Buffer.isBuffer(seed)); - t.ok(seed.length > 0); + const phrase = mnemonic.generate(); + const seed = await mnemonic.toSeed(phrase); + t.ok(Buffer.isBuffer(seed)); + t.ok(seed.length > 0); }); test("mnemonic.toSeed: should throw on invalid mnemonic", async (t) => { - try { - await mnemonic.toSeed("invalid mnemonic phrase"); - t.fail(); - } catch { - t.pass(); - } + try { + await mnemonic.toSeed("invalid mnemonic phrase"); + t.fail(); + } catch { + t.pass(); + } }); diff --git a/test/unit/nonce/nonce.test.js b/test/unit/nonce/nonce.test.js index b8cd4e3..3982bec 100644 --- a/test/unit/nonce/nonce.test.js +++ b/test/unit/nonce/nonce.test.js @@ -2,13 +2,13 @@ const test = require("brittle"); const nonce = require("../../../modules/nonce.js"); test("nonce.generate: should generate a 32-byte nonce", (t) => { - const n = nonce.generate(); - t.ok(Buffer.isBuffer(n)); - t.is(n.length, nonce.SIZE); + const n = nonce.generate(); + t.ok(Buffer.isBuffer(n)); + t.is(n.length, nonce.SIZE); }); test("nonce.generate: should generate unique nonces", (t) => { - const n1 = nonce.generate(); - const n2 = nonce.generate(); - t.not(n1.equals(n2), true); + const n1 = nonce.generate(); + const n2 = nonce.generate(); + t.not(n1.equals(n2), true); }); diff --git a/test/unit/operation/operation.test.js b/test/unit/operation/operation.test.js index 8f8cebf..63b6608 100644 --- a/test/unit/operation/operation.test.js +++ b/test/unit/operation/operation.test.js @@ -3,7 +3,12 @@ const api = require("../../../index.js"); const b4a = require("b4a"); const sodium = require("sodium-universal"); -const { TRAC_VALIDITY_SIZE_BYTES, TRAC_HASH_SIZE, TRAC_NONCE_SIZE, TRAC_NETWORK_MAINNET_ID } = require("../../../constants.js"); +const { + TRAC_VALIDITY_SIZE_BYTES, + TRAC_HASH_SIZE, + TRAC_NONCE_SIZE, + TRAC_NETWORK_MAINNET_ID +} = require("../../../constants.js"); const OP_TYPE_TX = 12; @@ -18,6 +23,11 @@ test("operation: should build a valid operation", async (t) => { const fromKeyPair = await api.address.generate("trac"); // Check Pre-Build + let validator; + let contentHash; + let originBootstrap; + let destinationBootstrap; + let validity; const txData = await api.operation.preBuild( fromKeyPair.address, validator = randomBuf(32).toString("hex"), @@ -48,7 +58,7 @@ test("operation: should build a valid operation", async (t) => { try { data = JSON.parse(b4a.from(payload, "base64").toString("utf-8")); t.ok(data, "Payload should be a base64 encoded JSON"); - } catch (error) { + } catch { t.fail("Payload is not a base64 encoded JSON"); } @@ -76,13 +86,18 @@ test("operation preBuild: networkId is used in serialization", async (t) => { const customNetworkId = 42; // Check Pre-Build with custom networkId + const validator = randomBuf(32).toString("hex"); + const contentHash = randomBuf(32).toString("hex"); + const originBootstrap = randomBuf(32).toString("hex"); + const destinationBootstrap = randomBuf(32).toString("hex"); + const validity = randomBuf(TRAC_VALIDITY_SIZE_BYTES).toString("hex"); const txData = await api.operation.preBuild( fromKeyPair.address, - validator = randomBuf(32).toString("hex"), - contentHash = randomBuf(32).toString("hex"), - originBootstrap = randomBuf(32).toString("hex"), - destinationBootstrap = randomBuf(32).toString("hex"), - validity = randomBuf(TRAC_VALIDITY_SIZE_BYTES).toString("hex"), + validator, + contentHash, + originBootstrap, + destinationBootstrap, + validity, customNetworkId ); diff --git a/test/unit/runtime/runtime.test.js b/test/unit/runtime/runtime.test.js index dbf5a76..e6c0e06 100644 --- a/test/unit/runtime/runtime.test.js +++ b/test/unit/runtime/runtime.test.js @@ -1,12 +1,12 @@ const t = require("brittle"); t.test("node env should not have browser globals after loading index", (t) => { - // Enforce clean state before testing - delete globalThis.document; - delete globalThis.location; + // Enforce clean state before testing + delete globalThis.document; + delete globalThis.location; - require("../../../index.js"); + require("../../../index.js"); - t.is(typeof globalThis.document, "undefined"); - t.is(typeof globalThis.location, "undefined"); + t.is(typeof globalThis.document, "undefined"); + t.is(typeof globalThis.location, "undefined"); }); diff --git a/test/unit/signature/signature.test.js b/test/unit/signature/signature.test.js index d2a7209..db06a70 100644 --- a/test/unit/signature/signature.test.js +++ b/test/unit/signature/signature.test.js @@ -5,28 +5,28 @@ const address = require("../../../modules/address.js"); const hrp = "trac"; test("signature.sign/verify: should sign and verify a message", async (t) => { - const { publicKey, secretKey } = await address.generate(hrp); - const message = Buffer.from("hello world"); - const sig = signature.sign(message, secretKey); - t.ok(Buffer.isBuffer(sig)); - t.ok(sig.length > 0); - const valid = signature.verify(sig, message, publicKey); - t.is(valid, true); + const { publicKey, secretKey } = await address.generate(hrp); + const message = Buffer.from("hello world"); + const sig = signature.sign(message, secretKey); + t.ok(Buffer.isBuffer(sig)); + t.ok(sig.length > 0); + const valid = signature.verify(sig, message, publicKey); + t.is(valid, true); }); test("signature.verify: should fail verification for wrong message", async (t) => { - const { publicKey, secretKey } = await address.generate(hrp); - const message = Buffer.from("hello world"); - const sig = signature.sign(message, secretKey); - const valid = signature.verify(sig, Buffer.from("other message"), publicKey); - t.is(valid, false); + const { publicKey, secretKey } = await address.generate(hrp); + const message = Buffer.from("hello world"); + const sig = signature.sign(message, secretKey); + const valid = signature.verify(sig, Buffer.from("other message"), publicKey); + t.is(valid, false); }); test("signature.sign: should throw on invalid private key", (t) => { - try { - signature.sign(Buffer.from("msg"), Buffer.alloc(10)); - t.fail(); - } catch { - t.pass(); - } + try { + signature.sign(Buffer.from("msg"), Buffer.alloc(10)); + t.fail(); + } catch { + t.pass(); + } }); diff --git a/test/unit/transaction/transaction.test.js b/test/unit/transaction/transaction.test.js index 57e81d5..3d08a8c 100644 --- a/test/unit/transaction/transaction.test.js +++ b/test/unit/transaction/transaction.test.js @@ -63,7 +63,7 @@ test("transaction: should build a valid transaction", async (t) => { try { data = JSON.parse(b4a.from(payload, "base64").toString("utf-8")); t.ok(data, "Payload should be a base64 encoded JSON"); - } catch (error) { + } catch { t.fail("Payload is not a base64 encoded JSON"); } diff --git a/test/unit/utils/utils.test.js b/test/unit/utils/utils.test.js index 348f8b5..ceb78c0 100644 --- a/test/unit/utils/utils.test.js +++ b/test/unit/utils/utils.test.js @@ -3,92 +3,92 @@ const api = require("../../../index.js"); const b4a = require("b4a"); test("memzero: should zero out a buffer", (t) => { - const buf = b4a.from("hello world"); - api.utils.memzero(buf); - t.ok(buf.every((byte) => byte === 0)); + const buf = b4a.from("hello world"); + api.utils.memzero(buf); + t.ok(buf.every((byte) => byte === 0)); }); test("toUInt32: should convert a number to a 4-byte big-endian buffer", (t) => { - const num = 16909060; // 0x01020304 - const buf = api.utils.toUInt32(num, 0); - t.ok(b4a.isBuffer(buf)); - t.is(buf.length, 4); - t.ok(b4a.equals(buf, b4a.from([1, 2, 3, 4]))); + const num = 16909060; // 0x01020304 + const buf = api.utils.toUInt32(num, 0); + t.ok(b4a.isBuffer(buf)); + t.is(buf.length, 4); + t.ok(b4a.equals(buf, b4a.from([1, 2, 3, 4]))); }); test("isUInt32: should validate if a number is a valid uint32", (t) => { - t.ok(api.utils.isUInt32(1)); - t.ok(api.utils.isUInt32(4294967295)); // Max uint32 - t.ok(api.utils.isUInt32(0)); // Min uint32 - t.not(api.utils.isUInt32(4294967296)); // Above range - t.not(api.utils.isUInt32(-1)); // Negative number - t.not(api.utils.isUInt32(3.14)); // Not an integer - t.not(api.utils.isUInt32("string")); // Not a number + t.ok(api.utils.isUInt32(1)); + t.ok(api.utils.isUInt32(4294967295)); // Max uint32 + t.ok(api.utils.isUInt32(0)); // Min uint32 + t.not(api.utils.isUInt32(4294967296)); // Above range + t.not(api.utils.isUInt32(-1)); // Negative number + t.not(api.utils.isUInt32(3.14)); // Not an integer + t.not(api.utils.isUInt32("string")); // Not a number }); test("isHexString: should validate if a string is a valid hex string", (t) => { - t.ok(api.utils.isHexString("abcdef123456")); - t.ok(api.utils.isHexString("ABCDEF123456")); - t.not(api.utils.isHexString("xyz123")); // Invalid characters - t.not(api.utils.isHexString("12345g")); // Invalid character 'g' - t.not(api.utils.isHexString("")); // Empty string - t.not(api.utils.isHexString(12345)); // Not a string + t.ok(api.utils.isHexString("abcdef123456")); + t.ok(api.utils.isHexString("ABCDEF123456")); + t.not(api.utils.isHexString("xyz123")); // Invalid characters + t.not(api.utils.isHexString("12345g")); // Invalid character 'g' + t.not(api.utils.isHexString("")); // Empty string + t.not(api.utils.isHexString(12345)); // Not a string }); test("serialize: should serialize buffers and uint32 numbers", (t) => { - const buf1 = b4a.from([1, 2, 3]); - const num = 16909060; // 0x01020304 - const buf2 = b4a.from([5, 6, 7, 8]); - const serialized = api.utils.serialize(buf1, num, buf2); - t.ok(b4a.isBuffer(serialized)); - t.ok(b4a.equals(serialized, b4a.from([1, 2, 3, 1, 2, 3, 4, 5, 6, 7, 8]))); + const buf1 = b4a.from([1, 2, 3]); + const num = 16909060; // 0x01020304 + const buf2 = b4a.from([5, 6, 7, 8]); + const serialized = api.utils.serialize(buf1, num, buf2); + t.ok(b4a.isBuffer(serialized)); + t.ok(b4a.equals(serialized, b4a.from([1, 2, 3, 1, 2, 3, 4, 5, 6, 7, 8]))); }); test("serialize: should throw error on invalid argument types", (t) => { - const buf = b4a.from([1, 2, 3]); - const invalidArgs = [ - "string", - {}, - null, - undefined, - 3.14, // Not an integer - -1, // Negative number - 4294967296, // Above uint32 range - ]; + const buf = b4a.from([1, 2, 3]); + const invalidArgs = [ + "string", + {}, + null, + undefined, + 3.14, // Not an integer + -1, // Negative number + 4294967296, // Above uint32 range + ]; - for (const arg of invalidArgs) { - try { - api.utils.serialize(buf, arg); - t.fail(`Expected error for argument: ${arg}`); - } catch (err) { - t.pass("Has thrown error as expected: " + err.message); // TODO: In the future, assert the error message + for (const arg of invalidArgs) { + try { + api.utils.serialize(buf, arg); + t.fail(`Expected error for argument: ${arg}`); + } catch (err) { + t.pass("Has thrown error as expected: " + err.message); // TODO: In the future, assert the error message + } } - } }); test("toBase64: should convert an object to a base64 string", (t) => { - const obj = { foo: "bar", num: 42 }; - const base64 = api.utils.toBase64(obj); - t.is(typeof base64, "string"); - const decoded = JSON.parse(b4a.from(base64, "base64").toString("utf-8")); - t.is(decoded.foo, obj.foo); - t.is(decoded.num, obj.num); + const obj = { foo: "bar", num: 42 }; + const base64 = api.utils.toBase64(obj); + t.is(typeof base64, "string"); + const decoded = JSON.parse(b4a.from(base64, "base64").toString("utf-8")); + t.is(decoded.foo, obj.foo); + t.is(decoded.num, obj.num); }); test("toBase64: should throw error if input is not an object", (t) => { - const invalidInputs = [ - "string", - 123, - null, - undefined, - ]; + const invalidInputs = [ + "string", + 123, + null, + undefined, + ]; - for (const input of invalidInputs) { - try { - api.utils.toBase64(input); - t.fail(`Expected error for input: ${input}`); - } catch (err) { - t.pass("Has thrown error as expected: " + err.message); // TODO: In the future, assert the error message + for (const input of invalidInputs) { + try { + api.utils.toBase64(input); + t.fail(`Expected error for input: ${input}`); + } catch (err) { + t.pass("Has thrown error as expected: " + err.message); // TODO: In the future, assert the error message + } } - } }); \ No newline at end of file