diff --git a/blue_modules/fs.ts b/blue_modules/fs.ts index 569c4ea4e..287a8ac03 100644 --- a/blue_modules/fs.ts +++ b/blue_modules/fs.ts @@ -90,7 +90,7 @@ export const writeFileAndExport = async function (fileName: string, contents: st /** * Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw). */ -export const openSignedTransaction = async function (): Promise { +export const openSignedTransaction = async function (): Promise { try { const res = await DocumentPicker.pickSingle({ type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles], diff --git a/components/WalletButton.tsx b/components/WalletButton.tsx index e3629fb8f..3175d7533 100644 --- a/components/WalletButton.tsx +++ b/components/WalletButton.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import { DimensionValue, I18nManager, Image, ImageSourcePropType, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { + ColorValue, + DimensionValue, + I18nManager, + Image, + ImageSourcePropType, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; import loc from '../loc'; import { Theme, useTheme } from './themes'; @@ -49,14 +59,14 @@ const WalletButton: React.FC = ({ buttonType, testID, onPress const borderColor = active ? colors[details.borderColorActive] : colors.buttonDisabledBackgroundColor; const dynamicStyles = StyleSheet.create({ buttonContainer: { - borderColor, + borderColor: borderColor as ColorValue, backgroundColor: colors.buttonDisabledBackgroundColor, minWidth: size.width, minHeight: size.height, height: size.height, }, textTitle: { - color: colors[details.borderColorActive], + color: colors[details.borderColorActive] as ColorValue, }, textExplain: { color: colors.alternativeTextColor, diff --git a/components/themes.ts b/components/themes.ts index 6b622b2d8..bb91e61bc 100644 --- a/components/themes.ts +++ b/components/themes.ts @@ -8,6 +8,7 @@ export const BlueDefaultTheme = { scanImage: require('../img/scan.png'), colors: { ...DefaultTheme.colors, + borderWidth: 0.5, brandingColor: '#ffffff', customHeader: '#ffffff', foregroundColor: '#0c2550', diff --git a/hooks/useExtendedNavigation.ts b/hooks/useExtendedNavigation.ts index 885f84bac..7da7f4d19 100644 --- a/hooks/useExtendedNavigation.ts +++ b/hooks/useExtendedNavigation.ts @@ -5,15 +5,13 @@ import { presentWalletExportReminder } from '../helpers/presentWalletExportRemin import { useBiometrics } from './useBiometrics'; // List of screens that require biometrics - const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot']; // List of screens that require wallet export to be saved - const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses']; -export const useExtendedNavigation = (): NavigationProp => { - const originalNavigation = useNavigation>(); +export const useExtendedNavigation = >(): T => { + const originalNavigation = useNavigation(); const { wallets, saveToDisk } = useStorage(); const { isBiometricUseEnabled, unlockWithBiometrics } = useBiometrics(); @@ -95,3 +93,7 @@ export const useExtendedNavigation = (): NavigationProp => { navigate: enhancedNavigate, }; }; + +// Usage example: +// type NavigationProps = NativeStackNavigationProp; +// const navigation = useExtendedNavigation(); diff --git a/models/networkTransactionFees.ts b/models/networkTransactionFees.ts index c7325ec2d..54898cc9b 100644 --- a/models/networkTransactionFees.ts +++ b/models/networkTransactionFees.ts @@ -10,9 +10,9 @@ export const NetworkTransactionFeeType = Object.freeze({ export class NetworkTransactionFee { static StorageKey = 'NetworkTransactionFee'; - private fastestFee: number; - private mediumFee: number; - private slowFee: number; + public fastestFee: number; + public mediumFee: number; + public slowFee: number; constructor(fastestFee = 2, mediumFee = 1, slowFee = 1) { this.fastestFee = fastestFee; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 4f1a6be26..575c7ed40 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -1,5 +1,5 @@ import { StackActions } from '@react-navigation/native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { NativeStackNavigationOptions, createNativeStackNavigator } from '@react-navigation/native-stack'; import React, { useMemo } from 'react'; import { I18nManager, Platform, TouchableOpacity } from 'react-native'; import { Icon } from 'react-native-elements'; @@ -67,8 +67,9 @@ import SignVerifyStackRoot from './SignVerifyStack'; import ViewEditMultisigCosignersStackRoot from './ViewEditMultisigCosignersStack'; import WalletExportStack from './WalletExportStack'; import WalletXpubStackRoot from './WalletXpubStack'; +import { DetailViewStackParamList } from './DetailViewStackParamList'; -const DetailViewRoot = createNativeStackNavigator(); +const DetailViewRoot = createNativeStackNavigator(); const DetailViewStackScreensStack = () => { const theme = useTheme(); const navigation = useExtendedNavigation(); @@ -79,7 +80,7 @@ const DetailViewStackScreensStack = () => { const SaveButton = useMemo(() => , []); - const useWalletListScreenOptions = useMemo(() => { + const useWalletListScreenOptions = useMemo(() => { const SettingsButton = ( { options={WalletTransactions.navigationOptions(theme)} /> { options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }} /> - - void; + showFileImportButton?: boolean; + backdoorVisible?: boolean; + animatedQRCodeData?: Record; + }; + PaymentCodeRoot: undefined; + ReorderWallets: undefined; +}; diff --git a/navigation/SendDetailsStack.tsx b/navigation/SendDetailsStack.tsx index 5f1894814..e46157742 100644 --- a/navigation/SendDetailsStack.tsx +++ b/navigation/SendDetailsStack.tsx @@ -15,18 +15,7 @@ import { SendDetailsComponent, SuccessComponent, } from './LazyLoadSendDetailsStack'; - -export type SendDetailsStackParamList = { - SendDetails: { isEditable: boolean }; // Now expects an isEditable boolean - Confirm: undefined; - PsbtWithHardwareWallet: undefined; - CreateTransaction: undefined; - PsbtMultisig: undefined; - PsbtMultisigQRCode: undefined; - Success: undefined; - SelectWallet: undefined; - CoinControl: undefined; -}; +import { SendDetailsStackParamList } from './SendDetailsStackParamList'; const Stack = createNativeStackNavigator(); diff --git a/navigation/SendDetailsStackParamList.ts b/navigation/SendDetailsStackParamList.ts new file mode 100644 index 000000000..77fa1693d --- /dev/null +++ b/navigation/SendDetailsStackParamList.ts @@ -0,0 +1,71 @@ +import { Psbt } from 'bitcoinjs-lib'; +import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../class/wallets/types'; +import { Chain } from '../models/bitcoinUnits'; + +export type SendDetailsStackParamList = { + SendDetails: { isEditable: boolean }; + Confirm: { + fee: number; + memo?: string; + walletID: string; + tx: string; + recipients: CreateTransactionTarget[]; + satoshiPerByte: number; + payjoinUrl?: string | null; + psbt: Psbt; + }; + PsbtWithHardwareWallet: { + memo?: string; + fromWallet: TWallet; + launchedBy?: string; + psbt?: Psbt; + txhex?: string; + }; + CreateTransaction: { + wallet: TWallet; + memo?: string; + psbt?: Psbt; + txhex?: string; + tx: string; + fee: number; + showAnimatedQr?: boolean; + recipients: CreateTransactionTarget[]; + satoshiPerByte: number; + feeSatoshi?: number; + }; + PsbtMultisig: { + memo?: string; + psbtBase64: string; + walletID: string; + launchedBy?: string; + }; + PsbtMultisigQRCode: { + memo?: string; + psbtBase64: string; + fromWallet: string; + launchedBy?: string; + }; + Success: undefined; + SelectWallet: { + onWalletSelect: (wallet: TWallet) => void; + chainType: Chain; + }; + CoinControl: { + walletID: string; + onUTXOChoose: (u: CreateTransactionUtxo[]) => void; + }; + ScanQRCodeRoot: { + screen: string; + params: { + isLoading?: boolean; + cameraStatusGranted?: boolean; + backdoorPressed?: boolean; + launchedBy?: string; + urTotal?: number; + urHave?: number; + backdoorText?: string; + showFileImportButton?: boolean; + onBarScanned: (data: string) => void; + }; + }; +}; diff --git a/screen/send/details.js b/screen/send/details.tsx similarity index 84% rename from screen/send/details.js rename to screen/send/details.tsx index 2418e9894..994988658 100644 --- a/screen/send/details.js +++ b/screen/send/details.tsx @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { StackActions, useFocusEffect, useRoute } from '@react-navigation/native'; import BigNumber from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; @@ -12,6 +12,8 @@ import { Keyboard, KeyboardAvoidingView, LayoutAnimation, + NativeScrollEvent, + NativeSyntheticEvent, Platform, StyleSheet, Text, @@ -47,43 +49,81 @@ import { requestCameraAuthorization, scanQrHelper } from '../../helpers/scan-qr' import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; +import { CreateTransactionUtxo, TWallet } from '../../class/wallets/types'; +import { TOptions } from 'bip21'; +import assert from 'assert'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList'; +import { isTablet } from '../../blue_modules/environment'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; +interface IParams { + memo: string; + address: string; + walletID: string; + amount: number; + amountSats: number; + unit: BitcoinUnit; + noRbf: boolean; + launchedBy: string; + isEditable: boolean; + uri: string; // payjoin uri +} + +interface IPaymentDestinations { + address: string; + amountSats?: number | string; + amount?: string | number | 'MAX'; + key: string; // random id to look up this record +} + +interface IFee { + current: number | null; + slowFee: number | null; + mediumFee: number | null; + fastestFee: number | null; +} +type NavigationProps = NativeStackNavigationProp; + const SendDetails = () => { const { wallets, setSelectedWalletID, sleep, txMetadata, saveToDisk } = useContext(BlueStorageContext); - const navigation = useNavigation(); - const { name, params: routeParams } = useRoute(); - const scrollView = useRef(); + const navigation = useExtendedNavigation(); + const route = useRoute(); + const name = route.name; + const routeParams = route.params as IParams; + const scrollView = useRef>(null); const scrollIndex = useRef(0); const { colors } = useTheme(); + const popAction = StackActions.pop(1); // state const [width, setWidth] = useState(Dimensions.get('window').width); const [isLoading, setIsLoading] = useState(false); - const [wallet, setWallet] = useState(null); + const [wallet, setWallet] = useState(null); const [walletSelectionOrCoinsSelectedHidden, setWalletSelectionOrCoinsSelectedHidden] = useState(false); const [isAmountToolbarVisibleForAndroid, setIsAmountToolbarVisibleForAndroid] = useState(false); const [isFeeSelectionModalVisible, setIsFeeSelectionModalVisible] = useState(false); const [optionsVisible, setOptionsVisible] = useState(false); - const [isTransactionReplaceable, setIsTransactionReplaceable] = useState(false); - const [addresses, setAddresses] = useState([]); - const [units, setUnits] = useState([]); - const [transactionMemo, setTransactionMemo] = useState(''); + const [isTransactionReplaceable, setIsTransactionReplaceable] = useState(false); + const [addresses, setAddresses] = useState([]); + const [units, setUnits] = useState([]); + const [transactionMemo, setTransactionMemo] = useState(''); const [networkTransactionFees, setNetworkTransactionFees] = useState(new NetworkTransactionFee(3, 2, 1)); const [networkTransactionFeesIsLoading, setNetworkTransactionFeesIsLoading] = useState(false); - const [customFee, setCustomFee] = useState(null); - const [feePrecalc, setFeePrecalc] = useState({ current: null, slowFee: null, mediumFee: null, fastestFee: null }); - const [feeUnit, setFeeUnit] = useState(); - const [amountUnit, setAmountUnit] = useState(); - const [utxo, setUtxo] = useState(null); - const [frozenBalance, setFrozenBlance] = useState(false); - const [payjoinUrl, setPayjoinUrl] = useState(null); - const [changeAddress, setChangeAddress] = useState(); + const [customFee, setCustomFee] = useState(null); + const [feePrecalc, setFeePrecalc] = useState({ current: null, slowFee: null, mediumFee: null, fastestFee: null }); + const [feeUnit, setFeeUnit] = useState(); + const [amountUnit, setAmountUnit] = useState(); + const [utxo, setUtxo] = useState(null); + const [frozenBalance, setFrozenBlance] = useState(0); + const [payjoinUrl, setPayjoinUrl] = useState(null); + const [changeAddress, setChangeAddress] = useState(null); const [dumb, setDumb] = useState(false); const { isEditable } = routeParams; // if utxo is limited we use it to calculate available balance - const balance = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : wallet?.getBalance(); + const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : wallet?.getBalance() ?? 0; const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true); // if cutomFee is not set, we need to choose highest possible fee for wallet balance @@ -103,6 +143,7 @@ const SendDetails = () => { }, [customFee, feePrecalc, networkTransactionFees]); useEffect(() => { + console.log('send/details - useEffect'); if (wallet) { setHeaderRightOptions(); } @@ -145,13 +186,13 @@ const SendDetails = () => { if (currentAddress) { currentAddress.address = address; if (Number(amount) > 0) { - currentAddress.amount = amount; - currentAddress.amountSats = btcToSatoshi(amount); + currentAddress.amount = amount!; + currentAddress.amountSats = btcToSatoshi(amount!); } addrs[scrollIndex.current] = currentAddress; return [...addrs]; } else { - return [...addrs, { address, amount, amountSats: btcToSatoshi(amount), key: String(Math.random()) }]; + return [...addrs, { address, amount, amountSats: btcToSatoshi(amount!), key: String(Math.random()) } as IPaymentDestinations]; } }); @@ -183,7 +224,7 @@ const SendDetails = () => { return [...u]; }); } else { - setAddresses([{ address: '', key: String(Math.random()) }]); // key is for the FlatList + setAddresses([{ address: '', key: String(Math.random()) } as IPaymentDestinations]); // key is for the FlatList } // eslint-disable-next-line react-hooks/exhaustive-deps }, [routeParams.uri, routeParams.address]); @@ -207,6 +248,7 @@ const SendDetails = () => { // load cached fees AsyncStorage.getItem(NetworkTransactionFee.StorageKey) .then(res => { + if (!res) return; const fees = JSON.parse(res); if (!fees?.fastestFee) return; setNetworkTransactionFees(fees); @@ -253,7 +295,6 @@ const SendDetails = () => { useEffect(() => { if (!wallet) return; // wait for it const fees = networkTransactionFees; - const change = getChangeAddressFast(); const requestedSatPerByte = Number(feeRate); const lutxo = utxo || wallet.getUtxo(); let frozen = 0; @@ -272,7 +313,7 @@ const SendDetails = () => { { key: 'fastestFee', fee: fees.fastestFee }, ]; - const newFeePrecalc = { ...feePrecalc }; + const newFeePrecalc: /* Record */ IFee = { ...feePrecalc }; for (const opt of options) { let targets = []; @@ -282,8 +323,8 @@ const SendDetails = () => { targets = [{ address: transaction.address }]; break; } - const value = parseInt(transaction.amountSats, 10); - if (value > 0) { + const value = transaction.amountSats; + if (Number(value) > 0) { targets.push({ address: transaction.address, value }); } else if (transaction.amount) { if (btcToSatoshi(transaction.amount) > 0) { @@ -309,11 +350,12 @@ const SendDetails = () => { let flag = false; while (true) { try { - const { fee } = wallet.coinselect(lutxo, targets, opt.fee, change); + const { fee } = wallet.coinselect(lutxo, targets, opt.fee); + // @ts-ignore options& opt are used only to iterate keys we predefined and we know exist newFeePrecalc[opt.key] = fee; break; - } catch (e) { + } catch (e: any) { if (e.message.includes('Not enough') && !flag) { flag = true; // if we don't have enough funds, construct maximum possible transaction @@ -321,6 +363,7 @@ const SendDetails = () => { continue; } + // @ts-ignore options& opt are used only to iterate keys we predefined and we know exist newFeePrecalc[opt.key] = null; break; } @@ -339,34 +382,17 @@ const SendDetails = () => { }, []), ); - const getChangeAddressFast = () => { - if (changeAddress) return changeAddress; // cache - - let change; - if (WatchOnlyWallet.type === wallet.type && !wallet.isHd()) { - // plain watchonly - just get the address - change = wallet.getAddress(); - } else if (WatchOnlyWallet.type === wallet.type || wallet instanceof AbstractHDElectrumWallet) { - change = wallet._getInternalAddressByIndex(wallet.getNextFreeChangeAddressIndex()); - } else { - // legacy wallets - change = wallet.getAddress(); - } - - return change; - }; - const getChangeAddressAsync = async () => { if (changeAddress) return changeAddress; // cache let change; - if (WatchOnlyWallet.type === wallet.type && !wallet.isHd()) { + if (WatchOnlyWallet.type === wallet?.type && !wallet.isHd()) { // plain watchonly - just get the address change = wallet.getAddress(); } else { // otherwise, lets call widely-used getChangeAddressAsync() try { - change = await Promise.race([sleep(2000), wallet.getChangeAddressAsync()]); + change = await Promise.race([sleep(2000), wallet?.getChangeAddressAsync()]); } catch (_) {} if (!change) { @@ -375,7 +401,7 @@ const SendDetails = () => { change = wallet._getInternalAddressByIndex(wallet.getNextFreeChangeAddressIndex()); } else { // legacy wallets - change = wallet.getAddress(); + change = wallet?.getAddress(); } } } @@ -390,7 +416,10 @@ const SendDetails = () => { * * @param data {String} Can be address or `bitcoin:xxxxxxx` uri scheme, or invalid garbage */ - const processAddressData = data => { + const processAddressData = (data: string | { data?: any }) => { + if (typeof data !== 'string') { + data = String(data.data); + } const currentIndex = scrollIndex.current; setIsLoading(true); if (!data.replace) { @@ -400,7 +429,7 @@ const SendDetails = () => { } const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', ''); - if (wallet.isAddressValid(dataWithoutSchema)) { + if (wallet?.isAddressValid(dataWithoutSchema)) { setAddresses(addrs => { addrs[scrollIndex.current].address = dataWithoutSchema; return [...addrs]; @@ -410,7 +439,7 @@ const SendDetails = () => { } let address = ''; - let options; + let options: TOptions; try { if (!data.toLowerCase().startsWith('bitcoin:')) data = `bitcoin:${data}`; const decoded = DeeplinkSchemaMatch.bip21decode(data); @@ -428,19 +457,19 @@ const SendDetails = () => { if (btcAddressRx.test(address) || address.startsWith('bc1') || address.startsWith('BC1')) { setAddresses(addrs => { addrs[scrollIndex.current].address = address; - addrs[scrollIndex.current].amount = options.amount; - addrs[scrollIndex.current].amountSats = new BigNumber(options.amount).multipliedBy(100000000).toNumber(); + addrs[scrollIndex.current].amount = options?.amount ?? 0; + addrs[scrollIndex.current].amountSats = new BigNumber(options?.amount ?? 0).multipliedBy(100000000).toNumber(); return [...addrs]; }); setUnits(u => { u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC return [...u]; }); - setTransactionMemo(options.label || options.message); + setTransactionMemo(options.label || ''); // there used to be `options.message` here as well. bug? setAmountUnit(BitcoinUnit.BTC); setPayjoinUrl(options.pj || ''); // RN Bug: contentOffset gets reset to 0 when state changes. Remove code once this bug is resolved. - setTimeout(() => scrollView.current.scrollToIndex({ index: currentIndex, animated: false }), 50); + setTimeout(() => scrollView.current?.scrollToIndex({ index: currentIndex, animated: false }), 50); } setIsLoading(false); @@ -452,10 +481,10 @@ const SendDetails = () => { const requestedSatPerByte = feeRate; for (const [index, transaction] of addresses.entries()) { let error; - if (!transaction.amount || transaction.amount < 0 || parseFloat(transaction.amount) === 0) { + if (!transaction.amount || Number(transaction.amount) < 0 || parseFloat(String(transaction.amount)) === 0) { error = loc.send.details_amount_field_is_not_valid; console.log('validation error'); - } else if (parseFloat(transaction.amountSats) <= 500) { + } else if (parseFloat(String(transaction.amountSats)) <= 500) { error = loc.send.details_amount_field_is_less_than_minimum_amount_sat; console.log('validation error'); } else if (!requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) { @@ -464,7 +493,7 @@ const SendDetails = () => { } else if (!transaction.address) { error = loc.send.details_address_field_is_not_valid; console.log('validation error'); - } else if (balance - transaction.amountSats < 0) { + } else if (balance - Number(transaction.amountSats) < 0) { // first sanity check is that sending amount is not bigger than available balance error = frozenBalance > 0 ? loc.send.details_total_exceeds_balance_frozen : loc.send.details_total_exceeds_balance; console.log('validation error'); @@ -477,14 +506,14 @@ const SendDetails = () => { } if (!error) { - if (!wallet.isAddressValid(transaction.address)) { + if (!wallet?.isAddressValid(transaction.address)) { console.log('validation error'); error = loc.send.details_address_field_is_not_valid; } } if (error) { - scrollView.current.scrollToIndex({ index }); + scrollView.current?.scrollToIndex({ index }); setIsLoading(false); presentAlert({ title: loc.errors.error, message: error }); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); @@ -494,7 +523,7 @@ const SendDetails = () => { try { await createPsbtTransaction(); - } catch (Err) { + } catch (Err: any) { setIsLoading(false); presentAlert({ title: loc.errors.error, message: Err.message }); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); @@ -502,9 +531,11 @@ const SendDetails = () => { }; const createPsbtTransaction = async () => { + if (!wallet) return; const change = await getChangeAddressAsync(); + assert(change, 'Could not get change address'); const requestedSatPerByte = Number(feeRate); - const lutxo = utxo || wallet.getUtxo(); + const lutxo: CreateTransactionUtxo[] = utxo || (wallet?.getUtxo() ?? []); console.log({ requestedSatPerByte, lutxo: lutxo.length }); const targets = []; @@ -514,7 +545,7 @@ const SendDetails = () => { targets.push({ address: transaction.address }); continue; } - const value = parseInt(transaction.amountSats, 10); + const value = parseInt(String(transaction.amountSats), 10); if (value > 0) { targets.push({ address: transaction.address, value }); } else if (transaction.amount) { @@ -524,20 +555,24 @@ const SendDetails = () => { } } - const { tx, outputs, psbt, fee } = wallet.createTransaction( + // without forcing `HDSegwitBech32Wallet` i had a weird ts error, complaining about last argument (fp) + const { tx, outputs, psbt, fee } = (wallet as HDSegwitBech32Wallet)?.createTransaction( lutxo, targets, requestedSatPerByte, change, isTransactionReplaceable ? HDSegwitBech32Wallet.defaultRBFSequence : HDSegwitBech32Wallet.finalRBFSequence, + false, + 0, ); if (tx && routeParams.launchedBy && psbt) { console.warn('navigating back to ', routeParams.launchedBy); + // @ts-ignore idk how to fix FIXME? navigation.navigate(routeParams.launchedBy, { psbt }); } - if (wallet.type === WatchOnlyWallet.type) { + if (wallet?.type === WatchOnlyWallet.type) { // watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code // so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask // user whether he wants to broadcast it @@ -551,7 +586,7 @@ const SendDetails = () => { return; } - if (wallet.type === MultisigHDWallet.type) { + if (wallet?.type === MultisigHDWallet.type) { navigation.navigate('PsbtMultisig', { memo: transactionMemo, psbtBase64: psbt.toBase64(), @@ -562,8 +597,9 @@ const SendDetails = () => { return; } + assert(tx, 'createTRansaction failed'); + txMetadata[tx.getId()] = { - txhex: tx.toHex(), memo: transactionMemo, }; await saveToDisk(); @@ -589,9 +625,9 @@ const SendDetails = () => { setIsLoading(false); }; - const onWalletSelect = w => { + const onWalletSelect = (w: TWallet) => { setWallet(w); - navigation.pop(); + navigation.dispatch(popAction); }; /** @@ -600,7 +636,7 @@ const SendDetails = () => { * @returns {Promise} */ const importQrTransaction = () => { - if (wallet.type !== WatchOnlyWallet.type) { + if (wallet?.type !== WatchOnlyWallet.type) { return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' }); } @@ -616,8 +652,9 @@ const SendDetails = () => { }); }; - const importQrTransactionOnBarScanned = ret => { - navigation.getParent().pop(); + 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' }); @@ -649,7 +686,7 @@ const SendDetails = () => { * @returns {Promise} */ const importTransaction = async () => { - if (wallet.type !== WatchOnlyWallet.type) { + if (wallet?.type !== WatchOnlyWallet.type) { return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' }); } @@ -722,27 +759,29 @@ const SendDetails = () => { }); }; - const _importTransactionMultisig = async base64arg => { + 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 - if (wallet.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { + if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { hideOptions(); setIsLoading(true); await sleep(100); - wallet.cosignPsbt(psbt); + (wallet as MultisigHDWallet).cosignPsbt(psbt); setIsLoading(false); await sleep(100); } - navigation.navigate('PsbtMultisig', { - memo: transactionMemo, - psbtBase64: psbt.toBase64(), - walletID: wallet.getID(), - }); - } catch (error) { + 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 }); } setIsLoading(false); @@ -750,11 +789,11 @@ const SendDetails = () => { }; const importTransactionMultisig = () => { - return _importTransactionMultisig(); + return _importTransactionMultisig(false); }; - const onBarScanned = ret => { - navigation.getParent().pop(); + const onBarScanned = (ret: any) => { + navigation.getParent()?.dispatch(popAction); 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' }); @@ -781,12 +820,13 @@ const SendDetails = () => { }; const handleAddRecipient = async () => { - setAddresses(addrs => [...addrs, { address: '', key: String(Math.random()) }]); + console.log('handleAddRecipient'); + setAddresses(addrs => [...addrs, { address: '', key: String(Math.random()) } as IPaymentDestinations]); setOptionsVisible(false); await sleep(200); // wait for animation - scrollView.current.scrollToEnd(); + scrollView.current?.scrollToEnd(); if (addresses.length === 0) return; - scrollView.current.flashScrollIndicators(); + scrollView.current?.flashScrollIndicators(); }; const handleRemoveRecipient = async () => { @@ -799,15 +839,16 @@ const SendDetails = () => { setOptionsVisible(false); if (addresses.length === 0) return; await sleep(200); // wait for animation - scrollView.current.flashScrollIndicators(); - if (last && Platform.OS === 'android') scrollView.current.scrollToEnd(); // fix white screen on android + scrollView.current?.flashScrollIndicators(); + if (last && Platform.OS === 'android') scrollView.current?.scrollToEnd(); // fix white screen on android }; const handleCoinControl = () => { + if (!wallet) return; setOptionsVisible(false); navigation.navigate('CoinControl', { - walletID: wallet.getID(), - onUTXOChoose: u => setUtxo(u), + walletID: wallet?.getID(), + onUTXOChoose: (u: CreateTransactionUtxo[]) => setUtxo(u), }); }; @@ -822,22 +863,24 @@ const SendDetails = () => { let psbt; try { psbt = bitcoin.Psbt.fromBase64(scannedData); - tx = wallet.cosignPsbt(psbt).tx; - } catch (e) { + tx = (wallet as MultisigHDWallet).cosignPsbt(psbt).tx; + } catch (e: any) { presentAlert({ title: loc.errors.error, message: e.message }); return; } finally { setIsLoading(false); } - if (!tx) return 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 = []; + 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(address)); + const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address))); navigation.navigate('CreateTransaction', { fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(), @@ -858,7 +901,7 @@ const SendDetails = () => { // Header Right Button - const headerRightOnPress = id => { + const headerRightOnPress = (id: string) => { if (id === SendDetails.actionKeys.AddRecipient) { handleAddRecipient(); } else if (id === SendDetails.actionKeys.RemoveRecipient) { @@ -888,11 +931,11 @@ const SendDetails = () => { const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX); actions.push([{ id: SendDetails.actionKeys.SendMax, text: loc.send.details_adv_full, disabled: balance === 0 || isSendMaxUsed }]); - if (wallet.type === HDSegwitBech32Wallet.type) { + if (wallet?.type === HDSegwitBech32Wallet.type) { actions.push([{ id: SendDetails.actionKeys.AllowRBF, text: loc.send.details_adv_fee_bump, menuStateOn: isTransactionReplaceable }]); } const transactionActions = []; - if (wallet.type === WatchOnlyWallet.type && wallet.isHd()) { + if (wallet?.type === WatchOnlyWallet.type && wallet.isHd()) { transactionActions.push( { id: SendDetails.actionKeys.ImportTransaction, @@ -906,21 +949,21 @@ const SendDetails = () => { }, ); } - if (wallet.type === MultisigHDWallet.type) { + if (wallet?.type === MultisigHDWallet.type) { transactionActions.push({ id: SendDetails.actionKeys.ImportTransactionMultsig, text: loc.send.details_adv_import, icon: SendDetails.actionIcons.ImportTransactionMultsig, }); } - if (wallet.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) { + if (wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) { transactionActions.push({ id: SendDetails.actionKeys.CoSignTransaction, text: loc.multisig.co_sign_transaction, icon: SendDetails.actionIcons.SignPSBT, }); } - if (wallet.allowCosignPsbt()) { + if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) { transactionActions.push({ id: SendDetails.actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: SendDetails.actionIcons.SignPSBT }); } actions.push(transactionActions, [ @@ -952,6 +995,7 @@ const SendDetails = () => { isButton isMenuPrimaryAction onPressMenuItem={headerRightOnPress} + // @ts-ignore idk how to fix actions={headerRightActions()} > @@ -977,7 +1021,7 @@ const SendDetails = () => { }); }; - const onReplaceableFeeSwitchValueChanged = value => { + const onReplaceableFeeSwitchValueChanged = (value: boolean) => { setIsTransactionReplaceable(value); }; @@ -985,7 +1029,7 @@ const SendDetails = () => { // because of https://github.com/facebook/react-native/issues/21718 we use // onScroll for android and onMomentumScrollEnd for iOS - const handleRecipientsScrollEnds = e => { + const handleRecipientsScrollEnds = (e: NativeSyntheticEvent) => { if (Platform.OS === 'android') return; // for android we use handleRecipientsScroll const contentOffset = e.nativeEvent.contentOffset; const viewSize = e.nativeEvent.layoutMeasurement; @@ -993,7 +1037,7 @@ const SendDetails = () => { scrollIndex.current = index; }; - const handleRecipientsScroll = e => { + const handleRecipientsScroll = (e: NativeSyntheticEvent) => { if (Platform.OS === 'ios') return; // for iOS we use handleRecipientsScrollEnds const contentOffset = e.nativeEvent.contentOffset; const viewSize = e.nativeEvent.layoutMeasurement; @@ -1032,7 +1076,7 @@ const SendDetails = () => { ); }; - const formatFee = fee => formatBalance(fee, feeUnit, true); + const formatFee = (fee: number) => formatBalance(fee, feeUnit!, true); const stylesHook = StyleSheet.create({ loading: { @@ -1126,12 +1170,8 @@ const SendDetails = () => { ]; return ( - setIsFeeSelectionModalVisible(false)} - > - + setIsFeeSelectionModalVisible(false)}> + {options.map(({ label, time, fee, rate, active, disabled }, index) => ( { onPress={async () => { let error = loc.send.fee_satvbyte; while (true) { - let fee; + let fee: number | string; try { fee = await prompt(loc.send.create_fee, error, true, 'numeric'); @@ -1181,7 +1221,7 @@ const SendDetails = () => { continue; } - if (fee < 1) fee = '1'; + if (Number(fee) < 1) fee = '1'; fee = Number(fee).toString(); // this will remove leading zeros if any setCustomFee(fee); setIsFeeSelectionModalVisible(false); @@ -1201,8 +1241,8 @@ const SendDetails = () => { const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX); return ( - - + + {isEditable && ( { onPress={onUseAllPressed} /> )} - {wallet.type === HDSegwitBech32Wallet.type && isEditable && ( + {wallet?.type === HDSegwitBech32Wallet.type && isEditable && ( )} - {wallet.type === WatchOnlyWallet.type && wallet.isHd() && ( - + {wallet?.type === WatchOnlyWallet.type && wallet.isHd() && ( + )} - {wallet.type === WatchOnlyWallet.type && wallet.isHd() && ( + {wallet?.type === WatchOnlyWallet.type && wallet.isHd() && ( { onPress={importQrTransaction} /> )} - {wallet.type === MultisigHDWallet.type && isEditable && ( + {wallet?.type === MultisigHDWallet.type && isEditable && ( )} - {wallet.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && isEditable && ( + {wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && isEditable && ( )} {isEditable && ( @@ -1250,7 +1290,7 @@ const SendDetails = () => { )} - {wallet.allowCosignPsbt() && isEditable && ( + {(wallet as MultisigHDWallet)?.allowCosignPsbt() && isEditable && ( )} @@ -1307,34 +1347,34 @@ const SendDetails = () => { onPress={() => navigation.navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN })} disabled={!isEditable || isLoading} > - {wallet.getLabel()} + {wallet?.getLabel()} ); }; - const renderBitcoinTransactionInfoFields = params => { + const renderBitcoinTransactionInfoFields = (params: { item: IPaymentDestinations; index: number }) => { const { item, index } = params; return ( { + onAmountUnitChange={(unit: BitcoinUnit) => { setAddresses(addrs => { const addr = addrs[index]; switch (unit) { case BitcoinUnit.SATS: - addr.amountSats = parseInt(addr.amount, 10); + addr.amountSats = parseInt(String(addr.amount), 10); break; case BitcoinUnit.BTC: - addr.amountSats = btcToSatoshi(addr.amount); + addr.amountSats = btcToSatoshi(String(addr.amount)); break; case BitcoinUnit.LOCAL_CURRENCY: // also accounting for cached fiat->sat conversion to avoid rounding error - addr.amountSats = AmountInput.getCachedSatoshis(addr.amount) || btcToSatoshi(fiatToBTC(addr.amount)); + addr.amountSats = AmountInput.getCachedSatoshis(addr.amount) || btcToSatoshi(fiatToBTC(Number(addr.amount))); break; } @@ -1346,7 +1386,7 @@ const SendDetails = () => { return [...u]; }); }} - onChangeText={text => { + onChangeText={(text: string) => { setAddresses(addrs => { item.amount = text; switch (units[index] || amountUnit) { @@ -1354,7 +1394,7 @@ const SendDetails = () => { item.amountSats = btcToSatoshi(item.amount); break; case BitcoinUnit.LOCAL_CURRENCY: - item.amountSats = btcToSatoshi(fiatToBTC(item.amount)); + item.amountSats = btcToSatoshi(fiatToBTC(Number(item.amount))); break; case BitcoinUnit.SATS: default: @@ -1396,6 +1436,7 @@ const SendDetails = () => { onBarScanned={processAddressData} address={item.address} isLoading={isLoading} + /* @ts-ignore marcos fixme */ inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} launchedBy={name} editable={isEditable} @@ -1417,14 +1458,14 @@ const SendDetails = () => { return ( setWidth(e.nativeEvent.layout.width)}> - + 1} data={addresses} renderItem={renderBitcoinTransactionInfoFields} - ref={scrollView} horizontal + ref={scrollView} pagingEnabled removeClippedSubviews={false} onMomentumScrollBegin={Keyboard.dismiss} @@ -1444,6 +1485,7 @@ const SendDetails = () => { style={styles.memoText} editable={!isLoading} onSubmitEditing={Keyboard.dismiss} + /* @ts-ignore marcos fixme */ inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID} /> @@ -1473,9 +1515,9 @@ const SendDetails = () => { {Platform.select({ - ios: 0} onUseAllPressed={onUseAllPressed} balance={allBalance} />, + ios: 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} />, android: isAmountToolbarVisibleForAndroid && ( - 0} onUseAllPressed={onUseAllPressed} balance={allBalance} /> + 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} /> ), })} diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 1b57298a2..4488511aa 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -17,10 +17,12 @@ import { useTheme } from '../../components/themes'; import { TransactionListItem } from '../../components/TransactionListItem'; import WalletsCarousel from '../../components/WalletsCarousel'; import { scanQrHelper } from '../../helpers/scan-qr'; -import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useIsLargeScreen } from '../../hooks/useIsLargeScreen'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; +import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' }; @@ -87,6 +89,8 @@ function reducer(state: WalletListState, action: WalletListAction) { } } +type NavigationProps = NativeStackNavigationProp; + const WalletsList: React.FC = () => { const [state, dispatch] = useReducer>(reducer, initialState); const { isLoading } = state; @@ -104,7 +108,7 @@ const WalletsList: React.FC = () => { } = useStorage(); const { width } = useWindowDimensions(); const { colors, scanImage } = useTheme(); - const { navigate } = useExtendedNavigation(); + const { navigate } = useExtendedNavigation(); const isFocused = useIsFocused(); const routeName = useRoute().name; const dataSource = getTransactions(undefined, 10); @@ -321,6 +325,7 @@ const WalletsList: React.FC = () => { if (!value) return; DeeplinkSchemaMatch.navigationRouteFor({ url: value }, completionValue => { triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + // @ts-ignore: Fix later navigate(...completionValue); }); };