@dashevo/dapi-client 0.24.0-dev.6 → 0.25.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,18 @@
1
1
  /**
2
- * @returns {{coreChainLockedHeight: number, height: number}}
2
+ * @returns {{
3
+ * coreChainLockedHeight: number,
4
+ * height: number,
5
+ * signature: Buffer,
6
+ * protocolVersion: number,
7
+ * timeMs: number,
8
+ * }}
3
9
  */
4
10
  function getMetadataFixture() {
5
11
  return {
6
12
  height: 10,
7
13
  coreChainLockedHeight: 42,
14
+ timeMs: new Date().getTime(),
15
+ protocolVersion: 1,
8
16
  };
9
17
  }
10
18
 
@@ -2,14 +2,15 @@
2
2
  * @returns {{
3
3
  * merkleProof: Buffer,
4
4
  * signature: Buffer,
5
- * signatureLLMQHash: Buffer
5
+ * quorumHash: Buffer
6
6
  * }}
7
7
  */
8
8
  function getProofFixture() {
9
9
  return {
10
- signatureLLMQHash: Buffer.from('AQEBAQEBAQEBAQEB', 'base64'),
10
+ quorumHash: Buffer.from('AQEBAQEBAQEBAQEB', 'base64'),
11
11
  signature: Buffer.from('AgICAgICAgICAgIC', 'base64'),
12
12
  merkleProof: Buffer.from('0100000001f0faf5f55674905a68eba1be2f946e667c1cb5010101', 'hex'),
13
+ round: 42,
13
14
  };
14
15
  }
15
16
 
@@ -0,0 +1,54 @@
1
+ const EventEmitter = require('events');
2
+
3
+ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
4
+ constructor(sinon) {
5
+ super();
6
+
7
+ sinon.spy(this, 'on');
8
+ sinon.spy(this, 'removeListener');
9
+ sinon.spy(this, 'emit');
10
+ sinon.spy(this, 'removeAllListeners');
11
+ sinon.spy(this, 'cancel');
12
+
13
+ this.errored = false;
14
+ }
15
+
16
+ cancel() {
17
+ if (!this.errored) {
18
+ const err = new Error('CANCELED_ON_CLIENT');
19
+ err.code = 1;
20
+ this.emit('error', err);
21
+ }
22
+ }
23
+
24
+ emit(event, data) {
25
+ if (event === 'error') {
26
+ this.errored = true;
27
+ }
28
+ super.emit(event, data);
29
+ }
30
+
31
+ /**
32
+ * @param headers {BlockHeader[]}
33
+ */
34
+ sendHeaders(headers) {
35
+ this.emit('data', {
36
+ getBlockHeaders: () => ({
37
+ getHeadersList() {
38
+ return headers.map((header) => header.toBuffer());
39
+ },
40
+ }),
41
+ });
42
+ }
43
+
44
+ errorHandler(e) {
45
+ this.emit('error', e);
46
+ }
47
+
48
+ end() {
49
+ this.emit('end');
50
+ this.removeAllListeners();
51
+ }
52
+ }
53
+
54
+ module.exports = BlockHeadersWithChainLocksStreamMock;
@@ -0,0 +1,55 @@
1
+ const X11 = require('wasm-x11-hash');
2
+ const { BlockHeader, configure } = require('@dashevo/dashcore-lib');
3
+ const { genesis } = require('@dashevo/dash-spv');
4
+
5
+ const getRoot = (network) => {
6
+ switch (network) {
7
+ case 'testnet':
8
+ return genesis.getTestnetGenesis();
9
+ case 'devnet':
10
+ return genesis.getDevnetGenesis();
11
+ case 'regtest':
12
+ return genesis.getRegtestGenesis();
13
+ default:
14
+ break;
15
+ }
16
+
17
+ return null;
18
+ };
19
+
20
+ const BLOCK_TIME = 2.5 * 60;
21
+
22
+ let x11;
23
+
24
+ const mockHeadersChain = async (network, length, root) => {
25
+ if (!x11) {
26
+ x11 = await X11();
27
+ // Configure Dashcore lib to operate with wasm x11
28
+ configure({
29
+ x11hash: x11,
30
+ });
31
+ }
32
+
33
+ const rootHeader = root || getRoot(network);
34
+
35
+ const chain = [rootHeader];
36
+
37
+ let prevHeader = rootHeader;
38
+ for (let i = 0; i < length - 1; i += 1) {
39
+ const header = new BlockHeader({
40
+ version: prevHeader.version,
41
+ prevHash: Buffer.from(prevHeader.hash, 'hex').reverse(),
42
+ merkleRoot: Buffer.alloc(32),
43
+ time: prevHeader.time + BLOCK_TIME,
44
+ bits: prevHeader.bits,
45
+ nonce: 3861367235,
46
+ });
47
+
48
+ chain.push(header);
49
+ prevHeader = header;
50
+ }
51
+
52
+ return chain;
53
+ };
54
+
55
+ module.exports = mockHeadersChain;
@@ -83,7 +83,7 @@ class GrpcTransport {
83
83
  throw error;
84
84
  }
