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.
 
 
 
 
 
 

319 lines
11 KiB

import AsyncStorage from '@react-native-community/async-storage';
import { SegwitBech32Wallet } from './class';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: '50001' };
const hardcodedPeers = [
// { host: 'noveltybobble.coinjoined.com', tcp: '50001' }, // down
// { host: 'electrum.be', tcp: '50001' },
// { host: 'node.ispol.sk', tcp: '50001' }, // down
// { host: '139.162.14.142', tcp: '50001' },
// { host: 'electrum.coinucopia.io', tcp: '50001' }, // SLOW
// { host: 'Bitkoins.nl', tcp: '50001' }, // down
// { host: 'fullnode.coinkite.com', tcp: '50001' },
// { host: 'preperfect.eleCTruMioUS.com', tcp: '50001' }, // down
{ host: 'electrum1.bluewallet.io', tcp: '50001' },
{ host: 'electrum1.bluewallet.io', tcp: '50001' }, // 2x weight
{ host: 'electrum2.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' },
{ host: 'electrum3.bluewallet.io', tcp: '50001' }, // 2x weight
];
let mainClient = false;
let mainConnected = false;
async function connectMain() {
let usingPeer = await getRandomHardcodedPeer();
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.4');
let peers = await mainClient.serverPeers_subscribe();
if (peers && peers.length > 0) {
console.log('connected to ', ver);
mainConnected = true;
AsyncStorage.setItem(storageKey, JSON.stringify(peers));
}
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e);
}
if (!mainConnected) {
console.log('retry');
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
setTimeout(connectMain, 500);
}
}
connectMain();
/**
* Returns random hardcoded electrum server guaranteed to work
* at the time of writing.
*
* @returns {Promise<{tcp, host}|*>}
*/
async function getRandomHardcodedPeer() {
return hardcodedPeers[(hardcodedPeers.length * Math.random()) | 0];
}
/**
* Returns random electrum server out of list of servers
* previous electrum server told us. Nearly half of them is
* usually offline.
* Not used for now.
*
* @returns {Promise<{tcp: number, host: string}>}
*/
// eslint-disable-next-line
async function getRandomDynamicPeer() {
try {
let peers = JSON.parse(await AsyncStorage.getItem(storageKey));
peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (let peer of peers) {
let ret = {};
ret.host = peer[1];
for (let item of peer[2]) {
if (item.startsWith('t')) {
ret.tcp = item.replace('t', '');
}
}
if (ret.host && ret.tcp) return ret;
}
return defaultPeer; // failed to find random client, using default
} catch (_) {
return defaultPeer; // smth went wrong, using default
}
}
/**
*
* @param address {String}
* @returns {Promise<Object>}
*/
async function getBalanceByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
}
/**
*
* @param address {String}
* @returns {Promise<Array>}
*/
async function getTransactionsByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex'));
return history;
}
async function getTransactionsFullByAddress(address) {
let txs = await this.getTransactionsByAddress(address);
let ret = [];
for (let tx of txs) {
let full = await mainClient.blockchainTransaction_get(tx.tx_hash, true);
full.address = address;
for (let input of full.vin) {
input.address = SegwitBech32Wallet.witnessToAddress(input.txinwitness[1]);
input.addresses = [input.address];
// now we need to fetch previous TX where this VIN became an output, so we can see its amount
let prevTxForVin = await mainClient.blockchainTransaction_get(input.txid, true);
if (prevTxForVin && prevTxForVin.vout && prevTxForVin.vout[input.vout]) {
input.value = prevTxForVin.vout[input.vout].value;
}
}
for (let output of full.vout) {
if (output.scriptPubKey && output.scriptPubKey.addresses) output.addresses = output.scriptPubKey.addresses;
}
full.inputs = full.vin;
full.outputs = full.vout;
delete full.vin;
delete full.vout;
delete full.hex; // compact
delete full.hash; // compact
ret.push(full);
}
return ret;
}
/**
*
* @param addresses {Array}
* @param batchsize {Number}
* @returns {Promise<{balance: number, unconfirmed_balance: number, addresses: object}>}
*/
async function multiGetBalanceByAddress(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = { balance: 0, unconfirmed_balance: 0, addresses: {} };
let chunks = splitIntoChunks(addresses, batchsize);
for (let chunk of chunks) {
let scripthashes = [];
let scripthash2addr = {};
for (let addr of chunk) {
let script = bitcoin.address.toOutputScript(addr);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
reversedHash = reversedHash.toString('hex');
scripthashes.push(reversedHash);
scripthash2addr[reversedHash] = addr;
}
let balances = await mainClient.blockchainScripthash_getBalanceBatch(scripthashes);
for (let bal of balances) {
ret.balance += +bal.result.confirmed;
ret.unconfirmed_balance += +bal.result.unconfirmed;
ret.addresses[scripthash2addr[bal.param]] = bal.result;
}
}
return ret;
}
async function multiGetUtxoByAddress(addresses, batchsize) {
batchsize = batchsize || 100;
if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {};
let chunks = splitIntoChunks(addresses, batchsize);
for (let chunk of chunks) {
let scripthashes = [];
let scripthash2addr = {};
for (let addr of chunk) {
let script = bitcoin.address.toOutputScript(addr);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
reversedHash = reversedHash.toString('hex');
scripthashes.push(reversedHash);
scripthash2addr[reversedHash] = addr;
}
let results = await mainClient.blockchainScripthash_listunspentBatch(scripthashes);
for (let utxos of results) {
ret[scripthash2addr[utxos.param]] = utxos.result;
for (let utxo of ret[scripthash2addr[utxos.param]]) {
utxo.address = scripthash2addr[utxos.param];
utxo.txId = utxo.tx_hash;
utxo.vout = utxo.tx_pos;
delete utxo.tx_pos;
delete utxo.tx_hash;
}
}
}
return ret;
}
/**
* Simple waiter till `mainConnected` becomes true (which means
* it Electrum was connected in other function), or timeout 30 sec.
*
*
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async function waitTillConnected() {
let waitTillConnectedInterval = false;
let retriesCounter = 0;
return new Promise(function(resolve, reject) {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
resolve(true);
}
if (retriesCounter++ >= 30) {
clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 1000);
});
}
async function estimateFees() {
if (!mainClient) throw new Error('Electrum client is not connected');
const fast = await mainClient.blockchainEstimatefee(1);
const medium = await mainClient.blockchainEstimatefee(5);
const slow = await mainClient.blockchainEstimatefee(10);
return { fast, medium, slow };
}
async function broadcast(hex) {
if (!mainClient) throw new Error('Electrum client is not connected');
try {
const broadcast = await mainClient.blockchainTransaction_broadcast(hex);
return broadcast;
} catch (error) {
return error;
}
}
module.exports.getBalanceByAddress = getBalanceByAddress;
module.exports.getTransactionsByAddress = getTransactionsByAddress;
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
module.exports.getTransactionsFullByAddress = getTransactionsFullByAddress;
module.exports.waitTillConnected = waitTillConnected;
module.exports.estimateFees = estimateFees;
module.exports.broadcast = broadcast;
module.exports.multiGetUtxoByAddress = multiGetUtxoByAddress;
module.exports.forceDisconnect = () => {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
};
module.exports.hardcodedPeers = hardcodedPeers;
let splitIntoChunks = function(arr, chunkSize) {
let groups = [];
let i;
for (i = 0; i < arr.length; i += chunkSize) {
groups.push(arr.slice(i, i + chunkSize));
}
return groups;
};
/*
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
addr4elect = '1BWwXJH3q6PRsizBkSGm2Uw4Sz1urZ5sCj';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000); */