This commit is contained in:
Rodrigo Bonilla 2026-08-13 11:34:31 +07:00 committed by GitHub
commit 7edd45ddbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2,11 +2,22 @@ const http = require('http');
const https = require('https');
import { readFileSync } from 'fs';
const RETRY_SAFE_RPCS = new Set([
'getbestblockhash', 'getblock', 'getblockchaininfo', 'getblockcount',
'getblockhash', 'getblockheader', 'getblockstats', 'getchaintips',
'getdifficulty', 'getindexinfo', 'getmempoolancestors', 'getmempoolentry',
'getmempoolinfo', 'getnetworkhashps', 'getnetworkinfo', 'getrawmempool',
'getrawtransaction', 'gettxout', 'gettxoutsetinfo',
'decoderawtransaction', 'testmempoolaccept', 'validateaddress',
]);
const JsonRPC = function (opts) {
// @ts-ignore
this.opts = opts || {};
// @ts-ignore
this.http = this.opts.ssl ? https : http;
// @ts-ignore
this.agent = new this.http.Agent({ keepAlive: true, maxSockets: 16 }); // We match the max sockets to bitcoin core's default max threads on v31
};
JsonRPC.prototype.call = function (method, params) {
@ -46,7 +57,7 @@ JsonRPC.prototype.call = function (method, params) {
'Host': this.opts.host || 'localhost',
'Content-Length': requestJSON.length
},
agent: false,
agent: this.agent,
rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false
};
@ -67,110 +78,122 @@ JsonRPC.prototype.call = function (method, params) {
requestOptions.auth = this.opts.user + ':' + this.opts.pass;
}
// Now we'll make a request to the server
let cbCalled = false;
const request = this.http.request(requestOptions);
const retrySafe = (Array.isArray(method) ? method.map((m) => m.method) : [method]).every((m) => RETRY_SAFE_RPCS.has(String(m).toLowerCase()));
// start request timeout timer
const reqTimeout = setTimeout(function () {
if (cbCalled) {return;}
cbCalled = true;
request.abort();
const err = new Error('ETIMEDOUT');
// @ts-ignore
err.code = 'ETIMEDOUT';
reject(err);
}, this.opts.timeout || 30000);
// set additional timeout on socket in case of remote freeze after sending headers
request.setTimeout(this.opts.timeout || 30000, function () {
if (cbCalled) {return;}
cbCalled = true;
request.abort();
const err = new Error('ESOCKETTIMEDOUT');
// @ts-ignore
err.code = 'ESOCKETTIMEDOUT';
reject(err);
});
request.on('error', function (err) {
if (cbCalled) {return;}
cbCalled = true;
clearTimeout(reqTimeout);
reject(err);
});
request.on('response', (response) => {
clearTimeout(reqTimeout);
// We need to buffer the response chunks in a nonblocking way.
let buffer = '';
response.on('data', function (chunk) {
buffer = buffer + chunk;
});
// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
response.on('end', () => {
let err;
const attempt = (canRetry: boolean) => {
// Now we'll make a request to the server
let cbCalled = false;
const request = this.http.request(canRetry ? requestOptions : {...requestOptions, agent: false});
// start request timeout timer
const reqTimeout = setTimeout(function () {
if (cbCalled) {return;}
cbCalled = true;
request.abort();
const err = new Error('ETIMEDOUT');
// @ts-ignore
err.code = 'ETIMEDOUT';
reject(err);
}, this.opts.timeout || 30000);
try {
var decoded = JSON.parse(buffer);
} catch (e) {
// if we authenticated using a cookie and it failed, read the cookie file again
if (
response.statusCode === 401 /* Unauthorized */ &&
this.opts.cookie
) {
this.cachedCookie = undefined;
}
// set additional timeout on socket in case of remote freeze after sending headers
request.setTimeout(this.opts.timeout || 30000, function () {
if (cbCalled) {return;}
cbCalled = true;
request.abort();
const err = new Error('ESOCKETTIMEDOUT');
// @ts-ignore
err.code = 'ESOCKETTIMEDOUT';
reject(err);
});
if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode);
err.code = -32602;
reject(err);
} else {
err = new Error('Problem parsing JSON response from server');
err.code = -32603;
reject(err);
}
request.on('error', (err) => {
if (cbCalled) {return;}
cbCalled = true;
clearTimeout(reqTimeout);
// bitcoind may close an idle keep-alive socket at the same moment we reuse it;
// retry once on a fresh connection, only for that specific race
if (canRetry && retrySafe && err.code === 'ECONNRESET' && request.reusedSocket) {
attempt(false);
return;
}
reject(err);
});
if (!Array.isArray(decoded)) {
decoded = [decoded];
}
request.on('response', (response) => {
clearTimeout(reqTimeout);
// iterate over each response, normally there will be just one
// unless a batch rpc call response is being processed
decoded.forEach(function (decodedResponse, i) {
if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) {
if (reject) {
err = new Error(decodedResponse.error.message || '');
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code;
}
reject(err);
}
} else if (decodedResponse.hasOwnProperty('result')) {
// @ts-ignore
resolve(decodedResponse.result, response.headers);
} else {
if (reject) {
err = new Error(decodedResponse.error.message || '');
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code;
}
// We need to buffer the response chunks in a nonblocking way.
const chunks: Buffer[] = [];
response.on('data', function (chunk) {
chunks.push(chunk);
});
// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
response.on('end', () => {
let err;
if (cbCalled) {return;}
cbCalled = true;
try {
const buffer = Buffer.concat(chunks).toString('utf8');
var decoded = JSON.parse(buffer);
} catch (e) {
// if we authenticated using a cookie and it failed, read the cookie file again
if (
response.statusCode === 401 /* Unauthorized */ &&
this.opts.cookie
) {
this.cachedCookie = undefined;
}
if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode);
err.code = -32602;
reject(err);
} else {
err = new Error('Problem parsing JSON response from server');
err.code = -32603;
reject(err);
}
return;
}
if (!Array.isArray(decoded)) {
decoded = [decoded];
}
// iterate over each response, normally there will be just one
// unless a batch rpc call response is being processed
decoded.forEach(function (decodedResponse, i) {
if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) {
if (reject) {
err = new Error(decodedResponse.error.message || '');
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code;
}
reject(err);
}
} else if (decodedResponse.hasOwnProperty('result')) {
resolve(decodedResponse.result);
} else {
if (reject) {
err = new Error(decodedResponse.error.message || '');
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code;
}
reject(err);
}
}
});
});
});
});
request.end(requestJSON);
request.end(requestJSON);
};
attempt(true);
});
};