Unverified Commit f95256ae authored by Tankred Hase's avatar Tankred Hase Committed by GitHub

Merge pull request #1304 from lightninglabs/dev/send-coin-fee-selection

Dev/send coin fee selection
parents 6f89cac9 26de7af6
......@@ -20,7 +20,7 @@ neutrino.feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
autopilot.active=0
autopilot.private=1
autopilot.minconfs=1
autopilot.conftarget=6
autopilot.conftarget=16
autopilot.allocation=1.0
autopilot.heuristic=externalscore:0.95
autopilot.heuristic=preferential:0.05
......@@ -20,7 +20,7 @@ neutrino.feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
autopilot.active=0
autopilot.private=1
autopilot.minconfs=1
autopilot.conftarget=6
autopilot.conftarget=16
autopilot.allocation=1.0
autopilot.heuristic=externalscore:0.95
autopilot.heuristic=preferential:0.05
......@@ -3896,6 +3896,11 @@
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.pad": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
......@@ -5523,6 +5528,14 @@
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-3.1.3.tgz",
"integrity": "sha512-eWUbjYJge4icX8FhWJk/OPlyGxPnW9bZDysBX3WwOG37iurdH692HKnM2Ih+S+0te65RytImvUrcVnHVBbumYg=="
},
"react-native-picker-select": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-6.3.2.tgz",
"integrity": "sha512-f7DNP9M4UHOtsXizpbd1jWgzV38Yn9WEPu+rY2o8to4rr+CHXl/V6W2G1qXkDPMIzUa9oClYsK8PylVRSoH8iQ==",
"requires": {
"lodash.isequal": "^4.5.0"
}
},
"react-native-randombytes": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/react-native-randombytes/-/react-native-randombytes-3.5.3.tgz",
......
......@@ -25,6 +25,7 @@
"react-native-gesture-handler": "^1.3.0",
"react-native-icloudstore": "^0.9.0",
"react-native-keychain": "^3.1.3",
"react-native-picker-select": "^6.3.2",
"react-native-share": "^1.2.1",
"react-native-svg": "^9.6.2",
"react-native-unimodules": "^0.5.4",
......
......@@ -52,6 +52,10 @@ module.exports = {
__dirname,
'node_modules/react-native-device-info'
),
'react-native-picker-select': path.resolve(
__dirname,
'node_modules/react-native-picker-select'
),
mobx: path.resolve(__dirname, 'node_modules/mobx'),
'mobx-react': path.resolve(__dirname, 'node_modules/mobx-react'),
'locale-currency': path.resolve(
......
......@@ -66,7 +66,7 @@ module.exports.startLndProcess = async function({
'--historicalsyncinterval=20m',
'--autopilot.private',
'--autopilot.minconfs=1',
'--autopilot.conftarget=6',
'--autopilot.conftarget=16',
'--autopilot.allocation=1.0',
'--autopilot.heuristic=externalscore:0.95',
'--autopilot.heuristic=preferential:0.05',
......
......@@ -7,7 +7,9 @@ import {
PREFIX_REGEX,
PAYMENT_TIMEOUT,
POLL_STORE_TIMEOUT,
SEND_TARGET_CONF,
LOW_TARGET_CONF,
MED_TARGET_CONF,
HIGH_TARGET_CONF,
} from '../config';
import { toSatoshis, toAmount, isLnUri, isAddress, nap } from '../helper';
import * as log from './log';
......@@ -105,6 +107,7 @@ class PaymentAction {
init() {
this._store.payment.address = '';
this._store.payment.amount = '';
this._store.payment.targetConf = MED_TARGET_CONF;
this._store.payment.fee = '';
this._store.payment.note = '';
this._store.payment.useScanner = false;
......@@ -223,14 +226,40 @@ class PaymentAction {
* @return {Promise<undefined>}
*/
async estimateFee() {
const { payment } = this._store;
payment.feeEstimates = [];
await this._fetchEstimate(LOW_TARGET_CONF, 'Low');
await this._fetchEstimate(MED_TARGET_CONF, 'Med');
await this._fetchEstimate(HIGH_TARGET_CONF, 'High');
payment.fee = payment.feeEstimates[1].fee;
}
async _fetchEstimate(targetConf, prio) {
const { payment, settings } = this._store;
const AddrToAmount = {};
AddrToAmount[payment.address] = toSatoshis(payment.amount, settings);
const { feeSat } = await this._grpc.sendCommand('estimateFee', {
AddrToAmount,
targetConf: SEND_TARGET_CONF,
targetConf,
});
payment.fee = toAmount(feeSat, settings);
payment.feeEstimates.push({
fee: toAmount(feeSat, settings),
targetConf,
prio,
});
}
/**
* Set the target_conf for the on-chain send operation and set a fee.
* @param {number} options.targetConf The number blocks to target
*/
setTargetConf({ targetConf }) {
const { payment } = this._store;
payment.targetConf = targetConf;
if (!payment.feeEstimates.length) return;
payment.fee = payment.feeEstimates.find(
e => e.targetConf === targetConf
).fee;
}
/**
......@@ -286,7 +315,7 @@ class PaymentAction {
await this._grpc.sendCommand('sendCoins', {
addr: payment.address,
amount,
targetConf: SEND_TARGET_CONF,
targetConf: payment.targetConf,
sendAll: payment.sendAll,
});
}
......
import React from 'react';
import { StyleSheet, View, ViewPropTypes } from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import PropTypes from 'prop-types';
import Text from './text';
import ArrowDownIcon from '../asset/icon/arrow-down';
import { color, font } from './style';
//
// Named Field Select
//
const namedSelectStyles = StyleSheet.create({
content: {
alignSelf: 'stretch',
flexDirection: 'row',
justifyContent: 'space-between',
borderBottomColor: color.blackText,
borderBottomWidth: 1,
},
name: {
color: color.blackText,
fontSize: font.sizeM,
lineHeight: font.lineHeightM + 2 * 12,
marginRight: 15,
},
wrapper: {
flexDirection: 'row',
alignItems: 'center',
},
});
const baseInputStyles = {
fontFamily: 'OpenSans Regular',
fontSize: font.sizeM,
lineHeight: font.lineHeightM + 3,
height: font.lineHeightM + 2 * 12,
color: color.blackText,
padding: 0,
};
const pickerStyles = StyleSheet.create({
inputIOS: {
...baseInputStyles,
opacity: 0.75,
},
inputAndroid: {
...baseInputStyles,
opacity: 0.5,
},
});
export const NamedFieldSelect = ({ name, style, ...props }) => (
<View style={[namedSelectStyles.content, style]}>
<Text style={namedSelectStyles.name}>{name}</Text>
<View style={namedSelectStyles.wrapper}>
<RNPickerSelect
placeholder={{}}
style={pickerStyles}
useNativeAndroidPickerStyle={false}
{...props}
/>
<ArrowDownIcon height={22} width={22} stroke="#969596" />
</View>
</View>
);
NamedFieldSelect.propTypes = {
name: PropTypes.string,
style: ViewPropTypes.style,
};
......@@ -10,6 +10,15 @@ const ComputedPayment = store => {
get paymentAmountLabel() {
return toLabel(store.payment.amount, store.settings);
},
get paymentFeeEstimateItems() {
return store.payment.feeEstimates.map(e => {
const feeLabel = toLabel(e.fee, store.settings);
return {
label: `${e.prio} ${feeLabel} ${store.unitLabel || ''}`.trim(),
value: e.targetConf,
};
});
},
get paymentFeeLabel() {
return toLabel(store.payment.fee, store.settings);
},
......
......@@ -24,7 +24,9 @@ module.exports.PREFIX_URI = `${prefixName}:`;
module.exports.PREFIX_REGEX = /^[a-zA-Z]*:/;
module.exports.DEFAULT_ROUTE = 'Welcome';
module.exports.SEND_TARGET_CONF = 6;
module.exports.LOW_TARGET_CONF = 26;
module.exports.MED_TARGET_CONF = 16;
module.exports.HIGH_TARGET_CONF = 4;
module.exports.PIN_LENGTH = 6;
module.exports.MIN_PASSWORD_LENGTH = 8;
module.exports.STRONG_PASSWORD_LENGTH = 12;
......
......@@ -14,7 +14,12 @@ import ComputedPayment from './computed/payment';
import ComputedNotification from './computed/notification';
import ComputedSetting from './computed/setting';
import ComputedSeed from './computed/seed';
import { DEFAULT_ROUTE, DEFAULT_UNIT, DEFAULT_FIAT } from './config';
import {
DEFAULT_ROUTE,
DEFAULT_UNIT,
DEFAULT_FIAT,
MED_TARGET_CONF,
} from './config';
export class Store {
constructor() {
......@@ -69,6 +74,8 @@ export class Store {
payment: {
address: '',
amount: '',
targetConf: MED_TARGET_CONF,
feeEstimates: [],
fee: '',
note: '',
sendAll: false,
......
......@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import Background from '../component/background';
import MainContent from '../component/main-content';
import { NamedField } from '../component/field';
import { NamedFieldSelect } from '../component/field-mobile';
import { Header, Title } from '../component/header';
import { CancelButton, BackButton, SmallGlasButton } from '../component/button';
import Card from '../component/card';
......@@ -66,9 +67,12 @@ const PayBitcoinConfirmView = ({ store, nav, payment }) => (
{store.unitLabel}
</BalanceLabelUnit>
</BalanceLabel>
<NamedField name="Fee">
{store.paymentFeeLabel} {store.unitLabel}
</NamedField>
<NamedFieldSelect
name="Fee"
items={store.paymentFeeEstimateItems}
value={store.payment.targetConf}
onValueChange={targetConf => payment.setTargetConf({ targetConf })}
/>
<NamedField name="Total" style={styles.totalLbl}>
{store.paymentTotalLabel} {store.unitLabel}
</NamedField>
......
......@@ -53,7 +53,6 @@ import PayLightningDoneMobile from '../src/view/pay-lightning-done-mobile';
import PayBitcoin from '../src/view/pay-bitcoin';
import PayBitcoinMobile from '../src/view/pay-bitcoin-mobile';
import PayBitcoinConfirm from '../src/view/pay-bitcoin-confirm';
import PayBitcoinConfirmMobile from '../src/view/pay-bitcoin-confirm-mobile';
import PayBitcoinDone from '../src/view/pay-bitcoin-done';
import PayBitcoinDoneMobile from '../src/view/pay-bitcoin-done-mobile';
import PaymentFailed from '../src/view/payment-failed';
......@@ -304,9 +303,6 @@ storiesOf('Screens', module)
.add('Pay Bitcoin Confirm', () => (
<PayBitcoinConfirm store={store} payment={payment} nav={nav} />
))
.add('Pay Bitcoin Confirm (Mobile)', () => (
<PayBitcoinConfirmMobile store={store} payment={payment} nav={navMobile} />
))
.add('Pay Bitcoin Done', () => <PayBitcoinDone payment={payment} nav={nav} />)
.add('Pay Bitcoin Done (Mobile)', () => (
<PayBitcoinDoneMobile payment={payment} nav={navMobile} />
......
......@@ -161,6 +161,7 @@ describe('Action Payments Unit Tests', () => {
store.payment.amount = 'bar';
store.payment.note = 'baz';
store.payment.fee = 'blub';
store.payment.targetConf = 1;
store.payment.useScanner = true;
store.payment.sendAll = true;
payment.init();
......@@ -169,11 +170,45 @@ describe('Action Payments Unit Tests', () => {
expect(store.payment.note, 'to equal', '');
expect(store.payment.fee, 'to equal', '');
expect(store.payment.useScanner, 'to equal', false);
expect(store.payment.targetConf, 'to equal', 16);
expect(store.payment.sendAll, 'to equal', false);
expect(nav.goPay, 'was called once');
});
});
describe('estimateFee()', () => {
beforeEach(() => {
store.payment.address = 'foo';
store.payment.amount = '2000';
grpc.sendCommand.withArgs('estimateFee').resolves({
feeSat: 10000,
});
});
it('should get three fee estimates', async () => {
await payment.estimateFee();
expect(grpc.sendCommand, 'was called thrice');
expect(store.payment.feeEstimates[0].prio, 'to equal', 'Low');
expect(store.payment.feeEstimates[1].prio, 'to equal', 'Med');
expect(store.payment.feeEstimates[2].prio, 'to equal', 'High');
});
});
describe('setTargetConf()', () => {
it('should set target conf and fee', async () => {
store.payment.feeEstimates = [{ targetConf: 6, fee: '42' }];
await payment.setTargetConf({ targetConf: 6 });
expect(store.payment.targetConf, 'to equal', 6);
expect(store.payment.fee, 'to equal', '42');
});
it('should set target conf but not fee if not estimates', async () => {
await payment.setTargetConf({ targetConf: 6 });
expect(store.payment.targetConf, 'to equal', 6);
expect(store.payment.fee, 'to equal', '');
});
});
describe('initPayBitcoinConfirm()', () => {
beforeEach(() => {
store.payment.address = 'foo';
......@@ -185,7 +220,7 @@ describe('Action Payments Unit Tests', () => {
it('should get estimate and navigate to confirm view', async () => {
await payment.initPayBitcoinConfirm();
expect(grpc.sendCommand, 'was called once');
expect(grpc.sendCommand, 'was called thrice');
expect(nav.goPayBitcoinConfirm, 'was called once');
expect(notification.display, 'was not called');
expect(store.payment.fee, 'to be', '0.0001');
......@@ -204,7 +239,7 @@ describe('Action Payments Unit Tests', () => {
it('should get estimate and navigate if fee is set', async () => {
store.payment.fee = '0.0002';
await payment.initPayBitcoinConfirm();
expect(grpc.sendCommand, 'was called once');
expect(grpc.sendCommand, 'was called thrice');
expect(nav.goPayBitcoinConfirm, 'was called once');
expect(notification.display, 'was not called');
expect(store.payment.fee, 'to be', '0.0001');
......@@ -213,7 +248,7 @@ describe('Action Payments Unit Tests', () => {
it('should get estimate and navigate if sendAll is set', async () => {
store.payment.sendAll = true;
await payment.initPayBitcoinConfirm();
expect(grpc.sendCommand, 'was called once');
expect(grpc.sendCommand, 'was called thrice');
expect(nav.goPayBitcoinConfirm, 'was called once');
expect(notification.display, 'was not called');
expect(store.payment.fee, 'to be', '0.0001');
......
......@@ -14,10 +14,29 @@ describe('Computed Payment Unit Tests', () => {
it('should work with initial store', () => {
ComputedPayment(store);
expect(store.paymentAmountLabel, 'to equal', '0');
expect(store.paymentFeeEstimateItems, 'to equal', []);
expect(store.paymentFeeLabel, 'to equal', '0');
expect(store.paymentTotalLabel, 'to equal', '0');
});
it('should calculate fee estimate label', () => {
store.unitLabel = 'sats';
store.payment.feeEstimates = [
{
fee: '10',
targetConf: 6,
prio: 'Med',
},
];
ComputedPayment(store);
expect(
store.paymentFeeEstimateItems[0].label,
'to match',
/^Med 10 sats$/
);
expect(store.paymentFeeEstimateItems[0].value, 'to equal', 6);
});
it('should calculate btc total', () => {
store.payment.fee = '0.0001';
store.payment.amount = '0.1';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment