From 7b138a94891d444ae63edfd9bd32d9914a0e9c5b Mon Sep 17 00:00:00 2001 From: pbca26 <pbca26@gmail.com> Date: Mon, 14 Aug 2017 17:29:56 +0300 Subject: [PATCH] claim interest modal --- react/src/actions/actionCreators.js | 9 ++ react/src/actions/actions/interest.js | 86 +++++++++++ react/src/actions/actions/log.js | 1 - react/src/actions/actions/nativeSend.js | 43 +++++- react/src/actions/actions/nativeSyncInfo.js | 2 +- react/src/actions/storeType.js | 3 +- .../claimInterestModal/claimInterestModal.js | 118 +++++++++++++++ .../claimInterestModal.render.js | 140 ++++++++++++++++++ .../src/components/dashboard/navbar/navbar.js | 6 + .../dashboard/navbar/navbar.render.js | 2 +- .../walletsNativeInfo/walletsNativeInfo.js | 7 + .../walletsNativeInfo.render.js | 10 ++ react/src/components/overrides.scss | 44 ++++++ react/src/reducers/dashboard.js | 13 +- 14 files changed, 476 insertions(+), 8 deletions(-) create mode 100644 react/src/actions/actions/interest.js create mode 100755 react/src/components/dashboard/claimInterestModal/claimInterestModal.js create mode 100644 react/src/components/dashboard/claimInterestModal/claimInterestModal.render.js diff --git a/react/src/actions/actionCreators.js b/react/src/actions/actionCreators.js index 68e99e4..5919f83 100644 --- a/react/src/actions/actionCreators.js +++ b/react/src/actions/actionCreators.js @@ -27,6 +27,7 @@ import { DASHBOARD_ACTIVE_COIN_NATIVE_TXHISTORY, DISPLAY_LOGIN_SETTINGS_MODAL, DISPLAY_COIND_DOWN_MODAL, + DISPLAY_CLAIM_INTEREST_MODAL, START_INTERVAL, STOP_INTERVAL } from './storeType'; @@ -69,6 +70,7 @@ export * from './actions/iguanaHelpers'; export * from './actions/cli'; export * from './actions/update'; export * from './actions/jumblr'; +export * from './actions/interest'; export function changeActiveAddress(address) { return { @@ -367,4 +369,11 @@ export function toggleLoginSettingsModal(display) { type: DISPLAY_LOGIN_SETTINGS_MODAL, displayLoginSettingsModal: display, } +} + +export function toggleClaimInterestModal(display) { + return { + type: DISPLAY_CLAIM_INTEREST_MODAL, + displayClaimInterestModal: display, + } } \ No newline at end of file diff --git a/react/src/actions/actions/interest.js b/react/src/actions/actions/interest.js new file mode 100644 index 0000000..1dd95c6 --- /dev/null +++ b/react/src/actions/actions/interest.js @@ -0,0 +1,86 @@ +import { + triggerToaster +} from '../actionCreators'; +import { + logGuiHttp, + guiLogState +} from './log'; +import Config from '../../config'; + +export function getListUnspent(coin) { + return new Promise((resolve, reject) => { + const payload = { + mode: null, + chain: coin, + cmd: 'listunspent', + }; + + const _fetchConfig = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ 'payload': payload }), + }; + + fetch( + `http://127.0.0.1:${Config.agamaPort}/shepherd/cli`, + _fetchConfig + ) + .catch(function(error) { + console.log(error); + dispatch( + triggerToaster( + 'getListUnspent', + 'Error', + 'error' + ) + ); + }) + .then(response => response.json()) + .then(json => { + resolve(json.result ? json.result : json); + }) + }); +} + +export function getRawTransaction(coin, txid) { + return new Promise((resolve, reject) => { + const payload = { + mode: null, + chain: coin, + cmd: 'getrawtransaction', + params: [ + txid, + 1 + ], + }; + + const _fetchConfig = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ 'payload': payload }), + }; + + fetch( + `http://127.0.0.1:${Config.agamaPort}/shepherd/cli`, + _fetchConfig + ) + .catch(function(error) { + console.log(error); + dispatch( + triggerToaster( + 'getTransaction', + 'Error', + 'error' + ) + ); + }) + .then(response => response.json()) + .then(json => { + resolve(json.result ? json.result : json); + }) + }); +} \ No newline at end of file diff --git a/react/src/actions/actions/log.js b/react/src/actions/actions/log.js index ca42d5b..c53f402 100644 --- a/react/src/actions/actions/log.js +++ b/react/src/actions/actions/log.js @@ -41,7 +41,6 @@ export function getAgamaLog(type) { ); }) .then(response => response.json()) - .then() } } diff --git a/react/src/actions/actions/nativeSend.js b/react/src/actions/actions/nativeSend.js index 0f788c8..e6cbfbc 100644 --- a/react/src/actions/actions/nativeSend.js +++ b/react/src/actions/actions/nativeSend.js @@ -195,7 +195,7 @@ export function getKMDOPID(opid, coin) { passthruAgent = getPassthruAgent(coin), tmpIguanaRPCAuth = `tmpIgRPCUser@${sessionStorage.getItem('IguanaRPCAuth')}`; - if (passthruAgent == 'iguana') { + if (passthruAgent === 'iguana') { payload = { 'userpass': tmpIguanaRPCAuth, 'agent': passthruAgent, @@ -284,4 +284,45 @@ export function getKMDOPID(opid, coin) { }) }) } +} + +export function sendFromPromise(coin, address, amount) { + return new Promise((resolve, reject) => { + const payload = { + mode: null, + chain: coin, + cmd: 'sendfrom', + params: [ + address, + amount + ] + }; + + const _fetchConfig = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ 'payload': payload }), + }; + + fetch( + `http://127.0.0.1:${Config.agamaPort}/shepherd/cli`, + _fetchConfig + ) + .catch(function(error) { + console.log(error); + dispatch( + triggerToaster( + 'sendFrom', + 'Error', + 'error' + ) + ); + }) + .then(response => response.json()) + .then(json => { + resolve(json.result ? json.result : json); + }) + }); } \ No newline at end of file diff --git a/react/src/actions/actions/nativeSyncInfo.js b/react/src/actions/actions/nativeSyncInfo.js index 196ef53..b75b161 100644 --- a/react/src/actions/actions/nativeSyncInfo.js +++ b/react/src/actions/actions/nativeSyncInfo.js @@ -13,7 +13,7 @@ import Config from '../../config'; export function getSyncInfoNativeKMD(skipDebug, json) { const coin = 'KMD'; - + // https://www.kmd.host/ return dispatch => { const _timestamp = Date.now(); if (Config.debug) { diff --git a/react/src/actions/storeType.js b/react/src/actions/storeType.js index 23deb33..1e4c8af 100644 --- a/react/src/actions/storeType.js +++ b/react/src/actions/storeType.js @@ -45,4 +45,5 @@ export const LOG_GUI_HTTP = 'LOG_GUI_HTTP'; export const CLI = 'CLI'; export const LOGOUT = 'LOGOUT'; export const DISPLAY_COIND_DOWN_MODAL = 'DISPLAY_COIND_DOWN_MODAL'; -export const DISPLAY_LOGIN_SETTINGS_MODAL = 'DISPLAY_LOGIN_SETTINGS_MODAL'; \ No newline at end of file +export const DISPLAY_LOGIN_SETTINGS_MODAL = 'DISPLAY_LOGIN_SETTINGS_MODAL'; +export const DISPLAY_CLAIM_INTEREST_MODAL = 'DISPLAY_CLAIM_INTEREST_MODAL'; \ No newline at end of file diff --git a/react/src/components/dashboard/claimInterestModal/claimInterestModal.js b/react/src/components/dashboard/claimInterestModal/claimInterestModal.js new file mode 100755 index 0000000..95b70bb --- /dev/null +++ b/react/src/components/dashboard/claimInterestModal/claimInterestModal.js @@ -0,0 +1,118 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Store from '../../../store'; +import { + toggleClaimInterestModal, + getListUnspent, + getRawTransaction, + copyString, + sendFromPromise +} from '../../../actions/actionCreators'; +import { translate } from '../../../translate/translate'; +import { + ClaimInterestModalRender, + _ClaimInterestTableRender +} from './claimInterestModal.render'; + +class ClaimInterestModal extends React.Component { + constructor(props) { + super(props); + this.state = { + open: false, + isLoading: true, + transactionsList: [], + showZeroInterest: true, + }; + this.claimInterestTableRender = this.claimInterestTableRender.bind(this); + this.toggleZeroInterest = this.toggleZeroInterest.bind(this); + this.loadListUnspent = this.loadListUnspent.bind(this); + this.checkTransactionsListLength = this.checkTransactionsListLength.bind(this); + } + + componentWillMount() { + this.loadListUnspent(); + } + + loadListUnspent() { + let _transactionsList = []; + + getListUnspent('KMD') + .then((json) => { + if (json && + json.length) { + for (let i = 0; i < json.length; i++) { + getRawTransaction('KMD', json[i].txid) + .then((_json) => { + _transactionsList.push({ + address: json[i].address, + locktime: _json.locktime, + amount: json[i].amount, + interest: json[i].interest, + txid: json[i].txid, + }); + + if (i === json.length - 1) { + this.setState({ + transactionsList: _transactionsList, + isLoading: false, + }); + } + }); + } + } + }); + } + + claimInterest(address, amount) { + console.warn('claim interest', `${address} ${amount}`); + /*sendFromPromise(address, amount) + .then((json) => { + console.warn(json); + });*/ + } + + checkTransactionsListLength() { + if (this.state.transactionsList && this.state.transactionsList.length) { + return true; + } else if (!this.state.transactionsList || !this.state.transactionsList.length) { + return false; + } + } + + toggleZeroInterest() { + this.setState({ + showZeroInterest: !this.state.showZeroInterest, + }); + } + + copyTxId(txid) { + Store.dispatch(copyString(txid, 'Transaction ID copied')); + } + + claimInterestTableRender() { + return _ClaimInterestTableRender.call(this); + } + + componentWillReceiveProps(props) { + if (props.Dashboard.displayClaimInterestModal !== this.state.open) { + this.setState({ + open: props.Dashboard.displayClaimInterestModal, + }); + } + + if (!this.state.open && + props.Dashboard.displayClaimInterestModal) { + this.loadListUnspent(); + } + } + + closeModal() { + Store.dispatch(toggleClaimInterestModal(false)); + } + + render() { + return ClaimInterestModalRender.call(this); + } +} + +export default ClaimInterestModal; \ No newline at end of file diff --git a/react/src/components/dashboard/claimInterestModal/claimInterestModal.render.js b/react/src/components/dashboard/claimInterestModal/claimInterestModal.render.js new file mode 100644 index 0000000..8872fc2 --- /dev/null +++ b/react/src/components/dashboard/claimInterestModal/claimInterestModal.render.js @@ -0,0 +1,140 @@ +import React from 'react'; +import { translate } from '../../../translate/translate'; + +const MIN_INTEREST_THRESHOLD = 0.001; + +export const _ClaimInterestTableRender = function() { + const _transactionsList = this.state.transactionsList; + let _items = []; + + for (let i = 0; i < _transactionsList.length; i++) { + if ((_transactionsList[i].interest === 0 && this.state.showZeroInterest) || (_transactionsList[i].amount > 0 && _transactionsList[i].interest > 0)) { + _items.push( + <tr key={ `${_transactionsList[i].txid}${_transactionsList[i].address}` }> + <td> + <button + className="btn btn-default btn-xs clipboard-edexaddr copy-string-btn" + title={ translate('INDEX.COPY_TO_CLIPBOARD') } + onClick={ () => this.copyTxId(_transactionsList[i].txid) }> + <i className="icon wb-copy"></i> { translate('INDEX.COPY') } + </button> + </td> + <td>{ _transactionsList[i].address }</td> + <td className={ _transactionsList[i].amount > 10 ? 'green bold' : '' }>{ _transactionsList[i].amount }</td> + <td>{ _transactionsList[i].interest }</td> + <td className="locktime center"> + { _transactionsList[i].locktime && + <i className="fa-check-circle green"></i> + } + { !_transactionsList[i].locktime && + <i className="fa-exclamation-circle red"></i> + } + </td> + <td> + <button + type="button" + className={ 'btn btn-success waves-effect waves-light' + (_transactionsList[i].interest < MIN_INTEREST_THRESHOLD ? ' show' : '') } + onClick={ () => this.claimInterest(_transactionsList[i].address, _transactionsList[i].amount) }> + <i className="icon fa-dollar"></i> Claim + </button> + </td> + </tr> + ); + } + } + + return ( + <span> + <div className="padding-bottom-20"> + <strong>Requirements to accure interest:</strong> locktime field is set and amount is greater than 10 KMD + </div> + <div className="text-left padding-top-10 padding-bottom-10"> + <label className="switch"> + <input + type="checkbox" + checked={ this.state.showZeroInterest } /> + <div + className="slider" + onClick={ this.toggleZeroInterest }></div> + </label> + <div + className="toggle-label margin-right-15 pointer" + onClick={ this.toggleZeroInterest }> + Show zero interest + </div> + </div> + <div className="table-scroll"> + <table className="table table-hover dataTable table-striped"> + <thead> + <tr> + <th></th> + <th>Address</th> + <th>Amount</th> + <th>Interest</th> + <th>Locktime</th> + <th></th> + </tr> + </thead> + <tbody> + { _items } + </tbody> + <tfoot> + <tr> + <th></th> + <th>Address</th> + <th>Amount</th> + <th>Interest</th> + <th>Locktime</th> + <th></th> + </tr> + </tfoot> + </table> + </div> + </span> + ); +}; + + //{ this.renderAddressList('public') } + //{ this.isNativeMode() && this.renderAddressList('private') } + + +export const ClaimInterestModalRender = function() { + return ( + <span> + <div className={ 'modal modal-claim-interest modal-3d-sign ' + (this.state.open ? 'show in' : 'fade hide') }> + <div className="modal-dialog modal-center modal-sm"> + <div className="modal-content"> + <div className="modal-header bg-orange-a400 wallet-send-header"> + <button + type="button" + className="close white" + onClick={ this.closeModal }> + <span>×</span> + </button> + <h4 className="modal-title white text-left">Claim interest</h4> + </div> + <div className="modal-body"> + <i + className="icon fa-refresh pointer refresh-icon" + onClick={ this.loadListUnspent }></i> + <div className="animsition vertical-align fade-in"> + <div className="page-content vertical-align-middle full-width"> + { this.state.isLoading && + <span>Loading interest data...</span> + } + { !this.state.isLoading && this.checkTransactionsListLength() && + <div>{ this.claimInterestTableRender() }</div> + } + { !this.state.isLoading && !this.checkTransactionsListLength() && + <div>No data</div> + } + </div> + </div> + </div> + </div> + </div> + </div> + <div className={ 'modal-backdrop ' + (this.state.open ? 'show in' : 'fade hide') }></div> + </span> + ); +}; \ No newline at end of file diff --git a/react/src/components/dashboard/navbar/navbar.js b/react/src/components/dashboard/navbar/navbar.js index 6480b97..dfacb44 100755 --- a/react/src/components/dashboard/navbar/navbar.js +++ b/react/src/components/dashboard/navbar/navbar.js @@ -10,6 +10,7 @@ import { } from '../../../actions/actionCreators'; import Store from '../../../store'; import Config from '../../../config'; +import { checkAC } from '../../addcoin/payload'; import NavbarRender from './navbar.render'; @@ -23,6 +24,7 @@ class Navbar extends React.Component { this.openDropMenu = this.openDropMenu.bind(this); this.logout = this.logout.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + this._checkAC = this._checkAC.bind(this); } componentWillMount() { @@ -67,6 +69,10 @@ class Navbar extends React.Component { Store.dispatch(dashboardChangeSection(sectionName)); } + _checkAC() { + return checkAC(this.props.ActiveCoin.coin); + } + logout() { Store.dispatch( stopInterval( diff --git a/react/src/components/dashboard/navbar/navbar.render.js b/react/src/components/dashboard/navbar/navbar.render.js index 48816d0..4449f5f 100644 --- a/react/src/components/dashboard/navbar/navbar.render.js +++ b/react/src/components/dashboard/navbar/navbar.render.js @@ -57,7 +57,7 @@ const NavbarRender = function() { <i className="site-menu-icon"></i> BarterDEX </a> </li> - { this.props.ActiveCoin && this.props.ActiveCoin.mode === 'native' && + { this.props.ActiveCoin && this.props.ActiveCoin.mode === 'native' && (this._checkAC() || this.props.ActiveCoin.coin === 'KMD') && <li className={ this.isSectionActive('jumblr') ? 'active nav-top-menu' : 'nav-top-menu' }> <a onClick={ () => this.dashboardChangeSection('jumblr') }> <i className="site-menu-icon"></i> Jumblr diff --git a/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.js b/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.js index 6e025e1..4630f4e 100644 --- a/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.js +++ b/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.js @@ -1,9 +1,16 @@ import React from 'react'; +import { toggleClaimInterestModal } from '../../../actions/actionCreators'; +import Store from '../../../store'; import WalletsNativeInfoRender from './walletsNativeInfo.render'; class WalletsNativeInfo extends React.Component { constructor(props) { super(props); + this.openClaimInterestModal = this.openClaimInterestModal.bind(this); + } + + openClaimInterestModal() { + Store.dispatch(toggleClaimInterestModal(true)); } render() { diff --git a/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.render.js b/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.render.js index 07bd784..19acf59 100644 --- a/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.render.js +++ b/react/src/components/dashboard/walletsNativeInfo/walletsNativeInfo.render.js @@ -1,5 +1,6 @@ import React from 'react'; import { translate } from '../../../translate/translate'; +import ClaimInterestModal from '../claimInterestModal/claimInterestModal'; const WalletsNativeInfoRender = function() { return ( @@ -40,6 +41,15 @@ const WalletsNativeInfoRender = function() { </table> </div> </div> + { this.props.ActiveCoin.coin === 'KMD' && + <div> + <button + type="button" + className="btn btn-success waves-effect waves-light margin-top-20 btn-next" + onClick={ () => this.openClaimInterestModal() }>Claim interest</button> + <ClaimInterestModal {...this.props} /> + </div> + } </div> <div className="col-xlg-6 col-md-8"> diff --git a/react/src/components/overrides.scss b/react/src/components/overrides.scss index c675967..ed599ec 100644 --- a/react/src/components/overrides.scss +++ b/react/src/components/overrides.scss @@ -836,4 +836,48 @@ select{ padding: 0; } } +} + +.modal-claim-interest { + .modal-dialog { + width: 70%; + + .table > tbody > tr > td, + .table > tbody > tr > th, + .table > tfoot > tr > td, + .table > tfoot > tr > th, + .table > thead > tr > td, + .table > thead > tr > th { + padding: 8px 30px 8px 0; + } + + .table-scroll { + height: 366px; + overflow-y: auto; + overflow-x: hidden; + width: 100%; + } + .bold { + font-weight: bold; + } + .green { + color: #66bb6a; + } + .red { + color: #f96868; + } + .locktime { + i { + font-size: 20px; + line-height: 1.1; + } + } + + .refresh-icon { + position: absolute; + right: 20px; + font-size: 20px; + z-index: 100; + } + } } \ No newline at end of file diff --git a/react/src/reducers/dashboard.js b/react/src/reducers/dashboard.js index 98df82e..54e2e63 100644 --- a/react/src/reducers/dashboard.js +++ b/react/src/reducers/dashboard.js @@ -9,14 +9,15 @@ import { VIEW_CACHE_DATA, LOG_GUI_HTTP, TOGGLE_NOTIFICATIONS_MODAL, - DISPLAY_COIND_DOWN_MODAL + DISPLAY_COIND_DOWN_MODAL, + DISPLAY_CLAIM_INTEREST_MODAL } from '../actions/storeType'; const HTTP_STACK_MAX_ENTRIES = 150; // limit stack mem length to N records per type const trimHTTPLogs = (logObject) => { const logObjectArray = Object.keys(logObject); - + if (logObjectArray.length - HTTP_STACK_MAX_ENTRIES === 1) { delete logObject[logObjectArray.shift()]; } @@ -38,6 +39,7 @@ export function Dashboard(state = { }, guiLog: {}, displayCoindDownModal: false, + displayClaimInterestModal: false, }, action) { switch (action.type) { case DASHBOARD_SECTION_CHANGE: @@ -95,7 +97,7 @@ export function Dashboard(state = { const logItem = { [actionTS]: action.log }; newLogState = trimHTTPLogs(Object.assign({}, logState, logItem)); } - + return Object.assign({}, state, { guiLog: newLogState, }); @@ -104,6 +106,11 @@ export function Dashboard(state = { displayCoindDownModal: action.displayCoindDownModal, }); break; + case DISPLAY_CLAIM_INTEREST_MODAL: + return Object.assign({}, state, { + displayClaimInterestModal: action.displayClaimInterestModal, + }); + break; default: return state; }