Merge branch 'feat/bip47' of github.com:abhiShandy/BlueWallet into abhiShandy-feat-bip47

This commit is contained in:
overtorment 2023-03-04 15:04:42 +00:00
commit 49619e4666
15 changed files with 595 additions and 16 deletions

View file

@ -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 }}

View file

@ -83,6 +83,8 @@ 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 loc from './loc';
const WalletsStack = createNativeStackNavigator();
@ -465,6 +467,15 @@ const ExportMultisigCoordinationSetupRoot = () => {
);
};
const PaymentCodeStack = createNativeStackNavigator();
const PaymentCodeStackRoot = () => {
return (
<PaymentCodeStack.Navigator name="PaymentCodeRoot" screenOptions={{ headerHideShadow: true }} initialRouteName="PaymentCode">
<PaymentCodeStack.Screen name="PaymentCode" component={PaymentCode} options={{ headerTitle: loc.bip47.payment_code }} />
</PaymentCodeStack.Navigator>
);
};
const RootStack = createNativeStackNavigator();
const NavigationDefaultOptions = { headerShown: false, stackPresentation: isDesktop ? 'containedModal' : 'modal' };
const Navigation = () => {
@ -500,6 +511,8 @@ const Navigation = () => {
stackPresentation: isDesktop ? 'containedModal' : 'fullScreenModal',
}}
/>
<RootStack.Screen name="PaymentCodeRoot" component={PaymentCodeStackRoot} options={NavigationDefaultOptions} />
</RootStack.Navigator>
);
};

View file

