Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automatic node switching #424

Merged
merged 5 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -1,5 +1,5 @@
import { cChainParams } from './chain_params.js';
import { createAlert } from './misc.js';

Check warning on line 2 in scripts/network.js

View workflow job for this annotation

GitHub Actions / Run linters

'createAlert' is defined but never used
import {
debugLog,
debugTimerEnd,
Expand All @@ -8,9 +8,9 @@
} 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 @@
*/

/**
* Virtual class rapresenting any network backend
* Virtual class representing any network backend
*
*/
export class Network {
Expand All @@ -57,13 +57,28 @@
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 @@
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 @@
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 @@
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 @@
*/
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 @@
}

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 @@
}

/**
* 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 @@
} 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
Loading