diff --git a/helpers/clients.js b/helpers/clients.js new file mode 100644 index 0000000..e496f32 --- /dev/null +++ b/helpers/clients.js @@ -0,0 +1,32 @@ +class Clients { + constructor() { + this.network = "bitcoin"; + this.mainClient = { + bitcoin: false, + bitcoinTestnet: false + }; + this.peer = { + bitcoin: { port: 0, host: "", protocol: "" }, + bitcoinTestnet: { port: 0, host: "", protocol: "" } + }; + this.peers = { + bitcoin: [], + bitcoinTestnet: [] + }; + } + + updateNetwork(network) { + this.network = network; + } + + updateMainClient(mainClient) { + this.mainClient = mainClient; + } + + updatePeer(peer) { + this.peer = peer; + } + +} + +module.exports = new Clients(); diff --git a/helpers/index.js b/helpers/index.js new file mode 100644 index 0000000..c73c184 --- /dev/null +++ b/helpers/index.js @@ -0,0 +1,244 @@ +const ElectrumClient = require("../lib/electrum_client"); +const clients = require("./clients"); + +let electrumKeepAlive = () => null; +let electrumKeepAliveInterval = 60000; + +const getTimeout = ({ arr = undefined, timeout = 1000 } = {}) => { + try { + if (arr && Array.isArray(arr)) return arr.length * timeout; + return timeout; + } catch { + return timeout; + } +} + +const promiseTimeout = (ms, promise) => { + let id; + let timeout = new Promise((resolve) => { + id = setTimeout(() => { + resolve({ error: true, data: "Timed Out." }); + }, ms); + }); + + return Promise.race([ + promise, + timeout + ]).then((result) => { + clearTimeout(id); + try {if ("error" in result && "data" in result) return result;} catch {} + return { error: false, data: result }; + }); +}; + +const pauseExecution = (duration = 500) => { + return new Promise(async (resolve) => { + try { + const wait = () => resolve({error: false}); + await setTimeout(wait, duration); + } catch (e) { + console.log(e); + resolve({error: true}); + } + }); +}; + +const getDefaultPeers = (network, protocol) => { + return require("./peers.json")[network].map(peer => { + try { + return { ...peer, protocol }; + } catch {} + }); +}; + +const pingServer = ({ id = Math.random() } = {}) => { + const method = "pingServer"; + return new Promise(async (resolve) => { + try { + if (clients.mainClient[clients.network] === false) await connectToRandomPeer(clients.network, clients.peers[clients.network]); + const { error, data } = await promiseTimeout(getTimeout(), clients.mainClient[clients.network].server_ping()); + resolve({ id, error, method, data }); + } catch (e) { + resolve({ id, error: true, method, data: e }); + } + }); +}; + +//peers = A list of peers acquired from default electrum servers using the getPeers method. +//customPeers = A list of peers added by the user to connect to by default in lieu of the default peer list. +const start = ({ id = Math.random(), network = "", peers = [], customPeers = []} = {}) => { + const method = "connectToPeer"; + return new Promise(async (resolve) => { + try { + if (!network) resolve({error: true, data: {}}); + //Clear/Remove any previous keep-alive message. + try {clearInterval(electrumKeepAlive);} catch {} + clients.network = network; + let customPeersLength = 0; + try {customPeersLength = customPeers.length;} catch {} + //Attempt to connect to specified peer + let connectionResponse = { error: true, data: "" }; + if (customPeersLength > 0) { + const { host = "", port = "", protocol = "ssl" } = customPeers[0]; + connectionResponse = await connectToPeer({ host, port, protocol, network }); + } else { + //Attempt to connect to random peer if none specified + connectionResponse = await connectToRandomPeer(network, peers); + } + resolve({ + ...connectionResponse, + id, + method: "connectToPeer", + customPeers, + network + }); + } catch (e) { + console.log(e); + resolve({ error: true, method, data: e }); + } + }); +}; + +const connectToPeer = ({ port = 50002, host = "", protocol = "ssl", network = "bitcoin" } = {}) => { + return new Promise(async (resolve) => { + try { + clients.network = network; + let needToConnect = clients.mainClient[network] === false; + let connectionResponse = { error: false, data: clients.peer[network] }; + if (!needToConnect) { + //Ensure the server is still alive + const pingResponse = await pingServer(); + if (pingResponse.error) { + await disconnectFromPeer({ network }); + needToConnect = true; + } + } + if (needToConnect) { + clients.mainClient[network] = new ElectrumClient(port, host, protocol); + connectionResponse = await promiseTimeout(1000, clients.mainClient[network].connect()); + if (!connectionResponse.error) { + try { + //Clear/Remove Electrum's keep-alive message. + clearInterval(electrumKeepAlive); + //Start Electrum's keep-alive function. It’s sent every minute as a keep-alive message. + electrumKeepAlive = setInterval(async () => { + try {pingServer({ id: Math.random() });} catch {} + }, electrumKeepAliveInterval); + } catch (e) {} + clients.peer[network] = { port, host, protocol }; + } + } + await pauseExecution(); + resolve(connectionResponse); + } catch (e) {resolve({ error: true, data: e });} + }); +}; + +const connectToRandomPeer = async (network, peers = [], protocol = "ssl") => { + //Peers can be found in peers.json. + //Additional Peers can be located here in servers.json & servers_testnet.json for reference: https://github.com/spesmilo/electrum/tree/master/electrum + let hasPeers = false; + try { + hasPeers = (Array.isArray(peers) && peers.length) || (Array.isArray(clients.peers[network]) && clients.peers[network].length); + } catch {} + if (hasPeers) { + if (Array.isArray(peers) && peers.length) { + //Update peer list + clients.peers[network] = peers; + } else { + //Set the saved peer list + peers = clients.peers[network]; + } + } else { + //Use the default peer list for a connection if no other peers were passed down and no saved peer list is present. + peers = getDefaultPeers(network, protocol); + } + const initialPeerLength = peers.length; //Acquire length of our default peers. + //Attempt to connect to a random default peer. Continue to iterate through default peers at random if unable to connect. + for (let i = 0; i <= initialPeerLength; i++) { + try { + const randomIndex = peers.length * Math.random() | 0; + const peer = peers[randomIndex]; + let port = "50002"; + let host = ""; + if (hasPeers) { + port = peer.port; + host = peer.host; + protocol = peer.protocol; + } else { + port = peer[peer.protocol]; + host = peer.host; + protocol = peer.protocol; + } + const connectionResponse = await connectToPeer({ port, host, protocol, network }); + if (connectionResponse.error === false && connectionResponse.data) { + return { + error: connectionResponse.error, + method: "connectToRandomPeer", + data: connectionResponse.data, + network + }; + } else { + //clients.mainClient[network].close && clients.mainClient[network].close(); + clients.mainClient[network] = false; + if (peers.length === 1) { + return { + error: true, + method: "connectToRandomPeer", + data: connectionResponse.data, + network + }; + } + peers.splice(randomIndex, 1); + } + } catch (e) {console.log(e);} + } + return { error: true, method: "connectToRandomPeer", data: "Unable to connect to any peer." }; +}; + +const stop = async ({ network = "" } = {}) => { + return new Promise(async (resolve) => { + try { + //Clear/Remove Electrum's keep-alive message. + clearInterval(electrumKeepAlive); + //Disconnect from peer + const response = await disconnectFromPeer({ network }); + resolve(response); + } catch (e) { + resolve({ error: true, data: e }); + } + }); + +}; + +const disconnectFromPeer = async ({ id = Math.random(), network = "" } = {}) => { + const failure = (data = {}) => { + return { error: true, id, method: "disconnectFromPeer", data }; + }; + try { + //console.log("Disconnecting from any previous peer..."); + if (clients.mainClient[network] === false) { + //No peer to disconnect from... + return { + error: false, + data: "No peer to disconnect from.", + id, + network, + method: "disconnectFromPeer" + }; + } + //Attempt to disconnect from peer... + clients.mainClient[network].close(); + clients.mainClient[network] = false; + clients.network = ""; + await pauseExecution(); + return { error: false, id, method: "disconnectFromPeer", network, data: "Disconnected..." }; + } catch (e) { + failure(e); + } +}; + +module.exports = { + start, + stop +}; diff --git a/helpers/peers.json b/helpers/peers.json new file mode 100644 index 0000000..e914206 --- /dev/null +++ b/helpers/peers.json @@ -0,0 +1,29 @@ +{ + "bitcoin": [ + {"host": "bitcoin.lukechilds.co", "ssl": 50002, "tcp": 50001}, + {"host": "electrumx.nmdps", "ssl": 50002, "tcp": 50001}, + {"host": "oneweek.duckdns.org", "ssl": 50002, "tcp": 50001}, + {"host": "electrum.vom-stausee.de", "ssl": 50002, "tcp": 50001}, + {"host": "electrum.hsmiths.com", "ssl": 50002, "tcp": 50001}, + {"host": "electrum.hodlister.co", "ssl": 50002, "tcp": 50001}, + {"host": "electrum3.hodlister.co", "ssl": 50002, "tcp": 50001}, + {"host": "btc.usebsv.com", "ssl": 50006, "tcp": 50001}, + {"host": "fortress.qtornado.com", "ssl": 443, "tcp": 50001}, + {"host": "ecdsa.net", "ssl": 110, "tcp": 50001}, + {"host": "electrum.be", "ssl": 50002, "tcp": 50001}, + {"host": "elex01.blackpole.online", "ssl": 50002, "tcp": 50001}, + {"host": "kirsche.emzy.de", "ssl": 50002, "tcp": 50001}, + {"host": "Electrum.hsmiths.com", "ssl": 50002, "tcp": 50001}, + {"host": "elec.luggs.co", "ssl": 443, "tcp": 50001}, + {"host": "btc.smsys.me", "ssl": 995, "tcp": 110} + ], + "bitcoinTestnet": [ + {"host": "testnet.hsmiths.com", "ssl": 53012, "tcp": 53011}, + {"host": "testnet1.bauerj.eu", "ssl": 50002, "tcp": 50001}, + {"host": "tn.not.fyi", "ssl": 55002, "tcp": 55001}, + {"host": "bitcoin.cluelessperson.com", "ssl": 51002, "tcp": 51001}, + {"host": "electrum.blockstream.info", "ssl": 60002, "tcp": 60001}, + {"host": "testnet.aranguren.org", "ssl": 51002, "tcp": 51001}, + {"host": "blockstream.info", "ssl": 993, "tcp": 143} + ] +}