Merge pull request #4350 from BlueWallet/limpbrains-multisig-passphrase

ADD: Multisig seed with passphrase
This commit is contained in:
GLaDOS 2022-02-19 12:22:42 +00:00 committed by GitHub
commit 309c5b10e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 504 additions and 145 deletions

View file

@ -1136,8 +1136,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* @param mnemonic {string} Mnemonic phrase (12 or 24 words) * @param mnemonic {string} Mnemonic phrase (12 or 24 words)
* @returns {string} Hex fingerprint * @returns {string} Hex fingerprint
*/ */
static mnemonicToFingerprint(mnemonic) { static mnemonicToFingerprint(mnemonic, passphrase) {
const seed = bip39.mnemonicToSeedSync(mnemonic); const seed = bip39.mnemonicToSeedSync(mnemonic, passphrase);
return AbstractHDElectrumWallet.seedToFingerprint(seed); return AbstractHDElectrumWallet.seedToFingerprint(seed);
} }

View file

@ -13,13 +13,15 @@ const createHash = require('create-hash');
const reverse = require('buffer-reverse'); const reverse = require('buffer-reverse');
const mn = require('electrum-mnemonic'); const mn = require('electrum-mnemonic');
const MNEMONIC_TO_SEED_OPTS_SEGWIT = { const electrumSegwit = passphrase => ({
prefix: mn.PREFIXES.segwit, prefix: mn.PREFIXES.segwit,
}; ...(passphrase ? { passphrase } : {}),
});
const MNEMONIC_TO_SEED_OPTS_STANDARD = { const electrumStandart = passphrase => ({
prefix: mn.PREFIXES.standard, prefix: mn.PREFIXES.standard,
}; ...(passphrase ? { passphrase } : {}),
});
const ELECTRUM_SEED_PREFIX = 'electrumseed:'; const ELECTRUM_SEED_PREFIX = 'electrumseed:';
@ -42,6 +44,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
this._cosigners = []; // array of xpubs or mnemonic seeds this._cosigners = []; // array of xpubs or mnemonic seeds
this._cosignersFingerprints = []; // array of according fingerprints (if any provided) this._cosignersFingerprints = []; // array of according fingerprints (if any provided)
this._cosignersCustomPaths = []; // array of according paths (if any provided) this._cosignersCustomPaths = []; // array of according paths (if any provided)
this._cosignersPassphrases = []; // array of according passphrases (if any provided)
this._derivationPath = ''; this._derivationPath = '';
this._isNativeSegwit = false; this._isNativeSegwit = false;
this._isWrappedSegwit = false; this._isWrappedSegwit = false;
@ -130,6 +133,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return this._cosigners[index]; return this._cosigners[index];
} }
getPassphrase(index) {
if (index === 0) throw new Error('cosigners indexation starts from 1');
return this._cosignersPassphrases[index - 1];
}
static isXpubValid(key) { static isXpubValid(key) {
let xpub; let xpub;
@ -157,8 +165,9 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
* @param key {string} Either xpub or mnemonic phrase * @param key {string} Either xpub or mnemonic phrase
* @param fingerprint {string} Fingerprint for cosigner that is added as xpub * @param fingerprint {string} Fingerprint for cosigner that is added as xpub
* @param path {string} Custom path (if any) for cosigner that is added as mnemonics * @param path {string} Custom path (if any) for cosigner that is added as mnemonics
* @param passphrase {string} BIP38 Passphrase (if any)
*/ */
addCosigner(key, fingerprint, path) { addCosigner(key, fingerprint, path, passphrase) {
if (MultisigHDWallet.isXpubString(key) && !fingerprint) { if (MultisigHDWallet.isXpubString(key) && !fingerprint) {
throw new Error('fingerprint is required when adding cosigner as xpub (watch-only)'); throw new Error('fingerprint is required when adding cosigner as xpub (watch-only)');
} }
@ -176,11 +185,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
// its an electrum seed // its an electrum seed
const mnemonic = key.replace(ELECTRUM_SEED_PREFIX, ''); const mnemonic = key.replace(ELECTRUM_SEED_PREFIX, '');
try { try {
mn.mnemonicToSeedSync(mnemonic, MNEMONIC_TO_SEED_OPTS_STANDARD); mn.mnemonicToSeedSync(mnemonic, electrumStandart(passphrase));
this.setLegacy(); this.setLegacy();
} catch (_) { } catch (_) {
try { try {
mn.mnemonicToSeedSync(mnemonic, MNEMONIC_TO_SEED_OPTS_SEGWIT); mn.mnemonicToSeedSync(mnemonic, electrumSegwit(passphrase));
this.setNativeSegwit(); this.setNativeSegwit();
} catch (__) { } catch (__) {
throw new Error('Not a valid electrum seed'); throw new Error('Not a valid electrum seed');
@ -189,7 +198,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else { } else {
// mnemonics. lets derive fingerprint (if it wasnt provided) // mnemonics. lets derive fingerprint (if it wasnt provided)
if (!bip39.validateMnemonic(key)) throw new Error('Not a valid mnemonic phrase'); if (!bip39.validateMnemonic(key)) throw new Error('Not a valid mnemonic phrase');
fingerprint = fingerprint || MultisigHDWallet.mnemonicToFingerprint(key); fingerprint = fingerprint || MultisigHDWallet.mnemonicToFingerprint(key, passphrase);
} }
if (fingerprint && this._cosignersFingerprints.indexOf(fingerprint.toUpperCase()) !== -1 && fingerprint !== '00000000') { if (fingerprint && this._cosignersFingerprints.indexOf(fingerprint.toUpperCase()) !== -1 && fingerprint !== '00000000') {
@ -201,6 +210,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
this._cosigners[index] = key; this._cosigners[index] = key;
if (fingerprint) this._cosignersFingerprints[index] = fingerprint.toUpperCase(); if (fingerprint) this._cosignersFingerprints[index] = fingerprint.toUpperCase();
if (path) this._cosignersCustomPaths[index] = path; if (path) this._cosignersCustomPaths[index] = path;
if (passphrase) this._cosignersPassphrases[index] = passphrase;
} }
static convertMultisigXprvToRegularXprv(Zprv) { static convertMultisigXprvToRegularXprv(Zprv) {
@ -226,7 +236,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
let xpub = cosigner; let xpub = cosigner;
if (!MultisigHDWallet.isXpubString(cosigner)) { if (!MultisigHDWallet.isXpubString(cosigner)) {
const index = this._cosigners.indexOf(cosigner); const index = this._cosigners.indexOf(cosigner);
xpub = MultisigHDWallet.seedToXpub(cosigner, this._cosignersCustomPaths[index] || this._derivationPath); xpub = MultisigHDWallet.seedToXpub(
cosigner,
this._cosignersCustomPaths[index] || this._derivationPath,
this._cosignersPassphrases[index],
);
} }
return this.constructor._zpubToXpub(xpub); return this.constructor._zpubToXpub(xpub);
} }
@ -243,8 +257,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getAddressFromNode(nodeIndex, index) { _getAddressFromNode(nodeIndex, index) {
const pubkeys = []; const pubkeys = [];
let cosignerIndex = 0; for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
for (const cosigner of this._cosigners) {
this._nodes = this._nodes || []; this._nodes = this._nodes || [];
this._nodes[nodeIndex] = this._nodes[nodeIndex] || []; this._nodes[nodeIndex] = this._nodes[nodeIndex] || [];
let _node; let _node;
@ -259,7 +272,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} }
pubkeys.push(_node.derive(index).publicKey); pubkeys.push(_node.derive(index).publicKey);
cosignerIndex++;
} }
if (this.isWrappedSegwit()) { if (this.isWrappedSegwit()) {
@ -297,12 +309,12 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return address; return address;
} }
static seedToXpub(mnemonic, path) { static seedToXpub(mnemonic, path, passphrase) {
let seed; let seed;
if (mnemonic.startsWith(ELECTRUM_SEED_PREFIX)) { if (mnemonic.startsWith(ELECTRUM_SEED_PREFIX)) {
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic); seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic, passphrase);
} else { } else {
seed = bip39.mnemonicToSeedSync(mnemonic); seed = bip39.mnemonicToSeedSync(mnemonic, passphrase);
} }
const root = bip32.fromSeed(seed); const root = bip32.fromSeed(seed);
@ -434,13 +446,18 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else { } else {
if (coordinationSetup) { if (coordinationSetup) {
const xpub = this.convertXpubToMultisignatureXpub( const xpub = this.convertXpubToMultisignatureXpub(
MultisigHDWallet.seedToXpub(this._cosigners[index], this._cosignersCustomPaths[index] || this._derivationPath), MultisigHDWallet.seedToXpub(
this._cosigners[index],
this._cosignersCustomPaths[index] || this._derivationPath,
this._cosignersPassphrases[index],
),
); );
const fingerprint = MultisigHDWallet.mnemonicToFingerprint(this._cosigners[index]); const fingerprint = MultisigHDWallet.mnemonicToFingerprint(this._cosigners[index], this._cosignersPassphrases[index]);
ret += fingerprint + ': ' + xpub + '\n'; ret += fingerprint + ': ' + xpub + '\n';
} else { } else {
ret += 'seed: ' + this._cosigners[index] + '\n'; ret += 'seed: ' + this._cosigners[index];
ret += '# warning! sensitive information, do not disclose ^^^ \n'; if (this._cosignersPassphrases[index]) ret += ' - ' + this._cosignersPassphrases[index];
ret += '\n# warning! sensitive information, do not disclose ^^^ \n';
} }
} }
@ -480,7 +497,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
? MultisigHDWallet.ckccXfp2fingerprint(cosignerData.ckcc_xfp) ? MultisigHDWallet.ckccXfp2fingerprint(cosignerData.ckcc_xfp)
: cosignerData.root_fingerprint?.toUpperCase()) || '00000000'; : cosignerData.root_fingerprint?.toUpperCase()) || '00000000';
if (cosignerData.seed) { if (cosignerData.seed) {
this.addCosigner(ELECTRUM_SEED_PREFIX + cosignerData.seed, fingerprint, cosignerData.derivation); this.addCosigner(ELECTRUM_SEED_PREFIX + cosignerData.seed, fingerprint, cosignerData.derivation, cosignerData.passphrase);
} else if (cosignerData.xprv && MultisigHDWallet.isXprvValid(cosignerData.xprv)) { } else if (cosignerData.xprv && MultisigHDWallet.isXprvValid(cosignerData.xprv)) {
this.addCosigner(cosignerData.xprv, fingerprint, cosignerData.derivation); this.addCosigner(cosignerData.xprv, fingerprint, cosignerData.derivation);
} else { } else {
@ -533,7 +550,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else if (key.replace('#', '').trim() === 'derivation') { } else if (key.replace('#', '').trim() === 'derivation') {
customPathForCurrentCosigner = value.trim(); customPathForCurrentCosigner = value.trim();
} else if (key === 'seed') { } else if (key === 'seed') {
this.addCosigner(value.trim(), false, customPathForCurrentCosigner); const [seed, passphrase] = value.split(' - ');
this.addCosigner(seed.trim(), false, customPathForCurrentCosigner, passphrase);
} }
break; break;
} }
@ -640,11 +658,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) { _addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
const bip32Derivation = []; // array per each pubkey thats gona be used const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = []; const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) { for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
const cosigner = this._cosigners[c]; const path = this._getDerivationPathByAddressWithCustomPath(
const path = this._getDerivationPathByAddressWithCustomPath(input.address, this._cosignersCustomPaths[c] || this._derivationPath); input.address,
this._cosignersCustomPaths[cosignerIndex] || this._derivationPath,
);
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used // ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex'); const masterFingerprint = Buffer.from(this._cosignersFingerprints[cosignerIndex], 'hex');
const xpub = this._getXpubFromCosigner(cosigner); const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = bip32.fromBase58(xpub); const hdNode0 = bip32.fromBase58(xpub);
@ -731,14 +751,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getOutputDataForChange(outputData) { _getOutputDataForChange(outputData) {
const bip32Derivation = []; // array per each pubkey thats gona be used const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = []; const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) { for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
const cosigner = this._cosigners[c];
const path = this._getDerivationPathByAddressWithCustomPath( const path = this._getDerivationPathByAddressWithCustomPath(
outputData.address, outputData.address,
this._cosignersCustomPaths[c] || this._derivationPath, this._cosignersCustomPaths[cosignerIndex] || this._derivationPath,
); );
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used // ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex'); const masterFingerprint = Buffer.from(this._cosignersFingerprints[cosignerIndex], 'hex');
const xpub = this._getXpubFromCosigner(cosigner); const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = bip32.fromBase58(xpub); const hdNode0 = bip32.fromBase58(xpub);
@ -844,22 +863,22 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
if (!skipSigning) { if (!skipSigning) {
for (let cc = 0; cc < c; cc++) { for (let cc = 0; cc < c; cc++) {
let signaturesMade = 0; let signaturesMade = 0;
for (const cosigner of this._cosigners) { for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
if (!MultisigHDWallet.isXpubString(cosigner)) { if (MultisigHDWallet.isXpubString(cosigner)) continue;
// ok this is a mnemonic, lets try to sign // ok this is a mnemonic, lets try to sign
if (signaturesMade >= this.getM()) { if (signaturesMade >= this.getM()) {
// dont sign more than we need, otherwise there will be "Too many signatures" error // dont sign more than we need, otherwise there will be "Too many signatures" error
continue; continue;
}
let seed = bip39.mnemonicToSeedSync(cosigner);
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner);
}
const hdRoot = bip32.fromSeed(seed);
psbt.signInputHD(cc, hdRoot);
signaturesMade++;
} }
const passphrase = this._cosignersPassphrases[cosignerIndex];
let seed = bip39.mnemonicToSeedSync(cosigner, passphrase);
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner, passphrase);
}
const hdRoot = bip32.fromSeed(seed);
psbt.signInputHD(cc, hdRoot);
signaturesMade++;
} }
} }
} }
@ -871,13 +890,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return { tx, inputs, outputs, fee, psbt }; return { tx, inputs, outputs, fee, psbt };
} }
static convertElectrumMnemonicToSeed(cosigner) { static convertElectrumMnemonicToSeed(cosigner, passphrase) {
let seed; let seed;
try { try {
seed = mn.mnemonicToSeedSync(cosigner.replace(ELECTRUM_SEED_PREFIX, ''), MNEMONIC_TO_SEED_OPTS_SEGWIT); seed = mn.mnemonicToSeedSync(cosigner.replace(ELECTRUM_SEED_PREFIX, ''), electrumSegwit(passphrase));
} catch (_) { } catch (_) {
try { try {
seed = mn.mnemonicToSeedSync(cosigner.replace(ELECTRUM_SEED_PREFIX, ''), MNEMONIC_TO_SEED_OPTS_STANDARD); seed = mn.mnemonicToSeedSync(cosigner.replace(ELECTRUM_SEED_PREFIX, ''), electrumStandart(passphrase));
} catch (__) { } catch (__) {
throw new Error('Not a valid electrum mnemonic'); throw new Error('Not a valid electrum mnemonic');
} }
@ -997,39 +1016,50 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
cosignPsbt(psbt) { cosignPsbt(psbt) {
for (let cc = 0; cc < psbt.inputCount; cc++) { for (let cc = 0; cc < psbt.inputCount; cc++) {
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) { for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
if (!MultisigHDWallet.isXpubString(cosigner)) { if (MultisigHDWallet.isXpubString(cosigner)) continue;
// ok this is a mnemonic, lets try to sign
const seed = bip39.mnemonicToSeedSync(cosigner);
const hdRoot = bip32.fromSeed(seed);
try {
psbt.signInputHD(cc, hdRoot);
} catch (_) {} // protects agains duplicate cosignings
if (!psbt.inputHasHDKey(cc, hdRoot)) { let hdRoot;
// failed signing as HD. probably bitcoinjs-lib could not match provided hdRoot's if (MultisigHDWallet.isXprvString(cosigner)) {
// fingerprint (or path?) to the ones in psbt, which is the case of stupid Electrum desktop which can const xprv = MultisigHDWallet.convertMultisigXprvToRegularXprv(cosigner);
// put bullshit paths and fingerprints in created psbt. hdRoot = bip32.fromBase58(xprv);
// lets try to find correct priv key and sign manually. } else {
for (const derivation of psbt.data.inputs[cc].bip32Derivation || []) { const passphrase = this._cosignersPassphrases[cosignerIndex];
// okay, here we assume that fingerprint is irrelevant, but ending of the path is somewhat correct and const seed = cosigner.startsWith(ELECTRUM_SEED_PREFIX)
// correctly points to `/internal/index`, so we extract pubkey from our stored mnemonics+path and ? MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner, passphrase)
// match it to the one provided in PSBT's input, and if we have a match - we are in luck! we can sign : bip39.mnemonicToSeedSync(cosigner, passphrase);
// with this private key. hdRoot = bip32.fromSeed(seed);
const seed = bip39.mnemonicToSeedSync(cosigner); }
const root = bip32.fromSeed(seed);
const splt = derivation.path.split('/');
const internal = +splt[splt.length - 2];
const index = +splt[splt.length - 1];
const path = this.getCustomDerivationPathForCosigner(cosignerIndex + 1) + `/${internal ? 1 : 0}/${index}`; try {
// ^^^ we assume that counterparty has Zpub for specified derivation path psbt.signInputHD(cc, hdRoot);
const child = root.derivePath(path); } catch (_) {} // protects agains duplicate cosignings
if (psbt.inputHasPubkey(cc, child.publicKey)) {
const keyPair = ECPair.fromPrivateKey(child.privateKey); if (!psbt.inputHasHDKey(cc, hdRoot)) {
try { // failed signing as HD. probably bitcoinjs-lib could not match provided hdRoot's
psbt.signInput(cc, keyPair); // fingerprint (or path?) to the ones in psbt, which is the case of stupid Electrum desktop which can
} catch (_) {} // put bullshit paths and fingerprints in created psbt.
} // lets try to find correct priv key and sign manually.
for (const derivation of psbt.data.inputs[cc].bip32Derivation || []) {
// okay, here we assume that fingerprint is irrelevant, but ending of the path is somewhat correct and
// correctly points to `/internal/index`, so we extract pubkey from our stored mnemonics+path and
// match it to the one provided in PSBT's input, and if we have a match - we are in luck! we can sign
// with this private key.
const splt = derivation.path.split('/');
const internal = +splt[splt.length - 2];
const index = +splt[splt.length - 1];
const path =
hdRoot.depth === 0
? this.getCustomDerivationPathForCosigner(cosignerIndex + 1) + `/${internal ? 1 : 0}/${index}`
: `${internal ? 1 : 0}/${index}`;
// ^^^ we assume that counterparty has Zpub for specified derivation path
// if hdRoot.depth !== 0 than this hdnode was recovered from xprv and it already has been set to root path
const child = hdRoot.derivePath(path);
if (psbt.inputHasPubkey(cc, child.publicKey)) {
const keyPair = ECPair.fromPrivateKey(child.privateKey);
try {
psbt.signInput(cc, keyPair);
} catch (_) {}
} }
} }
} }
@ -1045,30 +1075,38 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} }
/** /**
* Looks up cosigner by Fingerprint, and repalces all its data with new data * Looks up xpub cosigner by index, and repalces it with seed + passphrase
* *
* @param oldFp {string} Looks up cosigner by this fp * @param externalIndex {number}
* @param newCosigner {string} * @param mnemonic {string}
* @param newFp {string} * @param passphrase {string}
* @param newPath {string}
*/ */
replaceCosigner(oldFp, newCosigner, newFp, newPath) { replaceCosignerXpubWithSeed(externalIndex, mnemonic, passphrase) {
const index = this._cosignersFingerprints.indexOf(oldFp); const index = externalIndex - 1;
if (index === -1) return; const fingerprint = this._cosignersFingerprints[index];
if (!MultisigHDWallet.isXpubValid(newCosigner)) { if (!MultisigHDWallet.isXpubValid(this._cosigners[index])) throw new Error('This cosigner doesnt contain valid xpub');
// its not an xpub, so lets derive fingerprint ourselves if (!bip39.validateMnemonic(mnemonic)) throw new Error('Not a valid mnemonic phrase');
newFp = MultisigHDWallet.mnemonicToFingerprint(newCosigner); if (fingerprint !== MultisigHDWallet.mnemonicToFingerprint(mnemonic, passphrase)) {
if (oldFp !== newFp) { throw new Error('Fingerprint of new seed doesnt match');
throw new Error('Fingerprint of new seed doesnt match');
}
} }
this._cosigners[index] = mnemonic.trim();
this._cosignersPassphrases[index] = passphrase || undefined;
}
this._cosignersFingerprints[index] = newFp; /**
this._cosigners[index] = newCosigner; * Looks up cosigner with seed by index, and repalces it with xpub
*
if (newPath && this.getDerivationPath() !== newPath) { * @param externalIndex {number}
this._cosignersCustomPaths[index] = newPath; */
} replaceCosignerSeedWithXpub(externalIndex) {
const index = externalIndex - 1;
const mnemonics = this._cosigners[index];
if (!bip39.validateMnemonic(mnemonics)) throw new Error('This cosigner doesnt contain valid xpub mnemonic phrase');
const passphrase = this._cosignersPassphrases[index];
const path = this._cosignersCustomPaths[index] || this._derivationPath;
const xpub = this.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(mnemonics, path, passphrase));
this._cosigners[index] = xpub;
this._cosignersPassphrases[index] = undefined;
} }
deleteCosigner(fp) { deleteCosigner(fp) {
@ -1087,6 +1125,10 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return index !== foundIndex; return index !== foundIndex;
}); });
this._cosignersPassphrases = this._cosignersPassphrases.filter((el, index) => {
return index !== foundIndex;
});
/* const newCosigners = []; /* const newCosigners = [];
for (let c = 0; c < this._cosignersFingerprints.length; c++) { for (let c = 0; c < this._cosignersFingerprints.length; c++) {
if (c !== index) newCosigners.push(this._cosignersFingerprints[c]); if (c !== index) newCosigners.push(this._cosignersFingerprints[c]);

View file

@ -1,4 +1,4 @@
import React, { useContext, useRef, useState } from 'react'; import React, { useContext, useRef, useState, useEffect } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@ -8,6 +8,7 @@ import {
LayoutAnimation, LayoutAnimation,
Platform, Platform,
StyleSheet, StyleSheet,
Switch,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, View,
@ -24,7 +25,7 @@ import {
BlueFormMultiInput, BlueFormMultiInput,
BlueSpacing10, BlueSpacing10,
BlueSpacing20, BlueSpacing20,
BlueSpacing40, BlueText,
BlueTextCentered, BlueTextCentered,
} from '../../BlueComponents'; } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle'; import navigationStyle from '../../components/navigationStyle';
@ -48,7 +49,7 @@ const isDesktop = getSystemName() === 'Mac OS X';
const staticCache = {}; const staticCache = {};
const WalletsAddMultisigStep2 = () => { const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled } = useContext(BlueStorageContext); const { addWallet, saveToDisk, isElectrumDisabled, isAdancedModeEnabled, sleep } = useContext(BlueStorageContext);
const { colors } = useTheme(); const { colors } = useTheme();
const navigation = useNavigation(); const navigation = useNavigation();
@ -64,9 +65,16 @@ const WalletsAddMultisigStep2 = () => {
const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.json'); const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.json');
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', isLoading: false }); // string rendered in modal const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', isLoading: false }); // string rendered in modal
const [importText, setImportText] = useState(''); const [importText, setImportText] = useState('');
const [askPassphrase, setAskPassphrase] = useState(false);
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
const openScannerButton = useRef(); const openScannerButton = useRef();
const data = useRef(new Array(n)); const data = useRef(new Array(n));
useEffect(() => {
isAdancedModeEnabled().then(setIsAdvancedModeEnabledRender);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleOnHelpPress = () => { const handleOnHelpPress = () => {
navigation.navigate('WalletsAddMultisigHelp'); navigation.navigate('WalletsAddMultisigHelp');
}; };
@ -104,9 +112,16 @@ const WalletsAddMultisigStep2 = () => {
}, },
}); });
const onCreate = () => { const onCreate = async () => {
setIsLoading(true); setIsLoading(true);
setTimeout(_onCreate, 100); await sleep(100);
try {
await _onCreate(); // this can fail with "Duplicate fingerprint" error or other
} catch (e) {
setIsLoading(false);
alert(e.message);
console.log('create MS wallet error', e);
}
}; };
const _onCreate = async () => { const _onCreate = async () => {
@ -130,8 +145,8 @@ const WalletsAddMultisigStep2 = () => {
throw new Error('This should never happen'); throw new Error('This should never happen');
} }
for (const cc of cosigners) { for (const cc of cosigners) {
const fp = cc[1] || getFpCacheForMnemonics(cc[0]); const fp = cc[1] || getFpCacheForMnemonics(cc[0], cc[3]);
w.addCosigner(cc[0], fp, cc[2]); w.addCosigner(cc[0], fp, cc[2], cc[3]);
} }
w.setLabel(walletLabel); w.setLabel(walletLabel);
if (!isElectrumDisabled) { if (!isElectrumDisabled) {
@ -195,7 +210,7 @@ const WalletsAddMultisigStep2 = () => {
const path = getPath(); const path = getPath();
const xpub = getXpubCacheForMnemonics(cosigner[0]); const xpub = getXpubCacheForMnemonics(cosigner[0]);
const fp = getFpCacheForMnemonics(cosigner[0]); const fp = getFpCacheForMnemonics(cosigner[0], cosigner[3]);
setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path)); setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path));
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setCosignerXpubFilename('bw-cosigner-' + fp + '.json'); setCosignerXpubFilename('bw-cosigner-' + fp + '.json');
@ -216,13 +231,13 @@ const WalletsAddMultisigStep2 = () => {
return staticCache[seed + path]; return staticCache[seed + path];
}; };
const getFpCacheForMnemonics = seed => { const getFpCacheForMnemonics = (seed, passphrase) => {
return staticCache[seed] || setFpCacheForMnemonics(seed); return staticCache[seed + (passphrase ?? '')] || setFpCacheForMnemonics(seed, passphrase);
}; };
const setFpCacheForMnemonics = seed => { const setFpCacheForMnemonics = (seed, passphrase) => {
staticCache[seed] = MultisigHDWallet.mnemonicToFingerprint(seed); staticCache[seed + (passphrase ?? '')] = MultisigHDWallet.mnemonicToFingerprint(seed, passphrase);
return staticCache[seed]; return staticCache[seed + (passphrase ?? '')];
}; };
const iHaveMnemonics = () => { const iHaveMnemonics = () => {
@ -234,6 +249,7 @@ const WalletsAddMultisigStep2 = () => {
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setIsLoading(false); setIsLoading(false);
setImportText(''); setImportText('');
setAskPassphrase(false);
alert(loc.multisig.not_a_multisignature_xpub); alert(loc.multisig.not_a_multisignature_xpub);
return; return;
} }
@ -261,6 +277,7 @@ const WalletsAddMultisigStep2 = () => {
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setIsLoading(false); setIsLoading(false);
setImportText(''); setImportText('');
setAskPassphrase(false);
const cosignersCopy = [...cosigners]; const cosignersCopy = [...cosigners];
cosignersCopy.push([xpub, fp, path]); cosignersCopy.push([xpub, fp, path]);
@ -268,7 +285,7 @@ const WalletsAddMultisigStep2 = () => {
setCosigners(cosignersCopy); setCosigners(cosignersCopy);
}; };
const useMnemonicPhrase = () => { const useMnemonicPhrase = async () => {
setIsLoading(true); setIsLoading(true);
if (MultisigHDWallet.isXpubValid(importText)) { if (MultisigHDWallet.isXpubValid(importText)) {
@ -281,14 +298,28 @@ const WalletsAddMultisigStep2 = () => {
return alert(loc.multisig.invalid_mnemonics); return alert(loc.multisig.invalid_mnemonics);
} }
let passphrase;
if (askPassphrase) {
try {
passphrase = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
} catch (e) {
if (e.message === 'Cancel Pressed') {
setIsLoading(false);
return;
}
throw e;
}
}
const cosignersCopy = [...cosigners]; const cosignersCopy = [...cosigners];
cosignersCopy.push([hd.getSecret(), false, false]); cosignersCopy.push([hd.getSecret(), false, false, passphrase]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy); setCosigners(cosignersCopy);
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setIsLoading(false); setIsLoading(false);
setImportText(''); setImportText('');
setAskPassphrase(false);
}; };
const isValidMnemonicSeed = mnemonicSeed => { const isValidMnemonicSeed = mnemonicSeed => {
@ -392,7 +423,7 @@ const WalletsAddMultisigStep2 = () => {
navigation.navigate('ScanQRCodeRoot', { navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode', screen: 'ScanQRCode',
params: { params: {
onBarScanned: onBarScanned, onBarScanned,
showFileImportButton: true, showFileImportButton: true,
}, },
}), }),
@ -529,6 +560,7 @@ const WalletsAddMultisigStep2 = () => {
Keyboard.dismiss(); Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setImportText(''); setImportText('');
setAskPassphrase(false);
}; };
const renderProvideMnemonicsModal = () => { const renderProvideMnemonicsModal = () => {
@ -539,7 +571,16 @@ const WalletsAddMultisigStep2 = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered> <BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} /> <BlueFormMultiInput value={importText} onChangeText={setImportText} />
<BlueSpacing40 /> {isAdvancedModeEnabledRender && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<BlueSpacing20 />
{isLoading ? ( {isLoading ? (
<ActivityIndicator /> <ActivityIndicator />
) : ( ) : (
@ -704,6 +745,12 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
marginLeft: 8, marginLeft: 8,
}, },
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
}); });
WalletsAddMultisigStep2.navigationOptions = navigationStyle({ WalletsAddMultisigStep2.navigationOptions = navigationStyle({

View file

@ -1,8 +1,7 @@
import React, { useContext, useRef, useState, useCallback } from 'react'; import React, { useContext, useRef, useState, useCallback, useEffect } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
findNodeHandle,
FlatList, FlatList,
InteractionManager, InteractionManager,
Keyboard, Keyboard,
@ -11,8 +10,10 @@ import {
Platform, Platform,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Switch,
Text, Text,
View, View,
findNodeHandle,
} from 'react-native'; } from 'react-native';
import { Icon, Badge } from 'react-native-elements'; import { Icon, Badge } from 'react-native-elements';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native'; import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
@ -24,6 +25,7 @@ import {
BlueSpacing10, BlueSpacing10,
BlueSpacing20, BlueSpacing20,
BlueSpacing40, BlueSpacing40,
BlueText,
BlueTextCentered, BlueTextCentered,
} from '../../BlueComponents'; } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle'; import navigationStyle from '../../components/navigationStyle';
@ -44,11 +46,12 @@ import { encodeUR } from '../../blue_modules/ur';
import QRCodeComponent from '../../components/QRCodeComponent'; import QRCodeComponent from '../../components/QRCodeComponent';
import alert from '../../components/Alert'; import alert from '../../components/Alert';
const fs = require('../../blue_modules/fs'); const fs = require('../../blue_modules/fs');
const prompt = require('../../blue_modules/prompt');
const ViewEditMultisigCosigners = () => { const ViewEditMultisigCosigners = () => {
const hasLoaded = useRef(false); const hasLoaded = useRef(false);
const { colors } = useTheme(); const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useContext(BlueStorageContext); const { wallets, setWalletsWithNewOrder, isElectrumDisabled, isAdancedModeEnabled } = useContext(BlueStorageContext);
const { navigate, goBack } = useNavigation(); const { navigate, goBack } = useNavigation();
const route = useRoute(); const route = useRoute();
const openScannerButtonRef = useRef(); const openScannerButtonRef = useRef();
@ -67,6 +70,8 @@ const ViewEditMultisigCosigners = () => {
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
const [exportFilename, setExportFilename] = useState('bw-cosigner.json'); const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', path: '', fp: '', isLoading: false }); // string rendered in modal const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', path: '', fp: '', isLoading: false }); // string rendered in modal
const [askPassphrase, setAskPassphrase] = useState(false);
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
const data = useRef(); const data = useRef();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
root: { root: {
@ -99,6 +104,11 @@ const ViewEditMultisigCosigners = () => {
}, },
}); });
useEffect(() => {
isAdancedModeEnabled().then(setIsAdvancedModeEnabledRender);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const exportCosigner = () => { const exportCosigner = () => {
setIsShareModalVisible(false); setIsShareModalVisible(false);
setTimeout(() => fs.writeFileAndExport(exportFilename, exportString), 1000); setTimeout(() => fs.writeFileAndExport(exportFilename, exportString), 1000);
@ -238,6 +248,7 @@ const ViewEditMultisigCosigners = () => {
const secret = wallet.getCosigner(el.index + 1).split(' '); const secret = wallet.getCosigner(el.index + 1).split(' ');
leftText = `${secret[0]}...${secret[secret.length - 1]}`; leftText = `${secret[0]}...${secret[secret.length - 1]}`;
} }
return ( return (
<View> <View>
<MultipleStepsListItem <MultipleStepsListItem
@ -373,45 +384,54 @@ const ViewEditMultisigCosigners = () => {
); );
}; };
const handleUseMnemonicPhrase = () => { const handleUseMnemonicPhrase = async () => {
return _handleUseMnemonicPhrase(importText); let passphrase;
if (askPassphrase) {
try {
passphrase = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
} catch (e) {
if (e.message === 'Cancel Pressed') {
setIsLoading(false);
return;
}
throw e;
}
}
return _handleUseMnemonicPhrase(importText, passphrase);
}; };
const _handleUseMnemonicPhrase = mnemonic => { const _handleUseMnemonicPhrase = (mnemonic, passphrase) => {
const hd = new HDSegwitBech32Wallet(); const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic); hd.setSecret(mnemonic);
if (!hd.validateMnemonic()) return alert(loc.multisig.invalid_mnemonics); if (!hd.validateMnemonic()) return alert(loc.multisig.invalid_mnemonics);
try {
wallet.replaceCosignerXpubWithSeed(currentlyEditingCosignerNum, hd.getSecret(), passphrase);
} catch (e) {
console.log(e);
return alert(e.message);
}
const newFp = MultisigHDWallet.mnemonicToFingerprint(hd.getSecret());
if (newFp !== wallet.getFingerprint(currentlyEditingCosignerNum)) return alert(loc.multisig.invalid_fingerprint);
wallet.deleteCosigner(newFp);
wallet.addCosigner(hd.getSecret());
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet); setWallet(wallet);
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setIsSaveButtonDisabled(false); setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
}; };
const xpubInsteadOfSeed = index => { const xpubInsteadOfSeed = index => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
try { try {
const mnemonics = wallet.getCosigner(index); wallet.replaceCosignerSeedWithXpub(index);
const newFp = MultisigHDWallet.mnemonicToFingerprint(mnemonics);
const path = wallet.getCustomDerivationPathForCosigner(index);
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(mnemonics, path));
wallet.deleteCosigner(newFp);
wallet.addCosigner(xpub, newFp, path);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
setIsSaveButtonDisabled(false);
resolve();
} catch (e) { } catch (e) {
alert(e.message);
console.log(e);
reject(e); reject(e);
return alert(e.message);
} }
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
setIsSaveButtonDisabled(false);
resolve();
}); });
}); });
}; };
@ -450,6 +470,7 @@ const ViewEditMultisigCosigners = () => {
Keyboard.dismiss(); Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false); setIsProvideMnemonicsModalVisible(false);
setImportText(''); setImportText('');
setAskPassphrase(false);
}; };
const hideShareModal = () => { const hideShareModal = () => {
@ -464,7 +485,16 @@ const ViewEditMultisigCosigners = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered> <BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} /> <BlueFormMultiInput value={importText} onChangeText={setImportText} />
<BlueSpacing40 /> {isAdvancedModeEnabledRender && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<BlueSpacing20 />
{isLoading ? ( {isLoading ? (
<ActivityIndicator /> <ActivityIndicator />
) : ( ) : (
@ -625,6 +655,12 @@ const styles = StyleSheet.create({
tipLabelText: { tipLabelText: {
fontWeight: '500', fontWeight: '500',
}, },
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
}); });
ViewEditMultisigCosigners.navigationOptions = navigationStyle( ViewEditMultisigCosigners.navigationOptions = navigationStyle(

View file

@ -0,0 +1,104 @@
{
"addr_history": {
"bc1q24rc4v9r6fjtkrwfp4j57ufef56ez46rrpyjtdkhjpr687f5de0sa7ryv5": [],
"bc1q2ajjhxhddfywm7l3uac8flehy8tjkzkd404w8k7w9zgf3fpjtr4qh4g0q3": [],
"bc1q2ltyvkrs0uay39acfk4y0gmw7flghd3403p94x26tc8579t9adwsjp83yz": [],
"bc1q6hqwvrfrcw3eme3gv37hk5yqe8zykmedp6jaq3g0svlgm8gajeksfjfr7t": [],
"bc1q7n8twph2zlfw6w0p5ms9vkvj9klxqhpjy5mv5tnqpcf2pl3d3qrst2pjz7": [],
"bc1qdd4jqq0cex38k9l3rnqrp5ltt5a4uyf2hu5ejdcs4unmuy8rxeaqt74lac": [],
"bc1qdk2g0hxq76d806r8emp8jxpw4wtnpdcf3v60cx3tawfk6epdgghqgvl7qx": [],
"bc1qdm743j76hm3wdzvw4xa3dx4l77vvhwg5hmwr0n94wlr6yjshny7qjecgfy": [],
"bc1qdng92jr5er6fmxjjqd4ffqsjjhxs7s9kq2e9mwxcyf6dtt5m5tsszmwk0y": [],
"bc1qhwrqzyqs8w5zkefkh38d4qf8xygnq4t6lrdzpncjzpghfj37wcmqvzr34t": [],
"bc1qje83yzfand5zqt2gjawcxcygdkzrx56686p2r2650xsqrutpseyqcl2j98": [],
"bc1qk6qe9tnlt9wz0xkllw64jdl5tqd8ha6rct8fed5khhf485p4tr2q04kfq2": [],
"bc1qlgt26dj4g5wh20kkryvx44mlw58myu8q05wz95l99n965ccw2pxq4kpp24": [],
"bc1qmnjpytz3kgqkpl8n6t28k88semprxw4v85lhzcun3xvf5jxjgjtslyywzn": [],
"bc1qmpyrvv6fmkv494r9qk9nllyuyngtqj62fywcl2xzessgwf9qgrssxff69u": [],
"bc1qnm86j32s0nlnrs7mk4c84z8rru52954cv26jlhlulkrp927xfqsqp2n6sr": [],
"bc1qnn0n9zzfnnrtvrp0yug4gpg42vpqty0utftvs77s46un9kegzn0syksa3c": [],
"bc1qnpmtxpfnj9grfldlc3y5gyxghzyjgw5682sdn46uum63vdh56tssw6mzdt": [],
"bc1qq75kuw877vzywwvlstgjvyy6ep40htg4jsreavnz6wmj0qzweufsuhdapm": [],
"bc1qqgh3vparlgnfljkk6kfk2tpakz6rvzulczx5tuw63tnw4aw52crs44hcqw": [],
"bc1qqjurkk6q78qzm7pnvn0vmmcm8lxwj8ux6yrhzzy2u5984uhl62dqf87csz": [],
"bc1qrhrfw75nhg367gvkpw7mn5w0pj3k9tg3mam3j4x0sslp9p6ejzxsmauvds": [],
"bc1qs5q9kzr4v79455urwep5sq37nxtq5kaltk522t6tu8epdkgvjjcsvx60dj": [],
"bc1qsydvt0dttn0wd6t8ua6p4xje5yfku0f42nr8hlepasamsqffan9s6cvpps": [],
"bc1qvfk305khj7j38zpm9x4yt5cr6dtqr6cepdjuwnvk2qqnc3d87eqqadegaj": [],
"bc1qvjgk3rd9vhs9fel8602ae5jjp4zjnkdeh85jhfgfykr4xkpmnmjqwvp5pn": [],
"bc1qwqrvajjn6uer8jg4rm2mfu0v6ewa2vg27ucwdn7az39rma2wqe5q0pv0j3": [],
"bc1qwvrrrm9kcgc42y8saghlxhydgdx7aaedgv3r5x2uxc8ns2htr0gqumzhzw": [],
"bc1qxgws6t6gxtmltzlwqjcqpy2d468arpkq6calp2d2ln7a9udflfjssqhy2x": [],
"bc1qynttcstsknxguha7pcqxe0grfkh7xceqauc0gc0yqn4aathjzw8s4nktx4": []
},
"addresses": {
"change": [
"bc1q2ltyvkrs0uay39acfk4y0gmw7flghd3403p94x26tc8579t9adwsjp83yz",
"bc1q24rc4v9r6fjtkrwfp4j57ufef56ez46rrpyjtdkhjpr687f5de0sa7ryv5",
"bc1qqjurkk6q78qzm7pnvn0vmmcm8lxwj8ux6yrhzzy2u5984uhl62dqf87csz",
"bc1q2ajjhxhddfywm7l3uac8flehy8tjkzkd404w8k7w9zgf3fpjtr4qh4g0q3",
"bc1qwvrrrm9kcgc42y8saghlxhydgdx7aaedgv3r5x2uxc8ns2htr0gqumzhzw",
"bc1qlgt26dj4g5wh20kkryvx44mlw58myu8q05wz95l99n965ccw2pxq4kpp24",
"bc1qs5q9kzr4v79455urwep5sq37nxtq5kaltk522t6tu8epdkgvjjcsvx60dj",
"bc1qvfk305khj7j38zpm9x4yt5cr6dtqr6cepdjuwnvk2qqnc3d87eqqadegaj",
"bc1qq75kuw877vzywwvlstgjvyy6ep40htg4jsreavnz6wmj0qzweufsuhdapm",
"bc1qrhrfw75nhg367gvkpw7mn5w0pj3k9tg3mam3j4x0sslp9p6ejzxsmauvds"
],
"receiving": [
"bc1qmpyrvv6fmkv494r9qk9nllyuyngtqj62fywcl2xzessgwf9qgrssxff69u",
"bc1q7n8twph2zlfw6w0p5ms9vkvj9klxqhpjy5mv5tnqpcf2pl3d3qrst2pjz7",
"bc1qsydvt0dttn0wd6t8ua6p4xje5yfku0f42nr8hlepasamsqffan9s6cvpps",
"bc1qvjgk3rd9vhs9fel8602ae5jjp4zjnkdeh85jhfgfykr4xkpmnmjqwvp5pn",
"bc1qnpmtxpfnj9grfldlc3y5gyxghzyjgw5682sdn46uum63vdh56tssw6mzdt",
"bc1qnm86j32s0nlnrs7mk4c84z8rru52954cv26jlhlulkrp927xfqsqp2n6sr",
"bc1qmnjpytz3kgqkpl8n6t28k88semprxw4v85lhzcun3xvf5jxjgjtslyywzn",
"bc1qdk2g0hxq76d806r8emp8jxpw4wtnpdcf3v60cx3tawfk6epdgghqgvl7qx",
"bc1qje83yzfand5zqt2gjawcxcygdkzrx56686p2r2650xsqrutpseyqcl2j98",
"bc1q6hqwvrfrcw3eme3gv37hk5yqe8zykmedp6jaq3g0svlgm8gajeksfjfr7t",
"bc1qqgh3vparlgnfljkk6kfk2tpakz6rvzulczx5tuw63tnw4aw52crs44hcqw",
"bc1qynttcstsknxguha7pcqxe0grfkh7xceqauc0gc0yqn4aathjzw8s4nktx4",
"bc1qhwrqzyqs8w5zkefkh38d4qf8xygnq4t6lrdzpncjzpghfj37wcmqvzr34t",
"bc1qdm743j76hm3wdzvw4xa3dx4l77vvhwg5hmwr0n94wlr6yjshny7qjecgfy",
"bc1qnn0n9zzfnnrtvrp0yug4gpg42vpqty0utftvs77s46un9kegzn0syksa3c",
"bc1qdng92jr5er6fmxjjqd4ffqsjjhxs7s9kq2e9mwxcyf6dtt5m5tsszmwk0y",
"bc1qdd4jqq0cex38k9l3rnqrp5ltt5a4uyf2hu5ejdcs4unmuy8rxeaqt74lac",
"bc1qwqrvajjn6uer8jg4rm2mfu0v6ewa2vg27ucwdn7az39rma2wqe5q0pv0j3",
"bc1qxgws6t6gxtmltzlwqjcqpy2d468arpkq6calp2d2ln7a9udflfjssqhy2x",
"bc1qk6qe9tnlt9wz0xkllw64jdl5tqd8ha6rct8fed5khhf485p4tr2q04kfq2"
]
},
"fiat_value": {},
"frozen_coins": {},
"invoices": {},
"labels": {},
"payment_requests": {},
"prevouts_by_scripthash": {},
"seed_version": 40,
"spent_outpoints": {},
"transactions": {},
"tx_fees": {},
"txi": {},
"txo": {},
"use_encryption": false,
"verified_tx3": {},
"wallet_type": "2of2",
"x1/": {
"derivation": "m/1'",
"passphrase": "BlueWallet",
"pw_hash_version": 1,
"root_fingerprint": "8de7b2c3",
"seed": "diagram grape account sustain bright member ethics strategy burger senior capital enforce",
"seed_type": "segwit",
"type": "bip32",
"xprv": "ZprvAkWFuUtVQZqsWhwomGuqCAWpcfpd5tNXuQoBMLqqqmTVqmiRiMKXbig3PH3BcVPh2Xmb7TH7w3tDfa1Z7ik3ZgFQB3G9rAxXvp6Cy1onbYc",
"xpub": "Zpub6yVcJzRPEwQAjC2GsJSqZJTZAhf7VM6PGdin9jFTQ6zUia3aFtdn9WzXEZBk1AcgSgQ7kLcysk5CWTuJppjFKdYJDMYVT9hZF71PyPijNUF"
},
"x2/": {
"derivation": "m/48'/0'/0'/2'",
"pw_hash_version": 1,
"root_fingerprint": "84431270",
"type": "bip32",
"xprv": "ZprvAqPkyb5ridHr1gGiqSuAWcsrZ6jqv31Vyuaj4fzVcgt8v6PXH9tSigE6F8iw8pL16HWnhzEsXvJ5ur9HKvkAW16oHZuFeEYA1CBdsGGDFFB",
"xpub": "Zpub74P7P6ckYzr9EAMBwUSAskpb78aLKVjMM8WKs4Q7B2R7ntifphChGUYa6T5viWMe5Rjy6kMwRArcg9sQsMey3s9k4JLcKiCzYrDRQMdMo8L"
}
}

View file

@ -1417,6 +1417,59 @@ describe('multisig-wallet (native segwit)', () => {
} }
}); });
it('can import electrum json file format with seeds and passphrase', () => {
const json = require('./fixtures/electrum-multisig-wallet-with-seed-and-passphrase.json');
const w = new MultisigHDWallet();
w.setSecret(JSON.stringify(json));
assert.strictEqual(w.getM(), 2);
assert.strictEqual(w.getN(), 2);
assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1qmpyrvv6fmkv494r9qk9nllyuyngtqj62fywcl2xzessgwf9qgrssxff69u');
assert.strictEqual(w._getExternalAddressByIndex(1), 'bc1q7n8twph2zlfw6w0p5ms9vkvj9klxqhpjy5mv5tnqpcf2pl3d3qrst2pjz7');
assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1q2ltyvkrs0uay39acfk4y0gmw7flghd3403p94x26tc8579t9adwsjp83yz');
assert.strictEqual(w._getInternalAddressByIndex(1), 'bc1q24rc4v9r6fjtkrwfp4j57ufef56ez46rrpyjtdkhjpr687f5de0sa7ryv5');
assert.ok(w.isNativeSegwit());
assert.ok(!w.isWrappedSegwit());
assert.ok(!w.isLegacy());
assert.strictEqual(w.getCustomDerivationPathForCosigner(1), "m/1'");
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), "m/48'/0'/0'/2'");
assert.strictEqual(w.getFingerprint(1), '8de7b2c3'.toUpperCase());
assert.strictEqual(w.getFingerprint(2), '84431270'.toUpperCase());
const utxos = [
{
address: 'bc1qmpyrvv6fmkv494r9qk9nllyuyngtqj62fywcl2xzessgwf9qgrssxff69u',
amount: 68419,
height: 0,
txId: '2d40b967bb3a4ecd8517843d01042b0dd4227192acbe0e1ad1f1cf144a1ec0c9',
txhex:
'02000000000101d7bf498a92b19bab8a58260efedd7e6cd3b7713ff1e9d2603ff9f06a64f66291000000001716001440512e04b685a0cd66a03bea0896c27000c828dcffffffff01430b010000000000220020d848363349dd9952d465058b3ffc9c24d0b04b4a491d8fa8c2cc208724a040e10247304402201ad742ffee74e5ae4b3867d9818b8ad6505ca5239280138f9da3f93e4c27ee0802202918fa6034485077596bf64501ae6954371e91d250ee98f5a3c5889d4dee923e012103a681da832358050bd9b197aaa55d921f1447025b999eadb018aa67c5b8f64a0900000000',
txid: '2d40b967bb3a4ecd8517843d01042b0dd4227192acbe0e1ad1f1cf144a1ec0c9',
value: 68419,
vout: 0,
wif: false,
},
];
const { psbt } = w.createTransaction(
utxos,
[{ address: '39RXMPjwKwoEGJeABJvdG1N4nQAzfEgcos' }],
1,
w._getInternalAddressByIndex(3),
false,
true,
);
assert.ok(psbt);
// we are using .cosignPsbt for now, because .createTransaction throws
// Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint
// https://github.com/BlueWallet/BlueWallet/pull/2466
const { tx } = w.cosignPsbt(psbt);
assert.ok(tx);
});
it('cant import garbage', () => { it('cant import garbage', () => {
const w = new MultisigHDWallet(); const w = new MultisigHDWallet();
w.setSecret('garbage'); w.setSecret('garbage');
@ -1668,14 +1721,12 @@ describe('multisig-wallet (native segwit)', () => {
assert.strictEqual(w.getCosignerForFingerprint(fp2coldcard), process.env.MNEMONICS_COLDCARD); assert.strictEqual(w.getCosignerForFingerprint(fp2coldcard), process.env.MNEMONICS_COLDCARD);
assert.strictEqual(w.howManySignaturesCanWeMake(), 1); assert.strictEqual(w.howManySignaturesCanWeMake(), 1);
w.replaceCosigner(fp2coldcard, Zpub2, fp2coldcard, path); // <------------------- w.replaceCosignerSeedWithXpub(2);
assert.strictEqual(w.getCosigner(2), Zpub2); assert.strictEqual(w.getCosigner(2), Zpub2);
assert.strictEqual(w.getFingerprint(2), fp2coldcard); assert.strictEqual(w.getFingerprint(2), fp2coldcard);
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path); assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path);
w.replaceCosigner(fp2coldcard, process.env.MNEMONICS_COLDCARD); // <--------------------------- w.replaceCosignerXpubWithSeed(2, process.env.MNEMONICS_COLDCARD);
assert.strictEqual(w.getCosigner(2), process.env.MNEMONICS_COLDCARD); assert.strictEqual(w.getCosigner(2), process.env.MNEMONICS_COLDCARD);
assert.strictEqual(w.getFingerprint(2), fp2coldcard); assert.strictEqual(w.getFingerprint(2), fp2coldcard);
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path); assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path);
@ -1704,6 +1755,49 @@ describe('multisig-wallet (native segwit)', () => {
assert.ok(!w.getCustomDerivationPathForCosigner(2)); assert.ok(!w.getCustomDerivationPathForCosigner(2));
assert.strictEqual(w.getN(), 1); assert.strictEqual(w.getN(), 1);
assert.strictEqual(w.getM(), 2); assert.strictEqual(w.getM(), 2);
w.addCosigner(
'salon smoke bubble dolphin powder govern rival sport better arrest certain manual',
undefined,
undefined,
'9WDdFSZX4d6mPxkr',
);
assert.strictEqual(w.getN(), 2);
w.replaceCosignerSeedWithXpub(2);
assert.strictEqual(
w.getCosigner(2),
'Zpub752NRx3S4ax3S5oLHLB2DAQx9X3Ek4EGvtsyYTpzQ2VRdXB6DjL5ZKiHhcUqfZM6M2KCVB5vSXEQ4jMosHWuF4dD5pwowfzL4fmJz5FaJHh',
);
assert.strictEqual(w.getFingerprint(2), '2C0908B6');
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path);
assert.ok(!w.getPassphrase(2));
w.replaceCosignerXpubWithSeed(
2,
'salon smoke bubble dolphin powder govern rival sport better arrest certain manual',
'9WDdFSZX4d6mPxkr',
);
assert.strictEqual(w.getCosigner(2), 'salon smoke bubble dolphin powder govern rival sport better arrest certain manual');
assert.strictEqual(w.getFingerprint(2), '2C0908B6');
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path);
assert.strictEqual(w.getPassphrase(2), '9WDdFSZX4d6mPxkr');
// test that after deleting cosinger with passphrase, it has been cleaned out properly
w.deleteCosigner('2C0908B6');
assert.ok(!w.getCosigner(2));
assert.ok(!w.getFingerprint(2));
assert.ok(!w.getCustomDerivationPathForCosigner(2));
assert.ok(!w.getPassphrase(2));
assert.strictEqual(w.getN(), 1);
assert.strictEqual(w.getM(), 2);
// after chaning first cosigner, make sure that he changed, not the second one
w.replaceCosignerXpubWithSeed(1, process.env.MNEMONICS_COBO);
assert.strictEqual(w.getCosigner(1), process.env.MNEMONICS_COBO);
assert.strictEqual(w.getFingerprint(1), fp1cobo);
assert.strictEqual(w.getCustomDerivationPathForCosigner(1), path);
assert.strictEqual(w.getPassphrase(1), undefined);
}); });
it('can sign valid tx if we have more keys than quorum ("Too many signatures" error)', async () => { it('can sign valid tx if we have more keys than quorum ("Too many signatures" error)', async () => {
@ -1829,6 +1923,42 @@ describe('multisig-wallet (native segwit)', () => {
assert.strictEqual(psbt.data.inputs.length, 2); assert.strictEqual(psbt.data.inputs.length, 2);
assert.strictEqual(psbt.data.outputs.length, 1); assert.strictEqual(psbt.data.outputs.length, 1);
}); });
it('can generate proper addresses for wallets with passphrases. Export and import such wallet', () => {
// test case from https://github.com/BlueWallet/BlueWallet/issues/3665#issuecomment-907377442
const path = "m/48'/0'/0'/2'";
const w = new MultisigHDWallet();
w.addCosigner(
'salon smoke bubble dolphin powder govern rival sport better arrest certain manual',
undefined,
undefined,
'9WDdFSZX4d6mPxkr',
);
w.addCosigner('chaos word void picture gas update shop wave task blossom close inner', undefined, undefined, 'E5jMAzsf464Hgwns');
w.addCosigner(
'plate inform scissors pill asset scatter people emotion dose primary together expose',
undefined,
undefined,
'RyBFfLr7weK3nDUG',
);
w.setDerivationPath(path);
w.setM(2);
assert.strictEqual(w.getPassphrase(1), '9WDdFSZX4d6mPxkr');
assert.strictEqual(w.getPassphrase(2), 'E5jMAzsf464Hgwns');
assert.strictEqual(w.getPassphrase(3), 'RyBFfLr7weK3nDUG');
assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1q8rks34ypj5edxx82f7z7yzy4qy6dynfhcftjs9axzr2ml37p4pfs7j4uvm');
assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1qjpjgumzs2afrr3mk85anwdnzd9qg5hc5p6f62un4umpyf4ccde5q4cywgy');
const w2 = new MultisigHDWallet();
w2.setSecret(w.getSecret());
assert.strictEqual(w._getExternalAddressByIndex(0), w2._getExternalAddressByIndex(0));
assert.strictEqual(w._getExternalAddressByIndex(1), w2._getExternalAddressByIndex(1));
assert.strictEqual(w.getPassphrase(1), w2.getPassphrase(1));
assert.strictEqual(w.getPassphrase(2), w2.getPassphrase(2));
assert.strictEqual(w.getPassphrase(3), w2.getPassphrase(3));
});
}); });
describe('multisig-cosigner', () => { describe('multisig-cosigner', () => {