diff --git a/class/wallets/abstract-hd-electrum-wallet.ts b/class/wallets/abstract-hd-electrum-wallet.ts index 9149436b5..d2d9b1a74 100644 --- a/class/wallets/abstract-hd-electrum-wallet.ts +++ b/class/wallets/abstract-hd-electrum-wallet.ts @@ -5,17 +5,17 @@ import b58 from 'bs58check'; import BIP32Factory, { BIP32Interface } from 'bip32'; import * as ecc from 'tiny-secp256k1'; import BIP47Factory, { BIP47Interface } from '@spsina/bip47'; - -import { randomBytes } from '../rng'; -import { AbstractHDWallet } from './abstract-hd-wallet'; import { ECPairFactory } from 'ecpair'; -import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types'; -import { ElectrumHistory } from '../../blue_modules/BlueElectrum'; -import type BlueElectrumNs from '../../blue_modules/BlueElectrum'; import { ECPairInterface } from 'ecpair/src/ecpair'; import { Psbt, Transaction as BTransaction } from 'bitcoinjs-lib'; import { CoinSelectReturnInput, CoinSelectTarget } from 'coinselect'; +import { randomBytes } from '../rng'; +import { AbstractHDWallet } from './abstract-hd-wallet'; +import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types'; +import { ElectrumHistory } from '../../blue_modules/BlueElectrum'; +import type BlueElectrumNs from '../../blue_modules/BlueElectrum'; + const ECPair = ECPairFactory(ecc); const bitcoin = require('bitcoinjs-lib'); const BlueElectrum: typeof BlueElectrumNs = require('../../blue_modules/BlueElectrum'); @@ -336,7 +336,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { const inpTxid = txdatas[txid].vin[inpNum].txid; const inpVout = txdatas[txid].vin[inpNum].vout; // got txid and output number of _previous_ transaction we shoud look into - if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { + if (vintxdatas[inpTxid]?.vout[inpVout]) { // extracting amount & addresses from previous output and adding it to _our_ input: txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; @@ -538,17 +538,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { for (const vin of tx.inputs) { // if input (spending) goes from our address - we are loosing! - if ( - (vin.address && ownedAddressesHashmap[vin.address]) || - (vin.addresses && vin.addresses[0] && ownedAddressesHashmap[vin.addresses[0]]) - ) { + if ((vin.address && ownedAddressesHashmap[vin.address]) || (vin.addresses?.[0] && ownedAddressesHashmap[vin.addresses[0]])) { tx.value -= new BigNumber(vin.value ?? 0).multipliedBy(100000000).toNumber(); } } for (const vout of tx.outputs) { // when output goes to our address - this means we are gaining! - if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses[0] && ownedAddressesHashmap[vout.scriptPubKey.addresses[0]]) { + if (vout.scriptPubKey.addresses?.[0] && ownedAddressesHashmap[vout.scriptPubKey.addresses[0]]) { tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); } } @@ -864,23 +861,19 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { // considering confirmed balance: for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { + if (this._balances_by_external_index?.[c]?.c > 0) { addressess.push(this._getExternalAddressByIndex(c)); } } for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { + if (this._balances_by_internal_index?.[c]?.c > 0) { addressess.push(this._getInternalAddressByIndex(c)); } } for (const pc of this._sender_payment_codes) { for (let c = 0; c < this._next_free_payment_code_address_index[pc] + this.gap_limit; c++) { - if ( - this._balances_by_payment_code_index[pc] && - this._balances_by_payment_code_index[pc].c && - this._balances_by_payment_code_index[pc].c > 0 - ) { + if (this._balances_by_payment_code_index?.[pc]?.c > 0) { addressess.push(this._getBIP47Address(pc, c)); } } @@ -888,23 +881,19 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { // considering UNconfirmed balance: for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - if (this._balances_by_external_index[c] && this._balances_by_external_index[c].u && this._balances_by_external_index[c].u > 0) { + if (this._balances_by_external_index?.[c]?.u > 0) { addressess.push(this._getExternalAddressByIndex(c)); } } for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].u && this._balances_by_internal_index[c].u > 0) { + if (this._balances_by_internal_index?.[c]?.u > 0) { addressess.push(this._getInternalAddressByIndex(c)); } } for (const pc of this._sender_payment_codes) { for (let c = 0; c < this._next_free_payment_code_address_index[pc] + this.gap_limit; c++) { - if ( - this._balances_by_payment_code_index[pc] && - this._balances_by_payment_code_index[pc].u && - this._balances_by_payment_code_index[pc].u > 0 - ) { + if (this._balances_by_payment_code_index?.[pc]?.u > 0) { addressess.push(this._getBIP47Address(pc, c)); } } @@ -1334,14 +1323,29 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { /** * Probes zero address in external hierarchy for transactions, if there are any returns TRUE. - * Zero address is a pretty good indicator, since its a first one to fund the wallet. How can you use the wallet and - * not fund it first? + * Zero address is a pretty good indicator, since its a first one to fund the wallet. + * Q: How can you use the wallet and not fund it first? + * A: You can if it is a BIP47 wallet! * * @returns {Promise} */ - async wasEverUsed() { - const txs = await BlueElectrum.getTransactionsByAddress(this._getExternalAddressByIndex(0)); - return txs.length > 0; + async wasEverUsed(): Promise { + const txs1 = await BlueElectrum.getTransactionsByAddress(this._getExternalAddressByIndex(0)); + if (txs1.length > 0) { + return true; + } + if (!this.allowBIP47()) { + return false; + } + // only check BIP47 if derivation path is regular, otherwise too many wallets will be found + if (!["m/84'/0'/0'", "m/44'/0'/0'", "m/49'/0'/0'"].includes(this.getDerivationPath() as string)) { + return false; + } + + const bip47_instance = this.getBIP47FromSeed(); + const address = bip47_instance.getNotificationAddress(); + const txs2 = await BlueElectrum.getTransactionsByAddress(address); + return txs2.length > 0; } /** @@ -1475,11 +1479,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return this._payment_code; } + getBIP47NotificationAddress(): string { + const bip47 = this.getBIP47FromSeed(); + return bip47.getNotificationAddress(); + } + async fetchBIP47SenderPaymentCodes(): Promise { const bip47_instance = this.getBIP47FromSeed(); - const address = bip47_instance.getNotificationAddress(); - const histories = await BlueElectrum.multiGetHistoryByAddress([address]); const txHashes = histories[address].map(({ tx_hash }) => tx_hash); diff --git a/class/wallets/hd-legacy-electrum-seed-p2pkh-wallet.js b/class/wallets/hd-legacy-electrum-seed-p2pkh-wallet.js index 0108de30d..3f8f69885 100644 --- a/class/wallets/hd-legacy-electrum-seed-p2pkh-wallet.js +++ b/class/wallets/hd-legacy-electrum-seed-p2pkh-wallet.js @@ -23,6 +23,10 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet { return mn.validateMnemonic(this.secret, PREFIX); } + allowBIP47() { + return false; + } + async generate() { throw new Error('Not implemented'); } diff --git a/class/wallets/hd-segwit-electrum-seed-p2wpkh-wallet.js b/class/wallets/hd-segwit-electrum-seed-p2wpkh-wallet.js index 91bd43b1c..804f012b7 100644 --- a/class/wallets/hd-segwit-electrum-seed-p2wpkh-wallet.js +++ b/class/wallets/hd-segwit-electrum-seed-p2wpkh-wallet.js @@ -24,6 +24,10 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet { return mn.validateMnemonic(this.secret, PREFIX); } + allowBIP47() { + return false; + } + async generate() { throw new Error('Not implemented'); } diff --git a/class/wallets/slip39-wallets.js b/class/wallets/slip39-wallets.js index 6c085db7d..a8672ec1a 100644 --- a/class/wallets/slip39-wallets.js +++ b/class/wallets/slip39-wallets.js @@ -67,6 +67,10 @@ export class SLIP39LegacyP2PKHWallet extends HDLegacyP2PKHWallet { static type = 'SLIP39legacyP2PKH'; static typeReadable = 'SLIP39 Legacy (P2PKH)'; + allowBIP47() { + return false; + } + _getSeed = SLIP39Mixin._getSeed; validateMnemonic = SLIP39Mixin.validateMnemonic; setSecret = SLIP39Mixin.setSecret; @@ -87,6 +91,10 @@ export class SLIP39SegwitBech32Wallet extends HDSegwitBech32Wallet { static type = 'SLIP39segwitBech32'; static typeReadable = 'SLIP39 SegWit (Bech32)'; + allowBIP47() { + return false; + } + _getSeed = SLIP39Mixin._getSeed; validateMnemonic = SLIP39Mixin.validateMnemonic; setSecret = SLIP39Mixin.setSecret; diff --git a/loc/en.json b/loc/en.json index 0ecdcd80d..37e3752ee 100644 --- a/loc/en.json +++ b/loc/en.json @@ -603,6 +603,7 @@ "payment_code": "Payment Code", "payment_codes_list": "Payment Codes List", "who_can_pay_me": "Who can pay me:", - "purpose": "Reusable and shareable code (BIP47)" + "purpose": "Reusable and shareable code (BIP47)", + "not_found": "Payment code not found" } } diff --git a/screen/wallets/paymentCode.tsx b/screen/wallets/paymentCode.tsx index 0a53386c5..9c0c5384a 100644 --- a/screen/wallets/paymentCode.tsx +++ b/screen/wallets/paymentCode.tsx @@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native'; import { NativeStackScreenProps } from 'react-native-screens/lib/typescript/native-stack'; import { BlueCopyTextToClipboard } from '../../BlueComponents'; import QRCodeComponent from '../../components/QRCodeComponent'; +import loc from '../../loc'; type PaymentCodeStackParamList = { PaymentCode: { paymentCode: string }; @@ -13,7 +14,7 @@ export default function PaymentCode({ route }: NativeStackScreenProps - {!paymentCode && Payment code not found} + {!paymentCode && {loc.bip47.not_found}} {paymentCode && ( <> diff --git a/tests/integration/import.test.js b/tests/integration/import.test.js index 71320b550..2fe26c164 100644 --- a/tests/integration/import.test.js +++ b/tests/integration/import.test.js @@ -455,4 +455,18 @@ describe('import procedure', () => { 'receive own flight sentence tide hood silent bunker derive manage wink belt loud apology monster pill raw gate hurdle match night wish toddler achieve', ); }); + + it('can import BIP47 wallet that only has notification transaction', async () => { + if (!process.env.BIP47_HD_MNEMONIC) { + console.error('process.env.BIP47_HD_MNEMONIC not set, skipped'); + return; + } + + const store = createStore('1'); + const { promise } = startImport(process.env.BIP47_HD_MNEMONIC.split(':')[0], true, false, ...store.callbacks); + await promise; + assert.strictEqual(store.state.wallets[0].type, HDLegacyP2PKHWallet.type); + assert.strictEqual(store.state.wallets[1].type, HDSegwitBech32Wallet.type); + assert.strictEqual(store.state.wallets.length, 2); + }); });