diff --git a/.circleci/config.yml b/.circleci/config.yml index cd3700fbb1..227fe17c63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: lint: docker: - - image: cimg/node:16.20.1 + - image: cimg/node:16.20.2 working_directory: ~/repo @@ -26,7 +26,7 @@ jobs: unit: docker: - - image: cimg/node:16.20.1 + - image: cimg/node:16.20.2 working_directory: ~/repo @@ -50,7 +50,7 @@ jobs: integration: docker: - - image: cimg/node:16.20.1 + - image: cimg/node:16.20.2 working_directory: ~/repo diff --git a/class/wallets/abstract-wallet.ts b/class/wallets/abstract-wallet.ts index 9c26def58b..a457debec8 100644 --- a/class/wallets/abstract-wallet.ts +++ b/class/wallets/abstract-wallet.ts @@ -301,6 +301,27 @@ export class AbstractWallet { } } + // is it output descriptor? + if (this.secret.startsWith('wpkh(') || this.secret.startsWith('pkh(') || this.secret.startsWith('sh(')) { + const xpubIndex = Math.max(this.secret.indexOf('xpub'), this.secret.indexOf('ypub'), this.secret.indexOf('zpub')); + const fpAndPath = this.secret.substring(this.secret.indexOf('(') + 1, xpubIndex); + const xpub = this.secret.substring(xpubIndex).replace(/\(|\)/, ''); + const pathIndex = fpAndPath.indexOf('/'); + const path = 'm' + fpAndPath.substring(pathIndex); + const fp = fpAndPath.substring(0, pathIndex); + + this._derivationPath = path; + const mfp = Buffer.from(fp, 'hex').reverse().toString('hex'); + this.masterFingerprint = parseInt(mfp, 16); + + if (this.secret.startsWith('wpkh(')) { + this.secret = this._xpubToZpub(xpub); + } else { + // nop + this.secret = xpub; + } + } + return this; } diff --git a/package.json b/package.json index 596e68f07c..5494d9772b 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "coinselect": "3.1.13", "crypto-js": "4.1.1", "dayjs": "1.11.9", - "detox": "20.9.1", + "detox": "20.11.3", "ecpairgrs": "2.0.1", "ecurve": "1.0.6", "electrum-client": "https://github.com/BlueWallet/rn-electrum-client#76c0ea35e1a50c47f3a7f818d529ebd100161496", @@ -149,7 +149,7 @@ "react-native-crypto": "2.2.0", "react-native-default-preference": "1.4.4", "react-native-device-info": "8.7.1", - "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#301c551970fd053d9f1b99a05e2236210ad9dcf8", + "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#857655cdddf17751c0fae1286a9121fda2a6d568", "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4", "react-native-elements": "3.4.3", "react-native-fingerprint-scanner": "https://github.com/BlueWallet/react-native-fingerprint-scanner#ce644673681716335d786727bab998f7e632ab5e", @@ -161,7 +161,7 @@ "react-native-image-picker": "4.8.5", "react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#v1.15.3", "react-native-keychain": "8.1.2", - "react-native-linear-gradient": "2.8.0", + "react-native-linear-gradient": "2.8.2", "react-native-localize": "3.0.2", "react-native-modal": "13.0.1", "react-native-navigation-bar-color": "https://github.com/BlueWallet/react-native-navigation-bar-color#3b2894ae62fbce99a3bd24105f0921cebaef5c94", @@ -179,7 +179,7 @@ "react-native-screens": "3.20.0", "react-native-secure-key-store": "https://github.com/BlueWallet/react-native-secure-key-store#2076b48", "react-native-share": "8.2.2", - "react-native-svg": "13.10.0", + "react-native-svg": "13.11.0", "react-native-tcp-socket": "5.6.2", "react-native-tor": "0.1.8", "react-native-vector-icons": "9.2.0", @@ -187,13 +187,13 @@ "react-native-webview": "12.4.0", "react-native-widget-center": "https://github.com/BlueWallet/react-native-widget-center#a128c38", "readable-stream": "3.6.2", - "realm": "11.10.1", + "realm": "11.10.2", "rn-ldk": "github:BlueWallet/rn-ldk#v0.8.4", "rn-nodeify": "10.3.0", "scryptsy": "2.1.0", "slip39": "https://github.com/BlueWallet/slip39-js", "stream-browserify": "3.0.0", - "url": "0.11.0", + "url": "0.11.1", "wifgrs": "2.0.6" }, "react-native": { diff --git a/tests/e2e/bluewallet3.spec.js b/tests/e2e/bluewallet3.spec.js index b13c1d317a..e4a7c27351 100644 --- a/tests/e2e/bluewallet3.spec.js +++ b/tests/e2e/bluewallet3.spec.js @@ -22,7 +22,8 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => { } await device.launchApp({ newInstance: true }); await helperImportWallet( - 'zpub6rDWXE4wbwefeCrHWehXJheXnti5F9PbpamDUeB5eFbqaY89x3jq86JADBuXpnJnSvRVwqkaTnyMaZERUg4BpxD9V4tSZfKeYh1ozPdL1xK', + // MNEMONICS_KEYSTONE + 'zpub6s2EvLxwvDpaHNVP5vfordTyi8cH1fR8usmEjz7RsSQjfTTGU2qA5VEcEyYYBxpZAyBarJoTraB4VRJKVz97Au9jRNYfLAeeHC5UnRZbz8Y', 'watchOnly', 'Imported Watch-only', '0.0001 GRS', @@ -54,10 +55,11 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => { await element(by.id('advancedOptionsMenuButton')).tap(); await element(by.id('ImportQrTransactionButton')).tap(); // opens camera + // produced by real Keystone device using MNEMONICS_KEYSTONE const unsignedPsbt = - 'ur:bytes/tzahqumzwnlszqzjqgqqqqqp6uu247pvcz6zld9p77ghlnl753q8fgygggzv9ugjxsmggyy5gqcqqqqqqqq0llllluqepssqqqqqqqqqzcqpfkxmzh6ud2yrvcl37uyy9yswr2z4mx276qqqqqqqqqgpragvxqqqqqqqqqqkqq2tgxjzwa0000egemyzygsv92j2zdwvg5ejypszwe3qctjvrwul6t2ts7yhk8e5takxwzey2z70kdnykwd43jsptrzps95d6cp4gqqqsqqqqqyqqqqqpqqqqqqqqpqqqqqqqqq0vr0lj'; + 'UR:CRYPTO-PSBT/HDRNJOJKIDJYZMADAEGOAOAEAEAEADLFIAYKFPTOTIHSMNDLJTLFTYPAHTFHZESOAODIBNADFDCPFZZEKSSTTOJYKPRLJOAEAEAEAEAEZMZMZMZMADNBDSAEAEAEAEAEAECFKOPTBBCFBGNTGUVAEHNDPECFUYNBHKRNPMCMJNYTBKROYKLOPSAEAEAEAEAEADADCTBEDIAEAEAEAEAEAECMAEBBFTZSECYTJZTEKGOEKECAVOGHMTVWGYIAMHCSKOSWCPAMAXENRDWMCPOTZMHKGMFPNTHLMNDMCETOHLOXTANDAMEOTSURLFHHPLTSDPCSJTWSGACSRPLEYNVEGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAEGETNJYFN'; const signedPsbt = - 'ur:bytes/tyqjuurnvf607qgq2gpqqqqqq8tn32hc9nqtgta558mezl70l6jyqa9q3ppqfsh3zg6rdpqsj3qrqqqqqqqqpllllllsryxzqqqqqqqqqqtqq9xcmv2lt34gsdnr78msss5jpcdg2hvetmgqqqqqqqqpqy04pscqqqqqqqqqzcqpfdq6gfm4aaal9r8vsg3zps42fgf4e3znxgszqfmxyrpwfsdmnlfdfwrcj7clx30kcecty3gte7ekvjeekkx2q9vvgjpsg5pzzqxjc9xv3rlhu2n6u87pm94agwcmvcywwsx9k0jpvwyng8crytgrkcpzqae6amp5xy03x2lsklv5zgnmeht0grzns27tmsjtsg2j0ne2969kqyqsxpqpqqqqqgsxqfmxyrpwfsdmnlfdfwrcj7clx30kcecty3gte7ekvjeekkx2q9vvgxqk3htqx4qqqzqqqqqqsqqqqqyqqqqqqqqyqqqqqqqqear8ke'; + 'UR:CRYPTO-PSBT/HDWTJOJKIDJYZMADAEGOAOAEAEAEADLFIAYKFPTOTIHSMNDLJTLFTYPAHTFHZESOAODIBNADFDCPFZZEKSSTTOJYKPRLJOAEAEAEAEAEZMZMZMZMADNBDSAEAEAEAEAEAECFKOPTBBCFBGNTGUVAEHNDPECFUYNBHKRNPMCMJNYTBKROYKLOPSAEAEAEAEAEADADCTBEDIAEAEAEAEAEAECMAEBBFTZSECYTJZTEKGOEKECAVOGHMTVWGYIAMHCSKOSWADAYJEAOFLDYFYAOCXGEUTDNBDTNMKTOQDLASKMTTSCLCSHPOLGDBEHDBBZMNERLRFSFIDLTMHTLMTLYWKAOCXFRBWHGOSGYRLYKTSSSSSIEWDZOVOSTFNISKTBYCLLRLRHSHFCMSGTTVDRHURNSOLADCLAXENRDWMCPOTZMHKGMFPNTHLMNDMCETOHLOXTANDAMEOTSURLFHHPLTSDPCSJTWSGAAEAEDLFPLTSW'; // tapping 5 times invisible button is a backdoor: for (let c = 0; c <= 5; c++) { diff --git a/tests/unit/watch-only-wallet.test.js b/tests/unit/watch-only-wallet.test.js index 1845565ac8..1538e5428e 100644 --- a/tests/unit/watch-only-wallet.test.js +++ b/tests/unit/watch-only-wallet.test.js @@ -334,6 +334,33 @@ describe('Watch only wallet', () => { assert.ok(!w.useWithHardwareWalletEnabled()); }); + it('can import wallet descriptor for BIP84 from Sparrow Wallet', async () => { + const payload = + 'UR:CRYPTO-OUTPUT/TAADMWTAADDLOLAOWKAXHDCLAXINTOCTFTNNIERONTNYGALYEMAAWPHDAXDIEOWPJEGHKPGMKERHIABDTBLUBNMUMWAAHDCXFHSNBGTSGWSWPTDWVTDIHYHNHPLBBSJEOLSNFZBDIYJLTTPFIMEYTEECKTGSBZBDAHTAADEHOEADAEAOAEAMTAADDYOTADLNCSGHYKAEYKAEYKAOCYFNLBCYGMAXAXAYCYSRRTSPGADLMKBGTD'; + + const decoder = new BlueURDecoder(); + decoder.receivePart(payload); + let data; + if (decoder.isComplete()) { + data = decoder.toString(); + } + + const w = new WatchOnlyWallet(); + w.setSecret(data); + w.init(); + assert.ok(w.valid()); + + assert.strictEqual(w.getMasterFingerprintHex(), '3c7f1a52'); + assert.strictEqual(w.getDerivationPath(), "m/84'/0'/0'"); + + assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1qr0y5c96xtfeulnzxnjl086f2njcmf8qmhenvpp'); + + assert.strictEqual( + w.getSecret(), + 'zpub6rkkMBH6dE8bUPM9MC3WTMYQ3pDYR1kHnNDrqEGY3FotR4EUifR1S4xd7ynwczREFCbfWyk5S4mhzPL8YuGsCSgey1AwH7fk4w9AULpyDYL', + ); + }); + it('can combine signed PSBT and prepare it for broadcast', async () => { const w = new WatchOnlyWallet(); w.setSecret('zpub6rjLjQVqVnj7crz9E4QWj4WgczmEseJq22u2B6k2HZr6NE2PQx3ZYg8BnbjN9kCfHymSeMd2EpwpM5iiz5Nrb3TzvddxW2RMcE3VXiWvk3Q');