Merge branch 'master' into fix-2088

This commit is contained in:
marcosrdz 2020-11-23 18:19:53 -05:00
commit af2a966b02
28 changed files with 762 additions and 155 deletions

View file

@ -23,7 +23,6 @@ import {
TouchableOpacity,
TouchableWithoutFeedback,
UIManager,
useWindowDimensions,
View,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
@ -66,7 +65,6 @@ Platform.OS === 'android' ? (ActivityIndicator.defaultProps.color = PlatformColo
export const BlueButton = props => {
const { colors } = useTheme();
const { width } = useWindowDimensions();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor || BlueCurrentTheme.colors.mainColor;
let fontColor = props.buttonTextColor || colors.buttonTextColor;
@ -75,11 +73,6 @@ export const BlueButton = props => {
fontColor = colors.buttonDisabledTextColor;
}
let buttonWidth = props.width ? props.width : width / 1.5;
if ('noMinWidth' in props) {
buttonWidth = 0;
}
return (
<TouchableOpacity
style={{
@ -91,9 +84,9 @@ export const BlueButton = props => {
height: 45,
maxHeight: 45,
borderRadius: 25,
minWidth: buttonWidth,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 16,
}}
{...props}
>
@ -105,45 +98,6 @@ export const BlueButton = props => {
);
};
export const BlueButtonHook = props => {
const { width } = useWindowDimensions();
const { colors } = useTheme();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor;
let fontColor = colors.buttonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
}
let buttonWidth = props.width ? props.width : width / 1.5;
if ('noMinWidth' in props) {
buttonWidth = 0;
}
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
minHeight: 45,
height: 45,
maxHeight: 45,
borderRadius: 25,
minWidth: buttonWidth,
justifyContent: 'center',
alignItems: 'center',
}}
{...props}
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{props.title}</Text>}
</View>
</TouchableOpacity>
);
};
export const SecondButton = props => {
const { colors } = useTheme();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.buttonBlueBackgroundColor;
@ -152,10 +106,7 @@ export const SecondButton = props => {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;
}
// let buttonWidth = this.props.width ? this.props.width : width / 1.5;
// if ('noMinWidth' in this.props) {
// buttonWidth = 0;
// }
return (
<TouchableOpacity
style={{
@ -1061,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
@ -1096,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
@ -1115,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>
@ -1128,6 +1076,7 @@ export class BlueUseAllFundsButton extends Component {
</View>
</View>
);
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {

View file

@ -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';
@ -132,13 +133,7 @@ const WalletsRoot = () => (
<WalletsStack.Screen name="LightningSettings" component={LightningSettings} options={LightningSettings.navigationOptions} />
<WalletsStack.Screen name="ElectrumSettings" component={ElectrumSettings} options={ElectrumSettings.navigationOptions} />
<WalletsStack.Screen name="SettingsPrivacy" component={SettingsPrivacy} options={SettingsPrivacy.navigationOptions} />
<WalletsStack.Screen
name="LNDViewInvoice"
component={LNDViewInvoice}
options={LNDViewInvoice.navigationOptions}
swipeEnabled={false}
gestureEnabled={false}
/>
<WalletsStack.Screen name="LNDViewInvoice" component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions} />
<WalletsStack.Screen
name="LNDViewAdditionalInvoiceInformation"
component={LNDViewAdditionalInvoiceInformation}
@ -190,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>
);
@ -198,13 +194,7 @@ const LNDCreateInvoiceRoot = () => (
<LNDCreateInvoiceStack.Navigator screenOptions={defaultStackScreenOptions}>
<LNDCreateInvoiceStack.Screen name="LNDCreateInvoice" component={LNDCreateInvoice} options={LNDCreateInvoice.navigationOptions} />
<LNDCreateInvoiceStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions} />
<LNDCreateInvoiceStack.Screen
name="LNDViewInvoice"
component={LNDViewInvoice}
options={LNDViewInvoice.navigationOptions}
swipeEnabled={false}
gestureEnabled={false}
/>
<LNDCreateInvoiceStack.Screen name="LNDViewInvoice" component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions} />
<LNDCreateInvoiceStack.Screen
name="LNDViewAdditionalInvoiceInformation"
component={LNDViewAdditionalInvoiceInformation}
@ -304,7 +294,7 @@ const Navigation = () => (
<RootStack.Navigator mode="modal" screenOptions={defaultScreenOptions} initialRouteName="LoadingScreenRoot">
{/* stacks */}
<RootStack.Screen name="WalletsRoot" component={WalletsRoot} options={{ headerShown: false }} />
<RootStack.Screen name="AddWalletRoot" component={AddWalletRoot} options={{ headerShown: false, gestureEnabled: false }} />
<RootStack.Screen name="AddWalletRoot" component={AddWalletRoot} options={{ headerShown: false }} />
<RootStack.Screen name="SendDetailsRoot" component={SendDetailsRoot} options={{ headerShown: false }} />
<RootStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={{ headerShown: false }} />
<RootStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={{ headerShown: false }} />

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View 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;

View file

@ -237,7 +237,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
{loc.wallets.list_latest_transaction}
</Text>
<Text numberOfLines={1} style={[iStyles.latestTxTime, { color: colors.inverseForegroundColor }]}>
{transactionTimeToReadable(item.getLatestTransactionTime())}
{item.getBalance() !== 0 && item.getLatestTransactionTime() === 0
? loc.wallets.pull_to_refresh
: transactionTimeToReadable(item.getLatestTransactionTime())}
</Text>
</LinearGradient>
</TouchableWithoutFeedback>

View file

@ -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',
},
};

View file

@ -400,6 +400,7 @@
"select_wallet": "Select Wallet",
"take_photo": "Take Photo",
"xpub_copiedToClipboard": "Copied to clipboard.",
"pull_to_refresh": "pull to refresh",
"xpub_title": "wallet XPUB"
},
"multisig": {
@ -454,5 +455,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."
}
}

