diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6498f63c1..4ecd19304 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,7 @@ jobs:
- name: Run tests
run: npm test || npm test || npm test
env:
+ BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
diff --git a/Navigation.js b/Navigation.js
index e3a426319..cbe95d84b 100644
--- a/Navigation.js
+++ b/Navigation.js
@@ -83,6 +83,9 @@ import { isDesktop, isTablet, isHandset } from './blue_modules/environment';
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
import LdkViewLogs from './screen/wallets/ldkViewLogs';
+import PaymentCode from './screen/wallets/paymentCode';
+import PaymentCodesList from './screen/wallets/paymentCodesList';
+import loc from './loc';
const WalletsStack = createNativeStackNavigator();
@@ -465,6 +468,20 @@ const ExportMultisigCoordinationSetupRoot = () => {
);
};
+const PaymentCodeStack = createNativeStackNavigator();
+const PaymentCodeStackRoot = () => {
+ return (
+
+
+
+
+ );
+};
+
const RootStack = createNativeStackNavigator();
const NavigationDefaultOptions = { headerShown: false, stackPresentation: isDesktop ? 'containedModal' : 'modal' };
const Navigation = () => {
@@ -500,6 +517,8 @@ const Navigation = () => {
stackPresentation: isDesktop ? 'containedModal' : 'fullScreenModal',
}}
/>
+
+
);
};
diff --git a/blue_modules/storage-context.js b/blue_modules/storage-context.js
index b230a7184..700c1254c 100644
--- a/blue_modules/storage-context.js
+++ b/blue_modules/storage-context.js
@@ -120,6 +120,10 @@ export const BlueStorageProvider = ({ children }) => {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
await BlueElectrum.waitTillConnected();
+ const paymentCodesStart = Date.now();
+ await fetchSenderPaymentCodes(lastSnappedTo);
+ const paymentCodesEnd = Date.now();
+ console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
const balanceStart = +new Date();
await fetchWalletBalances(lastSnappedTo);
const balanceEnd = +new Date();
@@ -201,6 +205,7 @@ export const BlueStorageProvider = ({ children }) => {
const getTransactions = BlueApp.getTransactions;
const isAdvancedModeEnabled = BlueApp.isAdvancedModeEnabled;
+ const fetchSenderPaymentCodes = BlueApp.fetchSenderPaymentCodes;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
const getBalance = BlueApp.getBalance;
diff --git a/class/app-storage.js b/class/app-storage.js
index 08f1e29cc..7dcb7cb90 100644
--- a/class/app-storage.js
+++ b/class/app-storage.js
@@ -682,6 +682,7 @@ export class AppStorage {
}
} else {
for (const wallet of this.wallets) {
+ console.log('fetching balance for', wallet.getLabel());
await wallet.fetchBalance();
}
}
@@ -725,6 +726,27 @@ export class AppStorage {
}
};
+ fetchSenderPaymentCodes = async index => {
+ console.log('fetchSenderPaymentCodes for wallet#', typeof index === 'undefined' ? '(all)' : index);
+ if (index || index === 0) {
+ try {
+ if (!this.wallets[index].allowBIP47()) return;
+ await this.wallets[index].fetchBIP47SenderPaymentCodes();
+ } catch (error) {
+ console.error('Failed to fetch sender payment codes for wallet', index, error);
+ }
+ } else {
+ for (const wallet of this.wallets) {
+ try {
+ if (!wallet.allowBIP47()) continue;
+ await wallet.fetchBIP47SenderPaymentCodes();
+ } catch (error) {
+ console.error('Failed to fetch sender payment codes for wallet', wallet.label, error);
+ }
+ }
+ }
+ };
+
/**
*
* @returns {Array.}
diff --git a/class/wallets/abstract-hd-electrum-wallet.ts b/class/wallets/abstract-hd-electrum-wallet.ts
index 409782fd5..393b9c58e 100644
--- a/class/wallets/abstract-hd-electrum-wallet.ts
+++ b/class/wallets/abstract-hd-electrum-wallet.ts
@@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js';
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';
@@ -20,6 +21,7 @@ const bitcoin = require('bitcoinjs-lib');
const BlueElectrum: typeof BlueElectrumNs = require('../../blue_modules/BlueElectrum');
const reverse = require('buffer-reverse');
const bip32 = BIP32Factory(ecc);
+const bip47 = BIP47Factory(ecc);
type BalanceByIndex = {
c: number;
@@ -45,6 +47,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
_utxo: any[];
+ // BIP47
+ _enable_BIP47: boolean;
+ _payment_code: string;
+ _sender_payment_codes: string[];
+ _addresses_by_payment_code: Record;
+ _next_free_payment_code_address_index: Record;
+ _txs_by_payment_code_index: Record;
+ _balances_by_payment_code_index: Record;
+ _bip47_instance?: BIP47Interface;
+
constructor() {
super();
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
@@ -54,6 +66,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
this._txs_by_internal_index = {};
this._utxo = [];
+
+ // BIP47
+ this._enable_BIP47 = false;
+ this._payment_code = '';
+ this._sender_payment_codes = [];
+ this._next_free_payment_code_address_index = {};
+ this._txs_by_payment_code_index = {};
+ this._balances_by_payment_code_index = {};
+ this._addresses_by_payment_code = {};
}
/**
@@ -67,6 +88,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const bal of Object.values(this._balances_by_internal_index)) {
ret += bal.c;
}
+ for (const pc of this._sender_payment_codes) {
+ ret += this._getBalancesByPaymentCodeIndex(pc).c;
+ }
return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
}
@@ -82,6 +106,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const bal of Object.values(this._balances_by_internal_index)) {
ret += bal.u;
}
+ for (const pc of this._sender_payment_codes) {
+ ret += this._getBalancesByPaymentCodeIndex(pc).u;
+ }
return ret;
}
@@ -121,7 +148,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return child.toWIF();
}
- _getNodeAddressByIndex(node: number, index: number) {
+ _getNodeAddressByIndex(node: number, index: number): string {
index = index * 1; // cast to int
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
@@ -143,22 +170,20 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
this._node1 = hdNode.derive(node);
}
- let address;
+ let address: string;
if (node === 0) {
// @ts-ignore
- address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index));
- }
-
- if (node === 1) {
+ address = this._hdNodeToAddress(this._node0.derive(index));
+ } else {
+ // tbh the only possible else is node === 1
// @ts-ignore
- address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index));
+ address = this._hdNodeToAddress(this._node1.derive(index));
}
if (node === 0) {
return (this.external_addresses_cache[index] = address);
- }
-
- if (node === 1) {
+ } else {
+ // tbh the only possible else option is node === 1
return (this.internal_addresses_cache[index] = address);
}
}
@@ -189,7 +214,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
throw new Error('Internal error: this._node0 or this._node1 is undefined');
}
- _getExternalAddressByIndex(index: number) {
+ _getExternalAddressByIndex(index: number): string {
return this._getNodeAddressByIndex(0, index);
}
@@ -266,6 +291,21 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ // next, bip47 addresses
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ let hasUnconfirmed = false;
+ this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
+ this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
+ for (const tx of this._txs_by_payment_code_index[pc][c])
+ hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
+
+ if (hasUnconfirmed || this._txs_by_payment_code_index[pc][c].length === 0 || this._balances_by_payment_code_index[pc].u !== 0) {
+ addresses2fetch.push(this._getBIP47Address(pc, c));
+ }
+ }
+ }
+
// first: batch fetch for all addresses histories
const histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
const txs: Record = {};
@@ -312,6 +352,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c].filter(tx => !!tx.confirmations);
+ }
+ }
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
@@ -397,6 +442,51 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ for (const tx of Object.values(txdatas)) {
+ for (const vin of tx.vin) {
+ if (vin.addresses && vin.addresses.indexOf(this._getBIP47Address(pc, c)) !== -1) {
+ // this TX is related to our address
+ this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
+ this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
+ const { vin: txVin, vout: txVout, ...txRest } = tx;
+ const clonedTx = { ...txRest, inputs: txVin.slice(0), outputs: txVout.slice(0) };
+
+ // trying to replace tx if it exists already (because it has lower confirmations, for example)
+ let replaced = false;
+ for (let cc = 0; cc < this._txs_by_payment_code_index[pc][c].length; cc++) {
+ if (this._txs_by_payment_code_index[pc][c][cc].txid === clonedTx.txid) {
+ replaced = true;
+ this._txs_by_payment_code_index[pc][c][cc] = clonedTx;
+ }
+ }
+ if (!replaced) this._txs_by_payment_code_index[pc][c].push(clonedTx);
+ }
+ }
+ for (const vout of tx.vout) {
+ if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47Address(pc, c)) !== -1) {
+ // this TX is related to our address
+ this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
+ this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
+ const { vin: txVin, vout: txVout, ...txRest } = tx;
+ const clonedTx = { ...txRest, inputs: txVin.slice(0), outputs: txVout.slice(0) };
+
+ // trying to replace tx if it exists already (because it has lower confirmations, for example)
+ let replaced = false;
+ for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
+ if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
+ replaced = true;
+ this._txs_by_internal_index[c][cc] = clonedTx;
+ }
+ }
+ if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
+ }
+ }
+ }
+ }
+ }
+
this._lastTxFetch = +new Date();
}
@@ -409,6 +499,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const addressTxs of Object.values(this._txs_by_internal_index)) {
txs = txs.concat(addressTxs);
}
+ if (this._sender_payment_codes) {
+ for (const pc of this._sender_payment_codes) {
+ if (this._txs_by_payment_code_index[pc])
+ for (const addressTxs of Object.values(this._txs_by_payment_code_index[pc])) {
+ txs = txs.concat(addressTxs);
+ }
+ }
+ }
if (txs.length === 0) return []; // guard clause; so we wont spend time calculating addresses
@@ -421,6 +519,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
+ if (this._sender_payment_codes)
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
+ ownedAddressesHashmap[this._getBIP47Address(pc, c)] = true;
+ }
+ }
// hack: in case this code is called from LegacyWallet:
if (this.getAddress()) ownedAddressesHashmap[String(this.getAddress())] = true;
@@ -499,7 +603,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
) {
const address = this._getInternalAddressByIndex(c);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
- lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
+ lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unused
}
}
}
@@ -542,7 +646,50 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
) {
const address = this._getExternalAddressByIndex(c);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
- lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
+ lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unused
+ }
+ }
+ }
+
+ return lastUsedIndex;
+ }
+
+ async _binarySearchIterationForBIP47Address(paymentCode: string, index: number) {
+ const generateChunkAddresses = (chunkNum: number) => {
+ const ret = [];
+ for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
+ ret.push(this._getBIP47Address(paymentCode, c));
+ }
+ return ret;
+ };
+
+ let lastChunkWithUsedAddressesNum = null;
+ let lastHistoriesWithUsedAddresses = null;
+ for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
+ const histories = await BlueElectrum.multiGetHistoryByAddress(generateChunkAddresses(c));
+ // @ts-ignore
+ if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
+ // in this particular chunk we have used addresses
+ lastChunkWithUsedAddressesNum = c;
+ lastHistoriesWithUsedAddresses = histories;
+ } else {
+ // empty chunk. no sense searching more chunks
+ break;
+ }
+ }
+
+ let lastUsedIndex = 0;
+
+ if (lastHistoriesWithUsedAddresses) {
+ // now searching for last used address in batch lastChunkWithUsedAddressesNum
+ for (
+ let c = Number(lastChunkWithUsedAddressesNum) * this.gap_limit;
+ c < Number(lastChunkWithUsedAddressesNum) * this.gap_limit + this.gap_limit;
+ c++
+ ) {
+ const address = this._getBIP47Address(paymentCode, c);
+ if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
+ lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unused
}
}
}
@@ -556,6 +703,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// doing binary search for last used address:
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000);
+ if (this._sender_payment_codes) {
+ for (const pc of this._sender_payment_codes) {
+ this._next_free_payment_code_address_index[pc] = await this._binarySearchIterationForBIP47Address(pc, 1000);
+ }
+ }
} // end rescanning fresh wallet
// finally fetching balance
@@ -577,6 +729,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = this.next_free_change_address_index; c < this.next_free_change_address_index + this.gap_limit; c++) {
lagAddressesToFetch.push(this._getInternalAddressByIndex(c));
}
+ for (const pc of this._sender_payment_codes) {
+ for (
+ let c = this._next_free_payment_code_address_index[pc];
+ c < this._next_free_payment_code_address_index[pc] + this.gap_limit;
+ c++
+ ) {
+ lagAddressesToFetch.push(this._getBIP47Address(pc, c));
+ }
+ }
const txs = await BlueElectrum.multiGetHistoryByAddress(lagAddressesToFetch); // <------ electrum call
@@ -596,6 +757,20 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ for (const pc of this._sender_payment_codes) {
+ for (
+ let c = this._next_free_payment_code_address_index[pc];
+ c < this._next_free_payment_code_address_index[pc] + this.gap_limit;
+ c++
+ ) {
+ const address = this._getBIP47Address(pc, c);
+ if (txs[address] && Array.isArray(txs[address]) && txs[address].length > 0) {
+ // whoa, someone uses our wallet outside! better catch up
+ this._next_free_payment_code_address_index[pc] = c + 1;
+ }
+ }
+ }
+
// next, business as usuall. fetch balances
const addresses2fetch = [];
@@ -614,6 +789,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
addresses2fetch.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++) {
+ addresses2fetch.push(this._getBIP47Address(pc, c));
+ }
+ }
+
const balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch);
// converting to a more compact internal format
@@ -658,6 +839,22 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ for (const pc of this._sender_payment_codes) {
+ let confirmed = 0;
+ let unconfirmed = 0;
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ const addr = this._getBIP47Address(pc, c);
+ if (balances.addresses[addr].confirmed || balances.addresses[addr].unconfirmed) {
+ confirmed = confirmed + balances.addresses[addr].confirmed;
+ unconfirmed = unconfirmed + balances.addresses[addr].unconfirmed;
+ }
+ }
+ this._balances_by_payment_code_index[pc] = {
+ c: confirmed,
+ u: unconfirmed,
+ };
+ }
+
this._lastBalanceFetch = +new Date();
}
@@ -677,6 +874,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ 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
+ ) {
+ addressess.push(this._getBIP47Address(pc, c));
+ }
+ }
+ }
+
// 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) {
@@ -689,6 +898,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
+ 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
+ ) {
+ addressess.push(this._getBIP47Address(pc, c));
+ }
+ }
+ }
+
// note: we could remove checks `.c` and `.u` to simplify code, but the resulting `addressess` array would be bigger, thus bigger batch
// to fetch (or maybe even several fetches), which is not critical but undesirable.
// anyway, result has `.confirmations` property for each utxo, so outside caller can easily filter out unconfirmed if he wants to
@@ -756,6 +977,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
+ ownedAddressesHashmap[this._getBIP47Address(pc, c)] = true;
+ }
+ }
for (const tx of this.getTransactions()) {
for (const output of tx.outputs) {
@@ -827,6 +1053,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c);
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ if (this._getBIP47Address(pc, c) === address) return this._getBIP47PubkeyByIndex(pc, c);
+ }
+ }
return false;
}
@@ -844,6 +1075,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return this._getWIFByIndex(true, c);
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ if (this._getBIP47Address(pc, c) === address) return this._getBIP47WIF(pc, c);
+ }
+ }
return false;
}
@@ -861,6 +1097,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === cleanAddress) return true;
}
+ for (const pc of this._sender_payment_codes) {
+ for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
+ if (this._getBIP47Address(pc, c) === address) return true;
+ }
+ }
return false;
}
@@ -1052,22 +1293,29 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
/**
* Creates Segwit Bech32 Bitcoin address
- *
- * @param hdNode
- * @returns {String}
*/
- static _nodeToBech32SegwitAddress(hdNode: BIP32Interface) {
+ _nodeToBech32SegwitAddress(hdNode: BIP32Interface): string {
return bitcoin.payments.p2wpkh({
pubkey: hdNode.publicKey,
}).address;
}
- static _nodeToLegacyAddress(hdNode: BIP32Interface) {
+ _nodeToLegacyAddress(hdNode: BIP32Interface): string {
return bitcoin.payments.p2pkh({
pubkey: hdNode.publicKey,
}).address;
}
+ /**
+ * Creates Segwit P2SH Bitcoin address
+ */
+ _nodeToP2shSegwitAddress(hdNode: BIP32Interface): string {
+ const { address } = bitcoin.payments.p2sh({
+ redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }),
+ });
+ return address;
+ }
+
static _getTransactionsFromHistories(histories: Record) {
const txs = [];
for (const history of Object.values(histories)) {
@@ -1192,4 +1440,108 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
const seed = this._getSeed();
return AbstractHDElectrumWallet.seedToFingerprint(seed);
}
+
+ prepareForSerialization() {
+ super.prepareForSerialization();
+ delete this._bip47_instance;
+ }
+
+ /**
+ * Whether BIP47 is enabled. This is per-wallet setting that can be changed, NOT a feature-flag
+ * @returns boolean
+ */
+ isBIP47Enabled(): boolean {
+ return this._enable_BIP47;
+ }
+
+ switchBIP47(value: boolean): void {
+ this._enable_BIP47 = value;
+ }
+
+ getBIP47FromSeed(): BIP47Interface {
+ if (!this._bip47_instance) {
+ this._bip47_instance = bip47.fromBip39Seed(this.secret, undefined, this.passphrase);
+ }
+
+ return this._bip47_instance;
+ }
+
+ getBIP47PaymentCode(): string {
+ if (!this._payment_code) {
+ this._payment_code = this.getBIP47FromSeed().getSerializedPaymentCode();
+ }
+
+ return this._payment_code;
+ }
+
+ async fetchBIP47SenderPaymentCodes(): Promise {
+ const bip47_instance = BIP47Factory(ecc).fromBip39Seed(this.secret, undefined, this.passphrase);
+
+ const address = bip47_instance.getNotificationAddress();
+
+ const histories = await BlueElectrum.multiGetHistoryByAddress([address]);
+ const txHashes = histories[address].map(({ tx_hash }) => tx_hash);
+
+ const txHexs = await BlueElectrum.multiGetTransactionByTxid(txHashes, 50, false);
+ for (const txHex of Object.values(txHexs)) {
+ try {
+ const paymentCode = bip47_instance.getPaymentCodeFromRawNotificationTransaction(txHex);
+ if (this._sender_payment_codes.includes(paymentCode)) continue; // already have it
+ this._sender_payment_codes.push(paymentCode);
+ this._next_free_payment_code_address_index[paymentCode] = 0; // initialize
+ this._balances_by_payment_code_index[paymentCode] = { c: 0, u: 0 };
+ } catch (e) {
+ // do nothing
+ }
+ }
+ }
+
+ getBIP47SenderPaymentCodes(): string[] {
+ return this._sender_payment_codes;
+ }
+
+ _hdNodeToAddress(hdNode: BIP32Interface): string {
+ return this._nodeToBech32SegwitAddress(hdNode);
+ }
+
+ _getBIP47Address(paymentCode: string, index: number): string {
+ if (!this._addresses_by_payment_code[paymentCode]) this._addresses_by_payment_code[paymentCode] = [];
+
+ if (this._addresses_by_payment_code[paymentCode][index]) {
+ return this._addresses_by_payment_code[paymentCode][index];
+ }
+
+ const bip47_instance = this.getBIP47FromSeed();
+ const senderBIP47_instance = bip47.fromPaymentCode(paymentCode);
+ const remotePaymentNode = senderBIP47_instance.getPaymentCodeNode();
+ const hdNode = bip47_instance.getPaymentWallet(remotePaymentNode, index);
+ const address = this._hdNodeToAddress(hdNode);
+ this._address_to_wif_cache[address] = hdNode.toWIF();
+ this._addresses_by_payment_code[paymentCode][index] = address;
+ return address;
+ }
+
+ _getNextFreePaymentCodeAddress(paymentCode: string) {
+ return this._next_free_payment_code_address_index[paymentCode] || 0;
+ }
+
+ _getBalancesByPaymentCodeIndex(paymentCode: string): BalanceByIndex {
+ return this._balances_by_payment_code_index[paymentCode] || { c: 0, u: 0 };
+ }
+
+ _getBIP47WIF(paymentCode: string, index: number): string {
+ const bip47_instance = this.getBIP47FromSeed();
+ const senderBIP47_instance = bip47.fromPaymentCode(paymentCode);
+ const remotePaymentNode = senderBIP47_instance.getPaymentCodeNode();
+ const hdNode = bip47_instance.getPaymentWallet(remotePaymentNode, index);
+ return hdNode.toWIF();
+ }
+
+ _getBIP47PubkeyByIndex(paymentCode: string, index: number): Buffer {
+ const bip47_instance = this.getBIP47FromSeed();
+ const senderBIP47_instance = bip47.fromPaymentCode(paymentCode);
+ const remotePaymentNode = senderBIP47_instance.getPaymentCodeNode();
+ const hdNode = bip47_instance.getPaymentWallet(remotePaymentNode, index);
+ return hdNode.publicKey;
+ }
}
diff --git a/class/wallets/abstract-wallet.ts b/class/wallets/abstract-wallet.ts
index d86381bb6..a33684d73 100644
--- a/class/wallets/abstract-wallet.ts
+++ b/class/wallets/abstract-wallet.ts
@@ -143,6 +143,10 @@ export class AbstractWallet {
return BitcoinUnit.BTC;
}
+ allowBIP47(): boolean {
+ return false;
+ }
+
allowReceive(): boolean {
return true;
}
@@ -358,6 +362,10 @@ export class AbstractWallet {
return false;
}
+ isBIP47Enabled(): boolean {
+ return false;
+ }
+
async wasEverUsed(): Promise {
throw new Error('Not implemented');
}
diff --git a/class/wallets/hd-legacy-p2pkh-wallet.js b/class/wallets/hd-legacy-p2pkh-wallet.js
index c1a012d47..a85486ca0 100644
--- a/class/wallets/hd-legacy-p2pkh-wallet.js
+++ b/class/wallets/hd-legacy-p2pkh-wallet.js
@@ -34,6 +34,10 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
return true;
}
+ allowBIP47() {
+ return true;
+ }
+
getXpub() {
if (this._xpub) {
return this._xpub; // cache hit
@@ -48,44 +52,8 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
return this._xpub;
}
- _getNodeAddressByIndex(node, index) {
- index = index * 1; // cast to int
- if (node === 0) {
- if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
- }
-
- if (node === 1) {
- if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
- }
-
- if (node === 0 && !this._node0) {
- const xpub = this.getXpub();
- const hdNode = bip32.fromBase58(xpub);
- this._node0 = hdNode.derive(node);
- }
-
- if (node === 1 && !this._node1) {
- const xpub = this.getXpub();
- const hdNode = bip32.fromBase58(xpub);
- this._node1 = hdNode.derive(node);
- }
-
- let address;
- if (node === 0) {
- address = this.constructor._nodeToLegacyAddress(this._node0.derive(index));
- }
-
- if (node === 1) {
- address = this.constructor._nodeToLegacyAddress(this._node1.derive(index));
- }
-
- if (node === 0) {
- return (this.external_addresses_cache[index] = address);
- }
-
- if (node === 1) {
- return (this.internal_addresses_cache[index] = address);
- }
+ _hdNodeToAddress(hdNode) {
+ return this._nodeToLegacyAddress(hdNode);
}
async fetchUtxo() {
diff --git a/class/wallets/hd-segwit-bech32-wallet.js b/class/wallets/hd-segwit-bech32-wallet.js
index 82c6caaf0..e86d158ed 100644
--- a/class/wallets/hd-segwit-bech32-wallet.js
+++ b/class/wallets/hd-segwit-bech32-wallet.js
@@ -46,4 +46,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowXpub() {
return true;
}
+
+ allowBIP47() {
+ return true;
+ }
}
diff --git a/class/wallets/hd-segwit-p2sh-wallet.js b/class/wallets/hd-segwit-p2sh-wallet.js
index d5fff5f16..41da38c64 100644
--- a/class/wallets/hd-segwit-p2sh-wallet.js
+++ b/class/wallets/hd-segwit-p2sh-wallet.js
@@ -40,44 +40,8 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
return true;
}
- _getNodeAddressByIndex(node, index) {
- index = index * 1; // cast to int
- if (node === 0) {
- if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
- }
-
- if (node === 1) {
- if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
- }
-
- if (node === 0 && !this._node0) {
- const xpub = this.constructor._ypubToXpub(this.getXpub());
- const hdNode = bip32.fromBase58(xpub);
- this._node0 = hdNode.derive(0);
- }
-
- if (node === 1 && !this._node1) {
- const xpub = this.constructor._ypubToXpub(this.getXpub());
- const hdNode = bip32.fromBase58(xpub);
- this._node1 = hdNode.derive(1);
- }
-
- let address;
- if (node === 0) {
- address = this.constructor._nodeToP2shSegwitAddress(this._node0.derive(index));
- }
-
- if (node === 1) {
- address = this.constructor._nodeToP2shSegwitAddress(this._node1.derive(index));
- }
-
- if (node === 0) {
- return (this.external_addresses_cache[index] = address);
- }
-
- if (node === 1) {
- return (this.internal_addresses_cache[index] = address);
- }
+ _hdNodeToAddress(hdNode) {
+ return this._nodeToP2shSegwitAddress(hdNode);
}
/**
@@ -134,18 +98,6 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
return psbt;
}
- /**
- * Creates Segwit P2SH Bitcoin address
- * @param hdNode
- * @returns {String}
- */
- static _nodeToP2shSegwitAddress(hdNode) {
- const { address } = bitcoin.payments.p2sh({
- redeem: bitcoin.payments.p2wpkh({ pubkey: hdNode.publicKey }),
- });
- return address;
- }
-
isSegwit() {
return true;
}
diff --git a/class/wallets/watch-only-wallet.js b/class/wallets/watch-only-wallet.js
index 1ea4abac0..bf20dd3e9 100644
--- a/class/wallets/watch-only-wallet.js
+++ b/class/wallets/watch-only-wallet.js
@@ -100,6 +100,7 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) {
delete this._hdWalletInstance._node0;
delete this._hdWalletInstance._node1;
+ delete this._hdWalletInstance._bip47_instance;
}
}
diff --git a/components/TransactionsNavigationHeader.js b/components/TransactionsNavigationHeader.js
index 95e63c0b8..aabfb8ebd 100644
--- a/components/TransactionsNavigationHeader.js
+++ b/components/TransactionsNavigationHeader.js
@@ -16,7 +16,10 @@ export default class TransactionsNavigationHeader extends Component {
static propTypes = {
wallet: PropTypes.shape().isRequired,
onWalletUnitChange: PropTypes.func,
- navigation: PropTypes.shape(),
+ navigation: PropTypes.shape({
+ navigate: PropTypes.func,
+ goBack: PropTypes.func,
+ }),
onManageFundsPressed: PropTypes.func,
};
@@ -241,6 +244,21 @@ export default class TransactionsNavigationHeader extends Component {
{loc.lnd.title}
)}
+ {this.state.wallet.allowBIP47() && this.state.wallet.isBIP47Enabled() && (
+ {
+ this.props.navigation.navigate('PaymentCodeRoot', {
+ screen: 'PaymentCode',
+ params: { paymentCode: this.state.wallet.getBIP47PaymentCode() },
+ });
+ }}
+ >
+
+ {loc.bip47.payment_code}
+
+
+ )}
{this.state.wallet.type === LightningLdkWallet.type && (
diff --git a/loc/en.json b/loc/en.json
index 9ed1d9111..8b11d00a7 100644
--- a/loc/en.json
+++ b/loc/en.json
@@ -16,7 +16,7 @@
"seed": "Seed",
"success": "Success",
"wallet_key": "Wallet key",
- "invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment. Please try again.",
+ "invalid_animated_qr_code_fragment": "Invalid animated QRCode fragment. Please try again.",
"file_saved": "File {filePath} has been saved in your {destination}.",
"downloads_folder": "Downloads Folder"
},
@@ -43,13 +43,13 @@
"network": "Network Error"
},
"lnd": {
- "active":"Active",
- "inactive":"Inactive",
+ "active": "Active",
+ "inactive": "Inactive",
"channels": "Channels",
"no_channels": "No channels",
"claim_balance": "Claim balance {balance}",
"close_channel": "Close channel",
- "new_channel" : "New channel",
+ "new_channel": "New channel",
"errorInvoiceExpired": "Invoice expired",
"force_close_channel": "Force close channel?",
"expired": "Expired",
@@ -59,7 +59,7 @@
"placeholder": "Invoice",
"open_channel": "Open Channel",
"funding_amount_placeholder": "Funding amount, for example 0.001",
- "opening_channnel_for_from":"Opening channel for wallet {forWalletLabel}, by funding from {fromWalletLabel}",
+ "opening_channnel_for_from": "Opening channel for wallet {forWalletLabel}, by funding from {fromWalletLabel}",
"are_you_sure_open_channel": "Are you sure you want to open this channel?",
"potentialFee": "Potential Fee: {fee}",
"remote_host": "Remote host",
@@ -68,7 +68,7 @@
"refill_create": "In order to proceed, please create a Bitcoin wallet to refill with.",
"refill_external": "Refill with External Wallet",
"refill_lnd_balance": "Refill Lightning Wallet Balance",
- "sameWalletAsInvoiceError": "You can’t pay an invoice with the same wallet used to create it.",
+ "sameWalletAsInvoiceError": "You can't pay an invoice with the same wallet used to create it.",
"title": "Manage Funds",
"can_send": "Can Send",
"can_receive": "Can Receive",
@@ -287,7 +287,7 @@
"network_electrum": "Electrum Server",
"not_a_valid_uri": "Invalid URI",
"notifications": "Notifications",
- "open_link_in_explorer" : "Open link in explorer",
+ "open_link_in_explorer": "Open link in explorer",
"password": "Password",
"password_explain": "Create the password you will use to decrypt the storage.",
"passwords_do_not_match": "Passwords do not match.",
@@ -306,7 +306,7 @@
"selfTest": "Self-Test",
"save": "Save",
"saved": "Saved",
- "success_transaction_broadcasted" : "Success! Your transaction has been broadcasted!",
+ "success_transaction_broadcasted": "Success! Your transaction has been broadcasted!",
"total_balance": "Total Balance",
"total_balance_explanation": "Display the total balance of all your wallets on your home screen widgets.",
"widgets": "Widgets",
@@ -590,5 +590,11 @@
"auth_answer": "You have successfully authenticated at {hostname}!",
"could_not_auth": "We couldn’t authenticate you to {hostname}.",
"authenticate": "Authenticate"
+ },
+ "bip47": {
+ "payment_code": "Payment Code",
+ "payment_codes_list": "Payment Codes List",
+ "who_can_pay_me": "Who can pay me:",
+ "purpose": "Reusable and shareable code (BIP47)"
}
}
diff --git a/package-lock.json b/package-lock.json
index 8fd47cb94..9d49981ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@react-navigation/drawer": "5.12.9",
"@react-navigation/native": "5.9.8",
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
+ "@spsina/bip47": "1.0.1",
"aez": "1.0.1",
"assert": "2.0.0",
"base-x": "3.0.9",
@@ -5860,6 +5861,24 @@
"@sinonjs/commons": "^2.0.0"
}
},
+ "node_modules/@spsina/bip47": {
+ "version": "1.0.1",
+ "resolved": "git+ssh://git@github.com/abhishandy/bip47.git#1abcd4c20a387e43ed55bacc52726690bf417559",
+ "integrity": "sha512-lsgEpiEMDgpiYOA2kizOwiSS3vjTeLe2VnkOTIGnJ7Eu7Mkgl9dLES7oSLAjY64aQXr0VolqCRciRDc2nAC++w==",
+ "license": "MIT",
+ "dependencies": {
+ "bip32": "^3.0.1",
+ "bip39": "^3.0.4",
+ "bitcoinjs-lib": "^6.0.1",
+ "bs58check": "^2.1.1",
+ "create-hmac": "^1.1.7",
+ "ecpair": "^2.0.1",
+ "tiny-secp256k1": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -28459,6 +28478,20 @@
"@sinonjs/commons": "^2.0.0"
}
},
+ "@spsina/bip47": {
+ "version": "git+ssh://git@github.com/abhishandy/bip47.git#1abcd4c20a387e43ed55bacc52726690bf417559",
+ "integrity": "sha512-lsgEpiEMDgpiYOA2kizOwiSS3vjTeLe2VnkOTIGnJ7Eu7Mkgl9dLES7oSLAjY64aQXr0VolqCRciRDc2nAC++w==",
+ "from": "@spsina/bip47@1.0.1",
+ "requires": {
+ "bip32": "^3.0.1",
+ "bip39": "^3.0.4",
+ "bitcoinjs-lib": "^6.0.1",
+ "bs58check": "^2.1.1",
+ "create-hmac": "^1.1.7",
+ "ecpair": "^2.0.1",
+ "tiny-secp256k1": "^1.1.6"
+ }
+ },
"@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
diff --git a/package.json b/package.json
index e4ace72f8..a28aac789 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
"@react-navigation/drawer": "5.12.9",
"@react-navigation/native": "5.9.8",
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
+ "@spsina/bip47": "1.0.1",
"aez": "1.0.1",
"assert": "2.0.0",
"base-x": "3.0.9",
diff --git a/screen/wallets/details.js b/screen/wallets/details.js
index 18a50c227..c665aef08 100644
--- a/screen/wallets/details.js
+++ b/screen/wallets/details.js
@@ -128,6 +128,7 @@ const WalletDetails = () => {
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
const { isAdvancedModeEnabled } = useContext(BlueStorageContext);
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
+ const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled());
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
const { goBack, navigate, setOptions, popToTop } = useNavigation();
const { colors } = useTheme();
@@ -179,7 +180,7 @@ const WalletDetails = () => {
}
}, [wallet]);
- const setLabel = () => {
+ const save = () => {
setIsLoading(true);
if (walletName.trim().length > 0) {
wallet.setLabel(walletName.trim());
@@ -187,6 +188,7 @@ const WalletDetails = () => {
wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet);
}
wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList);
+ wallet.switchBIP47(isBIP47Enabled);
}
saveToDisk()
.then(() => {
@@ -209,14 +211,14 @@ const WalletDetails = () => {
testID="Save"
disabled={isLoading}
style={[styles.save, stylesHook.save]}
- onPress={setLabel}
+ onPress={save}
>
{loc.wallets.details_save}
),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isLoading, colors, walletName, useWithHardwareWallet, hideTransactionsInWalletsList]);
+ }, [isLoading, colors, walletName, useWithHardwareWallet, hideTransactionsInWalletsList, isBIP47Enabled]);
useEffect(() => {
if (wallets.some(wallet => wallet.getID() === walletID)) {
@@ -310,6 +312,14 @@ const WalletDetails = () => {
walletID: wallet.getID(),
});
+ const navigateToPaymentCodes = () =>
+ navigate('PaymentCodeRoot', {
+ screen: 'PaymentCodesList',
+ params: {
+ walletID: wallet.getID(),
+ },
+ });
+
const exportInternals = async () => {
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
setBackdoorPressed(0);
@@ -566,6 +576,16 @@ const WalletDetails = () => {
{wallet.getTransactions().length}
>
+ {wallet.allowBIP47() ? (
+ <>
+ {loc.bip47.payment_code}
+
+ {loc.bip47.purpose}
+
+
+ >
+ ) : null}
+
{wallet.type === WatchOnlyWallet.type && wallet.isHd() && (
<>
@@ -601,6 +621,7 @@ const WalletDetails = () => {
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (
)}
+ {wallet.allowBIP47() && isBIP47Enabled && }
diff --git a/screen/wallets/paymentCode.tsx b/screen/wallets/paymentCode.tsx
new file mode 100644
index 000000000..0a53386c5
--- /dev/null
+++ b/screen/wallets/paymentCode.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+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';
+
+type PaymentCodeStackParamList = {
+ PaymentCode: { paymentCode: string };
+};
+
+export default function PaymentCode({ route }: NativeStackScreenProps) {
+ const { paymentCode } = route.params;
+
+ return (
+
+ {!paymentCode && Payment code not found}
+ {paymentCode && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
diff --git a/screen/wallets/paymentCodesList.tsx b/screen/wallets/paymentCodesList.tsx
new file mode 100644
index 000000000..2e757c588
--- /dev/null
+++ b/screen/wallets/paymentCodesList.tsx
@@ -0,0 +1,67 @@
+import React, { useContext, useEffect, useState } from 'react';
+import { SectionList, StyleSheet, Text, View } from 'react-native';
+import { NativeStackScreenProps } from 'react-native-screens/lib/typescript/native-stack';
+import { BlueStorageContext } from '../../blue_modules/storage-context';
+import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
+import { BlueCopyTextToClipboard } from '../../BlueComponents';
+import loc from '../../loc';
+
+type PaymentCodesListStackParamList = {
+ PaymentCodesList: { walletID: string };
+};
+
+interface DataSection {
+ title: string;
+ data: string[];
+}
+
+export default function PaymentCodesList({ route }: NativeStackScreenProps) {
+ const { walletID } = route.params;
+ const { wallets } = useContext(BlueStorageContext);
+ const [data, setData] = useState([]);
+
+ useEffect(() => {
+ if (!walletID) return;
+
+ const foundWallet: AbstractHDElectrumWallet = wallets.find((w: AbstractHDElectrumWallet) => w.getID() === walletID);
+ if (!foundWallet) return;
+
+ const newData: DataSection[] = [
+ {
+ title: loc.bip47.who_can_pay_me,
+ data: foundWallet.getBIP47SenderPaymentCodes(),
+ },
+ ];
+ setData(newData);
+ }, [walletID, wallets]);
+
+ return (
+
+ {!walletID ? (
+ Internal error
+ ) : (
+
+ item + index}
+ renderItem={({ item }) => (
+
+
+
+ )}
+ renderSectionHeader={({ section: { title } }) => {title}}
+ />
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ titleText: { fontSize: 20 },
+});
diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js
index b107ad75f..846f005fa 100644
--- a/screen/wallets/transactions.js
+++ b/screen/wallets/transactions.js
@@ -33,6 +33,7 @@ import LNNodeBar from '../../components/LNNodeBar';
import TransactionsNavigationHeader from '../../components/TransactionsNavigationHeader';
import { TransactionListItem } from '../../components/TransactionListItem';
import alert from '../../components/Alert';
+import PropTypes from 'prop-types';
const fs = require('../../blue_modules/fs');
const BlueElectrum = require('../../blue_modules/BlueElectrum');
@@ -42,7 +43,7 @@ const buttonFontSize =
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
-const WalletTransactions = () => {
+const WalletTransactions = ({ navigation }) => {
const { wallets, saveToDisk, setSelectedWallet, walletTransactionUpdateStatus, isElectrumDisabled } = useContext(BlueStorageContext);
const [isLoading, setIsLoading] = useState(false);
const { walletID } = useRoute().params;
@@ -177,7 +178,12 @@ const WalletTransactions = () => {
refreshLnNodeInfo();
// await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
- /** @type {LegacyWallet} */
+ if (wallet.allowBIP47()) {
+ const pcStart = +new Date();
+ await wallet.fetchBIP47SenderPaymentCodes();
+ const pcEnd = +new Date();
+ console.log(wallet.getLabel(), 'fetch payment codes took', (pcEnd - pcStart) / 1000, 'sec');
+ }
const balanceStart = +new Date();
const oldBalance = wallet.getBalance();
await wallet.fetchBalance();
@@ -467,6 +473,7 @@ const WalletTransactions = () => {
InteractionManager.runAfterInteractions(async () => {
@@ -605,6 +612,10 @@ WalletTransactions.navigationOptions = navigationStyle({}, (options, { theme, na
};
});
+WalletTransactions.propTypes = {
+ navigation: PropTypes.shape(),
+};
+
const styles = StyleSheet.create({
flex: {
flex: 1,
diff --git a/scripts/find-unused-loc.js b/scripts/find-unused-loc.js
index 58d4c819c..2c3e4c837 100644
--- a/scripts/find-unused-loc.js
+++ b/scripts/find-unused-loc.js
@@ -3,7 +3,7 @@ const path = require('path');
const mainLocFile = './loc/en.json';
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class'];
-const addFiles = ['BlueComponents.js', 'App.js', 'BlueApp.js'];
+const addFiles = ['BlueComponents.js', 'App.js', 'BlueApp.js', 'Navigation.js'];
const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];
const allLocKeysHashmap = {}; // loc key -> used or not
diff --git a/tests/integration/bip47.test.ts b/tests/integration/bip47.test.ts
new file mode 100644
index 000000000..e320535ac
--- /dev/null
+++ b/tests/integration/bip47.test.ts
@@ -0,0 +1,139 @@
+// import assert from 'assert';
+import { ECPairFactory } from 'ecpair';
+
+import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet } from '../../class';
+import * as BlueElectrum from '../../blue_modules/BlueElectrum';
+import BIP47Factory from '@spsina/bip47';
+import assert from 'assert';
+
+const bitcoin = require('bitcoinjs-lib');
+const ecc = require('tiny-secp256k1');
+const ECPair = ECPairFactory(ecc);
+
+jest.setTimeout(30 * 1000);
+
+afterAll(async () => {
+ // after all tests we close socket so the test suite can actually terminate
+ BlueElectrum.forceDisconnect();
+});
+
+beforeAll(async () => {
+ // awaiting for Electrum to be connected. For RN Electrum would naturally connect
+ // while app starts up, but for tests we need to wait for it
+ await BlueElectrum.connectMain();
+});
+
+describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
+ it('should work', async () => {
+ const hd = new HDLegacyP2PKHWallet();
+ // @see https://gist.github.com/SamouraiDev/6aad669604c5930864bd
+ hd.setSecret('reward upper indicate eight swift arch injury crystal super wrestle already dentist');
+
+ expect(hd.getBIP47PaymentCode()).toEqual(
+ 'PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97',
+ );
+
+ expect(hd.allowBIP47()).toEqual(true);
+
+ await hd.fetchBIP47SenderPaymentCodes();
+ expect(hd.getBIP47SenderPaymentCodes().length).toBeGreaterThanOrEqual(3);
+ expect(hd.getBIP47SenderPaymentCodes()).toContain(
+ 'PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA',
+ );
+ expect(hd.getBIP47SenderPaymentCodes()).toContain(
+ 'PM8TJgndZSWCBPG5zCsqdXmCKLi7sP13jXuRp6b5X7G9geA3vRXQKAoXDf4Eym2RJB3vvcBdpDQT4vbo5QX7UfeV2ddjM8s79ERUTFS2ScKggSrciUsU',
+ );
+ expect(hd.getBIP47SenderPaymentCodes()).toContain(
+ 'PM8TJNiWKcyiA2MsWCfuAr9jvhA5qMEdEkjNypEnUbxMRa1D5ttQWdggQ7ib9VNFbRBSuw7i6RkqPSkCMR1XGPSikJHaCSfqWtsb1fn4WNAXjp5JVL5z',
+ );
+
+ await hd.fetchBalance();
+ await hd.fetchTransactions();
+ expect(hd.getTransactions().length).toBeGreaterThanOrEqual(4);
+ });
+
+ it('should work (samurai)', async () => {
+ if (!process.env.BIP47_HD_MNEMONIC) {
+ console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
+ return;
+ }
+
+ const w = new HDSegwitBech32Wallet();
+ w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[0]);
+ w.setPassphrase('1');
+
+ expect(w.getBIP47PaymentCode()).toEqual(
+ 'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
+ );
+
+ expect(w._getExternalAddressByIndex(0)).toEqual('bc1q07l355j4yd5kyut36vjxn2u60d3dknnpt39t6y');
+
+ const bip47 = BIP47Factory(ecc).fromBip39Seed(w.getSecret(), undefined, w.getPassphrase());
+ const ourNotificationAddress = bip47.getNotificationAddress();
+
+ const publicBip47 = BIP47Factory(ecc).fromPaymentCode(w.getBIP47PaymentCode());
+ expect(ourNotificationAddress).toEqual(publicBip47.getNotificationAddress());
+
+ expect(ourNotificationAddress).toEqual('1EiP2kSqxNqRhn8MPMkrtSEqaWiCWLYyTS'); // our notif address
+
+ await w.fetchBIP47SenderPaymentCodes();
+ assert.ok(
+ w
+ .getBIP47SenderPaymentCodes()
+ .includes('PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo'),
+ ); // sparrow payment code
+
+ assert.ok(w.weOwnAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe')); // this is an address that was derived (and paid) from counterparty payment code
+
+ const keyPair2 = ECPair.fromWIF(w._getWIFbyAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe') || '');
+ const address = bitcoin.payments.p2wpkh({
+ pubkey: keyPair2.publicKey,
+ }).address;
+ assert.strictEqual(address, 'bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe');
+
+ await w.fetchTransactions();
+
+ assert.ok(w.getTransactions().length >= 3);
+
+ assert.strictEqual(
+ w.getTransactions().find(tx => tx.txid === '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f')?.value,
+ 100000,
+ ); // initial deposit from sparrow after sparrow made a notification tx
+
+ assert.strictEqual(
+ w.getTransactions().find(tx => tx.txid === '06b4c14587182fd0474f265a77b156519b4778769a99c21623863a8194d0fa4f')?.value,
+ -22692,
+ ); // notification tx to sparrow so we can pay sparrow
+
+ assert.strictEqual(
+ w.getTransactions().find(tx => tx.txid === '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d')?.value,
+ -77308,
+ ); // paying to sparrow
+
+ // now, constructing OP_RETURN data to notify sparrow about us
+
+ const aliceBip47 = bip47;
+ const keyPair = ECPair.fromWIF(w._getWIFbyAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe') || '');
+ const bobBip47 = BIP47Factory(ecc).fromPaymentCode(
+ 'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
+ );
+ const blindedPaymentCode = aliceBip47.getBlindedPaymentCode(
+ bobBip47,
+ keyPair.privateKey as Buffer,
+ // txid is reversed, as well as output number ()
+ Buffer.from('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f', 'hex').reverse().toString('hex') + '01000000',
+ );
+
+ assert.strictEqual(
+ blindedPaymentCode,
+ '0100039da7642943ec5d16c9bce09b71f240fe246d891fa3b52a7d236fece98318e1ae972f3747672f7e79a23fc88c4dc91a8d014233e14a9e4417e132405b6a6c166d00000000000000000000000000',
+ );
+
+ // checking that this is exactly a data payload we have in an actual notification transaction we have sent:
+ assert.strictEqual(
+ w.getTransactions().find(tx => tx.txid === '06b4c14587182fd0474f265a77b156519b4778769a99c21623863a8194d0fa4f')?.outputs?.[0]
+ ?.scriptPubKey.hex,
+ '6a4c50' + blindedPaymentCode,
+ );
+ });
+});
diff --git a/tests/setup.js b/tests/setup.js
index b56bb046a..5f1e062b9 100644
--- a/tests/setup.js
+++ b/tests/setup.js
@@ -5,8 +5,9 @@ import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock
const consoleWarnOrig = console.warn;
console.warn = (...args) => {
if (
- args[0]?.startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
- args[0]?.startsWith('only compressed public keys are good')
+ typeof args[0] === 'string' &&
+ (args[0].startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
+ args[0].startsWith('only compressed public keys are good'))
) {
return;
}
@@ -16,10 +17,11 @@ console.warn = (...args) => {
const consoleLogOrig = console.log;
console.log = (...args) => {
if (
- args[0]?.startsWith('updating exchange rate') ||
- args[0]?.startsWith('begin connection') ||
- args[0]?.startsWith('TLS Connected to') ||
- args[0]?.startsWith('connected to')
+ typeof args[0] === 'string' &&
+ (args[0].startsWith('updating exchange rate') ||
+ args[0].startsWith('begin connection') ||
+ args[0].startsWith('TLS Connected to') ||
+ args[0].startsWith('connected to'))
) {
return;
}
diff --git a/tests/unit/bip47.test.ts b/tests/unit/bip47.test.ts
new file mode 100644
index 000000000..0da10894a
--- /dev/null
+++ b/tests/unit/bip47.test.ts
@@ -0,0 +1,96 @@
+import BIP47Factory from '@spsina/bip47';
+import ecc from 'tiny-secp256k1';
+import assert from 'assert';
+
+import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
+
+describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
+ it('should work', async () => {
+ const bobWallet = new HDSegwitBech32Wallet();
+ // @see https://gist.github.com/SamouraiDev/6aad669604c5930864bd
+ bobWallet.setSecret('reward upper indicate eight swift arch injury crystal super wrestle already dentist');
+
+ expect(bobWallet.getBIP47PaymentCode()).toEqual(
+ 'PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97',
+ );
+
+ const bip47 = BIP47Factory(ecc).fromBip39Seed(bobWallet.getSecret(), undefined, '');
+ const bobNotificationAddress = bip47.getNotificationAddress();
+
+ expect(bobNotificationAddress).toEqual('1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV'); // our notif address
+
+ assert.ok(!bobWallet.weOwnAddress('1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW')); // alice notif address, we dont own it
+ });
+
+ it('getters, setters, flags work', async () => {
+ const w = new HDSegwitBech32Wallet();
+ await w.generate();
+
+ expect(w.allowBIP47()).toEqual(true);
+
+ expect(w.isBIP47Enabled()).toEqual(false);
+ w.switchBIP47(true);
+ expect(w.isBIP47Enabled()).toEqual(true);
+ w.switchBIP47(false);
+ expect(w.isBIP47Enabled()).toEqual(false);
+
+ // checking that derived watch-only does not support that:
+ const ww = new WatchOnlyWallet();
+ ww.setSecret(w.getXpub());
+ expect(ww.allowBIP47()).toEqual(false);
+ });
+
+ it('should work (samurai)', async () => {
+ if (!process.env.BIP47_HD_MNEMONIC) {
+ console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
+ return;
+ }
+
+ const w = new HDSegwitBech32Wallet();
+ w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[0]);
+ w.setPassphrase('1');
+
+ expect(w.getBIP47PaymentCode()).toEqual(
+ 'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
+ );
+
+ expect(w._getExternalAddressByIndex(0)).toEqual('bc1q07l355j4yd5kyut36vjxn2u60d3dknnpt39t6y');
+
+ const bip47 = BIP47Factory(ecc).fromBip39Seed(w.getSecret(), undefined, w.getPassphrase());
+ const ourNotificationAddress = bip47.getNotificationAddress();
+
+ const publicBip47 = BIP47Factory(ecc).fromPaymentCode(w.getBIP47PaymentCode());
+ expect(ourNotificationAddress).toEqual(publicBip47.getNotificationAddress());
+
+ expect(ourNotificationAddress).toEqual('1EiP2kSqxNqRhn8MPMkrtSEqaWiCWLYyTS'); // our notif address
+
+ assert.ok(!w.weOwnAddress('1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW')); // alice notif address, we dont own it
+ });
+
+ it('should work (sparrow)', async () => {
+ if (!process.env.BIP47_HD_MNEMONIC) {
+ console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
+ return;
+ }
+
+ const w = new HDSegwitBech32Wallet();
+ w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
+
+ assert.strictEqual(
+ w.getXpub(),
+ 'zpub6r4KaQRsLuhHSGx8b9wGHh18UnawBs49jtiDzZYh9DSgKGwD72jWR3v54fkyy1UKVxt9HvCkYHmMAUe2YjKefofWzYp9YD62sUp6nNsEDMs',
+ );
+
+ expect(w.getBIP47PaymentCode()).toEqual(
+ 'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
+ );
+
+ const bip47 = BIP47Factory(ecc).fromBip39Seed(w.getSecret(), undefined, w.getPassphrase());
+ const ourNotificationAddress = bip47.getNotificationAddress();
+
+ const publicBip47 = BIP47Factory(ecc).fromPaymentCode(w.getBIP47PaymentCode());
+ expect(ourNotificationAddress).toEqual(publicBip47.getNotificationAddress());
+
+ expect(ourNotificationAddress).toEqual('16xPugarxLzuNdhDu6XCMJBsMYrTN2fghN'); // our notif address
+ });
+});