From f44e662dbe19fac35d8cbd9d8fa09d8d3682664a Mon Sep 17 00:00:00 2001
From: Thomas Kerin <thomas.kerin@bitmaintech.com>
Date: Sun, 4 Mar 2018 20:48:55 +0100
Subject: [PATCH 1/2] allow custom bip32 prefixes, and varying types of
 addresses returned get hdnode.getAddress

---
 src/hdnode.js      |  56 ++++++++++++++------
 src/keytoscript.js | 124 +++++++++++++++++++++++++++++++++++++++++++++
 src/networks.js    |  23 +++++++--
 test/hdaddress.js  |  95 ++++++++++++++++++++++++++++++++++
 test/hdnode.js     |  13 +++--
 5 files changed, 286 insertions(+), 25 deletions(-)
 create mode 100644 src/keytoscript.js
 create mode 100644 test/hdaddress.js

diff --git a/src/hdnode.js b/src/hdnode.js
index a96b20c..8f98846 100644
--- a/src/hdnode.js
+++ b/src/hdnode.js
@@ -1,6 +1,7 @@
 var Buffer = require('safe-buffer').Buffer
 var base58check = require('bs58check')
 var bcrypto = require('./crypto')
+var baddress = require('./address')
 var createHmac = require('create-hmac')
 var typeforce = require('typeforce')
 var types = require('./types')
@@ -9,12 +10,15 @@ var NETWORKS = require('./networks')
 var BigInteger = require('bigi')
 var ECPair = require('./ecpair')
 
+// var KeyToScript = require('./keytoscript')
 var ecurve = require('ecurve')
 var curve = ecurve.getCurveByName('secp256k1')
 