View file

@ -212,6 +212,7 @@
"psbt_clipboard": "Gekopieerd naar Plakbord",
"psbt_this_is_psbt": "Dit is een gedeeltelijk ondertekende bitcoin-transactie (PSBT). Voltooi het door te ondertekenen met uw hardware wallet. ",
"psbt_tx_export": "Exporteer naar bestand",
"no_tx_signing_in_progress": "Er is geen ondertekening van een transactie bezig",
"psbt_tx_open": "Open ondertekende transactie",
"psbt_tx_scan": "Scan ondertekende transactie",
"qr_error_no_qrcode": "De geselecteerde afbeelding bevat geen QR-code.",
@ -233,7 +234,7 @@
"about_sm_twitter": "Volg ons op Twitter",
"advanced_options": "Geavanceerde opties",
"currency": "Valuta",
"currency_source": "Prijzen verstrekt bij",
"currency_source": "Prijzen zijn opgehaald bij",
"default_desc": "Indien uitgeschakeld zal BlueWallet meteen de geselecteerde wallet openen bij het opstarten.",
"default_info": "Standaard info",
"default_title": "Bij opstarten",
@ -257,7 +258,7 @@
"encrypt_use": "Gebruik {type}",
"encrypt_use_expl": "{type} wordt gebruikt om uw identiteit te bevestigen voordat u een transactie uitvoert, een wallet ontgrendelt, exporteert of verwijdert. {type} wordt niet gebruikt om een versleutelde opslag te ontgrendelen.",
"general": "Algemeen",
"general_adv_mode": "Enable advanced mode",
"general_adv_mode": "Geavanceerde modus",
"general_adv_mode_e": "Indien ingeschakeld ziet u geavanceerde opties zoals verschillende wallettypes, de mogelijkheid om de LNDHub-instantie te specificeren waarmee u verbinding wilt maken en aangepaste entropie tijdens het aanmaken van een wallet.",
"general_continuity": "Continuïteit",
"general_continuity_e": "Indien ingeschakeld, kunt u geselecteerde wallets en transacties bekijken met uw andere Apple iCloud-apparaten.",
@ -292,7 +293,7 @@
},
"notifications": {
"would_you_like_to_receive_notifications": "Wil je meldingen ontvangen als je binnenkomende betalingen ontvangt?",
"no_and_dont_ask": "Nee en vraag het mij niet meer",
"no_and_dont_ask": "Nee, en vraag het me niet nog een keer",
"ask_me_later": "Vraag het mij later"
},
"transactions": {
@ -326,14 +327,13 @@
"transactions_count": "transactieteller"
},
"wallets": {
"add_bitcoin_explain": "Eenvoudige en krachtige Bitcoin-wallet",
"add_bitcoin": "Bitcoin",
"add_bitcoin_explain": "Eenvoudige en krachtige Bitcoin-wallet",
"add_create": "Aanmaken",
"add_entropy_generated": "{gen} bytes gegenereerde entropie",
"add_entropy_provide": "Zorg voor entropie via dobbelstenen",
"add_entropy_remain": "{gen} bytes gegenereerde entropie. Resterende {rem} bytes zullen worden verkregen uit het systeem voor willekeurige getallen.",
"add_import_wallet": "Wallet importeren",
"import_file": "Importeer bestand",
"add_lightning": "Lightning",
"add_lightning_explain": "Voor uitgaven met directe transacties",
"add_lndhub": "Verbind met uw LNDHub",
@ -343,6 +343,8 @@
"add_title": "wallet toevoegen",
"add_wallet_name": "naam",
"add_wallet_type": "type",
"clipboard_bitcoin": "U heeft een Bitcoin-adres op uw klembord. Wilt u deze gebruiken voor een transactie?",
"clipboard_lightning": "Je hebt een Lightning-factuur op je klembord. Wilt u deze gebruiken voor een transactie?",
"details_address": "Adres",
"details_advanced": "Geavenceerd",
"details_are_you_sure": "Weet u het zeker?",
@ -364,15 +366,15 @@
"details_use_with_hardware_wallet": "Gebruik met hardware wallet",
"details_wallet_updated": "Wallet bijgewerkt",
"details_yes_delete": "Ja, verwijder",
"enter_bip38_password": "Voer wachtwoord in om te ontgrendelen",
"export_title": "wallet exporteren",
"import_do_import": "Importeren",
"import_error": "Importeren mislukt. Zorg ervoor dat de verstrekte gegevens geldig zijn.",
"import_explanation": "Schrijf hier je mnemonic phrase, private key, WIF of wat je maar hebt. BlueWallet zal zijn best doen om het juiste formaat te raden en uw wallet te importeren",
"import_file": "Importeer bestand",
"import_imported": "Geïmporteerd",
"import_scan_qr": "QR-code scannen of importeren?",
"import_success": "Succes",
"looks_like_bip38": "Dit lijkt op een met een wachtwoord beveiligde private key (BIP38)",
"enter_bip38_password": "Voer wachtwoord in om te ontgrendelen",
"import_title": "importeren",
"list_create_a_button": "Voeg nu toe",
"list_create_a_wallet": "Wallet aanmaken",
@ -388,14 +390,15 @@
"list_long_choose": "Kies foto",
"list_long_clipboard": "Kopiëren van klembord",
"list_long_scan": "Scan QR-code",
"take_photo": "Maak foto",
"list_tap_here_to_buy": "Koop Bitcoin",
"list_title": "wallets",
"list_tryagain": "Probeer opnieuw",
"looks_like_bip38": "Dit lijkt op een met een wachtwoord beveiligde private key (BIP38)",
"reorder_title": "Wallets opnieuw ordenen",
"select_no_bitcoin": "Er is momenteel geen Bitcoin-wallet beschikbaar",
"select_no_bitcoin_exp": "Een Bitcoin-wallet is vereist om Lightning-wallets opnieuw te vullen. Maak of importeer er een.",
"select_wallet": "Selecteer wallet",
"take_photo": "Maak foto",
"xpub_copiedToClipboard": "Gekopieerd naar het klembord.",
"xpub_title": "wallet XPUB"
},
@ -420,7 +423,7 @@
"native_segwit_title": "Beste oefening",
"wrapped_segwit_title": "Beste compatibiliteit",
"legacy_title": "Legacy",
"co_sign_transaction": "Onderteken QR-airgapped transactie",
"co_sign_transaction": "Signeer een transactie",
"what_is_vault": "Een kluis is een",
"what_is_vault_numberOfWallets": "{m}-van-{n} multisig",
"what_is_vault_wallet": "wallet",

