diff --git a/.pnp.cjs b/.pnp.cjs index b2a860bb236..8304b5561bd 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2576,7 +2576,6 @@ const RAW_RUNTIME_STATE = ["assert-browserify", "npm:2.0.0"],\ ["babel-loader", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:9.1.3"],\ ["browserify-zlib", "npm:0.2.0"],\ - ["bs58", "npm:4.0.1"],\ ["buffer", "npm:6.0.3"],\ ["cbor", "npm:8.1.0"],\ ["chai", "npm:4.3.10"],\ @@ -2595,25 +2594,21 @@ const RAW_RUNTIME_STATE = ["karma-mocha", "npm:2.0.1"],\ ["karma-mocha-reporter", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:2.2.5"],\ ["karma-webpack", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:5.0.0"],\ - ["lodash", "npm:4.17.23"],\ ["mocha", "npm:11.1.0"],\ - ["node-fetch", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:2.6.7"],\ - ["node-inspect-extracted", "npm:1.0.8"],\ ["nyc", "npm:15.1.0"],\ ["os-browserify", "npm:0.3.0"],\ ["path-browserify", "npm:1.0.1"],\ ["process", "npm:0.11.10"],\ - ["setimmediate", "npm:1.0.5"],\ ["sinon", "npm:18.0.1"],\ ["sinon-chai", "virtual:e2d057e7cc143d3cb9bec864f4a2d862441b5a09f81f8e6c46e7a098cbc89e4d07017cc6e2e2142d5704bb55da853cbec2d025ebc0b30e8696c31380c00f2c7d#npm:3.7.0"],\ ["stream-browserify", "npm:3.0.0"],\ ["string_decoder", "npm:1.3.0"],\ + ["undici", "npm:6.25.0"],\ ["url", "npm:0.11.3"],\ ["util", "npm:0.12.4"],\ ["wasm-x11-hash", "npm:0.0.2"],\ ["webpack", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:5.105.0"],\ - ["webpack-cli", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:4.9.1"],\ - ["winston", "npm:3.3.3"]\ + ["webpack-cli", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:4.9.1"]\ ],\ "linkType": "SOFT"\ }]\ @@ -4422,7 +4417,7 @@ const RAW_RUNTIME_STATE = ["@octokit/request-error", "npm:2.1.0"],\ ["@octokit/types", "npm:6.34.0"],\ ["is-plain-object", "npm:5.0.0"],\ - ["node-fetch", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:2.6.7"],\ + ["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"],\ ["universal-user-agent", "npm:6.0.0"]\ ],\ "linkType": "HARD"\ @@ -16640,12 +16635,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:2.6.7", {\ - "packageLocation": "./.yarn/__virtual__/node-fetch-virtual-a3f0ba2944/0/cache/node-fetch-npm-2.6.7-777aa2a6df-4bc9245383.zip/node_modules/node-fetch/",\ + ["virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7", {\ + "packageLocation": "./.yarn/__virtual__/node-fetch-virtual-d3846f8e12/0/cache/node-fetch-npm-2.6.7-777aa2a6df-4bc9245383.zip/node_modules/node-fetch/",\ "packageDependencies": [\ ["@types/encoding", null],\ ["encoding", null],\ - ["node-fetch", "virtual:8f25fc90e0fb5fd89843707863857591fa8c52f9f33eadced4bf404b1871d91959f7bb86948ae0e1b53ee94d491ef8fde9c0b58b39c9490c0d0fa6c931945f97#npm:2.6.7"],\ + ["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"],\ ["whatwg-url", "npm:5.0.0"]\ ],\ "packagePeers": [\ @@ -22063,6 +22058,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["undici", [\ + ["npm:6.25.0", {\ + "packageLocation": "./.yarn/cache/undici-npm-6.25.0-6002e70879-a475e45da3.zip/node_modules/undici/",\ + "packageDependencies": [\ + ["undici", "npm:6.25.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["undici-types", [\ ["npm:6.21.0", {\ "packageLocation": "./.yarn/cache/undici-types-npm-6.21.0-eb2b0ed56a-ec8f41aa43.zip/node_modules/undici-types/",\ diff --git a/.yarn/cache/undici-npm-6.25.0-6002e70879-a475e45da3.zip b/.yarn/cache/undici-npm-6.25.0-6002e70879-a475e45da3.zip new file mode 100644 index 00000000000..c2c62d4636a Binary files /dev/null and b/.yarn/cache/undici-npm-6.25.0-6002e70879-a475e45da3.zip differ diff --git a/packages/dapi-grpc/clients/core/v0/web/CorePromiseClient.js b/packages/dapi-grpc/clients/core/v0/web/CorePromiseClient.js index 3f1549d8058..818ef24e84a 100644 --- a/packages/dapi-grpc/clients/core/v0/web/CorePromiseClient.js +++ b/packages/dapi-grpc/clients/core/v0/web/CorePromiseClient.js @@ -1,4 +1,11 @@ -const { promisify } = require('util'); +// Inline promisify shim — avoids requiring Node's `util` module so this file +// can be bundled for browsers without a polyfill. If the codegen template +// is regenerated, restore this shim. +function promisify(fn) { + return (...args) => new Promise((resolve, reject) => { + fn(...args, (err, result) => (err ? reject(err) : resolve(result))); + }); +} const GrpcError = require('@dashevo/grpc-common/lib/server/error/GrpcError'); const { CoreClient } = require('./core_pb_service'); diff --git a/packages/dapi-grpc/clients/platform/v0/web/PlatformPromiseClient.js b/packages/dapi-grpc/clients/platform/v0/web/PlatformPromiseClient.js index 13be28a2d50..fbf39888b2e 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/PlatformPromiseClient.js +++ b/packages/dapi-grpc/clients/platform/v0/web/PlatformPromiseClient.js @@ -1,5 +1,13 @@ const { PlatformClient } = require('./platform_pb_service'); -const { promisify } = require('util'); + +// Inline promisify shim — avoids requiring Node's `util` module so this file +// can be bundled for browsers without a polyfill. If the codegen template +// is regenerated, restore this shim. +function promisify(fn) { + return (...args) => new Promise((resolve, reject) => { + fn(...args, (err, result) => (err ? reject(err) : resolve(result))); + }); +} class PlatformPromiseClient { /** diff --git a/packages/js-dapi-client/lib/dapiAddressProvider/ListDAPIAddressProvider.js b/packages/js-dapi-client/lib/dapiAddressProvider/ListDAPIAddressProvider.js index d9133a1ad4c..c13217e98d4 100644 --- a/packages/js-dapi-client/lib/dapiAddressProvider/ListDAPIAddressProvider.js +++ b/packages/js-dapi-client/lib/dapiAddressProvider/ListDAPIAddressProvider.js @@ -1,4 +1,4 @@ -const sample = require('lodash/sample'); +const sample = (arr) => arr[Math.floor(Math.random() * arr.length)]; const networks = require('@dashevo/dashcore-lib/lib/networks'); class ListDAPIAddressProvider { diff --git a/packages/js-dapi-client/lib/index.js b/packages/js-dapi-client/lib/index.js index f10beb47695..65ad0f4deda 100644 --- a/packages/js-dapi-client/lib/index.js +++ b/packages/js-dapi-client/lib/index.js @@ -1,5 +1,3 @@ -require('../polyfills/fetch-polyfill'); - const DAPIClient = require('./DAPIClient'); const NotFoundError = require('./transport/GrpcTransport/errors/NotFoundError'); diff --git a/packages/js-dapi-client/lib/logger/index.js b/packages/js-dapi-client/lib/logger/index.js index a06b34c145d..feebafb9c66 100644 --- a/packages/js-dapi-client/lib/logger/index.js +++ b/packages/js-dapi-client/lib/logger/index.js @@ -1,80 +1,42 @@ -const util = require('util'); -const winston = require('winston'); +const LOG_LEVEL = (typeof process !== 'undefined' && process.env && process.env.LOG_LEVEL) || 'silent'; -// TODO: Refactor to use params instead on envs - -const LOG_LEVEL = process.env.LOG_LEVEL || 'silent'; -const LOG_TO_FILE = process.env.LOG_WALLET_TO_FILE || 'false'; - -// Log levels: -// error 0 -// warn 1 -// info 2 (default) -// verbose 3 -// debug 4 -// silly 5 - -const loggers = {}; - -const createLogger = (formats = [], id = '') => { - const format = winston.format.combine( - { - transform: (info) => { - const args = info[Symbol.for('splat')]; - const result = { ...info }; - if (args) { - result.message = util.format(info.message, ...args); - } - return result; - }, - }, - ...formats, - winston.format.colorize(), - winston.format.printf(({ - level, message, - }) => `${level}: ${message}`), - ); - - const transports = [ - new winston.transports.Console({ - format, - silent: LOG_LEVEL === 'silent', - }), - ]; - - if (LOG_TO_FILE === 'true' && typeof window === 'undefined') { - transports.push( - new winston.transports.File({ - filename: `wallet${id !== '' ? `_${id}` : ''}`, - format, - silent: LOG_LEVEL === 'silent', - }), - ); - } - - return winston.createLogger({ - level: LOG_LEVEL, - transports, - }); +const LEVELS = { + silent: -1, error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5, }; -const logger = createLogger(); - -logger.getForId = (id) => { - if (!loggers[id]) { - const format = { - transform: (info) => { - const message = `[DAPIClient: ${id}] ${info.message}`; - return { ...info, message }; - }, - }; - - loggers[id] = createLogger([format], id); - } - - return loggers[id]; -}; - -logger.verbose(`Logger uses "${LOG_LEVEL}" level`, { level: LOG_LEVEL }); +const cache = {}; + +function build(level = LOG_LEVEL, prefix = '') { + const threshold = LEVELS[level] != null ? LEVELS[level] : LEVELS.silent; + const noop = () => {}; + // Preserve printf-style interpolation (%s/%d/%o/...): when there is a + // prefix and the first argument is the format string, merge the prefix + // into it. Otherwise hand the prefix to console.* as a leading argument. + const fmt = prefix + ? (...a) => { + if (a.length === 0) return [prefix]; + const [first, ...rest] = a; + return typeof first === 'string' ? [`${prefix} ${first}`, ...rest] : [prefix, first, ...rest]; + } + : (...a) => a; + + const logger = { + error: threshold >= 0 ? (...a) => console.error(...fmt(...a)) : noop, + warn: threshold >= 1 ? (...a) => console.warn(...fmt(...a)) : noop, + info: threshold >= 2 ? (...a) => console.info(...fmt(...a)) : noop, + verbose: threshold >= 3 ? (...a) => console.debug(...fmt(...a)) : noop, + debug: threshold >= 4 ? (...a) => console.debug(...fmt(...a)) : noop, + silly: threshold >= 5 ? (...a) => console.debug(...fmt(...a)) : noop, + getForId(id, overrideLevel) { + const effective = overrideLevel || level; + const key = `${id}\0${effective}`; + if (!cache[key]) { + cache[key] = build(effective, `[DAPIClient: ${id}]`); + } + return cache[key]; + }, + }; + return logger; +} -module.exports = logger; +module.exports = build(); diff --git a/packages/js-dapi-client/lib/test/bootstrap.js b/packages/js-dapi-client/lib/test/bootstrap.js index 473d16fb1d1..85d7947d666 100644 --- a/packages/js-dapi-client/lib/test/bootstrap.js +++ b/packages/js-dapi-client/lib/test/bootstrap.js @@ -1,6 +1,3 @@ -require('../../polyfills/fetch-polyfill'); -require('setimmediate'); - const { expect, use } = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); diff --git a/packages/js-dapi-client/lib/test/karma/bootstrap.js b/packages/js-dapi-client/lib/test/karma/bootstrap.js index 3a2cda9bdb3..d3052dd3f57 100644 --- a/packages/js-dapi-client/lib/test/karma/bootstrap.js +++ b/packages/js-dapi-client/lib/test/karma/bootstrap.js @@ -1,6 +1,3 @@ -require('../../../polyfills/fetch-polyfill'); -require('setimmediate'); - const { expect, use } = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); diff --git a/packages/js-dapi-client/lib/transport/JsonRpcTransport/requestJsonRpc.js b/packages/js-dapi-client/lib/transport/JsonRpcTransport/requestJsonRpc.js index ddbdf797088..49bab7c6338 100644 --- a/packages/js-dapi-client/lib/transport/JsonRpcTransport/requestJsonRpc.js +++ b/packages/js-dapi-client/lib/transport/JsonRpcTransport/requestJsonRpc.js @@ -1,6 +1,10 @@ -const https = require('https'); const JsonRpcError = require('./errors/JsonRpcError'); const WrongHttpCodeError = require('./errors/WrongHttpCodeError'); + +// Lazily-created undici Agent that disables TLS verification, shared across +// all self-signed requests. A per-request Agent would leak its socket pool +// since nothing destroys it after the fetch completes. +let sharedSelfSignedAgent; /** * @typedef {requestJsonRpc} * @param {string} protocol @@ -47,17 +51,24 @@ async function requestJsonRpc(protocol, host, port, selfSigned, method, params, Object.assign(requestOptions, { signal: controller.signal }); } - // For NodeJS Client + // Self-signed HTTPS: Node 18+ built-in fetch is backed by undici, which + // accepts a `dispatcher` for per-request TLS settings. Browsers can't + // bypass TLS verification, so the flag is a no-op there. eval('require') + // hides undici from bundler static analysis so it isn't pulled into + // browser bundles. if (typeof process !== 'undefined' && process.versions != null && process.versions.node != null && protocol === 'https' && selfSigned) { - requestOptions.agent = new https.Agent({ - rejectUnauthorized: false, - }); + if (!sharedSelfSignedAgent) { + // eslint-disable-next-line no-eval, global-require + const { Agent } = eval('require')('undici'); + sharedSelfSignedAgent = new Agent({ connect: { rejectUnauthorized: false } }); + } + requestOptions.dispatcher = sharedSelfSignedAgent; } - // eslint-disable-next-line + const response = await fetch(url, requestOptions); if (typeof requestTimeoutId !== 'undefined') { diff --git a/packages/js-dapi-client/package.json b/packages/js-dapi-client/package.json index 94ad7945e96..46dab74f186 100644 --- a/packages/js-dapi-client/package.json +++ b/packages/js-dapi-client/package.json @@ -31,14 +31,10 @@ "@dashevo/dashcore-lib": "~0.22.0", "@dashevo/grpc-common": "workspace:*", "@dashevo/wasm-dpp": "workspace:*", - "bs58": "^4.0.1", "cbor": "^8.0.0", "google-protobuf": "^3.12.2", - "lodash": "^4.17.23", - "node-fetch": "^2.6.7", - "node-inspect-extracted": "^1.0.8", - "wasm-x11-hash": "~0.0.2", - "winston": "^3.2.1" + "undici": "^6.0.0", + "wasm-x11-hash": "~0.0.2" }, "devDependencies": { "@babel/core": "^7.26.10", @@ -66,7 +62,6 @@ "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "setimmediate": "^1.0.5", "sinon": "^18.0.1", "sinon-chai": "^3.7.0", "stream-browserify": "^3.0.0", @@ -76,10 +71,12 @@ "webpack": "^5.104.0", "webpack-cli": "^4.9.1" }, + "engines": { + "node": ">=18.18" + }, "files": [ "docs", "lib", - "polyfills", "dist" ], "scripts": { diff --git a/packages/js-dapi-client/polyfills/fetch-polyfill.js b/packages/js-dapi-client/polyfills/fetch-polyfill.js deleted file mode 100644 index 8fa8ac2ffce..00000000000 --- a/packages/js-dapi-client/polyfills/fetch-polyfill.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable */ - -const { default: fetch, Headers, Request, Response } = require('node-fetch'); - -if (typeof window === 'undefined') { - globalThis.fetch = fetch; - globalThis.Headers = Headers; - globalThis.Request = Request; - globalThis.Response = Response; -} diff --git a/packages/js-dapi-client/test/unit/transport/JsonRpcTransport/requestJsonRpc.spec.js b/packages/js-dapi-client/test/unit/transport/JsonRpcTransport/requestJsonRpc.spec.js index fb056544927..fb77eaa464c 100644 --- a/packages/js-dapi-client/test/unit/transport/JsonRpcTransport/requestJsonRpc.spec.js +++ b/packages/js-dapi-client/test/unit/transport/JsonRpcTransport/requestJsonRpc.spec.js @@ -2,6 +2,8 @@ const requestJsonRpc = require('../../../../lib/transport/JsonRpcTransport/reque const JsonRpcError = require('../../../../lib/transport/JsonRpcTransport/errors/JsonRpcError'); const WrongHttpCodeError = require('../../../../lib/transport/JsonRpcTransport/errors/WrongHttpCodeError'); +const nodeOnlyIt = typeof window === 'undefined' ? it : it.skip; + describe('requestJsonRpc', () => { let protocol; let host; @@ -73,7 +75,7 @@ describe('requestJsonRpc', () => { expect(result).to.equal('passed'); }); - it('should make https rpc request with self-signed certificate and return result', async () => { + nodeOnlyIt('should pass an undici Agent that skips TLS verification when selfSigned is true', async () => { protocol = 'https'; selfSigned = true; @@ -96,6 +98,61 @@ describe('requestJsonRpc', () => { ); expect(result).to.equal('passed'); + + // Verify fetch was actually given a dispatcher that disables TLS verification. + // Without this, the selfSigned flag would be silently inert in Node. + // eslint-disable-next-line + const [, requestOptions] = fetch.firstCall.args; + expect(requestOptions.dispatcher).to.exist(); + }); + + nodeOnlyIt('should reuse a single undici Agent across multiple self-signed calls (no socket leak)', async () => { + protocol = 'https'; + selfSigned = true; + + // Each fetch call gets a fresh Response (body can only be read once). + // eslint-disable-next-line + fetch.callsFake(() => Promise.resolve(new Response( + JSON.stringify({ result: 'passed', error: null }), + { status: 200 }, + ))); + + await requestJsonRpc(protocol, host, port, selfSigned, 'a', params, { timeout }); + await requestJsonRpc(protocol, host, port, selfSigned, 'b', params, { timeout }); + + // eslint-disable-next-line + const firstDispatcher = fetch.firstCall.args[1].dispatcher; + // eslint-disable-next-line + const secondDispatcher = fetch.secondCall.args[1].dispatcher; + expect(firstDispatcher).to.exist(); + expect(secondDispatcher).to.equal(firstDispatcher); + }); + + it('should not pass a dispatcher when selfSigned is false', async () => { + protocol = 'https'; + selfSigned = false; + + // eslint-disable-next-line + fetch.resolves(new Response( + JSON.stringify({ result: 'passed', error: null }), + { + status: 200, + }, + )); + + await requestJsonRpc( + protocol, + host, + port, + selfSigned, + 'httpsRequest', + params, + { timeout }, + ); + + // eslint-disable-next-line + const [, requestOptions] = fetch.firstCall.args; + expect(requestOptions.dispatcher).to.equal(undefined); }); it('should throw WrongHttpCodeError if response status is not 200', async () => { diff --git a/yarn.lock b/yarn.lock index 57d28cca8c9..30cd9fc5881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,7 +1523,6 @@ __metadata: assert-browserify: "npm:^2.0.0" babel-loader: "npm:^9.1.3" browserify-zlib: "npm:^0.2.0" - bs58: "npm:^4.0.1" buffer: "npm:^6.0.3" cbor: "npm:^8.0.0" chai: "npm:^4.3.10" @@ -1542,25 +1541,21 @@ __metadata: karma-mocha: "npm:^2.0.1" karma-mocha-reporter: "npm:^2.2.5" karma-webpack: "npm:^5.0.0" - lodash: "npm:^4.17.23" mocha: "npm:^11.1.0" - node-fetch: "npm:^2.6.7" - node-inspect-extracted: "npm:^1.0.8" nyc: "npm:^15.1.0" os-browserify: "npm:^0.3.0" path-browserify: "npm:^1.0.1" process: "npm:^0.11.10" - setimmediate: "npm:^1.0.5" sinon: "npm:^18.0.1" sinon-chai: "npm:^3.7.0" stream-browserify: "npm:^3.0.0" string_decoder: "npm:^1.3.0" + undici: "npm:^6.0.0" url: "npm:^0.11.3" util: "npm:^0.12.4" wasm-x11-hash: "npm:~0.0.2" webpack: "npm:^5.104.0" webpack-cli: "npm:^4.9.1" - winston: "npm:^3.2.1" languageName: unknown linkType: soft @@ -17867,6 +17862,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.0.0": + version: 6.25.0 + resolution: "undici@npm:6.25.0" + checksum: 10/a475e45da3e1d1073283bb70531666f09a432eabff2b857bd7063d469a1ee1486192ff61dc0dadbb526673ce1120fee14d66a59b6b17d1e0bd3a4d5f0a52d0a6 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"