@ -1053,3 +1053,24 @@ function txhexToElectrumTransaction(txhex) {
// exported only to be used in unit tests
module.exports.txhexToElectrumTransaction = txhexToElectrumTransaction;
// module.exports.multiGetTransactionHexByTxid = async function (txids, batchsize = 100) {
// if (!mainClient) throw new Error('Electrum client is not connected');
// const chunks = splitIntoChunks(txids, batchsize);
// const result = {};
// for (const chunk of chunks) {
// if (!disableBatching) {
// await Promise.all(
// chunk.map(async txid => {
// const hex = await mainClient.blockchainTransaction_get(txid);
// result[txid] = hex;
// }),
// );
// } else {
// const res = await mainClient.blockchainTransaction_getBatch(chunk);
// res.forEach(({ result: r, param }) => (result[param] = r));
// }
// }
// return result;
// };

View file

@ -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 isAdancedModeEnabled = BlueApp.isAdancedModeEnabled;
const fetchSenderPaymentCodes = BlueApp.fetchSenderPaymentCodes;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
const getBalance = BlueApp.getBalance;

View file

@ -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,25 @@ export class AppStorage {
}
};
fetchSenderPaymentCodes = async index => {
console.log('fetchSenderPaymentCodes for wallet#', typeof index === 'undefined' ? '(all)' : index);
if (index || index === 0) {
try {
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 {
await wallet.fetchBIP47SenderPaymentCodes();
} catch (error) {
console.error('Failed to fetch sender payment codes for wallet', wallet.label, error);
}
}
}
};
/**
*
* @returns {Array.<AbstractWallet>}

View file

@ -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<string, string[]>;
_next_free_payment_code_address_index: Record<string, number>;
_txs_by_payment_code_index: Record<string, Transaction[][]>;
_balances_by_payment_code_index: Record<string, BalanceByIndex>;
_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,18 +106,23 @@ 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;
}
async generate() {
const buf = await randomBytes(16);
this.secret = bip39.entropyToMnemonic(buf.toString('hex'));
this.setBIP47PaymentCode();
}
async generateFromEntropy(user: Buffer) {
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
this.secret = bip39.entropyToMnemonic(buf.toString('hex'));
this.setBIP47PaymentCode();
}
_getExternalWIFByIndex(index: number): string | false {
@ -266,6 +295,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<string, ElectrumHistory> = {};
@ -312,6 +356,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 +446,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 +503,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 +523,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 +607,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 +650,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 +707,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, 10);
}
}
} // end rescanning fresh wallet
// finally fetching balance
@ -577,6 +733,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 +761,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 +793,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 +843,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 +878,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 +902,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 +981,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 +1057,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) + 1; c++) {
if (this._getBIP47Address(pc, c) === address) return this._getBIP47PubkeyByIndex(pc, c);
}
}
return false;
}
@ -844,6 +1079,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) + 1; c++) {
if (this._getBIP47Address(pc, c) === address) return this._getBIP47WIF(pc, c);
}
}
return false;
}
@ -861,6 +1101,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) + 1; c++) {
if (this._getBIP47Address(pc, c) === address) return true;
}
}
return false;
}
@ -1192,4 +1437,123 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
const seed = this._getSeed();
return AbstractHDElectrumWallet.seedToFingerprint(seed);
}
// Same as that in AbstractHDWallet, but also sets the BIP47 payment code
setSecret(newSecret: string): this {
this.secret = newSecret.trim().toLowerCase();
this.secret = this.secret.replace(/[^a-zA-Z0-9]/g, ' ').replace(/\s+/g, ' ');
// Try to match words to the default bip39 wordlist and complete partial words
const wordlist = bip39.wordlists[bip39.getDefaultWordlist()];
const lookupMap = wordlist.reduce((map, word) => {
const prefix3 = word.substr(0, 3);
const prefix4 = word.substr(0, 4);
map.set(prefix3, !map.has(prefix3) ? word : false);
map.set(prefix4, !map.has(prefix4) ? word : false);
return map;
}, new Map<string, string | false>());
this.secret = this.secret
.split(' ')
.map(word => lookupMap.get(word) || word)
.join(' ');
this.setBIP47PaymentCode();
return this;
}
/**
* Whether BIP47 is enabled
* @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;
}
setBIP47PaymentCode(): void {
this._payment_code = this.getBIP47FromSeed().getSerializedPaymentCode();
}
getBIP47PaymentCode(): string {
return this._payment_code;
}
async fetchBIP47SenderPaymentCodes(): Promise<void> {
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;
}
_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 = bip47_instance.getAddressFromNode(hdNode, bip47_instance.network);
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;
}
}

View file

@ -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 {
<Text style={styles.manageFundsButtonText}>{loc.lnd.title}</Text>
</ToolTipMenu>
)}
{this.state.wallet.isBIP47Enabled() && (
<TouchableOpacity
accessibilityRole="button"
onPress={() => {
this.props.navigation.navigate('PaymentCodeRoot', {
screen: 'PaymentCode',
params: { paymentCode: this.state.wallet.getBIP47PaymentCode() },
});
}}
>
<View style={styles.manageFundsButton}>
<Text style={styles.manageFundsButtonText}>{loc.bip47.payment_code}</Text>
</View>
</TouchableOpacity>
)}
{this.state.wallet.type === LightningLdkWallet.type && (
<TouchableOpacity accessibilityRole="button" onPress={this.manageFundsPressed}>
<View style={styles.manageFundsButton}>

View file

@ -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 cant 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,9 @@
"auth_answer": "You have successfully authenticated at {hostname}!",
"could_not_auth": "We couldnt authenticate you to {hostname}.",
"authenticate": "Authenticate"
},
"bip47": {
"payment_code": "Payment Code",
"purpose": "Reusable and shareable code (BIP47)"
}
}

33
package-lock.json generated
View file

@ -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",
@ -5774,6 +5775,24 @@
"@sinonjs/commons": "^1.7.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": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -31269,6 +31288,20 @@
"@sinonjs/commons": "^1.7.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": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",

View file

@ -107,6 +107,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",

View file

@ -5,9 +5,9 @@ import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BlueButton, BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import TransactionIncomingIcon from '../components/icons/TransactionIncomingIcon';
import TransactionOutgoingIcon from '../components/icons/TransactionOutgoingIcon';
import TransactionPendingIcon from '../components/icons/TransactionPendingIcon';
import TransactionIncomingIcon from '../../components/icons/TransactionIncomingIcon';
import TransactionOutgoingIcon from '../../components/icons/TransactionOutgoingIcon';
import TransactionPendingIcon from '../../components/icons/TransactionPendingIcon';
import navigationStyle from '../../components/navigationStyle';
import HandoffComponent from '../../components/handoff';
import { HDSegwitBech32Transaction } from '../../class';

View file

@ -128,6 +128,7 @@ const WalletDetails = () => {
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
const { isAdancedModeEnabled } = 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();
@ -187,6 +188,7 @@ const WalletDetails = () => {
wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet);
}
wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList);
wallet.switchBIP47(isBIP47Enabled);
}
saveToDisk()
.then(() => {
@ -216,7 +218,7 @@ const WalletDetails = () => {
),
});
// 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)) {
@ -566,6 +568,14 @@ const WalletDetails = () => {
<BlueText>{wallet.getTransactions().length}</BlueText>
</>
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.bip47.payment_code}</Text>
<View style={styles.hardware}>
<BlueText>{loc.bip47.purpose}</BlueText>
<Switch value={isBIP47Enabled} onValueChange={setIsBIP47Enabled} />
</View>
</>
<View>
{wallet.type === WatchOnlyWallet.type && wallet.isHd() && (
<>

View file

@ -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<PaymentCodeStackParamList, 'PaymentCode'>) {
const { paymentCode } = route.params;
return (
<View style={styles.container}>
{!paymentCode && <Text>Payment code not found</Text>}
{paymentCode && (
<>
<QRCodeComponent value={paymentCode} />
<BlueCopyTextToClipboard text={paymentCode} />
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -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;
@ -178,6 +179,7 @@ const WalletTransactions = () => {
// await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
/** @type {LegacyWallet} */
await wallet.fetchBIP47SenderPaymentCodes(); // FIXME:
const balanceStart = +new Date();
const oldBalance = wallet.getBalance();
await wallet.fetchBalance();
@ -467,6 +469,7 @@ const WalletTransactions = () => {
<View style={styles.flex}>
<StatusBar barStyle="light-content" backgroundColor={WalletGradient.headerColorFor(wallet.type)} animated />
<TransactionsNavigationHeader
navigation={navigation}
wallet={wallet}
onWalletUnitChange={passedWallet =>
InteractionManager.runAfterInteractions(async () => {
@ -605,6 +608,10 @@ WalletTransactions.navigationOptions = navigationStyle({}, (options, { theme, na
};
});
WalletTransactions.propTypes = {
navigation: PropTypes.shape(),
};
const styles = StyleSheet.create({
flex: {
flex: 1,

View file

@ -0,0 +1,49 @@
// import assert from 'assert';
import { HDSegwitBech32Wallet } from '../../class';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
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 () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const hd = new HDSegwitBech32Wallet();
hd.gap_limit = 1;
hd.setSecret(process.env.BIP47_HD_MNEMONIC);
expect(hd.getBIP47PaymentCode()).toEqual(
'PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97',
);
await hd.fetchBIP47SenderPaymentCodes();
expect(hd._sender_payment_codes.length).toBeGreaterThanOrEqual(3);
expect(hd._sender_payment_codes).toContain(
'PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA',
);
expect(hd._sender_payment_codes).toContain(
'PM8TJgndZSWCBPG5zCsqdXmCKLi7sP13jXuRp6b5X7G9geA3vRXQKAoXDf4Eym2RJB3vvcBdpDQT4vbo5QX7UfeV2ddjM8s79ERUTFS2ScKggSrciUsU',
);
expect(hd._sender_payment_codes).toContain(
'PM8TJNiWKcyiA2MsWCfuAr9jvhA5qMEdEkjNypEnUbxMRa1D5ttQWdggQ7ib9VNFbRBSuw7i6RkqPSkCMR1XGPSikJHaCSfqWtsb1fn4WNAXjp5JVL5z',
);
await hd.fetchTransactions();
expect(hd.getTransactions().length).toBeGreaterThanOrEqual(4);
});
});