Browse Source

ADD: support for ElectrumPersonalServer (disable protocol batching)

tradermain
Overtorment 5 years ago
parent
commit
38b7ea525a
  1. 154
      BlueElectrum.js
  2. 43
      class/abstract-hd-electrum-wallet.js
  3. 9
      tests/integration/hd-segwit-bech32-wallet.test.js

154
BlueElectrum.js

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { AppStorage } from './class'; import { AppStorage, LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet } from './class';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const ElectrumClient = require('electrum-client'); const ElectrumClient = require('electrum-client');
let reverse = require('buffer-reverse'); let reverse = require('buffer-reverse');
@ -27,6 +27,10 @@ const hardcodedPeers = [
let mainClient: ElectrumClient = false; let mainClient: ElectrumClient = false;
let mainConnected = false; let mainConnected = false;
let wasConnectedAtLeastOnce = false; let wasConnectedAtLeastOnce = false;
let serverName = false;
let disableBatching = false;
let txhashHeightCache = {};
async function connectMain() { async function connectMain() {
let usingPeer = await getRandomHardcodedPeer(); let usingPeer = await getRandomHardcodedPeer();
@ -54,8 +58,13 @@ async function connectMain() {
const ver = await mainClient.initElectrum({ client: 'bluewallet', version: '1.4' }); const ver = await mainClient.initElectrum({ client: 'bluewallet', version: '1.4' });
if (ver && ver[0]) { if (ver && ver[0]) {
console.log('connected to ', ver); console.log('connected to ', ver);
serverName = ver[0];
mainConnected = true; mainConnected = true;
wasConnectedAtLeastOnce = true; wasConnectedAtLeastOnce = true;
if (ver[0].startsWith('ElectrumPersonalServer') || ver[0].startsWith('electrs')) {
// TODO: once they release support for batching - disable batching only for lower versions
disableBatching = true;
}
// AsyncStorage.setItem(storageKey, JSON.stringify(peers)); TODO: refactor // AsyncStorage.setItem(storageKey, JSON.stringify(peers)); TODO: refactor
} }
} catch (e) { } catch (e) {
@ -154,6 +163,7 @@ module.exports.getTransactionsByAddress = async function(address) {
let hash = bitcoin.crypto.sha256(script); let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash)); let reversedHash = Buffer.from(reverse(hash));
let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex')); let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex'));
if (history.tx_hash) txhashHeightCache[history.tx_hash] = history.height; // cache tx height
return history; return history;
}; };
@ -224,7 +234,16 @@ module.exports.multiGetBalanceByAddress = async function(addresses, batchsize) {
scripthash2addr[reversedHash] = addr; scripthash2addr[reversedHash] = addr;
} }
let balances = await mainClient.blockchainScripthash_getBalanceBatch(scripthashes); let balances = [];
if (disableBatching) {
for (let sh of scripthashes) {
let balance = await mainClient.blockchainScripthash_getBalance(sh);
balances.push({ result: balance, param: sh });
}
} else {
balances = await mainClient.blockchainScripthash_getBalanceBatch(scripthashes);
}
for (let bal of balances) { for (let bal of balances) {
ret.balance += +bal.result.confirmed; ret.balance += +bal.result.confirmed;
@ -254,7 +273,13 @@ module.exports.multiGetUtxoByAddress = async function(addresses, batchsize) {
scripthash2addr[reversedHash] = addr; scripthash2addr[reversedHash] = addr;
} }
let results = await mainClient.blockchainScripthash_listunspentBatch(scripthashes); let results = [];
if (disableBatching) {
// ElectrumPersonalServer doesnt support `blockchain.scripthash.listunspent`
} else {
results = await mainClient.blockchainScripthash_listunspentBatch(scripthashes);
}
for (let utxos of results) { for (let utxos of results) {
ret[scripthash2addr[utxos.param]] = utxos.result; ret[scripthash2addr[utxos.param]] = utxos.result;
@ -289,10 +314,20 @@ module.exports.multiGetHistoryByAddress = async function(addresses, batchsize) {
scripthash2addr[reversedHash] = addr; scripthash2addr[reversedHash] = addr;
} }
let results = await mainClient.blockchainScripthash_getHistoryBatch(scripthashes); let results = [];
if (disableBatching) {
for (let sh of scripthashes) {
let history = await mainClient.blockchainScripthash_getHistory(sh);
results.push({ result: history, param: sh });
}
} else {
results = await mainClient.blockchainScripthash_getHistoryBatch(scripthashes);
}
for (let history of results) { for (let history of results) {
ret[scripthash2addr[history.param]] = history.result; ret[scripthash2addr[history.param]] = history.result;
if (history.result[0]) txhashHeightCache[history.result[0].tx_hash] = history.result[0].height; // cache tx height
for (let hist of ret[scripthash2addr[history.param]]) { for (let hist of ret[scripthash2addr[history.param]]) {
hist.address = scripthash2addr[history.param]; hist.address = scripthash2addr[history.param];
} }
@ -309,10 +344,35 @@ module.exports.multiGetTransactionByTxid = async function(txids, batchsize, verb
verbose = verbose !== false; verbose = verbose !== false;
if (!mainClient) throw new Error('Electrum client is not connected'); if (!mainClient) throw new Error('Electrum client is not connected');
let ret = {}; let ret = {};
txids = [...new Set(txids)]; // deduplicate just for any case
let chunks = splitIntoChunks(txids, batchsize); let chunks = splitIntoChunks(txids, batchsize);
for (let chunk of chunks) { for (let chunk of chunks) {
let results = await mainClient.blockchainTransaction_getBatch(chunk, verbose); let results = [];
if (disableBatching) {
for (let txid of chunk) {
try {
// in case of ElectrumPersonalServer it might not track some transactions (like source transactions for our transactions)
// so we wrap it in try-catch
let tx = await mainClient.blockchainTransaction_get(txid, verbose);
if (typeof tx === 'string' && verbose) {
// apparently electrum server (EPS?) didnt recognize VERBOSE parameter, and sent us plain txhex instead of decoded tx.
// lets decode it manually on our end then:
tx = txhexToElectrumTransaction(tx);
if (txhashHeightCache[txid]) {
// got blockheight where this tx was confirmed
tx.confirmations = this.estimateCurrentBlockheight() - txhashHeightCache[txid];
tx.time = this.calculateBlockTime(txhashHeightCache[txid]);
tx.blocktime = this.calculateBlockTime(txhashHeightCache[txid]);
}
}
results.push({ result: tx, param: txid });
} catch (_) {}
}
} else {
results = await mainClient.blockchainTransaction_getBatch(chunk, verbose);
}
for (let txdata of results) { for (let txdata of results) {
ret[txdata.param] = txdata.result; ret[txdata.param] = txdata.result;
@ -400,6 +460,23 @@ module.exports.broadcastV2 = async function(hex) {
return mainClient.blockchainTransaction_broadcast(hex); return mainClient.blockchainTransaction_broadcast(hex);
}; };
module.exports.estimateCurrentBlockheight = function() {
const baseTs = 1585837504347; // uS
const baseHeight = 624083;
return Math.floor(baseHeight + (+new Date() - baseTs) / 1000 / 60 / 10);
};
/**
*
* @param height
* @returns {number} Timestamp in seconds
*/
module.exports.calculateBlockTime = function(height) {
const baseTs = 1585837504; // sec
const baseHeight = 624083;
return baseTs + (height - baseHeight) * 10 * 60;
};
/** /**
* *
* @param host * @param host
@ -434,3 +511,70 @@ let splitIntoChunks = function(arr, chunkSize) {
} }
return groups; return groups;
}; };
function txhexToElectrumTransaction(txhex) {
let tx = bitcoin.Transaction.fromHex(txhex);
let ret = {
txid: tx.getId(),
hash: tx.getId(),
version: tx.version,
size: Math.ceil(txhex.length / 2),
vsize: tx.virtualSize(),
weight: tx.weight(),
locktime: tx.locktime,
vin: [],
vout: [],
hex: txhex,
blockhash: '',
confirmations: 0,
time: 0,
blocktime: 0,
};
for (let inn of tx.ins) {
let txinwitness = [];
if (inn.witness[0]) txinwitness.push(inn.witness[0].toString('hex'));
if (inn.witness[1]) txinwitness.push(inn.witness[1].toString('hex'));
ret.vin.push({
txid: reverse(inn.hash).toString('hex'),
vout: inn.index,
scriptSig: { hex: inn.script.toString('hex'), asm: '' },
txinwitness,
sequence: inn.sequence,
});
}
let n = 0;
for (let out of tx.outs) {
let value = new BigNumber(out.value).dividedBy(100000000).toNumber();
let address = false;
let type = false;
if (SegwitBech32Wallet.scriptPubKeyToAddress(out.script.toString('hex'))) {
address = SegwitBech32Wallet.scriptPubKeyToAddress(out.script.toString('hex'));
type = 'witness_v0_keyhash';
} else if (SegwitP2SHWallet.scriptPubKeyToAddress(out.script.toString('hex'))) {
address = SegwitP2SHWallet.scriptPubKeyToAddress(out.script.toString('hex'));
type = '???'; // TODO
} else if (LegacyWallet.scriptPubKeyToAddress(out.script.toString('hex'))) {
address = LegacyWallet.scriptPubKeyToAddress(out.script.toString('hex'));
type = '???'; // TODO
}
ret.vout.push({
value,
n,
scriptPubKey: {
asm: '',
hex: out.script.toString('hex'),
reqSigs: 1, // todo
type,
addresses: [address],
},
});
n++;
}
return ret;
}

43
class/abstract-hd-electrum-wallet.js

@ -702,9 +702,52 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* @returns {[]} * @returns {[]}
*/ */
getUtxo() { getUtxo() {
if (this._utxo.length === 0) return this.getDerivedUtxoFromOurTransaction(); // oy vey, no stored utxo. lets attempt to derive it from stored transactions
return this._utxo; return this._utxo;
} }
getDerivedUtxoFromOurTransaction() {
let utxos = [];
for (let tx of this.getTransactions()) {
for (let output of tx.outputs) {
let address = false;
if (output.scriptPubKey && output.scriptPubKey.addresses && output.scriptPubKey.addresses[0]) {
address = output.scriptPubKey.addresses[0];
}
if (this.weOwnAddress(address)) {
let value = new BigNumber(output.value).multipliedBy(100000000).toNumber();
utxos.push({
txid: tx.txid,
txId: tx.txid,
vout: output.n,
address,
value,
amount: value,
confirmations: tx.confirmations,
wif: this._getWifForAddress(address),
height: BlueElectrum.estimateCurrentBlockheight() - tx.confirmations,
});
}
}
}
// got all utxos we ever had. lets filter out the ones that are spent:
let ret = [];
for (let utxo of utxos) {
let spent = false;
for (let tx of this.getTransactions()) {
for (let input of tx.inputs) {
if (input.txid === utxo.txid && input.vout === utxo.vout) spent = true;
// utxo we got previously was actually spent right here ^^
}
}
if (!spent) ret.push(utxo);
}
return ret;
}
_getDerivationPathByAddress(address) { _getDerivationPathByAddress(address) {
const path = "m/84'/0'/0'"; const path = "m/84'/0'/0'";
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {

9
tests/integration/hd-segwit-bech32-wallet.test.js

@ -197,6 +197,15 @@ describe('Bech32 Segwit HD (BIP84)', () => {
assert.strictEqual(txFound, 4); assert.strictEqual(txFound, 4);
await hd.fetchUtxo(); await hd.fetchUtxo();
assert.strictEqual(hd.getUtxo().length, 2);
assert.strictEqual(hd.getDerivedUtxoFromOurTransaction().length, 2);
let u1 = hd.getUtxo()[0];
let u2 = hd.getDerivedUtxoFromOurTransaction()[0];
delete u1.confirmations;
delete u2.confirmations;
delete u1.height;
delete u2.height;
assert.deepStrictEqual(u1, u2);
let changeAddress = await hd.getChangeAddressAsync(); let changeAddress = await hd.getChangeAddressAsync();
assert.ok(changeAddress && changeAddress.startsWith('bc1')); assert.ok(changeAddress && changeAddress.startsWith('bc1'));

Loading…
Cancel
Save