From ef40c5e1a8235d15a88f0786958553bd11ab9854 Mon Sep 17 00:00:00 2001 From: pbca26 Date: Fri, 16 Feb 2018 21:50:59 +0300 Subject: [PATCH] btc spv --- react/package.json | 1 + react/src/actions/actions/electrum.js | 38 +++- .../addcoin/addcoinOptionsCrypto.js | 4 +- .../notaryElectionsModal.scss | 7 + .../components/dashboard/sendCoin/sendCoin.js | 171 ++++++++++++++++-- .../dashboard/sendCoin/sendCoin.render.js | 42 ++++- .../dashboard/sendCoin/sendCoin.scss | 35 ++++ .../dashboard/walletsNav/walletsNav.render.js | 1 - react/src/components/overrides.scss | 46 +++++ react/src/styles/index.scss | 1 + 10 files changed, 316 insertions(+), 30 deletions(-) diff --git a/react/package.json b/react/package.json index 067d946..b801b60 100644 --- a/react/package.json +++ b/react/package.json @@ -38,6 +38,7 @@ "express": "^4.14.0", "file-loader": "^0.10.0", "qrcode.react": "^0.7.1", + "rc-slider": "8.5.0", "react": "^15.3.1", "react-dom": "^15.3.1", "react-hot-loader": "^1.3.0", diff --git a/react/src/actions/actions/electrum.js b/react/src/actions/actions/electrum.js index 584392c..ccfe5a8 100644 --- a/react/src/actions/actions/electrum.js +++ b/react/src/actions/actions/electrum.js @@ -11,6 +11,32 @@ import { } from '../actionCreators'; import Store from '../../store'; +// src: atomicexplorer +export function shepherdGetRemoteBTCFees() { + return new Promise((resolve, reject) => { + fetch(`http://atomicexplorer.com/api/btc/fees`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + .catch((error) => { + console.log(error); + Store.dispatch( + triggerToaster( + 'shepherdGetRemoteBTCFees', + 'Error', + 'error' + ) + ); + }) + .then(response => response.json()) + .then(json => { + resolve(json); + }); + }); +} + export function shepherdElectrumSetServer(coin, address, port) { return new Promise((resolve, reject) => { fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/coins/server/set?address=${address}&port=${port}&coin=${coin}&token=${Config.token}`, { @@ -198,11 +224,11 @@ export function shepherdElectrumCoinsState(json) { } // value in sats -export function shepherdElectrumSend(coin, value, sendToAddress, changeAddress) { +export function shepherdElectrumSend(coin, value, sendToAddress, changeAddress, btcFee) { value = Math.floor(value); return dispatch => { - return fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}&gui=true&push=true&verify=true&token=${Config.token}`, { + return fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}${btcFee ? '&btcfee=' + btcFee : ''}&gui=true&push=true&verify=true&token=${Config.token}`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -225,11 +251,11 @@ export function shepherdElectrumSend(coin, value, sendToAddress, changeAddress) } } -export function shepherdElectrumSendPromise(coin, value, sendToAddress, changeAddress) { +export function shepherdElectrumSendPromise(coin, value, sendToAddress, changeAddress, btcFee) { value = Math.floor(value); return new Promise((resolve, reject) => { - return fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}&gui=true&push=true&verify=true&token=${Config.token}`, { + return fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}${btcFee ? '&btcfee=' + btcFee : ''}&gui=true&push=true&verify=true&token=${Config.token}`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -252,11 +278,11 @@ export function shepherdElectrumSendPromise(coin, value, sendToAddress, changeAd }); } -export function shepherdElectrumSendPreflight(coin, value, sendToAddress, changeAddress) { +export function shepherdElectrumSendPreflight(coin, value, sendToAddress, changeAddress, btcFee) { value = Math.floor(value); return new Promise((resolve, reject) => { - fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}&gui=true&push=false&verify=true&token=${Config.token}`, { + fetch(`http://127.0.0.1:${Config.agamaPort}/shepherd/electrum/createrawtx?coin=${coin}&address=${sendToAddress}&value=${value}&change=${changeAddress}${btcFee ? '&btcfee=' + btcFee : ''}&gui=true&push=false&verify=true&token=${Config.token}`, { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/react/src/components/addcoin/addcoinOptionsCrypto.js b/react/src/components/addcoin/addcoinOptionsCrypto.js index 7f06d55..e8d49e9 100644 --- a/react/src/components/addcoin/addcoinOptionsCrypto.js +++ b/react/src/components/addcoin/addcoinOptionsCrypto.js @@ -24,11 +24,11 @@ const addCoinOptionsCrypto = () => { label: 'BitcoinCash (BCH)', icon: 'BCH', value: `BCH|spv`, - },/* { + }, { label: 'Bitcoin (BTC)', icon: 'BTC', value: `BTC|spv`, - }, */{ + }, { label: 'Crown (CRW)', icon: 'CRW', value: `CRW|spv`, diff --git a/react/src/components/dashboard/notaryElectionsModal/notaryElectionsModal.scss b/react/src/components/dashboard/notaryElectionsModal/notaryElectionsModal.scss index 3167d7e..36740fa 100644 --- a/react/src/components/dashboard/notaryElectionsModal/notaryElectionsModal.scss +++ b/react/src/components/dashboard/notaryElectionsModal/notaryElectionsModal.scss @@ -167,4 +167,11 @@ display: block; } } +} + +.page-login .notary-elections-modal { + input, + textarea { + color: #5f5d5d; + } } \ No newline at end of file diff --git a/react/src/components/dashboard/sendCoin/sendCoin.js b/react/src/components/dashboard/sendCoin/sendCoin.js index 93a1cd2..d4f43ae 100644 --- a/react/src/components/dashboard/sendCoin/sendCoin.js +++ b/react/src/components/dashboard/sendCoin/sendCoin.js @@ -10,6 +10,7 @@ import { clearLastSendToResponseState, shepherdElectrumSend, shepherdElectrumSendPreflight, + shepherdGetRemoteBTCFees, copyString, } from '../../../actions/actionCreators'; import Store from '../../../store'; @@ -21,11 +22,20 @@ import { } from './sendCoin.render'; import { isPositiveNumber } from '../../../util/number'; import mainWindow from '../../../util/mainWindow'; +import Slider, { Range } from 'rc-slider'; +import ReactTooltip from 'react-tooltip'; // TODO: - add links to explorers // - render z address trim // - handle click outside +const _feeLookup = [ + 'fastestFee', + 'halfHourFee', + 'hourFee', + 'advanced' +]; + class SendCoin extends React.Component { constructor(props) { super(props); @@ -44,7 +54,14 @@ class SendCoin extends React.Component { coin: null, spvVerificationWarning: false, spvPreflightSendInProgress: false, + btcFees: {}, + btcFeesType: 'halfHourFee', + btcFeesAdvancedStep: 9, + btcFeesSize: 0, + btcFeesTimeBasedStep: 1, + btcPreflightRes: null, }; + this.defaultState = JSON.parse(JSON.stringify(this.state)); this.updateInput = this.updateInput.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.openDropMenu = this.openDropMenu.bind(this); @@ -58,13 +75,17 @@ class SendCoin extends React.Component { this.isFullySynced = this.isFullySynced.bind(this); this.setSendAmountAll = this.setSendAmountAll.bind(this); this.setSendToSelf = this.setSendToSelf.bind(this); + this.fetchBTCFees = this.fetchBTCFees.bind(this); + this.onSliderChange = this.onSliderChange.bind(this); + this.onSliderChangeTime = this.onSliderChangeTime.bind(this); } setSendAmountAll() { const _amount = this.state.amount; const _amountSats = this.state.amount * 100000000; const _balanceSats = this.props.ActiveCoin.balance.balanceSats; - const _fees = mainWindow.spvFees; + let _fees = mainWindow.spvFees; + _fees.BTC = 0; this.setState({ amount: Number((0.00000001 * (_balanceSats - _fees[this.props.ActiveCoin.coin])).toFixed(8)), @@ -136,6 +157,11 @@ class SendCoin extends React.Component { Store.dispatch(clearLastSendToResponseState()); } this.checkZAddressCount(props); + + if (this.props.ActiveCoin.activeSection !== props.ActiveCoin.activeSection && + this.props.ActiveCoin.activeSection !== 'send') { + this.fetchBTCFees(); + } } setRecieverFromScan(receiver) { @@ -400,8 +426,29 @@ class SendCoin extends React.Component { }); } + fetchBTCFees() { + if (this.props.ActiveCoin.mode === 'spv' && + this.props.ActiveCoin.coin === 'BTC') { + shepherdGetRemoteBTCFees() + .then((res) => { + if (res.msg === 'success') { + // TODO: check, approx fiat value + this.setState({ + btcFees: res.result, + btcFeesSize: this.state.btcFeesType === 'advanced' ? res.result.electrum[this.state.btcFeesAdvancedStep] : res.result.recommended[_feeLookup[this.state.btcFeesTimeBasedStep]], + }); + } else { + // TODO: fallback to local electrum + } + console.warn('btcfees', res); + }); + } + } + changeSendCoinStep(step, back) { if (step === 0) { + this.fetchBTCFees(); + if (back) { this.setState({ currentStep: 0, @@ -411,21 +458,7 @@ class SendCoin extends React.Component { } else { Store.dispatch(clearLastSendToResponseState()); - this.setState({ - currentStep: 0, - addressType: null, - sendFrom: null, - sendFromAmount: 0, - sendTo: '', - sendToOA: null, - amount: 0, - fee: 0, - addressSelectorOpen: false, - renderAddressDropdown: true, - subtractFee: false, - spvVerificationWarning: false, - spvPreflightSendInProgress: false, - }); + this.setState(this.defaultState); } } @@ -444,7 +477,8 @@ class SendCoin extends React.Component { this.props.ActiveCoin.coin, this.state.amount * 100000000, this.state.sendTo, - this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub + this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub, + this.props.ActiveCoin.coin === 'BTC' ? this.state.btcFeesSize : null ) .then((sendPreflight) => { if (sendPreflight && @@ -452,6 +486,7 @@ class SendCoin extends React.Component { this.setState(Object.assign({}, this.state, { spvVerificationWarning: !sendPreflight.result.utxoVerified, spvPreflightSendInProgress: false, + btcPreflightRes: this.props.ActiveCoin.coin === 'BTC' ? { fee: sendPreflight.result.fee, value: sendPreflight.result.value, change: sendPreflight.result.change } : null, })); } else { this.setState(Object.assign({}, this.state, { @@ -502,7 +537,8 @@ class SendCoin extends React.Component { this.props.ActiveCoin.coin, this.state.amount * 100000000, this.state.sendTo, - this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub + this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub, + this.props.ActiveCoin.coin === 'BTC' ? this.state.btcFeesSize : null ) ); } @@ -517,7 +553,8 @@ class SendCoin extends React.Component { const _amount = this.state.amount; const _amountSats = this.state.amount * 100000000; const _balanceSats = this.props.ActiveCoin.balance.balanceSats; - const _fees = mainWindow.spvFees; + let _fees = mainWindow.spvFees; + _fees.BTC = 0; if (Number(_amountSats) + _fees[this.props.ActiveCoin.coin] > _balanceSats) { Store.dispatch( @@ -638,6 +675,102 @@ class SendCoin extends React.Component { } } + onSliderChange(value) { + console.warn(value); + console.warn(`btc fee /byte ${this.state.btcFees.electrum[value]}`); + this.setState({ + btcFeesSize: this.state.btcFees.electrum[value], + btcFeesAdvancedStep: value, + }); + } + + onSliderChangeTime(value) { + console.warn(value); + console.warn(`btc fee /byte ${_feeLookup[value]}`); + this.setState({ + btcFeesSize: this.state.btcFees.recommended[_feeLookup[value]], + btcFeesTimeBasedStep: value, + btcFeesType: _feeLookup[value] === 'advanced' ? 'advanced' : null, + }); + } + + renderBTCFees() { + if (this.props.ActiveCoin.mode === 'spv' && + this.props.ActiveCoin.coin === 'BTC' && + !this.state.btcFees.lastUpdated) { + return (
Fetching BTC fees...
); + } else if (this.props.ActiveCoin.mode === 'spv' && this.props.ActiveCoin.coin === 'BTC' && this.state.btcFees.lastUpdated) { + const _min = 0; + const _max = this.state.btcFees.electrum.length - 1; + const _confTime = [ + 'within less than 30 min', + 'within 30 min', + 'within 60 min', + ]; + const _minTimeBased = 0; + const _maxTimeBased = 3; + + /*let _marks = {}; + + for (let i = _min; i < _max; i++) { + _marks[i] = i + 1; + }*/ + + return ( +
+
+
+ Fee + + It is advised to use fast/average/slow options if you want your transaction to be confirmed within 60 min time frame.' : 'Estimates are based on bitcoinfees.earn.com data.
Around 90% probability for a transaction to be confirmed within desired time frame.' }>
+ +
+
+
+ { this.state.btcFeesType !== 'advanced' && + Confirmation time { _confTime[this.state.btcFeesTimeBasedStep] } + } + { this.state.btcFeesType === 'advanced' && + Advanced selection + } +
+ + { this.state.btcFeesType === 'advanced' && +
+
Estimated to be included within the next {this.state.btcFeesAdvancedStep + 1} {(this.state.btcFeesAdvancedStep + 1) > 1 ? 'blocks' : 'block'}
+ +
+ } + { this.state.btcFeesSize > 0 && +
Fee per byte {this.state.btcFeesSize}, per KB {this.state.btcFeesSize * 1024}
+ } +
+
+ ); + } + } + render() { if (this.props && this.props.ActiveCoin && diff --git a/react/src/components/dashboard/sendCoin/sendCoin.render.js b/react/src/components/dashboard/sendCoin/sendCoin.render.js index f16b6f9..fbb6b21 100644 --- a/react/src/components/dashboard/sendCoin/sendCoin.render.js +++ b/react/src/components/dashboard/sendCoin/sendCoin.render.js @@ -2,6 +2,8 @@ import React from 'react'; import { translate } from '../../../translate/translate'; import QRModal from '../qrModal/qrModal'; import { isKomodoCoin } from '../../../util/coinHelper'; +import { formatValue } from '../../../util/formatValue'; +import ReactTooltip from 'react-tooltip'; export const AddressListRender = function() { return ( @@ -117,6 +119,7 @@ export const _SendFormRender = function() { } + { this.renderBTCFees() }