diff --git a/HDWallet.test.js b/HDWallet.test.js new file mode 100644 index 000000000..731c473f4 --- /dev/null +++ b/HDWallet.test.js @@ -0,0 +1,88 @@ +/* global it, jasmine */ +import { + SegwitP2SHWallet, + SegwitBech32Wallet, + HDSegwitP2SHWallet, + HDLegacyBreadwalletWallet, +} from './class'; +let assert = require('assert'); + +it.only('can convert witness to address', () => { + let address = SegwitP2SHWallet.witnessToAddress( + '035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8', + ); + assert.equal(address, '34ZVGb3gT8xMLT6fpqC6dNVqJtJmvdjbD7'); + + address = SegwitBech32Wallet.witnessToAddress( + '035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8', + ); + assert.equal(address, 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv'); +}); + +it('can create a BIP49', function() { + let bip39 = require('bip39'); + let bitcoin = require('bitcoinjs-lib'); + let mnemonic = + 'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode'; + assert.ok(bip39.validateMnemonic(mnemonic)); + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + + let path = "m/49'/0'/0'/0/0"; + let child = root.derivePath(path); + + let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer()); + let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash); + let addressBytes = bitcoin.crypto.hash160(scriptSig); + let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes); + let address = bitcoin.address.fromOutputScript( + outputScript, + bitcoin.networks.bitcoin, + ); + + assert.equal(address, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); + + // checking that WIF from HD corresponds to derived segwit address + let Segwit = new SegwitP2SHWallet(); + Segwit.setSecret(child.keyPair.toWIF()); + assert.equal(address, Segwit.getAddress()); + + // testing our class + let hd = new HDSegwitP2SHWallet(); + hd.setSecret(mnemonic); + assert.equal(address, hd._getExternalAddressByIndex(0)); + assert.equal(true, hd.validateMnemonic()); + + assert.equal(child.keyPair.toWIF(), hd._getExternalWIFByIndex(0)); +}); + +it('HD breadwallet works', async function() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000; + let hdBread = new HDLegacyBreadwalletWallet(); + hdBread.setSecret( + 'high relief amount witness try remember adult destroy puppy fox giant peace', + ); + console.log( + 'bread 0/0 = ', + hdBread._getExternalAddressByIndex(0), + 'bread 1/0 = ', + hdBread._getInternalAddressByIndex(0), + 'valid = ', + hdBread.validateMnemonic(), + ); + console.log(hdBread.getXpub()); + await hdBread.fetchBalance(); + console.log(hdBread.balance); + + await hdBread.fetchTransactions(); + console.log('tx count = ', hdBread.transactions.length); + + console.log( + hdBread.next_free_address_index, + hdBread.next_free_change_address_index, + ); + + // let bitcoin = require('bitcoinjs-lib'); + // let node = bitcoin.HDNode.fromBase58( hdBread.getXpub() ); + // console.log( "normal address", node.derive(0).derive(0).getAddress()); +}); diff --git a/class/hd-legacy-breadwallet-wallet.js b/class/hd-legacy-breadwallet-wallet.js new file mode 100644 index 000000000..ff4a7678d --- /dev/null +++ b/class/hd-legacy-breadwallet-wallet.js @@ -0,0 +1,62 @@ +import { HDSegwitP2SHWallet } from './'; +const bitcoin = require('bitcoinjs-lib'); +const bip39 = require('bip39'); + +/** + * HD Wallet (BIP39). + * In particular, BIP49 (P2SH Segwit) https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki + */ +export class HDLegacyBreadwalletWallet extends HDSegwitP2SHWallet { + constructor() { + super(); + this.type = 'HDLegacyBreadwallet'; + } + + getTypeReadable() { + return 'HD Legacy Breadwallet-compatible (P2PKH)'; + } + + getAddress() { + // TODO: derive from hierarchy, return next free address + } + + /** + * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/584 + * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/914 + * @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/997 + */ + getXpub() { + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + + let path = "m/0'"; + let child = root.derivePath(path).neutered(); + return child.toBase58(); + } + + _getExternalAddressByIndex(index) { + index = index * 1; // cast to int + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + + let path = "m/0'/0/" + index; + let child = root.derivePath(path); + + return child.getAddress(); + } + + _getInternalAddressByIndex(index) { + index = index * 1; // cast to int + + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + + let path = "m/0'/1/" + index; + let child = root.derivePath(path); + + return child.getAddress(); + } +} diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js new file mode 100644 index 000000000..96e61329a --- /dev/null +++ b/class/hd-segwit-p2sh-wallet.js @@ -0,0 +1,233 @@ +import { LegacyWallet } from './legacy-wallet'; +import { SegwitP2SHWallet } from './segwit-p2sh-wallet'; +import Frisbee from 'frisbee'; +const bitcoin = require('bitcoinjs-lib'); +const bip39 = require('bip39'); + +/** + * HD Wallet (BIP39). + * In particular, BIP49 (P2SH Segwit) https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki + */ +export class HDSegwitP2SHWallet extends LegacyWallet { + constructor() { + super(); + this.type = 'HDsegwitP2SH'; + this.next_free_address_index = 0; + this.next_free_change_address_index = 0; + this.internal_addresses_cache = {}; // index => address + this.external_addresses_cache = {}; // index => address + } + + validateMnemonic() { + return bip39.validateMnemonic(this.secret); + } + + getTypeReadable() { + return 'HD SegWit (P2SH)'; + } + + /** + * Derives from hierarchy, returns next free address + * (the one that has no transactions). Looks for several, + * gve ups if none found, and returns the used one + * + * @return {Promise.} + */ + async getAddressAsync() { + // looking for free external address + let freeAddress = ''; + let c; + for (c = -1; c < 5; c++) { + let Segwit = new SegwitP2SHWallet(); + Segwit.setSecret( + this._getExternalWIFByIndex(this.next_free_address_index + c), + ); + await Segwit.fetchTransactions(); + if (Segwit.transactions.length === 0) { + // found free address + freeAddress = Segwit.getAddress(); + this.next_free_address_index += c + 1; // now points to the one _after_ + break; + } + } + + if (!freeAddress) { + // could not find in cycle above, give up + freeAddress = this._getExternalAddressByIndex( + this.next_free_address_index + c, + ); // we didnt check this one, maybe its free + this.next_free_address_index += c + 1; // now points to the one _after_ + } + + return freeAddress; + } + + _getExternalWIFByIndex(index) { + index = index * 1; // cast to int + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + let path = "m/49'/0'/0'/0/" + index; + let child = root.derivePath(path); + return child.keyPair.toWIF(); + } + + _getInternalWIFByIndex(index) { + index = index * 1; // cast to int + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + let path = "m/49'/0'/0'/1/" + index; + let child = root.derivePath(path); + return child.keyPair.toWIF(); + } + + _getExternalAddressByIndex(index) { + index = index * 1; // cast to int + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + let path = "m/49'/0'/0'/0/" + index; + let child = root.derivePath(path); + + let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer()); + let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash); + let addressBytes = bitcoin.crypto.hash160(scriptSig); + let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes); + let address = bitcoin.address.fromOutputScript( + outputScript, + bitcoin.networks.bitcoin, + ); + + return address; + } + + _getInternalAddressByIndex(index) { + index = index * 1; // cast to int + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + + let path = "m/49'/0'/0'/1/" + index; + let child = root.derivePath(path); + + let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer()); + let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash); + let addressBytes = bitcoin.crypto.hash160(scriptSig); + let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes); + let address = bitcoin.address.fromOutputScript( + outputScript, + bitcoin.networks.bitcoin, + ); + + return address; + } + + async fetchBalance() { + const api = new Frisbee({ baseURI: 'https://blockchain.info' }); + + let response = await api.get('/balance?active=' + this.getXpub()); + + // console.log(response); + if (response && response.body) { + for (let xpub of Object.keys(response.body)) { + this.balance = response.body[xpub].final_balance / 100000000; + } + } else { + throw new Error('Could not fetch balance from API'); + } + } + + async fetchTransactions() { + const api = new Frisbee({ baseURI: 'https://blockchain.info' }); + this.transactions = []; + let offset = 0; + + while (1) { + console.log('fetching ', offset); + let response = await api.get( + '/multiaddr?active=' + this.getXpub() + '&n=100&offset=' + offset, + ); + + if (response && response.body) { + if (response.body.txs && response.body.txs.length === 0) { + break; + } + + // processing TXs and adding to internal memory + console.log('response.body.txs = ', response.body.txs.length); + if (response.body.txs) { + for (let tx of response.body.txs) { + let value = 0; + + for (let input of tx.inputs) { + // ----- INPUTS + if (input.prev_out.xpub) { + // sent FROM US + value -= input.prev_out.value; + + // setting internal caches to help ourselves in future... + let path = input.prev_out.xpub.path.split('/'); + if (path[path.length - 2] === '1') { + // change address + this.next_free_change_address_index = Math.max( + path[path.length - 1] * 1 + 1, + this.next_free_change_address_index, + ); + // setting to point to last maximum known change address + 1 + } + if (path[path.length - 2] === '0') { + // main (aka external) address + this.next_free_address_index = Math.max( + path[path.length - 1] * 1 + 1, + this.next_free_address_index, + ); + // setting to point to last maximum known main address + 1 + } + // done with cache + } + } + + for (let output of tx.out) { + // ----- OUTPUTS + if (output.xpub) { + // sent TO US (change) + value += output.value; + + // setting internal caches to help ourselves in future... + let path = output.xpub.path.split('/'); + if (path[path.length - 2] === '1') { + // change address + this.next_free_change_address_index = Math.max( + path[path.length - 1] * 1 + 1, + this.next_free_change_address_index, + ); + // setting to point to last maximum known change address + 1 + } + if (path[path.length - 2] === '0') { + // main (aka external) address + this.next_free_address_index = Math.max( + path[path.length - 1] * 1 + 1, + this.next_free_address_index, + ); + // setting to point to last maximum known main address + 1 + } + // done with cache + } + } + + tx.value = value / 100000000; + + this.transactions.push(tx); + } + } else { + break; // error ? + } + } else { + throw new Error('Could not fetch balance from API'); // breaks here + } + + offset += 100; + } + } +} diff --git a/class/index.js b/class/index.js index d683d9280..efc5e3c01 100644 --- a/class/index.js +++ b/class/index.js @@ -4,3 +4,5 @@ export * from './constants'; export * from './legacy-wallet'; export * from './segwit-bech-wallet'; export * from './segwit-p2sh-wallet'; +export * from './hd-segwit-p2sh-wallet'; +export * from './hd-legacy-breadwallet-wallet'; diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index fc47b84ce..9a3b77d15 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -1,5 +1,6 @@ /* global fetch */ import { AbstractWallet } from './abstract-wallet'; +import { SegwitBech32Wallet } from './'; import { useBlockcypherTokens } from './constants'; import Frisbee from 'frisbee'; const isaac = require('isaac');