diff --git a/package.json b/package.json index 0add3d573..a8a20809b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nOS", "description": "nOS: NEO Operating System", "author": "nOS", - "version": "0.4.2", + "version": "0.4.3", "private": true, "main": "dist/main/main.js", "license": "MIT", diff --git a/src/common/util/getRPCEndpoint.js b/src/common/util/getRPCEndpoint.js new file mode 100644 index 000000000..87d34820a --- /dev/null +++ b/src/common/util/getRPCEndpoint.js @@ -0,0 +1,73 @@ +import fetch from 'node-fetch'; +import { rpc, settings } from '@cityofzion/neon-js'; + +let cachedRPC = null; + +function getAPIEndpoint(net) { + if (settings.networks[net]) { + return settings.networks[net].extra.neoscan; + } + + return net; +} + +function isUnreliableNode(url) { + return url.match(/redpulse\.com/i) || + url.match(/ddns\.net/i) || + url.match(/neeeo\.org/i) || + url.match(/otcgo\.cn/i) || + url.match(/seed1\.aphelion-neo\.com/i); +} + +function raceToSuccess(promises) { + return Promise.all( + promises.map((p) => ( + // If a request fails, count that as a resolution so it will keep + // waiting for other possible successes. If a request succeeds, + // treat it as a rejection so Promise.all immediately bails out. + p.then((val) => Promise.reject(val), (err) => err) + )) + ).then( + // If '.all' resolved, we've just got an array of errors. + (errors) => Promise.reject(errors), + // If '.all' rejected, we've got the result we wanted. + (val) => val + ); +} + +export default async function getRPCEndpoint(net) { + const apiEndpoint = getAPIEndpoint(net); + const response = await fetch(`${apiEndpoint}/v1/get_all_nodes`); + const data = await response.json(); + let nodes = data.sort((a, b) => b.height - a.height); + + if (settings.httpsOnly) { + nodes = nodes.filter((n) => n.url.includes('https://')); + } + + if (nodes.length === 0) { + throw new Error('No eligible nodes found!'); + } + + const heightThreshold = nodes[0].height - 1; + const goodNodes = nodes.filter((n) => n.height >= heightThreshold); + const urls = goodNodes.map((n) => n.url).filter((url) => !isUnreliableNode(url)); + + if (urls.length === 0) { + throw new Error('No eligible nodes found!'); + } + + if (urls.includes(cachedRPC)) { + return new rpc.RPCClient(cachedRPC).ping().then((num) => { + if (num <= settings.timeout.ping) return cachedRPC; + cachedRPC = null; + return getRPCEndpoint(net); + }); + } + + const clients = urls.map((u) => new rpc.RPCClient(u)); + + const fastestUrl = await raceToSuccess(clients.map((c) => c.ping().then(() => c.net))); + cachedRPC = fastestUrl; + return fastestUrl; +} diff --git a/src/main/util/resolve.js b/src/main/util/resolve.js index 51b058b53..c6b1b2d74 100644 --- a/src/main/util/resolve.js +++ b/src/main/util/resolve.js @@ -1,9 +1,10 @@ import isDev from 'electron-is-dev'; import path from 'path'; import { resolve as resolveURL, format as formatURL } from 'url'; -import { api, rpc, u } from '@cityofzion/neon-js'; +import { rpc, u } from '@cityofzion/neon-js'; import updateNetworks from 'util/updateNetworks'; +import getRPCEndpoint from 'util/getRPCEndpoint'; // TODO: Configurable network import { NOS_TESTNET } from 'values/networks'; @@ -46,7 +47,7 @@ function resolveLocal(url) { async function resolveNameService(url) { const { host, pathname } = url; - const endpoint = await api.getRPCEndpointFrom({ net: NOS_TESTNET }, api.neoscan); + const endpoint = await getRPCEndpoint(NOS_TESTNET); const client = new rpc.RPCClient(endpoint); const storageKey = u.str2hexstring(`${host}.target`); const response = await client.getStorage(NS_SCRIPT_HASH, storageKey); diff --git a/src/renderer/browser/actions/makeTestInvokeActions.js b/src/renderer/browser/actions/makeTestInvokeActions.js index 8503d5106..a1aa8388f 100644 --- a/src/renderer/browser/actions/makeTestInvokeActions.js +++ b/src/renderer/browser/actions/makeTestInvokeActions.js @@ -1,7 +1,9 @@ import { createActions } from 'spunky'; -import { api, rpc, wallet } from '@cityofzion/neon-js'; +import { rpc, wallet } from '@cityofzion/neon-js'; import { isArray } from 'lodash'; +import getRPCEndpoint from 'util/getRPCEndpoint'; + import createScript from 'shared/util/createScript'; import generateDAppActionId from './generateDAppActionId'; @@ -21,7 +23,7 @@ const testInvoke = async ({ net, scriptHash, operation, args, encodeArgs }) => { throw new Error(`Invalid arguments: "${args}"`); } - const endpoint = await api.getRPCEndpointFrom({ net }, api.neoscan); + const endpoint = await getRPCEndpoint(net); const script = createScript(scriptHash, operation, args, encodeArgs); const { result } = await rpc.Query.invokeScript(script).execute(endpoint); diff --git a/src/renderer/shared/actions/blockActions.js b/src/renderer/shared/actions/blockActions.js index 3ee06d221..d844109ec 100644 --- a/src/renderer/shared/actions/blockActions.js +++ b/src/renderer/shared/actions/blockActions.js @@ -1,10 +1,12 @@ -import { api, rpc } from '@cityofzion/neon-js'; +import { rpc } from '@cityofzion/neon-js'; import { createActions } from 'spunky'; +import getRPCEndpoint from 'util/getRPCEndpoint'; + export const ID = 'block'; export default createActions(ID, ({ net }) => async () => { - const endpoint = await api.getRPCEndpointFrom({ net }, api.neoscan); + const endpoint = await getRPCEndpoint(net); const client = new rpc.RPCClient(endpoint); const height = await client.getBlockCount(); return client.getBlock(height - 1); diff --git a/src/renderer/shared/util/getBalances.js b/src/renderer/shared/util/getBalances.js index 1ea78fc09..60cc70ce6 100644 --- a/src/renderer/shared/util/getBalances.js +++ b/src/renderer/shared/util/getBalances.js @@ -1,6 +1,8 @@ import { extend, get, find } from 'lodash'; import { api, rpc, wallet } from '@cityofzion/neon-js'; +import getRPCEndpoint from 'util/getRPCEndpoint'; + import getTokens from './getTokens'; import { GAS, NEO, ASSETS } from '../values/assets'; @@ -32,7 +34,7 @@ async function getAssetBalances(endpoint, address) { } export default async function getBalances({ net, address }) { - const endpoint = await api.getRPCEndpointFrom({ net }, api.neoscan); + const endpoint = await getRPCEndpoint(net); if (!wallet.isAddress(address)) { throw new Error(`Invalid address: "${address}"`); diff --git a/src/renderer/shared/util/getStorage.js b/src/renderer/shared/util/getStorage.js index cbf532085..71271a423 100644 --- a/src/renderer/shared/util/getStorage.js +++ b/src/renderer/shared/util/getStorage.js @@ -1,4 +1,6 @@ -import { wallet, api, rpc, u } from '@cityofzion/neon-js'; +import { wallet, rpc, u } from '@cityofzion/neon-js'; + +import getRPCEndpoint from 'util/getRPCEndpoint'; export const encode = (value) => u.str2hexstring(value); export const decode = (value) => u.hexstring2str(value); @@ -12,7 +14,7 @@ export default async function getStorage({ net, scriptHash, key, encodeInput, de throw new Error(`Invalid key: "${key}"`); } - const endpoint = await api.getRPCEndpointFrom({ net }, api.neoscan); + const endpoint = await getRPCEndpoint(net); const input = encodeInput ? encode(key) : key; const { result } = await rpc.Query.getStorage(scriptHash, input).execute(endpoint);