mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 07:15:35 +01:00
Merge pull request #5373 from BlueWallet/abhiShandy-feat-bip47
Abhi shandy feat bip47
This commit is contained in:
commit
accbb4db87
22 changed files with 886 additions and 127 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -39,6 +39,7 @@ jobs:
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test || npm test || npm test
|
run: npm test || npm test || npm test
|
||||||
env:
|
env:
|
||||||
|
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
|
||||||
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
|
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
|
||||||
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
|
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
|
||||||
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
||||||
|
|
|
@ -83,6 +83,9 @@ import { isDesktop, isTablet, isHandset } from './blue_modules/environment';
|
||||||
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
|
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
|
||||||
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
|
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
|
||||||
import LdkViewLogs from './screen/wallets/ldkViewLogs';
|
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();
|
const WalletsStack = createNativeStackNavigator();
|
||||||
|
|
||||||
|
@ -465,6 +468,20 @@ 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.Screen
|
||||||
|
name="PaymentCodesList"
|
||||||
|
component={PaymentCodesList}
|
||||||
|
options={{ headerTitle: loc.bip47.payment_codes_list }}
|
||||||
|
/>
|
||||||
|
</PaymentCodeStack.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const RootStack = createNativeStackNavigator();
|
const RootStack = createNativeStackNavigator();
|
||||||
const NavigationDefaultOptions = { headerShown: false, stackPresentation: isDesktop ? 'containedModal' : 'modal' };
|
const NavigationDefaultOptions = { headerShown: false, stackPresentation: isDesktop ? 'containedModal' : 'modal' };
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
|
@ -500,6 +517,8 @@ const Navigation = () => {
|
||||||
stackPresentation: isDesktop ? 'containedModal' : 'fullScreenModal',
|
stackPresentation: isDesktop ? 'containedModal' : 'fullScreenModal',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RootStack.Screen name="PaymentCodeRoot" component={PaymentCodeStackRoot} options={NavigationDefaultOptions} />
|
||||||
</RootStack.Navigator>
|
</RootStack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,6 +120,10 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
||||||
}
|
}
|
||||||
await BlueElectrum.waitTillConnected();
|
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();
|
const balanceStart = +new Date();
|
||||||
await fetchWalletBalances(lastSnappedTo);
|
await fetchWalletBalances(lastSnappedTo);
|
||||||
const balanceEnd = +new Date();
|
const balanceEnd = +new Date();
|
||||||
|
@ -201,6 +205,7 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
const getTransactions = BlueApp.getTransactions;
|
const getTransactions = BlueApp.getTransactions;
|
||||||
const isAdvancedModeEnabled = BlueApp.isAdvancedModeEnabled;
|
const isAdvancedModeEnabled = BlueApp.isAdvancedModeEnabled;
|
||||||
|
|
||||||
|
const fetchSenderPaymentCodes = BlueApp.fetchSenderPaymentCodes;
|
||||||
const fetchWalletBalances = BlueApp.fetchWalletBalances;
|
const fetchWalletBalances = BlueApp.fetchWalletBalances;
|
||||||
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
|
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
|
||||||
const getBalance = BlueApp.getBalance;
|
const getBalance = BlueApp.getBalance;
|
||||||
|
|
|
@ -682,6 +682,7 @@ export class AppStorage {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const wallet of this.wallets) {
|
for (const wallet of this.wallets) {
|
||||||
|
console.log('fetching balance for', wallet.getLabel());
|
||||||
await wallet.fetchBalance();
|
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.<AbstractWallet>}
|
* @returns {Array.<AbstractWallet>}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import BigNumber from 'bignumber.js';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
import BIP32Factory, { BIP32Interface } from 'bip32';
|
import BIP32Factory, { BIP32Interface } from 'bip32';
|
||||||
import * as ecc from 'tiny-secp256k1';
|
import * as ecc from 'tiny-secp256k1';
|
||||||
|
import BIP47Factory, { BIP47Interface } from '@spsina/bip47';
|
||||||
|
|
||||||
import { randomBytes } from '../rng';
|
import { randomBytes } from '../rng';
|
||||||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
import { AbstractHDWallet } from './abstract-hd-wallet';
|
||||||
|
@ -20,6 +21,7 @@ const bitcoin = require('bitcoinjs-lib');
|
||||||
const BlueElectrum: typeof BlueElectrumNs = require('../../blue_modules/BlueElectrum');
|
const BlueElectrum: typeof BlueElectrumNs = require('../../blue_modules/BlueElectrum');
|
||||||
const reverse = require('buffer-reverse');
|
const reverse = require('buffer-reverse');
|
||||||
const bip32 = BIP32Factory(ecc);
|
const bip32 = BIP32Factory(ecc);
|
||||||
|
const bip47 = BIP47Factory(ecc);
|
||||||
|
|
||||||
type BalanceByIndex = {
|
type BalanceByIndex = {
|
||||||
c: number;
|
c: number;
|
||||||
|
@ -45,6 +47,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
|
|
||||||
_utxo: any[];
|
_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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
|
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._txs_by_internal_index = {};
|
||||||
|
|
||||||
this._utxo = [];
|
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)) {
|
for (const bal of Object.values(this._balances_by_internal_index)) {
|
||||||
ret += bal.c;
|
ret += bal.c;
|
||||||
}
|
}
|
||||||
|
for (const pc of this._sender_payment_codes) {
|
||||||
|
ret += this._getBalancesByPaymentCodeIndex(pc).c;
|
||||||
|
}
|
||||||
return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
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)) {
|
for (const bal of Object.values(this._balances_by_internal_index)) {
|
||||||
ret += bal.u;
|
ret += bal.u;
|
||||||
}
|
}
|
||||||
|
for (const pc of this._sender_payment_codes) {
|
||||||
|
ret += this._getBalancesByPaymentCodeIndex(pc).u;
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +148,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
return child.toWIF();
|
return child.toWIF();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNodeAddressByIndex(node: number, index: number) {
|
_getNodeAddressByIndex(node: number, index: number): string {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
if (node === 0) {
|
if (node === 0) {
|
||||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
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);
|
this._node1 = hdNode.derive(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
let address;
|
let address: string;
|
||||||
if (node === 0) {
|
if (node === 0) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index));
|
address = this._hdNodeToAddress(this._node0.derive(index));
|
||||||
}
|
} else {
|
||||||
|
// tbh the only possible else is node === 1
|
||||||
if (node === 1) {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index));
|
address = this._hdNodeToAddress(this._node1.derive(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === 0) {
|
if (node === 0) {
|
||||||
return (this.external_addresses_cache[index] = address);
|
return (this.external_addresses_cache[index] = address);
|
||||||
}
|
} else {
|
||||||
|
// tbh the only possible else option is node === 1
|
||||||
if (node === 1) {
|
|
||||||
return (this.internal_addresses_cache[index] = address);
|
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');
|
throw new Error('Internal error: this._node0 or this._node1 is undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
_getExternalAddressByIndex(index: number) {
|
_getExternalAddressByIndex(index: number): string {
|
||||||
return this._getNodeAddressByIndex(0, index);
|
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
|
// first: batch fetch for all addresses histories
|
||||||
const histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
|
const histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch);
|
||||||
const txs: Record<string, ElectrumHistory> = {};
|
const txs: Record<string, ElectrumHistory> = {};
|
||||||
|
@ -312,6 +352,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
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);
|
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
|
// 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();
|
this._lastTxFetch = +new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,6 +499,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
for (const addressTxs of Object.values(this._txs_by_internal_index)) {
|
for (const addressTxs of Object.values(this._txs_by_internal_index)) {
|
||||||
txs = txs.concat(addressTxs);
|
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
|
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++) {
|
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
|
||||||
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
|
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:
|
// hack: in case this code is called from LegacyWallet:
|
||||||
if (this.getAddress()) ownedAddressesHashmap[String(this.getAddress())] = true;
|
if (this.getAddress()) ownedAddressesHashmap[String(this.getAddress())] = true;
|
||||||
|
|
||||||
|
@ -499,7 +603,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
) {
|
) {
|
||||||
const address = this._getInternalAddressByIndex(c);
|
const address = this._getInternalAddressByIndex(c);
|
||||||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
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);
|
const address = this._getExternalAddressByIndex(c);
|
||||||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
|
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:
|
// doing binary search for last used address:
|
||||||
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
|
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
|
||||||
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(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
|
} // end rescanning fresh wallet
|
||||||
|
|
||||||
// finally fetching balance
|
// 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++) {
|
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));
|
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
|
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
|
// next, business as usuall. fetch balances
|
||||||
|
|
||||||
const addresses2fetch = [];
|
const addresses2fetch = [];
|
||||||
|
@ -614,6 +789,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
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);
|
const balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch);
|
||||||
|
|
||||||
// converting to a more compact internal format
|
// 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();
|
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:
|
// considering UNconfirmed balance:
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].u && this._balances_by_external_index[c].u > 0) {
|
if (this._balances_by_external_index[c] && 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
|
// 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.
|
// 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
|
// 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++) {
|
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
|
||||||
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
|
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 tx of this.getTransactions()) {
|
||||||
for (const output of tx.outputs) {
|
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++) {
|
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);
|
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;
|
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++) {
|
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);
|
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;
|
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++) {
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
if (this._getInternalAddressByIndex(c) === cleanAddress) return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1052,22 +1293,29 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates Segwit Bech32 Bitcoin address
|
* Creates Segwit Bech32 Bitcoin address
|
||||||
*
|
|
||||||
* @param hdNode
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
*/
|
||||||
static _nodeToBech32SegwitAddress(hdNode: BIP32Interface) {
|
_nodeToBech32SegwitAddress(hdNode: BIP32Interface): string {
|
||||||
return bitcoin.payments.p2wpkh({
|
return bitcoin.payments.p2wpkh({
|
||||||
pubkey: hdNode.publicKey,
|
pubkey: hdNode.publicKey,
|
||||||
}).address;
|
}).address;
|
||||||
}
|
}
|
||||||
|
|
||||||
static _nodeToLegacyAddress(hdNode: BIP32Interface) {
|
_nodeToLegacyAddress(hdNode: BIP32Interface): string {
|
||||||
return bitcoin.payments.p2pkh({
|
return bitcoin.payments.p2pkh({
|
||||||
pubkey: hdNode.publicKey,
|
pubkey: hdNode.publicKey,
|
||||||
}).address;
|
}).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<string, ElectrumHistory[]>) {
|
static _getTransactionsFromHistories(histories: Record<string, ElectrumHistory[]>) {
|
||||||
const txs = [];
|
const txs = [];
|
||||||
for (const history of Object.values(histories)) {
|
for (const history of Object.values(histories)) {
|
||||||
|
@ -1192,4 +1440,108 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
const seed = this._getSeed();
|
const seed = this._getSeed();
|
||||||
return AbstractHDElectrumWallet.seedToFingerprint(seed);
|
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<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,10 @@ export class AbstractWallet {
|
||||||
return BitcoinUnit.BTC;
|
return BitcoinUnit.BTC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowBIP47(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
allowReceive(): boolean {
|
allowReceive(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -358,6 +362,10 @@ export class AbstractWallet {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBIP47Enabled(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async wasEverUsed(): Promise<boolean> {
|
async wasEverUsed(): Promise<boolean> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,10 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowBIP47() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
getXpub() {
|
getXpub() {
|
||||||
if (this._xpub) {
|
if (this._xpub) {
|
||||||
return this._xpub; // cache hit
|
return this._xpub; // cache hit
|
||||||
|
@ -48,44 +52,8 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
|
||||||
return this._xpub;
|
return this._xpub;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNodeAddressByIndex(node, index) {
|
_hdNodeToAddress(hdNode) {
|
||||||
index = index * 1; // cast to int
|
return this._nodeToLegacyAddress(hdNode);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUtxo() {
|
async fetchUtxo() {
|
||||||
|
|
|
@ -46,4 +46,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
|
||||||
allowXpub() {
|
allowXpub() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowBIP47() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,44 +40,8 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNodeAddressByIndex(node, index) {
|
_hdNodeToAddress(hdNode) {
|
||||||
index = index * 1; // cast to int
|
return this._nodeToP2shSegwitAddress(hdNode);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -134,18 +98,6 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
|
||||||
return psbt;
|
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() {
|
isSegwit() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
if (this._hdWalletInstance) {
|
if (this._hdWalletInstance) {
|
||||||
delete this._hdWalletInstance._node0;
|
delete this._hdWalletInstance._node0;
|
||||||
delete this._hdWalletInstance._node1;
|
delete this._hdWalletInstance._node1;
|
||||||
|
delete this._hdWalletInstance._bip47_instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,10 @@ export default class TransactionsNavigationHeader extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
wallet: PropTypes.shape().isRequired,
|
wallet: PropTypes.shape().isRequired,
|
||||||
onWalletUnitChange: PropTypes.func,
|
onWalletUnitChange: PropTypes.func,
|
||||||
navigation: PropTypes.shape(),
|
navigation: PropTypes.shape({
|
||||||
|
navigate: PropTypes.func,
|
||||||
|
goBack: PropTypes.func,
|
||||||
|
}),
|
||||||
onManageFundsPressed: PropTypes.func,
|
onManageFundsPressed: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -241,6 +244,21 @@ export default class TransactionsNavigationHeader extends Component {
|
||||||
<Text style={styles.manageFundsButtonText}>{loc.lnd.title}</Text>
|
<Text style={styles.manageFundsButtonText}>{loc.lnd.title}</Text>
|
||||||
</ToolTipMenu>
|
</ToolTipMenu>
|
||||||
)}
|
)}
|
||||||
|
{this.state.wallet.allowBIP47() && 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 && (
|
{this.state.wallet.type === LightningLdkWallet.type && (
|
||||||
<TouchableOpacity accessibilityRole="button" onPress={this.manageFundsPressed}>
|
<TouchableOpacity accessibilityRole="button" onPress={this.manageFundsPressed}>
|
||||||
<View style={styles.manageFundsButton}>
|
<View style={styles.manageFundsButton}>
|
||||||
|
|
22
loc/en.json
22
loc/en.json
|
@ -16,7 +16,7 @@
|
||||||
"seed": "Seed",
|
"seed": "Seed",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"wallet_key": "Wallet key",
|
"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}.",
|
"file_saved": "File {filePath} has been saved in your {destination}.",
|
||||||
"downloads_folder": "Downloads Folder"
|
"downloads_folder": "Downloads Folder"
|
||||||
},
|
},
|
||||||
|
@ -43,13 +43,13 @@
|
||||||
"network": "Network Error"
|
"network": "Network Error"
|
||||||
},
|
},
|
||||||
"lnd": {
|
"lnd": {
|
||||||
"active":"Active",
|
"active": "Active",
|
||||||
"inactive":"Inactive",
|
"inactive": "Inactive",
|
||||||
"channels": "Channels",
|
"channels": "Channels",
|
||||||
"no_channels": "No channels",
|
"no_channels": "No channels",
|
||||||
"claim_balance": "Claim balance {balance}",
|
"claim_balance": "Claim balance {balance}",
|
||||||
"close_channel": "Close channel",
|
"close_channel": "Close channel",
|
||||||
"new_channel" : "New channel",
|
"new_channel": "New channel",
|
||||||
"errorInvoiceExpired": "Invoice expired",
|
"errorInvoiceExpired": "Invoice expired",
|
||||||
"force_close_channel": "Force close channel?",
|
"force_close_channel": "Force close channel?",
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
"placeholder": "Invoice",
|
"placeholder": "Invoice",
|
||||||
"open_channel": "Open Channel",
|
"open_channel": "Open Channel",
|
||||||
"funding_amount_placeholder": "Funding amount, for example 0.001",
|
"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?",
|
"are_you_sure_open_channel": "Are you sure you want to open this channel?",
|
||||||
"potentialFee": "Potential Fee: {fee}",
|
"potentialFee": "Potential Fee: {fee}",
|
||||||
"remote_host": "Remote host",
|
"remote_host": "Remote host",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"refill_create": "In order to proceed, please create a Bitcoin wallet to refill with.",
|
"refill_create": "In order to proceed, please create a Bitcoin wallet to refill with.",
|
||||||
"refill_external": "Refill with External Wallet",
|
"refill_external": "Refill with External Wallet",
|
||||||
"refill_lnd_balance": "Refill Lightning Wallet Balance",
|
"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",
|
"title": "Manage Funds",
|
||||||
"can_send": "Can Send",
|
"can_send": "Can Send",
|
||||||
"can_receive": "Can Receive",
|
"can_receive": "Can Receive",
|
||||||
|
@ -287,7 +287,7 @@
|
||||||
"network_electrum": "Electrum Server",
|
"network_electrum": "Electrum Server",
|
||||||
"not_a_valid_uri": "Invalid URI",
|
"not_a_valid_uri": "Invalid URI",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"open_link_in_explorer" : "Open link in explorer",
|
"open_link_in_explorer": "Open link in explorer",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"password_explain": "Create the password you will use to decrypt the storage.",
|
"password_explain": "Create the password you will use to decrypt the storage.",
|
||||||
"passwords_do_not_match": "Passwords do not match.",
|
"passwords_do_not_match": "Passwords do not match.",
|
||||||
|
@ -306,7 +306,7 @@
|
||||||
"selfTest": "Self-Test",
|
"selfTest": "Self-Test",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saved": "Saved",
|
"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": "Total Balance",
|
||||||
"total_balance_explanation": "Display the total balance of all your wallets on your home screen widgets.",
|
"total_balance_explanation": "Display the total balance of all your wallets on your home screen widgets.",
|
||||||
"widgets": "Widgets",
|
"widgets": "Widgets",
|
||||||
|
@ -590,5 +590,11 @@
|
||||||
"auth_answer": "You have successfully authenticated at {hostname}!",
|
"auth_answer": "You have successfully authenticated at {hostname}!",
|
||||||
"could_not_auth": "We couldn’t authenticate you to {hostname}.",
|
"could_not_auth": "We couldn’t authenticate you to {hostname}.",
|
||||||
"authenticate": "Authenticate"
|
"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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
package-lock.json
generated
33
package-lock.json
generated
|
@ -21,6 +21,7 @@
|
||||||
"@react-navigation/drawer": "5.12.9",
|
"@react-navigation/drawer": "5.12.9",
|
||||||
"@react-navigation/native": "5.9.8",
|
"@react-navigation/native": "5.9.8",
|
||||||
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
|
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
|
||||||
|
"@spsina/bip47": "1.0.1",
|
||||||
"aez": "1.0.1",
|
"aez": "1.0.1",
|
||||||
"assert": "2.0.0",
|
"assert": "2.0.0",
|
||||||
"base-x": "3.0.9",
|
"base-x": "3.0.9",
|
||||||
|
@ -5860,6 +5861,24 @@
|
||||||
"@sinonjs/commons": "^2.0.0"
|
"@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": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
|
@ -28459,6 +28478,20 @@
|
||||||
"@sinonjs/commons": "^2.0.0"
|
"@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": {
|
"@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
"@react-navigation/drawer": "5.12.9",
|
"@react-navigation/drawer": "5.12.9",
|
||||||
"@react-navigation/native": "5.9.8",
|
"@react-navigation/native": "5.9.8",
|
||||||
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
|
"@remobile/react-native-qrcode-local-image": "https://github.com/BlueWallet/react-native-qrcode-local-image",
|
||||||
|
"@spsina/bip47": "1.0.1",
|
||||||
"aez": "1.0.1",
|
"aez": "1.0.1",
|
||||||
"assert": "2.0.0",
|
"assert": "2.0.0",
|
||||||
"base-x": "3.0.9",
|
"base-x": "3.0.9",
|
||||||
|
|
|
@ -128,6 +128,7 @@ const WalletDetails = () => {
|
||||||
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
|
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
|
||||||
const { isAdvancedModeEnabled } = useContext(BlueStorageContext);
|
const { isAdvancedModeEnabled } = useContext(BlueStorageContext);
|
||||||
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
|
const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false);
|
||||||
|
const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled());
|
||||||
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
|
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
|
||||||
const { goBack, navigate, setOptions, popToTop } = useNavigation();
|
const { goBack, navigate, setOptions, popToTop } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
@ -179,7 +180,7 @@ const WalletDetails = () => {
|
||||||
}
|
}
|
||||||
}, [wallet]);
|
}, [wallet]);
|
||||||
|
|
||||||
const setLabel = () => {
|
const save = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (walletName.trim().length > 0) {
|
if (walletName.trim().length > 0) {
|
||||||
wallet.setLabel(walletName.trim());
|
wallet.setLabel(walletName.trim());
|
||||||
|
@ -187,6 +188,7 @@ const WalletDetails = () => {
|
||||||
wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet);
|
wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet);
|
||||||
}
|
}
|
||||||
wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList);
|
wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList);
|
||||||
|
wallet.switchBIP47(isBIP47Enabled);
|
||||||
}
|
}
|
||||||
saveToDisk()
|
saveToDisk()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -209,14 +211,14 @@ const WalletDetails = () => {
|
||||||
testID="Save"
|
testID="Save"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
style={[styles.save, stylesHook.save]}
|
style={[styles.save, stylesHook.save]}
|
||||||
onPress={setLabel}
|
onPress={save}
|
||||||
>
|
>
|
||||||
<Text style={[styles.saveText, stylesHook.saveText]}>{loc.wallets.details_save}</Text>
|
<Text style={[styles.saveText, stylesHook.saveText]}>{loc.wallets.details_save}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isLoading, colors, walletName, useWithHardwareWallet, hideTransactionsInWalletsList]);
|
}, [isLoading, colors, walletName, useWithHardwareWallet, hideTransactionsInWalletsList, isBIP47Enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wallets.some(wallet => wallet.getID() === walletID)) {
|
if (wallets.some(wallet => wallet.getID() === walletID)) {
|
||||||
|
@ -310,6 +312,14 @@ const WalletDetails = () => {
|
||||||
walletID: wallet.getID(),
|
walletID: wallet.getID(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigateToPaymentCodes = () =>
|
||||||
|
navigate('PaymentCodeRoot', {
|
||||||
|
screen: 'PaymentCodesList',
|
||||||
|
params: {
|
||||||
|
walletID: wallet.getID(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const exportInternals = async () => {
|
const exportInternals = async () => {
|
||||||
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
|
if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1);
|
||||||
setBackdoorPressed(0);
|
setBackdoorPressed(0);
|
||||||
|
@ -566,6 +576,16 @@ const WalletDetails = () => {
|
||||||
<BlueText>{wallet.getTransactions().length}</BlueText>
|
<BlueText>{wallet.getTransactions().length}</BlueText>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
{wallet.allowBIP47() ? (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
{wallet.type === WatchOnlyWallet.type && wallet.isHd() && (
|
{wallet.type === WatchOnlyWallet.type && wallet.isHd() && (
|
||||||
<>
|
<>
|
||||||
|
@ -601,6 +621,7 @@ const WalletDetails = () => {
|
||||||
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (
|
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (
|
||||||
<BlueListItem onPress={navigateToAddresses} title={loc.wallets.details_show_addresses} chevron />
|
<BlueListItem onPress={navigateToAddresses} title={loc.wallets.details_show_addresses} chevron />
|
||||||
)}
|
)}
|
||||||
|
{wallet.allowBIP47() && isBIP47Enabled && <BlueListItem onPress={navigateToPaymentCodes} title="Show payment codes" chevron />}
|
||||||
<BlueCard style={styles.address}>
|
<BlueCard style={styles.address}>
|
||||||
<View>
|
<View>
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
|
|
33
screen/wallets/paymentCode.tsx
Normal file
33
screen/wallets/paymentCode.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
67
screen/wallets/paymentCodesList.tsx
Normal file
67
screen/wallets/paymentCodesList.tsx
Normal file
|
@ -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<PaymentCodesListStackParamList, 'PaymentCodesList'>) {
|
||||||
|
const { walletID } = route.params;
|
||||||
|
const { wallets } = useContext(BlueStorageContext);
|
||||||
|
const [data, setData] = useState<DataSection[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{!walletID ? (
|
||||||
|
<Text>Internal error</Text>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<SectionList
|
||||||
|
sections={data}
|
||||||
|
keyExtractor={(item, index) => item + index}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View>
|
||||||
|
<BlueCopyTextToClipboard truncated text={item} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
renderSectionHeader={({ section: { title } }) => <Text style={styles.titleText}>{title}</Text>}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
titleText: { fontSize: 20 },
|
||||||
|
});
|
|
@ -33,6 +33,7 @@ import LNNodeBar from '../../components/LNNodeBar';
|
||||||
import TransactionsNavigationHeader from '../../components/TransactionsNavigationHeader';
|
import TransactionsNavigationHeader from '../../components/TransactionsNavigationHeader';
|
||||||
import { TransactionListItem } from '../../components/TransactionListItem';
|
import { TransactionListItem } from '../../components/TransactionListItem';
|
||||||
import alert from '../../components/Alert';
|
import alert from '../../components/Alert';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const fs = require('../../blue_modules/fs');
|
const fs = require('../../blue_modules/fs');
|
||||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||||
|
@ -42,7 +43,7 @@ const buttonFontSize =
|
||||||
? 22
|
? 22
|
||||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||||
|
|
||||||
const WalletTransactions = () => {
|
const WalletTransactions = ({ navigation }) => {
|
||||||
const { wallets, saveToDisk, setSelectedWallet, walletTransactionUpdateStatus, isElectrumDisabled } = useContext(BlueStorageContext);
|
const { wallets, saveToDisk, setSelectedWallet, walletTransactionUpdateStatus, isElectrumDisabled } = useContext(BlueStorageContext);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { walletID } = useRoute().params;
|
const { walletID } = useRoute().params;
|
||||||
|
@ -177,7 +178,12 @@ const WalletTransactions = () => {
|
||||||
refreshLnNodeInfo();
|
refreshLnNodeInfo();
|
||||||
// await BlueElectrum.ping();
|
// await BlueElectrum.ping();
|
||||||
await BlueElectrum.waitTillConnected();
|
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 balanceStart = +new Date();
|
||||||
const oldBalance = wallet.getBalance();
|
const oldBalance = wallet.getBalance();
|
||||||
await wallet.fetchBalance();
|
await wallet.fetchBalance();
|
||||||
|
@ -467,6 +473,7 @@ const WalletTransactions = () => {
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<StatusBar barStyle="light-content" backgroundColor={WalletGradient.headerColorFor(wallet.type)} animated />
|
<StatusBar barStyle="light-content" backgroundColor={WalletGradient.headerColorFor(wallet.type)} animated />
|
||||||
<TransactionsNavigationHeader
|
<TransactionsNavigationHeader
|
||||||
|
navigation={navigation}
|
||||||
wallet={wallet}
|
wallet={wallet}
|
||||||
onWalletUnitChange={passedWallet =>
|
onWalletUnitChange={passedWallet =>
|
||||||
InteractionManager.runAfterInteractions(async () => {
|
InteractionManager.runAfterInteractions(async () => {
|
||||||
|
@ -605,6 +612,10 @@ WalletTransactions.navigationOptions = navigationStyle({}, (options, { theme, na
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WalletTransactions.propTypes = {
|
||||||
|
navigation: PropTypes.shape(),
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
flex: {
|
flex: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
@ -3,7 +3,7 @@ const path = require('path');
|
||||||
|
|
||||||
const mainLocFile = './loc/en.json';
|
const mainLocFile = './loc/en.json';
|
||||||
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class'];
|
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 allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];
|
||||||
|
|
||||||
const allLocKeysHashmap = {}; // loc key -> used or not
|
const allLocKeysHashmap = {}; // loc key -> used or not
|
||||||
|
|
139
tests/integration/bip47.test.ts
Normal file
139
tests/integration/bip47.test.ts
Normal file
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,8 +5,9 @@ import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock
|
||||||
const consoleWarnOrig = console.warn;
|
const consoleWarnOrig = console.warn;
|
||||||
console.warn = (...args) => {
|
console.warn = (...args) => {
|
||||||
if (
|
if (
|
||||||
args[0]?.startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
|
typeof args[0] === 'string' &&
|
||||||
args[0]?.startsWith('only compressed public keys are good')
|
(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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -16,10 +17,11 @@ console.warn = (...args) => {
|
||||||
const consoleLogOrig = console.log;
|
const consoleLogOrig = console.log;
|
||||||
console.log = (...args) => {
|
console.log = (...args) => {
|
||||||
if (
|
if (
|
||||||
args[0]?.startsWith('updating exchange rate') ||
|
typeof args[0] === 'string' &&
|
||||||
args[0]?.startsWith('begin connection') ||
|
(args[0].startsWith('updating exchange rate') ||
|
||||||
args[0]?.startsWith('TLS Connected to') ||
|
args[0].startsWith('begin connection') ||
|
||||||
args[0]?.startsWith('connected to')
|
args[0].startsWith('TLS Connected to') ||
|
||||||
|
args[0].startsWith('connected to'))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
96
tests/unit/bip47.test.ts
Normal file
96
tests/unit/bip47.test.ts
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue