ADD: show fingerprint and derivation path for HD wallets

This commit is contained in:
Ivan Vershigora 2021-03-23 18:58:13 +03:00
parent 49e2b6d211
commit 9c9af0a3b0
15 changed files with 91 additions and 34 deletions

View File

@ -1112,4 +1112,23 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return { tx };
}
/**
* @param mnemonic {string} Mnemonic seed phrase
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has lenght of 8 chars and correct leading zeroes
*/
static seedToFingerprint(mnemonic) {
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.bip32.fromSeed(seed);
let hex = root.fingerprint.toString('hex');
while (hex.length < 8) hex = '0' + hex; // leading zeroes
return hex.toUpperCase();
}
/**
* @returns {string} Hex string of fingerprint derived from wallet mnemonics. Always has lenght of 8 chars and correct leading zeroes
*/
getMasterFingerprintHex() {
return AbstractHDElectrumWallet.seedToFingerprint(this.secret);
}
}

View File

@ -20,6 +20,7 @@ export class AbstractWallet {
this.type = this.constructor.type;
this.typeReadable = this.constructor.typeReadable;
this.segwitType = this.constructor.segwitType;
this._derivationPath = this.constructor.derivationPath;
this.label = '';
this.secret = ''; // private key or recovery phrase
this.balance = 0;
@ -128,6 +129,10 @@ export class AbstractWallet {
return false;
}
allowMasterFingerprint() {
return false;
}
weOwnAddress(address) {
throw Error('not implemented');
}
@ -329,4 +334,11 @@ export class AbstractWallet {
if ('frozen' in opts) meta.frozen = opts.frozen;
this._utxoMetadata[`${txid}:${vout}`] = meta;
}
/**
* @returns {string|null} Root derivation path for wallet if any
*/
getDerivationPath() {
return this._derivationPath || null;
}
}

View File

@ -16,6 +16,7 @@ export class HDAezeedWallet extends AbstractHDElectrumWallet {
static type = 'HDAezeedWallet';
static typeReadable = 'HD Aezeed';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
setSecret(newSecret) {
this.secret = newSecret.trim();

View File

@ -12,6 +12,7 @@ const BlueElectrum = require('../../blue_modules/BlueElectrum');
export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
static derivationPath = "m/0'";
// track address index at which wallet switched to segwit
_external_segwit_index = null; // eslint-disable-line camelcase

View File

@ -18,6 +18,7 @@ const MNEMONIC_TO_SEED_OPTS = {
export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'HDlegacyElectrumSeedP2PKH';
static typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
static derivationPath = 'm';
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -12,6 +12,7 @@ const BlueElectrum = require('../../blue_modules/BlueElectrum');
export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
static type = 'HDlegacyP2PKH';
static typeReadable = 'HD Legacy (BIP44 P2PKH)';
static derivationPath = "m/44'/0'/0'";
allowSend() {
return true;
@ -29,6 +30,10 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
return true;
}
allowMasterFingerprint() {
return true;
}
getXpub() {
if (this._xpub) {
return this._xpub; // cache hit

View File

@ -9,6 +9,7 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
allowSend() {
return true;
@ -41,4 +42,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowSignVerifyMessage() {
return true;
}
allowMasterFingerprint() {
return true;
}
}

View File

@ -18,6 +18,7 @@ const MNEMONIC_TO_SEED_OPTS = {
export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
static type = 'HDSegwitElectrumSeedP2WPKHWallet';
static typeReadable = 'HD Electrum (BIP32 P2WPKH)';
static derivationPath = "m/0'";
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -13,6 +13,7 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitP2SH';
static typeReadable = 'HD SegWit (BIP49 P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static derivationPath = "m/49'/0'/0'";
allowSend() {
return true;
@ -30,6 +31,14 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
return true;
}
allowHodlHodlTrading() {
return true;
}
allowMasterFingerprint() {
return true;
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
@ -142,8 +151,4 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
});
return address;
}
allowHodlHodlTrading() {
return true;
}
}

View File

@ -105,10 +105,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
}
getDerivationPath() {
return this._derivationPath;
}
getCustomDerivationPathForCosigner(index) {
if (index === 0) throw new Error('cosigners indexation starts from 1');
if (index > this.getN()) return false;
@ -189,7 +185,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else {
// mnemonics. lets derive fingerprint (if it wasnt provided)
if (!bip39.validateMnemonic(key)) throw new Error('Not a valid mnemonic phrase');
fingerprint = fingerprint || MultisigHDWallet.seedToFingerprint(key);
fingerprint = fingerprint || AbstractHDElectrumWallet.seedToFingerprint(key);
}
if (fingerprint && this._cosignersFingerprints.indexOf(fingerprint.toUpperCase()) !== -1 && fingerprint !== '00000000') {
@ -310,18 +306,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return child.toBase58();
}
/**
* @param mnemonic {string} Mnemonic seed phrase
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has lenght of 8 chars and correct leading zeroes
*/
static seedToFingerprint(mnemonic) {
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.bip32.fromSeed(seed);
let hex = root.fingerprint.toString('hex');
while (hex.length < 8) hex = '0' + hex; // leading zeroes
return hex.toUpperCase();
}
/**
* Returns xpub with correct prefix accodting to this objects set derivation path, for example 'Zpub' (with
* capital Z) for bech32 multisig
@ -448,7 +432,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
const xpub = this.convertXpubToMultisignatureXpub(
MultisigHDWallet.seedToXpub(this._cosigners[index], this._cosignersCustomPaths[index] || this._derivationPath),
);
const fingerprint = MultisigHDWallet.seedToFingerprint(this._cosigners[index]);
const fingerprint = AbstractHDElectrumWallet.seedToFingerprint(this._cosigners[index]);
ret += fingerprint + ': ' + xpub + '\n';
} else {
ret += 'seed: ' + this._cosigners[index] + '\n';
@ -1053,7 +1037,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
if (index === -1) return;
if (!MultisigHDWallet.isXpubValid(newCosigner)) {
// its not an xpub, so lets derive fingerprint ourselves
newFp = MultisigHDWallet.seedToFingerprint(newCosigner);
newFp = AbstractHDElectrumWallet.seedToFingerprint(newCosigner);
if (oldFp !== newFp) {
throw new Error('Fingerprint of new seed doesnt match');
}

View File

@ -221,6 +221,10 @@ export class WatchOnlyWallet extends LegacyWallet {
return this.isHd();
}
allowMasterFingerprint() {
return this.getSecret().startsWith('zpub');
}
useWithHardwareWalletEnabled() {
return !!this.use_with_hardware_wallet;
}

View File

@ -16,7 +16,7 @@ import {
StatusBar,
PermissionsAndroid,
} from 'react-native';
import { SecondButton, SafeBlueArea, BlueCard, BlueSpacing20, BlueText, BlueLoading } from '../../BlueComponents';
import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText, SafeBlueArea, SecondButton } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import { HDLegacyBreadwalletWallet } from '../../class/wallets/hd-legacy-breadwallet-wallet';
@ -457,7 +457,7 @@ const WalletDetails = () => {
</>
)}
{wallet.type === MultisigHDWallet.type && !!wallet.getDerivationPath() && (
{wallet.getDerivationPath() && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_derivation_path}</Text>
<BlueText>{wallet.getDerivationPath()}</BlueText>
@ -492,29 +492,32 @@ const WalletDetails = () => {
</Text>
<BlueText>{wallet.getTransactions().length}</BlueText>
</>
<View>
{wallet.type === WatchOnlyWallet.type && wallet.getSecret().startsWith('zpub') && (
<>
<BlueSpacing10 />
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_advanced.toLowerCase()}</Text>
<View style={styles.hardware}>
<BlueText>{loc.wallets.details_use_with_hardware_wallet}</BlueText>
<Switch value={useWithHardwareWallet} onValueChange={setUseWithHardwareWallet} />
</View>
<>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_master_fingerprint.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]}>{wallet.getMasterFingerprintHex()}</Text>
</>
<BlueSpacing20 />
</>
)}
<BlueSpacing20 />
{wallet.allowMasterFingerprint() && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_master_fingerprint.toLowerCase()}</Text>
<BlueText>{wallet.getMasterFingerprintHex()}</BlueText>
</>
)}
<BlueSpacing20 />
<SecondButton onPress={navigateToWalletExport} testID="WalletExport" title={loc.wallets.details_export_backup} />
<BlueSpacing20 />
{wallet.type === MultisigHDWallet.type && (
<>
<BlueSpacing20 />
<SecondButton
onPress={navigateToMultisigCoordinationSetup}
testID="MultisigCoordinationSetup"
@ -536,8 +539,8 @@ const WalletDetails = () => {
wallet.type === HDAezeedWallet.type ||
wallet.type === HDSegwitP2SHWallet.type) && (
<>
<BlueSpacing20 />
<SecondButton onPress={navigateToXPub} testID="XPub" title={loc.wallets.details_show_xpub} />
<BlueSpacing20 />
{renderMarketplaceButton()}
</>

View File

@ -149,4 +149,11 @@ describe('Legacy HD (BIP44)', () => {
assert.strictEqual(signature, 'H98hmvtyPFUbR6E5Tcsqmc+eSjlYhP2vy41Y6IyHS9DVKEI5n8VEMpIEDtvlMARVce96nOqbRHXo9nD05WXH/Eo=');
assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), signature), true);
});
it('can show fingerprint', async () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const hd = new HDLegacyP2PKHWallet();
hd.setSecret(mnemonic);
assert.strictEqual(hd.getMasterFingerprintHex(), '73C5DA0A');
});
});

View File

@ -43,6 +43,8 @@ describe('Bech32 Segwit HD (BIP84)', () => {
assert.strictEqual(hd._getDerivationPathByAddress(hd._getExternalAddressByIndex(1)), "m/84'/0'/0'/0/1");
assert.strictEqual(hd._getDerivationPathByAddress(hd._getInternalAddressByIndex(0)), "m/84'/0'/0'/1/0");
assert.strictEqual(hd._getDerivationPathByAddress(hd._getInternalAddressByIndex(1)), "m/84'/0'/0'/1/1");
assert.strictEqual(hd.getMasterFingerprintHex(), '73C5DA0A');
});
it('can generate addresses only via zpub', function () {

View File

@ -182,4 +182,11 @@ describe('P2SH Segwit HD (BIP49)', () => {
assert.strictEqual(signature, 'I5WkniWTnJhTW74t3kTAkHq3HdiupTNgOZLpMp0hvUfAJw2HMuyRiNLl2pbNWobNCCrmvffSWM7IgkOBz/J9fYA=');
assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), signature), true);
});
it('can show fingerprint', async () => {
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
assert.strictEqual(hd.getMasterFingerprintHex(), '73C5DA0A');
});
});