You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
621 lines
17 KiB
621 lines
17 KiB
import React from 'react';
|
|
import { connect } from 'react-redux';
|
|
import Config from '../../../config';
|
|
import { translate } from '../../../translate/translate';
|
|
import { secondsToString } from '../../../util/time';
|
|
import {
|
|
triggerToaster,
|
|
sendNativeTx,
|
|
getKMDOPID,
|
|
clearLastSendToResponseState,
|
|
shepherdElectrumSend,
|
|
shepherdElectrumSendPreflight,
|
|
copyString,
|
|
} from '../../../actions/actionCreators';
|
|
import Store from '../../../store';
|
|
import {
|
|
AddressListRender,
|
|
SendRender,
|
|
SendFormRender,
|
|
_SendFormRender
|
|
} from './sendCoin.render';
|
|
import { isPositiveNumber } from '../../../util/number';
|
|
|
|
// TODO: - add links to explorers
|
|
// - render z address trim
|
|
|
|
class SendCoin extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
currentStep: 0,
|
|
addressType: null,
|
|
sendFrom: null,
|
|
sendFromAmount: 0,
|
|
sendTo: '',
|
|
amount: 0,
|
|
fee: 0,
|
|
addressSelectorOpen: false,
|
|
renderAddressDropdown: true,
|
|
subtractFee: false,
|
|
lastSendToResponse: null,
|
|
coin: null,
|
|
spvVerificationWarning: false,
|
|
spvPreflightSendInProgress: false,
|
|
};
|
|
this.updateInput = this.updateInput.bind(this);
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
this.openDropMenu = this.openDropMenu.bind(this);
|
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
|
this.checkZAddressCount = this.checkZAddressCount.bind(this);
|
|
this.setRecieverFromScan = this.setRecieverFromScan.bind(this);
|
|
this.renderOPIDListCheck = this.renderOPIDListCheck.bind(this);
|
|
this.SendFormRender = _SendFormRender.bind(this);
|
|
this.isTransparentTx = this.isTransparentTx.bind(this);
|
|
this.toggleSubtractFee = this.toggleSubtractFee.bind(this);
|
|
this.isFullySynced = this.isFullySynced.bind(this);
|
|
}
|
|
|
|
copyTXID(txid) {
|
|
Store.dispatch(copyString(txid, 'TXID copied to clipboard'));
|
|
}
|
|
|
|
openExplorerWindow(txid) {
|
|
const url = `http://${this.props.ActiveCoin.coin}.explorer.supernet.org/tx/${txid}`;
|
|
const remote = window.require('electron').remote;
|
|
const BrowserWindow = remote.BrowserWindow;
|
|
|
|
const externalWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 800,
|
|
title: `${translate('INDEX.LOADING')}...`,
|
|
icon: remote.getCurrentWindow().iguanaIcon,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
|
|
externalWindow.loadURL(url);
|
|
externalWindow.webContents.on('did-finish-load', () => {
|
|
setTimeout(() => {
|
|
externalWindow.show();
|
|
}, 40);
|
|
});
|
|
}
|
|
|
|
SendFormRender() {
|
|
return _SendFormRender.call(this);
|
|
}
|
|
|
|
toggleSubtractFee() {
|
|
this.setState({
|
|
subtractFee: !this.state.subtractFee,
|
|
});
|
|
}
|
|
|
|
componentWillMount() {
|
|
document.addEventListener(
|
|
'click',
|
|
this.handleClickOutside,
|
|
false
|
|
);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener(
|
|
'click',
|
|
this.handleClickOutside,
|
|
false
|
|
);
|
|
}
|
|
|
|
componentWillReceiveProps(props) {
|
|
this.checkZAddressCount(props);
|
|
}
|
|
|
|
setRecieverFromScan(receiver) {
|
|
try {
|
|
const recObj = JSON.parse(receiver);
|
|
|
|
if (recObj &&
|
|
typeof recObj === 'object') {
|
|
if (recObj.coin === this.props.ActiveCoin.coin) {
|
|
if (recObj.amount) {
|
|
this.setState({
|
|
amount: recObj.amount,
|
|
});
|
|
}
|
|
if (recObj.address) {
|
|
this.setState({
|
|
sendTo: recObj.address,
|
|
});
|
|
}
|
|
} else {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
translate('SEND.QR_COIN_MISMATCH_MESSAGE_IMPORT_COIN') +
|
|
recObj.coin +
|
|
translate('SEND.QR_COIN_MISMATCH_MESSAGE_ACTIVE_COIN') +
|
|
this.props.ActiveCoin.coin +
|
|
translate('SEND.QR_COIN_MISMATCH_MESSAGE_END'),
|
|
translate('SEND.QR_COIN_MISMATCH_TITLE'),
|
|
'warning'
|
|
)
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.setState({
|
|
sendTo: receiver,
|
|
});
|
|
}
|
|
|
|
document.getElementById('kmdWalletSendTo').focus();
|
|
}
|
|
|
|
handleClickOutside(e) {
|
|
if (e.srcElement.className !== 'btn dropdown-toggle btn-info' &&
|
|
(e.srcElement.offsetParent && e.srcElement.offsetParent.className !== 'btn dropdown-toggle btn-info') &&
|
|
(e.path && e.path[4] && e.path[4].className.indexOf('showkmdwalletaddrs') === -1)) {
|
|
this.setState({
|
|
addressSelectorOpen: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
checkZAddressCount(props) {
|
|
const _addresses = this.props.ActiveCoin.addresses;
|
|
const _defaultState = {
|
|
currentStep: 0,
|
|
addressType: null,
|
|
sendFrom: null,
|
|
sendFromAmount: 0,
|
|
sendTo: '',
|
|
amount: 0,
|
|
fee: 0,
|
|
addressSelectorOpen: false,
|
|
renderAddressDropdown: true,
|
|
subtractFee: false,
|
|
lastSendToResponse: null,
|
|
};
|
|
let updatedState;
|
|
|
|
if (_addresses &&
|
|
(!_addresses.private ||
|
|
_addresses.private.length === 0)) {
|
|
updatedState = {
|
|
renderAddressDropdown: false,
|
|
lastSendToResponse: props.ActiveCoin.lastSendToResponse,
|
|
coin: props.ActiveCoin.coin,
|
|
};
|
|
} else {
|
|
updatedState = {
|
|
renderAddressDropdown: true,
|
|
lastSendToResponse: props.ActiveCoin.lastSendToResponse,
|
|
coin: props.ActiveCoin.coin,
|
|
};
|
|
}
|
|
|
|
if (this.state.coin !== props.ActiveCoin.coin) {
|
|
this.setState(Object.assign({}, _defaultState, updatedState));
|
|
} else {
|
|
this.setState(updatedState);
|
|
}
|
|
}
|
|
|
|
renderAddressByType(type) {
|
|
let _items = [];
|
|
|
|
if (this.props.ActiveCoin.addresses &&
|
|
this.props.ActiveCoin.addresses[type] &&
|
|
this.props.ActiveCoin.addresses[type].length) {
|
|
this.props.ActiveCoin.addresses[type].map((address) => {
|
|
if (address.amount > 0) {
|
|
_items.push(
|
|
<li
|
|
className="selected"
|
|
key={ address.address }>
|
|
<a onClick={ () => this.updateAddressSelection(address.address, type, address.amount) }>
|
|
<i className={ 'icon fa-eye' + (type === 'public' ? '' : '-slash') }></i>
|
|
<span className="text">
|
|
[ { address.amount } { this.props.ActiveCoin.coin } ]
|
|
{ type === 'public' ? address.address : address.address.substring(0, 34) + '...' }
|
|
</span>
|
|
<span
|
|
className="glyphicon glyphicon-ok check-mark pull-right"
|
|
style={{ display: this.state.sendFrom === address.address ? 'inline-block' : 'none' }}></span>
|
|
</a>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
return _items;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
renderOPIDListCheck() {
|
|
if (this.state.renderAddressDropdown &&
|
|
this.props.ActiveCoin.opids &&
|
|
this.props.ActiveCoin.opids.length) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
renderSelectorCurrentLabel() {
|
|
if (this.state.sendFrom) {
|
|
return (
|
|
<span>
|
|
<i className={ 'icon fa-eye' + this.state.addressType === 'public' ? '' : '-slash' }></i>
|
|
<span className="text">
|
|
[ { this.state.sendFromAmount } { this.props.ActiveCoin.coin } ]
|
|
{ this.state.addressType === 'public' ? this.state.sendFrom : this.state.sendFrom.substring(0, 34) + '...' }
|
|
</span>
|
|
</span>
|
|
);
|
|
} else {
|
|
return (
|
|
<span>{ this.props.ActiveCoin.mode === 'spv' ? `[ ${this.props.ActiveCoin.balance.balance} ${this.props.ActiveCoin.coin} ] ${this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub}` : translate('INDEX.T_FUNDS') }</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
renderAddressList() {
|
|
return AddressListRender.call(this);
|
|
}
|
|
|
|
renderOPIDLabel(opid) {
|
|
const _satatusDef = {
|
|
queued: {
|
|
icon: 'warning',
|
|
label: 'QUEUED',
|
|
},
|
|
executing: {
|
|
icon: 'info',
|
|
label: 'EXECUTING',
|
|
},
|
|
failed: {
|
|
icon: 'danger',
|
|
label: 'FAILED',
|
|
},
|
|
success: {
|
|
icon: 'success',
|
|
label: 'SUCCESS',
|
|
},
|
|
};
|
|
|
|
return (
|
|
<span className={ `label label-${_satatusDef[opid.status].icon}` }>
|
|
<i className="icon fa-eye"></i>
|
|
<span>{ translate(`KMD_NATIVE.${_satatusDef[opid.status].label}`) }</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
renderOPIDResult(opid) {
|
|
let isWaitingStatus = true;
|
|
|
|
if (opid.status === 'queued') {
|
|
isWaitingStatus = false;
|
|
return (
|
|
<i>{ translate('SEND.AWAITING') }...</i>
|
|
);
|
|
} else if (opid.status === 'executing') {
|
|
isWaitingStatus = false;
|
|
return (
|
|
<i>{ translate('SEND.PROCESSING') }...</i>
|
|
);
|
|
} else if (opid.status === 'failed') {
|
|
isWaitingStatus = false;
|
|
return (
|
|
<span>
|
|
<strong>{ translate('SEND.ERROR_CODE') }:</strong> <span>{ opid.error.code }</span>
|
|
<br />
|
|
<strong>{ translate('KMD_NATIVE.MESSAGE') }:</strong> <span>{ opid.error.message }</span>
|
|
</span>
|
|
);
|
|
} else if (opid.status === 'success') {
|
|
isWaitingStatus = false;
|
|
return (
|
|
<span>
|
|
<strong>{ translate('KMD_NATIVE.TXID') }:</strong> <span>{ opid.result.txid }</span>
|
|
<br />
|
|
<strong>{ translate('KMD_NATIVE.EXECUTION_SECONDS') }:</strong> <span>{ opid.execution_secs }</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (isWaitingStatus) {
|
|
return (
|
|
<span>{ translate('SEND.WAITING') }...</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
renderOPIDList() {
|
|
if (this.props.ActiveCoin.opids &&
|
|
this.props.ActiveCoin.opids.length) {
|
|
return this.props.ActiveCoin.opids.map((opid) =>
|
|
<tr key={ opid.id }>
|
|
<td>{ this.renderOPIDLabel(opid) }</td>
|
|
<td>{ opid.id }</td>
|
|
<td>{ secondsToString(opid.creation_time) }</td>
|
|
<td>{ this.renderOPIDResult(opid) }</td>
|
|
</tr>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
openDropMenu() {
|
|
this.setState(Object.assign({}, this.state, {
|
|
addressSelectorOpen: !this.state.addressSelectorOpen,
|
|
}));
|
|
}
|
|
|
|
updateAddressSelection(address, type, amount) {
|
|
this.setState(Object.assign({}, this.state, {
|
|
sendFrom: address,
|
|
addressType: type,
|
|
sendFromAmount: amount,
|
|
addressSelectorOpen: !this.state.addressSelectorOpen,
|
|
}));
|
|
}
|
|
|
|
updateInput(e) {
|
|
this.setState({
|
|
[e.target.name]: e.target.value,
|
|
});
|
|
}
|
|
|
|
changeSendCoinStep(step, back) {
|
|
if (step === 0) {
|
|
if (back) {
|
|
this.setState({
|
|
currentStep: 0,
|
|
spvVerificationWarning: false,
|
|
spvPreflightSendInProgress: false,
|
|
});
|
|
} 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (step === 1) {
|
|
if (!this.validateSendFormData()) {
|
|
return;
|
|
} else {
|
|
this.setState(Object.assign({}, this.state, {
|
|
spvPreflightSendInProgress: this.props.ActiveCoin.mode === 'spv' ? true : false,
|
|
currentStep: step,
|
|
}));
|
|
|
|
// spv pre tx push request
|
|
if (this.props.ActiveCoin.mode === 'spv') {
|
|
shepherdElectrumSendPreflight(
|
|
this.props.ActiveCoin.coin,
|
|
this.state.amount * 100000000,
|
|
this.state.sendTo,
|
|
this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub
|
|
).then((sendPreflight) => {
|
|
if (sendPreflight &&
|
|
sendPreflight.msg === 'success') {
|
|
this.setState(Object.assign({}, this.state, {
|
|
spvVerificationWarning: !sendPreflight.result.utxoVerified,
|
|
spvPreflightSendInProgress: false,
|
|
}));
|
|
} else {
|
|
this.setState(Object.assign({}, this.state, {
|
|
spvPreflightSendInProgress: false,
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (step === 2) {
|
|
this.setState(Object.assign({}, this.state, {
|
|
currentStep: step,
|
|
}));
|
|
this.handleSubmit();
|
|
}
|
|
}
|
|
|
|
handleSubmit() {
|
|
if (!this.validateSendFormData()) {
|
|
return;
|
|
}
|
|
|
|
if (this.props.ActiveCoin.mode === 'native') {
|
|
Store.dispatch(
|
|
sendNativeTx(
|
|
this.props.ActiveCoin.coin,
|
|
this.state
|
|
)
|
|
);
|
|
|
|
if (this.state.addressType === 'private') {
|
|
setTimeout(() => {
|
|
Store.dispatch(
|
|
getKMDOPID(
|
|
null,
|
|
this.props.ActiveCoin.coin
|
|
)
|
|
);
|
|
}, 1000);
|
|
}
|
|
} else if (this.props.ActiveCoin.mode === 'spv') {
|
|
// no op
|
|
if (this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub) {
|
|
Store.dispatch(
|
|
shepherdElectrumSend(
|
|
this.props.ActiveCoin.coin,
|
|
this.state.amount * 100000000,
|
|
this.state.sendTo,
|
|
this.props.Dashboard.electrumCoins[this.props.ActiveCoin.coin].pub
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: reduce to a single toast
|
|
validateSendFormData() {
|
|
let valid = true;
|
|
|
|
if (this.props.ActiveCoin.mode === 'spv') {
|
|
const _amount = this.state.amount;
|
|
const _amountSats = this.state.amount * 100000000;
|
|
const _balanceSats = this.props.ActiveCoin.balance.balanceSats;
|
|
|
|
if (Number(_amountSats) + 10000 > _balanceSats) {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
`${translate('SEND.INSUFFICIENT_FUNDS')} max available balance is ${(0.00000001 * (_balanceSats - 10000)).toFixed(8)} ${this.props.ActiveCoin.coin}`,
|
|
translate('TOASTR.WALLET_NOTIFICATION'),
|
|
'error'
|
|
)
|
|
);
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
if (!this.state.sendTo ||
|
|
this.state.sendTo.length < 34) {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
translate('SEND.SEND_TO_ADDRESS_MIN_LENGTH'),
|
|
translate('TOASTR.WALLET_NOTIFICATION'),
|
|
'error'
|
|
)
|
|
);
|
|
valid = false;
|
|
}
|
|
|
|
if (!isPositiveNumber(this.state.amount)) {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
translate('SEND.AMOUNT_POSITIVE_NUMBER'),
|
|
translate('TOASTR.WALLET_NOTIFICATION'),
|
|
'error'
|
|
)
|
|
);
|
|
valid = false;
|
|
}
|
|
|
|
if (((!this.state.sendFrom || this.state.addressType === 'public') &&
|
|
this.state.sendTo &&
|
|
this.state.sendTo.length === 34 &&
|
|
this.props.ActiveCoin.balance &&
|
|
this.props.ActiveCoin.balance.transparent &&
|
|
Number(Number(this.state.amount) + 0.0001) > Number(this.props.ActiveCoin.balance.transparent)) ||
|
|
(this.state.addressType === 'public' &&
|
|
this.state.sendTo &&
|
|
this.state.sendTo.length > 34 &&
|
|
Number(Number(this.state.amount) + 0.0001) > Number(this.state.sendFromAmount)) ||
|
|
(this.state.addressType === 'private' &&
|
|
this.state.sendTo &&
|
|
this.state.sendTo.length >= 34 &&
|
|
Number(Number(this.state.amount) + 0.0001) > Number(this.state.sendFromAmount))) {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
`${translate('SEND.INSUFFICIENT_FUNDS')} max available balance is ${Number(this.state.sendFromAmount || this.props.ActiveCoin.balance.transparent)} ${this.props.ActiveCoin.coin}`,
|
|
translate('TOASTR.WALLET_NOTIFICATION'),
|
|
'error'
|
|
)
|
|
);
|
|
valid = false;
|
|
}
|
|
|
|
if (this.state.sendTo.length > 34 &&
|
|
(!this.state.sendFrom || this.state.sendFrom.length < 34)) {
|
|
Store.dispatch(
|
|
triggerToaster(
|
|
translate('SEND.SELECT_SOURCE_ADDRESS'),
|
|
translate('TOASTR.WALLET_NOTIFICATION'),
|
|
'error'
|
|
)
|
|
);
|
|
valid = false;
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
isTransparentTx() {
|
|
if (((this.state.sendFrom && this.state.sendFrom.length === 34) || !this.state.sendFrom) &&
|
|
(this.state.sendTo && this.state.sendTo.length === 34)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
isFullySynced() {
|
|
if (this.props.ActiveCoin.progress &&
|
|
this.props.ActiveCoin.progress.longestchain &&
|
|
this.props.ActiveCoin.progress.blocks &&
|
|
this.props.ActiveCoin.progress.longestchain > 0 &&
|
|
this.props.ActiveCoin.progress.blocks > 0 &&
|
|
Number(this.props.ActiveCoin.progress.blocks) * 100 / Number(this.props.ActiveCoin.progress.longestchain) === 100) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (this.props &&
|
|
this.props.ActiveCoin &&
|
|
(this.props.ActiveCoin.activeSection === 'send' || this.props.activeSection === 'send')) {
|
|
return SendRender.call(this);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const mapStateToProps = (state, props) => {
|
|
let _mappedProps = {
|
|
ActiveCoin: {
|
|
addresses: state.ActiveCoin.addresses,
|
|
coin: state.ActiveCoin.coin,
|
|
mode: state.ActiveCoin.mode,
|
|
opids: state.ActiveCoin.opids,
|
|
balance: state.ActiveCoin.balance,
|
|
activeSection: state.ActiveCoin.activeSection,
|
|
lastSendToResponse: state.ActiveCoin.lastSendToResponse,
|
|
progress: state.ActiveCoin.progress,
|
|
},
|
|
Dashboard: state.Dashboard,
|
|
};
|
|
|
|
if (props &&
|
|
props.activeSection &&
|
|
props.renderFormOnly) {
|
|
_mappedProps.ActiveCoin.activeSection = props.activeSection;
|
|
_mappedProps.renderFormOnly = props.renderFormOnly;
|
|
}
|
|
|
|
return _mappedProps;
|
|
};
|
|
|
|
export default connect(mapStateToProps)(SendCoin);
|
|
|