mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 15:20:55 +01:00
Merge pull request #4350 from BlueWallet/limpbrains-multisig-passphrase
ADD: Multisig seed with passphrase
This commit is contained in:
commit
309c5b10e1
6 changed files with 504 additions and 145 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,22 +863,22 @@ 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)) {
|
||||
// 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);
|
||||
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
|
||||
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner);
|
||||
}
|
||||
|
||||
const hdRoot = bip32.fromSeed(seed);
|
||||
psbt.signInputHD(cc, hdRoot);
|
||||
signaturesMade++;
|
||||
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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
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,39 +1016,50 @@ 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);
|
||||
try {
|
||||
psbt.signInputHD(cc, hdRoot);
|
||||
} catch (_) {} // protects agains duplicate cosignings
|
||||
if (MultisigHDWallet.isXpubString(cosigner)) continue;
|
||||
|
||||
if (!psbt.inputHasHDKey(cc, hdRoot)) {
|
||||
// failed signing as HD. probably bitcoinjs-lib could not match provided hdRoot's
|
||||
// fingerprint (or path?) to the ones in psbt, which is the case of stupid Electrum desktop which can
|
||||
// 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 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];
|
||||
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);
|
||||
}
|
||||
|
||||
const path = this.getCustomDerivationPathForCosigner(cosignerIndex + 1) + `/${internal ? 1 : 0}/${index}`;
|
||||
// ^^^ we assume that counterparty has Zpub for specified derivation path
|
||||
const child = root.derivePath(path);
|
||||
if (psbt.inputHasPubkey(cc, child.publicKey)) {
|
||||
const keyPair = ECPair.fromPrivateKey(child.privateKey);
|
||||
try {
|
||||
psbt.signInput(cc, keyPair);
|
||||
} catch (_) {}
|
||||
}
|
||||
try {
|
||||
psbt.signInputHD(cc, hdRoot);
|
||||
} catch (_) {} // protects agains duplicate cosignings
|
||||
|
||||
if (!psbt.inputHasHDKey(cc, hdRoot)) {
|
||||
// failed signing as HD. probably bitcoinjs-lib could not match provided hdRoot's
|
||||
// fingerprint (or path?) to the ones in psbt, which is the case of stupid Electrum desktop which can
|
||||
// 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 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) {
|
||||
throw new Error('Fingerprint of new seed doesnt match');
|
||||
}
|
||||
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]);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
setWallet(wallet);
|
||||
setIsSaveButtonDisabled(false);
|
||||
resolve();
|
||||
wallet.replaceCosignerSeedWithXpub(index);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
console.log(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();
|
||||
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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
Loading…
Add table
Reference in a new issue