Skip to content

Commit

Permalink
Add automatic node switching (#424)
Browse files Browse the repository at this point in the history
* Add automatic node switching

* JSDoc typo

* Refactor RPC calls to `callRPC` in the Network class

* Remove manual Networking from Masternode class

* Refactor: remove raw `cExplorer` access from global.js
  • Loading branch information
JSKitty authored Oct 14, 2024
1 parent 37299ca commit 4ad5a0c
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 67 deletions.
6 changes: 3 additions & 3 deletions scripts/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { wallet, hasEncryptedWallet, Wallet } from './wallet.js';
import { getNetwork } from './network.js';
import {
start as settingsStart,
cExplorer,
strCurrency,
fAdvancedMode,
} from './settings.js';
Expand Down Expand Up @@ -389,12 +388,13 @@ export function optimiseCurrencyLocale(nAmount) {
* @param {string?} strAddress - Optional address to open, if void, the master key is used
*/
export async function openExplorer(strAddress = '') {
const strExplorerURL = getNetwork().strUrl;
if (wallet.isLoaded() && wallet.isHD() && !strAddress) {
const xpub = wallet.getXPub();
window.open(cExplorer.url + '/xpub/' + xpub, '_blank');
window.open(strExplorerURL + '/xpub/' + xpub, '_blank');
} else {
const address = strAddress || wallet.getAddress();
window.open(cExplorer.url + '/address/' + address, '_blank');
window.open(strExplorerURL + '/address/' + address, '_blank');
}
}

Expand Down
1 change: 0 additions & 1 deletion scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import.meta.webpackContext('@fontsource/montserrat/', {
regExp: /\.css$/,
});
import './global.js';
import { getNetwork } from './network.js';

// Export global functions to the MPW namespace so we can use them in html
export {
Expand Down
63 changes: 26 additions & 37 deletions scripts/masternode.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { cNode, cExplorer } from './settings.js';
import { cChainParams, COIN } from './chain_params.js';
import { wallet } from './wallet.js';
import { parseWIF, deriveAddress } from './encoding.js';
Expand All @@ -11,6 +10,7 @@ import { OP } from './script.js';
import bs58 from 'bs58';
import base32 from 'base32';
import { isStandardAddress } from './misc.js';
import { getNetwork } from './network.js';

/**
* Construct a Masternode
Expand Down Expand Up @@ -49,9 +49,9 @@ export default class Masternode {
@return {Promise<Object>} The object containing masternode information for this masternode
*/
async getFullData() {
const strURL = `${cNode.url}/listmasternodes?params=${this.collateralTxId}`;
const strURL = `/listmasternodes?params=${this.collateralTxId}`;
try {
const cMasternodes = (await (await fetch(strURL)).json()).filter(
const cMasternodes = (await getNetwork().callRPC(strURL)).filter(
(m) => m.outidx === this.outidx
);
if (cMasternodes.length > 0) {
Expand Down Expand Up @@ -186,14 +186,6 @@ export default class Masternode {
return bytesToHex(dSHA256(pkt).reverse());
}

/**
* @return {Promise<string>} The last block hash
*/
static async getLastBlockHash() {
const status = await (await fetch(`${cExplorer.url}/api/`)).json();
return status.backend.bestBlockHash;
}

/**
* @return {Promise<string>} The signed message signed with the collateral private key
*/
Expand Down Expand Up @@ -276,7 +268,7 @@ export default class Masternode {
*/
async broadcastMessageToHex() {
const sigTime = Math.round(Date.now() / 1000);
const blockHash = await Masternode.getLastBlockHash();
const blockHash = await getNetwork().getBestBlockHash();
let ip, port;
if (this.addr.includes('.')) {
// IPv4
Expand Down Expand Up @@ -341,8 +333,8 @@ export default class Masternode {
*/
async start() {
const message = await this.broadcastMessageToHex();
const url = `${cNode.url}/relaymasternodebroadcast?params=${message}`;
const response = await (await fetch(url)).text();
const url = `/relaymasternodebroadcast?params=${message}`;
const response = await getNetwork().callRPC(url, true);
return response.includes('Masternode broadcast sent');
}

Expand All @@ -353,8 +345,8 @@ export default class Masternode {
* @return {Promise<Array<object>} A list of currently active proposal
*/
static async getProposals({ fAllowFinished = false } = {}) {
const url = `${cNode.url}/getbudgetinfo`;
let arrProposals = await (await fetch(url)).json();
const url = `/getbudgetinfo`;
let arrProposals = await getNetwork().callRPC(url);

// Apply optional filters
if (!fAllowFinished) {
Expand Down Expand Up @@ -408,9 +400,9 @@ export default class Masternode {
const filter =
`${encodeURI(filterString)}` +
`${this.collateralTxId}-${this.outidx}")`;
const url = `${cNode.url}/getbudgetvotes?params=${proposalName}&filter=${filter}`;
const url = `/getbudgetvotes?params=${proposalName}&filter=${filter}`;
try {
const { Vote: vote } = await (await fetch(url)).json();
const { Vote: vote } = await getNetwork().callRPC(url);
return vote === 'YES' ? 1 : 2;
} catch (e) {
//Cannot parse JSON! This means that you did not vote hence return null
Expand Down Expand Up @@ -445,12 +437,12 @@ export default class Masternode {
voteCode,
sigTime
);
const url = `${cNode.url}/mnbudgetrawvote?params=${
this.collateralTxId
},${this.outidx},${hash},${
voteCode === 1 ? 'yes' : 'no'
},${sigTime},${encodeURI(signature).replaceAll('+', '%2b')}`;
const text = await (await fetch(url)).text();
const url = `/mnbudgetrawvote?params=${this.collateralTxId},${
this.outidx
},${hash},${voteCode === 1 ? 'yes' : 'no'},${sigTime},${encodeURI(
signature
).replaceAll('+', '%2b')}`;
const text = await getNetwork().callRPC(url, true);
return text;
}

Expand Down Expand Up @@ -520,15 +512,14 @@ export default class Masternode {
txid,
}) {
try {
const res = await (
await fetch(
`${cNode.url}/submitbudget?params=${encodeURI(
name
)},${encodeURI(url)},${nPayments},${start},${encodeURI(
address
)},${monthlyPayment / COIN},${txid}`
)
).text();
const res = await getNetwork().callRPC(
`/submitbudget?params=${encodeURI(name)},${encodeURI(
url
)},${nPayments},${start},${encodeURI(address)},${
monthlyPayment / COIN
},${txid}`,
true
);

if (/^"[a-f0-9]"$/ && res.length == 64 + 2) {
return { ok: true, hash: res };
Expand All @@ -552,17 +543,15 @@ export default class Masternode {
}

static async getNextSuperblock() {
return parseInt(
await (await fetch(`${cNode.url}/getnextsuperblock`)).text()
);
return parseInt(await getNetwork().callRPC(`/getnextsuperblock`, true));
}

/**
* Fetches the masternode count object, containing each status and network.
* @returns {Promise<{total:number, stable:number, enabled:number, inqueue:number, ipv4:number, ipv6:number, onion:number}>} - The masternode count object
*/
static async getMasternodeCount() {
return await (await fetch(`${cNode.url}/getmasternodecount`)).json();
return await getNetwork().callRPC('/getmasternodecount');
}

/**
Expand Down
110 changes: 85 additions & 25 deletions scripts/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from './debug.js';
import { sleep } from './utils.js';
import { getEventEmitter } from './event_bus.js';
import { setExplorer, fAutoSwitch } from './settings.js';
import { setExplorer, fAutoSwitch, setNode } from './settings.js';
import { cNode } from './settings.js';
import { ALERTS, tr, translation } from './i18n.js';

Check warning on line 13 in scripts/network.js

View workflow job for this annotation

GitHub Actions / Run linters

'ALERTS' is defined but never used
import { Transaction } from './transaction.js';
Expand Down Expand Up @@ -43,7 +43,7 @@ import { Transaction } from './transaction.js';
*/

/**
* Virtual class rapresenting any network backend
* Virtual class representing any network backend
*
*/
export class Network {
Expand All @@ -57,13 +57,28 @@ export class Network {
throw new Error('getBlockCount must be implemented');
}

getBestBlockHash() {
throw new Error('getBestBlockHash must be implemented');
}

sendTransaction() {
throw new Error('sendTransaction must be implemented');
}

async getTxInfo(_txHash) {
throw new Error('getTxInfo must be implemented');
}

/**
* A safety-wrapped RPC interface for calling Node RPCs with automatic correction handling
* @param {string} api - The API endpoint to call
* @param {boolean} isText - Optionally parse the result as Text rather than JSON
* @returns {Promise<object|string>} - The RPC response; JSON by default, text if `isText` is true.
*/
async callRPC(api, isText = false) {
const cRes = await retryWrapper(fetchNode, false, api);
return isText ? await cRes.text() : await cRes.json();
}
}

/**
Expand Down Expand Up @@ -113,14 +128,30 @@ export class ExplorerNetwork extends Network {
return block;
}

/**
* Fetch the block height of the current explorer
* @returns {Promise<number>} - Block height
*/
async getBlockCount() {
const { backend } = await (
await retryWrapper(fetchBlockbook, `/api/v2/api`)
await retryWrapper(fetchBlockbook, true, `/api/v2/api`)
).json();

return backend.blocks;
}

/**
* Fetch the latest block hash of the current explorer
* @returns {Promise<string>} - Block hash
*/
async getBestBlockHash() {
const { backend } = await (
await retryWrapper(fetchBlockbook, true, `/api/v2/api`)
).json();

return backend.bestBlockHash;
}

/**
* Sometimes blockbook might return internal error, in this case this function will sleep for some times and retry
* @param {string} strCommand - The specific Blockbook api to call
Expand All @@ -133,7 +164,7 @@ export class ExplorerNetwork extends Network {
let error;
while (trials < maxTrials) {
trials += 1;
const res = await retryWrapper(fetchBlockbook, strCommand);
const res = await retryWrapper(fetchBlockbook, true, strCommand);
if (!res.ok) {
try {
error = (await res.json()).error;
Expand Down Expand Up @@ -240,7 +271,11 @@ export class ExplorerNetwork extends Network {
let publicKey = strAddress;
// Fetch UTXOs for the key
const arrUTXOs = await (
await retryWrapper(fetchBlockbook, `/api/v2/utxo/${publicKey}`)
await retryWrapper(
fetchBlockbook,
true,
`/api/v2/utxo/${publicKey}`
)
).json();
return arrUTXOs;
} catch (e) {
Expand All @@ -255,14 +290,14 @@ export class ExplorerNetwork extends Network {
*/
async getXPubInfo(strXPUB) {
return await (
await retryWrapper(fetchBlockbook, `/api/v2/xpub/${strXPUB}`)
await retryWrapper(fetchBlockbook, true, `/api/v2/xpub/${strXPUB}`)
).json();
}

async sendTransaction(hex) {
try {
const data = await (
await retryWrapper(fetchBlockbook, '/api/v2/sendtx/', {
await retryWrapper(fetchBlockbook, true, '/api/v2/sendtx/', {
method: 'post',
body: hex,
})
Expand All @@ -281,15 +316,19 @@ export class ExplorerNetwork extends Network {
}

async getTxInfo(txHash) {
const req = await retryWrapper(fetchBlockbook, `/api/v2/tx/${txHash}`);
const req = await retryWrapper(
fetchBlockbook,
true,
`/api/v2/tx/${txHash}`
);
return await req.json();
}

/**
* @return {Promise<Number[]>} The list of blocks which have at least one shield transaction
*/
async getShieldBlockList() {
return await (await fetch(`${cNode.url}/getshieldblocks`)).json();
return await this.callRPC('/getshieldblocks');
}
}

Expand Down Expand Up @@ -322,22 +361,38 @@ export function fetchBlockbook(api, options) {
}

/**
* A wrapper for Blockbook calls which can, in the event of an unresponsive explorer,
* seamlessly attempt the same call on multiple other explorers until success.
* A Fetch wrapper which uses the current Node's base URL
* @param {string} api - The specific Node api to call
* @param {RequestInit} options - The Fetch options
* @returns {Promise<Response>} - The unresolved Fetch promise
*/
export function fetchNode(api, options) {
return fetch(cNode.url + api, options);
}

/**
* A wrapper for Blockbook and Node calls which can, in the event of an unresponsive instance,
* seamlessly attempt the same call on multiple other instances until success.
* @param {Function} func - The function to re-attempt with
* @param {boolean} isExplorer - Whether this is an Explorer or Node call
* @param {...any} args - The arguments to pass to the function
*/
async function retryWrapper(func, ...args) {
export async function retryWrapper(func, isExplorer, ...args) {
// Track internal errors from the wrapper
let err;

// If allowed by the user, Max Tries is ALL MPW-supported explorers, otherwise, restrict to only the current one.
let nMaxTries = cChainParams.current.Explorers.length;
// Select the instances list to use - Explorers or Nodes
const arrInstances = isExplorer
? cChainParams.current.Explorers
: cChainParams.current.Nodes;

// If allowed by the user, Max Tries is ALL MPW-supported instances, otherwise, restrict to only the current one.
let nMaxTries = arrInstances.length + 1;
let retries = 0;

// The explorer index we started at
let nIndex = cChainParams.current.Explorers.findIndex(
(a) => a.url === getNetwork().strUrl
// The instance index we started at
let nIndex = arrInstances.findIndex((a) =>
a.url === isExplorer ? getNetwork().strUrl : cNode.url
);

// Run the call until successful, or all attempts exhausted
Expand All @@ -354,15 +409,20 @@ async function retryWrapper(func, ...args) {
} catch (error) {
err = error;

// If allowed, switch explorers
// If allowed, switch instances
if (!fAutoSwitch) throw err;
nIndex = (nIndex + 1) % cChainParams.current.Explorers.length;
const cNewExplorer = cChainParams.current.Explorers[nIndex];

// Set the explorer at Network-class level, then as a hacky workaround for the current callback; we
// ... adjust the internal URL to the new explorer.
getNetwork().strUrl = cNewExplorer.url;
setExplorer(cNewExplorer, true);
nIndex = (nIndex + 1) % arrInstances.length;
const cNewInstance = arrInstances[nIndex];

if (isExplorer) {
// Set the explorer at Network-class level, then as a hacky workaround for the current callback; we
// ... adjust the internal URL to the new explorer.
getNetwork().strUrl = cNewInstance.url;
setExplorer(cNewInstance, true);
} else {
// For the Node, we change the setting directly
setNode(cNewInstance, true);
}

// Bump the attempts, and re-try next loop
retries++;
Expand Down
Loading

0 comments on commit 4ad5a0c

Please sign in to comment.