Compare commits

...

20 Commits

Author SHA1 Message Date
Ojok
7e82bfa150
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-23 13:54:38 +03:00
Ojok Emmanuel Nsubuga
2c4fffd336 FIX: Remove redundan code and fix ui inconsistency 2026-06-23 13:54:06 +03:00
Ojok Emmanuel Nsubuga
29fbbadd56 TST: Add tests for negative cases 2026-06-23 13:52:50 +03:00
Ojok
03e2913c1f
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-22 18:45:21 +03:00
Ojok Emmanuel Nsubuga
b6438e6a3a FIX: Remove e2e for edit masterfingerprint 2026-06-22 18:45:02 +03:00
Ojok Emmanuel Nsubuga
15ec76b3e5
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-22 12:45:20 +03:00
Ojok Emmanuel Nsubuga
15c7a7574a FIX: Remove support for binary and decimal fingerprint, move e2e test 2026-06-22 12:44:37 +03:00
Ojok Emmanuel Nsubuga
1ed80d348f
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-14 14:26:38 +03:00
Ojok Emmanuel Nsubuga
075164d39f
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-08 13:11:52 +03:00
Ojok Emmanuel Nsubuga
c1619a4af5
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-04 11:47:52 +03:00
Ojok Emmanuel Nsubuga
ae18a326d2
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-06-01 07:54:50 +03:00
Ojok Emmanuel Nsubuga
e3d490fc72 TST: Extract e2e test for edit masterfingerprint 2026-05-30 14:27:59 +03:00
Ojok Emmanuel Nsubuga
4b9b78dabc
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-05-29 20:42:02 +03:00
Ojok Emmanuel Nsubuga
5aaf2b00df ADD: Accept binary and decimal format and add e2e tests 2026-05-29 20:33:50 +03:00
Ojok Emmanuel Nsubuga
da5f4a3bed ADD: Add alert for invalid master fingerprint 2026-05-26 12:48:59 +03:00
Ojok Emmanuel Nsubuga
da4dd9c983
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-05-26 09:15:55 +03:00
Ojok Emmanuel Nsubuga
44bb007591
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-05-22 22:24:28 +03:00
Ojok Emmanuel Nsubuga
250fcb212c ADD: Add edit masterfingerprint on new wallet details ui 2026-05-22 11:35:31 +03:00
Ojok Emmanuel Nsubuga
cbd9937f58
Merge branch 'master' into edit-masterfingerprint-watch-only-wallet 2026-05-22 06:06:30 +03:00
Ojok Emmanuel Nsubuga
32b1e7be5e ADD: Edit masterfingerprint for watch only wallets
- Includes an edit box containing current master fingerprint
- if master finger print is deleted and input box left empty, it resets to old value
- if master fingerprint entered is less than 8 characters and contains invalid hex characters, it will be reset to old value
- Added wallet getMasterFingerprintHex() function to watch only wallets class
- Added an integration test for editing master finger print for watch only wallet
2026-04-29 15:07:10 +03:00
5 changed files with 127 additions and 22 deletions

View File

