nano-wallet-js-test-1
Version:
SDK for developers to create and interact with Nanocurrency wallet easily
137 lines (136 loc) • 5.12 kB
JavaScript
import { fetchWithTimeout } from '../utils';
import { DEFAULT_TIMEOUT } from '../Constants';
import Logger from '../logger';
const LOG_REQUEST_RESPONSE = false;
export default class NanoRPC {
rpcUrls;
workerUrls;
timeout;
logger;
constructor({ rpcUrls, workerUrls, timeout = DEFAULT_TIMEOUT, debug = false, }) {
this.rpcUrls = rpcUrls instanceof Array ? rpcUrls : [rpcUrls];
if (this.rpcUrls.length < 0) {
throw new Error('No RPC addresses provided');
}
this.rpcUrls.forEach(addr => {
try {
new URL(addr);
}
catch (err) {
throw new Error(`Invalid RPC address: ${addr}`);
}
});
this.workerUrls = workerUrls instanceof Array ? workerUrls : [workerUrls];
if (this.workerUrls.length < 0) {
throw new Error('No workers addresses provided');
}
this.workerUrls.forEach(addr => {
try {
new URL(addr);
}
catch (err) {
throw new Error(`Invalid workers address: ${addr}`);
}
});
this.timeout = timeout;
this.logger = new Logger('NANO_RPC', debug);
}
async postRPC(data, urls = this.rpcUrls, retry = 0) {
const url = urls[retry];
const startedAt = Date.now();
try {
const response = await fetchWithTimeout(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
timeout: this.timeout,
});
const took = Date.now() - startedAt;
if (!response?.ok) {
this.logger.error(`url: ${url} | action: ${data.action} | status: ${response.status} (${response.statusText}) | took: ${took}ms`, LOG_REQUEST_RESPONSE
? `\n\t- Request: ${JSON.stringify(data)}\n\t- Response: ${JSON.stringify(await response.text())}`
: '');
throw new Error('bad status in response');
}
let body;
// clone, so we can consume the body if response.json() fails
const responseClone = response.clone();
try {
body = await response.json();
}
catch (err) {
this.logger.error(`url: ${url} | action: ${data.action} | status: bad json in response | took: ${took}ms`, LOG_REQUEST_RESPONSE
? `\n\t- Request: ${JSON.stringify(data)}\n\t- Response: ${await responseClone.clone().text()}`
: '');
throw new Error('bad json in response');
}
if (typeof body === 'object' && 'error' in body) {
throw new Error(body.error);
}
this.logger.info(`url: ${url} | action: ${data.action} | status: ${response.status} | took: ${took}ms`, LOG_REQUEST_RESPONSE
? `\n\t- Request: ${JSON.stringify(data)}\n\t- Response: ${JSON.stringify(body)}`
: '');
return body;
}
catch (error) {
const isRetryableError = error instanceof DOMException &&
(error.name === 'AbortError' ||
error.message === 'bad status in response' ||
error.message === 'bad json in response');
const canRetry = isRetryableError && retry < urls.length - 1;
if (error instanceof DOMException && error.name === 'AbortError') {
const took = Date.now() - startedAt;
this.logger.error(`url: ${url} | action: ${data.action} | status: ${error.message} | took: ${took}ms | will retry: ${canRetry}`, LOG_REQUEST_RESPONSE ? `\n\t- Request: ${JSON.stringify(data)}` : '');
}
if (canRetry) {
return await this.postRPC(data, urls, retry + 1);
}
throw error;
}
}
async process(block) {
const data = {
action: 'process',
json_block: 'true',
block,
};
return this.postRPC(data);
}
async workGenerate(hash, difficulty) {
const data = {
action: 'work_generate',
hash,
difficulty,
};
return this.postRPC(data, this.workerUrls);
}
async accountInfo(account) {
const data = {
action: 'account_info',
account,
representative: true,
weight: true,
receivable: true,
};
return this.postRPC(data);
}
async accountBalance(account) {
const data = {
action: 'account_balance',
account,
receivable: true,
};
return this.postRPC(data);
}
async receivable(account, { count = 100, threshold = '1' }) {
const data = {
action: 'receivable',
account,
count,
threshold,
};
return this.postRPC(data);
}
}