View file

@ -35,10 +35,9 @@ const currency = require('../../blue_modules/currency');
const styles = StyleSheet.create({
createButton: {
marginHorizontal: 56,
marginHorizontal: 16,
marginVertical: 16,
minHeight: 45,
alignItems: 'center',
},
scanRoot: {
height: 36,

View file

@ -367,13 +367,16 @@ LNDViewInvoice.navigationOptions = ({ navigation, route }) =>
title: 'Lightning Invoice',
headerLeft: null,
headerStyle: {
...BlueNavigationStyle().headerStyle,
backgroundColor: BlueCurrentTheme.colors.customHeader,
},
gestureEnabled: false,
}
: {
...BlueNavigationStyle(),
title: 'Lightning Invoice',
headerStyle: {
...BlueNavigationStyle().headerStyle,
backgroundColor: BlueCurrentTheme.colors.customHeader,
},
};

View file

@ -53,9 +53,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginVertical: 4,
},
payButtonContainer: {
alignItems: 'center',
},
walletWrapTouch: {
flexDirection: 'row',
alignItems: 'center',
@ -438,7 +435,7 @@ export default class ScanLndInvoice extends React.Component {
<ActivityIndicator />
</View>
) : (
<View style={styles.payButtonContainer}>
<View>
<BlueButton title={loc.lnd.payButton} onPress={() => this.pay()} disabled={this.shouldDisablePayButton()} />
</View>
)}

View file

@ -7,7 +7,7 @@ import ImagePicker from 'react-native-image-picker';
import { decodeUR, extractSingleWorkload } from 'bc-ur';
import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native';
import loc from '../../loc';
import { BlueLoadingHook, BlueTextHooks, BlueButtonHook, BlueSpacing40 } from '../../BlueComponents';
import { BlueLoadingHook, BlueTextHooks, BlueButton, BlueSpacing40 } from '../../BlueComponents';
import { BlueCurrentTheme } from '../../components/themes';
import { openPrivacyDesktopSettings } from '../../class/camera';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
@ -255,7 +255,7 @@ const ScanQRCode = () => {
<View style={[styles.openSettingsContainer, stylesHook.openSettingsContainer]}>
<BlueTextHooks>{loc.send.permission_camera_message}</BlueTextHooks>
<BlueSpacing40 />
<BlueButtonHook title={loc.send.open_settings} onPress={openPrivacyDesktopSettings} />
<BlueButton title={loc.send.open_settings} onPress={openPrivacyDesktopSettings} />
</View>
)}
<TouchableOpacity style={styles.closeTouch} onPress={dismiss}>
@ -293,7 +293,7 @@ const ScanQRCode = () => {
value={backdoorText}
onChangeText={setBackdoorText}
/>
<BlueButtonHook
<BlueButton
title="OK"
testID="scanQrBackdoorOkButton"
onPress={() => {

299
screen/send/coinControl.js Normal file
View 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;

View file

@ -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;
}
for (const recipient of recipients) {
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}>

View file

@ -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}>

View file

@ -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';
@ -127,13 +128,14 @@ const styles = StyleSheet.create({
fontWeight: '600',
},
createButton: {
marginHorizontal: 56,
marginVertical: 16,
maxWidth: 260,
marginHorizontal: 16,
alignContent: 'center',
minHeight: 44,
},
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,
};
}
}
@ -402,11 +405,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() {
@ -524,6 +527,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
? [
@ -566,7 +570,7 @@ export default class SendDetails extends Component {
while (true) {
try {
const { fee } = wallet.coinselect(
wallet.getUtxo(),
utxo,
targets,
opt.fee,
changeAddress,
@ -597,7 +601,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) {
@ -616,8 +621,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,
@ -652,12 +657,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,
@ -665,9 +673,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();
@ -1034,6 +1046,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 });
@ -1121,6 +1148,13 @@ export default class SendDetails extends Component {
/>
</>
)}
<BlueListItem
testID="CoinControl"
title={loc.cc.header}
hideChevron
component={TouchableOpacity}
onPress={this.handleCoinControl}
/>
</View>
</KeyboardAvoidingView>
</BottomModal>
@ -1153,8 +1187,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 && (
@ -1308,7 +1357,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 />
@ -1316,6 +1366,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}>
@ -1370,14 +1424,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>
);

View file

@ -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,
},
});

