mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-01-19 05:45:15 +01:00
commit
bd68d10d90
@ -1012,14 +1012,11 @@ export class BlueList extends Component {
|
||||
export class BlueUseAllFundsButton extends Component {
|
||||
static InputAccessoryViewID = 'useMaxInputAccessoryViewID';
|
||||
static propTypes = {
|
||||
wallet: PropTypes.shape().isRequired,
|
||||
balance: PropTypes.string.isRequired,
|
||||
canUseAll: PropTypes.bool.isRequired,
|
||||
onUseAllPressed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
unit: BitcoinUnit.BTC,
|
||||
};
|
||||
|
||||
render() {
|
||||
const inputView = (
|
||||
<View
|
||||
@ -1047,11 +1044,11 @@ export class BlueUseAllFundsButton extends Component {
|
||||
>
|
||||
{loc.send.input_total}
|
||||
</Text>
|
||||
{this.props.wallet.allowSendMax() && this.props.wallet.getBalance() > 0 ? (
|
||||
{this.props.canUseAll ? (
|
||||
<BlueButtonLink
|
||||
onPress={this.props.onUseAllPressed}
|
||||
style={{ marginLeft: 8, paddingRight: 0, paddingLeft: 0, paddingTop: 12, paddingBottom: 12 }}
|
||||
title={`${formatBalanceWithoutSuffix(this.props.wallet.getBalance(), BitcoinUnit.BTC, true).toString()} ${BitcoinUnit.BTC}`}
|
||||
title={`${this.props.balance} ${BitcoinUnit.BTC}`}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
@ -1066,7 +1063,7 @@ export class BlueUseAllFundsButton extends Component {
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
>
|
||||
{formatBalanceWithoutSuffix(this.props.wallet.getBalance(), BitcoinUnit.BTC, true).toString()} {BitcoinUnit.BTC}
|
||||
{this.props.balance} {BitcoinUnit.BTC}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@ -1079,6 +1076,7 @@ export class BlueUseAllFundsButton extends Component {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return <InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
|
||||
} else {
|
||||
|
@ -60,6 +60,7 @@ import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
|
||||
import PsbtMultisig from './screen/send/psbtMultisig';
|
||||
import Success from './screen/send/success';
|
||||
import Broadcast from './screen/send/broadcast';
|
||||
import CoinControl from './screen/send/coinControl';
|
||||
|
||||
import ScanLndInvoice from './screen/lnd/scanLndInvoice';
|
||||
import LappBrowser from './screen/lnd/browser';
|
||||
@ -184,6 +185,7 @@ const SendDetailsRoot = () => (
|
||||
}}
|
||||
/>
|
||||
<SendDetailsStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions} />
|
||||
<SendDetailsStack.Screen name="CoinControl" component={CoinControl} options={CoinControl.navigationOptions} />
|
||||
</SendDetailsStack.Navigator>
|
||||
);
|
||||
|
||||
|
@ -714,11 +714,21 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
* wif: 'string',
|
||||
* confirmations: 0 } ]
|
||||
*
|
||||
* @param respectFrozen {boolean} Add Frozen outputs
|
||||
* @returns {[]}
|
||||
*/
|
||||
getUtxo() {
|
||||
if (this._utxo.length === 0) return this.getDerivedUtxoFromOurTransaction(); // oy vey, no stored utxo. lets attempt to derive it from stored transactions
|
||||
return this._utxo;
|
||||
getUtxo(respectFrozen = false) {
|
||||
let ret = [];
|
||||
|
||||
if (this._utxo.length === 0) {
|
||||
ret = this.getDerivedUtxoFromOurTransaction(); // oy vey, no stored utxo. lets attempt to derive it from stored transactions
|
||||
} else {
|
||||
ret = this._utxo;
|
||||
}
|
||||
if (!respectFrozen) {
|
||||
ret = ret.filter(({ txid, vout }) => !this.getUTXOMetadata(txid, vout).frozen);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
getDerivedUtxoFromOurTransaction(returnSpentUtxoAsWell = false) {
|
||||
@ -1025,4 +1035,17 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if address is a Change address. Needed for Coin control.
|
||||
*
|
||||
* @param address
|
||||
* @returns {Boolean} Either address is a change or not
|
||||
*/
|
||||
addressIsChange(address) {
|
||||
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
|
||||
if (address === this._getInternalAddressByIndex(c)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export class AbstractWallet {
|
||||
this.hideBalance = false;
|
||||
this.userHasSavedExport = false;
|
||||
this._hideTransactionsInWalletsList = false;
|
||||
this._utxoMetadata = {};
|
||||
}
|
||||
|
||||
getID() {
|
||||
@ -280,4 +281,28 @@ export class AbstractWallet {
|
||||
}
|
||||
|
||||
prepareForSerialization() {}
|
||||
|
||||
/*
|
||||
* Get metadata (frozen, memo) for a specific UTXO
|
||||
*
|
||||
* @param {String} txid - transaction id
|
||||
* @param {number} vout - an index number of the output in transaction
|
||||
*/
|
||||
getUTXOMetadata(txid, vout) {
|
||||
return this._utxoMetadata[`${txid}:${vout}`] || {};
|
||||
}
|
||||
|
||||
/*
|
||||
* Set metadata (frozen, memo) for a specific UTXO
|
||||
*
|
||||
* @param {String} txid - transaction id
|
||||
* @param {number} vout - an index number of the output in transaction
|
||||
* @param {{memo: String, frozen: Boolean}} opts - options to attach to UTXO
|
||||
*/
|
||||
setUTXOMetadata(txid, vout, opts) {
|
||||
const meta = this._utxoMetadata[`${txid}:${vout}`] || {};
|
||||
if ('memo' in opts) meta.memo = opts.memo;
|
||||
if ('frozen' in opts) meta.frozen = opts.frozen;
|
||||
this._utxoMetadata[`${txid}:${vout}`] = meta;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,10 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
|
||||
return true;
|
||||
}
|
||||
|
||||
allowSendMax() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getXpub() {
|
||||
if (this._xpub) {
|
||||
return this._xpub; // cache hit
|
||||
@ -148,8 +152,4 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
allowSendMax() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -139,13 +139,31 @@ export class LegacyWallet extends AbstractWallet {
|
||||
}
|
||||
}
|
||||
|
||||
getUtxo() {
|
||||
const ret = [];
|
||||
/**
|
||||
* Getter for previously fetched UTXO. For example:
|
||||
* [ { height: 0,
|
||||
* value: 666,
|
||||
* address: 'string',
|
||||
* txId: 'string',
|
||||
* vout: 1,
|
||||
* txid: 'string',
|
||||
* amount: 666,
|
||||
* wif: 'string',
|
||||
* confirmations: 0 } ]
|
||||
*
|
||||
* @param respectFrozen {boolean} Add Frozen outputs
|
||||
* @returns {[]}
|
||||
*/
|
||||
getUtxo(respectFrozen = false) {
|
||||
let ret = [];
|
||||
for (const u of this.utxo) {
|
||||
if (u.txId) u.txid = u.txId;
|
||||
if (!u.confirmations && u.height) u.confirmations = BlueElectrum.estimateCurrentBlockheight() - u.height;
|
||||
ret.push(u);
|
||||
}
|
||||
if (!respectFrozen) {
|
||||
ret = ret.filter(({ txid, vout }) => !this.getUTXOMetadata(txid, vout).frozen);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -400,4 +418,15 @@ export class LegacyWallet extends AbstractWallet {
|
||||
allowSendMax() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if address is a Change address. Needed for Coin control.
|
||||
* Useless for Legacy wallets, so it is always false
|
||||
*
|
||||
* @param address
|
||||
* @returns {Boolean} Either address is a change or not
|
||||
*/
|
||||
addressIsChange(address) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -150,8 +150,8 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
getUtxo() {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo();
|
||||
getUtxo(...args) {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(...args);
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
@ -252,4 +252,19 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
addressIsChange(...args) {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.addressIsChange(...args);
|
||||
return super.addressIsChange(...args);
|
||||
}
|
||||
|
||||
getUTXOMetadata(...args) {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.getUTXOMetadata(...args);
|
||||
return super.getUTXOMetadata(...args);
|
||||
}
|
||||
|
||||
setUTXOMetadata(...args) {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.setUTXOMetadata(...args);
|
||||
return super.setUTXOMetadata(...args);
|
||||
}
|
||||
}
|
||||
|
53
components/CoinsSelected.js
Normal file
53
components/CoinsSelected.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Avatar } from 'react-native-elements';
|
||||
|
||||
import loc from '../loc';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#3477F6',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 16,
|
||||
},
|
||||
labelText: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
buttonContainer: {
|
||||
width: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
ball: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.32)',
|
||||
},
|
||||
});
|
||||
|
||||
const CoinsSelected = ({ number, onClose }) => (
|
||||
<View style={styles.root}>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text style={styles.labelText}>{loc.formatString(loc.cc.coins_selected, { number })}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.buttonContainer} onPress={onClose}>
|
||||
<Avatar rounded containerStyle={[styles.ball]} icon={{ name: 'close', size: 22, type: 'ionicons', color: 'white' }} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
CoinsSelected.propTypes = {
|
||||
number: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CoinsSelected;
|
@ -57,6 +57,8 @@ export const BlueDefaultTheme = {
|
||||
msSuccessBG: '#37c0a1',
|
||||
msSuccessCheck: '#ffffff',
|
||||
newBlue: '#007AFF',
|
||||
redBG: '#F8D2D2',
|
||||
redText: '#D0021B',
|
||||
},
|
||||
};
|
||||
|
||||
@ -102,6 +104,8 @@ export const BlueDarkTheme = {
|
||||
msSuccessBG: '#8EFFE5',
|
||||
msSuccessCheck: '#000000',
|
||||
newBlue: '#007AFF',
|
||||
redBG: '#5A4E4E',
|
||||
redText: '#FC6D6D',
|
||||
},
|
||||
};
|
||||
|
||||
|
10
loc/en.json
10
loc/en.json
@ -454,5 +454,15 @@
|
||||
"this_cosigner_is_already_imported": "This cosigner is already imported",
|
||||
"export_signed_psbt": "Export Signed PSBT",
|
||||
"view_edit_cosigners_title": "Edit Cosigners"
|
||||
},
|
||||
"cc": {
|
||||
"change": "change",
|
||||
"coins_selected": "Coins selected ({number})",
|
||||
"empty": "This wallet doesn't have any coins at the moment",
|
||||
"freeze": "freeze",
|
||||
"freezeLabel": "Freeze",
|
||||
"header": "Coin control",
|
||||
"use_coin": "Use coin",
|
||||
"tip": "Allows you to see, label, freeze or select coins for improved wallet management."
|
||||
}
|
||||
}
|
||||
|
299
screen/send/coinControl.js
Normal file
299
screen/send/coinControl.js
Normal file
@ -0,0 +1,299 @@
|
||||
import React, { useMemo, useState, useContext, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ListItem, Avatar, Badge } from 'react-native-elements';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableWithoutFeedback,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
|
||||
|
||||
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
import { BlueNavigationStyle, SafeBlueArea, BlueSpacing10, BlueSpacing20, BlueButton, BlueListItem } from '../../BlueComponents';
|
||||
import BottomModal from '../../components/BottomModal';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
|
||||
// https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
|
||||
const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
const Output = ({ item: { address, txid, value, vout }, oMemo, frozen, change = false, full = false, onPress }) => {
|
||||
const { colors } = useTheme();
|
||||
const { txMetadata } = useContext(BlueStorageContext);
|
||||
const cs = useColorScheme();
|
||||
const memo = oMemo || txMetadata[txid]?.memo || '';
|
||||
const fullId = `${txid}:${vout}`;
|
||||
const shortId = `${address.substring(0, 9)}...${address.substr(address.length - 9)}`;
|
||||
const color = `#${txid.substring(0, 6)}`;
|
||||
const amount = formatBalanceWithoutSuffix(value, BitcoinUnit.BTC, true);
|
||||
|
||||
const oStyles = StyleSheet.create({
|
||||
containerFull: { paddingHorizontal: 0 },
|
||||
avatar: { borderColor: 'white', borderWidth: 1 },
|
||||
amount: { fontWeight: 'bold' },
|
||||
memo: { fontSize: 13, marginTop: 3 },
|
||||
changeLight: { backgroundColor: colors.buttonDisabledBackgroundColor },
|
||||
changeDark: { backgroundColor: colors.buttonDisabledBackgroundColor, borderWidth: 0 },
|
||||
changeText: { color: colors.alternativeTextColor },
|
||||
freezeLight: { backgroundColor: colors.redBG },
|
||||
freezeDark: { backgroundColor: colors.redBG, borderWidth: 0 },
|
||||
freezeText: { color: colors.redText },
|
||||
});
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
bottomDivider
|
||||
onPress={onPress}
|
||||
containerStyle={[{ borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated }, full && oStyles.containerFull]}
|
||||
>
|
||||
<Avatar rounded overlayContainerStyle={[oStyles.avatar, { backgroundColor: color }]} />
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={[oStyles.amount, { color: colors.foregroundColor }]}>{amount}</ListItem.Title>
|
||||
{full ? (
|
||||
<>
|
||||
{memo ? (
|
||||
<>
|
||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{memo}</ListItem.Subtitle>
|
||||
<BlueSpacing10 />
|
||||
</>
|
||||
) : null}
|
||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{address}</ListItem.Subtitle>
|
||||
<BlueSpacing10 />
|
||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]}>{fullId}</ListItem.Subtitle>
|
||||
</>
|
||||
) : (
|
||||
<ListItem.Subtitle style={[oStyles.memo, { color: colors.alternativeTextColor }]} numberOfLines={1}>
|
||||
{memo || shortId}
|
||||
</ListItem.Subtitle>
|
||||
)}
|
||||
</ListItem.Content>
|
||||
{change && (
|
||||
<Badge value={loc.cc.change} badgeStyle={oStyles[cs === 'dark' ? 'changeDark' : 'changeLight']} textStyle={oStyles.changeText} />
|
||||
)}
|
||||
{frozen && (
|
||||
<Badge value={loc.cc.freeze} badgeStyle={oStyles[cs === 'dark' ? 'freezeDark' : 'freezeLight']} textStyle={oStyles.freezeText} />
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
Output.propTypes = {
|
||||
item: PropTypes.shape({
|
||||
address: PropTypes.string.isRequired,
|
||||
txid: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
vout: PropTypes.number.isRequired,
|
||||
}),
|
||||
oMemo: PropTypes.string,
|
||||
frozen: PropTypes.bool,
|
||||
change: PropTypes.bool,
|
||||
full: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
};
|
||||
|
||||
const mStyles = StyleSheet.create({
|
||||
memoTextInput: {
|
||||
flexDirection: 'row',
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0.5,
|
||||
minHeight: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
marginVertical: 8,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 8,
|
||||
color: '#81868e',
|
||||
},
|
||||
buttonContainer: {
|
||||
height: 45,
|
||||
},
|
||||
});
|
||||
|
||||
const OutputModalContent = ({ output, wallet, onUseCoin }) => {
|
||||
const { colors } = useTheme();
|
||||
const { txMetadata, saveToDisk } = useContext(BlueStorageContext);
|
||||
const [frozen, setFrozen] = useState(wallet.getUTXOMetadata(output.txid, output.vout).frozen || false);
|
||||
const [memo, setMemo] = useState(wallet.getUTXOMetadata(output.txid, output.vout).memo || txMetadata[output.txid]?.memo || '');
|
||||
const onMemoChange = value => setMemo(value);
|
||||
const switchValue = useMemo(() => ({ value: frozen, onValueChange: value => setFrozen(value) }), [frozen, setFrozen]);
|
||||
|
||||
// save on form change. Because effect called on each event, debounce it.
|
||||
const debouncedSave = useRef(
|
||||
debounce(async (frozen, memo) => {
|
||||
wallet.setUTXOMetadata(output.txid, output.vout, { frozen, memo });
|
||||
await saveToDisk();
|
||||
}, 500),
|
||||
);
|
||||
useEffect(() => {
|
||||
debouncedSave.current(frozen, memo);
|
||||
}, [frozen, memo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Output item={output} full />
|
||||
<BlueSpacing20 />
|
||||
<TextInput
|
||||
testID="OutputMemo"
|
||||
placeholder={loc.send.details_note_placeholder}
|
||||
value={memo}
|
||||
placeholderTextColor="#81868e"
|
||||
style={[
|
||||
mStyles.memoTextInput,
|
||||
{
|
||||
borderColor: colors.formBorder,
|
||||
borderBottomColor: colors.formBorder,
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
},
|
||||
]}
|
||||
onChangeText={onMemoChange}
|
||||
/>
|
||||
<BlueListItem title={loc.cc.freezeLabel} Component={TouchableWithoutFeedback} switch={switchValue} />
|
||||
<BlueSpacing20 />
|
||||
<View style={mStyles.buttonContainer}>
|
||||
<BlueButton testID="UseCoin" title={loc.cc.use_coin} onPress={() => onUseCoin([output])} />
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
OutputModalContent.propTypes = {
|
||||
output: PropTypes.object,
|
||||
wallet: PropTypes.object,
|
||||
onUseCoin: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const CoinControl = () => {
|
||||
const { colors } = useTheme();
|
||||
const navigation = useNavigation();
|
||||
const { walletId, onUTXOChoose } = useRoute().params;
|
||||
const { wallets } = useContext(BlueStorageContext);
|
||||
const wallet = wallets.find(w => w.getID() === walletId);
|
||||
// sort by height ascending, txid , vout ascending
|
||||
const utxo = wallet.getUtxo(true).sort((a, b) => a.height - b.height || a.txid.localeCompare(b.txid) || a.vout - b.vout);
|
||||
const [output, setOutput] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
tip: {
|
||||
backgroundColor: colors.ballOutgoingExpired,
|
||||
},
|
||||
});
|
||||
|
||||
const tipCoins = () => {
|
||||
return (
|
||||
utxo.length >= 1 && (
|
||||
<View style={[styles.tip, stylesHook.tip]}>
|
||||
<Text style={{ color: colors.foregroundColor }}>{loc.cc.tip}</Text>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
wallet.fetchUtxo().then(() => setLoading(false));
|
||||
}, [wallet, setLoading]);
|
||||
|
||||
const handleChoose = item => setOutput(item);
|
||||
|
||||
const handleUseCoin = utxo => {
|
||||
setOutput(null);
|
||||
navigation.pop();
|
||||
onUTXOChoose(utxo);
|
||||
};
|
||||
|
||||
const renderItem = p => {
|
||||
const { memo, frozen } = wallet.getUTXOMetadata(p.item.txid, p.item.vout);
|
||||
const change = wallet.addressIsChange(p.item.address);
|
||||
return <Output item={p.item} oMemo={memo} frozen={frozen} change={change} onPress={() => handleChoose(p.item)} />;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeBlueArea style={[styles.root, styles.center, { backgroundColor: colors.elevated }]}>
|
||||
<ActivityIndicator testID="Loading" />
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeBlueArea style={[styles.root, { backgroundColor: colors.elevated }]}>
|
||||
{utxo.length === 0 && (
|
||||
<View style={styles.empty}>
|
||||
<Text style={{ color: colors.foregroundColor }}>{loc.cc.empty}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<BottomModal
|
||||
isVisible={Boolean(output)}
|
||||
onClose={() => {
|
||||
Keyboard.dismiss();
|
||||
setOutput(false);
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
||||
<View style={[styles.modalContent, { backgroundColor: colors.elevated }]}>
|
||||
{output && <OutputModalContent output={output} wallet={wallet} onUseCoin={handleUseCoin} />}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</BottomModal>
|
||||
|
||||
<FlatList ListHeaderComponent={tipCoins} data={utxo} renderItem={renderItem} keyExtractor={item => `${item.txid}:${item.vout}`} />
|
||||
</SafeBlueArea>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalContent: {
|
||||
padding: 22,
|
||||
justifyContent: 'center',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
empty: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
tip: {
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginVertical: 24,
|
||||
},
|
||||
});
|
||||
|
||||
CoinControl.navigationOptions = () => ({
|
||||
...BlueNavigationStyle(null, false),
|
||||
title: loc.cc.header,
|
||||
gestureEnabled: false,
|
||||
});
|
||||
|
||||
export default CoinControl;
|
@ -71,12 +71,8 @@ export default class Confirm extends Component {
|
||||
Notifications.majorTomToGroundControl([], [], txids2watch);
|
||||
let amount = 0;
|
||||
const recipients = this.state.recipients;
|
||||
if (recipients[0].amount === BitcoinUnit.MAX || (!recipients[0].amount && !recipients[0].value)) {
|
||||
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
|
||||
} else {
|
||||
for (const recipient of recipients) {
|
||||
amount += recipient.amount ? +recipient.amount : recipient.value;
|
||||
}
|
||||
amount += recipient.value;
|
||||
}
|
||||
|
||||
amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
|
||||
@ -122,17 +118,11 @@ export default class Confirm extends Component {
|
||||
<>
|
||||
<View style={styles.valueWrap}>
|
||||
<Text testID="TransactionValue" style={styles.valueValue}>
|
||||
{!item.value || item.value === BitcoinUnit.MAX
|
||||
? currency.satoshiToBTC(this.state.fromWallet.getBalance() - this.state.feeSatoshi)
|
||||
: item.amount || currency.satoshiToBTC(item.value)}
|
||||
{currency.satoshiToBTC(item.value)}
|
||||
</Text>
|
||||
<Text style={styles.valueUnit}>{' ' + BitcoinUnit.BTC}</Text>
|
||||
</View>
|
||||
<Text style={styles.transactionAmountFiat}>
|
||||
{item.value !== BitcoinUnit.MAX && item.value
|
||||
? currency.satoshiToLocalCurrency(item.value)
|
||||
: currency.satoshiToLocalCurrency(this.state.fromWallet.getBalance() - this.state.feeSatoshi)}
|
||||
</Text>
|
||||
<Text style={styles.transactionAmountFiat}>{currency.satoshiToLocalCurrency(item.value)}</Text>
|
||||
<BlueCard>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create_to}</Text>
|
||||
<Text testID="TransactionAddress" style={styles.transactionDetailsSubtitle}>
|
||||
|
@ -100,10 +100,7 @@ export default class SendCreate extends Component {
|
||||
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create_amount}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>
|
||||
{item.value === BitcoinUnit.MAX || !item.value
|
||||
? currency.satoshiToBTC(this.state.wallet.getBalance()) - this.state.fee
|
||||
: currency.satoshiToBTC(item.value)}{' '}
|
||||
{BitcoinUnit.BTC}
|
||||
{currency.satoshiToBTC(item.value)} {BitcoinUnit.BTC}
|
||||
</Text>
|
||||
{this.state.recipients.length > 1 && (
|
||||
<BlueText style={styles.itemOf}>
|
||||
|
@ -42,8 +42,9 @@ import { HDSegwitBech32Wallet, LightningCustodianWallet, MultisigHDWallet, Watch
|
||||
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
import loc from '../../loc';
|
||||
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||
import { BlueCurrentTheme } from '../../components/themes';
|
||||
import CoinsSelected from '../../components/CoinsSelected';
|
||||
import BottomModal from '../../components/BottomModal';
|
||||
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
@ -134,6 +135,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
select: {
|
||||
marginBottom: 24,
|
||||
marginHorizontal: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
selectTouch: {
|
||||
@ -252,6 +254,7 @@ export default class SendDetails extends Component {
|
||||
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
|
||||
renderWalletSelectionButtonHidden: false,
|
||||
width: Dimensions.get('window').width,
|
||||
utxo: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -403,11 +406,11 @@ export default class SendDetails extends Component {
|
||||
}
|
||||
|
||||
_keyboardDidShow = () => {
|
||||
this.setState({ renderWalletSelectionButtonHidden: true, isAmountToolbarVisibleForAndroid: true });
|
||||
this.setState({ renderWalletSelectionOrCoinsSelectedHidden: true, isAmountToolbarVisibleForAndroid: true });
|
||||
};
|
||||
|
||||
_keyboardDidHide = () => {
|
||||
this.setState({ renderWalletSelectionButtonHidden: false, isAmountToolbarVisibleForAndroid: false });
|
||||
this.setState({ renderWalletSelectionOrCoinsSelectedHidden: false, isAmountToolbarVisibleForAndroid: false });
|
||||
};
|
||||
|
||||
async createTransaction() {
|
||||
@ -525,6 +528,7 @@ export default class SendDetails extends Component {
|
||||
const changeAddress = this.getChangeAddressFast();
|
||||
const requestedSatPerByte = Number(this.state.fee);
|
||||
const feePrecalc = { ...this.state.feePrecalc };
|
||||
const utxo = this.state.utxo || wallet.getUtxo();
|
||||
|
||||
const options = all
|
||||
? [
|
||||
@ -567,7 +571,7 @@ export default class SendDetails extends Component {
|
||||
while (true) {
|
||||
try {
|
||||
const { fee } = wallet.coinselect(
|
||||
wallet.getUtxo(),
|
||||
utxo,
|
||||
targets,
|
||||
opt.fee,
|
||||
changeAddress,
|
||||
@ -598,7 +602,8 @@ export default class SendDetails extends Component {
|
||||
const wallet = this.state.fromWallet;
|
||||
const changeAddress = await this.getChangeAddressAsync();
|
||||
const requestedSatPerByte = Number(this.state.fee);
|
||||
console.log({ requestedSatPerByte, utxo: wallet.getUtxo() });
|
||||
const utxo = this.state.utxo || wallet.getUtxo();
|
||||
console.log({ requestedSatPerByte, utxo });
|
||||
|
||||
let targets = [];
|
||||
for (const transaction of this.state.addresses) {
|
||||
@ -617,8 +622,8 @@ export default class SendDetails extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const { tx, fee, psbt } = wallet.createTransaction(
|
||||
wallet.getUtxo(),
|
||||
const { tx, outputs, fee, psbt } = wallet.createTransaction(
|
||||
utxo,
|
||||
targets,
|
||||
requestedSatPerByte,
|
||||
changeAddress,
|
||||
@ -653,12 +658,15 @@ export default class SendDetails extends Component {
|
||||
memo: this.state.memo,
|
||||
};
|
||||
await this.context.saveToDisk();
|
||||
|
||||
const recipients = outputs.filter(({ address }) => address !== changeAddress);
|
||||
|
||||
this.props.navigation.navigate('Confirm', {
|
||||
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
|
||||
memo: this.state.memo,
|
||||
fromWallet: wallet,
|
||||
tx: tx.toHex(),
|
||||
recipients: targets,
|
||||
recipients,
|
||||
satoshiPerByte: requestedSatPerByte,
|
||||
payjoinUrl: this.state.payjoinUrl,
|
||||
psbt,
|
||||
@ -666,9 +674,13 @@ export default class SendDetails extends Component {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
|
||||
onUTXOChoose = utxo => {
|
||||
this.setState({ utxo }, this.reCalcTx);
|
||||
};
|
||||
|
||||
onWalletSelect = wallet => {
|
||||
const changeWallet = () => {
|
||||
this.setState({ fromWallet: wallet }, () => {
|
||||
this.setState({ fromWallet: wallet, utxo: null }, () => {
|
||||
this.renderNavigationHeader();
|
||||
this.context.setSelectedWallet(wallet.getID());
|
||||
this.props.navigation.pop();
|
||||
@ -1035,6 +1047,21 @@ export default class SendDetails extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
handleCoinControl = () => {
|
||||
this.setState(
|
||||
{
|
||||
isAdvancedTransactionOptionsVisible: false,
|
||||
},
|
||||
() => {
|
||||
const { fromWallet } = this.state;
|
||||
this.props.navigation.navigate('CoinControl', {
|
||||
walletId: fromWallet.getID(),
|
||||
onUTXOChoose: this.onUTXOChoose,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
hideAdvancedTransactionOptionsModal = () => {
|
||||
Keyboard.dismiss();
|
||||
this.setState({ isAdvancedTransactionOptionsVisible: false });
|
||||
@ -1122,6 +1149,13 @@ export default class SendDetails extends Component {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<BlueListItem
|
||||
testID="CoinControl"
|
||||
title={loc.cc.header}
|
||||
hideChevron
|
||||
component={TouchableOpacity}
|
||||
onPress={this.handleCoinControl}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</BottomModal>
|
||||
@ -1154,8 +1188,23 @@ export default class SendDetails extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
renderWalletSelectionButton = () => {
|
||||
if (this.state.renderWalletSelectionButtonHidden) return;
|
||||
renderWalletSelectionOrCoinsSelected = () => {
|
||||
if (this.state.renderWalletSelectionOrCoinsSelectedHidden) return;
|
||||
|
||||
if (this.state.utxo !== null) {
|
||||
return (
|
||||
<View style={styles.select}>
|
||||
<CoinsSelected
|
||||
number={this.state.utxo.length}
|
||||
onClose={() => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
this.setState({ utxo: null }, this.reCalcTx);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.select}>
|
||||
{!this.state.isLoading && (
|
||||
@ -1309,7 +1358,8 @@ export default class SendDetails extends Component {
|
||||
keyExtractor = (_item, index) => `${index}`;
|
||||
|
||||
render() {
|
||||
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
|
||||
const { fromWallet, utxo } = this.state;
|
||||
if (this.state.isLoading || typeof fromWallet === 'undefined') {
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<BlueLoading />
|
||||
@ -1317,6 +1367,10 @@ export default class SendDetails extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
// if utxo is limited we use it to calculate available balance
|
||||
const balance = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : fromWallet.getBalance();
|
||||
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||
<View style={styles.root} onLayout={this.onLayout}>
|
||||
@ -1371,14 +1425,22 @@ export default class SendDetails extends Component {
|
||||
<BlueDismissKeyboardInputAccessory />
|
||||
{Platform.select({
|
||||
ios: (
|
||||
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
|
||||
<BlueUseAllFundsButton
|
||||
canUseAll={fromWallet.allowSendMax() && allBalance > 0}
|
||||
onUseAllPressed={this.onUseAllPressed}
|
||||
balance={allBalance}
|
||||
/>
|
||||
),
|
||||
android: this.state.isAmountToolbarVisibleForAndroid && (
|
||||
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
|
||||
<BlueUseAllFundsButton
|
||||
canUseAll={fromWallet.allowSendMax() && allBalance > 0}
|
||||
onUseAllPressed={this.onUseAllPressed}
|
||||
balance={allBalance}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
{this.renderWalletSelectionButton()}
|
||||
{this.renderWalletSelectionOrCoinsSelected()}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
|
@ -272,6 +272,9 @@ const PsbtWithHardwareWallet = () => {
|
||||
<BlueCard>
|
||||
<BlueText testID="TextHelperForPSBT">{loc.send.psbt_this_is_psbt}</BlueText>
|
||||
<BlueSpacing20 />
|
||||
<Text testID="PSBTHex" style={styles.hidden}>
|
||||
{psbt.toHex()}
|
||||
</Text>
|
||||
<DynamicQRCode value={psbt.toHex()} capacity={200} />
|
||||
<BlueSpacing20 />
|
||||
<SecondButton
|
||||
@ -371,4 +374,8 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
hidden: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
|
@ -251,14 +251,14 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: BlueCurrentTheme.colors.feeLabel,
|
||||
},
|
||||
containerDisconnected: {
|
||||
backgroundColor: '#F8D2D2',
|
||||
backgroundColor: BlueCurrentTheme.colors.redBG,
|
||||
},
|
||||
textConnected: {
|
||||
color: BlueCurrentTheme.colors.feeValue,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
textDisconnected: {
|
||||
color: '#D0021B',
|
||||
color: BlueCurrentTheme.colors.redText,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
hostname: {
|
||||
|
@ -209,7 +209,18 @@ const WalletTransactions = () => {
|
||||
};
|
||||
|
||||
const renderListHeaderComponent = () => {
|
||||
const style = { opacity: isLoading ? 0.5 : 1.0 };
|
||||
const style = {};
|
||||
if (!isCatalyst) {
|
||||
// we need this button for testing
|
||||
style.opacity = 0;
|
||||
style.height = 1;
|
||||
style.width = 1;
|
||||
} else if (isLoading) {
|
||||
style.opacity = 0.5;
|
||||
} else {
|
||||
style.opacity = 1.0;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<View style={styles.listHeader}>
|
||||
@ -233,11 +244,9 @@ const WalletTransactions = () => {
|
||||
</View>
|
||||
<View style={[styles.listHeaderTextRow, stylesHook.listHeaderTextRow]}>
|
||||
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
|
||||
{isCatalyst && (
|
||||
<TouchableOpacity style={style} onPress={refreshTransactions} disabled={isLoading}>
|
||||
<TouchableOpacity testID="refreshTransactions" style={style} onPress={refreshTransactions} disabled={isLoading}>
|
||||
<Icon name="refresh" type="font-awesome" color={colors.feeText} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@ -606,7 +615,9 @@ const WalletTransactions = () => {
|
||||
|
||||
{!isLightning() && (
|
||||
<TouchableOpacity onPress={navigateToBuyBitcoin} style={styles.buyBitcoin}>
|
||||
<Text style={styles.buyBitcoinText}>{loc.wallets.list_tap_here_to_buy}</Text>
|
||||
<Text testID="NoTxBuyBitcoin" style={styles.buyBitcoinText}>
|
||||
{loc.wallets.list_tap_here_to_buy}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
@ -734,6 +734,109 @@ describe('BlueWallet UI Tests', () => {
|
||||
|
||||
process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1');
|
||||
});
|
||||
|
||||
it('can manage UTXO', async () => {
|
||||
const lockFile = '/tmp/travislock.' + hashIt(jasmine.currentTest.fullName);
|
||||
if (process.env.TRAVIS) {
|
||||
if (require('fs').existsSync(lockFile))
|
||||
return console.warn('skipping', JSON.stringify(jasmine.currentTest.fullName), 'as it previously passed on Travis');
|
||||
}
|
||||
|
||||
await helperImportWallet(
|
||||
'zpub6qoWjSiZRHzSYPGYJ6EzxEXJXP1b2Rj9syWwJZFNCmupMwkbSAWSBk3UvSkJyQLEhQpaBAwvhmNj3HPKpwCJiTBB9Tutt46FtEmjL2DoU3J',
|
||||
'Imported Watch-only',
|
||||
'0.00105526 BTC',
|
||||
);
|
||||
|
||||
// refresh transactions
|
||||
await element(by.id('refreshTransactions')).tap();
|
||||
await waitFor(element(by.id('NoTxBuyBitcoin')))
|
||||
.not.toExist()
|
||||
.withTimeout(300 * 1000);
|
||||
|
||||
// change note of 0.001 tx output
|
||||
await element(by.text('0.001')).atIndex(0).tap();
|
||||
await element(by.text('details')).tap();
|
||||
await expect(element(by.text('49944e90fe917952e36b1967cdbc1139e60c89b4800b91258bf2345a77a8b888'))).toBeVisible();
|
||||
await element(by.type('android.widget.EditText')).typeText('test1');
|
||||
await element(by.text('Save')).tap();
|
||||
await element(by.text('OK')).tap();
|
||||
|
||||
// back to wallet screen
|
||||
await device.pressBack();
|
||||
await device.pressBack();
|
||||
|
||||
// open CoinControl
|
||||
await element(by.id('SendButton')).tap();
|
||||
await element(by.text('OK')).tap();
|
||||
await element(by.id('advancedOptionsMenuButton')).tap();
|
||||
await element(by.id('CoinControl')).tap();
|
||||
await waitFor(element(by.id('Loading'))) // wait for outputs to be loaded
|
||||
.not.toExist()
|
||||
.withTimeout(300 * 1000);
|
||||
await expect(element(by.text('test1')).atIndex(0)).toBeVisible();
|
||||
|
||||
// change output note and freeze it
|
||||
await element(by.text('test1')).atIndex(0).tap();
|
||||
await element(by.id('OutputMemo')).replaceText('test2');
|
||||
await element(by.type('android.widget.CompoundButton')).tap(); // freeze switch
|
||||
await device.pressBack(); // closing modal
|
||||
await expect(element(by.text('test2')).atIndex(0)).toBeVisible();
|
||||
await expect(element(by.text('freeze')).atIndex(0)).toBeVisible();
|
||||
|
||||
// use frozen output to create tx using "Use coin" feature
|
||||
await element(by.text('test2')).atIndex(0).tap();
|
||||
await element(by.id('UseCoin')).tap();
|
||||
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
|
||||
await element(by.id('advancedOptionsMenuButton')).tap();
|
||||
await element(by.id('sendMaxButton')).tap();
|
||||
await element(by.text('OK')).tap();
|
||||
// setting fee rate:
|
||||
await element(by.id('chooseFee')).tap();
|
||||
await element(by.id('feeCustom')).tap();
|
||||
await element(by.type('android.widget.EditText')).typeText('1');
|
||||
await element(by.text('OK')).tap();
|
||||
|
||||
await element(by.id('CreateTransactionButton')).tap();
|
||||
await yo('TextHelperForPSBT');
|
||||
|
||||
const psbthex1 = await extractTextFromElementById('PSBTHex');
|
||||
const psbt1 = bitcoin.Psbt.fromHex(psbthex1);
|
||||
assert.strictEqual(psbt1.txOutputs.length, 1);
|
||||
assert.strictEqual(psbt1.txOutputs[0].address, 'bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
|
||||
assert.strictEqual(psbt1.txOutputs[0].value, 99808);
|
||||
assert.strictEqual(psbt1.data.inputs.length, 1);
|
||||
assert.strictEqual(psbt1.data.inputs[0].witnessUtxo.value, 100000);
|
||||
|
||||
// back to wallet screen
|
||||
await device.pressBack();
|
||||
await device.pressBack();
|
||||
|
||||
// create tx with unfrozen input
|
||||
await element(by.id('SendButton')).tap();
|
||||
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
|
||||
await element(by.id('advancedOptionsMenuButton')).tap();
|
||||
await element(by.id('sendMaxButton')).tap();
|
||||
await element(by.text('OK')).tap();
|
||||
// setting fee rate:
|
||||
await element(by.id('chooseFee')).tap();
|
||||
await element(by.id('feeCustom')).tap();
|
||||
await element(by.type('android.widget.EditText')).typeText('1');
|
||||
await element(by.text('OK')).tap();
|
||||
|
||||
await element(by.id('CreateTransactionButton')).tap();
|
||||
await yo('TextHelperForPSBT');
|
||||
|
||||
const psbthex2 = await extractTextFromElementById('PSBTHex');
|
||||
const psbt2 = bitcoin.Psbt.fromHex(psbthex2);
|
||||
assert.strictEqual(psbt2.txOutputs.length, 1);
|
||||
assert.strictEqual(psbt2.txOutputs[0].address, 'bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
|
||||
assert.strictEqual(psbt2.txOutputs[0].value, 5334);
|
||||
assert.strictEqual(psbt2.data.inputs.length, 1);
|
||||
assert.strictEqual(psbt2.data.inputs[0].witnessUtxo.value, 5526);
|
||||
|
||||
process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1');
|
||||
});
|
||||
});
|
||||
|
||||
async function sleep(ms) {
|
||||
|
@ -28,6 +28,9 @@ describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
assert.ok(hd.getAllExternalAddresses().includes('bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g'));
|
||||
assert.ok(!hd.getAllExternalAddresses().includes('bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el')); // not internal
|
||||
|
||||
assert.ok(hd.addressIsChange('bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'));
|
||||
assert.ok(!hd.addressIsChange('bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'));
|
||||
|
||||
assert.strictEqual(
|
||||
hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'),
|
||||
'0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c',
|
||||
@ -75,4 +78,31 @@ describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
hd2.setSecret(hd.getSecret());
|
||||
assert.ok(hd2.validateMnemonic());
|
||||
});
|
||||
|
||||
it('can coin control', async () => {
|
||||
const hd = new HDSegwitBech32Wallet();
|
||||
|
||||
// fake UTXO so we don't need to use fetchUtxo
|
||||
hd._utxo = [
|
||||
{ txid: '11111', vout: 0, value: 11111 },
|
||||
{ txid: '22222', vout: 0, value: 22222 },
|
||||
];
|
||||
|
||||
assert.ok(hd.getUtxo().length === 2);
|
||||
|
||||
// freeze one UTXO and set a memo on it
|
||||
hd.setUTXOMetadata('11111', 0, { memo: 'somememo', frozen: true });
|
||||
assert.strictEqual(hd.getUTXOMetadata('11111', 0).memo, 'somememo');
|
||||
assert.strictEqual(hd.getUTXOMetadata('11111', 0).frozen, true);
|
||||
|
||||
// now .getUtxo() should return a limited UTXO set
|
||||
assert.ok(hd.getUtxo().length === 1);
|
||||
assert.strictEqual(hd.getUtxo()[0].txid, '22222');
|
||||
|
||||
// now .getUtxo(true) should return a full UTXO set
|
||||
assert.ok(hd.getUtxo(true).length === 2);
|
||||
|
||||
// for UTXO with no metadata .getUTXOMetadata() should return an empty object
|
||||
assert.ok(Object.keys(hd.getUTXOMetadata('22222', 0)).length === 0);
|
||||
});
|
||||
});
|
||||
|
@ -194,6 +194,14 @@ describe('Watch only wallet', () => {
|
||||
assert.ok((await w._getExternalAddressByIndex(0)).startsWith('1'));
|
||||
assert.ok(w.getAllExternalAddresses().includes(await w._getExternalAddressByIndex(0)));
|
||||
});
|
||||
|
||||
it('can determine change address for HD wallet', async () => {
|
||||
const w = new WatchOnlyWallet();
|
||||
w.setSecret('ypub6Y9u3QCRC1HkZv3stNxcQVwmw7vC7KX5Ldz38En5P88RQbesP2oy16hNyQocVCfYRQPxdHcd3pmu9AFhLv7NdChWmw5iNLryZ2U6EEHdnfo');
|
||||
w.init();
|
||||
assert.ok(!w.addressIsChange(await w._getExternalAddressByIndex(0)));
|
||||
assert.ok(w.addressIsChange(await w._getInternalAddressByIndex(0)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('BC-UR', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user