You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

505 lines
18 KiB

const ElectrumClient = require("../lib/electrum_client");
const clients = require("./clients");
let electrumKeepAlive = () => null;
let electrumKeepAliveInterval = 60000;
const _getTimeout = ({ arr = [], timeout = 2000 } = {}) => {
try {
if (!Array.isArray(arr)) arr = _attemptToGetArray(arr);
if (arr && Array.isArray(arr) && arr.length > 0) return (arr.length * timeout)/2 | timeout;
return timeout;
} catch {
return timeout;
}
}
const _attemptToGetArray = (item = []) => {
try {
if (Array.isArray(item)) return item;
if ("data" in item) {
if (Array.isArray(item.data)) return item.data;
if (typeof item.data === 'object') return Object.values(item.data);
}
return [];
} catch { return []; }
};
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, network: clients.network });
} catch (e) {
resolve({ id, error: true, method, data: e, network: clients.network });
}
});
};
//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 = [], net, tls} = {}) => {
const method = "connectToPeer";
return new Promise(async (resolve) => {
try {
if (!network) {
resolve({
id,
method: "connectToPeer",
error: true,
data: "No network specified",
});
return;
}
//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 = "", protocol = "ssl" } = customPeers[0];
const port = customPeers[0][protocol];
connectionResponse = await connectToPeer({ host, port, protocol, network, net, tls });
} else {
//Attempt to connect to random peer if none specified
connectionResponse = await connectToRandomPeer(network, peers, 'ssl', net, tls);
}
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", net, tls } = {}) => {
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, net, tls);
connectionResponse = await promiseTimeout(_getTimeout(), clients.mainClient[network].connect());
if (connectionResponse.error) {
return resolve(connectionResponse);
}
/*
* The scripthash doesn't have to be valid.
* We're simply testing if the server will respond to a batch request.
*/
const scriptHash = "77ca78f9a84b48041ad71f7cc6ff6c33460c25f0cb99f558f9813ed9e63727dd";
const testResponses = await Promise.all([
pingServer(),
getAddressScriptHashBalances({ network, scriptHashes: [scriptHash] }),
])
if (testResponses[0].error || testResponses[1].error) {
return resolve({ error: true, data: "" });
}
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 };
}
resolve(connectionResponse);
} catch (e) {resolve({ error: true, data: e });}
});
};
const connectToRandomPeer = async (network, peers = [], protocol = "ssl", net, tls) => {
//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, net, tls });
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 = "" } = {}) => {
try {
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();
await pauseExecution();
//Reset the client.
clients.mainClient[network] = false;
clients.peer[network] = { port: 0, host: "", protocol: "" };
clients.peers[network] = [];
clients.subscribedAddresses[network] = [];
clients.subscribedHeaders[network] = false;
clients.onAddressReceive[network] = undefined;
clients.network = "";
return { error: false, id, method: "disconnectFromPeer", network, data: "Disconnected..." };
} catch (e) {
return { error: true, id, method: "disconnectFromPeer", data: e };
}
};
const getAddressScriptHashBalance = ({ scriptHash = "", id = Math.random(), network = "" } = {}) => {
const method = "getAddressScriptHashBalance";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
const { error, data } = await promiseTimeout(_getTimeout(), clients.mainClient[network].blockchainScripthash_getBalance(scriptHash));
resolve({ id, error, method, data, scriptHash, network });
} catch (e) {
console.log(e);
return { id, error: true, method, data: e, network };
}
});
};
const getPeers = ({ id = Math.random(), network = "" } = {}) => {
const method = "getPeers";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
const data = await clients.mainClient[network].serverPeers_subscribe();
resolve({ id, error: false, method, data, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: null, network });
}
});
};
const getConnectedPeer = (network = 'bitcoin') => {
try {
return clients?.peer[network] ?? '';
} catch {
return '';
}
};
const subscribeHeader = async ({ id = "subscribeHeader", network = "", onReceive = () => null } = {}) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (clients.subscribedHeaders[network] === true) return { id, error: false, method: "subscribeHeader", data: 'Already Subscribed.', network };
const res = await promiseTimeout(10000, clients.mainClient[network].subscribe.on('blockchain.headers.subscribe', (onReceive)));
if (res.error) return { ...res, id, method: "subscribeHeader" };
const response = await promiseTimeout(10000, clients.mainClient[network].blockchainHeaders_subscribe());
if (!response.error) clients.subscribedHeaders[network] = true;
return { ...response, id, method: "subscribeHeader" };
} catch (e) {
return { id, error: true, method: "subscribeHeader", data: e, network };
}
};
const subscribeAddress = async ({ id = Math.random(), scriptHash = "", network = "bitcoin", onReceive = undefined } = {}) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
// Set onAddressReceive if wasn't previously.
if (onReceive && !clients.onAddressReceive[network]) {
clients.onAddressReceive[network] = onReceive;
const res = await promiseTimeout(10000, clients.mainClient[network].subscribe.on('blockchain.scripthash.subscribe', clients.onAddressReceive[network]));
if (res.error) {
return { ...res, id, method: "subscribeAddress" };
}
}
//Ensure this address is not already subscribed
if (clients.subscribedAddresses[network].includes(scriptHash)) return { id, error: false, method: "subscribeAddress", data: "Already Subscribed." };
const response = await promiseTimeout(10000, clients.mainClient[network].blockchainScripthash_subscribe(scriptHash));
if (!response.error) clients.subscribedAddresses[network].push(scriptHash);
return { ...response, id, method: "subscribeAddress" };
} catch (e) {
return { id, error: true, method: "subscribeAddress", data: e };
}
};
const getFeeEstimate = ({ blocksWillingToWait = 8, id = Math.random(), network = "" } = {}) => {
const method = "getFeeEstimate";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
const response = await promiseTimeout(_getTimeout(), clients.mainClient[network].blockchainEstimatefee(blocksWillingToWait));
resolve({ ...response, id, method, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const getAddressScriptHashBalances = ({ scriptHashes = [], id = Math.random(), network = "" } = {}) => {
const method = "getAddressScriptHashBalances";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
const timeout = _getTimeout({ arr: scriptHashes });
const response = await promiseTimeout(timeout, clients.mainClient[network].blockchainScripthash_getBalanceBatch(scriptHashes));
resolve({ ...response, id, method, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const listUnspentAddressScriptHashes = ({ scriptHashes = [], id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "listUnspentAddressScriptHashes";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout({ arr: scriptHashes });
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainScripthash_listunspentBatch(scriptHashes));
resolve({ id, error, method, data, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const getAddressScriptHashesHistory = ({ scriptHashes = [], id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "getScriptHashesHistory";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout({ arr: scriptHashes });
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainScripthash_getHistoryBatch(scriptHashes));
resolve({ id, error, method, data, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const getAddressScriptHashesMempool = ({ scriptHashes = [], id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "getAddressScriptHashesMempool";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout({ arr: scriptHashes });
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainScripthash_getMempoolBatch(scriptHashes));
resolve({ id, error, method, data, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const getTransactions = ({ txHashes = [], id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "getTransactions";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout({ arr: txHashes });
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainTransaction_getBatch(txHashes, true));
resolve({ id, error, method, data, network });
} catch (e) {
console.log(e);
resolve({ id, error: true, method, data: e, network });
}
});
};
const broadcastTransaction = ({ rawTx = [], id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "broadcastTransaction";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout();
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainTransaction_broadcast(rawTx));
resolve({ id, error, method, data, network });
} catch (e) {
resolve({ id, error: true, method, data: e, network });
}
});
};
/**
* Returns header hex of the provided height and network.
* @param {Number} [height]
* @param {Number} [id]
* @param {"bitcoin" | "bitcoinTestnet" | "bitcoinRegtest"} network
* @param {Number | undefined} [timeout]
* @return {Promise<{id: Number, error: boolean, method: "getHeader", data: string, network: "bitcoin" | "bitcoinTestnet" | "bitcoinRegtest"}>}
*/
const getHeader = ({ height = 0, id = Math.random(), network = "", timeout = undefined } = {}) => {
const method = "getHeader";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
if (!timeout) timeout = _getTimeout();
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainBlock_getBlockHeader(height));
resolve({ id, error, method, data, network });
} catch (e) {
resolve({ id, error: true, method, data: e, network });
}
});
}
const getTransactionMerkle = ({ tx_hash, height, id = Math.random(), network = "", timeout = 2000 } = {}) => {
const method = "getTransactionMerkle";
return new Promise(async (resolve) => {
try {
if (clients.mainClient[network] === false) await connectToRandomPeer(network, clients.peers[network]);
const { error, data } = await promiseTimeout(timeout, clients.mainClient[network].blockchainTransaction_getMerkle(tx_hash, height));
resolve({ id, error, method, data, network });
} catch (e) {
resolve({ id, error: true, method, data: e, network });
}
});
};
module.exports = {
start,
stop,
pingServer,
getAddressScriptHashBalance,
getAddressScriptHashBalances,
listUnspentAddressScriptHashes,
getAddressScriptHashesHistory,
getAddressScriptHashesMempool,
getTransactions,
getPeers,
subscribeHeader,
subscribeAddress,
getFeeEstimate,
broadcastTransaction,
getConnectedPeer,
getHeader,
getTransactionMerkle,
};