-function HDNode (keyPair, chainCode) {
+function HDNode (keyPair, chainCode, prefix) {
   typeforce(types.tuple('ECPair', types.Buffer256bit), arguments)
-
+  if (!prefix) {
+    prefix = keyPair.network.bip32
+  }
   if (!keyPair.compressed) throw new TypeError('BIP32 only allows compressed keyPairs')
 
   this.keyPair = keyPair
@@ -22,13 +26,14 @@ function HDNode (keyPair, chainCode) {
   this.depth = 0
   this.index = 0
   this.parentFingerprint = 0x00000000
+  this.prefix = prefix
 }
 
 HDNode.HIGHEST_BIT = 0x80000000
 HDNode.LENGTH = 78
 HDNode.MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8')
 
-HDNode.fromSeedBuffer = function (seed, network) {
+HDNode.fromSeedBuffer = function (seed, network, prefix) {
   typeforce(types.tuple(types.Buffer, types.maybe(types.Network)), arguments)
 
   if (seed.length < 16) throw new TypeError('Seed should be at least 128 bits')
@@ -45,14 +50,21 @@ HDNode.fromSeedBuffer = function (seed, network) {
     network: network
   })
 
-  return new HDNode(keyPair, IR)
+  return new HDNode(keyPair, IR, prefix)
 }
 
-HDNode.fromSeedHex = function (hex, network) {
-  return HDNode.fromSeedBuffer(Buffer.from(hex, 'hex'), network)
+HDNode.fromSeedHex = function (hex, network, prefix) {
+  return HDNode.fromSeedBuffer(Buffer.from(hex, 'hex'), network, prefix)
 }
 
-HDNode.fromBase58 = function (string, networks) {
+/**
+ *
+ * @param {string} string
+ * @param {Array<network>|network} networks
+ * @param {Array<prefix>} prefixes - only used if networks is object/undefined(so bitcoin).
+ * @returns {HDNode}
+ */
+HDNode.fromBase58 = function (string, networks, prefixes) {
   var buffer = base58check.decode(string)
   if (buffer.length !== 78) throw new Error('Invalid buffer length')
 
@@ -61,6 +73,7 @@ HDNode.fromBase58 = function (string, networks) {
   var network
 
   // list of networks?
+  var prefix
   if (Array.isArray(networks)) {
     network = networks.filter(function (x) {
       return version === x.bip32.private ||
@@ -69,13 +82,26 @@ HDNode.fromBase58 = function (string, networks) {
 
     if (!network) throw new Error('Unknown network version')
 
+    // we found a network by it's bip32 prefixes, use that
+    prefix = network.bip32
+
   // otherwise, assume a network object (or default to bitcoin)
   } else {
     network = networks || NETWORKS.bitcoin
+    if (prefixes) {
+      prefix = prefixes.filter(function (x) {
+        return version === x.private ||
+               version === x.public
+      }).pop()
+    } else {
+      // no special prefixes to consider, use networks bip32 prefix
+      prefix = network.bip32
+    }
   }
 
-  if (version !== network.bip32.private &&
-    version !== network.bip32.public) throw new Error('Invalid network version')
+  // sanity check the version against the prefix
+  if (version !== prefix.private &&
+    version !== prefix.public) throw new Error('Invalid network version')
 
   // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, ...
   var depth = buffer[4]
@@ -114,7 +140,7 @@ HDNode.fromBase58 = function (string, networks) {
     keyPair = new ECPair(null, Q, { network: network })
   }
 
-  var hd = new HDNode(keyPair, chainCode)
+  var hd = new HDNode(keyPair, chainCode, prefix)
   hd.depth = depth
   hd.index = index
   hd.parentFingerprint = parentFingerprint
@@ -123,7 +149,8 @@ HDNode.fromBase58 = function (string, networks) {
 }
 
 HDNode.prototype.getAddress = function () {
-  return this.keyPair.getAddress()
+  var data = this.prefix.scriptFactory.convert(this.keyPair)
+  return baddress.fromOutputScript(data.scriptPubKey, this.keyPair.network)
 }
 
 HDNode.prototype.getIdentifier = function () {
@@ -147,7 +174,7 @@ HDNode.prototype.neutered = function () {
     network: this.keyPair.network
   })
 
-  var neutered = new HDNode(neuteredKeyPair, this.chainCode)
+  var neutered = new HDNode(neuteredKeyPair, this.chainCode, this.prefix)
   neutered.depth = this.depth
   neutered.index = this.index
   neutered.parentFingerprint = this.parentFingerprint
@@ -167,8 +194,7 @@ HDNode.prototype.toBase58 = function (__isPrivate) {
   if (__isPrivate !== undefined) throw new TypeError('Unsupported argument in 2.0.0')
 
   // Version
-  var network = this.keyPair.network
-  var version = (!this.isNeutered()) ? network.bip32.private : network.bip32.public
+  var version = (!this.isNeutered()) ? this.prefix.private : this.prefix.public
   var buffer = Buffer.allocUnsafe(78)
 
   // 4 bytes: version bytes
@@ -268,7 +294,7 @@ HDNode.prototype.derive = function (index) {
     })
   }
 
-  var hd = new HDNode(derivedKeyPair, IR)
+  var hd = new HDNode(derivedKeyPair, IR, this.prefix)
   hd.depth = this.depth + 1
   hd.index = index
   hd.parentFingerprint = this.getFingerprint().readUInt32BE(0)
diff --git a/src/keytoscript.js b/src/keytoscript.js
new file mode 100644
index 0000000..c16272d
--- /dev/null
+++ b/src/keytoscript.js
@@ -0,0 +1,124 @@
+var bcrypto = require('./crypto')
+var btemplates = require('./templates')
+
+function checkAllowedP2sh (keyFactory) {
+  if (!(keyFactory instanceof P2pkhFactory ||
+      keyFactory instanceof P2wpkhFactory ||
+      keyFactory instanceof P2pkFactory
+    )) {
+    throw new Error('Unsupported script factory for P2SH')
+  }
+}
+
+function checkAllowedP2wsh (keyFactory) {
+  if (!(keyFactory instanceof P2pkhFactory ||
+      keyFactory instanceof P2pkFactory
+    )) {
+    throw new Error('Unsupported script factory for P2SH')
+  }
+}
+
+var P2pkFactory = function () {
+
+}
+
+/**
+ * @param {bitcoin.ECPair} key
+ */
+P2pkFactory.prototype.convert = function (key) {
+  return {
+    scriptPubKey: btemplates.pubKey.output.encode(key.getPublicKeyBuffer()),
+    signData: {}
+  }
+}
+
+var P2pkhFactory = function () {
+
+}
+
+/**
+ * @param {bitcoin.ECPair} key
+ */
+P2pkhFactory.prototype.convert = function (key) {
+  var hash160 = bcrypto.hash160(key.getPublicKeyBuffer())
+  return {
+    scriptPubKey: btemplates.pubKeyHash.output.encode(hash160),
+    signData: {}
+  }
+}
+
+var P2wpkhFactory = function () {
+
+}
+
+/**
+ * @param {bitcoin.ECPair} key
+ */
+P2wpkhFactory.prototype.convert = function (key) {
+  var hash160 = bcrypto.hash160(key.getPublicKeyBuffer())
+  return {
+    scriptPubKey: btemplates.witnessPubKeyHash.output.encode(hash160),
+    signData: {}
+  }
+}
+
+var P2shFactory = function (keyFactory) {
+  checkAllowedP2sh(keyFactory)
+  this.factory = keyFactory
+}
+
+P2shFactory.prototype.convert = function (key) {
+  var detail = this.factory.convert(key)
+  var hash160 = bcrypto.hash160(detail.scriptPubKey)
+  return {
+    scriptPubKey: btemplates.scriptHash.output.encode(hash160),
+    signData: {
+      redeemScript: detail.scriptPubKey
+    }
+  }
+}
+
+var P2wshFactory = function (keyFactory) {
+  checkAllowedP2wsh(keyFactory)
+  this.factory = keyFactory
+}
+
+P2wshFactory.prototype.convert = function (key) {
+  var detail = this.factory.convert(key)
+  var hash160 = bcrypto.hash160(detail.scriptPubKey)
+  return {
+    scriptPubKey: btemplates.scriptHash.output.encode(hash160),
+    signData: {
+      redeemScript: detail.scriptPubKey
+    }
+  }
+}
+
+var P2shP2wshFactory = function (keyFactory) {
+  checkAllowedP2wsh(keyFactory)
+  this.factory = keyFactory
+}
+
+P2shP2wshFactory.prototype.convert = function (key) {
+  var detail = this.factory.convert(key)
+  var sha256 = bcrypto.sha256(detail.scriptPubKey)
+  var wp = btemplates.witnessScriptHash.output.encode(sha256)
+  var hash160 = bcrypto.hash160(wp)
+  var spk = btemplates.scriptHash.output.encode(hash160)
+  return {
+    scriptPubKey: spk,
+    signData: {
+      redeemScript: wp,
+      witnessScript: detail.scriptPubKey
+    }
+  }
+}
+
+module.exports = {
+  P2pkhFactory: P2pkhFactory,
+  P2wpkhFactory: P2wpkhFactory,
+  P2pkFactory: P2pkFactory,
+  P2shFactory: P2shFactory,
+  P2wshFactory: P2wshFactory,
+  P2shP2wshFactory: P2shP2wshFactory
+}
diff --git a/src/networks.js b/src/networks.js
index 43cd5fc..bfef034 100644
--- a/src/networks.js
+++ b/src/networks.js
@@ -1,13 +1,28 @@
+var KeyToScript = require('./keytoscript')
 // https://en.bitcoin.it/wiki/List_of_address_prefixes
 // Dogecoin BIP32 is a proposed standard: https://bitcointalk.org/index.php?topic=409731
 
+var p2pkh = new KeyToScript.P2pkhFactory()
+var p2wpkh = new KeyToScript.P2wpkhFactory()
+
 module.exports = {
   bitcoin: {
     messagePrefix: '\x18Bitcoin Signed Message:\n',
     bech32: 'bc',
     bip32: {
       public: 0x0488b21e,
-      private: 0x0488ade4
+      private: 0x0488ade4,
+      scriptFactory: p2pkh
+    },
+    bip49: {
+      private: 0x049d7878,
+      public: 0x049d7cb2,
+      scriptFactory: new KeyToScript.P2shFactory(p2wpkh)
+    },
+    bip84: {
+      private: 0x04b2430c,
+      public: 0x04b24746,
+      scriptFactory: p2wpkh
     },
     pubKeyHash: 0x00,
     scriptHash: 0x05,
@@ -18,7 +33,8 @@ module.exports = {
     bech32: 'tb',
     bip32: {
       public: 0x043587cf,
-      private: 0x04358394
+      private: 0x04358394,
+      scriptFactory: p2pkh
     },
     pubKeyHash: 0x6f,
     scriptHash: 0xc4,
@@ -28,7 +44,8 @@ module.exports = {
     messagePrefix: '\x19Litecoin Signed Message:\n',
     bip32: {
       public: 0x019da462,
-      private: 0x019d9cfe
+      private: 0x019d9cfe,
+      scriptFactory: p2pkh
     },
     pubKeyHash: 0x30,
     scriptHash: 0x32,
diff --git a/test/hdaddress.js b/test/hdaddress.js
new file mode 100644
index 0000000..59b84ab
--- /dev/null
+++ b/test/hdaddress.js
@@ -0,0 +1,95 @@
+/* global describe, it */
+/* eslint-disable no-new */
+
+var assert = require('assert')
+var HDNode = require('../src/hdnode')
+var bnetwork = require('../src/networks')
+
+describe('bip44', function () {
+  it('works without a ScriptFactory', function () {
+    // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
+    var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4'
+    var network = bnetwork.bitcoin
+
+    var root = HDNode.fromSeedHex(seed, network)
+    assert.equal(
+      root.toBase58(),
+      'xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu'
+    )
+
+    var account = root.derivePath('44\'/0\'/0\'')
+    assert.equal(
+      account.toBase58(),
+      'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb'
+    )
+    assert.equal(
+      'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj',
+      account.neutered().toBase58()
+    )
+
+    var key = account.derivePath('0/0')
+    assert.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', key.getAddress())
+  })
+
+  it('works with a prefix', function () {
+    // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
+    var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4'
+    var network = bnetwork.bitcoin
+
+    var root = HDNode.fromSeedHex(seed, network, network.bip32)
+    var account = root.derivePath('44\'/0\'/0\'')
+    assert.equal(
+      account.toBase58(),
+      'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb'
+    )
+    assert.equal(
+      'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj',
+      account.neutered().toBase58()
+    )
+
+    var key = account.derivePath('0/0')
+    assert.equal('1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', key.getAddress())
+  })
+})
+
+describe('bip49', function () {
+  it('serialization and address', function () {
+    // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
+    var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4'
+    var network = bnetwork.bitcoin
+    var root = HDNode.fromSeedHex(seed, network, network.bip49)
+    var account = root.derivePath('49\'/0\'/0\'')
+    assert.equal(
+      account.toBase58(),
+      'yprvAHwhK6RbpuS3dgCYHM5jc2ZvEKd7Bi61u9FVhYMpgMSuZS613T1xxQeKTffhrHY79hZ5PsskBjcc6C2V7DrnsMsNaGDaWev3GLRQRgV7hxF'
+    )
+    assert.equal(
+      'ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP',
+      account.neutered().toBase58()
+    )
+
+    var key = account.derivePath('0/0')
+    assert.equal('37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf', key.getAddress())
+  })
+})
+
+describe('bip84', function () {
+  it('serialization and address', function () {
+    // mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
+    var seed = '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4'
+    var network = bnetwork.bitcoin
+    var root = HDNode.fromSeedHex(seed, network, network.bip84)
+    var account = root.derivePath('84\'/0\'/0\'')
+    assert.equal(
+      account.toBase58(),
+      'zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE'
+    )
+    assert.equal(
+      'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
+      account.neutered().toBase58()
+    )
+
+    var key = account.derivePath('0/0')
+    assert.equal('bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', key.getAddress())
+  })
+})
diff --git a/test/hdnode.js b/test/hdnode.js
index 944dded..23cd042 100644
--- a/test/hdnode.js
+++ b/test/hdnode.js
@@ -50,7 +50,6 @@ describe('HDNode', function () {
 
     it('has a default depth/index of 0', function () {
       var hd = new HDNode(keyPair, chainCode)
-
       assert.strictEqual(hd.depth, 0)
       assert.strictEqual(hd.index, 0)
     })
@@ -124,12 +123,12 @@ describe('HDNode', function () {
     })
 
     describe('getAddress', function () {
-      it('wraps keyPair.getAddress', setupTest(function () {
-        this.mock(keyPair).expects('getAddress')
-          .once().withArgs().returns('foobar')
-
-        assert.strictEqual(hd.getAddress(), 'foobar')
-      }))
+      // it('wraps keyPair.getAddress', setupTest(function () {
+      //   this.mock(keyPair).expects('getAddress')
+      //     .once().withArgs().returns('foobar')
+      //
+      //   assert.strictEqual(hd.getAddress(), 'foobar')
+      // }))
     })
 
     describe('getNetwork', function () {

From 84e330df3dd153cb0a4d8626b5e65b382af37be4 Mon Sep 17 00:00:00 2001
From: Thomas Kerin <thomas.kerin@bitmaintech.com>
Date: Sun, 4 Mar 2018 21:08:23 +0100
Subject: [PATCH 2/2] hdnode: expose getScriptData

---
 src/hdnode.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/hdnode.js b/src/hdnode.js
index 8f98846..5ab8ffe 100644
--- a/src/hdnode.js
+++ b/src/hdnode.js
@@ -148,9 +148,13 @@ HDNode.fromBase58 = function (string, networks, prefixes) {
   return hd
 }
 
+HDNode.prototype.getScriptData = function () {
+  return this.prefix.scriptFactory.convert(this.keyPair)
+}
+
 HDNode.prototype.getAddress = function () {
-  var data = this.prefix.scriptFactory.convert(this.keyPair)
-  return baddress.fromOutputScript(data.scriptPubKey, this.keyPair.network)
+  var scriptData = this.getScriptData()
+  return baddress.fromOutputScript(scriptData.scriptPubKey, this.keyPair.network)
 }
 
 HDNode.prototype.getIdentifier = function () {