Browse Source

ADD: support for Electrum Seed format, legacy (closes #954)

receivehooks
Overtorment 5 years ago
parent
commit
bceacaca29
  1. 15
      class/abstract-hd-electrum-wallet.js
  2. 4
      class/abstract-wallet.js
  3. 65
      class/hd-legacy-electrum-seed-p2pkh-wallet.js
  4. 1
      class/index.js
  5. 9
      class/walletImport.js
  6. 10
      package-lock.json
  7. 3
      package.json
  8. 15
      tests/integration/hd-segwit-bech32-wallet.test.js
  9. 30
      tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js

15
class/abstract-hd-electrum-wallet.js

@ -12,6 +12,9 @@ const reverse = require('buffer-reverse');
const { RNRandomBytes } = NativeModules;
/**
* Electrum - means that it utilizes Electrum protocol for blockchain data
*/
export class AbstractHDElectrumWallet extends AbstractHDWallet {
static type = 'abstract';
static typeReadable = 'abstract';
@ -986,4 +989,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (broadcast.indexOf('successfully') !== -1) return true;
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
}
/**
* Probes zero address in external hierarchy for transactions, if there are any returns TRUE.
* Zero address is a pretty good indicator, since its a first one to fund the wallet. How can you use the wallet and
* not fund it first?
*
* @returns {Promise<boolean>}
*/
async wasEverUsed() {
let txs = await BlueElectrum.getTransactionsByAddress(this._getExternalAddressByIndex(0));
return txs.length > 0;
}
}

4
class/abstract-wallet.js

@ -171,4 +171,8 @@ export class AbstractWallet {
useWithHardwareWalletEnabled() {
return false;
}
async wasEverUsed() {
throw new Error('Not implemented');
}
}

65
class/hd-legacy-electrum-seed-p2pkh-wallet.js

