@mr-zwets/bchn-api-wrapper 1.0.1
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.
- package/CHANGELOG.md +7 -0
- package/README.md +129 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/interfaces/interfaces.d.ts +70 -0
- package/dist/interfaces/interfaces.js +1 -0
- package/dist/interfaces/restInterfaces/interfaces.d.ts +109 -0
- package/dist/interfaces/restInterfaces/interfaces.js +1 -0
- package/dist/interfaces/rpcInterfaces/blockchain.d.ts +692 -0
- package/dist/interfaces/rpcInterfaces/blockchain.js +3 -0
- package/dist/interfaces/rpcInterfaces/control.d.ts +54 -0
- package/dist/interfaces/rpcInterfaces/control.js +3 -0
- package/dist/interfaces/rpcInterfaces/generating.d.ts +17 -0
- package/dist/interfaces/rpcInterfaces/generating.js +3 -0
- package/dist/interfaces/rpcInterfaces/index.d.ts +9 -0
- package/dist/interfaces/rpcInterfaces/index.js +12 -0
- package/dist/interfaces/rpcInterfaces/mining.d.ts +131 -0
- package/dist/interfaces/rpcInterfaces/mining.js +3 -0
- package/dist/interfaces/rpcInterfaces/network.d.ts +179 -0
- package/dist/interfaces/rpcInterfaces/network.js +3 -0
- package/dist/interfaces/rpcInterfaces/rawtransactions.d.ts +283 -0
- package/dist/interfaces/rpcInterfaces/rawtransactions.js +3 -0
- package/dist/interfaces/rpcInterfaces/util.d.ts +44 -0
- package/dist/interfaces/rpcInterfaces/util.js +3 -0
- package/dist/interfaces/rpcInterfaces/wallet.d.ts +620 -0
- package/dist/interfaces/rpcInterfaces/wallet.js +3 -0
- package/dist/interfaces/rpcInterfaces/zmq.d.ts +8 -0
- package/dist/interfaces/rpcInterfaces/zmq.js +3 -0
- package/dist/restClient.d.ts +17 -0
- package/dist/restClient.js +100 -0
- package/dist/rpcClient.d.ts +12 -0
- package/dist/rpcClient.js +85 -0
- package/dist/utils/errors.d.ts +3 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/utils.d.ts +11 -0
- package/dist/utils/utils.js +49 -0
- package/package.json +40 -0
- package/src/index.ts +4 -0
- package/src/interfaces/interfaces.ts +87 -0
- package/src/interfaces/restInterfaces/interfaces.ts +117 -0
- package/src/interfaces/rpcInterfaces/blockchain.ts +759 -0
- package/src/interfaces/rpcInterfaces/control.ts +62 -0
- package/src/interfaces/rpcInterfaces/generating.ts +21 -0
- package/src/interfaces/rpcInterfaces/index.ts +14 -0
- package/src/interfaces/rpcInterfaces/mining.ts +143 -0
- package/src/interfaces/rpcInterfaces/network.ts +195 -0
- package/src/interfaces/rpcInterfaces/rawtransactions.ts +314 -0
- package/src/interfaces/rpcInterfaces/util.ts +52 -0
- package/src/interfaces/rpcInterfaces/wallet.ts +674 -0
- package/src/interfaces/rpcInterfaces/zmq.ts +11 -0
- package/src/restClient.ts +119 -0
- package/src/rpcClient.ts +93 -0
- package/src/utils/errors.ts +6 -0
- package/src/utils/utils.ts +55 -0
- package/test/restClient.test.ts +32 -0
- package/test/rpcClient.test.ts +115 -0
- package/test/setupTests.ts +54 -0
- package/test/tsconfig.json +4 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { RestClientConfig, formatOptions, ResponseType } from "./interfaces/interfaces.js";
|
|
2
|
+
import type {
|
|
3
|
+
BlockInfoNoTxDetails,
|
|
4
|
+
BlockInfoTxDetails,
|
|
5
|
+
ChainInfo,
|
|
6
|
+
HeaderInfo,
|
|
7
|
+
MempoolContent,
|
|
8
|
+
MempoolInfo,
|
|
9
|
+
TxDetails,
|
|
10
|
+
UtxosInfo
|
|
11
|
+
} from "./interfaces/restInterfaces/interfaces.js";
|
|
12
|
+
import { validateUrl } from "./utils/utils.js";
|
|
13
|
+
|
|
14
|
+
export class BchnRestClient {
|
|
15
|
+
private baseUrl: string;
|
|
16
|
+
private timeoutMs: number;
|
|
17
|
+
private logger: Console;
|
|
18
|
+
|
|
19
|
+
constructor(config: RestClientConfig) {
|
|
20
|
+
this.baseUrl = validateUrl(config.url);
|
|
21
|
+
this.timeoutMs = config.timeoutMs ?? 5000;
|
|
22
|
+
this.logger = config.logger ?? console;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async fetchFromNode<T, TFormat extends formatOptions>(
|
|
26
|
+
endpoint: string,
|
|
27
|
+
format: TFormat
|
|
28
|
+
): Promise<ResponseType<TFormat, T>> {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(`${this.baseUrl}/rest/${endpoint}`, {
|
|
31
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Error fetching data from ${endpoint}: ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (format === 'json') {
|
|
39
|
+
return await response.json() as ResponseType<TFormat, T>;
|
|
40
|
+
} else {
|
|
41
|
+
return await response.text() as ResponseType<TFormat, T>; // For 'bin' and 'hex', return raw text
|
|
42
|
+
}
|
|
43
|
+
} catch(error) {
|
|
44
|
+
let errorMessage: string | undefined
|
|
45
|
+
|
|
46
|
+
// Check if the error is due to timeout or other fetch-related issues
|
|
47
|
+
if (typeof error === 'string') {
|
|
48
|
+
errorMessage = error;
|
|
49
|
+
this.logger.error(error);
|
|
50
|
+
} else if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
51
|
+
// If error is an instance DOMException TimeoutError
|
|
52
|
+
errorMessage = 'Request timed out';
|
|
53
|
+
this.logger.error(`Request to ${endpoint} timed out after ${this.timeoutMs} ms`);
|
|
54
|
+
} else {
|
|
55
|
+
this.logger.error(`Unknown error occurred during request to ${endpoint}`);
|
|
56
|
+
throw new Error(`Unknown error: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Always rethrow the error after logging
|
|
60
|
+
throw new Error(errorMessage);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get transaction details by transaction hash
|
|
65
|
+
async getTransaction<TFormat extends formatOptions = 'json'>(
|
|
66
|
+
txid: string, format:TFormat = 'json' as TFormat
|
|
67
|
+
) {
|
|
68
|
+
return this.fetchFromNode<TxDetails, TFormat>(`tx/${txid}.${format}`, format);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// getBlock overload signatures
|
|
72
|
+
// This is needed so the getBlock return type can depend on the 'includeTxDetails' boolean flag
|
|
73
|
+
async getBlock<TFormat extends formatOptions = 'json'>(
|
|
74
|
+
blockhash: string, includeTxDetails: true, format?:TFormat
|
|
75
|
+
): Promise<TFormat extends 'json' ? BlockInfoTxDetails : string>;
|
|
76
|
+
|
|
77
|
+
async getBlock<TFormat extends formatOptions = 'json'>(
|
|
78
|
+
blockhash: string, includeTxDetails: false, format?:TFormat
|
|
79
|
+
): Promise<TFormat extends 'json' ? BlockInfoNoTxDetails : string>;
|
|
80
|
+
|
|
81
|
+
// getBlock Implementation
|
|
82
|
+
async getBlock<TFormat extends formatOptions = 'json'>(
|
|
83
|
+
blockhash: string, includeTxDetails: boolean, format:TFormat = 'json' as TFormat
|
|
84
|
+
): Promise<any> {
|
|
85
|
+
const path = includeTxDetails ? 'block' : 'block/notxdetails';
|
|
86
|
+
return this.fetchFromNode(`${path}/${blockhash}.${format}`, format);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get block headers starting from a specific block hash
|
|
90
|
+
async getBlockHeaders<TFormat extends formatOptions = 'json'>(
|
|
91
|
+
count: number, blockhash: string, format:TFormat = 'json' as TFormat
|
|
92
|
+
) {
|
|
93
|
+
return this.fetchFromNode<HeaderInfo, TFormat>(`headers/${count}/${blockhash}.${format}`, format);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get chain info (chain state details)
|
|
97
|
+
async getChainInfo() {
|
|
98
|
+
return this.fetchFromNode<ChainInfo, 'json'>('chaininfo.json', 'json');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Query UTXO set based on specific outpoints (txid and vout)
|
|
102
|
+
async getUTXOs<TFormat extends formatOptions = 'json'>(
|
|
103
|
+
checkmempool: boolean, outpoints: string[], format:TFormat = 'json' as TFormat
|
|
104
|
+
) {
|
|
105
|
+
const path = (checkmempool ? 'checkmempool/' : '') + outpoints.join('/');
|
|
106
|
+
const endpoint = `getutxos/${path}.${format}`;
|
|
107
|
+
return this.fetchFromNode<UtxosInfo, TFormat>(endpoint, format);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Get mempool information (basic)
|
|
111
|
+
async getMempoolInfo() {
|
|
112
|
+
return this.fetchFromNode<MempoolInfo, 'json'>('mempool/info.json', 'json');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get mempool contents (transactions currently in the mempool)
|
|
116
|
+
async getMempoolContents() {
|
|
117
|
+
return this.fetchFromNode<MempoolContent, 'json'>('mempool/contents.json', 'json');
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/rpcClient.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { RpcClientConfig, RpcRequest } from "./interfaces/interfaces.js";
|
|
2
|
+
import { getRandomId, validateAndConstructUrl } from "./utils/utils.js";
|
|
3
|
+
import { RetryLimitExceededError } from "./utils/errors.js";
|
|
4
|
+
|
|
5
|
+
export class BchnRpcClient {
|
|
6
|
+
private url: string
|
|
7
|
+
private rpcUser: string
|
|
8
|
+
private rpcPassword: string
|
|
9
|
+
|
|
10
|
+
private maxRetries: number // number of retries before throwing an exception
|
|
11
|
+
private retryDelayMs: number // delay between each retry
|
|
12
|
+
private logger: typeof console // logger
|
|
13
|
+
private timeoutMs: number // max timeout for each retry
|
|
14
|
+
|
|
15
|
+
constructor(config: RpcClientConfig){
|
|
16
|
+
this.url = validateAndConstructUrl(config)
|
|
17
|
+
if(!config.rpcUser) throw new Error('Need to provide rpcUser in config')
|
|
18
|
+
if(!config.rpcPassword) throw new Error('Need to provide rpcPassword in config')
|
|
19
|
+
this.rpcUser = config.rpcUser;
|
|
20
|
+
this.rpcPassword= config.rpcPassword;
|
|
21
|
+
|
|
22
|
+
// optional config
|
|
23
|
+
this.maxRetries = config.maxRetries ?? 0;
|
|
24
|
+
this.retryDelayMs= config.retryDelayMs ?? 100;
|
|
25
|
+
this.logger = config.logger ?? console;
|
|
26
|
+
this.timeoutMs = config.timeoutMs ?? 5000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async request<T extends RpcRequest>(
|
|
30
|
+
endpoint: T['method'],
|
|
31
|
+
...params: T['params']
|
|
32
|
+
): Promise<T['response']> {
|
|
33
|
+
const auth = Buffer.from(`${this.rpcUser}:${this.rpcPassword}`).toString('base64');
|
|
34
|
+
|
|
35
|
+
// Retry logic
|
|
36
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
// Send the request with a timeout and retries
|
|
39
|
+
const response = await fetch(this.url, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Authorization': `Basic ${auth}`
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: endpoint, params, id: getRandomId() }),
|
|
46
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
|
|
51
|
+
// Handle response errors
|
|
52
|
+
if (!response.ok || result.error) {
|
|
53
|
+
throw new Error(`Error: ${result.error?.message || response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result.result as T['response']; // Return the result if successful
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
let errorMessage: string | undefined
|
|
60
|
+
|
|
61
|
+
// Check if the error is due to timeout or other fetch-related issues
|
|
62
|
+
if(typeof error == 'string'){
|
|
63
|
+
errorMessage = error
|
|
64
|
+
this.logger.error(error)
|
|
65
|
+
}
|
|
66
|
+
else if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
67
|
+
// If error is an instance DOMException TimeoutError
|
|
68
|
+
errorMessage = error.message
|
|
69
|
+
this.logger.error(`Request timed out after ${this.timeoutMs} ms`);
|
|
70
|
+
}
|
|
71
|
+
else if (error instanceof Error) {
|
|
72
|
+
// If error is an instance of Error, you can safely access its properties
|
|
73
|
+
errorMessage = error.message
|
|
74
|
+
this.logger.error(`Request failed with error: ${error.message}`);
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Retry if allowed
|
|
79
|
+
if (attempt < this.maxRetries) {
|
|
80
|
+
this.logger.warn(`Retrying request... (${attempt + 1}/${this.maxRetries})`);
|
|
81
|
+
await new Promise(res => setTimeout(res, this.retryDelayMs)); // Wait before retrying
|
|
82
|
+
} else {
|
|
83
|
+
// If no retries are left, throw the final error
|
|
84
|
+
throw new RetryLimitExceededError(`Request failed after ${this.maxRetries + 1} attempts: ${errorMessage}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// This line ensures TypeScript is satisfied that a value will always be returned, but
|
|
90
|
+
// it should never be reached if the retries fail, as the last attempt should throw an error.
|
|
91
|
+
throw new Error('Request failed unexpectedly');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RpcClientConfig,
|
|
3
|
+
RpcClientUrlConfig,
|
|
4
|
+
RpcClientHostConfig,
|
|
5
|
+
} from "../interfaces/interfaces.js";
|
|
6
|
+
|
|
7
|
+
export function getRandomId(): number {
|
|
8
|
+
return Math.floor(Math.random() * 100000);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// A utility function to validate and construct the URL from the RpcClientConfig object
|
|
12
|
+
export function validateAndConstructUrl(config: RpcClientConfig): string {
|
|
13
|
+
let url: string
|
|
14
|
+
if (isUrlConfig(config)) {
|
|
15
|
+
url = validateUrl(config.url)
|
|
16
|
+
} else if (isHostConfig(config)) {
|
|
17
|
+
const { protocol, host, port } = config;
|
|
18
|
+
if (protocol !== 'http' && protocol !== 'https') {
|
|
19
|
+
throw new Error("Protocol should be 'http' or 'https'");
|
|
20
|
+
}
|
|
21
|
+
url = validateUrl(`${protocol}://${host}:${port}`)
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error('Invalid configuration: Either provide the url or protocol/host/port');
|
|
24
|
+
}
|
|
25
|
+
return url
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A utility function to validate a URL
|
|
29
|
+
export function validateUrl(url: string) {
|
|
30
|
+
if(!url) throw new Error('URL is required');
|
|
31
|
+
try {
|
|
32
|
+
new URL(url);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new Error('Invalid URL format');
|
|
35
|
+
}
|
|
36
|
+
return url
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Type guard to check if the config is RpcClientUrlConfig
|
|
40
|
+
function isUrlConfig(config: RpcClientConfig): config is RpcClientUrlConfig {
|
|
41
|
+
return 'url' in config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Type guard to check if the config is RpcClientHostConfig
|
|
45
|
+
function isHostConfig(config: RpcClientConfig): config is RpcClientHostConfig {
|
|
46
|
+
return 'protocol' in config && 'hostname' in config && 'port' in config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export enum BchnNetworkPort {
|
|
50
|
+
Mainnet = 8332,
|
|
51
|
+
Testnet = 18332,
|
|
52
|
+
Testnet4 = 28332,
|
|
53
|
+
Scalenet = 38332,
|
|
54
|
+
Regtest = 18443
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BchnRestClient } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
describe('BchnRestClient URL validation tests', () => {
|
|
4
|
+
it('should create an instance with a valid URL', () => {
|
|
5
|
+
const client = new BchnRestClient({url: 'http://localhost:8332'});
|
|
6
|
+
expect(client).toBeInstanceOf(BchnRestClient);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should throw an error for an invalid URL', () => {
|
|
10
|
+
expect(() => new BchnRestClient({url: 'invalid-url'})).toThrow('Invalid URL');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should throw an error if the URL is missing', () => {
|
|
14
|
+
expect(() => new BchnRestClient({url: ''})).toThrow('URL is required');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('BchnRestClient Timeout Handling', () => {
|
|
19
|
+
const config = {
|
|
20
|
+
url: 'http://localhost:8332',
|
|
21
|
+
timeoutMs: 1000, // 1 second timeout
|
|
22
|
+
};
|
|
23
|
+
const restClient = new BchnRestClient(config);
|
|
24
|
+
|
|
25
|
+
it('should throw a timeout error if the request exceeds the timeout limit', async () => {
|
|
26
|
+
await expect(restClient.getChainInfo()).rejects.toThrow('Request timed out');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not return a timeout error if the request completes in time', async () => {
|
|
30
|
+
await expect(restClient.getMempoolInfo()).resolves.toEqual({});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { BchnRpcClient, type GetBestBlockHash, type GetBlockCount, type GetBlockHash, type RpcClientConfig } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
describe('BchnRpcClient should have the correct constructor arguments', () => {
|
|
4
|
+
it('should create an instance with a valid URL', () => {
|
|
5
|
+
const config = {
|
|
6
|
+
url: 'http://localhost:8332',
|
|
7
|
+
rpcUser: 'rpcUser',
|
|
8
|
+
rpcPassword: 'rpcPassword'
|
|
9
|
+
}
|
|
10
|
+
const client = new BchnRpcClient(config);
|
|
11
|
+
expect(client).toBeInstanceOf(BchnRpcClient);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should throw an error for an invalid URL', () => {
|
|
15
|
+
const config = {
|
|
16
|
+
url: 'invalid-url',
|
|
17
|
+
rpcUser: 'rpcUser',
|
|
18
|
+
rpcPassword: 'rpcPassword'
|
|
19
|
+
}
|
|
20
|
+
expect(() => new BchnRpcClient(config)).toThrow('Invalid URL');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should throw an error if the URL is empty', () => {
|
|
24
|
+
const config = {
|
|
25
|
+
url: '',
|
|
26
|
+
rpcUser: 'rpcUser',
|
|
27
|
+
rpcPassword: 'rpcPassword'
|
|
28
|
+
}
|
|
29
|
+
expect(() => new BchnRpcClient(config)).toThrow('URL is required');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should throw an error if the URL is missing', () => {
|
|
33
|
+
const config = {
|
|
34
|
+
rpcUser: 'rpcUser',
|
|
35
|
+
rpcPassword: 'rpcPassword'
|
|
36
|
+
} as RpcClientConfig
|
|
37
|
+
expect(() => new BchnRpcClient(config)).toThrow('Invalid configuration: Either provide the url or protocol/host/port');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should throw an error if rpcUser is missing', () => {
|
|
41
|
+
const config = {
|
|
42
|
+
url: 'http://localhost:8332',
|
|
43
|
+
rpcPassword: 'rpcPassword'
|
|
44
|
+
} as RpcClientConfig
|
|
45
|
+
expect(() => new BchnRpcClient(config)).toThrow('Need to provide rpcUser in config');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw an error if rpcPassword is missing', () => {
|
|
49
|
+
const config = {
|
|
50
|
+
url: 'http://localhost:8332',
|
|
51
|
+
rpcUser: 'rpcUser'
|
|
52
|
+
} as RpcClientConfig
|
|
53
|
+
expect(() => new BchnRpcClient(config)).toThrow('Need to provide rpcPassword in config');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('BchnRpcClient Timeout and Retry Handling', () => {
|
|
58
|
+
it('should throw a timeout error if the request exceeds the timeout limit', async () => {
|
|
59
|
+
const config = {
|
|
60
|
+
url: 'http://localhost:8332',
|
|
61
|
+
rpcUser: 'rpcUser',
|
|
62
|
+
rpcPassword: 'rpcPassword',
|
|
63
|
+
timeoutMs: 1000,
|
|
64
|
+
}
|
|
65
|
+
const rpcClient = new BchnRpcClient(config);
|
|
66
|
+
|
|
67
|
+
await expect(rpcClient.request("getbestblockhash")).rejects.toThrow('Request failed after 1 attempts: The operation was aborted due to timeout');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should not return a timeout error if the request completes in time', async () => {
|
|
71
|
+
const config = {
|
|
72
|
+
url: 'http://localhost:8332',
|
|
73
|
+
rpcUser: 'rpcUser',
|
|
74
|
+
rpcPassword: 'rpcPassword',
|
|
75
|
+
timeoutMs: 1000,
|
|
76
|
+
}
|
|
77
|
+
const rpcClient = new BchnRpcClient(config);
|
|
78
|
+
|
|
79
|
+
await expect(rpcClient.request<GetBlockCount>("getblockcount")).resolves.toEqual({});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return an RetryLimitExceededError if all retries fail', async () => {
|
|
83
|
+
const config = {
|
|
84
|
+
url: 'http://localhost:8332',
|
|
85
|
+
rpcUser: 'rpcUser',
|
|
86
|
+
rpcPassword: 'rpcPassword',
|
|
87
|
+
maxRetries: 3,
|
|
88
|
+
timeoutMs: 1000,
|
|
89
|
+
}
|
|
90
|
+
const rpcClient = new BchnRpcClient(config);
|
|
91
|
+
await expect(rpcClient.request<GetBestBlockHash>("getbestblockhash")).rejects.toThrow("Request failed after 4 attempts: The operation was aborted due to timeout");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('BchnRpcClient Handling of Parameters', () => {
|
|
96
|
+
it('should error with incorrect number of params', async () => {
|
|
97
|
+
const config = {
|
|
98
|
+
url: 'http://localhost:8332',
|
|
99
|
+
rpcUser: 'rpcUser',
|
|
100
|
+
rpcPassword: 'rpcPassword',
|
|
101
|
+
}
|
|
102
|
+
const rpcClient = new BchnRpcClient(config);
|
|
103
|
+
await expect(rpcClient.request("getblockhash")).rejects.toThrow("Request failed after 1 attempts: Error: Invalid Request");
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should not error with correct number of params', async () => {
|
|
107
|
+
const config = {
|
|
108
|
+
url: 'http://localhost:8332',
|
|
109
|
+
rpcUser: 'rpcUser',
|
|
110
|
+
rpcPassword: 'rpcPassword',
|
|
111
|
+
}
|
|
112
|
+
const rpcClient = new BchnRpcClient(config);
|
|
113
|
+
await expect(rpcClient.request<GetBlockHash>("getblockhash", 5)).resolves.toEqual({});
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { http, delay, HttpResponse } from 'msw';
|
|
2
|
+
import { setupServer, SetupServerApi } from 'msw/node';
|
|
3
|
+
|
|
4
|
+
type jsonResult = { method: string; params?: unknown[] } | null | undefined
|
|
5
|
+
|
|
6
|
+
const server: SetupServerApi = setupServer(
|
|
7
|
+
// Mock endpoint with a delay to simulate timeout
|
|
8
|
+
http.get('http://localhost:8332/rest/chaininfo.json', async() => {
|
|
9
|
+
// Introduce a delay longer than the timeout setting to simulate a timeout scenario
|
|
10
|
+
await delay(3000)
|
|
11
|
+
return HttpResponse.json({})
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
http.get('http://localhost:8332/rest/mempool/info.json', async() => {
|
|
15
|
+
// Mock normally working REST endpoint
|
|
16
|
+
await delay(500)
|
|
17
|
+
return HttpResponse.json({})
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
http.post('http://localhost:8332', async ({ request }) => {
|
|
21
|
+
const json = await request.json() as jsonResult;
|
|
22
|
+
|
|
23
|
+
if (json === null || json === undefined) {
|
|
24
|
+
throw new Error('Invalid JSON response');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Introduce a delay longer than the timeout setting to simulate a timeout scenario
|
|
28
|
+
if (json.method === 'getbestblockhash') {
|
|
29
|
+
await delay(3000)
|
|
30
|
+
return HttpResponse.json({"jsonrpc": "2.0", "result": {}, "id": 4})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (json.method === 'getblockcount') {
|
|
34
|
+
// Mock normally working RPC command
|
|
35
|
+
await delay(500)
|
|
36
|
+
return HttpResponse.json({"jsonrpc": "2.0", "result": {}, "id": 5})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (json.method === 'getblockhash') {
|
|
40
|
+
// Mock normally working RPC command
|
|
41
|
+
await delay(500)
|
|
42
|
+
if (json.params?.[0]) {
|
|
43
|
+
return HttpResponse.json({"jsonrpc": "2.0", "result": {}, "id": 6})
|
|
44
|
+
} else {
|
|
45
|
+
return HttpResponse.json({"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": 7})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Start the server before tests and reset handlers after each test
|
|
52
|
+
beforeAll(() => server.listen());
|
|
53
|
+
afterEach(() => server.resetHandlers());
|
|
54
|
+
afterAll(() => server.close());
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "NodeNext",
|
|
4
|
+
"verbatimModuleSyntax": true,
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"target": "ES6",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"types": ["vitest/globals"],
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|