From 0ffb127e82c82f586ec4f597ecb6e2016eeacc80 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Wed, 1 Aug 2018 00:52:03 +0100 Subject: [PATCH] WIP: HD wallet --- HDWallet.test.js | 17 ++++++ class/abstract-hd-wallet.js | 1 - class/hd-legacy-p2pkh-wallet.js | 50 ++++++++++++------ class/hd-segwit-p2sh-wallet.js | 92 +++++++++++++++++++++++++-------- class/legacy-wallet.js | 2 +- 5 files changed, 122 insertions(+), 40 deletions(-) diff --git a/HDWallet.test.js b/HDWallet.test.js index 6d76f0aad..cdc26085f 100644 --- a/HDWallet.test.js +++ b/HDWallet.test.js @@ -42,6 +42,15 @@ it('can create a Segwit HD (BIP49)', async function() { assert.equal(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress); }); +it('Segwit HD (BIP49) can generate addressess only via ypub', async function() { + let ypub = 'ypub6WhHmKBmHNjcrUVNCa3sXduH9yxutMipDcwiKW31vWjcMbfhQHjXdyx4rqXbEtVgzdbhFJ5mZJWmfWwnP4Vjzx97admTUYKQt6b9D7jjSCp'; + let hd = new HDSegwitP2SHWallet(); + hd._xpub = ypub; + assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', hd._getExternalAddressByIndex(0)); + assert.equal('35p5LwCAE7mH2css7onyQ1VuS1jgWtQ4U3', hd._getExternalAddressByIndex(1)); + assert.equal('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0)); +}); + it('can generate Segwit HD (BIP49)', async () => { let hd = new HDSegwitP2SHWallet(); let hashmap = {}; @@ -117,6 +126,14 @@ it('can create a Legacy HD (BIP44)', async function() { assert.equal(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress); }); +it('Legacy HD (BIP44) can generate addressess based on xpub', async function() { + let xpub = 'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'; + let hd = new HDLegacyP2PKHWallet(); + hd._xpub = xpub; + assert.equal(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); + assert.equal(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX'); +}); + it('HD breadwallet works', async function() { jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000; let hdBread = new HDLegacyBreadwalletWallet(); diff --git a/class/abstract-hd-wallet.js b/class/abstract-hd-wallet.js index ba17e0b72..465fe9cce 100644 --- a/class/abstract-hd-wallet.js +++ b/class/abstract-hd-wallet.js @@ -126,7 +126,6 @@ export class AbstractHDWallet extends LegacyWallet { return freeAddress; } - /** * Derives from hierarchy, returns next free CHANGE address * (the one that has no transactions). Looks for several, diff --git a/class/hd-legacy-p2pkh-wallet.js b/class/hd-legacy-p2pkh-wallet.js index 63474a2da..d83f14a12 100644 --- a/class/hd-legacy-p2pkh-wallet.js +++ b/class/hd-legacy-p2pkh-wallet.js @@ -56,29 +56,47 @@ export class HDLegacyP2PKHWallet extends AbstractHDWallet { _getExternalAddressByIndex(index) { index = index * 1; // cast to int if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit - let mnemonic = this.secret; - let seed = bip39.mnemonicToSeed(mnemonic); - let root = bitcoin.HDNode.fromSeedBuffer(seed); - let path = "m/44'/0'/0'/0/" + index; - let child = root.derivePath(path); + if (!this._xpub) { + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); + let path = "m/44'/0'/0'/0/" + index; + let child = root.derivePath(path); - let w = new LegacyWallet(); - w.setSecret(child.keyPair.toWIF()); - return (this.external_addresses_cache[index] = w.getAddress()); + let w = new LegacyWallet(); + w.setSecret(child.keyPair.toWIF()); + return (this.external_addresses_cache[index] = w.getAddress()); + } else { + let node = bitcoin.HDNode.fromBase58(this._xpub); + let address = node + .derive(0) + .derive(0) + .getAddress(); + return (this.external_addresses_cache[index] = address); + } } _getInternalAddressByIndex(index) { index = index * 1; // cast to int if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit - let mnemonic = this.secret; - let seed = bip39.mnemonicToSeed(mnemonic); - let root = bitcoin.HDNode.fromSeedBuffer(seed); + if (!this._xpub) { + let mnemonic = this.secret; + let seed = bip39.mnemonicToSeed(mnemonic); + let root = bitcoin.HDNode.fromSeedBuffer(seed); - let path = "m/44'/0'/0'/1/" + index; - let child = root.derivePath(path); + let path = "m/44'/0'/0'/1/" + index; + let child = root.derivePath(path); - let w = new LegacyWallet(); - w.setSecret(child.keyPair.toWIF()); - return (this.internal_addresses_cache[index] = w.getAddress()); + let w = new LegacyWallet(); + w.setSecret(child.keyPair.toWIF()); + return (this.internal_addresses_cache[index] = w.getAddress()); + } else { + let node = bitcoin.HDNode.fromBase58(this._xpub); + let address = node + .derive(1) + .derive(0) + .getAddress(); + return (this.internal_addresses_cache[index] = address); + } } } diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index 6f363c77f..caa86d93a 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -89,34 +89,82 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { _getExternalAddressByIndex(index) { index = index * 1; // cast to int if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit - 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); + if (!this._xpub) { + 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); - return (this.external_addresses_cache[index] = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin)); + 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); + return (this.external_addresses_cache[index] = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin)); + } else { + let b58 = require('bs58check'); + // eslint-disable-next-line + function ypubToXpub(ypub) { + var data = b58.decode(ypub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + return b58.encode(data); + } + // eslint-disable-next-line + function nodeToP2shSegwitAddress(hdNode) { + let pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer(); + let hash = bitcoin.crypto.hash160(pubkeyBuf); + let redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash); + let hash2 = bitcoin.crypto.hash160(redeemScript); + let scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2); + return bitcoin.address.fromOutputScript(scriptPubkey); + } + let xpub = ypubToXpub(this._xpub); + let hdNode = bitcoin.HDNode.fromBase58(xpub); + let address = nodeToP2shSegwitAddress(hdNode.derive(0).derive(index)); + return (this.external_addresses_cache[index] = address); + } } _getInternalAddressByIndex(index) { index = index * 1; // cast to int if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit - let mnemonic = this.secret; - let seed = bip39.mnemonicToSeed(mnemonic); - let root = bitcoin.HDNode.fromSeedBuffer(seed); + if (!this._xpub) { + 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 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); - return (this.internal_addresses_cache[index] = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin)); + 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); + return (this.internal_addresses_cache[index] = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin)); + } else { + let b58 = require('bs58check'); + // eslint-disable-next-line + function ypubToXpub(ypub) { + var data = b58.decode(ypub); + data = data.slice(4); + data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); + return b58.encode(data); + } + // eslint-disable-next-line + function nodeToP2shSegwitAddress(hdNode) { + let pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer(); + let hash = bitcoin.crypto.hash160(pubkeyBuf); + let redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash); + let hash2 = bitcoin.crypto.hash160(redeemScript); + let scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2); + return bitcoin.address.fromOutputScript(scriptPubkey); + } + let xpub = ypubToXpub(this._xpub); + let hdNode = bitcoin.HDNode.fromBase58(xpub); + let address = nodeToP2shSegwitAddress(hdNode.derive(1).derive(index)); + return (this.internal_addresses_cache[index] = address); + } } /** @@ -157,8 +205,8 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { } let addresses = this.usedAddresses.join('|'); - addresses += '|' + this._getExternalAddressByIndex(this.next_free_address_index) - addresses += '|' + this._getInternalAddressByIndex(this.next_free_change_address_index) + addresses += '|' + this._getExternalAddressByIndex(this.next_free_address_index); + addresses += '|' + this._getInternalAddressByIndex(this.next_free_change_address_index); const api = new Frisbee({ baseURI: 'https://blockchain.info' }); this.transactions = []; diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index 61c5f5ce4..c9a65338c 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -256,7 +256,7 @@ export class LegacyWallet extends AbstractWallet { if (tx.block_height && tx.block_height === -1) { // unconfirmed console.log(tx); - if ((+new Date(tx.received)) < ((+new Date()) - 3600*24*1000)) { + if (+new Date(tx.received) < +new Date() - 3600 * 24 * 1000) { // nop, too old unconfirmed tx - skipping it } else { txsUnconf.push(tx);