@ -0,0 +1,65 @@
import { HDLegacyP2PKHWallet } from './';
const bitcoin = require('bitcoinjs-lib');
const mn = require('electrum-mnemonic');
/**
* 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 HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'HDlegacyElectrumSeedP2PKH';
static typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
validateMnemonic() {
try {
mn.mnemonicToSeedSync(this.secret);
return true;
} catch (_) {
return false;
}
}
getXpub() {
if (this._xpub) {
return this._xpub; // cache hit
}
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret));
this._xpub = root.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.p2pkh({
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.p2pkh({
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));
const path = `m/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.toWIF();
}
}

1
class/index.js

@ -13,3 +13,4 @@ export * from './abstract-hd-wallet';
export * from './hd-segwit-bech32-wallet';
export * from './hd-segwit-bech32-transaction';
export * from './placeholder-wallet';
export * from './hd-legacy-electrum-seed-p2pkh-wallet';

9
class/walletImport.js

@ -10,6 +10,7 @@ import {
LightningCustodianWallet,
PlaceholderWallet,
SegwitBech32Wallet,
HDLegacyElectrumSeedP2PKHWallet,
} from '../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const EV = require('../events');
@ -97,6 +98,7 @@ export default class WalletImport {
// 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44)
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
// 3.1 check HD Electrum legacy
// 4. check if its Segwit WIF (P2SH)
// 5. check if its Legacy WIF
// 6. check if its address (watch-only wallet)
@ -202,6 +204,13 @@ export default class WalletImport {
}
}
let hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet();
hdElectrumSeedLegacy.setSecret(importText);
if (await hdElectrumSeedLegacy.wasEverUsed()) {
// not fetching txs or balances, fuck it, yolo, life is too short
return WalletImport._saveWallet(hdElectrumSeedLegacy);
}
let hd2 = new HDSegwitP2SHWallet();
hd2.setSecret(importText);
if (hd2.validateMnemonic()) {

10
package-lock.json

@ -4460,6 +4460,16 @@
"version": "git+https://github.com/BlueWallet/rn-electrum-client.git#2a5bb11dd9a8d89f328049d9ed59bce49d88a15d",
"from": "git+https://github.com/BlueWallet/rn-electrum-client.git#2a5bb11dd9a8d89f328049d9ed59bce49d88a15d"
},
"electrum-mnemonic": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/electrum-mnemonic/-/electrum-mnemonic-1.0.5.tgz",
"integrity": "sha512-Wq5vFTTZzHu6w6GSxSNdSgEouqK5x7nky7XGQOnIvS5gx2on7EHysAqB6sSGOhhD5MnoT8nn9LhZPSBrDWkllw==",
"requires": {
"create-hmac": "^1.1.7",
"pbkdf2": "^3.0.17",
"randombytes": "^2.1.0"
}
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",

3
package.json

@ -45,6 +45,7 @@
"e2e:debug": "(test -f android/app/build/outputs/apk/debug/app-debug.apk && test -f android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk) || detox build -c android.emu.debug; detox test -c android.emu.debug",
"lint": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ tests/integration/ tests/e2e/ tests/unit/",
"lint:fix": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ tests/integration/ tests/e2e/ tests/unit/ --fix",
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep '\\.js' --color=never | awk '{print $2}' | xargs ./node_modules/.bin/eslint --fix; exit 0",
"unit": "node node_modules/jest/bin/jest.js tests/unit/*"
},
"jest": {
@ -79,6 +80,7 @@
"dayjs": "1.8.23",
"ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git#2a5bb11dd9a8d89f328049d9ed59bce49d88a15d",
"electrum-mnemonic": "1.0.5",
"eslint-config-prettier": "6.10.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-react": "7.0.2",
@ -90,6 +92,7 @@
"lottie-react-native": "3.1.1",
"node-libs-react-native": "1.0.3",
"path-browserify": "1.0.0",
"pbkdf2": "3.0.17",
"prettier": "1.19.1",
"process": "0.11.10",
"prop-types": "15.7.2",

15
tests/integration/hd-segwit-bech32-wallet.test.js

@ -235,4 +235,19 @@ describe('Bech32 Segwit HD (BIP84)', () => {
assert.strictEqual(totalInput - totalOutput, fee);
assert.strictEqual(outputs[outputs.length - 1].address, changeAddress);
});
it('wasEverUsed() works', async () => {
if (!process.env.HD_MNEMONIC) {
console.error('process.env.HD_MNEMONIC not set, skipped');
return;
}
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC);
assert.ok(await hd.wasEverUsed());
hd = new HDSegwitBech32Wallet();
await hd.generate();
assert.ok(!(await hd.wasEverUsed()), hd.getSecret());
});
});

30
tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js

@ -0,0 +1,30 @@
/* global describe, it */
import { HDLegacyElectrumSeedP2PKHWallet } from '../../class';
let assert = require('assert');
describe('HDLegacyElectrumSeedP2PKHWallet', () => {
it('can import mnemonics and generate addresses and WIFs', async function() {
if (!process.env.HD_ELECTRUM_SEED_LEGACY) {
console.error('process.env.HD_ELECTRUM_SEED_LEGACY not set, skipped');
return;
}
let hd = new HDLegacyElectrumSeedP2PKHWallet();
hd.setSecret(process.env.HD_ELECTRUM_SEED_LEGACY);
assert.ok(hd.validateMnemonic());
let address = hd._getExternalAddressByIndex(0);
assert.strictEqual(address, '1Ca9ZVshGdKiiMEMNTG1bYqbifYMZMwV8');
address = hd._getInternalAddressByIndex(0);
assert.strictEqual(address, '1JygAvTQS9haAYgRfPSdHgmXd3syjB8Fnp');
let wif = hd._getExternalWIFByIndex(0);
assert.strictEqual(wif, 'KxGPz9dyib26p6bL2vQPvBPHBMA8iHVqEetg3x5XA4Rk1trZ11Kz');
wif = hd._getInternalWIFByIndex(0);
assert.strictEqual(wif, 'L52d26QmYGW8ctHo1omM5fZeJMgaonSkEWCGpnEekNvkVUoqTsNF');
hd.setSecret('bs');
assert.ok(!hd.validateMnemonic());
});
});
Loading…
Cancel
Save