Merge branch 'master' into electrumpref

This commit is contained in:
Marcos Rodriguez Velez 2025-01-05 13:43:10 -04:00
commit 9555e5927e
37 changed files with 985 additions and 798 deletions

View File

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View File

@ -58,13 +58,20 @@
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:autoSizeMaxTextSize="24sp"
android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:duplicateParentState="false"
android:editable="false"
android:lines="1"
android:text="Loading..."
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:layout_gravity="end"/>
android:visibility="gone" />
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"

View File

@ -132,13 +132,19 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
return undefined;
} else if (response.errorCode) {
throw new Error(response.errorMessage);
} else if (response.assets?.[0]?.uri) {
} else if (response.assets) {
try {
const result = await RNQRGenerator.detect({ uri: decodeURI(response.assets[0].uri.toString()) });
return result?.values[0];
const uri = response.assets[0].uri;
if (uri) {
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
if (result?.values.length > 0) {
return result?.values[0];
}
}
throw new Error(loc.send.qr_error_no_qrcode);
} catch (error) {
console.error(error);
throw new Error(loc.send.qr_error_no_qrcode);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
}
@ -187,9 +193,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
if (result) {
return { data: result.values[0], uri: fileCopyUri };
}
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
}
}

View File

@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex');
}
const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
let path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
if (path === 'm/') {
// not considered valid by Bip32 lib
path = 'm/0';
}
let xpub = m[2];
if (xpub.indexOf('/') !== -1) {
xpub = xpub.substr(0, xpub.indexOf('/'));

View File

@ -1,15 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
import loc from '../loc';
import { scanQrHelper } from '../helpers/scan-qr';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert';
import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
import { useRoute } from '@react-navigation/native';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps {
isLoading: boolean;
@ -19,6 +20,10 @@ interface AddressInputScanButtonProps {
onChangeText: (text: string) => void;
}
interface RouteParams {
onBarScanned?: any;
}
export const AddressInputScanButton = ({
isLoading,
launchedBy,
@ -28,6 +33,9 @@ export const AddressInputScanButton = ({
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
const navigation = useExtendedNavigation();
const params = useRoute().params as RouteParams;
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
@ -40,8 +48,10 @@ export const AddressInputScanButton = ({
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, scanButtonTapped]);
const actions = useMemo(() => {
const availableActions = [
@ -57,20 +67,23 @@ export const AddressInputScanButton = ({
return availableActions;
}, [isClipboardGetContentEnabled]);
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned({ data });
navigation.setParams({ onBarScanned: undefined });
}
});
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
@ -134,7 +147,7 @@ export const AddressInputScanButton = ({
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
[navigation, onBarScanned, onChangeText, scanButtonTapped],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);

View File

@ -183,6 +183,9 @@ const CompanionDelegates = () => {
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
try {
if (!decodedUrl) {
throw new Error(loc.send.qr_error_no_qrcode);
}
const values = await RNQRGenerator.detect({
uri: decodedUrl,
});
@ -200,11 +203,12 @@ const CompanionDelegates = () => {
},
);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
throw new Error(loc.send.qr_error_no_qrcode);
}
} catch (error) {
console.error('Error detecting QR code:', error);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} else {
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {

View File

@ -155,9 +155,13 @@ const MultipleStepsListItem = props => {
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
{props.button.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
)}
</TouchableOpacity>
</View>
)}
@ -171,7 +175,11 @@ const MultipleStepsListItem = props => {
style={styles.rightButton}
onPress={props.rightButton.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
{props.rightButton.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
)}
</TouchableOpacity>
</View>
)}
@ -194,11 +202,13 @@ MultipleStepsListItem.propTypes = {
disabled: PropTypes.bool,
buttonType: PropTypes.number,
leftText: PropTypes.string,
showActivityIndicator: PropTypes.bool,
}),
rightButton: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
showActivityIndicator: PropTypes.bool,
}),
};

View File

@ -1,54 +1,5 @@
import { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService';
/**
* Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan,
* and then navigates back. If QRCode scan was closed, promise resolves to null.
*
* @param currentScreenName {string}
* @param showFileImportButton {boolean}
*
* @param onDismiss {function} - if camera is closed via X button it gets triggered
* @param useMerge {boolean} - if true, will merge the new screen with the current screen, otherwise will replace the current screen
* @return {Promise<string>}
*/
function scanQrHelper(
currentScreenName: string,
showFileImportButton = true,
onDismiss?: () => void,
useMerge = true,
): Promise<string | null> {
return requestCameraAuthorization().then(() => {
return new Promise(resolve => {
let params = {};
if (useMerge) {
const onBarScanned = function (data: any) {
setTimeout(() => resolve(data.data || data), 1);
navigationRef.navigate({ name: currentScreenName, params: data, merge: true });
};
params = {
showFileImportButton: Boolean(showFileImportButton),
onDismiss,
onBarScanned,
};
} else {
params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) };
}
navigationRef.navigate({
name: 'ScanQRCodeRoot',
params: {
screen: 'ScanQRCode',
params,
},
merge: true,
});
});
});
}
const isCameraAuthorizationStatusGranted = async () => {
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
@ -59,4 +10,4 @@ const requestCameraAuthorization = () => {
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
};
export { scanQrHelper, isCameraAuthorizationStatusGranted, requestCameraAuthorization };
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization };

View File

@ -3,6 +3,7 @@ import { navigationRef } from '../NavigationService';
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
import { useStorage } from './context/useStorage';
import { requestCameraAuthorization } from '../helpers/scan-qr';
// List of screens that require biometrics
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
@ -90,6 +91,10 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
return; // Prevent proceeding with the original navigation if the reminder is shown
}
}
if (screenName === 'ScanQRCode') {
await requestCameraAuthorization();
}
proceedWithNavigation();
})();
};

View File

@ -137,7 +137,7 @@
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>

View File

@ -193,7 +193,7 @@
"outdated_rate": "Rate was last updated: {date}",
"psbt_tx_open": "Open Signed Transaction",
"psbt_tx_scan": "Scan Signed Transaction",
"qr_error_no_qrcode": "We were unable to find a QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"qr_error_no_qrcode": "We were unable to find a valid QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"reset_amount": "Reset Amount",
"reset_amount_confirm": "Would you like to reset the amount?",
"success_done": "Done",

View File

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View File

@ -17,13 +17,15 @@ import {
WalletsAddMultisigHelpComponent,
WalletsAddMultisigStep2Component,
} from './LazyLoadAddWalletStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type AddWalletStackParamList = {
AddWallet: undefined;
ImportWallet?: {
label?: string;
triggerImport?: boolean;
scannedData?: string;
onBarScanned?: string;
};
ImportWalletDiscovery: {
importText: string;
@ -55,6 +57,7 @@ export type AddWalletStackParamList = {
format: string;
};
WalletsAddMultisigHelp: undefined;
ScanQRCode: ScanQRCodeParamList;
};
const Stack = createNativeStackNavigator<AddWalletStackParamList>();
@ -138,6 +141,16 @@ const AddWalletStack = () => {
headerShadowVisible: false,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View File

@ -33,7 +33,6 @@ import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack';
import ReceiveDetailsStackRoot from './ReceiveDetailsStack';
import ScanLndInvoiceRoot from './ScanLndInvoiceStack';
import ScanQRCodeStackRoot from './ScanQRCodeStack';
import SendDetailsStack from './SendDetailsStack';
import SignVerifyStackRoot from './SignVerifyStack';
import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack';
@ -65,6 +64,7 @@ import SelfTest from '../screen/settings/SelfTest';
import ReleaseNotes from '../screen/settings/ReleaseNotes';
import ToolsScreen from '../screen/settings/tools';
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const DetailViewStackScreensStack = () => {
const theme = useTheme();
@ -358,15 +358,6 @@ const DetailViewStackScreensStack = () => {
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<DetailViewStack.Screen name="ReceiveDetailsRoot" component={ReceiveDetailsStackRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen
name="ScanQRCodeRoot"
component={ScanQRCodeStackRoot}
options={{
headerShown: false,
presentation: 'fullScreenModal',
statusBarHidden: true,
}}
/>
<DetailViewStack.Screen
name="ManageWallets"
component={ManageWallets}
@ -378,6 +369,16 @@ const DetailViewStackScreensStack = () => {
statusBarStyle: 'auto',
})(theme)}
/>
<DetailViewStack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</DetailViewStack.Navigator>
);
};

