From d17b746cdfdf58c819ca659702cce421fd79ee74 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Wed, 7 Oct 2020 19:54:07 +0100 Subject: [PATCH] FIX: import multisig from caravan & fullynoded --- class/wallets/multisig-hd-wallet.js | 66 +++++++++++++++++++++++++++ package.json | 3 ++ tests/unit/multisig-hd-wallet.test.js | 38 +++++++++++---- 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/class/wallets/multisig-hd-wallet.js b/class/wallets/multisig-hd-wallet.js index 705648833..d825c8d42 100644 --- a/class/wallets/multisig-hd-wallet.js +++ b/class/wallets/multisig-hd-wallet.js @@ -452,6 +452,72 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet { } } + // is it wallet descriptor? + // @see https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md + // @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md + if (secret.indexOf('sortedmulti(') !== -1 && json.descriptor) { + if (json.label) this.setLabel(json.label); + if (json.descriptor.startsWith('wsh(')) { + this.setNativeSegwit(); + } + if (json.descriptor.startsWith('sh(')) { + this.setLegacy(); + } + if (json.descriptor.startsWith('sh(wsh(')) { + this.setLegacy(); + } + + const s2 = json.descriptor.substr(json.descriptor.indexOf('sortedmulti(') + 12); + const s3 = s2.split(','); + const m = parseInt(s3[0]); + if (m) this.setM(m); + + for (let c = 1; c < s3.length; c++) { + const re = /\[([^\]]+)\](.*)/; + const m = s3[c].match(re); + if (m && m.length === 3) { + let hexFingerprint = m[1].split('/')[0]; + if (hexFingerprint.length === 8) { + hexFingerprint = Buffer.from(hexFingerprint, 'hex').reverse().toString('hex'); + } + + const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'"); + let xpub = m[2]; + if (xpub.indexOf('/') !== -1) { + xpub = xpub.substr(0, xpub.indexOf('/')); + } + + // console.warn('m[2] = ', m[2], {hexFingerprint, path, xpub}); + this.addCosigner(xpub, hexFingerprint.toUpperCase(), path); + } + } + } + + // is it caravan? + if (json && json.network === 'mainnet' && json.quorum) { + this.setM(+json.quorum.requiredSigners); + if (json.name) this.setLabel(json.name); + + switch (json.addressType.toLowerCase()) { + case 'P2SH': + this.setLegacy(); + break; + case 'P2SH-P2WSH': + this.setWrappedSegwit(); + break; + default: + case 'P2WSH': + this.setNativeSegwit(); + break; + } + + for (const pk of json.extendedPublicKeys) { + const path = this.constructor.isPathValid(json.bip32Path) ? json.bip32Path : "m/1'"; + // wtf, where caravan stores fingerprints..? + this.addCosigner(pk.xpub, '00000000', path); + } + } + if (!this.getLabel()) this.setLabel('Multisig vault'); } diff --git a/package.json b/package.json index 3b91fe27b..638a38778 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,9 @@ "setupFiles": [ "./tests/setup.js" ], + "watchPathIgnorePatterns": [ + "/node_modules" + ], "setupFilesAfterEnv": [ "./tests/setupAfterEnv.js" ] diff --git a/tests/unit/multisig-hd-wallet.test.js b/tests/unit/multisig-hd-wallet.test.js index de5e99e36..7ce5b64dc 100644 --- a/tests/unit/multisig-hd-wallet.test.js +++ b/tests/unit/multisig-hd-wallet.test.js @@ -597,6 +597,7 @@ describe('multisig-wallet (native segwit)', () => { assert.ok(MultisigHDWallet.isPathValid("m/45'")); assert.ok(MultisigHDWallet.isPathValid("m/48'/0'/0'/2'")); assert.ok(!MultisigHDWallet.isPathValid('ROFLBOATS')); + assert.ok(!MultisigHDWallet.isPathValid('')); }); it('basic operations work', async () => { @@ -1184,7 +1185,7 @@ describe('multisig-wallet (native segwit)', () => { assert.strictEqual(w.getN(), 0); }); - it.skip('can import from caravan', () => { + it('can import from caravan', () => { const json = JSON.stringify({ name: 'My Multisig Wallet', addressType: 'P2WSH', @@ -1268,17 +1269,15 @@ describe('multisig-wallet (native segwit)', () => { assert.strictEqual(w.getM(), 2); assert.strictEqual(w.getN(), 2); - // assert.strictEqual(w.getCosigner(1), Zpub1); - // assert.strictEqual(w.getCosigner(2), Zpub2); - assert.strictEqual(w.getFingerprint(1), fp1cobo); - assert.strictEqual(w.getFingerprint(2), fp2coldcard); + assert.strictEqual(w.getFingerprint(1), '00000000'); // should be fp1cobo, but stupid caravan doesnt store fp + assert.strictEqual(w.getFingerprint(2), '00000000'); // should be fp2coldcard, but stupid caravan doesnt store fp assert.strictEqual(w.howManySignaturesCanWeMake(), 0); assert.ok(!w.isWrappedSegwit()); assert.ok(w.isNativeSegwit()); assert.ok(!w.isLegacy()); }); - it.skip('can import from specter-desktop/fullynoded', () => { + it('can import from specter-desktop/fullynoded', () => { // @see https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Wallets/Wallet-Export-Spec.md const json = JSON.stringify({ label: 'Multisig', @@ -1288,12 +1287,35 @@ describe('multisig-wallet (native segwit)', () => { }); const w = new MultisigHDWallet(); w.setSecret(json); - assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1q338rmdygx0weah4pdrp9xyycxlv2t48276gk3gxmg6m7xdkkglsqgzm6mz'); - assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1qcgn73pjlwtt6krs2u6as0kh2jp486fa0t93yyq4d7xxxc37rf24qg67ewq'); assert.strictEqual(w.getM(), 2); assert.strictEqual(w.getN(), 3); + assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1q338rmdygx0weah4pdrp9xyycxlv2t48276gk3gxmg6m7xdkkglsqgzm6mz'); + assert.strictEqual(w._getInternalAddressByIndex(0), 'bc1qcgn73pjlwtt6krs2u6as0kh2jp486fa0t93yyq4d7xxxc37rf24qg67ewq'); + assert.strictEqual(w.getLabel(), 'Multisig'); assert.ok(!w.isWrappedSegwit()); assert.ok(w.isNativeSegwit()); assert.ok(!w.isLegacy()); + + assert.strictEqual(w.getFingerprint(1), '2D440411'); + assert.strictEqual(w.getFingerprint(2), 'F863CE8C'); + assert.strictEqual(w.getFingerprint(3), '7BBD27BF'); + + assert.strictEqual( + w.getCosigner(1), + 'xpub6ERaLLFZ3qu7X4cpiMAvSZ6UZVXJfxY5FoNvVJgai1V78DmeNHTcNVfu4cK2RmvTNXU4s1tFpGMPTwqoQ1RraE2o9iiNw2s2aHESpandSFY', + ); + assert.strictEqual( + w.getCosigner(2), + 'xpub6FCSLcRY99737oUAnvXd1k2gSz9P4zi4gQJ8UChSPSCxCK7XS9kLzoLHKNBiR26d3ivT7w3oka9f4BepVLoQ875XzgejjbDo626R6NBUJDW', + ); + assert.strictEqual( + w.getCosigner(3), + 'xpub6FE9uTPh1RxPRAfFVaET75vdfdQzXKZrT7LxukkqY4KhwUm4haMSPCwERfPouG6da6uZTRCXettvYFDck7nbw6JdBztGr1VBLonWch7NpJo', + ); + + assert.strictEqual(w.getCustomDerivationPathForCosigner(1), "m/48'/0'/0'/2'"); + assert.strictEqual(w.getCustomDerivationPathForCosigner(2), "m/48'/0'/0'/2'"); + assert.strictEqual(w.getCustomDerivationPathForCosigner(3), "m/48'/0'/0'/2'"); + assert.strictEqual(w.getDerivationPath(), ''); }); });