View file

@ -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: {

View file

@ -20,7 +20,7 @@ import {
BitcoinButton,
VaultButton,
BlueFormLabel,
BlueButtonHook,
BlueButton,
BlueNavigationStyle,
BlueButtonLinkHook,
BlueSpacing20,
@ -312,7 +312,7 @@ const WalletsAdd = () => {
<BlueSpacing20 />
<View style={styles.createButton}>
{!isLoading ? (
<BlueButtonHook testID="Create" title={loc.wallets.add_create} disabled={!selectedWalletType} onPress={createWallet} />
<BlueButton testID="Create" title={loc.wallets.add_create} disabled={!selectedWalletType} onPress={createWallet} />
) : (
<ActivityIndicator />
)}
@ -342,9 +342,7 @@ WalletsAdd.navigationOptions = ({ navigation }) => ({
const styles = StyleSheet.create({
createButton: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
loading: {
flex: 1,

View file

@ -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}>
<Icon name="refresh" type="font-awesome" color={colors.feeText} />
</TouchableOpacity>
)}
<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>
@ -676,7 +687,7 @@ WalletTransactions.navigationOptions = ({ navigation, route }) => {
<Icon name="kebab-horizontal" type="octicon" size={22} color="#FFFFFF" />
</TouchableOpacity>
),
headerTitle: () => null,
headerTitle: '',
headerStyle: {
backgroundColor: WalletGradient.headerColorFor(route.params.walletType),
borderBottomWidth: 0,

View file

@ -22,7 +22,6 @@ import ImagePicker from 'react-native-image-picker';
import {
BlueButton,
BlueButtonHook,
BlueButtonLinkHook,
BlueFormMultiInput,
BlueLoadingHook,
@ -456,7 +455,7 @@ const ViewEditMultisigCosigners = () => {
</SafeAreaView>
);
const footer = <BlueButtonHook disabled={vaultKeyData.isLoading || isSaveButtonDisabled} title={loc._.save} onPress={onSave} />;
const footer = <BlueButton disabled={vaultKeyData.isLoading || isSaveButtonDisabled} title={loc._.save} onPress={onSave} />;
return (
<SafeAreaView style={[styles.root, stylesHook.root]}>

View file

@ -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) {

View file

@ -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);
});
});

View file

@ -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', () => {