85
85
 
86
- const responseError = this.createGrpcTransportError(error, address);
86
+ const responseError = await this.createGrpcTransportError(error, address);
87
87
 
88
88
  if (!(responseError instanceof RetriableResponseError)) {
89
89
  throw responseError;
@@ -133,20 +133,15 @@ class GrpcTransport {
133
133
  */
134
134
  makeGrpcUrlFromAddress(address) {
135
135
  let protocol = address.getProtocol();
136
- let port = address.getHttpPort();
137
-
138
136
  // For NodeJS Client
139
137
  if (typeof process !== 'undefined'
140
138
  && process.versions != null
141
- && process.versions.node != null) {
142
- port = address.getGrpcPort();
143
-
144
- if (address.isSelfSignedCertificateAllowed()) {
145
- protocol = 'http';
146
- }
139
+ && process.versions.node != null
140
+ && address.isSelfSignedCertificateAllowed()) {
141
+ protocol = 'http';
147
142
  }
148
143
 
149
- return `${protocol}://${address.getHost()}:${port}`;
144
+ return `${protocol}://${address.getHost()}:${address.getPort()}`;
150
145
  }
151
146
  }
152
147
 
@@ -1,9 +1,12 @@
1
1
  const cbor = require('cbor');
2
2
 
3
- const createConsensusError = require('@dashevo/dpp/lib/errors/consensus/createConsensusError');
4
3
  const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
5
- const { Metadata } = require('@grpc/grpc-js/build/src/metadata');
4
+ const { parseMetadata } = require('@dashevo/dapi-grpc');
6
5
 
6
+ const {
7
+ deserializeConsensusError,
8
+ } = require('@dashevo/wasm-dpp');
9
+ const { default: loadWasmDpp } = require('@dashevo/wasm-dpp');
7
10
  const NotFoundError = require('./errors/NotFoundError');
8
11
  const TimeoutError = require('./errors/TimeoutError');
9
12
  const ResponseError = require('../errors/response/ResponseError');
@@ -43,22 +46,16 @@ const errorClasses = {
43
46
  * @param {DAPIAddress} dapiAddress
44
47
  * @returns {ResponseError}
45
48
  */
46
- function createGrpcTransportError(grpcError, dapiAddress) {
49
+ async function createGrpcTransportError(grpcError, dapiAddress) {
50
+ await loadWasmDpp();
51
+
47
52
  // Extract error code and data
48
53
  let data = {};
49
54
  let { code } = grpcError;
50
55
 
51
56
  const message = grpcError.details || grpcError.message;
52
57
 
53
- let metadata = {};
54
-
55
- if (grpcError.metadata) {
56
- // In cases of gRPC-Web client we get plain map instead of Metadata instance
57
- metadata = grpcError.metadata;
58
- if (grpcError.metadata instanceof Metadata) {
59
- metadata = grpcError.metadata.getMap();
60
- }
61
- }
58
+ const metadata = parseMetadata(grpcError.metadata) || {};
62
59
 
63
60
  // Error data
64
61
  const driveErrorData = metadata['drive-error-data-bin'];
@@ -122,9 +119,9 @@ function createGrpcTransportError(grpcError, dapiAddress) {
122
119
 
123
120
  // DPP consensus errors
124
121
  if (code >= 1000 && code < 5000) {
125
- const consensusError = createConsensusError(code, data.arguments || []);
122
+ const consensusError = deserializeConsensusError(data.serializedError || []);
126
123
 
127
- delete data.arguments;
124
+ delete data.serializedError;
128
125
 
129
126
  return new InvalidRequestDPPError(consensusError, data, dapiAddress);
130
127
  }
@@ -67,7 +67,7 @@ class JsonRpcTransport {
67
67
  const result = await this.requestJsonRpc(
68
68
  address.getProtocol(),
69
69
  address.getHost(),
70
- address.getHttpPort(),
70
+ address.getPort(),
71
71
  address.isSelfSignedCertificateAllowed(),
72
72
  method,
73
73
  params,
@@ -1,6 +1,4 @@
1
- const axios = require('axios');
2
1
  const https = require('https');
3
-
4
2
  const JsonRpcError = require('./errors/JsonRpcError');
5
3
  const WrongHttpCodeError = require('./errors/WrongHttpCodeError');
6
4
 
@@ -25,11 +23,6 @@ async function requestJsonRpc(protocol, host, port, selfSigned, method, params,
25
23
  id: 1,
26
24
  };
27
25
 
28
- const postOptions = {};
29
- if (options.timeout !== undefined) {
30
- postOptions.timeout = options.timeout;
31
- }
32
-
33
26
  const requestInfo = {
34
27
  protocol,
35
28
  host,
@@ -40,40 +33,44 @@ async function requestJsonRpc(protocol, host, port, selfSigned, method, params,
40
33
  options,
41
34
  };
42
35
 
43
- let response;
36
+ const requestOptions = {
37
+ method: 'POST',
38
+ body: JSON.stringify(payload),
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ };
43
+
44
+ let requestTimeoutId;
45
+ if (options.timeout) {
46
+ const controller = new AbortController();
47
+ requestTimeoutId = setTimeout(() => controller.abort(), options.timeout);
48
+ Object.assign(requestOptions, { signal: controller.signal });
49
+ }
44
50
 
45
- const config = { timeout: options.timeout };
46
51
  // For NodeJS Client
47
52
  if (typeof process !== 'undefined'
48
53
  && process.versions != null
49
54
  && process.versions.node != null
50
55
  && protocol === 'https'
51
56
  && selfSigned) {
52
- config.httpsAgent = new https.Agent({
57
+ requestOptions.agent = new https.Agent({
53
58
  rejectUnauthorized: false,
54
59
  });
55
60
  }
56
61
 
57
- try {
58
- response = await axios.post(
59
- url,
60
- payload,
61
- config,
62
- );
63
- } catch (error) {
64
- if (error.response && error.response.status >= 500) {
65
- throw new WrongHttpCodeError(requestInfo, error.response.status, error.response.statusText);
66
- }
62
+ // eslint-disable-next-line
63
+ const response = await fetch(url, requestOptions);
67
64
 
68
- throw error;
65
+ if (typeof requestTimeoutId !== 'undefined') {
66
+ clearTimeout(requestTimeoutId);
69
67
  }
68
+ const data = await response.json();
70
69
 
71
- if (response.status !== 200) {
72
- throw new WrongHttpCodeError(requestInfo, response.status, response.statusMessage);
70
+ if (!response.ok) {
71
+ throw new WrongHttpCodeError(requestInfo, response.status, response.statusText);
73
72
  }
74
73
 
75
- const { data } = response;
76
-
77
74
  if (data.error) {
78
75
  throw new JsonRpcError(requestInfo, data.error);
79
76
  }
@@ -0,0 +1,284 @@
1
+ const EventEmitter = require('events');
2
+ const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
3
+ const wait = require('../utils/wait');
4
+
5
+ /**
6
+ * @typedef ReconnectableStreamOptions
7
+ * @property {number} [autoReconnectInterval]
8
+ * interval in MS to perform auto reconnect
9
+ * @property {number} [maxRetriesOnError]
10
+ * maximum amount of retry attempts on error happens. If set to -1, retries are infinite
11
+ * @property {number} [retryOnErrorDelay]
12
+ * delay in MS to perform retry after an error
13
+ */
14
+ const defaultOptions = {
15
+ // TODO: manage timeout according to the Nginx setting of the node
16
+ autoReconnectInterval: 50000,
17
+ maxRetriesOnError: 10,
18
+ retryOnErrorDelay: 1000,
19
+ };
20
+
21
+ const EVENTS = {
22
+ DATA: 'data',
23
+ ERROR: 'error',
24
+ END: 'end',
25
+ BEFORE_RECONNECT: 'beforeReconnect',
26
+ };
27
+
28
+ /**
29
+ * A wrapper around grpc-js/grpc-web streams that provides auto-reconnect
30
+ * and retry on error functionality
31
+ * - Auto reconnect is needed to not hang on one node for too long.
32
+ * It happens within provided interval
33
+ * - Retry on error logic performs retry attempts until it reaches it's limit.
34
+ * After reaching limit, ReconnectableStream emits an error.
35
+ */
36
+ class ReconnectableStream extends EventEmitter {
37
+ /**
38
+ * Helper that wraps stream creation function and performs auto connection
39
+ *
40
+ * @param {Function} createStreamFunction - function returning grpc-js/grpc-web stream
41
+ * @param {ReconnectableStreamOptions} options
42
+ * @returns {function(...[*]): Promise<ReconnectableStream>}
43
+ */
44
+ static create(createStreamFunction, options) {
45
+ return async (...args) => {
46
+ const reconnectableStream = new ReconnectableStream(createStreamFunction, options);
47
+ await reconnectableStream.connect(...args);
48
+ return reconnectableStream;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {Function} createStream - function returning grpc-js/grpc-web stream
54
+ * @param {ReconnectableStreamOptions} options
55
+ */
56
+ constructor(createStream, options = {}) {
57
+ super();
58
+
59
+ const opts = { ...defaultOptions, ...options };
60
+
61
+ /**
62
+ * Auto-reconnect interval in millisecond
63
+ * It is needed to automatically reconnect to another DAPI node
64
+ *
65
+ * @type {number}
66
+ */
67
+ this.autoReconnectInterval = opts.autoReconnectInterval;
68
+ this.retryOnErrorDelay = opts.retryOnErrorDelay;
69
+
70
+ /**
71
+ * Max amount of retries on error
72
+ */
73
+ this.maxRetriesOnError = opts.maxRetriesOnError;
74
+
75
+ /**
76
+ * Current amount of retries on error
77
+ * (Does not have effect if maxRetriesOnError === -1)
78
+ */
79
+ this.retriesOnError = 0;
80
+
81
+ /**
82
+ * createStream arguments
83
+ */
84
+ this.args = null;
85
+ /**
86
+ * grpc stream
87
+ */
88
+ this.stream = null;
89
+
90
+ /**
91
+ * Function to wrap around to handle interval reconnects and error retries
92
+ */
93
+ this.createStream = createStream;
94
+
95
+ /**
96
+ * Reference to setTimeout managed by autoReconnectInterval
97
+ */
98
+ this.reconnectTimeout = null;
99
+
100
+ // For mocks
101
+ this.setTimeout = (callback, delay) => setTimeout(callback, delay);
102
+ this.clearTimeout = (reference) => clearTimeout(reference);
103
+
104
+ this.connect = this.connect.bind(this);
105
+ this.reconnect = this.reconnect.bind(this);
106
+ this.cancel = this.cancel.bind(this);
107
+ this.errorHandler = this.errorHandler.bind(this);
108
+ this.dataHandler = this.dataHandler.bind(this);
109
+ this.endHandler = this.endHandler.bind(this);
110
+ this.addListeners = this.addListeners.bind(this);
111
+ }
112
+
113
+ async connect(...args) {
114
+ // Memorize current stream args (which can be altered by beforeReconnect logic)
115
+ this.args = args;
116
+
117
+ // Create grpc stream
118
+ this.stream = await this.createStream(...this.args);
119
+
120
+ // Add event listeners
121
+ this.addListeners();
122
+
123
+ if (this.autoReconnectInterval > 0) {
124
+ this.startAutoReconnect();
125
+ }
126
+ }
127
+
128
+ startAutoReconnect() {
129
+ if (this.reconnectTimeout) {
130
+ throw new Error('Auto reconnect timeout is already running.');
131
+ }
132
+ this.reconnectTimeout = this.setTimeout(
133
+ this.reconnect,
134
+ this.autoReconnectInterval,
135
+ );
136
+ }
137
+
138
+ reconnect() {
139
+ if (this.reconnectTimeout) {
140
+ this.reconnectTimeout = null;
141
+ this.stream.cancel();
142
+
143
+ let newArgs = this.args || [];
144
+ const updateArgs = (...args) => {
145
+ newArgs = args;
146
+ };
147
+
148
+ this.emit(EVENTS.BEFORE_RECONNECT, updateArgs);
149
+ this.connect(...newArgs)
150
+ .catch((connectError) => this.emit(EVENTS.ERROR, connectError));
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Function that adds EventEmitter style listeners
156
+ * to the stream and also rewires stream cancel function
157
+ * in order to automatically unsubscribe from events
158
+ *
159
+ * @private
160
+ */
161
+ addListeners() {
162
+ this.stream.on(EVENTS.DATA, this.dataHandler);
163
+ this.stream.on(EVENTS.ERROR, this.errorHandler);
164
+ this.stream.on(EVENTS.END, this.endHandler);
165
+
166
+ const { cancel } = this.stream;
167
+
168
+ // Rewire cancel function in order to
169
+ // unsubscribe from DATA and END events.
170
+ // We don't unsubscribe from ERROR event because it
171
+ // handles `CANCELLED_ON_CLIENT` error after calling .cancel() and must stay
172
+ this.stream.cancel = () => {
173
+ this.stream.removeListener(EVENTS.DATA, this.dataHandler);
174
+ this.stream.removeListener(EVENTS.END, this.endHandler);
175
+ cancel.call(this.stream);
176
+ };
177
+ }
178
+
179
+ /**
180
+ * stream.on('data') handler
181
+ *
182
+ * @param data
183
+ */
184
+ dataHandler(data) {
185
+ this.emit(EVENTS.DATA, data);
186
+ }
187
+
188
+ /**
189
+ * stream.on('end') handler
190
+ *
191
+ * @private
192
+ */
193
+ endHandler() {
194
+ this.stopAutoReconnect();
195
+ this.stream = null;
196
+ this.emit(EVENTS.END);
197
+ }
198
+
199
+ /**
200
+ * stream.on('error') handler
201
+ *
202
+ * @private
203
+ * @param e
204
+ */
205
+ errorHandler(e) {
206
+ // In case of cancellation nothing has to happen.
207
+ // TODO: do we need to propagate GrpcErrorCodes.CANCELLED further?
208
+ if (e.code === GrpcErrorCodes.CANCELLED) {
209
+ return;
210
+ }
211
+
212
+ // Retry stream
213
+ this.retryOnError(e);
214
+ }
215
+
216
+ /**
217
+ * Manages retry logic
218
+ *
219
+ * @param e
220
+ */
221
+ retryOnError(e) {
222
+ // Stop reconnect timeout if there is one
223
+ this.stopAutoReconnect();
224
+
225
+ // Unsubscribe from events in case of the external call
226
+ this.stream.removeListener(EVENTS.END, this.endHandler);
227
+ this.stream.removeListener(EVENTS.DATA, this.dataHandler);
228
+
229
+ const canRetry = this.maxRetriesOnError === -1 // Infinite retries
230
+ || this.retriesOnError < this.maxRetriesOnError; // Or less than max limit
231
+
232
+ if (canRetry) {
233
+ // Handle 'beforeReconnect` logic
234
+ let newArgs = this.args || [];
235
+ const updateArgsBeforeReconnect = (...args) => {
236
+ newArgs = args;
237
+ };
238
+
239
+ // Emit beforeReconnect event in case parent would want to alter arguments
240
+ this.emit(EVENTS.BEFORE_RECONNECT, updateArgsBeforeReconnect);
241
+
242
+ // Wait before reconnecting
243
+ wait(this.retryOnErrorDelay)
244
+ .then(() => this.connect(...newArgs))// Reconnect with new args
245
+ .then(() => {
246
+ this.retriesOnError += 1; // Increase amount of current retries
247
+ })
248
+ .catch((connectError) => {
249
+ this.emit(EVENTS.ERROR, connectError); // Or emit error in case retry has failed
250
+ });
251
+ } else {
252
+ // Or simply emit error in case retry attempts were exhausted
253
+ this.emit(EVENTS.ERROR, e);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Stops auto reconnect timeout
259
+ *
260
+ * @private
261
+ */
262
+ stopAutoReconnect() {
263
+ if (this.reconnectTimeout) {
264
+ this.clearTimeout(this.reconnectTimeout);
265
+ this.reconnectTimeout = null;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Cancels stream.
271
+ * Returns `stream.cancel()` to handle it from parent if needed.
272
+ * (grpc-js cancel() is asynchronous and grpc-web is synchronous)
273
+ *
274
+ * @returns {*}
275
+ */
276
+ cancel() {
277
+ this.stopAutoReconnect();
278
+ return this.stream.cancel();
279
+ }
280
+ }
281
+
282
+ ReconnectableStream.EVENTS = EVENTS;
283
+
284
+ module.exports = ReconnectableStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashevo/dapi-client",
3
- "version": "0.24.0-dev.6",
3
+ "version": "0.25.0-dev.2",
4
4
  "description": "Client library used to access Dash DAPI endpoints",
5
5
  "main": "lib/index.js",
6
6
  "contributors": [
@@ -26,17 +26,18 @@
26
26
  }
27
27
  ],
28
28
  "dependencies": {
29
- "@dashevo/dapi-grpc": "0.24.0-dev.6",
30
- "@dashevo/dash-spv": "0.24.0-dev.6",
31
- "@dashevo/dashcore-lib": "~0.19.44",
32
- "@dashevo/dpp": "0.24.0-dev.6",
33
- "@dashevo/grpc-common": "0.24.0-dev.6",
34
- "@grpc/grpc-js": "^1.3.7",
35
- "axios": "^0.27.2",
29
+ "@dashevo/dapi-grpc": "0.25.0-dev.2",
30
+ "@dashevo/dash-spv": "0.25.0-dev.2",
31
+ "@dashevo/dashcore-lib": "~0.20.3",
32
+ "@dashevo/dpp": "0.25.0-dev.2",
33
+ "@dashevo/grpc-common": "0.25.0-dev.2",
34
+ "@dashevo/wasm-dpp": "0.25.0-dev.2",
36
35
  "bs58": "^4.0.1",
37
36
  "cbor": "^8.0.0",
38
- "lodash.sample": "^4.2.1",
39
- "node-inspect-extracted": "^1.0.8"
37
+ "lodash": "^4.17.21",
38
+ "node-fetch": "^2.6.7",
39
+ "node-inspect-extracted": "^1.0.8",
40
+ "wasm-x11-hash": "~0.0.2"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@babel/core": "^7.15.5",
@@ -54,6 +55,7 @@
54
55
  "eslint-plugin-import": "^2.24.2",
55
56
  "eslint-plugin-jsdoc": "^27.0.0",
56
57
  "events": "^3.3.0",
58
+ "google-protobuf": "^3.12.2",
57
59
  "karma": "^6.4.1",
58
60
  "karma-chai": "^0.1.0",
59
61
  "karma-chrome-launcher": "^3.1.0",
@@ -71,12 +73,13 @@
71
73
  "string_decoder": "^1.3.0",
72
74
  "url": "^0.11.0",
73
75
  "util": "^0.12.4",
74
- "webpack": "^5.59.1",
76
+ "webpack": "^5.76.1",
75
77
  "webpack-cli": "^4.9.1"
76
78
  },
77
79
  "files": [
78
80
  "docs",
79
- "lib"
81
+ "lib",
82
+ "polyfills"
80
83
  ],
81
84
  "scripts": {
82
85
  "build:web": "webpack",
@@ -0,0 +1,10 @@
1
+ /* eslint-disable */
2
+
3
+ const { default: fetch, Headers, Request, Response } = require('node-fetch');
4
+
5
+ if (!globalThis.fetch) {
6
+ globalThis.fetch = fetch;
7
+ globalThis.Headers = Headers;
8
+ globalThis.Request = Request;
9
+ globalThis.Response = Response;
10
+ }
File without changes