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)
* @returns {string} Hex fingerprint
*/
static mnemonicToFingerprint(mnemonic) {
const seed = bip39.mnemonicToSeedSync(mnemonic);
static mnemonicToFingerprint(mnemonic, passphrase) {
const seed = bip39.mnemonicToSeedSync(mnemonic, passphrase);
return AbstractHDElectrumWallet.seedToFingerprint(seed);
}

View file

@ -13,13 +13,15 @@ const createHash = require('create-hash');
const reverse = require('buffer-reverse');
const mn = require('electrum-mnemonic');
const MNEMONIC_TO_SEED_OPTS_SEGWIT = {
const electrumSegwit = passphrase => ({
prefix: mn.PREFIXES.segwit,
};
...(passphrase ? { passphrase } : {}),
});
const MNEMONIC_TO_SEED_OPTS_STANDARD = {
const electrumStandart = passphrase => ({
prefix: mn.PREFIXES.standard,
};
...(passphrase ? { passphrase } : {}),
});
const ELECTRUM_SEED_PREFIX = 'electrumseed:';
@ -42,6 +44,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
this._cosigners = []; // array of xpubs or mnemonic seeds
this._cosignersFingerprints = []; // array of according fingerprints (if any provided)
this._cosignersCustomPaths = []; // array of according paths (if any provided)
this._cosignersPassphrases = []; // array of according passphrases (if any provided)
this._derivationPath = '';
this._isNativeSegwit = false;
this._isWrappedSegwit = false;
@ -130,6 +133,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
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) {
let xpub;
@ -157,8 +165,9 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
* @param key {string} Either xpub or mnemonic phrase
* @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 passphrase {string} BIP38 Passphrase (if any)
*/
addCosigner(key, fingerprint, path) {
addCosigner(key, fingerprint, path, passphrase) {
if (MultisigHDWallet.isXpubString(key) && !fingerprint) {
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
const mnemonic = key.replace(ELECTRUM_SEED_PREFIX, '');
try {
mn.mnemonicToSeedSync(mnemonic, MNEMONIC_TO_SEED_OPTS_STANDARD);
mn.mnemonicToSeedSync(mnemonic, electrumStandart(passphrase));
this.setLegacy();
} catch (_) {
try {
mn.mnemonicToSeedSync(mnemonic, MNEMONIC_TO_SEED_OPTS_SEGWIT);
mn.mnemonicToSeedSync(mnemonic, electrumSegwit(passphrase));
this.setNativeSegwit();
} catch (__) {
throw new Error('Not a valid electrum seed');
@ -189,7 +198,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.mnemonicToFingerprint(key);
fingerprint = fingerprint || MultisigHDWallet.mnemonicToFingerprint(key, passphrase);
}
if (fingerprint && this._cosignersFingerprints.indexOf(fingerprint.toUpperCase()) !== -1 && fingerprint !== '00000000') {
@ -201,6 +210,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
this._cosigners[index] = key;
if (fingerprint) this._cosignersFingerprints[index] = fingerprint.toUpperCase();
if (path) this._cosignersCustomPaths[index] = path;
if (passphrase) this._cosignersPassphrases[index] = passphrase;
}
static convertMultisigXprvToRegularXprv(Zprv) {
@ -226,7 +236,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
let xpub = cosigner;
if (!MultisigHDWallet.isXpubString(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);
}
@ -243,8 +257,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getAddressFromNode(nodeIndex, index) {
const pubkeys = [];
let cosignerIndex = 0;
for (const cosigner of this._cosigners) {
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
this._nodes = this._nodes || [];
this._nodes[nodeIndex] = this._nodes[nodeIndex] || [];
let _node;
@ -259,7 +272,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
pubkeys.push(_node.derive(index).publicKey);
cosignerIndex++;
}
if (this.isWrappedSegwit()) {
@ -297,12 +309,12 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return address;
}
static seedToXpub(mnemonic, path) {
static seedToXpub(mnemonic, path, passphrase) {
let seed;
if (mnemonic.startsWith(ELECTRUM_SEED_PREFIX)) {
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic);
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic, passphrase);
} else {
seed = bip39.mnemonicToSeedSync(mnemonic);
seed = bip39.mnemonicToSeedSync(mnemonic, passphrase);
}
const root = bip32.fromSeed(seed);
@ -434,13 +446,18 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else {
if (coordinationSetup) {
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';
} else {
ret += 'seed: ' + this._cosigners[index] + '\n';
ret += '# warning! sensitive information, do not disclose ^^^ \n';
ret += 'seed: ' + this._cosigners[index];
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)
: cosignerData.root_fingerprint?.toUpperCase()) || '00000000';
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)) {
this.addCosigner(cosignerData.xprv, fingerprint, cosignerData.derivation);
} else {
@ -533,7 +550,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
} else if (key.replace('#', '').trim() === 'derivation') {
customPathForCurrentCosigner = value.trim();
} else if (key === 'seed') {
this.addCosigner(value.trim(), false, customPathForCurrentCosigner);
const [seed, passphrase] = value.split(' - ');
this.addCosigner(seed.trim(), false, customPathForCurrentCosigner, passphrase);
}
break;
}
@ -640,11 +658,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) {
const cosigner = this._cosigners[c];
const path = this._getDerivationPathByAddressWithCustomPath(input.address, this._cosignersCustomPaths[c] || this._derivationPath);
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
const path = this._getDerivationPathByAddressWithCustomPath(
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
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
const masterFingerprint = Buffer.from(this._cosignersFingerprints[cosignerIndex], 'hex');
const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = bip32.fromBase58(xpub);
@ -731,14 +751,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getOutputDataForChange(outputData) {
const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (let c = 0; c < this._cosigners.length; c++) {
const cosigner = this._cosigners[c];
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
const path = this._getDerivationPathByAddressWithCustomPath(
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
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
const masterFingerprint = Buffer.from(this._cosignersFingerprints[cosignerIndex], 'hex');
const xpub = this._getXpubFromCosigner(cosigner);
const hdNode0 = bip32.fromBase58(xpub);
@ -844,16 +863,17 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
if (!skipSigning) {
for (let cc = 0; cc < c; cc++) {
let signaturesMade = 0;
for (const cosigner of this._cosigners) {
if (!MultisigHDWallet.isXpubString(cosigner)) {
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
if (MultisigHDWallet.isXpubString(cosigner)) continue;
// ok this is a mnemonic, lets try to sign
if (signaturesMade >= this.getM()) {
// dont sign more than we need, otherwise there will be "Too many signatures" error
continue;
}
let seed = bip39.mnemonicToSeedSync(cosigner);
const passphrase = this._cosignersPassphrases[cosignerIndex];
let seed = bip39.mnemonicToSeedSync(cosigner, passphrase);
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner);
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner, passphrase);
}
const hdRoot = bip32.fromSeed(seed);
@ -862,7 +882,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
}
}
}
let tx;
if (!skipSigning && this.howManySignaturesCanWeMake() >= this.getM()) {
@ -871,13 +890,13 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return { tx, inputs, outputs, fee, psbt };
}
static convertElectrumMnemonicToSeed(cosigner) {
static convertElectrumMnemonicToSeed(cosigner, passphrase) {
let seed;
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 (_) {
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 (__) {
throw new Error('Not a valid electrum mnemonic');
}
@ -997,10 +1016,20 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
cosignPsbt(psbt) {
for (let cc = 0; cc < psbt.inputCount; cc++) {
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
if (!MultisigHDWallet.isXpubString(cosigner)) {
// ok this is a mnemonic, lets try to sign
const seed = bip39.mnemonicToSeedSync(cosigner);
const hdRoot = bip32.fromSeed(seed);
if (MultisigHDWallet.isXpubString(cosigner)) continue;
let hdRoot;
if (MultisigHDWallet.isXprvString(cosigner)) {
const xprv = MultisigHDWallet.convertMultisigXprvToRegularXprv(cosigner);
hdRoot = bip32.fromBase58(xprv);
} else {
const passphrase = this._cosignersPassphrases[cosignerIndex];
const seed = cosigner.startsWith(ELECTRUM_SEED_PREFIX)
? MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner, passphrase)
: bip39.mnemonicToSeedSync(cosigner, passphrase);
hdRoot = bip32.fromSeed(seed);
}
try {
psbt.signInputHD(cc, hdRoot);
} catch (_) {} // protects agains duplicate cosignings
@ -1015,15 +1044,17 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
// 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 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}`;
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
const child = root.derivePath(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 {
@ -1034,7 +1065,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
}
}
}
let tx = false;
if (this.calculateHowManySignaturesWeHaveFromPsbt(psbt) >= this.getM()) {
@ -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 newCosigner {string}
* @param newFp {string}
* @param newPath {string}
* @param externalIndex {number}
* @param mnemonic {string}
* @param passphrase {string}
*/
replaceCosigner(oldFp, newCosigner, newFp, newPath) {
const index = this._cosignersFingerprints.indexOf(oldFp);
if (index === -1) return;
if (!MultisigHDWallet.isXpubValid(newCosigner)) {
// its not an xpub, so lets derive fingerprint ourselves
newFp = MultisigHDWallet.mnemonicToFingerprint(newCosigner);
if (oldFp !== newFp) {
replaceCosignerXpubWithSeed(externalIndex, mnemonic, passphrase) {
const index = externalIndex - 1;
const fingerprint = this._cosignersFingerprints[index];
if (!MultisigHDWallet.isXpubValid(this._cosigners[index])) throw new Error('This cosigner doesnt contain valid xpub');
if (!bip39.validateMnemonic(mnemonic)) throw new Error('Not a valid mnemonic phrase');
if (fingerprint !== MultisigHDWallet.mnemonicToFingerprint(mnemonic, passphrase)) {
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;
if (newPath && this.getDerivationPath() !== newPath) {
this._cosignersCustomPaths[index] = newPath;
}
/**
* Looks up cosigner with seed by index, and repalces it with xpub
*
* @param externalIndex {number}
*/
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) {
@ -1087,6 +1125,10 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
return index !== foundIndex;
});
this._cosignersPassphrases = this._cosignersPassphrases.filter((el, index) => {
return index !== foundIndex;
});
/* const newCosigners = [];
for (let c = 0; c < this._cosignersFingerprints.length; 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 {
ActivityIndicator,
FlatList,
@ -8,6 +8,7 @@ import {
LayoutAnimation,
Platform,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
@ -24,7 +25,7 @@ import {
BlueFormMultiInput,
BlueSpacing10,
BlueSpacing20,
BlueSpacing40,
BlueText,
BlueTextCentered,
} from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
@ -48,7 +49,7 @@ const isDesktop = getSystemName() === 'Mac OS X';
const staticCache = {};
const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled } = useContext(BlueStorageContext);
const { addWallet, saveToDisk, isElectrumDisabled, isAdancedModeEnabled, sleep } = useContext(BlueStorageContext);
const { colors } = useTheme();
const navigation = useNavigation();
@ -64,9 +65,16 @@ const WalletsAddMultisigStep2 = () => {
const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.json');
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', isLoading: false }); // string rendered in modal
const [importText, setImportText] = useState('');
const [askPassphrase, setAskPassphrase] = useState(false);
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
const openScannerButton = useRef();
const data = useRef(new Array(n));
useEffect(() => {
isAdancedModeEnabled().then(setIsAdvancedModeEnabledRender);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleOnHelpPress = () => {
navigation.navigate('WalletsAddMultisigHelp');
};
@ -104,9 +112,16 @@ const WalletsAddMultisigStep2 = () => {
},
});
const onCreate = () => {
const onCreate = async () => {
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 () => {
@ -130,8 +145,8 @@ const WalletsAddMultisigStep2 = () => {
throw new Error('This should never happen');
}
for (const cc of cosigners) {
const fp = cc[1] || getFpCacheForMnemonics(cc[0]);
w.addCosigner(cc[0], fp, cc[2]);
const fp = cc[1] || getFpCacheForMnemonics(cc[0], cc[3]);
w.addCosigner(cc[0], fp, cc[2], cc[3]);
}
w.setLabel(walletLabel);
if (!isElectrumDisabled) {
@ -195,7 +210,7 @@ const WalletsAddMultisigStep2 = () => {
const path = getPath();
const xpub = getXpubCacheForMnemonics(cosigner[0]);
const fp = getFpCacheForMnemonics(cosigner[0]);
const fp = getFpCacheForMnemonics(cosigner[0], cosigner[3]);
setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path));
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setCosignerXpubFilename('bw-cosigner-' + fp + '.json');
@ -216,13 +231,13 @@ const WalletsAddMultisigStep2 = () => {
return staticCache[seed + path];
};
const getFpCacheForMnemonics = seed => {
return staticCache[seed] || setFpCacheForMnemonics(seed);
const getFpCacheForMnemonics = (seed, passphrase) => {
return staticCache[seed + (passphrase ?? '')] || setFpCacheForMnemonics(seed, passphrase);
};
const setFpCacheForMnemonics = seed => {
staticCache[seed] = MultisigHDWallet.mnemonicToFingerprint(seed);
return staticCache[seed];
const setFpCacheForMnemonics = (seed, passphrase) => {
staticCache[seed + (passphrase ?? '')] = MultisigHDWallet.mnemonicToFingerprint(seed, passphrase);
return staticCache[seed + (passphrase ?? '')];
};
const iHaveMnemonics = () => {
@ -234,6 +249,7 @@ const WalletsAddMultisigStep2 = () => {
setIsProvideMnemonicsModalVisible(false);
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
alert(loc.multisig.not_a_multisignature_xpub);
return;
}
@ -261,6 +277,7 @@ const WalletsAddMultisigStep2 = () => {
setIsProvideMnemonicsModalVisible(false);
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
const cosignersCopy = [...cosigners];
cosignersCopy.push([xpub, fp, path]);
@ -268,7 +285,7 @@ const WalletsAddMultisigStep2 = () => {
setCosigners(cosignersCopy);
};
const useMnemonicPhrase = () => {
const useMnemonicPhrase = async () => {
setIsLoading(true);
if (MultisigHDWallet.isXpubValid(importText)) {
@ -281,14 +298,28 @@ const WalletsAddMultisigStep2 = () => {
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];
cosignersCopy.push([hd.getSecret(), false, false]);
cosignersCopy.push([hd.getSecret(), false, false, passphrase]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
setIsProvideMnemonicsModalVisible(false);
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
};
const isValidMnemonicSeed = mnemonicSeed => {
@ -392,7 +423,7 @@ const WalletsAddMultisigStep2 = () => {
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: onBarScanned,
onBarScanned,
showFileImportButton: true,
},
}),
@ -529,6 +560,7 @@ const WalletsAddMultisigStep2 = () => {
Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false);
setImportText('');
setAskPassphrase(false);
};
const renderProvideMnemonicsModal = () => {
@ -539,7 +571,16 @@ const WalletsAddMultisigStep2 = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<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 ? (
<ActivityIndicator />
) : (
@ -704,6 +745,12 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginLeft: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
});
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 {
ActivityIndicator,
Alert,
findNodeHandle,
FlatList,
InteractionManager,
Keyboard,
@ -11,8 +10,10 @@ import {
Platform,
StatusBar,
StyleSheet,
Switch,
Text,
View,
findNodeHandle,
} from 'react-native';
import { Icon, Badge } from 'react-native-elements';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
@ -24,6 +25,7 @@ import {
BlueSpacing10,
BlueSpacing20,
BlueSpacing40,
BlueText,
BlueTextCentered,
} from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
@ -44,11 +46,12 @@ import { encodeUR } from '../../blue_modules/ur';
import QRCodeComponent from '../../components/QRCodeComponent';
import alert from '../../components/Alert';
const fs = require('../../blue_modules/fs');
const prompt = require('../../blue_modules/prompt');
const ViewEditMultisigCosigners = () => {
const hasLoaded = useRef(false);
const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useContext(BlueStorageContext);
const { wallets, setWalletsWithNewOrder, isElectrumDisabled, isAdancedModeEnabled } = useContext(BlueStorageContext);
const { navigate, goBack } = useNavigation();
const route = useRoute();
const openScannerButtonRef = useRef();
@ -67,6 +70,8 @@ const ViewEditMultisigCosigners = () => {
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
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 stylesHook = StyleSheet.create({
root: {
@ -99,6 +104,11 @@ const ViewEditMultisigCosigners = () => {
},
});
useEffect(() => {
isAdancedModeEnabled().then(setIsAdvancedModeEnabledRender);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const exportCosigner = () => {
setIsShareModalVisible(false);
setTimeout(() => fs.writeFileAndExport(exportFilename, exportString), 1000);
@ -238,6 +248,7 @@ const ViewEditMultisigCosigners = () => {
const secret = wallet.getCosigner(el.index + 1).split(' ');
leftText = `${secret[0]}...${secret[secret.length - 1]}`;
}
return (
<View>
<MultipleStepsListItem
@ -373,45 +384,54 @@ const ViewEditMultisigCosigners = () => {
);
};
const handleUseMnemonicPhrase = () => {
return _handleUseMnemonicPhrase(importText);
const handleUseMnemonicPhrase = async () => {
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();
hd.setSecret(mnemonic);
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);
setWallet(wallet);
setIsProvideMnemonicsModalVisible(false);
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
};
const xpubInsteadOfSeed = index => {
return new Promise((resolve, reject) => {
InteractionManager.runAfterInteractions(() => {
try {
const mnemonics = wallet.getCosigner(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);
wallet.replaceCosignerSeedWithXpub(index);
} catch (e) {
reject(e);
return alert(e.message);
}
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
setIsSaveButtonDisabled(false);
resolve();
} catch (e) {
alert(e.message);
console.log(e);
reject(e);
}
});
});
};
@ -450,6 +470,7 @@ const ViewEditMultisigCosigners = () => {
Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false);
setImportText('');
setAskPassphrase(false);
};
const hideShareModal = () => {
@ -464,7 +485,16 @@ const ViewEditMultisigCosigners = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<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 ? (
<ActivityIndicator />
) : (
@ -625,6 +655,12 @@ const styles = StyleSheet.create({
tipLabelText: {
fontWeight: '500',
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
});
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', () => {
const w = new MultisigHDWallet();
w.setSecret('garbage');
@ -1668,14 +1721,12 @@ describe('multisig-wallet (native segwit)', () => {
assert.strictEqual(w.getCosignerForFingerprint(fp2coldcard), process.env.MNEMONICS_COLDCARD);
assert.strictEqual(w.howManySignaturesCanWeMake(), 1);
w.replaceCosigner(fp2coldcard, Zpub2, fp2coldcard, path); // <-------------------
w.replaceCosignerSeedWithXpub(2);
assert.strictEqual(w.getCosigner(2), Zpub2);
assert.strictEqual(w.getFingerprint(2), fp2coldcard);
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.getFingerprint(2), fp2coldcard);
assert.strictEqual(w.getCustomDerivationPathForCosigner(2), path);
@ -1704,6 +1755,49 @@ describe('multisig-wallet (native segwit)', () => {
assert.ok(!w.getCustomDerivationPathForCosigner(2));
assert.strictEqual(w.getN(), 1);
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 () => {
@ -1829,6 +1923,42 @@ describe('multisig-wallet (native segwit)', () => {
assert.strictEqual(psbt.data.inputs.length, 2);
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', () => {