Skip to content

Commit 3623d00

Browse files
committed
FIX: hw wallets with taproot integration
1 parent 5beff0f commit 3623d00

File tree

5 files changed

+143
-35
lines changed

5 files changed

+143
-35
lines changed

class/wallets/hd-taproot-wallet.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,22 @@ export class HDTaprootWallet extends AbstractHDElectrumWallet {
6262
index = index * 1; // cast to int
6363

6464
if (node === 0 && !this._node0) {
65-
const hdNode = bip32.fromBase58(this.getXpub());
65+
let xpub = this.getXpub();
66+
if (xpub.startsWith('zpub')) {
67+
// bip32.fromBase58() wont work with zpub prefix, need to swap it for the traditional one
68+
xpub = this._zpubToXpub(xpub);
69+
}
70+
const hdNode = bip32.fromBase58(xpub);
6671
this._node0 = hdNode.derive(node);
6772
}
6873

6974
if (node === 1 && !this._node1) {
70-
const hdNode = bip32.fromBase58(this.getXpub());
75+
let xpub = this.getXpub();
76+
if (xpub.startsWith('zpub')) {
77+
// bip32.fromBase58() wont work with zpub prefix, need to swap it for the traditional one
78+
xpub = this._zpubToXpub(xpub);
79+
}
80+
const hdNode = bip32.fromBase58(xpub);
7181
this._node1 = hdNode.derive(node);
7282
}
7383

class/wallets/watch-only-wallet.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,12 @@ export class WatchOnlyWallet extends LegacyWallet {
7575
*/
7676
init() {
7777
let hdWalletInstance: THDWalletForWatchOnly;
78-
if (this.secret.startsWith('xpub')) {
79-
// its either legacy OR taproot HD since industry decided to not add new prefixes (like ypub or zpub)
80-
if (this._derivationPath?.startsWith("m/86'")) {
81-
hdWalletInstance = new HDTaprootWallet();
82-
} else {
83-
hdWalletInstance = new HDLegacyP2PKHWallet();
84-
}
78+
79+
if (this._derivationPath?.startsWith("m/86'")) {
80+
// if path is explicit taproot path - its definately BIP86
81+
hdWalletInstance = new HDTaprootWallet();
82+
} else if (this.secret.startsWith('xpub')) {
83+
hdWalletInstance = new HDLegacyP2PKHWallet();
8584
} else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet();
8685
else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet();
8786
else return this;
@@ -251,7 +250,7 @@ export class WatchOnlyWallet extends LegacyWallet {
251250
}
252251

253252
allowMasterFingerprint() {
254-
return this.getSecret().startsWith('zpub');
253+
return this.getSecret().startsWith('zpub') || this.getSecret().startsWith('ypub') || this.getSecret().startsWith('xpub');
255254
}
256255

257256
useWithHardwareWalletEnabled() {

screen/send/SendDetails.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ const SendDetails = () => {
564564
} catch (Err: any) {
565565
setIsLoading(false);
566566
presentAlert({ title: loc.errors.error, message: Err.message });
567+
console.log(Err);
567568
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
568569
}
569570
};
@@ -864,6 +865,7 @@ const SendDetails = () => {
864865
psbt = bitcoin.Psbt.fromBase64(psbtBase64);
865866
tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx;
866867
} catch (e: any) {
868+
console.log(e);
867869
presentAlert({ title: loc.errors.error, message: e.message });
868870
return;
869871
} finally {

tests/e2e/bluewallet3.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
4545
await element(by.text(`No, and do not ask me again.`)).tap(); // sometimes the first click doesnt work (detox issue, not app's)
4646
} catch (_) {}
4747
await expect(element(by.id('BitcoinAddressQRCodeContainer'))).toBeVisible();
48-
await expect(element(by.text('bc1qc8wun6lf9vcajpddtgdpd2pdrp0kwp29j6upgv'))).toBeVisible();
48+
await expect(element(by.text('bc1qgrhr5xc5774maph97d73ydrjlqqmg2v6jjlr29'))).toBeVisible();
4949
await element(by.id('SetCustomAmountButton')).tap();
5050
await element(by.id('BitcoinAmountInput')).replaceText('1');
5151
await element(by.id('CustomAmountDescription')).typeText('Test');
@@ -56,7 +56,7 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
5656

5757
await expect(element(by.id('BitcoinAddressQRCodeContainer'))).toBeVisible();
5858

59-
await expect(element(by.text('bitcoin:BC1QC8WUN6LF9VCAJPDDTGDPD2PDRP0KWP29J6UPGV?amount=1&label=Test'))).toBeVisible();
59+
await expect(element(by.text('bitcoin:BC1QGRHR5XC5774MAPH97D73YDRJLQQMG2V6JJLR29?amount=1&label=Test'))).toBeVisible();
6060
await device.pressBack();
6161
await element(by.id('SendButton')).tap();
6262
await element(by.text('OK')).tap();

tests/unit/watch-only-wallet.test.js

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,38 @@ describe('Watch only wallet', () => {
6464
});
6565

6666
it('can create PSBT base64 without signature for HW wallet xpub', async () => {
67-
const w = new WatchOnlyWallet();
68-
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
69-
w.init();
70-
const changeAddress = '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX';
71-
// hardcoding so we wont have to call w.getChangeAddressAsync()
72-
const utxos = [
73-
{
74-
height: 530926,
75-
value: 1000,
76-
address: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG',
77-
txid: 'd0432027a86119c63a0be8fa453275c2333b59067f1e559389cd3e0e377c8b96',
78-
vout: 1,
79-
txhex:
80-
'0100000001b630ac364a04b83548994ded4705b98316b2d1fe18b9fffa2627be9eef11bf60000000006b48304502210096e68d94d374e3a688ed2e6605289f81172540abaab5f6cc431c231919860746022075ee4e64c867ed9d369d01a9b35d8b1689a821be8d729fff7fb3dfcc75d16f6401210281d2e40ba6422fc97b61fd5643bee83dd749d8369339edc795d7b3f00e96c681fdffffff02ef020000000000001976a914e4271ef9e9a03a89b981c73d3d6936d2f6fccc0688ace8030000000000001976a914120ad7854152901ebeb269acb6cef20e71b3cf5988acea190800',
81-
},
82-
];
83-
// hardcoding utxo so we wont have to call w.fetchUtxo() and w.getUtxo()
84-
85-
const { psbt } = await w.createTransaction(utxos, [{ address: '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5' }], 1, changeAddress);
67+
for (const cleanupInternals of [false, true]) {
68+
const w = new WatchOnlyWallet();
69+
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps');
70+
w.init();
71+
const changeAddress = '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX';
72+
// hardcoding so we wont have to call w.getChangeAddressAsync()
73+
const utxos = [
74+
{
75+
height: 530926,
76+
value: 1000,
77+
address: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG',
78+
txid: 'd0432027a86119c63a0be8fa453275c2333b59067f1e559389cd3e0e377c8b96',
79+
vout: 1,
80+
txhex:
81+
'0100000001b630ac364a04b83548994ded4705b98316b2d1fe18b9fffa2627be9eef11bf60000000006b48304502210096e68d94d374e3a688ed2e6605289f81172540abaab5f6cc431c231919860746022075ee4e64c867ed9d369d01a9b35d8b1689a821be8d729fff7fb3dfcc75d16f6401210281d2e40ba6422fc97b61fd5643bee83dd749d8369339edc795d7b3f00e96c681fdffffff02ef020000000000001976a914e4271ef9e9a03a89b981c73d3d6936d2f6fccc0688ace8030000000000001976a914120ad7854152901ebeb269acb6cef20e71b3cf5988acea190800',
82+
},
83+
];
84+
// hardcoding utxo so we wont have to call w.fetchUtxo() and w.getUtxo()
85+
86+
const { psbt } = await w.createTransaction(utxos, [{ address: '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5' }], 1, changeAddress);
87+
88+
if (cleanupInternals) {
89+
// these might be purged when preparing for serialization before saving to disk
90+
w._hdWalletInstance._node0 = undefined;
91+
w._hdWalletInstance._node1 = undefined;
92+
}
8693

87-
assert.strictEqual(
88-
psbt.toBase64(),
89-
'cHNidP8BAFUCAAAAAZaLfDcOPs2Jk1UefwZZOzPCdTJF+ugLOsYZYagnIEPQAQAAAAAAAACAASgDAAAAAAAAGXapFP6ZRvxlaU5S/9HQFr1i2lsgp58AiKwAAAAAAAEA4gEAAAABtjCsNkoEuDVImU3tRwW5gxay0f4Yuf/6Jie+nu8Rv2AAAAAAa0gwRQIhAJbmjZTTdOOmiO0uZgUon4EXJUCrqrX2zEMcIxkZhgdGAiB17k5kyGftnTadAamzXYsWiaghvo1yn/9/s9/MddFvZAEhAoHS5AumQi/Je2H9VkO+6D3XSdg2kzntx5XXs/AOlsaB/f///wLvAgAAAAAAABl2qRTkJx756aA6ibmBxz09aTbS9vzMBois6AMAAAAAAAAZdqkUEgrXhUFSkB6+smmsts7yDnGzz1mIrOoZCAAiBgPGm5BfckKzaIEi8GlRM5oe4A2mUvbsxlJ+pmMhRsrOYhgAAAAALAAAgAAAAIAAAACAAAAAAAAAAAAAAA==',
90-
);
94+
assert.strictEqual(
95+
psbt.toBase64(),
96+
'cHNidP8BAFUCAAAAAZaLfDcOPs2Jk1UefwZZOzPCdTJF+ugLOsYZYagnIEPQAQAAAAAAAACAASgDAAAAAAAAGXapFP6ZRvxlaU5S/9HQFr1i2lsgp58AiKwAAAAAAAEA4gEAAAABtjCsNkoEuDVImU3tRwW5gxay0f4Yuf/6Jie+nu8Rv2AAAAAAa0gwRQIhAJbmjZTTdOOmiO0uZgUon4EXJUCrqrX2zEMcIxkZhgdGAiB17k5kyGftnTadAamzXYsWiaghvo1yn/9/s9/MddFvZAEhAoHS5AumQi/Je2H9VkO+6D3XSdg2kzntx5XXs/AOlsaB/f///wLvAgAAAAAAABl2qRTkJx756aA6ibmBxz09aTbS9vzMBois6AMAAAAAAAAZdqkUEgrXhUFSkB6+smmsts7yDnGzz1mIrOoZCAAiBgPGm5BfckKzaIEi8GlRM5oe4A2mUvbsxlJ+pmMhRsrOYhgAAAAALAAAgAAAAIAAAACAAAAAAAAAAAAAAA==',
97+
);
98+
}
9199
});
92100

93101
it('can create PSBT base64 without signature for HW wallet ypub', async () => {
@@ -303,6 +311,29 @@ describe('Watch only wallet', () => {
303311
assert.ok(w.useWithHardwareWalletEnabled());
304312
});
305313

314+
it('can import taproot BIP86 from keystone with zpub instead of xpub', async () => {
315+
const w = new WatchOnlyWallet();
316+
w.setSecret(
317+
JSON.stringify({
318+
ExtPubKey: 'zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4',
319+
MasterFingerprint: 'B68AF6E4',
320+
AccountKeyPath: "m/86'/0'/0'",
321+
}),
322+
);
323+
w.init();
324+
assert.ok(w.valid());
325+
assert.strictEqual(
326+
w.getSecret(),
327+
'zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4',
328+
);
329+
assert.strictEqual(w.getMasterFingerprintHex(), 'B68AF6E4'.toLowerCase());
330+
assert.strictEqual(w.getLabel(), 'Wallet');
331+
assert.strictEqual(w.getDerivationPath(), "m/86'/0'/0'");
332+
assert.ok(w._getExternalAddressByIndex(0).startsWith('bc1p'), `not taproot address generated: ${w._getExternalAddressByIndex(0)}`);
333+
assert.ok(w.allowMasterFingerprint());
334+
// assert.ok(w.useWithHardwareWalletEnabled());
335+
});
336+
306337
it('can import zpub with master fingerprint and derivation path', async () => {
307338
const w = new WatchOnlyWallet();
308339
w.setSecret(require('fs').readFileSync('./tests/unit/fixtures/skeleton-walletdescriptor.txt', 'ascii'));
@@ -389,6 +420,72 @@ describe('Watch only wallet', () => {
389420
);
390421

391422
assert.ok(w._getExternalAddressByIndex(0).startsWith('bc1p'), 'not taproot address, got: ' + w._getExternalAddressByIndex(0));
423+
assert.ok(w.allowMasterFingerprint());
424+
assert.ok(!w.useWithHardwareWalletEnabled());
425+
}
426+
});
427+
428+
it('can import BIP86 (taproot) wallet descriptor but with zpub instead of xpub', async () => {
429+
const w = new WatchOnlyWallet();
430+
w.setSecret(
431+
"tr([b68af6e4/86'/0'/0']zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4)",
432+
);
433+
w.init();
434+
assert.ok(w.valid());
435+
436+
assert.strictEqual(w.getMasterFingerprintHex(), 'b68af6e4');
437+
assert.strictEqual(w.getDerivationPath(), "m/86'/0'/0'");
438+
439+
assert.strictEqual(
440+
w.getSecret(),
441+
'zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4',
442+
);
443+
444+
assert.ok(w._getExternalAddressByIndex(0).startsWith('bc1p'), 'not taproot address, got: ' + w._getExternalAddressByIndex(0));
445+
446+
assert.ok(!w.useWithHardwareWalletEnabled());
447+
});
448+
449+
it('can import BIP86 (taproot) wallet descriptor and create transaction', async () => {
450+
for (const cleanupInternals of [false, true]) {
451+
const w = new WatchOnlyWallet();
452+
// MNEMONICS_KEYSTONE
453+
w.setSecret(
454+
"tr([b68af6e4/86'/0'/0']zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4)",
455+
);
456+
w.init();
457+
assert.ok(w.valid());
458+
459+
assert.strictEqual(w.getMasterFingerprintHex(), 'b68af6e4');
460+
assert.strictEqual(w.getDerivationPath(), "m/86'/0'/0'");
461+
462+
assert.strictEqual(
463+
w.getSecret(),
464+
'zpub6rxQT4vrGrdLmFicJZnLxx1odj1C8xNtHW5pW84hMSXdtoFnCbqBFJm3bF5PrwYL5ScxFhdzRuv3pb9beyoraQLMuQWkV9faGuxstBPgLw4',
465+
);
466+
467+
assert.ok(w._getExternalAddressByIndex(0).startsWith('bc1p'), 'not taproot address, got: ' + w._getExternalAddressByIndex(0));
468+
469+
const utxos = [
470+
{
471+
height: 923789,
472+
value: 10108,
473+
address: 'bc1pyren45uwytsghuxelahgyjflrx9dhq9zhavangrcmw2avfre6spqtwxgm4',
474+
txid: 'dd8a90cfef8b5966781cfaddf8a5e8f1e2dce12e7ceed25c6d329c1df2e17c4f',
475+
vout: 0,
476+
wif: false,
477+
confirmations: 7,
478+
},
479+
];
480+
481+
if (cleanupInternals) {
482+
// these might be purged when preparing for serialization before saving to disk
483+
w._hdWalletInstance._node0 = undefined;
484+
w._hdWalletInstance._node1 = undefined;
485+
}
486+
487+
const { psbt } = w.createTransaction(utxos, [{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS' }], 1, w._getInternalAddressByIndex(0));
488+
assert.ok(psbt);
392489

393490
assert.ok(!w.useWithHardwareWalletEnabled());
394491
}

0 commit comments

Comments
 (0)