2024-03-31 21:59:14 +02:00
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
2024-05-28 15:33:15 +02:00
|
|
|
import { RouteProp, StackActions, useFocusEffect, useRoute } from '@react-navigation/native';
|
2024-03-31 21:59:14 +02:00
|
|
|
import BigNumber from 'bignumber.js';
|
|
|
|
import * as bitcoin from 'bitcoinjs-lib';
|
2024-05-31 19:18:01 +02:00
|
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2018-03-17 21:39:21 +01:00
|
|
|
import {
|
2020-06-09 16:08:18 +02:00
|
|
|
ActivityIndicator,
|
2019-08-07 07:45:27 +02:00
|
|
|
Alert,
|
2019-09-03 05:28:52 +02:00
|
|
|
Dimensions,
|
2024-09-06 01:58:25 +02:00
|
|
|
findNodeHandle,
|
2021-02-25 17:13:34 +01:00
|
|
|
FlatList,
|
2021-03-19 03:30:01 +01:00
|
|
|
I18nManager,
|
2021-02-25 17:13:34 +01:00
|
|
|
Keyboard,
|
|
|
|
LayoutAnimation,
|
2024-05-22 21:35:36 +02:00
|
|
|
NativeScrollEvent,
|
|
|
|
NativeSyntheticEvent,
|
2018-12-11 23:52:46 +01:00
|
|
|
Platform,
|
2021-02-25 17:13:34 +01:00
|
|
|
StyleSheet,
|
2018-12-24 01:44:31 +01:00
|
|
|
Text,
|
2021-02-25 17:13:34 +01:00
|
|
|
TextInput,
|
|
|
|
TouchableOpacity,
|
|
|
|
View,
|
2018-10-20 23:10:21 +02:00
|
|
|
} from 'react-native';
|
2024-03-31 21:59:14 +02:00
|
|
|
import DocumentPicker from 'react-native-document-picker';
|
2024-06-12 18:46:44 +02:00
|
|
|
import { Icon } from '@rneui/themed';
|
2020-12-25 17:09:53 +01:00
|
|
|
import RNFS from 'react-native-fs';
|
2024-03-31 21:59:14 +02:00
|
|
|
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
|
|
|
|
import * as fs from '../../blue_modules/fs';
|
|
|
|
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
2024-08-24 20:06:17 +02:00
|
|
|
import { BlueText } from '../../BlueComponents';
|
2021-02-25 17:13:34 +01:00
|
|
|
import { HDSegwitBech32Wallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
|
2020-04-28 16:48:36 +02:00
|
|
|
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
2024-03-31 21:59:14 +02:00
|
|
|
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
|
2021-02-25 17:13:34 +01:00
|
|
|
import AddressInput from '../../components/AddressInput';
|
2024-03-31 21:59:14 +02:00
|
|
|
import presentAlert from '../../components/Alert';
|
2021-02-25 17:13:34 +01:00
|
|
|
import AmountInput from '../../components/AmountInput';
|
2024-08-04 23:00:50 +02:00
|
|
|
import { BottomModalHandle } from '../../components/BottomModal';
|
2024-03-31 21:59:14 +02:00
|
|
|
import Button from '../../components/Button';
|
|
|
|
import CoinsSelected from '../../components/CoinsSelected';
|
2024-08-24 20:06:17 +02:00
|
|
|
import InputAccessoryAllFunds, { InputAccessoryAllFundsAccessoryViewID } from '../../components/InputAccessoryAllFunds';
|
2023-10-24 03:28:44 +02:00
|
|
|
import { useTheme } from '../../components/themes';
|
2024-10-01 19:26:57 +02:00
|
|
|
import { scanQrHelper } from '../../helpers/scan-qr';
|
2024-03-31 21:59:14 +02:00
|
|
|
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
|
|
|
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
|
|
|
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
|
2024-05-28 00:00:28 +02:00
|
|
|
import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../../class/wallets/types';
|
2024-05-22 21:35:36 +02:00
|
|
|
import { TOptions } from 'bip21';
|
|
|
|
import assert from 'assert';
|
|
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
|
|
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
|
|
|
|
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
2024-05-28 00:00:28 +02:00
|
|
|
import { ContactList } from '../../class/contact-list';
|
2024-05-31 19:18:01 +02:00
|
|
|
import { useStorage } from '../../hooks/context/useStorage';
|
2024-06-17 04:26:43 +02:00
|
|
|
import { Action } from '../../components/types';
|
2024-07-31 01:05:58 +02:00
|
|
|
import SelectFeeModal from '../../components/SelectFeeModal';
|
2024-08-24 18:16:38 +02:00
|
|
|
import { useKeyboard } from '../../hooks/useKeyboard';
|
2024-08-24 20:06:17 +02:00
|
|
|
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
|
2024-09-06 01:58:25 +02:00
|
|
|
import ActionSheet from '../ActionSheet';
|
2024-09-27 14:39:31 +02:00
|
|
|
import HeaderMenuButton from '../../components/HeaderMenuButton';
|
2024-10-13 09:08:52 +02:00
|
|
|
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
2019-02-03 10:09:46 +01:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
interface IPaymentDestinations {
|
2024-05-28 00:00:28 +02:00
|
|
|
address: string; // btc address or payment code
|
2024-05-22 21:35:36 +02:00
|
|
|
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<SendDetailsStackParamList, 'SendDetails'>;
|
2024-05-28 15:33:15 +02:00
|
|
|
type RouteProps = RouteProp<SendDetailsStackParamList, 'SendDetails'>;
|
2024-05-22 21:35:36 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const SendDetails = () => {
|
2024-05-31 17:52:29 +02:00
|
|
|
const { wallets, setSelectedWalletID, sleep, txMetadata, saveToDisk } = useStorage();
|
2024-05-22 21:35:36 +02:00
|
|
|
const navigation = useExtendedNavigation<NavigationProps>();
|
2024-06-07 20:50:50 +02:00
|
|
|
const setParams = navigation.setParams;
|
2024-05-28 15:33:15 +02:00
|
|
|
const route = useRoute<RouteProps>();
|
2024-05-22 21:35:36 +02:00
|
|
|
const name = route.name;
|
2024-05-28 15:33:15 +02:00
|
|
|
const routeParams = route.params;
|
2024-05-22 21:35:36 +02:00
|
|
|
const scrollView = useRef<FlatList<any>>(null);
|
2021-03-12 15:02:49 +01:00
|
|
|
const scrollIndex = useRef(0);
|
2021-02-25 17:13:34 +01:00
|
|
|
const { colors } = useTheme();
|
2024-05-22 21:35:36 +02:00
|
|
|
const popAction = StackActions.pop(1);
|
2021-02-25 17:13:34 +01:00
|
|
|
|
|
|
|
// state
|
|
|
|
const [width, setWidth] = useState(Dimensions.get('window').width);
|
2021-06-03 14:45:49 +02:00
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2024-05-22 21:35:36 +02:00
|
|
|
const [wallet, setWallet] = useState<TWallet | null>(null);
|
2024-06-30 19:17:55 +02:00
|
|
|
const feeModalRef = useRef<BottomModalHandle>(null);
|
2021-03-05 02:19:44 +01:00
|
|
|
const [walletSelectionOrCoinsSelectedHidden, setWalletSelectionOrCoinsSelectedHidden] = useState(false);
|
2021-04-15 10:38:48 +02:00
|
|
|
const [isAmountToolbarVisibleForAndroid, setIsAmountToolbarVisibleForAndroid] = useState(false);
|
2024-08-04 23:00:50 +02:00
|
|
|
const [isTransactionReplaceable, setIsTransactionReplaceable] = useState<boolean | undefined>(false);
|
2024-05-22 21:35:36 +02:00
|
|
|
const [addresses, setAddresses] = useState<IPaymentDestinations[]>([]);
|
|
|
|
const [units, setUnits] = useState<BitcoinUnit[]>([]);
|
|
|
|
const [transactionMemo, setTransactionMemo] = useState<string>('');
|
2021-02-25 17:13:34 +01:00
|
|
|
const [networkTransactionFees, setNetworkTransactionFees] = useState(new NetworkTransactionFee(3, 2, 1));
|
2021-03-25 01:45:07 +01:00
|
|
|
const [networkTransactionFeesIsLoading, setNetworkTransactionFeesIsLoading] = useState(false);
|
2024-05-22 21:35:36 +02:00
|
|
|
const [customFee, setCustomFee] = useState<string | null>(null);
|
|
|
|
const [feePrecalc, setFeePrecalc] = useState<IFee>({ current: null, slowFee: null, mediumFee: null, fastestFee: null });
|
|
|
|
const [feeUnit, setFeeUnit] = useState<BitcoinUnit>();
|
|
|
|
const [amountUnit, setAmountUnit] = useState<BitcoinUnit>();
|
|
|
|
const [utxo, setUtxo] = useState<CreateTransactionUtxo[] | null>(null);
|
|
|
|
const [frozenBalance, setFrozenBlance] = useState<number>(0);
|
|
|
|
const [payjoinUrl, setPayjoinUrl] = useState<string | null>(null);
|
|
|
|
const [changeAddress, setChangeAddress] = useState<string | null>(null);
|
2021-02-25 17:13:34 +01:00
|
|
|
const [dumb, setDumb] = useState(false);
|
2023-11-11 12:33:50 +01:00
|
|
|
const { isEditable } = routeParams;
|
2021-08-23 21:54:18 +02:00
|
|
|
// if utxo is limited we use it to calculate available balance
|
2024-09-04 02:02:29 +02:00
|
|
|
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
|
2021-08-23 21:54:18 +02:00
|
|
|
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
// if cutomFee is not set, we need to choose highest possible fee for wallet balance
|
2021-09-14 15:08:15 +02:00
|
|
|
// if there are no funds for even Slow option, use 1 sat/vbyte fee
|
2021-03-19 16:29:27 +01:00
|
|
|
const feeRate = useMemo(() => {
|
2021-02-25 17:13:34 +01:00
|
|
|
if (customFee) return customFee;
|
|
|
|
if (feePrecalc.slowFee === null) return '1'; // wait for precalculated fees
|
|
|
|
let initialFee;
|
|
|
|
if (feePrecalc.fastestFee !== null) {
|
|
|
|
initialFee = String(networkTransactionFees.fastestFee);
|
|
|
|
} else if (feePrecalc.mediumFee !== null) {
|
|
|
|
initialFee = String(networkTransactionFees.mediumFee);
|
2019-02-18 08:25:27 +01:00
|
|
|
} else {
|
2021-02-25 17:13:34 +01:00
|
|
|
initialFee = String(networkTransactionFees.slowFee);
|
2019-02-18 08:25:27 +01:00
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
return initialFee;
|
|
|
|
}, [customFee, feePrecalc, networkTransactionFees]);
|
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
useEffect(() => {
|
|
|
|
console.log('send/details - useEffect');
|
|
|
|
if (wallet) {
|
|
|
|
setHeaderRightOptions();
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [colors, wallet, isTransactionReplaceable, balance, addresses, isEditable, isLoading]);
|
2021-08-23 21:39:52 +02:00
|
|
|
|
2024-08-24 18:16:38 +02:00
|
|
|
useKeyboard({
|
|
|
|
onKeyboardDidShow: () => {
|
2021-02-25 17:13:34 +01:00
|
|
|
setWalletSelectionOrCoinsSelectedHidden(true);
|
|
|
|
setIsAmountToolbarVisibleForAndroid(true);
|
2024-08-24 18:16:38 +02:00
|
|
|
},
|
|
|
|
onKeyboardDidHide: () => {
|
2021-02-25 17:13:34 +01:00
|
|
|
setWalletSelectionOrCoinsSelectedHidden(false);
|
|
|
|
setIsAmountToolbarVisibleForAndroid(false);
|
2024-08-24 18:16:38 +02:00
|
|
|
},
|
|
|
|
});
|
2021-02-25 17:13:34 +01:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// decode route params
|
2021-08-28 06:49:46 +02:00
|
|
|
const currentAddress = addresses[scrollIndex.current];
|
2021-02-25 17:13:34 +01:00
|
|
|
if (routeParams.uri) {
|
2020-06-15 03:15:58 +02:00
|
|
|
try {
|
2023-07-25 15:50:04 +02:00
|
|
|
const { address, amount, memo, payjoinUrl: pjUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(routeParams.uri);
|
2021-09-04 21:39:31 +02:00
|
|
|
|
2023-07-25 15:50:04 +02:00
|
|
|
setUnits(u => {
|
|
|
|
u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC
|
|
|
|
return [...u];
|
2021-09-04 21:39:31 +02:00
|
|
|
});
|
|
|
|
|
2023-07-25 15:50:04 +02:00
|
|
|
setAddresses(addrs => {
|
2021-08-28 06:49:46 +02:00
|
|
|
if (currentAddress) {
|
|
|
|
currentAddress.address = address;
|
|
|
|
if (Number(amount) > 0) {
|
2024-05-22 21:35:36 +02:00
|
|
|
currentAddress.amount = amount!;
|
|
|
|
currentAddress.amountSats = btcToSatoshi(amount!);
|
2021-08-28 06:49:46 +02:00
|
|
|
}
|
2023-07-25 15:50:04 +02:00
|
|
|
addrs[scrollIndex.current] = currentAddress;
|
|
|
|
return [...addrs];
|
2021-08-28 06:49:46 +02:00
|
|
|
} else {
|
2024-05-22 21:35:36 +02:00
|
|
|
return [...addrs, { address, amount, amountSats: btcToSatoshi(amount!), key: String(Math.random()) } as IPaymentDestinations];
|
2021-08-28 06:49:46 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (memo?.trim().length > 0) {
|
|
|
|
setTransactionMemo(memo);
|
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
setAmountUnit(BitcoinUnit.BTC);
|
2023-07-25 15:50:04 +02:00
|
|
|
setPayjoinUrl(pjUrl);
|
2020-06-15 03:15:58 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
2024-02-07 20:24:24 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: loc.send.details_error_decode });
|
2019-09-03 05:28:52 +02:00
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
} else if (routeParams.address) {
|
2021-09-09 13:00:11 +02:00
|
|
|
const { amount, amountSats, unit = BitcoinUnit.BTC } = routeParams;
|
2024-06-10 21:11:02 +02:00
|
|
|
// @ts-ignore: needs fix
|
|
|
|
setAddresses(value => {
|
2024-06-07 15:37:45 +02:00
|
|
|
if (currentAddress && currentAddress.address && routeParams.address) {
|
2021-08-28 06:49:46 +02:00
|
|
|
currentAddress.address = routeParams.address;
|
2024-06-10 21:11:02 +02:00
|
|
|
value[scrollIndex.current] = currentAddress;
|
|
|
|
return [...value];
|
2021-08-28 06:49:46 +02:00
|
|
|
} else {
|
2024-06-10 21:11:02 +02:00
|
|
|
return [...value, { address: routeParams.address, key: String(Math.random()), amount, amountSats }];
|
2021-08-28 06:49:46 +02:00
|
|
|
}
|
|
|
|
});
|
2024-06-10 21:11:02 +02:00
|
|
|
if (routeParams.memo && routeParams.memo?.trim().length > 0) {
|
2021-08-28 06:49:46 +02:00
|
|
|
setTransactionMemo(routeParams.memo);
|
|
|
|
}
|
2023-07-25 15:50:04 +02:00
|
|
|
setUnits(u => {
|
|
|
|
u[scrollIndex.current] = unit;
|
|
|
|
return [...u];
|
2021-09-09 13:00:11 +02:00
|
|
|
});
|
2024-06-11 04:41:14 +02:00
|
|
|
} else if (routeParams.addRecipientParams) {
|
|
|
|
const index = addresses.length === 0 ? 0 : scrollIndex.current;
|
2024-06-29 01:11:08 +02:00
|
|
|
const { address, amount } = routeParams.addRecipientParams;
|
|
|
|
|
|
|
|
setAddresses(prevAddresses => {
|
|
|
|
const updatedAddresses = [...prevAddresses];
|
|
|
|
if (address) {
|
|
|
|
updatedAddresses[index] = {
|
|
|
|
...updatedAddresses[index],
|
|
|
|
address,
|
|
|
|
amount: amount ?? updatedAddresses[index].amount,
|
|
|
|
amountSats: amount ? btcToSatoshi(amount) : updatedAddresses[index].amountSats,
|
|
|
|
} as IPaymentDestinations;
|
|
|
|
}
|
|
|
|
return updatedAddresses;
|
|
|
|
});
|
2024-06-28 22:00:20 +02:00
|
|
|
|
2024-06-29 01:11:08 +02:00
|
|
|
// @ts-ignore: Fix later
|
|
|
|
setParams(prevParams => ({ ...prevParams, addRecipientParams: undefined }));
|
2021-02-01 17:26:31 +01:00
|
|
|
} else {
|
2024-05-22 21:35:36 +02:00
|
|
|
setAddresses([{ address: '', key: String(Math.random()) } as IPaymentDestinations]); // key is for the FlatList
|
2021-02-01 17:26:31 +01:00
|
|
|
}
|
2024-08-05 04:13:25 +02:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [routeParams.uri, routeParams.address, routeParams.addRecipientParams]);
|
2021-08-28 06:49:46 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// check if we have a suitable wallet
|
2023-07-25 15:50:04 +02:00
|
|
|
const suitable = wallets.filter(w => w.chain === Chain.ONCHAIN && w.allowSend());
|
2021-08-28 06:49:46 +02:00
|
|
|
if (suitable.length === 0) {
|
2024-02-07 20:24:24 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: loc.send.details_wallet_before_tx });
|
2021-08-28 06:49:46 +02:00
|
|
|
navigation.goBack();
|
|
|
|
return;
|
|
|
|
}
|
2023-07-25 15:50:04 +02:00
|
|
|
const newWallet = (routeParams.walletID && wallets.find(w => w.getID() === routeParams.walletID)) || suitable[0];
|
|
|
|
setWallet(newWallet);
|
|
|
|
setFeeUnit(newWallet.getPreferredBalanceUnit());
|
|
|
|
setAmountUnit(newWallet.preferredBalanceUnit); // default for whole screen
|
2019-02-01 07:19:09 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
// we are ready!
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
// load cached fees
|
|
|
|
AsyncStorage.getItem(NetworkTransactionFee.StorageKey)
|
2021-03-24 18:17:17 +01:00
|
|
|
.then(res => {
|
2024-05-22 21:35:36 +02:00
|
|
|
if (!res) return;
|
2021-02-25 17:13:34 +01:00
|
|
|
const fees = JSON.parse(res);
|
|
|
|
if (!fees?.fastestFee) return;
|
|
|
|
setNetworkTransactionFees(fees);
|
|
|
|
})
|
|
|
|
.catch(e => console.log('loading cached recommendedFees error', e));
|
|
|
|
|
|
|
|
// load fresh fees from servers
|
2021-03-25 01:45:07 +01:00
|
|
|
|
|
|
|
setNetworkTransactionFeesIsLoading(true);
|
2021-02-25 17:13:34 +01:00
|
|
|
NetworkTransactionFees.recommendedFees()
|
|
|
|
.then(async fees => {
|
|
|
|
if (!fees?.fastestFee) return;
|
|
|
|
setNetworkTransactionFees(fees);
|
|
|
|
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(fees));
|
|
|
|
})
|
2021-03-25 01:45:07 +01:00
|
|
|
.catch(e => console.log('loading recommendedFees error', e))
|
|
|
|
.finally(() => {
|
|
|
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
|
|
setNetworkTransactionFeesIsLoading(false);
|
|
|
|
});
|
2024-08-05 04:13:25 +02:00
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
2021-02-25 17:13:34 +01:00
|
|
|
|
|
|
|
// change header and reset state on wallet change
|
|
|
|
useEffect(() => {
|
|
|
|
if (!wallet) return;
|
2023-11-20 21:02:47 +01:00
|
|
|
setSelectedWalletID(wallet.getID());
|
2020-09-14 12:49:08 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
// reset other values
|
|
|
|
setUtxo(null);
|
|
|
|
setChangeAddress(null);
|
2024-08-04 23:00:50 +02:00
|
|
|
setIsTransactionReplaceable(wallet.type === HDSegwitBech32Wallet.type && !routeParams.noRbf ? true : undefined);
|
2021-02-25 17:13:34 +01:00
|
|
|
// update wallet UTXO
|
|
|
|
wallet
|
|
|
|
.fetchUtxo()
|
|
|
|
.then(() => {
|
|
|
|
// we need to re-calculate fees
|
|
|
|
setDumb(v => !v);
|
|
|
|
})
|
|
|
|
.catch(e => console.log('fetchUtxo error', e));
|
2024-08-05 04:13:25 +02:00
|
|
|
}, [wallet]); // eslint-disable-line react-hooks/exhaustive-deps
|
2021-02-25 17:13:34 +01:00
|
|
|
|
|
|
|
// recalc fees in effect so we don't block render
|
|
|
|
useEffect(() => {
|
|
|
|
if (!wallet) return; // wait for it
|
|
|
|
const fees = networkTransactionFees;
|
2021-03-19 16:29:27 +01:00
|
|
|
const requestedSatPerByte = Number(feeRate);
|
2021-02-25 17:13:34 +01:00
|
|
|
const lutxo = utxo || wallet.getUtxo();
|
2021-12-23 10:46:29 +01:00
|
|
|
let frozen = 0;
|
|
|
|
if (!utxo) {
|
|
|
|
// if utxo is not limited search for frozen outputs and calc it's balance
|
|
|
|
frozen = wallet
|
|
|
|
.getUtxo(true)
|
|
|
|
.filter(o => !lutxo.some(i => i.txid === o.txid && i.vout === o.vout))
|
|
|
|
.reduce((prev, curr) => prev + curr.value, 0);
|
|
|
|
}
|
2020-10-05 19:53:47 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const options = [
|
|
|
|
{ key: 'current', fee: requestedSatPerByte },
|
|
|
|
{ key: 'slowFee', fee: fees.slowFee },
|
|
|
|
{ key: 'mediumFee', fee: fees.mediumFee },
|
|
|
|
{ key: 'fastestFee', fee: fees.fastestFee },
|
|
|
|
];
|
2018-01-30 23:42:38 +01:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
const newFeePrecalc: /* Record<string, any> */ IFee = { ...feePrecalc };
|
2019-01-30 05:00:12 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
for (const opt of options) {
|
|
|
|
let targets = [];
|
|
|
|
for (const transaction of addresses) {
|
|
|
|
if (transaction.amount === BitcoinUnit.MAX) {
|
|
|
|
// single output with MAX
|
|
|
|
targets = [{ address: transaction.address }];
|
|
|
|
break;
|
|
|
|
}
|
2024-05-22 21:35:36 +02:00
|
|
|
const value = transaction.amountSats;
|
|
|
|
if (Number(value) > 0) {
|
2021-02-25 17:13:34 +01:00
|
|
|
targets.push({ address: transaction.address, value });
|
|
|
|
} else if (transaction.amount) {
|
2024-01-28 16:11:08 +01:00
|
|
|
if (btcToSatoshi(transaction.amount) > 0) {
|
|
|
|
targets.push({ address: transaction.address, value: btcToSatoshi(transaction.amount) });
|
2021-02-25 17:13:34 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-30 05:00:12 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
// if targets is empty, insert dust
|
|
|
|
if (targets.length === 0) {
|
|
|
|
targets.push({ address: '36JxaUrpDzkEerkTf1FzwHNE1Hb7cCjgJV', value: 546 });
|
|
|
|
}
|
2019-01-30 05:00:12 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
// replace wrong addresses with dump
|
|
|
|
targets = targets.map(t => {
|
2021-12-05 19:46:42 +01:00
|
|
|
if (!wallet.isAddressValid(t.address)) {
|
2021-02-25 17:13:34 +01:00
|
|
|
return { ...t, address: '36JxaUrpDzkEerkTf1FzwHNE1Hb7cCjgJV' };
|
2021-12-05 19:46:42 +01:00
|
|
|
} else {
|
|
|
|
return t;
|
2019-09-03 05:28:52 +02:00
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
});
|
2019-09-03 05:28:52 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
let flag = false;
|
|
|
|
while (true) {
|
2019-09-03 05:28:52 +02:00
|
|
|
try {
|
2024-05-22 21:35:36 +02:00
|
|
|
const { fee } = wallet.coinselect(lutxo, targets, opt.fee);
|
2021-02-25 17:13:34 +01:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
// @ts-ignore options& opt are used only to iterate keys we predefined and we know exist
|
2021-02-25 17:13:34 +01:00
|
|
|
newFeePrecalc[opt.key] = fee;
|
|
|
|
break;
|
2024-05-22 21:35:36 +02:00
|
|
|
} catch (e: any) {
|
2021-02-25 17:13:34 +01:00
|
|
|
if (e.message.includes('Not enough') && !flag) {
|
|
|
|
flag = true;
|
|
|
|
// if we don't have enough funds, construct maximum possible transaction
|
|
|
|
targets = targets.map((t, index) => (index > 0 ? { ...t, value: 546 } : { address: t.address }));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
// @ts-ignore options& opt are used only to iterate keys we predefined and we know exist
|
2021-02-25 17:13:34 +01:00
|
|
|
newFeePrecalc[opt.key] = null;
|
|
|
|
break;
|
2019-09-03 05:28:52 +02:00
|
|
|
}
|
|
|
|
}
|
2018-10-27 21:38:22 +02:00
|
|
|
}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
setFeePrecalc(newFeePrecalc);
|
2021-12-23 10:46:29 +01:00
|
|
|
setFrozenBlance(frozen);
|
2024-08-05 04:13:25 +02:00
|
|
|
}, [wallet, networkTransactionFees, utxo, addresses, feeRate, dumb]); // eslint-disable-line react-hooks/exhaustive-deps
|
2018-01-30 23:42:38 +01:00
|
|
|
|
2021-12-23 10:46:29 +01:00
|
|
|
// we need to re-calculate fees if user opens-closes coin control
|
2021-12-23 16:39:33 +01:00
|
|
|
useFocusEffect(
|
|
|
|
useCallback(() => {
|
2024-05-18 06:36:03 +02:00
|
|
|
setIsLoading(false);
|
2021-12-23 16:39:33 +01:00
|
|
|
setDumb(v => !v);
|
2024-07-23 19:44:04 +02:00
|
|
|
return () => {
|
|
|
|
feeModalRef.current?.dismiss();
|
|
|
|
};
|
2021-12-23 16:39:33 +01:00
|
|
|
}, []),
|
|
|
|
);
|
2021-12-23 10:46:29 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const getChangeAddressAsync = async () => {
|
|
|
|
if (changeAddress) return changeAddress; // cache
|
2020-10-05 19:53:47 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
let change;
|
2024-05-22 21:35:36 +02:00
|
|
|
if (WatchOnlyWallet.type === wallet?.type && !wallet.isHd()) {
|
2020-10-05 19:53:47 +02:00
|
|
|
// plain watchonly - just get the address
|
2021-02-25 17:13:34 +01:00
|
|
|
change = wallet.getAddress();
|
2020-10-05 19:53:47 +02:00
|
|
|
} else {
|
|
|
|
// otherwise, lets call widely-used getChangeAddressAsync()
|
|
|
|
try {
|
2024-05-22 21:35:36 +02:00
|
|
|
change = await Promise.race([sleep(2000), wallet?.getChangeAddressAsync()]);
|
2020-10-05 19:53:47 +02:00
|
|
|
} catch (_) {}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
if (!change) {
|
2020-10-05 19:53:47 +02:00
|
|
|
// either sleep expired or getChangeAddressAsync threw an exception
|
|
|
|
if (wallet instanceof AbstractHDElectrumWallet) {
|
2021-02-25 17:13:34 +01:00
|
|
|
change = wallet._getInternalAddressByIndex(wallet.getNextFreeChangeAddressIndex());
|
2020-10-05 19:53:47 +02:00
|
|
|
} else {
|
|
|
|
// legacy wallets
|
2024-05-22 21:35:36 +02:00
|
|
|
change = wallet?.getAddress();
|
2020-10-05 19:53:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
if (change) setChangeAddress(change); // cache
|
2020-10-05 19:53:47 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
return change;
|
|
|
|
};
|
2020-09-14 12:49:08 +02:00
|
|
|
/**
|
2021-02-25 17:13:34 +01:00
|
|
|
* TODO: refactor this mess, get rid of regexp, use https://github.com/bitcoinjs/bitcoinjs-lib/issues/890 etc etc
|
|
|
|
*
|
|
|
|
* @param data {String} Can be address or `bitcoin:xxxxxxx` uri scheme, or invalid garbage
|
2020-09-14 12:49:08 +02:00
|
|
|
*/
|
2024-05-28 00:00:28 +02:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
const processAddressData = (data: string | { data?: any }) => {
|
2024-05-28 00:00:28 +02:00
|
|
|
assert(wallet, 'Internal error: wallet not set');
|
2024-05-22 21:35:36 +02:00
|
|
|
if (typeof data !== 'string') {
|
|
|
|
data = String(data.data);
|
|
|
|
}
|
2021-07-05 04:32:35 +02:00
|
|
|
const currentIndex = scrollIndex.current;
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(true);
|
|
|
|
if (!data.replace) {
|
|
|
|
// user probably scanned PSBT and got an object instead of string..?
|
|
|
|
setIsLoading(false);
|
2024-02-07 20:24:24 +01:00
|
|
|
return presentAlert({ title: loc.errors.error, message: loc.send.details_address_field_is_not_valid });
|
2021-02-25 17:13:34 +01:00
|
|
|
}
|
2020-09-14 12:49:08 +02:00
|
|
|
|
2024-05-28 00:00:28 +02:00
|
|
|
const cl = new ContactList();
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
|
2024-05-28 00:00:28 +02:00
|
|
|
if (wallet.isAddressValid(dataWithoutSchema) || cl.isPaymentCodeValid(dataWithoutSchema)) {
|
2023-07-25 15:50:04 +02:00
|
|
|
setAddresses(addrs => {
|
|
|
|
addrs[scrollIndex.current].address = dataWithoutSchema;
|
|
|
|
return [...addrs];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-05-24 17:06:37 +02:00
|
|
|
setTimeout(() => scrollView.current?.scrollToIndex({ index: currentIndex, animated: false }), 50);
|
2021-02-25 17:13:34 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let address = '';
|
2024-05-22 21:35:36 +02:00
|
|
|
let options: TOptions;
|
2021-02-25 17:13:34 +01:00
|
|
|
try {
|
|
|
|
if (!data.toLowerCase().startsWith('bitcoin:')) data = `bitcoin:${data}`;
|
|
|
|
const decoded = DeeplinkSchemaMatch.bip21decode(data);
|
|
|
|
address = decoded.address;
|
|
|
|
options = decoded.options;
|
|
|
|
} catch (error) {
|
|
|
|
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
|
|
|
|
const decoded = DeeplinkSchemaMatch.bip21decode(data);
|
|
|
|
decoded.options.amount = 0;
|
|
|
|
address = decoded.address;
|
|
|
|
options = decoded.options;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('options', options);
|
2024-05-28 00:00:28 +02:00
|
|
|
if (wallet.isAddressValid(address)) {
|
2023-07-25 15:50:04 +02:00
|
|
|
setAddresses(addrs => {
|
|
|
|
addrs[scrollIndex.current].address = address;
|
2024-05-22 21:35:36 +02:00
|
|
|
addrs[scrollIndex.current].amount = options?.amount ?? 0;
|
|
|
|
addrs[scrollIndex.current].amountSats = new BigNumber(options?.amount ?? 0).multipliedBy(100000000).toNumber();
|
2023-07-25 15:50:04 +02:00
|
|
|
return [...addrs];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2023-07-25 15:50:04 +02:00
|
|
|
setUnits(u => {
|
|
|
|
u[scrollIndex.current] = BitcoinUnit.BTC; // also resetting current unit to BTC
|
|
|
|
return [...u];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2024-05-22 21:35:36 +02:00
|
|
|
setTransactionMemo(options.label || ''); // there used to be `options.message` here as well. bug?
|
2021-02-25 17:13:34 +01:00
|
|
|
setAmountUnit(BitcoinUnit.BTC);
|
|
|
|
setPayjoinUrl(options.pj || '');
|
2021-07-05 04:32:35 +02:00
|
|
|
// RN Bug: contentOffset gets reset to 0 when state changes. Remove code once this bug is resolved.
|
2024-05-22 21:35:36 +02:00
|
|
|
setTimeout(() => scrollView.current?.scrollToIndex({ index: currentIndex, animated: false }), 50);
|
2021-02-25 17:13:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
const createTransaction = async () => {
|
2024-05-28 00:00:28 +02:00
|
|
|
assert(wallet, 'Internal error: wallet is not set');
|
2021-02-25 17:13:34 +01:00
|
|
|
Keyboard.dismiss();
|
|
|
|
setIsLoading(true);
|
2021-03-19 16:29:27 +01:00
|
|
|
const requestedSatPerByte = feeRate;
|
2021-02-25 17:13:34 +01:00
|
|
|
for (const [index, transaction] of addresses.entries()) {
|
|
|
|
let error;
|
2024-05-22 21:35:36 +02:00
|
|
|
if (!transaction.amount || Number(transaction.amount) < 0 || parseFloat(String(transaction.amount)) === 0) {
|
2021-02-25 17:13:34 +01:00
|
|
|
error = loc.send.details_amount_field_is_not_valid;
|
|
|
|
console.log('validation error');
|
2024-05-22 21:35:36 +02:00
|
|
|
} else if (parseFloat(String(transaction.amountSats)) <= 500) {
|
2021-02-25 17:13:34 +01:00
|
|
|
error = loc.send.details_amount_field_is_less_than_minimum_amount_sat;
|
|
|
|
console.log('validation error');
|
|
|
|
} else if (!requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) {
|
|
|
|
error = loc.send.details_fee_field_is_not_valid;
|
|
|
|
console.log('validation error');
|
|
|
|
} else if (!transaction.address) {
|
|
|
|
error = loc.send.details_address_field_is_not_valid;
|
|
|
|
console.log('validation error');
|
2024-05-22 21:35:36 +02:00
|
|
|
} else if (balance - Number(transaction.amountSats) < 0) {
|
2021-02-25 17:13:34 +01:00
|
|
|
// first sanity check is that sending amount is not bigger than available balance
|
2021-12-23 10:46:29 +01:00
|
|
|
error = frozenBalance > 0 ? loc.send.details_total_exceeds_balance_frozen : loc.send.details_total_exceeds_balance;
|
2021-02-25 17:13:34 +01:00
|
|
|
console.log('validation error');
|
|
|
|
} else if (transaction.address) {
|
|
|
|
const address = transaction.address.trim().toLowerCase();
|
|
|
|
if (address.startsWith('lnb') || address.startsWith('lightning:lnb')) {
|
2021-10-17 16:57:16 +02:00
|
|
|
error = loc.send.provided_address_is_invoice;
|
2021-02-25 17:13:34 +01:00
|
|
|
console.log('validation error');
|
2020-09-14 12:49:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
if (!error) {
|
2024-05-28 00:00:28 +02:00
|
|
|
const cl = new ContactList();
|
|
|
|
if (!wallet.isAddressValid(transaction.address) && !cl.isPaymentCodeValid(transaction.address)) {
|
2021-02-25 17:13:34 +01:00
|
|
|
console.log('validation error');
|
|
|
|
error = loc.send.details_address_field_is_not_valid;
|
2020-09-14 12:49:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-02 23:37:58 +02:00
|
|
|
// validating payment codes, if any
|
|
|
|
if (!error) {
|
|
|
|
if (transaction.address.startsWith('sp1')) {
|
|
|
|
if (!wallet.allowSilentPaymentSend()) {
|
|
|
|
console.log('validation error');
|
|
|
|
error = loc.send.cant_send_to_silentpayment_adress;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (transaction.address.startsWith('PM')) {
|
|
|
|
if (!wallet.allowBIP47()) {
|
|
|
|
console.log('validation error');
|
|
|
|
error = loc.send.cant_send_to_bip47;
|
|
|
|
} else if (!(wallet as unknown as AbstractHDElectrumWallet).getBIP47NotificationTransaction(transaction.address)) {
|
|
|
|
console.log('validation error');
|
|
|
|
error = loc.send.cant_find_bip47_notification;
|
|
|
|
} else {
|
|
|
|
// BIP47 is allowed, notif tx is in place, lets sync joint addresses with the receiver
|
|
|
|
await (wallet as unknown as AbstractHDElectrumWallet).syncBip47ReceiversAddresses(transaction.address);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
if (error) {
|
2024-05-22 21:35:36 +02:00
|
|
|
scrollView.current?.scrollToIndex({ index });
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-03-21 05:52:21 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: error });
|
2023-12-29 12:52:12 +01:00
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
2021-02-25 17:13:34 +01:00
|
|
|
return;
|
2021-02-01 17:26:31 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
try {
|
|
|
|
await createPsbtTransaction();
|
2024-05-22 21:35:36 +02:00
|
|
|
} catch (Err: any) {
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-02-07 20:24:24 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: Err.message });
|
2023-12-29 12:52:12 +01:00
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
2021-02-25 17:13:34 +01:00
|
|
|
}
|
2020-09-14 12:49:08 +02:00
|
|
|
};
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const createPsbtTransaction = async () => {
|
2024-05-22 21:35:36 +02:00
|
|
|
if (!wallet) return;
|
2023-07-25 15:50:04 +02:00
|
|
|
const change = await getChangeAddressAsync();
|
2024-05-22 21:35:36 +02:00
|
|
|
assert(change, 'Could not get change address');
|
2021-03-19 16:29:27 +01:00
|
|
|
const requestedSatPerByte = Number(feeRate);
|
2024-05-22 21:35:36 +02:00
|
|
|
const lutxo: CreateTransactionUtxo[] = utxo || (wallet?.getUtxo() ?? []);
|
2021-04-28 08:55:27 +02:00
|
|
|
console.log({ requestedSatPerByte, lutxo: lutxo.length });
|
2019-06-01 22:44:39 +02:00
|
|
|
|
2024-05-28 00:00:28 +02:00
|
|
|
const targets: CreateTransactionTarget[] = [];
|
2021-02-25 17:13:34 +01:00
|
|
|
for (const transaction of addresses) {
|
2020-04-24 16:03:15 +02:00
|
|
|
if (transaction.amount === BitcoinUnit.MAX) {
|
2021-03-12 17:19:04 +01:00
|
|
|
// output with MAX
|
|
|
|
targets.push({ address: transaction.address });
|
|
|
|
continue;
|
2020-04-24 16:03:15 +02:00
|
|
|
}
|
2024-05-22 21:35:36 +02:00
|
|
|
const value = parseInt(String(transaction.amountSats), 10);
|
2020-04-24 16:03:15 +02:00
|
|
|
if (value > 0) {
|
|
|
|
targets.push({ address: transaction.address, value });
|
2020-06-24 21:23:56 +02:00
|
|
|
} else if (transaction.amount) {
|
2024-01-28 16:11:08 +01:00
|
|
|
if (btcToSatoshi(transaction.amount) > 0) {
|
|
|
|
targets.push({ address: transaction.address, value: btcToSatoshi(transaction.amount) });
|
2020-06-24 21:23:56 +02:00
|
|
|
}
|
2019-09-03 05:28:52 +02:00
|
|
|
}
|
2019-08-04 21:33:15 +02:00
|
|
|
}
|
2019-06-01 22:44:39 +02:00
|
|
|
|
2024-05-28 00:00:28 +02:00
|
|
|
const targetsOrig = JSON.parse(JSON.stringify(targets));
|
|
|
|
// preserving original since it will be mutated
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
// without forcing `HDSegwitBech32Wallet` i had a weird ts error, complaining about last argument (fp)
|
|
|
|
const { tx, outputs, psbt, fee } = (wallet as HDSegwitBech32Wallet)?.createTransaction(
|
2021-02-25 17:13:34 +01:00
|
|
|
lutxo,
|
2019-12-11 05:13:40 +01:00
|
|
|
targets,
|
|
|
|
requestedSatPerByte,
|
2023-07-25 15:50:04 +02:00
|
|
|
change,
|
2021-02-25 17:13:34 +01:00
|
|
|
isTransactionReplaceable ? HDSegwitBech32Wallet.defaultRBFSequence : HDSegwitBech32Wallet.finalRBFSequence,
|
2024-05-22 21:35:36 +02:00
|
|
|
false,
|
|
|
|
0,
|
2019-12-11 05:13:40 +01:00
|
|
|
);
|
2019-09-27 16:49:56 +02:00
|
|
|
|
2021-09-09 13:00:11 +02:00
|
|
|
if (tx && routeParams.launchedBy && psbt) {
|
|
|
|
console.warn('navigating back to ', routeParams.launchedBy);
|
2024-08-05 04:13:25 +02:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
// @ts-ignore idk how to fix FIXME?
|
2024-06-30 19:17:55 +02:00
|
|
|
|
2021-09-09 13:00:11 +02:00
|
|
|
navigation.navigate(routeParams.launchedBy, { psbt });
|
|
|
|
}
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type === WatchOnlyWallet.type) {
|
2019-09-27 16:49:56 +02:00
|
|
|
// 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
|
2021-02-25 17:13:34 +01:00
|
|
|
navigation.navigate('PsbtWithHardwareWallet', {
|
2021-08-28 06:49:46 +02:00
|
|
|
memo: transactionMemo,
|
2024-10-03 02:25:54 +02:00
|
|
|
walletID: wallet.getID(),
|
2019-12-12 04:17:09 +01:00
|
|
|
psbt,
|
2021-09-09 13:00:11 +02:00
|
|
|
launchedBy: routeParams.launchedBy,
|
2019-12-12 04:17:09 +01:00
|
|
|
});
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2019-09-27 16:49:56 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-06-01 22:44:39 +02:00
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type === MultisigHDWallet.type) {
|
2021-02-25 17:13:34 +01:00
|
|
|
navigation.navigate('PsbtMultisig', {
|
2021-08-28 06:49:46 +02:00
|
|
|
memo: transactionMemo,
|
2020-10-05 23:25:14 +02:00
|
|
|
psbtBase64: psbt.toBase64(),
|
2020-12-01 06:40:54 +01:00
|
|
|
walletID: wallet.getID(),
|
2021-09-09 13:00:11 +02:00
|
|
|
launchedBy: routeParams.launchedBy,
|
2020-10-05 23:25:14 +02:00
|
|
|
});
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2020-10-05 23:25:14 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
assert(tx, 'createTRansaction failed');
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
txMetadata[tx.getId()] = {
|
2021-08-28 06:49:46 +02:00
|
|
|
memo: transactionMemo,
|
2019-06-01 22:44:39 +02:00
|
|
|
};
|
2021-02-25 17:13:34 +01:00
|
|
|
await saveToDisk();
|
2020-11-06 16:52:12 +01:00
|
|
|
|
2023-07-25 15:50:04 +02:00
|
|
|
let recipients = outputs.filter(({ address }) => address !== change);
|
2021-06-09 15:51:53 +02:00
|
|
|
|
|
|
|
if (recipients.length === 0) {
|
|
|
|
// special case. maybe the only destination in this transaction is our own change address..?
|
|
|
|
// (ez can be the case for single-address wallet when doing self-payment for consolidation)
|
|
|
|
recipients = outputs;
|
|
|
|
}
|
2020-11-06 16:52:12 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
navigation.navigate('Confirm', {
|
2019-12-12 04:17:09 +01:00
|
|
|
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
|
2021-08-28 06:49:46 +02:00
|
|
|
memo: transactionMemo,
|
2021-08-10 16:35:25 +02:00
|
|
|
walletID: wallet.getID(),
|
2019-12-12 04:17:09 +01:00
|
|
|
tx: tx.toHex(),
|
2024-05-28 00:00:28 +02:00
|
|
|
targets: targetsOrig,
|
2020-11-06 16:52:12 +01:00
|
|
|
recipients,
|
2019-12-12 04:17:09 +01:00
|
|
|
satoshiPerByte: requestedSatPerByte,
|
2021-02-25 17:13:34 +01:00
|
|
|
payjoinUrl,
|
2020-09-21 21:32:20 +02:00
|
|
|
psbt,
|
2019-12-12 04:17:09 +01:00
|
|
|
});
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2020-10-26 18:50:03 +01:00
|
|
|
};
|
|
|
|
|
2024-06-08 18:01:56 +02:00
|
|
|
useEffect(() => {
|
|
|
|
const newWallet = wallets.find(w => w.getID() === routeParams.walletID);
|
|
|
|
if (newWallet) {
|
|
|
|
setWallet(newWallet);
|
|
|
|
}
|
2024-08-05 04:13:25 +02:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [routeParams.walletID]);
|
2018-12-24 01:44:31 +01:00
|
|
|
|
2020-11-19 15:33:18 +01:00
|
|
|
/**
|
|
|
|
* same as `importTransaction`, but opens camera instead.
|
|
|
|
*
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2024-10-01 19:26:57 +02:00
|
|
|
const importQrTransaction = async () => {
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type !== WatchOnlyWallet.type) {
|
2024-02-07 20:24:24 +01:00
|
|
|
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
|
2020-11-19 15:33:18 +01:00
|
|
|
}
|
|
|
|
|
2024-10-01 19:26:57 +02:00
|
|
|
const data = await scanQrHelper(route.name, true);
|
|
|
|
importQrTransactionOnBarScanned(data);
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
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?
|
|
|
|
|
|
|
|
// 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,
|
2024-10-03 02:25:54 +02:00
|
|
|
walletID: wallet.getID(),
|
2024-08-05 04:13:25 +02:00
|
|
|
psbt,
|
|
|
|
});
|
|
|
|
setIsLoading(false);
|
|
|
|
}
|
|
|
|
};
|
2020-11-19 15:33:18 +01:00
|
|
|
|
2020-09-16 18:52:10 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* alternatively, user can export psbt file, sign it externally and then import it
|
|
|
|
*
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2024-08-05 04:13:25 +02:00
|
|
|
const importTransaction = async () => {
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type !== WatchOnlyWallet.type) {
|
2024-02-07 20:24:24 +01:00
|
|
|
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
|
2020-09-16 18:52:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-01 04:31:04 +01:00
|
|
|
try {
|
2021-12-27 22:19:37 +01:00
|
|
|
const res = await DocumentPicker.pickSingle({
|
2020-10-05 23:25:14 +02:00
|
|
|
type:
|
|
|
|
Platform.OS === 'ios'
|
2024-09-10 03:39:35 +02:00
|
|
|
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, DocumentPicker.types.json]
|
2020-10-05 23:25:14 +02:00
|
|
|
: [DocumentPicker.types.allFiles],
|
2020-02-24 22:45:14 +01:00
|
|
|
});
|
2020-09-16 18:52:10 +02:00
|
|
|
|
|
|
|
if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(res.uri)) {
|
|
|
|
// we assume that transaction is already signed, so all we have to do is get txhex and pass it to next screen
|
|
|
|
// so user can broadcast:
|
2020-01-04 04:12:29 +01:00
|
|
|
const file = await RNFS.readFile(res.uri, 'ascii');
|
2020-09-16 18:52:10 +02:00
|
|
|
const psbt = bitcoin.Psbt.fromBase64(file);
|
|
|
|
const txhex = psbt.extractTransaction().toHex();
|
2024-10-03 02:25:54 +02:00
|
|
|
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), txhex });
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-06-30 19:17:55 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
return;
|
|
|
|
}
|
2020-09-16 18:52:10 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) {
|
2020-09-16 18:52:10 +02:00
|
|
|
// looks like transaction is UNsigned, so we construct PSBT object and pass to next screen
|
|
|
|
// so user can do smth with it:
|
|
|
|
const file = await RNFS.readFile(res.uri, 'ascii');
|
|
|
|
const psbt = bitcoin.Psbt.fromBase64(file);
|
2024-10-03 02:25:54 +02:00
|
|
|
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), psbt });
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-06-30 19:17:55 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (DeeplinkSchemaMatch.isTXNFile(res.uri)) {
|
2020-09-16 18:52:10 +02:00
|
|
|
// plain text file with txhex ready to broadcast
|
2020-11-27 17:35:14 +01:00
|
|
|
const file = (await RNFS.readFile(res.uri, 'ascii')).replace('\n', '').replace('\r', '');
|
2024-10-03 02:25:54 +02:00
|
|
|
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), txhex: file });
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2024-06-30 19:17:55 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
return;
|
2020-01-01 04:31:04 +01:00
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
|
2024-02-07 20:24:24 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: loc.send.details_unrecognized_file_format });
|
2020-01-01 04:31:04 +01:00
|
|
|
} catch (err) {
|
|
|
|
if (!DocumentPicker.isCancel(err)) {
|
2024-02-07 20:24:24 +01:00
|
|
|
presentAlert({ title: loc.errors.error, message: loc.send.details_no_signed_tx });
|
2020-01-01 04:31:04 +01:00
|
|
|
}
|
|
|
|
}
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2020-01-01 04:31:04 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const askCosignThisTransaction = async () => {
|
2020-10-08 17:39:31 +02:00
|
|
|
return new Promise(resolve => {
|
|
|
|
Alert.alert(
|
|
|
|
'',
|
2021-02-28 08:30:11 +01:00
|
|
|
loc.multisig.cosign_this_transaction,
|
2020-10-08 17:39:31 +02:00
|
|
|
[
|
|
|
|
{
|
|
|
|
text: loc._.no,
|
|
|
|
style: 'cancel',
|
|
|
|
onPress: () => resolve(false),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: loc._.yes,
|
|
|
|
onPress: () => resolve(true),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
{ cancelable: false },
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
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 as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
|
|
|
|
setIsLoading(true);
|
|
|
|
await sleep(100);
|
|
|
|
(wallet as MultisigHDWallet).cosignPsbt(psbt);
|
|
|
|
setIsLoading(false);
|
|
|
|
await sleep(100);
|
|
|
|
}
|
2024-06-17 04:26:43 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
if (wallet) {
|
|
|
|
navigation.navigate('PsbtMultisig', {
|
|
|
|
memo: transactionMemo,
|
|
|
|
psbtBase64: psbt.toBase64(),
|
|
|
|
walletID: wallet.getID(),
|
|
|
|
});
|
2024-05-22 21:35:36 +02:00
|
|
|
}
|
2024-08-05 04:13:25 +02:00
|
|
|
} catch (error: any) {
|
|
|
|
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
|
|
|
|
}
|
|
|
|
setIsLoading(false);
|
|
|
|
};
|
2020-10-05 23:25:14 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
const importTransactionMultisig = () => {
|
2024-05-22 21:35:36 +02:00
|
|
|
return _importTransactionMultisig(false);
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2024-06-17 20:27:45 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
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' });
|
|
|
|
} 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 importTransactionMultisigScanQr = async () => {
|
2024-10-01 19:26:57 +02:00
|
|
|
const data = await scanQrHelper(route.name, true);
|
|
|
|
onBarScanned(data);
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2020-10-08 17:39:31 +02:00
|
|
|
|
2024-08-08 02:32:16 +02:00
|
|
|
const handleAddRecipient = () => {
|
|
|
|
setAddresses(prevAddresses => [...prevAddresses, { address: '', key: String(Math.random()) } as IPaymentDestinations]);
|
|
|
|
|
|
|
|
// Wait for the state to update before scrolling
|
|
|
|
setTimeout(() => {
|
|
|
|
scrollIndex.current = addresses.length; // New index is at the end of the list
|
|
|
|
scrollView.current?.scrollToIndex({
|
|
|
|
index: scrollIndex.current,
|
|
|
|
animated: true,
|
|
|
|
});
|
|
|
|
}, 0);
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2020-09-23 08:30:14 +02:00
|
|
|
|
2024-10-13 09:08:52 +02:00
|
|
|
const onRemoveAllRecipientsConfirmed = useCallback(() => {
|
|
|
|
setAddresses([{ address: '', key: String(Math.random()) } as IPaymentDestinations]);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const handleRemoveAllRecipients = useCallback(() => {
|
|
|
|
Alert.alert(loc.send.details_recipients_title, loc.send.details_add_recc_rem_all_alert_description, [
|
|
|
|
{
|
|
|
|
text: loc._.cancel,
|
|
|
|
onPress: () => {},
|
|
|
|
style: 'cancel',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: loc._.ok,
|
|
|
|
onPress: onRemoveAllRecipientsConfirmed,
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
}, [onRemoveAllRecipientsConfirmed]);
|
|
|
|
|
2024-08-08 02:32:16 +02:00
|
|
|
const handleRemoveRecipient = () => {
|
|
|
|
if (addresses.length > 1) {
|
|
|
|
const newAddresses = [...addresses];
|
|
|
|
newAddresses.splice(scrollIndex.current, 1);
|
|
|
|
|
|
|
|
// Adjust the current index if the last item was removed
|
|
|
|
const newIndex = scrollIndex.current >= newAddresses.length ? newAddresses.length - 1 : scrollIndex.current;
|
|
|
|
|
|
|
|
setAddresses(newAddresses);
|
2024-06-30 19:17:55 +02:00
|
|
|
|
2024-08-08 02:32:16 +02:00
|
|
|
// Wait for the state to update before scrolling
|
|
|
|
setTimeout(() => {
|
|
|
|
scrollView.current?.scrollToIndex({
|
|
|
|
index: newIndex,
|
|
|
|
animated: true,
|
|
|
|
});
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
// Update the scroll index reference
|
|
|
|
scrollIndex.current = newIndex;
|
2024-08-07 06:07:44 +02:00
|
|
|
}
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2020-09-23 08:30:14 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
const handleCoinControl = async () => {
|
2024-05-22 21:35:36 +02:00
|
|
|
if (!wallet) return;
|
2021-02-25 17:13:34 +01:00
|
|
|
navigation.navigate('CoinControl', {
|
2024-05-22 21:35:36 +02:00
|
|
|
walletID: wallet?.getID(),
|
|
|
|
onUTXOChoose: (u: CreateTransactionUtxo[]) => setUtxo(u),
|
2021-02-25 17:13:34 +01:00
|
|
|
});
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2020-11-12 02:58:09 +01:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
const handleInsertContact = async () => {
|
2024-07-22 07:21:40 +02:00
|
|
|
if (!wallet) return;
|
2024-06-07 20:50:50 +02:00
|
|
|
navigation.navigate('PaymentCodeList', { walletID: wallet.getID() });
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2024-06-07 15:37:45 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
const handlePsbtSign = async () => {
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(true);
|
2021-02-18 14:37:43 +01:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations
|
|
|
|
|
2024-07-23 19:44:04 +02:00
|
|
|
const scannedData = await scanQrHelper(name, true, undefined);
|
2024-07-15 04:54:30 +02:00
|
|
|
if (!scannedData) return setIsLoading(false);
|
2021-02-25 17:13:34 +01:00
|
|
|
|
2024-07-15 04:54:30 +02:00
|
|
|
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);
|
|
|
|
}
|
2021-02-25 17:13:34 +01:00
|
|
|
|
2024-07-15 04:54:30 +02:00
|
|
|
if (!tx || !wallet) return setIsLoading(false);
|
2024-07-15 02:11:30 +02:00
|
|
|
|
2024-07-15 04:54:30 +02:00
|
|
|
// 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++) {
|
2024-05-22 21:35:36 +02:00
|
|
|
// @ts-ignore hacky
|
2024-07-15 04:54:30 +02:00
|
|
|
changeAddresses.push(wallet._getInternalAddressByIndex(c));
|
|
|
|
}
|
|
|
|
const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(String(address)));
|
2024-07-15 02:11:30 +02:00
|
|
|
|
2024-07-15 04:54:30 +02:00
|
|
|
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,
|
2021-02-25 17:13:34 +01:00
|
|
|
});
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
2021-08-23 21:39:52 +02:00
|
|
|
|
2024-08-05 04:13:25 +02:00
|
|
|
// Header Right Button
|
|
|
|
|
|
|
|
const headerRightOnPress = (id: string) => {
|
2024-10-13 09:08:52 +02:00
|
|
|
if (id === CommonToolTipActions.AddRecipient.id) {
|
2024-08-05 04:13:25 +02:00
|
|
|
handleAddRecipient();
|
2024-10-13 09:08:52 +02:00
|
|
|
} else if (id === CommonToolTipActions.RemoveRecipient.id) {
|
2024-08-05 04:13:25 +02:00
|
|
|
handleRemoveRecipient();
|
|
|
|
} else if (id === SendDetails.actionKeys.SignPSBT) {
|
|
|
|
handlePsbtSign();
|
|
|
|
} else if (id === SendDetails.actionKeys.SendMax) {
|
|
|
|
onUseAllPressed();
|
|
|
|
} else if (id === SendDetails.actionKeys.AllowRBF) {
|
|
|
|
onReplaceableFeeSwitchValueChanged(!isTransactionReplaceable);
|
|
|
|
} else if (id === SendDetails.actionKeys.ImportTransaction) {
|
|
|
|
importTransaction();
|
|
|
|
} else if (id === SendDetails.actionKeys.ImportTransactionQR) {
|
|
|
|
importQrTransaction();
|
|
|
|
} else if (id === SendDetails.actionKeys.ImportTransactionMultsig) {
|
|
|
|
importTransactionMultisig();
|
|
|
|
} else if (id === SendDetails.actionKeys.CoSignTransaction) {
|
|
|
|
importTransactionMultisigScanQr();
|
|
|
|
} else if (id === SendDetails.actionKeys.CoinControl) {
|
|
|
|
handleCoinControl();
|
|
|
|
} else if (id === SendDetails.actionKeys.InsertContact) {
|
|
|
|
handleInsertContact();
|
2024-10-13 09:08:52 +02:00
|
|
|
} else if (id === CommonToolTipActions.RemoveAllRecipients.id) {
|
|
|
|
handleRemoveAllRecipients();
|
2024-08-05 04:13:25 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const headerRightActions = () => {
|
2024-06-17 04:26:43 +02:00
|
|
|
const actions: Action[] & Action[][] = [];
|
2021-09-30 13:33:50 +02:00
|
|
|
if (isEditable) {
|
2024-06-07 15:37:45 +02:00
|
|
|
if (wallet?.allowBIP47() && wallet?.isBIP47Enabled()) {
|
2024-06-17 20:27:45 +02:00
|
|
|
actions.push([
|
|
|
|
{ id: SendDetails.actionKeys.InsertContact, text: loc.send.details_insert_contact, icon: SendDetails.actionIcons.InsertContact },
|
|
|
|
]);
|
2024-06-07 15:37:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (Number(wallet?.getBalance()) > 0) {
|
|
|
|
const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX);
|
2021-08-23 21:39:52 +02:00
|
|
|
|
2024-06-17 20:27:45 +02:00
|
|
|
actions.push([{ id: SendDetails.actionKeys.SendMax, text: loc.send.details_adv_full, disabled: balance === 0 || isSendMaxUsed }]);
|
2024-06-07 15:37:45 +02:00
|
|
|
}
|
2024-08-04 23:00:50 +02:00
|
|
|
if (wallet?.type === HDSegwitBech32Wallet.type && isTransactionReplaceable !== undefined) {
|
|
|
|
actions.push([{ id: SendDetails.actionKeys.AllowRBF, text: loc.send.details_adv_fee_bump, menuState: !!isTransactionReplaceable }]);
|
2021-09-30 13:33:50 +02:00
|
|
|
}
|
2021-10-04 05:47:01 +02:00
|
|
|
const transactionActions = [];
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd()) {
|
2021-10-04 05:47:01 +02:00
|
|
|
transactionActions.push(
|
2021-09-30 13:33:50 +02:00
|
|
|
{
|
2024-06-17 20:27:45 +02:00
|
|
|
id: SendDetails.actionKeys.ImportTransaction,
|
2021-09-30 13:33:50 +02:00
|
|
|
text: loc.send.details_adv_import,
|
2024-06-17 20:27:45 +02:00
|
|
|
icon: SendDetails.actionIcons.ImportTransaction,
|
2021-09-30 13:33:50 +02:00
|
|
|
},
|
|
|
|
{
|
2024-06-17 20:27:45 +02:00
|
|
|
id: SendDetails.actionKeys.ImportTransactionQR,
|
2021-09-30 13:33:50 +02:00
|
|
|
text: loc.send.details_adv_import_qr,
|
2024-06-17 20:27:45 +02:00
|
|
|
icon: SendDetails.actionIcons.ImportTransactionQR,
|
2021-09-30 13:33:50 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type === MultisigHDWallet.type) {
|
2021-10-04 05:47:01 +02:00
|
|
|
transactionActions.push({
|
2024-06-17 20:27:45 +02:00
|
|
|
id: SendDetails.actionKeys.ImportTransactionMultsig,
|
2021-08-23 21:39:52 +02:00
|
|
|
text: loc.send.details_adv_import,
|
2024-06-17 20:27:45 +02:00
|
|
|
icon: SendDetails.actionIcons.ImportTransactionMultsig,
|
2021-09-30 13:33:50 +02:00
|
|
|
});
|
|
|
|
}
|
2024-05-22 21:35:36 +02:00
|
|
|
if (wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) {
|
2021-10-04 05:47:01 +02:00
|
|
|
transactionActions.push({
|
2024-06-17 20:27:45 +02:00
|
|
|
id: SendDetails.actionKeys.CoSignTransaction,
|
2021-09-30 13:33:50 +02:00
|
|
|
text: loc.multisig.co_sign_transaction,
|
2024-06-17 20:27:45 +02:00
|
|
|
icon: SendDetails.actionIcons.SignPSBT,
|
2021-09-30 13:33:50 +02:00
|
|
|
});
|
|
|
|
}
|
2024-05-22 21:35:36 +02:00
|
|
|
if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) {
|
2024-06-17 20:27:45 +02:00
|
|
|
transactionActions.push({ id: SendDetails.actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: SendDetails.actionIcons.SignPSBT });
|
2021-09-30 13:33:50 +02:00
|
|
|
}
|
2024-10-13 09:08:52 +02:00
|
|
|
actions.push(transactionActions);
|
|
|
|
|
|
|
|
const recipientActions: Action[] = [CommonToolTipActions.AddRecipient, CommonToolTipActions.RemoveRecipient];
|
|
|
|
if (addresses.length > 1) {
|
|
|
|
recipientActions.push(CommonToolTipActions.RemoveAllRecipients);
|
|
|
|
}
|
|
|
|
actions.push(recipientActions);
|
2021-08-23 21:39:52 +02:00
|
|
|
}
|
2021-09-30 13:33:50 +02:00
|
|
|
|
2024-06-17 20:27:45 +02:00
|
|
|
actions.push({ id: SendDetails.actionKeys.CoinControl, text: loc.cc.header, icon: SendDetails.actionIcons.CoinControl });
|
2021-08-23 21:39:52 +02:00
|
|
|
|
|
|
|
return actions;
|
2024-08-05 04:13:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const setHeaderRightOptions = () => {
|
|
|
|
navigation.setOptions({
|
|
|
|
// eslint-disable-next-line react/no-unstable-nested-components
|
2024-09-27 14:39:31 +02:00
|
|
|
headerRight: () => <HeaderMenuButton disabled={isLoading} onPressMenuItem={headerRightOnPress} actions={headerRightActions()} />,
|
2024-08-05 04:13:25 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const onReplaceableFeeSwitchValueChanged = (value: boolean) => {
|
|
|
|
setIsTransactionReplaceable(value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleRecipientsScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
|
|
const contentOffset = e.nativeEvent.contentOffset;
|
|
|
|
const viewSize = e.nativeEvent.layoutMeasurement;
|
|
|
|
const index = Math.floor(contentOffset.x / viewSize.width);
|
|
|
|
scrollIndex.current = index;
|
|
|
|
};
|
2021-03-12 15:02:49 +01:00
|
|
|
|
2024-08-05 12:32:10 +02:00
|
|
|
const onUseAllPressed = () => {
|
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
2024-06-17 20:27:45 +02:00
|
|
|
const message = frozenBalance > 0 ? loc.send.details_adv_full_sure_frozen : loc.send.details_adv_full_sure;
|
2024-09-06 01:58:25 +02:00
|
|
|
|
|
|
|
const anchor = findNodeHandle(scrollView.current);
|
|
|
|
const options = {
|
|
|
|
title: loc.send.details_adv_full,
|
2024-06-17 20:27:45 +02:00
|
|
|
message,
|
2024-09-06 01:58:25 +02:00
|
|
|
options: [loc._.cancel, loc._.ok],
|
|
|
|
cancelButtonIndex: 0,
|
|
|
|
anchor: anchor ?? undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
ActionSheet.showActionSheetWithOptions(options, buttonIndex => {
|
|
|
|
if (buttonIndex === 1) {
|
|
|
|
Keyboard.dismiss();
|
|
|
|
setAddresses(addrs => {
|
|
|
|
addrs[scrollIndex.current].amount = BitcoinUnit.MAX;
|
|
|
|
addrs[scrollIndex.current].amountSats = BitcoinUnit.MAX;
|
|
|
|
return [...addrs];
|
|
|
|
});
|
|
|
|
setUnits(u => {
|
|
|
|
u[scrollIndex.current] = BitcoinUnit.BTC;
|
|
|
|
return [...u];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2024-06-17 20:27:45 +02:00
|
|
|
};
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
const formatFee = (fee: number) => formatBalance(fee, feeUnit!, true);
|
2021-02-25 17:13:34 +01:00
|
|
|
|
|
|
|
const stylesHook = StyleSheet.create({
|
|
|
|
root: {
|
|
|
|
backgroundColor: colors.elevated,
|
|
|
|
},
|
2024-07-31 01:05:58 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
selectLabel: {
|
|
|
|
color: colors.buttonTextColor,
|
|
|
|
},
|
|
|
|
of: {
|
|
|
|
color: colors.feeText,
|
|
|
|
},
|
|
|
|
memo: {
|
|
|
|
borderColor: colors.formBorder,
|
|
|
|
borderBottomColor: colors.formBorder,
|
|
|
|
backgroundColor: colors.inputBackgroundColor,
|
|
|
|
},
|
|
|
|
feeLabel: {
|
|
|
|
color: colors.feeText,
|
|
|
|
},
|
2024-08-04 21:41:49 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
feeRow: {
|
|
|
|
backgroundColor: colors.feeLabel,
|
|
|
|
},
|
|
|
|
feeValue: {
|
|
|
|
color: colors.feeValue,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-06-07 22:11:57 +02:00
|
|
|
const calculateTotalAmount = () => {
|
|
|
|
const totalAmount = addresses.reduce((total, item) => total + Number(item.amountSats || 0), 0);
|
|
|
|
const totalWithFee = totalAmount + (feePrecalc.current || 0);
|
|
|
|
return totalWithFee;
|
|
|
|
};
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const renderCreateButton = () => {
|
2024-06-07 22:11:57 +02:00
|
|
|
const totalWithFee = calculateTotalAmount();
|
|
|
|
const isDisabled = totalWithFee === 0 || totalWithFee > balance || balance === 0 || isLoading || addresses.length === 0;
|
|
|
|
|
2018-10-20 23:10:21 +02:00
|
|
|
return (
|
2020-05-24 11:17:26 +02:00
|
|
|
<View style={styles.createButton}>
|
2021-02-25 17:13:34 +01:00
|
|
|
{isLoading ? (
|
2020-04-27 10:23:56 +02:00
|
|
|
<ActivityIndicator />
|
|
|
|
) : (
|
2024-06-07 22:11:57 +02:00
|
|
|
<Button onPress={createTransaction} disabled={isDisabled} title={loc.send.details_next} testID="CreateTransactionButton" />
|
2020-04-27 10:23:56 +02:00
|
|
|
)}
|
2018-10-20 23:10:21 +02:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
2018-01-30 23:42:38 +01:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const renderWalletSelectionOrCoinsSelected = () => {
|
2021-03-05 02:19:44 +01:00
|
|
|
if (walletSelectionOrCoinsSelectedHidden) return null;
|
2021-02-25 17:13:34 +01:00
|
|
|
if (utxo !== null) {
|
2020-11-09 10:24:17 +01:00
|
|
|
return (
|
|
|
|
<View style={styles.select}>
|
2020-11-23 04:19:46 +01:00
|
|
|
<CoinsSelected
|
2021-02-25 17:13:34 +01:00
|
|
|
number={utxo.length}
|
|
|
|
onContainerPress={handleCoinControl}
|
2020-11-23 04:19:46 +01:00
|
|
|
onClose={() => {
|
|
|
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
2021-02-25 17:13:34 +01:00
|
|
|
setUtxo(null);
|
2020-11-23 04:19:46 +01:00
|
|
|
}}
|
|
|
|
/>
|
2020-11-09 10:24:17 +01:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-12-24 01:44:31 +01:00
|
|
|
return (
|
2020-05-24 11:17:26 +02:00
|
|
|
<View style={styles.select}>
|
2021-09-09 13:00:11 +02:00
|
|
|
{!isLoading && isEditable && (
|
2018-12-24 01:44:31 +01:00
|
|
|
<TouchableOpacity
|
2021-06-24 14:50:57 +02:00
|
|
|
accessibilityRole="button"
|
2020-05-24 11:17:26 +02:00
|
|
|
style={styles.selectTouch}
|
2024-06-30 19:17:55 +02:00
|
|
|
onPress={() => {
|
|
|
|
navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN });
|
|
|
|
}}
|
2018-12-24 01:44:31 +01:00
|
|
|
>
|
2020-05-24 11:17:26 +02:00
|
|
|
<Text style={styles.selectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
|
2021-03-19 03:30:01 +01:00
|
|
|
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
|
2018-12-24 01:44:31 +01:00
|
|
|
</TouchableOpacity>
|
|
|
|
)}
|
2020-05-24 11:17:26 +02:00
|
|
|
<View style={styles.selectWrap}>
|
2019-08-10 08:57:55 +02:00
|
|
|
<TouchableOpacity
|
2021-06-24 14:50:57 +02:00
|
|
|
accessibilityRole="button"
|
2020-05-24 11:17:26 +02:00
|
|
|
style={styles.selectTouch}
|
2024-06-30 19:17:55 +02:00
|
|
|
onPress={() => {
|
|
|
|
navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN });
|
|
|
|
}}
|
2021-10-26 22:47:09 +02:00
|
|
|
disabled={!isEditable || isLoading}
|
2019-08-06 19:31:23 +02:00
|
|
|
>
|
2024-05-22 21:35:36 +02:00
|
|
|
<Text style={[styles.selectLabel, stylesHook.selectLabel]}>{wallet?.getLabel()}</Text>
|
2019-08-10 08:57:55 +02:00
|
|
|
</TouchableOpacity>
|
2018-12-24 01:44:31 +01:00
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-05-22 21:35:36 +02:00
|
|
|
const renderBitcoinTransactionInfoFields = (params: { item: IPaymentDestinations; index: number }) => {
|
2021-02-25 17:13:34 +01:00
|
|
|
const { item, index } = params;
|
2020-11-10 15:21:54 +01:00
|
|
|
return (
|
2021-02-25 17:13:34 +01:00
|
|
|
<View style={{ width }} testID={'Transaction' + index}>
|
|
|
|
<AmountInput
|
|
|
|
isLoading={isLoading}
|
2020-11-10 15:21:54 +01:00
|
|
|
amount={item.amount ? item.amount.toString() : null}
|
2024-05-22 21:35:36 +02:00
|
|
|
onAmountUnitChange={(unit: BitcoinUnit) => {
|
2023-07-25 15:50:04 +02:00
|
|
|
setAddresses(addrs => {
|
|
|
|
const addr = addrs[index];
|
2021-03-12 17:19:04 +01:00
|
|
|
|
|
|
|
switch (unit) {
|
|
|
|
case BitcoinUnit.SATS:
|
2024-05-22 21:35:36 +02:00
|
|
|
addr.amountSats = parseInt(String(addr.amount), 10);
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
case BitcoinUnit.BTC:
|
2024-05-22 21:35:36 +02:00
|
|
|
addr.amountSats = btcToSatoshi(String(addr.amount));
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
case BitcoinUnit.LOCAL_CURRENCY:
|
|
|
|
// also accounting for cached fiat->sat conversion to avoid rounding error
|
2024-05-22 21:35:36 +02:00
|
|
|
addr.amountSats = AmountInput.getCachedSatoshis(addr.amount) || btcToSatoshi(fiatToBTC(Number(addr.amount)));
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-07-25 15:50:04 +02:00
|
|
|
addrs[index] = addr;
|
|
|
|
return [...addrs];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2023-07-25 15:50:04 +02:00
|
|
|
setUnits(u => {
|
|
|
|
u[index] = unit;
|
|
|
|
return [...u];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2020-11-10 15:21:54 +01:00
|
|
|
}}
|
2024-05-22 21:35:36 +02:00
|
|
|
onChangeText={(text: string) => {
|
2023-07-25 15:50:04 +02:00
|
|
|
setAddresses(addrs => {
|
2021-03-12 17:19:04 +01:00
|
|
|
item.amount = text;
|
|
|
|
switch (units[index] || amountUnit) {
|
|
|
|
case BitcoinUnit.BTC:
|
2024-01-28 16:11:08 +01:00
|
|
|
item.amountSats = btcToSatoshi(item.amount);
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
case BitcoinUnit.LOCAL_CURRENCY:
|
2024-05-22 21:35:36 +02:00
|
|
|
item.amountSats = btcToSatoshi(fiatToBTC(Number(item.amount)));
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
case BitcoinUnit.SATS:
|
2021-05-08 08:41:45 +02:00
|
|
|
default:
|
2023-07-25 15:50:04 +02:00
|
|
|
item.amountSats = parseInt(text, 10);
|
2021-03-12 17:19:04 +01:00
|
|
|
break;
|
|
|
|
}
|
2023-07-25 15:50:04 +02:00
|
|
|
addrs[index] = item;
|
|
|
|
return [...addrs];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2020-11-10 15:21:54 +01:00
|
|
|
}}
|
2021-02-25 17:13:34 +01:00
|
|
|
unit={units[index] || amountUnit}
|
2021-09-09 13:00:11 +02:00
|
|
|
editable={isEditable}
|
|
|
|
disabled={!isEditable}
|
2024-08-24 20:06:17 +02:00
|
|
|
inputAccessoryViewID={InputAccessoryAllFundsAccessoryViewID}
|
2020-11-10 15:21:54 +01:00
|
|
|
/>
|
2021-12-23 10:46:29 +01:00
|
|
|
|
|
|
|
{frozenBalance > 0 && (
|
2023-03-31 17:32:07 +02:00
|
|
|
<TouchableOpacity accessibilityRole="button" style={styles.frozenContainer} onPress={handleCoinControl}>
|
2021-12-23 10:46:29 +01:00
|
|
|
<BlueText>
|
|
|
|
{loc.formatString(loc.send.details_frozen, { amount: formatBalanceWithoutSuffix(frozenBalance, BitcoinUnit.BTC, true) })}
|
|
|
|
</BlueText>
|
|
|
|
</TouchableOpacity>
|
|
|
|
)}
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
<AddressInput
|
2021-03-24 18:17:17 +01:00
|
|
|
onChangeText={text => {
|
2020-11-10 15:21:54 +01:00
|
|
|
text = text.trim();
|
2023-07-25 15:50:04 +02:00
|
|
|
const { address, amount, memo, payjoinUrl: pjUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
|
|
|
|
setAddresses(addrs => {
|
2021-03-12 17:19:04 +01:00
|
|
|
item.address = address || text;
|
|
|
|
item.amount = amount || item.amount;
|
2023-07-25 15:50:04 +02:00
|
|
|
addrs[index] = item;
|
|
|
|
return [...addrs];
|
2021-03-12 17:19:04 +01:00
|
|
|
});
|
2021-08-28 06:49:46 +02:00
|
|
|
setTransactionMemo(memo || transactionMemo);
|
2021-02-25 17:13:34 +01:00
|
|
|
setIsLoading(false);
|
2023-07-25 15:50:04 +02:00
|
|
|
setPayjoinUrl(pjUrl);
|
2020-11-10 15:21:54 +01:00
|
|
|
}}
|
2021-02-25 17:13:34 +01:00
|
|
|
onBarScanned={processAddressData}
|
2020-11-10 15:21:54 +01:00
|
|
|
address={item.address}
|
2021-02-25 17:13:34 +01:00
|
|
|
isLoading={isLoading}
|
2024-05-22 21:35:36 +02:00
|
|
|
/* @ts-ignore marcos fixme */
|
2024-08-24 22:15:22 +02:00
|
|
|
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
|
2021-02-25 17:13:34 +01:00
|
|
|
launchedBy={name}
|
2021-09-09 13:00:11 +02:00
|
|
|
editable={isEditable}
|
2020-11-10 15:21:54 +01:00
|
|
|
/>
|
2021-02-25 17:13:34 +01:00
|
|
|
{addresses.length > 1 && (
|
2021-06-06 11:38:03 +02:00
|
|
|
<Text style={[styles.of, stylesHook.of]}>{loc.formatString(loc._.of, { number: index + 1, total: addresses.length })}</Text>
|
2020-11-10 15:21:54 +01:00
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
);
|
2019-09-03 05:28:52 +02:00
|
|
|
};
|
|
|
|
|
2024-08-08 02:32:16 +02:00
|
|
|
const getItemLayout = (_: any, index: number) => ({
|
|
|
|
length: width,
|
|
|
|
offset: width * index,
|
|
|
|
index,
|
|
|
|
});
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
return (
|
2024-04-22 02:35:17 +02:00
|
|
|
<View style={[styles.root, stylesHook.root]} onLayout={e => setWidth(e.nativeEvent.layout.width)}>
|
|
|
|
<View>
|
2024-08-20 21:22:11 +02:00
|
|
|
<FlatList
|
|
|
|
keyboardShouldPersistTaps="always"
|
|
|
|
scrollEnabled={addresses.length > 1}
|
|
|
|
data={addresses}
|
|
|
|
renderItem={renderBitcoinTransactionInfoFields}
|
|
|
|
horizontal
|
|
|
|
ref={scrollView}
|
|
|
|
automaticallyAdjustKeyboardInsets
|
|
|
|
pagingEnabled
|
|
|
|
removeClippedSubviews={false}
|
|
|
|
onMomentumScrollBegin={Keyboard.dismiss}
|
|
|
|
onScroll={handleRecipientsScroll}
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
scrollIndicatorInsets={styles.scrollViewIndicator}
|
|
|
|
contentContainerStyle={styles.scrollViewContent}
|
|
|
|
getItemLayout={getItemLayout}
|
|
|
|
/>
|
|
|
|
<View style={[styles.memo, stylesHook.memo]}>
|
|
|
|
<TextInput
|
|
|
|
onChangeText={setTransactionMemo}
|
|
|
|
placeholder={loc.send.details_note_placeholder}
|
|
|
|
placeholderTextColor="#81868e"
|
|
|
|
value={transactionMemo}
|
|
|
|
numberOfLines={1}
|
|
|
|
style={styles.memoText}
|
|
|
|
editable={!isLoading}
|
|
|
|
onSubmitEditing={Keyboard.dismiss}
|
|
|
|
/* @ts-ignore marcos fixme */
|
2024-08-24 20:06:17 +02:00
|
|
|
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
|
2024-08-04 21:41:49 +02:00
|
|
|
/>
|
2024-08-20 21:22:11 +02:00
|
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
|
|
testID="chooseFee"
|
|
|
|
accessibilityRole="button"
|
|
|
|
onPress={() => feeModalRef.current?.present()}
|
|
|
|
disabled={isLoading}
|
|
|
|
style={styles.fee}
|
|
|
|
>
|
|
|
|
<Text style={[styles.feeLabel, stylesHook.feeLabel]}>{loc.send.create_fee}</Text>
|
|
|
|
|
|
|
|
{networkTransactionFeesIsLoading ? (
|
|
|
|
<ActivityIndicator />
|
|
|
|
) : (
|
|
|
|
<View style={[styles.feeRow, stylesHook.feeRow]}>
|
|
|
|
<Text style={stylesHook.feeValue}>
|
|
|
|
{feePrecalc.current ? formatFee(feePrecalc.current) : feeRate + ' ' + loc.units.sat_vbyte}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
</TouchableOpacity>
|
|
|
|
{renderCreateButton()}
|
|
|
|
<SelectFeeModal
|
|
|
|
ref={feeModalRef}
|
|
|
|
networkTransactionFees={networkTransactionFees}
|
|
|
|
feePrecalc={feePrecalc}
|
|
|
|
feeRate={feeRate}
|
|
|
|
setCustomFee={setCustomFee}
|
|
|
|
setFeePrecalc={setFeePrecalc}
|
2024-09-19 04:24:36 +02:00
|
|
|
feeUnit={units[scrollIndex.current]}
|
2024-08-20 21:22:11 +02:00
|
|
|
/>
|
2021-02-25 17:13:34 +01:00
|
|
|
</View>
|
2024-08-24 20:06:17 +02:00
|
|
|
<DismissKeyboardInputAccessory />
|
2024-04-22 02:35:17 +02:00
|
|
|
{Platform.select({
|
2024-05-22 21:35:36 +02:00
|
|
|
ios: <InputAccessoryAllFunds canUseAll={balance > 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} />,
|
2024-04-22 02:35:17 +02:00
|
|
|
android: isAmountToolbarVisibleForAndroid && (
|
2024-05-22 21:35:36 +02:00
|
|
|
<InputAccessoryAllFunds canUseAll={balance > 0} onUseAllPressed={onUseAllPressed} balance={String(allBalance)} />
|
2024-04-22 02:35:17 +02:00
|
|
|
),
|
|
|
|
})}
|
|
|
|
|
|
|
|
{renderWalletSelectionOrCoinsSelected()}
|
|
|
|
</View>
|
2021-02-25 17:13:34 +01:00
|
|
|
);
|
2018-03-18 03:48:23 +01:00
|
|
|
};
|
2020-07-15 19:32:59 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
export default SendDetails;
|
|
|
|
|
2024-06-17 20:27:45 +02:00
|
|
|
SendDetails.actionKeys = {
|
2024-06-07 15:37:45 +02:00
|
|
|
InsertContact: 'InsertContact',
|
2021-08-23 21:39:52 +02:00
|
|
|
SignPSBT: 'SignPSBT',
|
|
|
|
SendMax: 'SendMax',
|
|
|
|
AllowRBF: 'AllowRBF',
|
|
|
|
ImportTransaction: 'ImportTransaction',
|
|
|
|
ImportTransactionMultsig: 'ImportTransactionMultisig',
|
|
|
|
ImportTransactionQR: 'ImportTransactionQR',
|
|
|
|
CoinControl: 'CoinControl',
|
2021-09-06 21:14:30 +02:00
|
|
|
CoSignTransaction: 'CoSignTransaction',
|
2021-08-23 21:39:52 +02:00
|
|
|
};
|
|
|
|
|
2024-06-17 20:27:45 +02:00
|
|
|
SendDetails.actionIcons = {
|
2024-06-17 21:57:13 +02:00
|
|
|
InsertContact: { iconValue: 'at.badge.plus' },
|
|
|
|
SignPSBT: { iconValue: 'signature' },
|
2021-08-23 21:39:52 +02:00
|
|
|
SendMax: 'SendMax',
|
|
|
|
AllowRBF: 'AllowRBF',
|
2024-06-17 21:57:13 +02:00
|
|
|
ImportTransaction: { iconValue: 'square.and.arrow.down' },
|
|
|
|
ImportTransactionMultsig: { iconValue: 'square.and.arrow.down.on.square' },
|
|
|
|
ImportTransactionQR: { iconValue: 'qrcode.viewfinder' },
|
|
|
|
CoinControl: { iconValue: 'switch.2' },
|
2021-08-23 21:39:52 +02:00
|
|
|
};
|
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
root: {
|
|
|
|
flex: 1,
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
},
|
|
|
|
scrollViewContent: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
},
|
2021-03-12 15:02:49 +01:00
|
|
|
scrollViewIndicator: {
|
|
|
|
top: 0,
|
|
|
|
left: 8,
|
|
|
|
bottom: 0,
|
|
|
|
right: 8,
|
|
|
|
},
|
2021-02-25 17:13:34 +01:00
|
|
|
createButton: {
|
|
|
|
marginVertical: 16,
|
|
|
|
marginHorizontal: 16,
|
|
|
|
alignContent: 'center',
|
|
|
|
minHeight: 44,
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
marginBottom: 24,
|
|
|
|
marginHorizontal: 24,
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
selectTouch: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
selectText: {
|
|
|
|
color: '#9aa0aa',
|
|
|
|
fontSize: 14,
|
|
|
|
marginRight: 8,
|
|
|
|
},
|
|
|
|
selectWrap: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
marginVertical: 4,
|
|
|
|
},
|
|
|
|
selectLabel: {
|
|
|
|
fontSize: 14,
|
|
|
|
},
|
|
|
|
of: {
|
|
|
|
alignSelf: 'flex-end',
|
|
|
|
marginRight: 18,
|
|
|
|
marginVertical: 8,
|
|
|
|
},
|
2024-07-31 01:05:58 +02:00
|
|
|
|
2021-02-25 17:13:34 +01:00
|
|
|
memo: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
borderWidth: 1,
|
|
|
|
borderBottomWidth: 0.5,
|
|
|
|
minHeight: 44,
|
|
|
|
height: 44,
|
|
|
|
marginHorizontal: 20,
|
|
|
|
alignItems: 'center',
|
|
|
|
marginVertical: 8,
|
|
|
|
borderRadius: 4,
|
|
|
|
},
|
|
|
|
memoText: {
|
|
|
|
flex: 1,
|
|
|
|
marginHorizontal: 8,
|
|
|
|
minHeight: 33,
|
|
|
|
color: '#81868e',
|
|
|
|
},
|
|
|
|
fee: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
marginHorizontal: 20,
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
feeLabel: {
|
|
|
|
fontSize: 14,
|
|
|
|
},
|
|
|
|
feeRow: {
|
|
|
|
minWidth: 40,
|
|
|
|
height: 25,
|
|
|
|
borderRadius: 4,
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
paddingHorizontal: 10,
|
|
|
|
},
|
2021-12-23 10:46:29 +01:00
|
|
|
frozenContainer: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'center',
|
|
|
|
marginVertical: 8,
|
|
|
|
},
|
2021-02-25 17:13:34 +01:00
|
|
|
});
|