FIX: BRD wallet with segwit

This commit is contained in:
Ivan 2020-11-27 16:42:12 +03:00 committed by GitHub
parent 97bd1a5c00
commit 7ffa3bbd41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 221 additions and 29 deletions

View File

@ -1,7 +1,9 @@
import bip39 from 'bip39';
import * as bip32 from 'bip32';
import * as bitcoinjs from 'bitcoinjs-lib';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
const bip32 = require('bip32');
const bitcoinjs = require('bitcoinjs-lib');
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
const BlueElectrum = require('../../blue_modules/BlueElectrum');
/**
* HD Wallet (BIP39).
@ -11,6 +13,14 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
// track address index at which wallet switched to segwit
_external_segwit_index = null; // eslint-disable-line camelcase
_internal_segwit_index = null; // eslint-disable-line camelcase
allowSendMax() {
return true;
}
/**
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/584
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/914
@ -31,28 +41,58 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return this._xpub;
}
_getExternalAddressByIndex(index) {
// we need a separate function without external_addresses_cache to use in binarySearch
_calcNodeAddressByIndex(node, index, p2wpkh = false) {
let _node;
if (node === 0) {
_node = this._node0 || (this._node0 = bitcoinjs.bip32.fromBase58(this.getXpub()).derive(node));
}
if (node === 1) {
_node = this._node1 || (this._node1 = bitcoinjs.bip32.fromBase58(this.getXpub()).derive(node));
}
const pubkey = _node.derive(index).publicKey;
const address = p2wpkh ? bitcoinjs.payments.p2wpkh({ pubkey }).address : bitcoinjs.payments.p2pkh({ pubkey }).address;
return address;
}
// this function is different from HDLegacyP2PKHWallet._getNodeAddressByIndex.
// It takes _external_segwit_index _internal_segwit_index for account
// and starts to generate segwit addresses if index more than them
_getNodeAddressByIndex(node, index) {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
}
const node = bitcoinjs.bip32.fromBase58(this.getXpub());
const address = bitcoinjs.payments.p2pkh({
pubkey: node.derive(0).derive(index).publicKey,
}).address;
if (node === 1) {
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
}
return (this.external_addresses_cache[index] = address);
let p2wpkh = false;
if (
(node === 0 && this._external_segwit_index !== null && index >= this._external_segwit_index) ||
(node === 1 && this._internal_segwit_index !== null && index >= this._internal_segwit_index)
) {
p2wpkh = true;
}
const address = this._calcNodeAddressByIndex(node, index, p2wpkh);
if (node === 0) {
return (this.external_addresses_cache[index] = address);
}
if (node === 1) {
return (this.internal_addresses_cache[index] = address);
}
}
_getExternalAddressByIndex(index) {
return this._getNodeAddressByIndex(0, index);
}
_getInternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
const node = bitcoinjs.bip32.fromBase58(this.getXpub());
const address = bitcoinjs.payments.p2pkh({
pubkey: node.derive(1).derive(index).publicKey,
}).address;
return (this.internal_addresses_cache[index] = address);
return this._getNodeAddressByIndex(1, index);
}
_getExternalWIFByIndex(index) {
@ -81,7 +121,96 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return child.toWIF();
}
allowSendMax() {
return true;
async fetchBalance() {
try {
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) {
// doing binary search for last used addresses external/internal and legacy/bech32:
const [nextFreeExternalLegacy, nextFreeInternalLegacy] = await Promise.all([
this._binarySearchIteration(0, 1000, 0, false),
this._binarySearchIteration(0, 1000, 1, false),
]);
const [nextFreeExternalBech32, nextFreeInternalBech32] = await Promise.all([
this._binarySearchIteration(nextFreeExternalLegacy, nextFreeExternalLegacy + 1000, 0, true),
this._binarySearchIteration(nextFreeInternalLegacy, nextFreeInternalLegacy + 1000, 1, true),
]);
// trying to detect if segwit activated. This condition can be deleted when BRD will enable segwit by default
if (nextFreeExternalLegacy < nextFreeExternalBech32) {
this._external_segwit_index = nextFreeExternalLegacy;
}
this.next_free_address_index = nextFreeExternalBech32;
this._internal_segwit_index = nextFreeInternalLegacy; // force segwit for change
this.next_free_change_address_index = nextFreeInternalBech32;
} // end rescanning fresh wallet
// finally fetching balance
await this._fetchBalance();
} catch (err) {
console.warn(err);
}
}
async _binarySearchIteration(startIndex, endIndex, node = 0, p2wpkh = false) {
const gerenateChunkAddresses = chunkNum => {
const ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._calcNodeAddressByIndex(node, c, p2wpkh));
}
return ret;
};
let lastChunkWithUsedAddressesNum = null;
let lastHistoriesWithUsedAddresses = null;
for (let c = 0; c < Math.round(endIndex / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
} else {
// empty chunk. no sense searching more chunks
break;
}
}
let lastUsedIndex = startIndex;
if (lastHistoriesWithUsedAddresses) {
// now searching for last used address in batch lastChunkWithUsedAddressesNum
for (
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
c++
) {
const address = this._calcNodeAddressByIndex(node, c, p2wpkh);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
}
}
}
return lastUsedIndex;
}
_getDerivationPathByAddress(address, BIP = 0) {
const path = `m/${BIP}'`;
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
}
return false;
}
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
// hack to use
// AbstractHDElectrumWallet._addPsbtInput for bech32 address
// HDLegacyP2PKHWallet._addPsbtInput for legacy address
const ProxyClass = input.address.startsWith('bc1') ? AbstractHDElectrumWallet : HDLegacyP2PKHWallet;
const proxy = new ProxyClass();
return proxy._addPsbtInput.apply(this, [psbt, input, sequence, masterFingerprintBuffer]);
}
}

View File

@ -0,0 +1,53 @@
/* global it, jasmine, afterAll, beforeAll */
import * as bitcoin from 'bitcoinjs-lib';
import { HDLegacyBreadwalletWallet } from '../../class';
import assert from 'assert';
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
global.tls = require('tls'); // needed by Electrum client. For RN it is proviced in shim.js
const BlueElectrum = require('../../blue_modules/BlueElectrum'); // so it connects ASAP
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
afterAll(async () => {
// after all tests we close socket so the test suite can actually terminate
BlueElectrum.forceDisconnect();
await sleep(20);
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
it('Legacy HD Breadwallet can fetch balance and create transaction', async () => {
if (!process.env.HD_MNEMONIC_BREAD) {
console.error('process.env.HD_MNEMONIC_BREAD not set, skipped');
return;
}
const wallet = new HDLegacyBreadwalletWallet();
wallet.setSecret(process.env.HD_MNEMONIC_BREAD);
await wallet.fetchBalance();
// m/0'/0/1 1K9ofAnenRn1aR9TMMTreiin9ddjKWbS7z x 0.0001
// m/0'/0/2 bc1qh0vtrnjn7zs99j4n6xaadde95ctnnvegh9l2jn x 0.00032084
// m/0'/1/0 1A9Sc4opR6c7Ui6NazECiGmsmnUPh2WeHJ x 0.00016378 BTC
// m/0'/1/1 bc1qksn08tz44fvnnrpgrrexvs9526t6jg3xnj9tpc x 0.00012422
// 0.0001 + 0.00016378 + 0.00012422 + 0.00032084 = 0.00070884
assert.strictEqual(wallet.getBalance(), 70884);
// try to create a tx
await wallet.fetchUtxo();
const { tx } = wallet.createTransaction(
wallet.getUtxo(),
[{ address: 'bc1q47efz9aav8g4mnnz9r6ql4pf48phy3g509p7gx' }],
1,
'bc1qk9hvkxqsqmps6ex3qawr79rvtg8es4ecjfu5v0',
);
const transaction = bitcoin.Transaction.fromHex(tx.toHex());
assert.ok(transaction.ins.length === 4);
assert.strictEqual(transaction.outs.length, 1);
});

View File

@ -1,6 +1,6 @@
/* global it */
import assert from 'assert';
import { HDLegacyBreadwalletWallet } from '../../class';
const assert = require('assert');
it('Legacy HD Breadwallet works', async () => {
if (!process.env.HD_MNEMONIC_BREAD) {
@ -11,22 +11,32 @@ it('Legacy HD Breadwallet works', async () => {
hdBread.setSecret(process.env.HD_MNEMONIC_BREAD);
assert.strictEqual(hdBread.validateMnemonic(), true);
assert.strictEqual(hdBread._getExternalAddressByIndex(0), '1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU');
assert.ok(hdBread.getAllExternalAddresses().includes('1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU'));
assert.strictEqual(hdBread._getInternalAddressByIndex(0), '1JLvA5D7RpWgChb4A5sFcLNrfxYbyZdw3V');
assert.strictEqual(hdBread._getExternalWIFByIndex(0), 'L25CoHfqWKR5byQhgp4M8sW1roifBteD3Lj3zCGNcV4JXhbxZ93F');
assert.strictEqual(hdBread._getInternalWIFByIndex(0), 'KyEQuB73eueeS7D6iBJrNSvkD1kkdkJoUsavuxGXv5fxWkPJxt96');
assert.strictEqual(hdBread._getExternalAddressByIndex(0), '1M1UphJDb1mpXV3FVEg6b2qqaBieNuaNrt');
assert.strictEqual(hdBread._getInternalAddressByIndex(0), '1A9Sc4opR6c7Ui6NazECiGmsmnUPh2WeHJ');
hdBread._internal_segwit_index = 2;
hdBread._external_segwit_index = 2;
assert.ok(hdBread._getExternalAddressByIndex(0).startsWith('1'));
assert.ok(hdBread._getInternalAddressByIndex(0).startsWith('1'));
assert.strictEqual(hdBread._getExternalAddressByIndex(2), 'bc1qh0vtrnjn7zs99j4n6xaadde95ctnnvegh9l2jn');
assert.strictEqual(hdBread._getInternalAddressByIndex(2), 'bc1qk9hvkxqsqmps6ex3qawr79rvtg8es4ecjfu5v0');
assert.strictEqual(hdBread._getDerivationPathByAddress('1M1UphJDb1mpXV3FVEg6b2qqaBieNuaNrt'), "m/0'/0/0");
assert.strictEqual(hdBread._getDerivationPathByAddress('bc1qk9hvkxqsqmps6ex3qawr79rvtg8es4ecjfu5v0'), "m/0'/1/2");
assert.strictEqual(
hdBread._getPubkeyByAddress(hdBread._getExternalAddressByIndex(0)).toString('hex'),
'0354d804a7943eb61ec13deef44586510506889175dc2f3a375867e4796debf2a9',
'029ba027f3f0a9fa69ce680a246198d56a3b047108f26791d1e4aa2d10e7e7a29a',
);
assert.strictEqual(
hdBread._getPubkeyByAddress(hdBread._getInternalAddressByIndex(0)).toString('hex'),
'02d241fadf3e48ff30a93360f6ef255cc3a797c588c907615d096510a918f46dce',
'03074225b31a95af63de31267104e07863d892d291a33ef5b2b32d59c772d5c784',
);
assert.strictEqual(
hdBread.getXpub(),
'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh',
'xpub68hPk9CrHimZMBQEja43qWRC2TuXmCDdgZcR5YMebr38XatUEPu2Q2oaBViSMshDcyuMDGkGbTS2aqNHFKdcN1sFWaZgK6SLg84dtN7Ym64',
);
assert.ok(hdBread.getAllExternalAddresses().includes('1M1UphJDb1mpXV3FVEg6b2qqaBieNuaNrt'));
assert.ok(hdBread.getAllExternalAddresses().includes('bc1qh0vtrnjn7zs99j4n6xaadde95ctnnvegh9l2jn'));
});