Skip to content

Commit

Permalink
Add automatic node switching
Browse files Browse the repository at this point in the history
  • Loading branch information
JSKitty committed Oct 12, 2024
1 parent 37299ca commit cbe348f
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 52 deletions.
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
66 changes: 40 additions & 26 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 { fetchBlockbook, fetchNode, retryWrapper } from './network.js';

/**
* Construct a Masternode
Expand Down Expand Up @@ -49,11 +49,11 @@ 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(
(m) => m.outidx === this.outidx
);
const cMasternodes = (
await (await retryWrapper(fetchNode, false, strURL)).json()
).filter((m) => m.outidx === this.outidx);
if (cMasternodes.length > 0) {
return cMasternodes[0];
} else {
Expand Down Expand Up @@ -190,7 +190,9 @@ export default class Masternode {
* @return {Promise<string>} The last block hash
*/
static async getLastBlockHash() {
const status = await (await fetch(`${cExplorer.url}/api/`)).json();
const status = await (
await retryWrapper(fetchBlockbook, true, `/api/`)
).json();
return status.backend.bestBlockHash;
}

Expand Down Expand Up @@ -341,8 +343,10 @@ 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 (
await retryWrapper(fetchNode, false, url)
).text();
return response.includes('Masternode broadcast sent');
}

Expand All @@ -353,8 +357,10 @@ 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 (
await retryWrapper(fetchNode, false, url)
).json();

// Apply optional filters
if (!fAllowFinished) {
Expand Down Expand Up @@ -408,9 +414,11 @@ 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 (
await retryWrapper(fetchNode, false, url)
).json();
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 +453,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 (await retryWrapper(fetchNode, false, url)).text();
return text;
}

Expand Down Expand Up @@ -521,12 +529,14 @@ export default class Masternode {
}) {
try {
const res = await (
await fetch(
`${cNode.url}/submitbudget?params=${encodeURI(
name
)},${encodeURI(url)},${nPayments},${start},${encodeURI(
address
)},${monthlyPayment / COIN},${txid}`
await retryWrapper(
fetchNode,
false,
`/submitbudget?params=${encodeURI(name)},${encodeURI(
url
)},${nPayments},${start},${encodeURI(address)},${
monthlyPayment / COIN
},${txid}`
)
).text();

Expand All @@ -553,7 +563,9 @@ export default class Masternode {

static async getNextSuperblock() {
return parseInt(
await (await fetch(`${cNode.url}/getnextsuperblock`)).text()
await (
await retryWrapper(fetchNode, false, `/getnextsuperblock`)
).text()
);
}

Expand All @@ -562,7 +574,9 @@ export default class Masternode {
* @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 (
await retryWrapper(fetchNode, false, `/getmasternodecount`)
).json();
}

/**
Expand Down
79 changes: 55 additions & 24 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 @@ -115,7 +115,7 @@ export class ExplorerNetwork extends Network {

async getBlockCount() {
const { backend } = await (
await retryWrapper(fetchBlockbook, `/api/v2/api`)
await retryWrapper(fetchBlockbook, true, `/api/v2/api`)
).json();

return backend.blocks;
Expand All @@ -133,7 +133,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 +240,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 +259,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 +285,21 @@ 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 (
await retryWrapper(fetchNode, false, `/getshieldblocks`)
).json();
}
}

Expand Down Expand Up @@ -322,22 +332,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 +380,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
5 changes: 4 additions & 1 deletion scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,14 @@ export async function setExplorer(explorer, fSilent = false) {
);
}

async function setNode(node, fSilent = false) {
export async function setNode(node, fSilent = false) {
cNode = node;
const database = await Database.getInstance();
database.setSettings({ node: node.url });

// Update the selector UI
doms.domNodeSelect.value = cNode.url;

if (!fSilent)
createAlert(
'success',
Expand Down

0 comments on commit cbe348f

Please sign in to comment.