@ -236,20 +236,35 @@ export class WatchOnlyWallet extends LegacyWallet {
getMasterFingerprintHex() {
if (!this.masterFingerprint) return '00000000';
let masterFingerprintHex = Number(this.masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
// poor man's little-endian conversion:
// ¯\_(ツ)_/¯
return (
masterFingerprintHex[6] +
masterFingerprintHex[7] +
masterFingerprintHex[4] +
masterFingerprintHex[5] +
masterFingerprintHex[2] +
masterFingerprintHex[3] +
masterFingerprintHex[0] +
masterFingerprintHex[1]
);
const hex = Number(this.masterFingerprint).toString(16).padStart(8, '0');
const bytes = hex.match(/../g);
if (!bytes) {
return '00000000';
} else {
// convert Big Endian to Little Endian
return bytes.reverse().join('');
}
}
setMasterFingerprintHex(hex: string) {
if (typeof hex !== 'string') {
throw new Error('Fingerprint must be a hex string');
}
// remove 0x
hex = hex.replace(/^0x/i, '');
if (!/^[0-9a-fA-F]{8}$/.test(hex)) {
throw new Error('Master fingerprint must be exactly 8 hex characters');
}
// convert Little Endian to Big Endian
const reversed = hex.slice(6, 8) + hex.slice(4, 6) + hex.slice(2, 4) + hex.slice(0, 2);
this.masterFingerprint = parseInt(reversed, 16);
}
isHd() {

View File

@ -138,9 +138,13 @@ const ListItem: React.FC<ListItemProps> = React.memo(
) : null}
{rightSubtitle != null && rightSubtitle !== '' ? (
<View style={styles.rightMemoWrapper}>
<Text style={[stylesHook.subtitle, rightSubtitleStyle, stylesHook.rightMemoText]} numberOfLines={1} ellipsizeMode="tail">
{rightSubtitle}
</Text>
{typeof rightSubtitle === 'string' ? (
<Text style={[stylesHook.subtitle, rightSubtitleStyle, stylesHook.rightMemoText]} numberOfLines={1} ellipsizeMode="tail">
{rightSubtitle}
</Text>
) : (
rightSubtitle
)}
</View>
) : null}
</View>

View File

@ -524,6 +524,8 @@
"xpub_title": "Wallet XPUB",
"manage_wallets_search_placeholder": "Search wallets, addresses, transactions and memos",
"more_info": "More Info",
"invalid_masterfingerprint_title": "Invalid Format",
"invalid_masterfingerprint_description": "Please enter a valid hex value",
"details_delete_wallet_error_message": "There was an issue confirming if this wallet was removed from notifications—this could be due to a network issue or poor connection. If you continue, you might still receive notifications for transactions related to this wallet, even after it is deleted.",
"details_delete_anyway": "Delete anyway"
},

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Pressable, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { writeFileAndExport } from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { uint8ArrayToHex } from '../../blue_modules/uint8array-extras';
@ -26,7 +26,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { useFocusEffect, useRoute, RouteProp, usePreventRemove, useLocale } from '@react-navigation/native';
import { useFocusEffect, useRoute, RouteProp, useLocale } from '@react-navigation/native';
import { LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import ToolTipMenu from '../../components/TooltipMenu';
@ -390,6 +390,10 @@ const WalletDetails: React.FC = () => {
backgroundColor: 'transparent',
borderBottomColor: colors.cardBorderColor,
},
input: {
borderColor: colors.formBorder,
color: colors.foregroundColor,
},
});
const navigateToWalletExport = () => {
@ -521,7 +525,31 @@ const WalletDetails: React.FC = () => {
}
}, [wallet, saveToDisk]);
usePreventRemove(false, () => {});
const walletMasterFingerprintInputOnBlur = useCallback(async () => {
if (wallet.type === WatchOnlyWallet.type) {
const mfp = masterFingerprint?.trim().toLocaleLowerCase();
// masterfingerprint before editing started
const currentMasterFingerprint = wallet.getMasterFingerprintHex();
if (!mfp) {
presentAlert({ title: loc.wallets.invalid_masterfingerprint_title, message: loc.wallets.invalid_masterfingerprint_description });
setMasterFingerprint(currentMasterFingerprint);
return;
}
if (mfp !== currentMasterFingerprint) {
try {
wallet.setMasterFingerprintHex(mfp);
await saveToDisk();
setMasterFingerprint(wallet.getMasterFingerprintHex());
} catch (error) {
presentAlert({ title: loc.wallets.invalid_masterfingerprint_title, message: loc.wallets.invalid_masterfingerprint_description });
setMasterFingerprint(currentMasterFingerprint);
}
}
}
}, [wallet, masterFingerprint, setMasterFingerprint, saveToDisk]);
const onViewMasterFingerPrintPress = () => {
setIsMasterFingerPrintVisible(true);
@ -822,6 +850,7 @@ const WalletDetails: React.FC = () => {
onPress={() => setIsAdvancedExpanded(prev => !prev)}
style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRowContainer]}
activeOpacity={0.85}
testID="advanced-details"
>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.wallets.details_advanced}</BlueText>
<Icon
@ -876,8 +905,34 @@ const WalletDetails: React.FC = () => {
onPress={isMasterFingerPrintVisible ? undefined : onViewMasterFingerPrintPress}
title={loc.wallets.details_master_fingerprint}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={
isMasterFingerPrintVisible ? (masterFingerprint ?? loc.wallets.import_derivation_loading) : loc.multisig.view
rightSubtitle={
<View>
{isMasterFingerPrintVisible ? (
<View>
{wallet.type === WatchOnlyWallet.type && wallet.isHd() ? (
<TextInput
value={masterFingerprint}
onChangeText={(text: string) => {
setMasterFingerprint(text);
}}
onBlur={walletMasterFingerprintInputOnBlur}
numberOfLines={1}
style={[styles.input, stylesHook.input, { writingDirection: direction }]}
editable={!isLoading}
underlineColorAndroid="transparent"
testID="masterfingerPrintInput"
autoFocus
/>
) : (
<BlueText selectable>{masterFingerprint ?? loc.wallets.import_derivation_loading}</BlueText>
)}
</View>
) : (
<Pressable onPress={onViewMasterFingerPrintPress} testID="viewMasterfingerprint">
<BlueText>{loc.multisig.view}</BlueText>
</Pressable>
)}
</View>
}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
rightTitleSelectable={isMasterFingerPrintVisible}
@ -983,6 +1038,17 @@ const styles = StyleSheet.create({
fontWeight: '500',
lineHeight: 20,
},
input: {
flexDirection: 'row',
borderWidth: 1,
alignItems: 'center',
borderRadius: 4,
padding: 6,
marginTop: 12,
minWidth: 88,
maxWidth: 88,
textAlign: 'center',
},
detailsCard: {
marginHorizontal: 16,
marginBottom: 40,

View File

@ -102,6 +102,24 @@ describe('Watch only wallet', () => {
assert.strictEqual(nextChangeAddress, 'bc1q74tz7eflqc62v8utqlazcs3tqtwmvvzud5dmrz');
});
it('can edit master finger print', async () => {
const w = new WatchOnlyWallet();
w.setSecret('zpub6rERe82dmmpndd2jSsRH5o3GaDfESv1Zk2cESDuB85HFNSujcDBDZTxvdNCdXzfi83okz7VKx46FA9RxkfYHcZLKU3FRY2b4sf2DzNoMdLU');
assert.strictEqual(w.getMasterFingerprintHex(), '00000000');
w.setMasterFingerprintHex('398e3e5b');
assert.strictEqual(w.getMasterFingerprintHex(), '398e3e5b');
w.setMasterFingerprintHex('0x398e3e5b');
assert.strictEqual(w.getMasterFingerprintHex(), '398e3e5b');
assert.throws(() => w.setMasterFingerprintHex(''), /Master fingerprint must be exactly 8 hex characters/);
assert.throws(() => w.setMasterFingerprintHex('1234'), /Master fingerprint must be exactly 8 hex characters/);
assert.throws(() => w.setMasterFingerprintHex('123456789'), /Master fingerprint must be exactly 8 hex characters/);
});
// skipped because its generally rare case
// eslint-disable-next-line jest/no-disabled-tests
it.skip('can fetch txs for address funded by genesis txs', async () => {