View File

@ -2,10 +2,24 @@ import { LightningTransaction, Transaction, TWallet } from '../class/wallets/typ
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
import { SendDetailsParams } from './SendDetailsStackParamList';
export type ScanQRCodeParamList = {
cameraStatusGranted?: boolean;
backdoorPressed?: boolean;
launchedBy?: string;
urTotal?: number;
urHave?: number;
backdoorText?: string;
onDismiss?: () => void;
onBarScanned?: (data: string) => void;
showFileImportButton?: boolean;
backdoorVisible?: boolean;
animatedQRCodeData?: Record<string, any>;
};
export type DetailViewStackParamList = {
UnlockWithScreen: undefined;
WalletsList: { scannedData?: string };
WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string };
WalletsList: { onBarScanned?: string };
WalletTransactions: { isLoading?: boolean; walletID: string; walletType: string; onBarScanned?: string };
WalletDetails: { walletID: string };
TransactionDetails: { tx: Transaction; hash: string; walletID: string };
TransactionStatus: { hash: string; walletID?: string };
@ -19,8 +33,8 @@ export type DetailViewStackParamList = {
LNDViewInvoice: { invoice: LightningTransaction; walletID: string };
LNDViewAdditionalInvoiceInformation: { invoiceId: string };
LNDViewAdditionalInvoicePreImage: { invoiceId: string };
Broadcast: { scannedData?: string };
IsItMyAddress: { address?: string };
Broadcast: { onBarScanned?: string };
IsItMyAddress: { address?: string; onBarScanned?: string };
GenerateWord: undefined;
LnurlPay: undefined;
LnurlPaySuccess: {
@ -57,12 +71,13 @@ export type DetailViewStackParamList = {
NetworkSettings: undefined;
About: undefined;
DefaultView: undefined;
ElectrumSettings: { server?: ElectrumServerItem };
ElectrumSettings: { server?: ElectrumServerItem; onBarScanned?: string };
SettingsBlockExplorer: undefined;
EncryptStorage: undefined;
Language: undefined;
LightningSettings: {
url?: string;
onBarScanned?: string;
};
NotificationSettings: undefined;
SelfTest: undefined;
@ -85,22 +100,7 @@ export type DetailViewStackParamList = {
address: string;
};
};
ScanQRCodeRoot: {
screen: string;
params: {
isLoading: false;
cameraStatusGranted?: boolean;
backdoorPressed?: boolean;
launchedBy?: string;
urTotal?: number;
urHave?: number;
backdoorText?: string;
onDismiss?: () => void;
showFileImportButton: true;
backdoorVisible?: boolean;
animatedQRCodeData?: Record<string, any>;
};
};
ScanQRCode: ScanQRCodeParamList;
PaymentCodeList: {
paymentCode: string;
walletID: string;

View File

@ -10,6 +10,7 @@ import {
LNDViewInvoiceComponent,
SelectWalletComponent,
} from './LazyLoadLNDCreateInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
@ -54,6 +55,16 @@ const LNDCreateInvoiceRoot = () => {
component={LNDViewAdditionalInvoicePreImageComponent}
options={navigationStyle({ title: loc.lndViewInvoice.additional_info })(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View File

@ -11,6 +11,7 @@ import {
SelectWalletComponent,
SuccessComponent,
} from './LazyLoadScanLndInvoiceStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
@ -50,6 +51,16 @@ const ScanLndInvoiceRoot = () => {
gestureEnabled: false,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View File

@ -1,28 +0,0 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator();
const ScanQRCodeStackRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
})(theme)}
/>
</Stack.Navigator>
);
};
export default ScanQRCodeStackRoot;

View File

@ -18,6 +18,7 @@ import {
import { SendDetailsStackParamList } from './SendDetailsStackParamList';
import HeaderRightButton from '../components/HeaderRightButton';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
const Stack = createNativeStackNavigator<SendDetailsStackParamList>();
@ -81,6 +82,16 @@ const SendDetailsStack = () => {
component={PaymentCodesListComponent}
options={navigationStyle({ title: loc.bip47.contacts })(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View File

@ -1,6 +1,7 @@
import { Psbt } from 'bitcoinjs-lib';
import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../class/wallets/types';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type SendDetailsParams = {
transactionMemo?: string;
@ -12,6 +13,7 @@ export type SendDetailsParams = {
address?: string;
amount?: number;
amountSats?: number;
onBarScanned?: string;
unit?: BitcoinUnit;
noRbf?: boolean;
walletID: string;
@ -84,4 +86,5 @@ export type SendDetailsStackParamList = {
PaymentCodeList: {
walletID: string;
};
ScanQRCode: ScanQRCodeParamList;
};

View File

@ -5,11 +5,15 @@ import navigationStyle from '../components/navigationStyle';
import { useTheme } from '../components/themes';
import loc from '../loc';
import { ViewEditMultisigCosignersComponent } from './LazyLoadViewEditMultisigCosignersStack';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
export type ViewEditMultisigCosignersStackParamList = {
ViewEditMultisigCosigners: {
walletID: string;
onBarScanned?: string;
};
ScanQRCode: ScanQRCodeParamList;
};
const Stack = createNativeStackNavigator<ViewEditMultisigCosignersStackParamList>();
@ -27,6 +31,16 @@ const ViewEditMultisigCosignersStackRoot = () => {
title: loc.multisig.manage_keys,
})(theme)}
/>
<Stack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</Stack.Navigator>
);
};

View File

@ -24,7 +24,6 @@ import AmountInput from '../../components/AmountInput';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalancePlain, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import * as NavigationService from '../../NavigationService';
@ -36,7 +35,7 @@ const LNDCreateInvoice = () => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
const { walletID, uri } = useRoute().params;
const wallet = useRef(wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN));
const { name } = useRoute();
const { params } = useRoute();
const { colors } = useTheme();
const { navigate, getParent, goBack, pop, setParams } = useNavigation();
const [unit, setUnit] = useState(wallet.current?.getPreferredBalanceUnit() || BitcoinUnit.BTC);
@ -75,6 +74,100 @@ const LNDCreateInvoice = () => {
},
});
const processLnurl = useCallback(
async data => {
setIsLoading(true);
if (!wallet.current) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.no_ln_wallet_error });
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
const { query } = parse(url, true);
if (query.tag === Lnurl.TAG_LOGIN_REQUEST) {
navigate('LnurlAuth', {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
});
return;
}
// calling the url
try {
const resp = await fetch(url, { method: 'GET' });
if (resp.status >= 300) {
throw new Error('Bad response from server');
}
const reply = await resp.json();
if (reply.status === 'ERROR') {
throw new Error('Reply from server: ' + reply.reason);
}
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
// invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
},
});
return;
}
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
throw new Error('Unsupported lnurl');
}
// amount that comes from lnurl is always in sats
let newAmount = (reply.maxWithdrawable / 1000).toString();
const sats = newAmount;
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
newAmount = satoshiToBTC(newAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(newAmount, sats);
break;
}
// setting the invoice creating screen with the parameters
setLNURLParams({
k1: reply.k1,
callback: reply.callback,
fixed: reply.minWithdrawable === reply.maxWithdrawable,
min: (reply.minWithdrawable || 0) / 1000,
max: reply.maxWithdrawable / 1000,
});
setAmount(newAmount);
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: Err.message });
}
},
[goBack, navigate, unit, walletID],
);
useEffect(() => {
const data = params.onBarScanned;
if (data) {
processLnurl(data);
setParams({ onBarScanned: undefined });
}
}, [params.onBarScanned, processLnurl, setParams]);
useEffect(() => {
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide);
@ -228,89 +321,6 @@ const LNDCreateInvoice = () => {
}
};
const processLnurl = async data => {
setIsLoading(true);
if (!wallet.current) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.wallets.no_ln_wallet_error });
return goBack();
}
// decoding the lnurl
const url = Lnurl.getUrlFromLnurl(data);
const { query } = parse(url, true);
if (query.tag === Lnurl.TAG_LOGIN_REQUEST) {
navigate('LnurlAuth', {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
});
return;
}
// calling the url
try {
const resp = await fetch(url, { method: 'GET' });
if (resp.status >= 300) {
throw new Error('Bad response from server');
}
const reply = await resp.json();
if (reply.status === 'ERROR') {
throw new Error('Reply from server: ' + reply.reason);
}
if (reply.tag === Lnurl.TAG_PAY_REQUEST) {
// we are here by mistake. user wants to SEND to lnurl-pay, but he is on a screen that creates
// invoices (including through lnurl-withdraw)
navigate('ScanLndInvoiceRoot', {
screen: 'LnurlPay',
params: {
lnurl: data,
walletID: walletID ?? wallet.current.getID(),
},
});
return;
}
if (reply.tag !== Lnurl.TAG_WITHDRAW_REQUEST) {
throw new Error('Unsupported lnurl');
}
// amount that comes from lnurl is always in sats
let newAmount = (reply.maxWithdrawable / 1000).toString();
const sats = newAmount;
switch (unit) {
case BitcoinUnit.SATS:
// nop
break;
case BitcoinUnit.BTC:
newAmount = satoshiToBTC(newAmount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
newAmount = formatBalancePlain(newAmount, BitcoinUnit.LOCAL_CURRENCY);
AmountInput.setCachedSatoshis(newAmount, sats);
break;
}
// setting the invoice creating screen with the parameters
setLNURLParams({
k1: reply.k1,
callback: reply.callback,
fixed: reply.minWithdrawable === reply.maxWithdrawable,
min: (reply.minWithdrawable || 0) / 1000,
max: reply.maxWithdrawable / 1000,
});
setAmount(newAmount);
setDescription(reply.defaultDescription);
setIsLoading(false);
} catch (Err) {
Keyboard.dismiss();
setIsLoading(false);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: Err.message });
}
};
const renderCreateButton = () => {
return (
<View style={styles.createButton}>
@ -320,7 +330,9 @@ const LNDCreateInvoice = () => {
};
const navigateToScanQRCode = () => {
scanQrHelper(name, true, processLnurl);
navigate('ScanQRCode', {
showFileImportButton: true,
});
Keyboard.dismiss();
};

View File

@ -18,13 +18,14 @@ import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const ScanLndInvoice = () => {
const { wallets, fetchAndSaveWalletTransactions } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { colors } = useTheme();
const route = useRoute();
const { walletID, uri, invoice } = useRoute().params;
const name = useRoute().name;
/** @type {LightningCustodianWallet} */
const [wallet, setWallet] = useState(
wallets.find(item => item.getID() === walletID) || wallets.find(item => item.chain === Chain.OFFCHAIN),
@ -281,6 +282,25 @@ const ScanLndInvoice = () => {
pop();
};
const onBarScanned = useCallback(
value => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigate(...completionValue);
});
},
[navigate],
);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
setParams({ onBarScanned: undefined });
}
}, [navigate, onBarScanned, route.params?.onBarScanned, setParams]);
if (wallet === undefined || !wallet) {
return (
<View style={[styles.loadingIndicator, stylesHook.root]}>
@ -323,7 +343,6 @@ const ScanLndInvoice = () => {
isLoading={isLoading}
placeholder={loc.lnd.placeholder}
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
launchedBy={name}
onBlur={onBlur}
keyboardType="email-address"
style={styles.addressInput}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import { ActivityIndicator, Keyboard, Linking, StyleSheet, TextInput, View } from 'react-native';
@ -19,11 +19,12 @@ import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useSettings } from '../../hooks/context/useSettings';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
import { navigate } from '../../NavigationService';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const BROADCAST_RESULT = Object.freeze({
none: 'Input transaction hex',
@ -35,12 +36,13 @@ const BROADCAST_RESULT = Object.freeze({
type RouteProps = RouteProp<DetailViewStackParamList, 'Broadcast'>;
const Broadcast: React.FC = () => {
const { name, params } = useRoute<RouteProps>();
const { params } = useRoute<RouteProps>();
const [tx, setTx] = useState<string | undefined>();
const [txHex, setTxHex] = useState<string | undefined>();
const { colors } = useTheme();
const [broadcastResult, setBroadcastResult] = useState<string>(BROADCAST_RESULT.none);
const { selectedBlockExplorer } = useSettings();
const { setParams } = useExtendedNavigation();
const stylesHooks = StyleSheet.create({
input: {
@ -50,13 +52,26 @@ const Broadcast: React.FC = () => {
},
});
const handleScannedData = useCallback((scannedData: string) => {
if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
return handleUpdateTxHex(scannedData);
}
try {
// should be base64 encoded PSBT
const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction();
return handleUpdateTxHex(validTx.toHex());
} catch (e) {}
}, []);
useEffect(() => {
const scannedData = params?.scannedData;
const scannedData = params?.onBarScanned;
if (scannedData) {
handleScannedData(scannedData);
setParams({ onBarScanned: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.scannedData]);
}, [handleScannedData, params?.onBarScanned, setParams]);
const handleUpdateTxHex = (nextValue: string) => setTxHex(nextValue.trim());
@ -88,21 +103,8 @@ const Broadcast: React.FC = () => {
}
};
const handleScannedData = (scannedData: string) => {
if (scannedData.indexOf('+') === -1 && scannedData.indexOf('=') === -1 && scannedData.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
return handleUpdateTxHex(scannedData);
}
try {
// should be base64 encoded PSBT
const validTx = bitcoin.Psbt.fromBase64(scannedData).extractTransaction();
return handleUpdateTxHex(validTx.toHex());
} catch (e) {}
};
const handleQRScan = () => {
scanQrHelper(name, true, undefined, false);
navigate('ScanQRCode');
};
let status;

View File

@ -5,20 +5,16 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Image, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { CameraScreen } from 'react-native-camera-kit';
import { Icon } from '@rneui/themed';
import { launchImageLibrary } from 'react-native-image-picker';
import Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs';
import { BlueURDecoder, decodeUR, extractSingleWorkload } from '../../blue_modules/ur';
import { BlueLoading, BlueSpacing40, BlueText } from '../../BlueComponents';
import { openPrivacyDesktopSettings } from '../../class/camera';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import RNQRGenerator from 'rn-qr-generator';
let decoder = false;
@ -89,7 +85,11 @@ const ScanQRCode = () => {
const { setIsDrawerShouldHide } = useSettings();
const navigation = useNavigation();
const route = useRoute();
const { launchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params;
const navigationState = navigation.getState();
const previousRoute = navigationState.routes[navigationState.routes.length - 2];
const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined;
const { launchedBy = defaultLaunchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params || {};
const scannedCache = {};
const { colors } = useTheme();
const isFocused = useIsFocused();
@ -139,13 +139,11 @@ const ScanQRCode = () => {
const data = decoder.toString();
decoder = false; // nullify for future use (?)
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
} else {
setUrTotal(100);
setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100));
@ -192,13 +190,11 @@ const ScanQRCode = () => {
data = Buffer.from(payload, 'hex').toString();
}
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
} else {
setAnimatedQRCodeData(animatedQRCodeData);
}
@ -259,13 +255,12 @@ const ScanQRCode = () => {
bitcoin.Psbt.fromHex(hex); // if it doesnt throw - all good
const data = Buffer.from(hex, 'hex').toString('base64');
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
}
onBarScanned && onBarScanned({ data });
return;
} catch (_) {}
@ -273,13 +268,12 @@ const ScanQRCode = () => {
setIsLoading(true);
try {
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: { scannedData: ret.data }, merge });
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge });
} else {
onBarScanned && onBarScanned(ret.data);
}
onBarScanned && onBarScanned(ret.data);
} catch (e) {
console.log(e);
}
@ -297,42 +291,11 @@ const ScanQRCode = () => {
const showImagePicker = () => {
if (!isLoading) {
setIsLoading(true);
launchImageLibrary(
{
title: null,
mediaType: 'photo',
takePhotoButtonTitle: null,
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
},
response => {
if (response.didCancel) {
setIsLoading(false);
} else {
const asset = response.assets[0];
if (asset.uri) {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
onBarCodeRead({ data: result.values[0] });
}
})
.catch(error => {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
})
.finally(() => {
setIsLoading(false);
});
} else {
setIsLoading(false);
}
}
},
);
fs.showImagePickerAndReadImage()
.then(data => {
if (data) onBarCodeRead({ data });
})
.finally(() => setIsLoading(false));
}
};

View File

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { RouteProp, StackActions, useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -39,7 +39,6 @@ import Button from '../../components/Button';
import CoinsSelected from '../../components/CoinsSelected';
import InputAccessoryAllFunds, { InputAccessoryAllFundsAccessoryViewID } from '../../components/InputAccessoryAllFunds';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
@ -56,7 +55,7 @@ import { useKeyboard } from '../../hooks/useKeyboard';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import ActionSheet from '../ActionSheet';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { CommonToolTipActions, ToolTipAction } from '../../typings/CommonToolTipActions';
import { Action } from '../../components/types';
interface IPaymentDestinations {
@ -79,6 +78,7 @@ type RouteProps = RouteProp<SendDetailsStackParamList, 'SendDetails'>;
const SendDetails = () => {
const { wallets, setSelectedWalletID, sleep, txMetadata, saveToDisk } = useStorage();
const navigation = useExtendedNavigation<NavigationProps>();
const selectedDataProcessor = useRef<ToolTipAction | undefined>();
const setParams = navigation.setParams;
const route = useRoute<RouteProps>();
const name = route.name;
@ -93,7 +93,6 @@ const SendDetails = () => {
const scrollView = useRef<FlatList<any>>(null);
const scrollIndex = useRef(0);
const { colors } = useTheme();
const popAction = StackActions.pop(1);
// state
const [width, setWidth] = useState(Dimensions.get('window').width);
@ -665,34 +664,35 @@ const SendDetails = () => {
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
}
const data = await scanQrHelper(route.name, true);
importQrTransactionOnBarScanned(data);
navigateToQRCodeScanner();
};
const importQrTransactionOnBarScanned = (ret: any) => {
navigation.getParent()?.getParent()?.dispatch(popAction);
if (!wallet) return;
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
const importQrTransactionOnBarScanned = useCallback(
(ret: any) => {
if (!wallet) return;
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
// we construct PSBT object and pass to next screen
// so user can do smth with it:
const psbt = bitcoin.Psbt.fromBase64(ret.data);
// we construct PSBT object and pass to next screen
// so user can do smth with it:
const psbt = bitcoin.Psbt.fromBase64(ret.data);
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
walletID: wallet.getID(),
psbt,
});
setIsLoading(false);
}
};
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
walletID: wallet.getID(),
psbt,
});
setIsLoading(false);
}
},
[navigation, transactionMemo, wallet],
);
/**
* watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
@ -776,53 +776,126 @@ const SendDetails = () => {
});
};
const _importTransactionMultisig = async (base64arg: string | false) => {
try {
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
const _importTransactionMultisig = useCallback(
async (base64arg: string | false) => {
try {
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
setIsLoading(true);
await sleep(100);
(wallet as MultisigHDWallet).cosignPsbt(psbt);
setIsLoading(false);
await sleep(100);
}
if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
setIsLoading(true);
await sleep(100);
(wallet as MultisigHDWallet).cosignPsbt(psbt);
setIsLoading(false);
await sleep(100);
}
if (wallet) {
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
walletID: wallet.getID(),
});
if (wallet) {
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
walletID: wallet.getID(),
});
}
} catch (error: any) {
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
}
} catch (error: any) {
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
}
setIsLoading(false);
};
setIsLoading(false);
},
[navigation, sleep, transactionMemo, wallet],
);
const importTransactionMultisig = () => {
return _importTransactionMultisig(false);
};
const onBarScanned = (ret: any) => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
return _importTransactionMultisig(ret.data);
}
};
const onBarScanned = useCallback(
(ret: any) => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
// psbt base64?
return _importTransactionMultisig(ret.data);
}
},
[_importTransactionMultisig],
);
const importTransactionMultisigScanQr = async () => {
const data = await scanQrHelper(route.name, true);
onBarScanned(data);
const handlePsbtSign = useCallback(
async (psbtBase64: string) => {
let tx;
let psbt;
try {
psbt = bitcoin.Psbt.fromBase64(psbtBase64);
tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx;
} catch (e: any) {
presentAlert({ title: loc.errors.error, message: e.message });
return;
} finally {
setIsLoading(false);
}
if (!tx || !wallet) return setIsLoading(false);
// we need to remove change address from recipients, so that Confirm screen show more accurate info
const changeAddresses: string[] = [];
// @ts-ignore hacky
for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) {
// @ts-ignore hacky
changeAddresses.push(wallet._getInternalAddressByIndex(c));
}
const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address)));
navigation.navigate('CreateTransaction', {
fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(),
feeSatoshi: psbt.getFee(),
wallet,
tx: tx.toHex(),
recipients,
satoshiPerByte: psbt.getFeeRate(),
showAnimatedQr: true,
psbt,
});
},
[navigation, wallet],
);
useEffect(() => {
const data = routeParams.onBarScanned;
if (data) {
if (selectedDataProcessor.current) {
if (
selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR ||
selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction ||
selectedDataProcessor.current === CommonToolTipActions.SignPSBT
) {
if (selectedDataProcessor.current === CommonToolTipActions.ImportTransactionQR) {
importQrTransactionOnBarScanned(data);
} else if (
selectedDataProcessor.current === CommonToolTipActions.CoSignTransaction ||
selectedDataProcessor.current === CommonToolTipActions.SignPSBT
) {
handlePsbtSign(data);
} else {
onBarScanned(data);
}
} else {
console.log('Unknown selectedDataProcessor:', selectedDataProcessor.current);
}
}
setParams({ onBarScanned: undefined });
}
}, [handlePsbtSign, importQrTransactionOnBarScanned, onBarScanned, routeParams.onBarScanned, setParams]);
const navigateToQRCodeScanner = () => {
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
};
const handleAddRecipient = () => {
@ -891,48 +964,6 @@ const SendDetails = () => {
navigation.navigate('PaymentCodeList', { walletID: wallet.getID() });
};
const handlePsbtSign = async () => {
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations
const scannedData = await scanQrHelper(name, true, undefined);
if (!scannedData) return setIsLoading(false);
let tx;
let psbt;
try {
psbt = bitcoin.Psbt.fromBase64(scannedData);
tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx;
} catch (e: any) {
presentAlert({ title: loc.errors.error, message: e.message });
return;
} finally {
setIsLoading(false);
}
if (!tx || !wallet) return setIsLoading(false);
// we need to remove change address from recipients, so that Confirm screen show more accurate info
const changeAddresses: string[] = [];
// @ts-ignore hacky
for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) {
// @ts-ignore hacky
changeAddresses.push(wallet._getInternalAddressByIndex(c));
}
const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address)));
navigation.navigate('CreateTransaction', {
fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(),
feeSatoshi: psbt.getFee(),
wallet,
tx: tx.toHex(),
recipients,
satoshiPerByte: psbt.getFeeRate(),
showAnimatedQr: true,
psbt,
});
};
// Header Right Button
const headerRightOnPress = (id: string) => {
@ -941,7 +972,8 @@ const SendDetails = () => {
} else if (id === CommonToolTipActions.RemoveRecipient.id) {
handleRemoveRecipient();
} else if (id === CommonToolTipActions.SignPSBT.id) {
handlePsbtSign();
selectedDataProcessor.current = CommonToolTipActions.SignPSBT;
navigateToQRCodeScanner();
} else if (id === CommonToolTipActions.SendMax.id) {
onUseAllPressed();
} else if (id === CommonToolTipActions.AllowRBF.id) {
@ -949,11 +981,13 @@ const SendDetails = () => {
} else if (id === CommonToolTipActions.ImportTransaction.id) {
importTransaction();
} else if (id === CommonToolTipActions.ImportTransactionQR.id) {
selectedDataProcessor.current = CommonToolTipActions.ImportTransactionQR;
importQrTransaction();
} else if (id === CommonToolTipActions.ImportTransactionMultsig.id) {
importTransactionMultisig();
} else if (id === CommonToolTipActions.CoSignTransaction.id) {
importTransactionMultisigScanQr();
selectedDataProcessor.current = CommonToolTipActions.CoSignTransaction;
navigateToQRCodeScanner();
} else if (id === CommonToolTipActions.CoinControl.id) {
handleCoinControl();
} else if (id === CommonToolTipActions.InsertContact.id) {

View File

@ -1,6 +1,6 @@
import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native';
import { BlueSpacing20 } from '../../BlueComponents';
@ -10,15 +10,14 @@ import SafeArea from '../../components/SafeArea';
import SaveFileButton from '../../components/SaveFileButton';
import { SquareButton } from '../../components/SquareButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
const PsbtMultisigQRCode = () => {
const { navigate } = useNavigation();
const { colors } = useTheme();
const openScannerButton = useRef();
const { psbtBase64, isShowOpenScanner } = useRoute().params;
const { name } = useRoute();
const { params } = useRoute();
const { psbtBase64, isShowOpenScanner } = params;
const [isLoading, setIsLoading] = useState(false);
const dynamicQRCode = useRef();
const isFocused = useIsFocused();
@ -45,23 +44,34 @@ const PsbtMultisigQRCode = () => {
}
}, [isFocused]);
const onBarScanned = ret => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
presentAlert({ message: loc.wallets.import_error });
} else {
// psbt base64?
navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true });
}
};
const onBarScanned = useCallback(
ret => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
presentAlert({ message: loc.wallets.import_error });
} else {
// psbt base64?
navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true });
}
},
[navigate],
);
const openScanner = async () => {
const scanned = await scanQrHelper(name, true);
onBarScanned({ data: scanned });
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned({ data });
}
}, [onBarScanned, params.onBarScanned]);
const openScanner = () => {
navigate('ScanQRCode', {
showFileImportButton: true,
});
};
const saveFileButtonBeforeOnPress = () => {

View File

@ -1,7 +1,7 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Linking, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
@ -14,7 +14,6 @@ import { DynamicQRCode } from '../../components/DynamicQRCode';
import SaveFileButton from '../../components/SaveFileButton';
import { SecondButton } from '../../components/SecondButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
@ -62,34 +61,40 @@ const PsbtWithHardwareWallet = () => {
},
});
const _combinePSBT = receivedPSBT => {
return wallet.combinePsbt(psbt, receivedPSBT);
};
const _combinePSBT = useCallback(
receivedPSBT => {
return wallet.combinePsbt(psbt, receivedPSBT);
},
[psbt, wallet],
);
const onBarScanned = ret => {
if (ret && !ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
}
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
setTxHex(ret.data);
return;
}
try {
const Tx = _combinePSBT(ret.data);
setTxHex(Tx.toHex());
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigation.navigate({ name: launchedBy, params: { psbt }, merge: true });
// ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT()
// (passed by reference)
const onBarScanned = useCallback(
ret => {
if (ret && !ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
}
} catch (Err) {
presentAlert({ message: Err.message });
}
};
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
setTxHex(ret.data);
return;
}
try {
const Tx = _combinePSBT(ret.data);
setTxHex(Tx.toHex());
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigation.navigate({ name: launchedBy, params: { psbt }, merge: true });
// ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT()
// (passed by reference)
}
} catch (Err) {
presentAlert({ message: Err.message });
}
},
[_combinePSBT, launchedBy, navigation, psbt],
);
useEffect(() => {
if (isFocused) {
@ -217,11 +222,18 @@ const PsbtWithHardwareWallet = () => {
}
};
const openScanner = async () => {
const data = await scanQrHelper(route.name, true);
useEffect(() => {
const data = route.params.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, onBarScanned, route.params.onBarScanned]);
const openScanner = async () => {
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
};
if (txHex) return _renderBroadcastHex();

View File

@ -6,7 +6,6 @@ import { BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComp
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import {
DoneAndDismissKeyboardInputAccessory,
@ -42,8 +41,9 @@ const DELETE_PREFIX = 'delete_';
const ElectrumSettings: React.FC = () => {
const { colors } = useTheme();
const { server } = useRoute<RouteProps>().params;
const { setOptions } = useExtendedNavigation();
const params = useRoute<RouteProps>().params;
const { server } = params;
const navigation = useExtendedNavigation();
const [isLoading, setIsLoading] = useState(true);
const [serverHistory, setServerHistory] = useState<Set<ElectrumServerItem>>(new Set());
const [config, setConfig] = useState<{ connected?: number; host?: string; port?: string }>({});
@ -423,10 +423,10 @@ const ElectrumSettings: React.FC = () => {
);
useEffect(() => {
setOptions({
navigation.setOptions({
headerRight: isElectrumDisabled ? null : () => HeaderRight,
});
}, [HeaderRight, isElectrumDisabled, setOptions]);
}, [HeaderRight, isElectrumDisabled, navigation]);
const checkServer = async () => {
setIsLoading(true);
@ -458,12 +458,17 @@ const ElectrumSettings: React.FC = () => {
};
const importScan = async () => {
const scanned = await scanQrHelper('ElectrumSettings', true);
if (scanned) {
onBarScanned(scanned);
}
navigation.navigate('ScanQRCode');
};
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, params.onBarScanned]);
const onSSLPortChange = (value: boolean) => {
Keyboard.dismiss();
if (value) {

View File

@ -4,7 +4,6 @@ import { Keyboard, StyleSheet, TextInput, View, ScrollView, TouchableOpacity, Te
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { TWallet } from '../../class/wallets/types';
@ -15,6 +14,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import { Divider } from '@rneui/themed';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import presentAlert from '../../components/Alert';
import { navigate } from '../../NavigationService';
type RouteProps = RouteProp<DetailViewStackParamList, 'IsItMyAddress'>;
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'IsItMyAddress'>;
@ -109,11 +109,16 @@ const IsItMyAddress: React.FC = () => {
};
const importScan = async () => {
const data = await scanQrHelper(route.name, true, undefined, true);
navigate('ScanQRCode');
};
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
};
}, [navigation, route.name, route.params?.onBarScanned]);
const viewQRCode = () => {
if (!resultCleanAddress) return;

View File

@ -9,11 +9,12 @@ import { LightningCustodianWallet } from '../../class/wallets/lightning-custodia
import presentAlert, { AlertType } from '../../components/Alert';
import { Button } from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency';
import { clearLNDHub, getLNDHub, setLNDHub } from '../../helpers/lndHub';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const styles = StyleSheet.create({
uri: {
@ -38,21 +39,14 @@ const styles = StyleSheet.create({
},
});
type LightingSettingsRouteProps = RouteProp<
{
params?: {
url?: string;
};
},
'params'
>;
type LightingSettingsRouteProps = RouteProp<DetailViewStackParamList, 'LightningSettings'>;
const LightningSettings: React.FC = () => {
const params = useRoute<LightingSettingsRouteProps>().params;
const [isLoading, setIsLoading] = useState(true);
const [URI, setURI] = useState<string>();
const { colors } = useTheme();
const route = useRoute();
const { navigate, setParams } = useExtendedNavigation();
const styleHook = StyleSheet.create({
uri: {
borderColor: colors.formBorder,
@ -106,38 +100,43 @@ const LightningSettings: React.FC = () => {
setURI(typeof setLndHubUrl === 'string' ? setLndHubUrl.trim() : value.trim());
};
const save = useCallback(async () => {
setIsLoading(true);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (URI) {
const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString();
const save = useCallback(async () => {
setIsLoading(true);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (URI) {
const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString();
await LightningCustodianWallet.isValidNodeAddress(normalizedURI);
await LightningCustodianWallet.isValidNodeAddress(normalizedURI);
await setLNDHub(normalizedURI);
} else {
await clearLNDHub();
}
presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast });
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.lightning_error_lndhub_uri });
console.log(error);
await setLNDHub(normalizedURI);
} else {
await clearLNDHub();
}
setIsLoading(false);
}, [URI]);
presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast });
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.lightning_error_lndhub_uri });
console.log(error);
}
setIsLoading(false);
}, [URI]);
const importScan = () => {
scanQrHelper(route.name).then(data => {
if (data) {
setLndhubURI(data);
}
navigate('ScanQRCode', {
showFileImportButton: true,
});
};
useEffect(() => {
const data = params?.onBarScanned;
if (data) {
setLndhubURI(data);
setParams({ onBarScanned: undefined });
}
}, [params?.onBarScanned, setParams]);
return (
<ScrollView automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<BlueCard>

View File

@ -11,7 +11,6 @@ import {
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useKeyboard } from '../../hooks/useKeyboard';
@ -30,7 +29,6 @@ const ImportWallet = () => {
const route = useRoute<RouteProps>();
const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? '';
const [importText, setImportText] = useState<string>(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState<boolean>(false);
const [, setSpeedBackdoor] = useState<number>(0);
@ -108,11 +106,18 @@ const ImportWallet = () => {
);
const importScan = useCallback(async () => {
const data = await scanQrHelper(route.name, true);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation]);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
}, [route.name, onBarScanned]);
}, [route.name, onBarScanned, route.params?.onBarScanned, navigation]);
const speedBackdoorTap = () => {
setSpeedBackdoor(v => {
@ -162,12 +167,6 @@ const ImportWallet = () => {
if (triggerImport) handleImport();
}, [triggerImport, handleImport]);
useEffect(() => {
if (scannedData) {
onBarScanned(scannedData);
}
}, [scannedData, onBarScanned]);
// Adding the ToolTipMenu to the header
useEffect(() => {
navigation.setOptions({

View File

@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommonActions, RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import {
ActivityIndicator,
Alert,
@ -38,7 +38,6 @@ import QRCodeComponent from '../../components/QRCodeComponent';
import SquareEnumeratedWords, { SquareEnumeratedWordsContentAlign } from '../../components/SquareEnumeratedWords';
import { useTheme } from '../../components/themes';
import prompt from '../../helpers/prompt';
import { scanQrHelper } from '../../helpers/scan-qr';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { disallowScreenshot } from 'react-native-screen-capture';
@ -48,6 +47,13 @@ import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { navigationRef } from '../../NavigationService';
import SafeArea from '../../components/SafeArea';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false);
@ -55,10 +61,10 @@ const ViewEditMultisigCosigners: React.FC = () => {
const { wallets, setWalletsWithNewOrder } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation();
const { navigate, dispatch, addListener, setParams } = useExtendedNavigation<NavigationProp>();
const openScannerButtonRef = useRef();
const route = useRoute();
const { walletID } = route.params as { walletID: string };
const route = useRoute<RouteParams>();
const { walletID } = route.params;
const w = useRef(wallets.find(wallet => wallet.getID() === walletID));
const tempWallet = useRef(new MultisigHDWallet());
const [wallet, setWallet] = useState<MultisigHDWallet>();
@ -73,6 +79,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', passphrase: '', path: '', fp: '', isLoading: false }); // string rendered in modal
const [isVaultKeyIndexDataLoading, setIsVaultKeyIndexDataLoading] = useState<number | undefined>(undefined);
const [askPassphrase, setAskPassphrase] = useState(false);
const data = useRef<any[]>();
/* discardChangesRef is only so the action sheet can be shown on mac catalyst when a
@ -183,7 +190,8 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsSaveButtonDisabled(true);
setTimeout(() => {
setWalletsWithNewOrder(newWallets);
navigate('WalletsList');
// dismiss this modal
navigationRef.dispatch(CommonActions.navigate({ name: 'WalletsList' }));
}, 500);
};
useFocusEffect(
@ -269,6 +277,24 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
};
const resetModalData = () => {
setVaultKeyData({
keyIndex: 1,
xpub: '',
seed: '',
passphrase: '',
path: '',
fp: '',
isLoading: false,
});
setImportText('');
setExportString('{}');
setExportStringURv2('');
setExportFilename('');
setIsSaveButtonDisabled(false);
setAskPassphrase(false);
};
const _renderKeyItem = (el: ListRenderItemInfo<any>) => {
if (!wallet) {
// failsafe
@ -306,29 +332,34 @@ const ViewEditMultisigCosigners: React.FC = () => {
buttonType: MultipleStepsListItemButtohType.partial,
leftText,
text: loc.multisig.view,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
disabled: vaultKeyData.isLoading,
onPress: () => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -357,31 +388,36 @@ const ViewEditMultisigCosigners: React.FC = () => {
leftText,
text: loc.multisig.view,
disabled: vaultKeyData.isLoading,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
buttonType: MultipleStepsListItemButtohType.partial,
onPress: () => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
mnemonicsModalRef.current?.present();
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -436,6 +472,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
await provideMnemonicsModalRef.current?.dismiss();
await shareModalRef.current?.dismiss();
await mnemonicsModalRef.current?.dismiss();
resetModalData();
};
const handleUseMnemonicPhrase = async () => {
let passphrase;
@ -472,9 +509,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
provideMnemonicsModalRef.current?.dismiss();
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const xpubInsteadOfSeed = (index: number): Promise<void> => {
@ -496,16 +531,22 @@ const ViewEditMultisigCosigners: React.FC = () => {
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current?.dismiss();
const scanned = await scanQrHelper(route.name, true, undefined);
setImportText(String(scanned));
provideMnemonicsModalRef.current?.present();
navigate('ScanQRCode', { showFileImportButton: true });
};
useEffect(() => {
const scannedData = route.params.onBarScanned;
if (scannedData) {
setImportText(String(scannedData));
setParams({ onBarScanned: undefined });
provideMnemonicsModalRef.current?.present();
}
}, [route.params.onBarScanned, setParams]);
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
provideMnemonicsModalRef.current?.dismiss();
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const hideShareModal = () => {};
@ -570,13 +611,15 @@ const ViewEditMultisigCosigners: React.FC = () => {
backgroundColor={colors.elevated}
shareContent={{ fileName: exportFilename, fileContent: exportString }}
>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<BlueSpacing20 />
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
</View>
<SafeArea>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<BlueSpacing20 />
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
</View>
</SafeArea>
</BottomModal>
);
};

View File

@ -1,4 +1,4 @@
import { useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
@ -26,7 +26,6 @@ import { FButton, FContainer } from '../../components/FloatButtons';
import { useTheme } from '../../components/themes';
import { TransactionListItem } from '../../components/TransactionListItem';
import TransactionsNavigationHeader, { actionKeys } from '../../components/TransactionsNavigationHeader';
import { scanQrHelper } from '../../helpers/scan-qr';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
@ -53,14 +52,15 @@ const buttonFontSize =
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const [isLoading, setIsLoading] = useState(false);
const { walletID } = route.params;
const { name } = useRoute();
const { params, name } = useRoute<RouteProps>();
const { walletID } = params;
const wallet = useMemo(() => wallets.find(w => w.getID() === walletID), [walletID, wallets]);
const [limit, setLimit] = useState(15);
const [pageSize] = useState(20);
@ -85,6 +85,33 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
}, [route, setOptions]),
);
const onBarCodeRead = useCallback(
(ret?: { data?: any }) => {
if (!isLoading) {
setIsLoading(true);
const parameters = {
walletID,
uri: ret?.data ? ret.data : ret,
};
if (wallet?.chain === Chain.ONCHAIN) {
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
} else {
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: parameters });
}
setIsLoading(false);
}
},
[isLoading, walletID, wallet?.chain, navigate],
);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
onBarCodeRead({ data });
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, onBarCodeRead, route.params]);
const getTransactions = useCallback(
(lmt = Infinity): Transaction[] => {
if (!wallet) return [];
@ -259,25 +286,6 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
<TransactionListItem item={item.item} itemPriceUnit={wallet?.preferredBalanceUnit} walletID={walletID} />
);
const onBarCodeRead = useCallback(
(ret?: { data?: any }) => {
if (!isLoading) {
setIsLoading(true);
const params = {
walletID,
uri: ret?.data ? ret.data : ret,
};
if (wallet?.chain === Chain.ONCHAIN) {
navigate('SendDetailsRoot', { screen: 'SendDetails', params });
} else {
navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params });
}
setIsLoading(false);
}
},
[isLoading, walletID, wallet?.chain, navigate],
);
const choosePhoto = () => {
fs.showImagePickerAndReadImage()
.then(data => {
@ -351,10 +359,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
break;
}
case 2: {
const data = await scanQrHelper(name, true);
if (data) {
onBarCodeRead({ data });
}
navigate('ScanQRCode', {
showImportFileButton: true,
});
break;
}
case 3:

View File

@ -13,7 +13,6 @@ import { FButton, FContainer } from '../../components/FloatButtons';
import { useTheme } from '../../components/themes';
import { TransactionListItem } from '../../components/TransactionListItem';
import WalletsCarousel from '../../components/WalletsCarousel';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useIsLargeScreen } from '../../hooks/useIsLargeScreen';
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
@ -104,10 +103,9 @@ const WalletsList: React.FC = () => {
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions();
const { colors, scanImage } = useTheme();
const { navigate } = useExtendedNavigation<NavigationProps>();
const navigation = useExtendedNavigation<NavigationProps>();
const isFocused = useIsFocused();
const route = useRoute<RouteProps>();
const routeName = route.name;
const dataSource = getTransactions(undefined, 10);
const walletsCount = useRef<number>(wallets.length);
const walletActionButtonsRef = useRef<any>();
@ -179,13 +177,25 @@ const WalletsList: React.FC = () => {
walletsCount.current = wallets.length;
}, [wallets]);
const onBarScanned = useCallback(
(value: any) => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
// @ts-ignore: for now
navigation.navigate(...completionValue);
});
},
[navigation],
);
useEffect(() => {
const scannedData = route.params?.scannedData;
if (scannedData) {
onBarScanned(scannedData);
const data = route.params?.onBarScanned;
if (data) {
onBarScanned(data);
navigation.setParams({ onBarScanned: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [route.params?.scannedData]);
}, [navigation, onBarScanned, route.params?.onBarScanned]);
useEffect(() => {
refreshTransactions(false, true);
@ -196,15 +206,15 @@ const WalletsList: React.FC = () => {
(item?: TWallet) => {
if (item?.getID) {
const walletID = item.getID();
navigate('WalletTransactions', {
navigation.navigate('WalletTransactions', {
walletID,
walletType: item.type,
});
} else {
navigate('AddWalletRoot');
navigation.navigate('AddWalletRoot');
}
},
[navigate],
[navigation],
);
const setIsLoading = useCallback((value: boolean) => {
@ -240,8 +250,8 @@ const WalletsList: React.FC = () => {
}, [stylesHook.listHeaderBack, stylesHook.listHeaderText]);
const handleLongPress = useCallback(() => {
navigate('ManageWallets');
}, [navigate]);
navigation.navigate('ManageWallets');
}, [navigation]);
const renderTransactionListsRow = useCallback(
(item: ExtendedTransaction) => (
@ -349,20 +359,10 @@ const WalletsList: React.FC = () => {
};
const onScanButtonPressed = useCallback(() => {
scanQrHelper(routeName, true, undefined, false);
}, [routeName]);
const onBarScanned = useCallback(
(value: any) => {
if (!value) return;
DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
// @ts-ignore: for now
navigate(...completionValue);
});
},
[navigate],
);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation]);
const pasteFromClipboard = useCallback(async () => {
onBarScanned(await getClipboardContent());
@ -397,7 +397,9 @@ const WalletsList: React.FC = () => {
});
break;
case 2:
scanQrHelper(routeName, true, undefined, false);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case 3:
if (!isClipboardEmpty) {
@ -406,7 +408,7 @@ const WalletsList: React.FC = () => {
break;
}
});
}, [pasteFromClipboard, onBarScanned, routeName]);
}, [onBarScanned, navigation, pasteFromClipboard]);
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };

View File

@ -32,7 +32,6 @@ import prompt from '../../helpers/prompt';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
@ -46,10 +45,9 @@ const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage();
const { colors } = useTheme();
const { navigate, navigateToWalletsList } = useExtendedNavigation();
const { m, n, format, walletLabel } = useRoute().params;
const { name } = useRoute();
const { navigate, navigateToWalletsList, setParams } = useExtendedNavigation();
const params = useRoute().params;
const { m, n, format, walletLabel } = params;
const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path]
const [isLoading, setIsLoading] = useState(false);
const mnemonicsModalRef = useRef(null);
@ -203,7 +201,7 @@ const WalletsAddMultisigStep2 = () => {
});
};
const getPath = () => {
const getPath = useCallback(() => {
let path = '';
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
@ -221,7 +219,7 @@ const WalletsAddMultisigStep2 = () => {
throw new Error('This should never happen');
}
return path;
};
}, [format]);
const viewKey = cosigner => {
if (MultisigHDWallet.isXpubValid(cosigner[0])) {
@ -267,52 +265,55 @@ const WalletsAddMultisigStep2 = () => {
provideMnemonicsModalRef.current.present();
};
const tryUsingXpub = async (xpub, fp, path) => {
if (!MultisigHDWallet.isXpubForMultisig(xpub)) {
const tryUsingXpub = useCallback(
async (xpub, fp, path) => {
if (!MultisigHDWallet.isXpubForMultisig(xpub)) {
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
return;
}
if (fp) {
// do nothing, it's already set
} else {
try {
fp = await prompt(loc.multisig.input_fp, loc.multisig.input_fp_explain, true, 'plain-text');
fp = (fp + '').toUpperCase();
if (!MultisigHDWallet.isFpValid(fp)) fp = '00000000';
} catch (e) {
return setIsLoading(false);
}
}
if (path) {
// do nothing, it's already set
} else {
try {
path = await prompt(
loc.multisig.input_path,
loc.formatString(loc.multisig.input_path_explain, { default: getPath() }),
true,
'plain-text',
);
if (!MultisigHDWallet.isPathValid(path)) path = getPath();
} catch {
return setIsLoading(false);
}
}
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
return;
}
if (fp) {
// do nothing, it's already set
} else {
try {
fp = await prompt(loc.multisig.input_fp, loc.multisig.input_fp_explain, true, 'plain-text');
fp = (fp + '').toUpperCase();
if (!MultisigHDWallet.isFpValid(fp)) fp = '00000000';
} catch (e) {
return setIsLoading(false);
}
}
if (path) {
// do nothing, it's already set
} else {
try {
path = await prompt(
loc.multisig.input_path,
loc.formatString(loc.multisig.input_path_explain, { default: getPath() }),
true,
'plain-text',
);
if (!MultisigHDWallet.isPathValid(path)) path = getPath();
} catch {
return setIsLoading(false);
}
}
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
const cosignersCopy = [...cosigners];
cosignersCopy.push([xpub, fp, path]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
};
const cosignersCopy = [...cosigners];
cosignersCopy.push([xpub, fp, path]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
},
[cosigners, getPath],
);
const useMnemonicPhrase = async () => {
setIsLoading(true);
@ -371,114 +372,122 @@ const WalletsAddMultisigStep2 = () => {
return hd.validateMnemonic();
};
const onBarScanned = ret => {
if (!ret.data) ret = { data: ret };
const onBarScanned = useCallback(
ret => {
if (!ret.data) ret = { data: ret };
try {
let retData = JSON.parse(ret.data);
if (Array.isArray(retData) && retData.length === 1) {
// UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one,
// so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for
// the code that expects it
retData = retData.pop();
ret.data = JSON.stringify(retData);
}
} catch (_) {}
try {
let retData = JSON.parse(ret.data);
if (Array.isArray(retData) && retData.length === 1) {
// UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one,
// so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for
// the code that expects it
retData = retData.pop();
ret.data = JSON.stringify(retData);
}
} catch (_) {}
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (isValidMnemonicSeed(ret.data)) {
setImportText(ret.data);
setTimeout(() => {
provideMnemonicsModalRef.current.present().then(() => {});
}, 100);
} else {
if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) {
return presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
}
if (MultisigHDWallet.isXpubValid(ret.data)) {
return tryUsingXpub(ret.data);
}
let cosigner = new MultisigCosigner(ret.data);
if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner });
provideMnemonicsModalRef.current.dismiss();
if (cosigner.howManyCosignersWeHave() > 1) {
// lets look for the correct cosigner. thats probably gona be the one with specific corresponding path,
// for example m/48'/0'/0'/2' if user chose to setup native segwit in BW
for (const cc of cosigner.getAllCosigners()) {
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cc.getPath().startsWith('m/45')) {
// found it
cosigner = cc;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (isValidMnemonicSeed(ret.data)) {
setImportText(ret.data);
setTimeout(() => {
provideMnemonicsModalRef.current.present().then(() => {});
}, 100);
} else {
if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) {
return presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
}
if (MultisigHDWallet.isXpubValid(ret.data)) {
return tryUsingXpub(ret.data);
}
let cosigner = new MultisigCosigner(ret.data);
if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner });
provideMnemonicsModalRef.current.dismiss();
if (cosigner.howManyCosignersWeHave() > 1) {
// lets look for the correct cosigner. thats probably gona be the one with specific corresponding path,
// for example m/48'/0'/0'/2' if user chose to setup native segwit in BW
for (const cc of cosigner.getAllCosigners()) {
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cc.getPath().startsWith('m/45')) {
// found it
cosigner = cc;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
}
}
for (const existingCosigner of cosigners) {
if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported });
}
// now, validating that cosigner is in correct format:
let correctFormat = false;
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cosigner.getPath().startsWith('m/45')) {
correctFormat = true;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) });
const cosignersCopy = [...cosigners];
cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
}
for (const existingCosigner of cosigners) {
if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported });
}
// now, validating that cosigner is in correct format:
let correctFormat = false;
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cosigner.getPath().startsWith('m/45')) {
correctFormat = true;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) });
const cosignersCopy = [...cosigners];
cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
}
};
},
[cosigners, format, tryUsingXpub],
);
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
const scanned = await scanQrHelper(name, true);
if (scanned) {
onBarScanned(scanned);
}
navigate('ScanQRCode');
};
useEffect(() => {
const scannedData = params.onBarScanned;
if (scannedData) {
onBarScanned(scannedData);
setParams({ onBarScanned: undefined });
}
}, [onBarScanned, params.onBarScanned, setParams]);
const dashType = ({ index, lastIndex, isChecked, isFocus }) => {
if (isChecked) {
if (index === lastIndex) {

View File

@ -2005,6 +2005,20 @@ describe('multisig-wallet (native segwit)', () => {
assert.strictEqual(w._getInternalAddressByIndex(0), w3coordinator._getInternalAddressByIndex(0));
});
it('can import wallet descriptor from Casa multisig', () => {
const w = new MultisigHDWallet();
w.setSecret(
'sh(wsh(sortedmulti(3,[35282ae3]Ypub6j9gr7f7uvWfTHdwVwhYRowZH9oqkh9DZz2hdbeKJw7yjQbsfqaruLva2piTZqN7jSKT43LpsnAqHU4vufUGAyGBzD6UJpwTeKiG5JRYUnm/<0;1>/*,[1b373e31/49/0/5]Ypub6iPAfqqinG5VHf2sJ3XfRjFh8BXm4C6x4U3DaPyu4RKocnqzb1peStw8Q624jwXpMZ5G81s7uwq8qLQ8rW55aZ3LvBbr9XVLTYxG78k28PE/<0;1>/*,[0d7bb846/49/0/5]Ypub6hwdyxEzktht47xgyyFY7TFrAzW7XgGbMSqS3QzfosYekG9LibhpohY6LrsjfP55VyU8i7iWS2s2Vs6RkAeze6bSpU3rNHsmESymE8X3J3k/<0;1>/*,[faa45c74/49/0/5]Ypub6iSiDkVrTTEh6eFbkCvohn1FfuhGr1Co9MCCHi6VPTT6crnX9Avq3PnZj7JcqhDTkWoDLEyeWmaPyxDY22b7k652Sg2eMc2g5tU6GDTvSng/<0;1>/*,[0551354e/49/0/5]Ypub6ipvk7JbDUXormRsSxXRf9eTUAVFwcjDfZXWYxRdN6nV7MNnnuD3WZBYjjWnMTncHhKJsnfkUGVYMRgQthZqY2wfHfMaJoVYBBhHT7MACGs/<0;1>/*)))',
);
assert.strictEqual(w._getExternalAddressByIndex(0), '3HJiAohE25FFBLPLZGVDwv7ZbSXVsSiZH7');
assert.strictEqual(w._getExternalAddressByIndex(1), '3GBvQK1iHJ9dw7H8datM4REtYdAu9iQjeh');
assert.strictEqual(w._getExternalAddressByIndex(2), '3KrAqZwVwND5XFNEAnCuWyf4nPNAfF2JCF');
assert.strictEqual(w._getInternalAddressByIndex(0), '36oNF12VNk4G5hPRR5zuZixnNjJikkTSWD');
assert.strictEqual(w._getInternalAddressByIndex(1), '3Q7SnCQK9DYPpFviiEB7NQHa1FJqprCWNX');
assert.strictEqual(w._getInternalAddressByIndex(2), '33Rkb8XURWkxZBN6dpdk64qa4raK2bRFJS');
});
it('can import descriptor from Sparrow', () => {
const payload =
'UR:CRYPTO-OUTPUT/TAADMETAADMSOEADAOAOLSTAADDLOLAOWKAXHDCLAOCEBDFLNNTKJTIOJSFSURBNFXRPEEHKDLGYRTEMRPYTGYZOCASWENCYMKPAVWJKHYAAHDCXJEFTGSZOIMFEYNDYHYZEJTBAMSJEHLDSRDDIYLSRFYTSZTKNRNYLRNDPAMTLDPZCAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYUOHFJPKOAXAAAYCYCSYASAVDTAADDLOLAOWKAXHDCLAXMSZTWZDIGERYDKFSFWTYDPFNDKLNAYSWTTMUHYZTOXHSETPEWSFXPEAYWLJSDEMTAAHDCXSPLTSTDPNTLESANSUTTLPRPFHNVSPFCNMHESOYGASTLRPYVAATNNDKFYHLQZPKLEAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYWZFEPLETAXAAAYCYCPCKRENBTAADDLOLAOWKAXHDCLAOLSFWYKYLKTFHJLPYEMGLCEDPFNSNRDDSRFASEOZTGWIALFLUIYDNFXHGVESFEMMEAAHDCXHTZETLJNKPHHAYLSCXWPNDSWPSTPGTEOJKKGHDAELSKPNNBKBSYAWZJTFWNNBDKTAHTAADEHOEADAEAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYSKTPJPMSAXAAAYCYCEBKWLAMTDWZGRZE\n';