From 68485b9a47fac170383378910f23cbb081f24bc9 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 4 May 2020 18:52:01 +0900 Subject: [PATCH] Add Electrum seed recovery for Segwit as well (not just legacy) --- class/app-storage.js | 4 + .../hd-segwit-electrum-seed-p2wpkh-wallet.js | 101 ++++++++++++++++++ class/index.js | 1 + class/walletGradient.js | 6 ++ class/walletImport.js | 10 ++ ...-legacy-electrum-seed-p2pkh-wallet.test.js | 4 + ...segwit-electrum-seed-p2wpkh-wallet.test.js | 39 +++++++ 7 files changed, 165 insertions(+) create mode 100644 class/hd-segwit-electrum-seed-p2wpkh-wallet.js create mode 100644 tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js diff --git a/class/app-storage.js b/class/app-storage.js index e840c44ce..10bd0d42c 100644 --- a/class/app-storage.js +++ b/class/app-storage.js @@ -12,6 +12,7 @@ import { PlaceholderWallet, LightningCustodianWallet, HDLegacyElectrumSeedP2PKHWallet, + HDSegwitElectrumSeedP2WPKHWallet, } from './'; import WatchConnectivity from '../WatchConnectivity'; import DeviceQuickActions from './quickActions'; @@ -266,6 +267,9 @@ export class AppStorage { case HDLegacyElectrumSeedP2PKHWallet.type: unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key); break; + case HDSegwitElectrumSeedP2WPKHWallet.type: + unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key); + break; case LightningCustodianWallet.type: /** @type {LightningCustodianWallet} */ unserializedWallet = LightningCustodianWallet.fromJson(key); diff --git a/class/hd-segwit-electrum-seed-p2wpkh-wallet.js b/class/hd-segwit-electrum-seed-p2wpkh-wallet.js new file mode 100644 index 000000000..b5a6a7875 --- /dev/null +++ b/class/hd-segwit-electrum-seed-p2wpkh-wallet.js @@ -0,0 +1,101 @@ +import { HDSegwitBech32Wallet } from './'; + +const bitcoin = require('bitcoinjs-lib'); +const mn = require('electrum-mnemonic'); +const HDNode = require('bip32'); + +const PREFIX = mn.PREFIXES.segwit; +const MNEMONIC_TO_SEED_OPTS = { + prefix: PREFIX, +}; + +/** + * ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise + * its a regular HD wallet that has all the properties of parent class. + * + * @see https://electrum.readthedocs.io/en/latest/seedphrase.html + */ +export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet { + static type = 'HDSegwitElectrumSeedP2WPKHWallet'; + static typeReadable = 'HD Electrum (BIP32 P2WPKH)'; + + validateMnemonic() { + return mn.validateMnemonic(this.secret, PREFIX); + } + + async generate() { + throw new Error('Not implemented'); + } + + getXpub() { + if (this._xpub) { + return this._xpub; // cache hit + } + const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS)); + this._xpub = root + .derivePath("m/0'") + .neutered() + .toBase58(); + return this._xpub; + } + + _getInternalAddressByIndex(index) { + index = index * 1; // cast to int + if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit + + const node = bitcoin.bip32.fromBase58(this.getXpub()); + const address = bitcoin.payments.p2wpkh({ + pubkey: node.derive(1).derive(index).publicKey, + }).address; + + return (this.internal_addresses_cache[index] = address); + } + + _getExternalAddressByIndex(index) { + index = index * 1; // cast to int + if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit + + const node = bitcoin.bip32.fromBase58(this.getXpub()); + const address = bitcoin.payments.p2wpkh({ + pubkey: node.derive(0).derive(index).publicKey, + }).address; + + return (this.external_addresses_cache[index] = address); + } + + _getWIFByIndex(internal, index) { + const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS)); + const path = `m/0'/${internal ? 1 : 0}/${index}`; + const child = root.derivePath(path); + + return child.toWIF(); + } + + allowSendMax() { + return true; + } + + _getNodePubkeyByIndex(node, index) { + index = index * 1; // cast to int + + if (node === 0 && !this._node0) { + const xpub = this.getXpub(); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = this.getXpub(); + const hdNode = HDNode.fromBase58(xpub); + this._node1 = hdNode.derive(node); + } + + if (node === 0) { + return this._node0.derive(index).publicKey; + } + + if (node === 1) { + return this._node1.derive(index).publicKey; + } + } +} diff --git a/class/index.js b/class/index.js index 3a3a714e0..3b412e61d 100644 --- a/class/index.js +++ b/class/index.js @@ -14,3 +14,4 @@ export * from './hd-segwit-bech32-wallet'; export * from './hd-segwit-bech32-transaction'; export * from './placeholder-wallet'; export * from './hd-legacy-electrum-seed-p2pkh-wallet'; +export * from './hd-segwit-electrum-seed-p2wpkh-wallet'; diff --git a/class/walletGradient.js b/class/walletGradient.js index b30358c74..d3c355f1c 100644 --- a/class/walletGradient.js +++ b/class/walletGradient.js @@ -7,6 +7,8 @@ import { WatchOnlyWallet } from './watch-only-wallet'; import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; import { PlaceholderWallet } from './placeholder-wallet'; import { SegwitBech32Wallet } from './segwit-bech32-wallet'; +import { HDLegacyElectrumSeedP2PKHWallet } from './hd-legacy-electrum-seed-p2pkh-wallet'; +import { HDSegwitElectrumSeedP2WPKHWallet } from './hd-segwit-electrum-seed-p2wpkh-wallet'; export default class WalletGradient { static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1']; @@ -30,6 +32,7 @@ export default class WalletGradient { gradient = WalletGradient.legacyWallet; break; case HDLegacyP2PKHWallet.type: + case HDLegacyElectrumSeedP2PKHWallet.type: gradient = WalletGradient.hdLegacyP2PKHWallet; break; case HDLegacyBreadwalletWallet.type: @@ -39,6 +42,7 @@ export default class WalletGradient { gradient = WalletGradient.hdSegwitP2SHWallet; break; case HDSegwitBech32Wallet.type: + case HDSegwitElectrumSeedP2WPKHWallet.type: gradient = WalletGradient.hdSegwitBech32Wallet; break; case LightningCustodianWallet.type: @@ -70,6 +74,7 @@ export default class WalletGradient { gradient = WalletGradient.legacyWallet; break; case HDLegacyP2PKHWallet.type: + case HDLegacyElectrumSeedP2PKHWallet.type: gradient = WalletGradient.hdLegacyP2PKHWallet; break; case HDLegacyBreadwalletWallet.type: @@ -79,6 +84,7 @@ export default class WalletGradient { gradient = WalletGradient.hdSegwitP2SHWallet; break; case HDSegwitBech32Wallet.type: + case HDSegwitElectrumSeedP2WPKHWallet.type: gradient = WalletGradient.hdSegwitBech32Wallet; break; case SegwitBech32Wallet.type: diff --git a/class/walletImport.js b/class/walletImport.js index 5788adb00..c95c276b1 100644 --- a/class/walletImport.js +++ b/class/walletImport.js @@ -11,6 +11,7 @@ import { PlaceholderWallet, SegwitBech32Wallet, HDLegacyElectrumSeedP2PKHWallet, + HDSegwitElectrumSeedP2WPKHWallet, } from '../class'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; const EV = require('../events'); @@ -204,6 +205,15 @@ export default class WalletImport { } } + try { + let hdElectrumSeedLegacy = new HDSegwitElectrumSeedP2WPKHWallet(); + hdElectrumSeedLegacy.setSecret(importText); + if (await hdElectrumSeedLegacy.wasEverUsed()) { + // not fetching txs or balances, fuck it, yolo, life is too short + return WalletImport._saveWallet(hdElectrumSeedLegacy); + } + } catch (_) {} + try { let hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet(); hdElectrumSeedLegacy.setSecret(importText); diff --git a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js index 62b6710ff..f5cb4c4b7 100644 --- a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js +++ b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js @@ -7,6 +7,10 @@ describe('HDLegacyElectrumSeedP2PKHWallet', () => { let hd = new HDLegacyElectrumSeedP2PKHWallet(); hd.setSecret('receive happy wash prosper update pet neck acid try profit proud hungry '); assert.ok(hd.validateMnemonic()); + assert.strictEqual( + hd.getXpub(), + 'xpub661MyMwAqRbcG6vx5SspHUzrhRtPKyeGp41JJLBi3kgeMCFkR6mzGkhEttBHTZg6FYYij52pqD2cW7XsutiZrRukXNLqeo87mZAV5k5bC22', + ); let address = hd._getExternalAddressByIndex(0); assert.strictEqual(address, '1Ca9ZVshGdKiiMEMNTG1bYqbifYMZMwV8'); diff --git a/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js b/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js new file mode 100644 index 000000000..b383c09c3 --- /dev/null +++ b/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js @@ -0,0 +1,39 @@ +/* global describe, it */ +import { HDSegwitElectrumSeedP2WPKHWallet } from '../../class'; +let assert = require('assert'); + +describe('HDSegwitElectrumSeedP2WPKHWallet', () => { + it('can import mnemonics and generate addresses and WIFs', async function() { + let hd = new HDSegwitElectrumSeedP2WPKHWallet(); + hd.setSecret('method goddess humble crumble output snake essay carpet monster barely trip betray '); + assert.ok(hd.validateMnemonic()); + assert.strictEqual( + hd.getXpub(), + 'xpub68RzTumZwSbVWwETioxTSk2PhBvBRDGNRHepHUC5x2gptbSVWhkezF3NKbq9sCJhnNKcPx2McNWJtFFdXLx97cknHhuDTDQsFg5cG7MSMY7', + ); + + let address = hd._getExternalAddressByIndex(0); + assert.strictEqual(address, 'bc1q2yv6rhtw9ycqeq2rkch65sucf66ytwsd3csawr'); + + address = hd._getInternalAddressByIndex(0); + assert.strictEqual(address, 'bc1qvdu80q26ghe66zq8tf5y09qr29vay4cg65mvuk'); + + let wif = hd._getExternalWIFByIndex(0); + assert.strictEqual(wif, 'L5a1N5JQzT9wDUmVS9hb2mrd1SMkwPfrWYS8C3Kngp7kiuBkpY2V'); + + wif = hd._getInternalWIFByIndex(0); + assert.strictEqual(wif, 'KwsLfaB2y9QZRd5cxY3uM3L4r2fE7ZPzocwjkPbp1cSFMFfE9tBq'); + + assert.strictEqual( + hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'), + '023cb68c37a1ca627c414e63dfb23706091eafb50e50d7de4e2a1a56d7085d42e6', + ); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getInternalAddressByIndex(0)).toString('hex'), + '02e7e6a8dc1fe62f7de88a7de3c5030f36ec6aec28c610bc1d573435fab18b9f94', + ); + + hd.setSecret('bs'); + assert.ok(!hd.validateMnemonic()); + }); +});