diff --git a/class/wallets/abstract-wallet.ts b/class/wallets/abstract-wallet.ts index f48a5bd45..ea4ac0b14 100644 --- a/class/wallets/abstract-wallet.ts +++ b/class/wallets/abstract-wallet.ts @@ -219,6 +219,7 @@ export class AbstractWallet { } setSecret(newSecret: string): this { + const origSecret = newSecret; this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', ''); if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase(); @@ -238,7 +239,7 @@ export class AbstractWallet { if (derivationPath.startsWith("m/84'/0'/") && this.secret.toLowerCase().startsWith('xpub')) { // need to convert xpub to zpub - this.secret = this._xpubToZpub(this.secret); + this.secret = this._xpubToZpub(this.secret.split('/')[0]); } if (derivationPath.startsWith("m/49'/0'/") && this.secret.toLowerCase().startsWith('xpub')) { @@ -288,6 +289,7 @@ export class AbstractWallet { ? parsedSecret.AccountKeyPath : `m/${parsedSecret.AccountKeyPath}`; if (parsedSecret.CoboVaultFirmwareVersion) this.use_with_hardware_wallet = true; + return this; } } catch (_) {} @@ -322,6 +324,31 @@ export class AbstractWallet { } } + // is it new-wasabi.json exported from coldcard? + try { + const json = JSON.parse(origSecret); + if (json.MasterFingerprint && json.ExtPubKey) { + // technically we should allow choosing which format user wants, BIP44 / BIP49 / BIP84, but meh... + this.secret = this._xpubToZpub(json.ExtPubKey); + const mfp = Buffer.from(json.MasterFingerprint, 'hex').reverse().toString('hex'); + this.masterFingerprint = parseInt(mfp, 16); + return this; + } + } catch (_) {} + + // is it sparrow-export ? + try { + const json = JSON.parse(origSecret); + if (json.chain && json.chain === 'BTC' && json.xfp && json.bip84) { + // technically we should allow choosing which format user wants, BIP44 / BIP49 / BIP84, but meh... + this.secret = json.bip84._pub; + const mfp = Buffer.from(json.xfp, 'hex').reverse().toString('hex'); + this.masterFingerprint = parseInt(mfp, 16); + this._derivationPath = json.bip84.deriv; + return this; + } + } catch (_) {} + return this; } diff --git a/tests/integration/import.test.js b/tests/integration/import.test.js index 8bd40ae01..8b72acf95 100644 --- a/tests/integration/import.test.js +++ b/tests/integration/import.test.js @@ -16,6 +16,7 @@ import { } from '../../class'; import startImport from '../../class/wallet-import'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +const fs = require('fs'); jest.setTimeout(90 * 1000); @@ -504,4 +505,55 @@ describe('import procedure', () => { assert.strictEqual(store.state.wallets[1].type, HDSegwitBech32Wallet.type); assert.strictEqual(store.state.wallets.length, 2); }); + + it('can import coldcard mk4 descriptor.txt', async () => { + const store = createStore(); + const { promise } = startImport( + fs.readFileSync('tests/unit/fixtures/coldcardmk4/descriptor.txt').toString('utf8'), + false, + false, + ...store.callbacks, + ); + await promise; + + assert.strictEqual(store.state.wallets.length, 1); + assert.strictEqual(store.state.wallets[0].type, WatchOnlyWallet.type); + assert.strictEqual(store.state.wallets[0].getMasterFingerprintHex(), '086ee178'); + assert.strictEqual(store.state.wallets[0].getDerivationPath(), "m/84'/0'/0'"); + assert.strictEqual(store.state.wallets[0]._getExternalAddressByIndex(0), 'bc1q5y4r767v5fzx74ez4nw36hjqrhr4ayeyut5px6'); + }); + + it('can import coldcard mk4 new-wasabi.json', async () => { + const store = createStore(); + const { promise } = startImport( + fs.readFileSync('tests/unit/fixtures/coldcardmk4/new-wasabi.json').toString('utf8'), + false, + false, + ...store.callbacks, + ); + await promise; + + assert.strictEqual(store.state.wallets.length, 1); + assert.strictEqual(store.state.wallets[0].type, WatchOnlyWallet.type); + assert.strictEqual(store.state.wallets[0].getMasterFingerprintHex(), '086ee178'); + assert.strictEqual(store.state.wallets[0].getDerivationPath(), "m/84'/0'/0'"); + assert.strictEqual(store.state.wallets[0]._getExternalAddressByIndex(0), 'bc1q5y4r767v5fzx74ez4nw36hjqrhr4ayeyut5px6'); + }); + + it('can import coldcard mk4 sparrow-export.json', async () => { + const store = createStore(); + const { promise } = startImport( + fs.readFileSync('tests/unit/fixtures/coldcardmk4/sparrow-export.json').toString('utf8'), + false, + false, + ...store.callbacks, + ); + await promise; + + assert.strictEqual(store.state.wallets.length, 1); + assert.strictEqual(store.state.wallets[0].type, WatchOnlyWallet.type); + assert.strictEqual(store.state.wallets[0].getMasterFingerprintHex(), '086ee178'); + assert.strictEqual(store.state.wallets[0].getDerivationPath(), "m/84'/0'/0'"); + assert.strictEqual(store.state.wallets[0]._getExternalAddressByIndex(0), 'bc1q5y4r767v5fzx74ez4nw36hjqrhr4ayeyut5px6'); + }); }); diff --git a/tests/unit/fixtures/coldcardmk4/descriptor.txt b/tests/unit/fixtures/coldcardmk4/descriptor.txt new file mode 100644 index 000000000..322066b9b --- /dev/null +++ b/tests/unit/fixtures/coldcardmk4/descriptor.txt @@ -0,0 +1 @@ +wpkh([086ee178/84h/0h/0h]xpub6CqWTnie1ut9ZDD9xDeCn1VXk83VdAPSm8ZPfPNbb8w5z1e7jyy8zuX721uKj8u4GNxXqAevgEZjciUansnyz6ZhnSKyQWZwx2dpAxuCuDe/<0;1>/*)#mthwej8w \ No newline at end of file diff --git a/tests/unit/fixtures/coldcardmk4/new-wasabi.json b/tests/unit/fixtures/coldcardmk4/new-wasabi.json new file mode 100644 index 000000000..7020366c2 --- /dev/null +++ b/tests/unit/fixtures/coldcardmk4/new-wasabi.json @@ -0,0 +1 @@ +{"ColdCardFirmwareVersion": "5.2.2", "MasterFingerprint": "086EE178", "ExtPubKey": "xpub6CqWTnie1ut9ZDD9xDeCn1VXk83VdAPSm8ZPfPNbb8w5z1e7jyy8zuX721uKj8u4GNxXqAevgEZjciUansnyz6ZhnSKyQWZwx2dpAxuCuDe"} \ No newline at end of file diff --git a/tests/unit/fixtures/coldcardmk4/sparrow-export.json b/tests/unit/fixtures/coldcardmk4/sparrow-export.json new file mode 100644 index 000000000..21d80a527 --- /dev/null +++ b/tests/unit/fixtures/coldcardmk4/sparrow-export.json @@ -0,0 +1 @@ +{"chain": "BTC", "xfp": "086EE178", "account": 0, "xpub": "xpub661MyMwAqRbcFA7Da9RWMKATMVYCGLC1JMGGPXs9cWHQnUJFFJ4Eo8PVwAcZP1EPa3rJNC192YA7ZLgN8BqETEK2mky8Co8V77aXB86sW9S", "bip44": {"name": "p2pkh", "xfp": "0949B877", "deriv": "m/44'/0'/0'", "xpub": "xpub6DNdnDgd2ULEUeRfpQhqJ8S9aqxZCWi9e4YijwPX1DsVfzPRPn2fk9jy72pn7rS2SAMWgfQK1JU3WsusseeConHH2LVjPEZkhxRBhFTdX69", "desc": "pkh([086ee178/44h/0h/0h]xpub6DNdnDgd2ULEUeRfpQhqJ8S9aqxZCWi9e4YijwPX1DsVfzPRPn2fk9jy72pn7rS2SAMWgfQK1JU3WsusseeConHH2LVjPEZkhxRBhFTdX69/<0;1>/*)#estefkna", "first": "1CuhYHhWe4awuUtgX5qREo5xYaoReXPLnZ"}, "bip49": {"name": "p2sh-p2wpkh", "xfp": "17BEC390", "deriv": "m/49'/0'/0'", "xpub": "xpub6CryNpp881kryGrBGvt2PB7gxCueY5bejof1qFcW1jMXPmcUByWeuhR2PAAhdTZe5viheDXkbvLY1jewwr7e1BMdBH1GiPnJU5A6JGHyV95", "desc": "sh(wpkh([086ee178/49h/0h/0h]xpub6CryNpp881kryGrBGvt2PB7gxCueY5bejof1qFcW1jMXPmcUByWeuhR2PAAhdTZe5viheDXkbvLY1jewwr7e1BMdBH1GiPnJU5A6JGHyV95/<0;1>/*))#76em9dr6", "_pub": "ypub6XhEgVV3GhJLpa3J7HfebGDC8B46Uhb9evBEceWPPjjQSsRhSdgDXm5AQN8HdNDZVZqWPh8K4ah5u2GWfYXeoR3E3chhJJbnjoDjgnzrviL", "first": "3DCeGZNxxhQDh5kAMi1EQQcpbBpo6pocHU"}, "bip84": {"name": "p2wpkh", "xfp": "7461A59E", "deriv": "m/84'/0'/0'", "xpub": "xpub6CqWTnie1ut9ZDD9xDeCn1VXk83VdAPSm8ZPfPNbb8w5z1e7jyy8zuX721uKj8u4GNxXqAevgEZjciUansnyz6ZhnSKyQWZwx2dpAxuCuDe", "desc": "wpkh([086ee178/84h/0h/0h]xpub6CqWTnie1ut9ZDD9xDeCn1VXk83VdAPSm8ZPfPNbb8w5z1e7jyy8zuX721uKj8u4GNxXqAevgEZjciUansnyz6ZhnSKyQWZwx2dpAxuCuDe/<0;1>/*)#mthwej8w", "_pub": "zpub6rW3584UKGy7FobPcwDTCBgY64LPWQNSbMbqEBANM9gr6DGaFJJGF2qP4RpVixCu5fC9L7r3bZGqPHhiEGd1aZvuX7ipaLCvVUm6xBzBhzQ", "first": "bc1q5y4r767v5fzx74ez4nw36hjqrhr4ayeyut5px6"}, "bip48_1": {"name": "p2sh-p2wsh", "xfp": "87843A8F", "deriv": "m/48'/0'/0'/1'", "xpub": "xpub6FAiR9GaPmCUdaQsWZuFEAMHiQnucQWs3681hXX3HnFDRXaksPSpFXD7Fw1216kng1uRCU8J5ULsfyoepFcu6foFEfFHQC8cFJUMLvy8gLd", "desc": "sh(wsh(sortedmulti(M,[086ee178/48'/0'/0'/1']xpub6FAiR9GaPmCUdaQsWZuFEAMHiQnucQWs3681hXX3HnFDRXaksPSpFXD7Fw1216kng1uRCU8J5ULsfyoepFcu6foFEfFHQC8cFJUMLvy8gLd/0/*,...)))", "_pub": "Ypub6ku4r3fw7QJKuSmNHb9rGKnbcAycmPBxGUHuQBgU3ZTW6oxttSzexhjB5qv5ZSdcK86CpXiyRM5vgS2yqBBs3PbWwU47PWR6QkosKWT9sZt"}, "bip48_2": {"name": "p2wsh", "xfp": "A57AB9B4", "deriv": "m/48'/0'/0'/2'", "xpub": "xpub6FAiR9GaPmCUfYtaQCR8Q8GH3bz5h9AQX8wu7up4MUdRdpY63R1KM9jJM6AqFiRpHn5dN15ewLxomv2UZ34aXsSB4b2SpgasgwPYBTwSJW9", "desc": "wsh(sortedmulti(M,[086ee178/48'/0'/0'/2']xpub6FAiR9GaPmCUfYtaQCR8Q8GH3bz5h9AQX8wu7up4MUdRdpY63R1KM9jJM6AqFiRpHn5dN15ewLxomv2UZ34aXsSB4b2SpgasgwPYBTwSJW9/0/*,...))", "_pub": "Zpub75jL9iLrG5qoniSC1aTMeNo67LKEnjpzfde1bxsNVGDbNCjTK8iigPuWCD3UoxxZLXPDjYGtjt4QfesNHf3ZGpv3djXhPugr87nhYb91mrY"}, "bip45": {"name": "p2sh", "xfp": "5D5A20AA", "deriv": "m/45'", "xpub": "xpub67x6MAer9yqzxozdhCAd1LNV2m8c34Vahi8AfNjRu8tndg3BvTb2FpotopkC1w98q29nUnGZ1eq6vWCjGMjx5m62Jk1L2koGDjnR69C1HcK", "desc": "sh(sortedmulti(M,[086ee178/45']xpub67x6MAer9yqzxozdhCAd1LNV2m8c34Vahi8AfNjRu8tndg3BvTb2FpotopkC1w98q29nUnGZ1eq6vWCjGMjx5m62Jk1L2koGDjnR69C1HcK/0/*,...))"}} \ No newline at end of file