diff --git a/app/assets/locales/locale-en.json b/app/assets/locales/locale-en.json index ce6916dcb2..4934402d3c 100644 --- a/app/assets/locales/locale-en.json +++ b/app/assets/locales/locale-en.json @@ -2461,7 +2461,9 @@ "minimum_amount": "Minimum withdraw amount: %(amount)s %(symbol)s", "noFeeBalance": "Your balance is insufficient to pay fees using this asset, please choose another asset to pay your fees with", "noFunds": "No funds", + "unknown": "Unknown error", "noPoolBalance": "That asset has an insufficient fee pool balance to pay the fees with. Please inform the asset owner or select another asset for paying fees.", + "noPoolBalanceShort": "Fee pool empty", "pos": "Amount must be positive", "precision": "Minimum withdraw precision value: %(precision)s", "req": "Required field", @@ -2482,6 +2484,7 @@ "see": "SEE MY TRANSFERS", "send": "Send", "total": "Total: ", + "to": "To", "warn_name_unable_read_memo": "Proposed sender will be unable to read this memo!" }, "trx_error": { diff --git a/app/components/Account/AccountSelector.jsx b/app/components/Account/AccountSelector.jsx index e11fc9b189..002f5b92fb 100644 --- a/app/components/Account/AccountSelector.jsx +++ b/app/components/Account/AccountSelector.jsx @@ -5,7 +5,13 @@ import AccountImage from "../Account/AccountImage"; import AccountStore from "stores/AccountStore"; import AccountActions from "actions/AccountActions"; import Translate from "react-translate-component"; -import {ChainStore, PublicKey, ChainValidation, FetchChain} from "bitsharesjs"; +import { + ChainStore, + PublicKey, + ChainValidation, + FetchChain, + FetchChainObjects +} from "bitsharesjs"; import ChainTypes from "../Utility/ChainTypes"; import BindToChainState from "../Utility/BindToChainState"; import counterpart from "counterpart"; @@ -22,6 +28,7 @@ import { Form } from "bitshares-ui-style-guide"; +const MAX_LOOKUP_ATTEMPTS = 5; /* * @brief Allows the user to enter an account by name or #ID * @@ -48,7 +55,8 @@ class AccountSelector extends React.Component { focus: PropTypes.bool, disabled: PropTypes.bool, editable: PropTypes.bool, - locked: PropTypes.bool + locked: PropTypes.bool, + requireActiveSelect: PropTypes.bool }; static defaultProps = { @@ -56,22 +64,34 @@ class AccountSelector extends React.Component { excludeAccounts: [], disabled: null, editable: null, - locked: false + locked: false, + requireActiveSelect: true // Should not be set to false, required for fallback }; constructor(props) { super(props); this.state = { - inputChanged: false, + accountIndex: [], locked: null }; + this.timer = null; } componentDidMount() { let {account, accountName} = this.props; - if (typeof account === "undefined") - account = ChainStore.getAccount(accountName); + if (accountName) { + this._addToIndex(accountName, true); + } + + // Populate account search array + this.props.myActiveAccounts.map(a => { + this._addToIndex(a, true); + }); + + this.props.contacts.map(a => { + this._addToIndex(a, true); + }); if (this.props.onAccountChanged && account) this.props.onAccountChanged(account); @@ -80,29 +100,198 @@ class AccountSelector extends React.Component { this.onInputChanged(accountName); } - componentDidUpdate() { + componentDidUpdate(prevProps) { if (this.props.focus && !!this.props.editable && !this.props.disabled) { this.refs.user_input.focus(); } - } - componentWillReceiveProps(np) { - if (np.account && np.account !== this.props.account) { + if (prevProps.account && prevProps.account !== this.props.account) { if (this.props.onAccountChanged) { - this.props.onAccountChanged(np.account); + this.props.onAccountChanged(this.props.account); } } } + _addToIndex(accountName, noDelay = false) { + if (noDelay) { + this._addThisToIndex(accountName); + this._fetchAccounts(); + } else { + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this._addToIndex(accountName, true); + }, 500); + } + } + + _addThisToIndex(accountName) { + let {accountIndex} = this.state; + + if (!accountName) return; + + let inAccountList = accountIndex.find(a => a.name === accountName); + + if (accountName && !inAccountList) { + accountIndex.push({ + name: accountName, + data: null, + attempts: 0 + }); + } + } + + _getIndex(name, index) { + return index.findIndex(a => a.name === name); + } + + _getSearchArray() { + let {accountIndex} = this.state; + + // For all objects in search_array, query with FetchChainObjects + // Update results for each object with returned data and remove from search_array + // Update search_array for all remaining objects with increased attempts count + // which is when account does not exists, but can also be if node failed to send results + // back in time, so we query at least `MAX_LOOKUP_ATTEMPTS` times before we stop + + // Filter out what objects we still require data for + let search_array = accountIndex + .filter(search => { + return !search.data && search.attempts < MAX_LOOKUP_ATTEMPTS + ? search.name + : null; + }) + .map(search => { + return search.name; + }); + + return search_array; + } + + _fetchAccounts() { + let {accountIndex} = this.state; + + let search_array = this._getSearchArray(); + + if (search_array.length > 0) { + if (__DEV__) + console.log("Looked for " + search_array.length + " accounts"); + FetchChainObjects( + ChainStore.getAccount, + search_array, + 3000, + {} + ).then(accounts => { + accounts.forEach(account => { + if (account) { + let objectIndex = this._getIndex( + account.get("name"), + accountIndex + ); + + let result = this._populateAccountIndex(account); + + if (result) { + accountIndex[objectIndex] = result; + search_array.splice(account.get("name")); + } + } + }); + + search_array.forEach(account_to_find => { + let objectIndex = this._getIndex( + account_to_find, + accountIndex + ); + accountIndex[objectIndex].attempts++; + }); + this.setState({ + accountIndex: accountIndex + }); + + // Run another fetch of accounts if data is still missing + let isDataMissing = this.state.accountIndex.find( + a => !a.data && a.attempts < MAX_LOOKUP_ATTEMPTS + ); + + if (isDataMissing) { + setTimeout(() => { + this._fetchAccounts(); + }, 500); + } + }); + } + } + + _populateAccountIndex(accountResult) { + let {myActiveAccounts, contacts} = this.props; + + // Should not happen, just failsafe + if (!accountResult) return null; + + let accountName = accountResult.get("name"); + let accountStatus = ChainStore.getAccountMemberStatus(accountResult); + let accountType = this.getInputType(accountName); + + let statusLabel = !accountUtils.isKnownScammer(accountName) + ? counterpart.translate("account.member." + accountStatus) + : counterpart.translate("account.member.suspected_scammer"); + + let rightLabel = + accountType === "name" + ? "#" + accountResult.get("id").substring(4) + : accountType === "id" + ? accountResult.get("name") + : accountType == "pubkey" && this.props.allowPubKey + ? "Public Key" + : null; + + return { + name: accountName, + attempts: 0, + data: { + id: accountResult.get("id"), + name: accountName, + type: accountType, + status: accountStatus, + isOwnAccount: myActiveAccounts.has(accountName), + isContact: contacts.has(accountName), + isKnownScammer: accountUtils.isKnownScammer(accountName), + statusLabel: statusLabel, + rightLabel: rightLabel, + className: + accountUtils.isKnownScammer(accountName) || !accountResult + ? "negative" + : null + } + }; + } + // can be used in parent component: this.refs.account_selector.getAccount() getAccount() { return this.props.account; } getError() { - let {account, error} = this.props; + let {account, accountName, error, typeahead} = this.props; + + let inputType = accountName ? this.getInputType(accountName) : null; - if (!error && account && !this.getInputType(account.get("name"))) + if (!typeahead) { + if (!account && accountName && inputType !== "pubkey") { + error = counterpart.translate("account.errors.unknown"); + } + } else { + // Typeahead can't select an unknown account! + // if ( + // !(allowPubKey && inputType === "pubkey") && + // !error && + // accountName && + // !account + // ) + // error = counterpart.translate("account.errors.unknown"); + } + + if (!error && account && !inputType) error = counterpart.translate("account.errors.invalid"); return error; @@ -140,9 +329,23 @@ class AccountSelector extends React.Component { return value; } - _notifyOnChange(selectedAccountName) { + _notifyOnChange(selectedAccountName, inputType) { let {props} = this; + // Clear selected account when we have new input data if we require an active select + if ( + inputType == "input" && + this.props.typeahead && + this.props.requireActiveSelect + ) { + if (!!props.onAccountChanged) { + props.onAccountChanged(null); + } + if (!!props.onChange) { + props.onChange(null); + } + } + let accountName = this.getVerifiedAccountName(selectedAccountName); // Synchronous onChange for input change @@ -156,7 +359,12 @@ class AccountSelector extends React.Component { [accountName]: false }) .then(account => { - if (!!account) { + if ( + !!account && + ((this.props.requireActiveSelect && + inputType == "select") || + !this.props.requireActiveSelect) + ) { props.onAccountChanged(account); } }) @@ -167,15 +375,12 @@ class AccountSelector extends React.Component { } onSelect(selectedAccountName) { - this._notifyOnChange(selectedAccountName); + this._notifyOnChange(selectedAccountName, "select"); } onInputChanged(e) { - this.setState({ - inputChanged: true - }); - - this._notifyOnChange(e); + this._addToIndex(this.getVerifiedAccountName(e)); + this._notifyOnChange(e, "input"); } onKeyDown(e) { @@ -203,154 +408,56 @@ class AccountSelector extends React.Component { } render() { - let { - accountName, - account, - allowPubKey, - typeahead, - disableActionButton, - contacts, - myActiveAccounts, - noPlaceHolder, - useHR, - labelClass, - reserveErrorSpace - } = this.props; - - const inputType = this.getInputType(accountName); - - let typeAheadAccounts = []; - let error = this.getError(); - let linkedAccounts = myActiveAccounts; - linkedAccounts = linkedAccounts.concat(contacts); + let {accountIndex} = this.state; - // Selected Account - let displayText; - if (account) { - account.isKnownScammer = accountUtils.isKnownScammer( - account.get("name") - ); - account.accountType = this.getInputType(account.get("name")); - account.accountStatus = ChainStore.getAccountMemberStatus(account); - account.statusText = !account.isKnownScammer - ? counterpart.translate( - "account.member." + account.accountStatus - ) - : counterpart.translate("account.member.suspected_scammer"); - displayText = - account.accountType === "name" - ? "#" + account.get("id").substring(4) - : account.accountType === "id" - ? account.get("name") - : null; - } + let {account, accountName, disableActionButton} = this.props; - // Without Typeahead Error Handling - if (!typeahead) { - if (!account && accountName && inputType !== "pubkey") { - error = counterpart.translate("account.errors.unknown"); - } - } else { - if ( - !(allowPubKey && inputType === "pubkey") && - !error && - accountName && - !account - ) - error = counterpart.translate("account.errors.unknown"); - } - if (allowPubKey && inputType === "pubkey") displayText = "Public Key"; + let searchInProgress = this.state.accountIndex.find( + a => !a.data && a.attempts < MAX_LOOKUP_ATTEMPTS + ); - if (account && linkedAccounts) - account.isFavorite = - myActiveAccounts.has(account.get("name")) || - contacts.has(account.get("name")); + const lockedState = + this.state.locked !== null ? this.state.locked : this.props.locked; - if (typeahead && linkedAccounts) { - linkedAccounts - .map(accountName => { - if (this.props.excludeAccounts.indexOf(accountName) !== -1) - return null; - let account = ChainStore.getAccount(accountName); - if (account) { - let account_status = ChainStore.getAccountMemberStatus( - account - ); - let account_status_text = !accountUtils.isKnownScammer( - accountName - ) - ? "account.member." + account_status - : "account.member.suspected_scammer"; - - typeAheadAccounts.push({ - id: accountName, - label: accountName, - status: counterpart.translate(account_status_text), - isOwn: myActiveAccounts.has(accountName), - isFavorite: contacts.has(accountName), - isKnownScammer: accountUtils.isKnownScammer( - accountName - ), - className: accountUtils.isKnownScammer(accountName) - ? "negative" - : "positive" - }); - } - return null; - }) - .filter(a => !!a); - } + let error = this.getError(), + formContainer, + selectedAccount, + disabledAction, + disabledInput, + editableInput, + linked_status; - let typeaheadHasAccount = !!accountName - ? typeAheadAccounts.reduce((boolean, a) => { - return boolean || a.label === accountName; - }, false) - : false; - - if (!!accountName && !typeaheadHasAccount && this.state.inputChanged) { - let _account = ChainStore.getAccount(accountName); - if (_account) { - let _account_status = ChainStore.getAccountMemberStatus( - _account - ); - let _account_status_text = _account - ? !accountUtils.isKnownScammer(_account.get("name")) - ? counterpart.translate( - "account.member." + _account_status - ) - : counterpart.translate( - "account.member.suspected_scammer" - ) - : counterpart.translate("account.errors.unknown"); - - typeAheadAccounts.push({ - id: this.props.accountName, - label: this.props.accountName, - status: _account_status_text, - isOwn: myActiveAccounts.has(accountName), - isFavorite: contacts.has(accountName), - isKnownScammer: accountUtils.isKnownScammer(accountName), - className: - accountUtils.isKnownScammer(accountName) || !_account - ? "negative" - : null, - disabled: !_account ? true : false - }); - } - } + editableInput = !!lockedState + ? false + : this.props.editable != null + ? this.props.editable + : undefined; - typeAheadAccounts.sort((a, b) => { - if (a.disabled && !b.disabled) { - if (a.label > b.label) return 1; - else return -1; - } else return -1; - }); + disabledInput = !!lockedState + ? true + : this.props.disabled != null + ? this.props.disabled + : undefined; + + // Selected Account + if (account) { + let objectIndex = this._getIndex(account.get("name"), accountIndex); + + selectedAccount = + accountIndex && accountIndex[objectIndex] + ? accountIndex[objectIndex].data + : null; + } - let linked_status; + disabledAction = + !( + account || + (selectedAccount && selectedAccount.type === "pubkey") + ) || + error || + disableActionButton; - if (!this.props.account) { - linked_status = null; - } else if (myActiveAccounts.has(account.get("name"))) { + if (selectedAccount && selectedAccount.isOwnAccount) { linked_status = ( ); - } else if (accountUtils.isKnownScammer(account.get("name"))) { + } else if (selectedAccount && selectedAccount.isKnownScammer) { linked_status = ( ); - } else if (contacts.has(account.get("name"))) { + } else if (selectedAccount && selectedAccount.isContact) { linked_status = ( ); - } else { + } else if (selectedAccount) { linked_status = ( { + // Filter accounts based on + // - Exclude without results (missing chain data at the moment) + // - Excluded accounts (by props) + // - Include users own accounts (isOwnAccount) + // - Include users contacts (isContact) unless it's a previously locked input + // - Include current input + + if (!account.data) { + return null; + } + if (this.props.excludeAccounts.indexOf(account.id) !== -1) { + return null; + } + if ( + account.data.isOwnAccount || + (!this.props.locked && account.data.isContact) || + (accountName && account.data.name === accountName) + ) { + return account; + } + }) + .sort((a, b) => { + if (a.data.isOwnAccount < b.data.isOwnAccount) { + if (a.data.name > b.data.name) { + return 1; + } else { + return -1; + } + } else { + return -1; + } + }) + .map(account => { + return ( + + {account.data.isOwnAccount ? ( + + ) : null} + {account.data.isContact ? ( + + ) : null} + {account.data.isKnownScammer ? ( + + ) : null} +   + {account.data.name} + + {account.data.statusLabel} + + + ); + }); - const lockedState = - this.state.locked !== null ? this.state.locked : this.props.locked; + formContainer = ( + + ); + } else { + formContainer = ( + + ); + } - let editableInput = !!lockedState - ? false - : this.props.editable != null - ? this.props.editable - : undefined; - let disabledInput = !!lockedState - ? true - : this.props.disabled != null - ? this.props.disabled - : undefined; + let accountImageContainer = this.props + .hideImage ? null : selectedAccount && + selectedAccount.accountType === "pubkey" ? ( +
+ +
+ ) : ( + + ); + + let lockedStateContainer = !lockedState ? null : ( + +
this.setState({locked: false})} + > + +
+
+ ); + + let rightLabelContainer = + !this.props.label || !selectedAccount ? null : ( +
+ +
+ ); return ( - {this.props.label ? ( -
- -
- ) : null} - {useHR &&
} + {rightLabelContainer} + {this.props.useHR &&
}
- {account && account.accountType === "pubkey" ? ( -
- -
- ) : this.props.hideImage ? null : ( - - )} - {typeof this.props.typeahead !== "undefined" ? ( - - ) : ( - - )} - {!!lockedState && ( - -
- this.setState({locked: false}) - } - > - -
-
- )} + {accountImageContainer} + {formContainer} + {searchInProgress ? ( + + ) : null} + {lockedStateContainer} {this.props.children} {this.props.onAction ? ( @@ -679,7 +676,7 @@ class SendModal extends React.Component { { const balanceObject = ChainStore.getObject(balances[asset_id]); result[asset_id] = balanceObject.get("balance"); @@ -37,22 +31,27 @@ class SetDefaultFeeAssetModal extends React.Component { }); } - componentWillReceiveProps(np) { - let account = np.account; - if (!account) { - account = ChainStore.getAccount(np.currentAccount); - } + componentDidMount() { + this._updateStateForAccount(); + } - if (account) { - if ( - Object.keys(this.state.balances).length === 0 || - account.get("name") !== this.props.currentAccount || - (np.current_asset && - this.state.selectedAssetId !== np.current_asset) - ) { - this._updateStateForAccount(account, np.current_asset); + componentDidUpdate(prevProps) { + const accountChanged = + this.props.account && + prevProps.account.get("id") !== this.props.account.get("id"); + if (accountChanged) { + if (Object.keys(this.state.balances).length === 0) { + this._updateStateForAccount(); } } + if ( + this.props.current_asset && + prevProps.current_asset !== this.props.current_asset + ) { + this.setState({ + selectedAssetId: this.props.current_asset + }); + } } _onSelectedAsset(event) { @@ -62,16 +61,22 @@ class SetDefaultFeeAssetModal extends React.Component { } _getAssetsRows(assets) { - return assets.filter(item => !!item).map(assetInfo => ({ - id: assetInfo.asset.get("id"), - key: assetInfo.asset.get("id"), - asset: assetInfo.asset.get("symbol"), - link: `/asset/${assetInfo.asset.get("symbol")}`, - balance: - assetInfo.balance / - Math.pow(10, assetInfo.asset.get("precision")), - fee: assetInfo.fee - })); + return assets + .filter(item => { + return !!item && item.balance > 0 && !!item.asset; + }) + .map(assetInfo => { + return { + id: assetInfo.asset.get("id"), + key: assetInfo.asset.get("id"), + asset: assetInfo.asset.get("symbol"), + link: `/asset/${assetInfo.asset.get("symbol")}`, + balance: + assetInfo.balance / + Math.pow(10, assetInfo.asset.get("precision")), + fee: assetInfo.fee + }; + }); } onSubmit() { @@ -143,17 +148,10 @@ class SetDefaultFeeAssetModal extends React.Component { asset: ChainStore.getAsset(asset_id), balance: this.state.balances[asset_id] })); - if (this.props.asset_types.length > 0) { - assets = this.props.asset_types.map(assetInfo => ({ - ...assetInfo, - asset: ChainStore.getAsset(assetInfo.asset), - balance: this.state.balances[assetInfo.asset] - })); - } } let dataSource = this._getAssetsRows(assets); const footer = ( -
+
diff --git a/app/components/Settings/FeeAssetSettings.jsx b/app/components/Settings/FeeAssetSettings.jsx index d79fb04ef6..5f309c40dd 100644 --- a/app/components/Settings/FeeAssetSettings.jsx +++ b/app/components/Settings/FeeAssetSettings.jsx @@ -50,20 +50,22 @@ class FeeAssetSettings extends React.Component { > {counterpart.translate("settings.change_default_fee_asset")} - { - this.setState({current_asset: value}); - }} - close={() => { - this.setState({showModal: false}); - }} - /> + {this.state.showModal && ( + { + this.setState({current_asset: value}); + }} + close={() => { + this.setState({showModal: false}); + }} + /> + )}
); } diff --git a/app/components/Utility/AmountSelectorStyleGuide.jsx b/app/components/Utility/AmountSelectorStyleGuide.jsx index 2d819c30cf..8ba7226672 100644 --- a/app/components/Utility/AmountSelectorStyleGuide.jsx +++ b/app/components/Utility/AmountSelectorStyleGuide.jsx @@ -114,18 +114,23 @@ class AmountSelector extends DecimalChecker { validateStatus={this.props.validateStatus} help={this.props.help} > - + + + {addonAfter} + ); } diff --git a/app/components/Utility/AssetSelect.jsx b/app/components/Utility/AssetSelect.jsx index 6ab5ea3271..e56e552b95 100644 --- a/app/components/Utility/AssetSelect.jsx +++ b/app/components/Utility/AssetSelect.jsx @@ -8,6 +8,7 @@ import ChainTypes from "../Utility/ChainTypes"; import BindToChainState from "../Utility/BindToChainState"; import {Map} from "immutable"; import AssetName from "../Utility/AssetName"; +import LoadingIndicator from "../LoadingIndicator"; const AssetSelectView = ({ label, @@ -17,26 +18,30 @@ const AssetSelectView = ({ style, placeholder, value, + onDropdownVisibleChange, ...props }) => { - const onlyOne = assets.filter(Map.isMap).length <= 1; + const disableSelect = + assets.filter(Map.isMap).length <= 1 && !onDropdownVisibleChange; + // if onDropdownVisibleChange given we assume that lazy loading takes place const select = ( ); return ( diff --git a/app/components/Utility/FeeAssetSelector.jsx b/app/components/Utility/FeeAssetSelector.jsx index f13775089e..61f5d44aa9 100644 --- a/app/components/Utility/FeeAssetSelector.jsx +++ b/app/components/Utility/FeeAssetSelector.jsx @@ -4,208 +4,256 @@ import Immutable from "immutable"; import counterpart from "counterpart"; import AssetWrapper from "./AssetWrapper"; import PropTypes from "prop-types"; -import {Form, Input, Button, Tooltip} from "bitshares-ui-style-guide"; +import {Form, Input, Button, Tooltip, Icon} from "bitshares-ui-style-guide"; import AssetSelect from "./AssetSelect"; -import {ChainStore} from "bitsharesjs"; +import {FetchChain} from "bitsharesjs"; import SetDefaultFeeAssetModal from "../Modal/SetDefaultFeeAssetModal"; -import {debounce} from "lodash-es"; +import debounceRender from "react-debounce-render"; import {connect} from "alt-react"; import SettingsStore from "../../stores/SettingsStore"; import {checkFeeStatusAsync} from "common/trxHelper"; class FeeAssetSelector extends React.Component { + static propTypes = { + // injected + defaultFeeAsset: PropTypes.any, + + // object wih data required for fee calculation + transaction: PropTypes.any, + + // assets to choose from + assets: PropTypes.any, + + // a translation key for the input label, defaults to "Fee" + label: PropTypes.string, + + // handler for changedFee (asset, or amount) + onChange: PropTypes.func, + + // account which pays fee + account: PropTypes.any, + + // tab index if needed + tabIndex: PropTypes.number, + + // do not allow to switch the asset or amount + disabled: PropTypes.bool + }; + + static defaultProps = { + label: "transfer.fee", + disabled: false + }; + constructor(props) { super(props); this.state = { - asset: null, - assets: [], - fee_amount: 0, - fee_asset_id: - ChainStore.assets_by_symbol.get( - props.settings.get("fee_asset") - ) || "1.3.0", - fees: {}, - feeStatus: {}, + feeAsset: props.defaultFeeAsset, + + calculatedFeeAmount: null, + + assets: null, + assetsLoading: false, + isModalVisible: false, - error: null, - assets_fetched: false, - last_fee_check_params: {} + error: null }; - this._updateFee = debounce(this._updateFee.bind(this), 250); } - async _getFees(assets, account, trxInfo) { - const accountID = account.get("id"); - let result = this.state.fees; - for (let asset_id of assets) { - const {fee} = await checkFeeStatusAsync({ - ...trxInfo, - accountID, - feeID: asset_id - }); - result[asset_id] = fee.getAmount({real: true}); + async _calculateFee(asset = null) { + const {account, transaction} = this.props; + const setState = asset == null; + if (!asset) { + asset = this.state.feeAsset; } - this.setState({fees: result}); - } - - shouldComponentUpdate(np, ns) { - return ( - ns.fee_amount !== this.state.fee_amount || - ns.fee_asset_id !== this.state.fee_asset_id || - ns.isModalVisible !== this.state.isModalVisible || - ns.assets_fetched !== this.state.assets_fetched || - ns.assets.length !== this.state.assets.length - ); - } + const feeID = typeof asset == "string" ? asset : asset.get("id"); + try { + const {fee, hasPoolBalance} = await checkFeeStatusAsync({ + ...transaction, + accountID: account.get("id"), + feeID + }); - __are_equal_shallow(o1, o2) { - for (var p in o1) { - if (o1.hasOwnProperty(p)) { - if (o1[p] !== o2[p]) { - return false; - } - } - } - for (var p in o2) { - if (o2.hasOwnProperty(p)) { - if (o1[p] !== o2[p]) { - return false; - } + if (setState) { + this.setState( + { + calculatedFeeAmount: fee.getAmount({real: true}), + error: !hasPoolBalance + ? { + key: "noPoolBalanceShort", + tooltip: "noPoolBalance" + } + : false + }, + () => { + if (this.props.onChange) { + this.props.onChange(fee); + } + } + ); } - } - return true; - } - - _updateFee(asset_id, trxInfo, onChange, account = this.props.account) { - if (!account) return null; - - let feeID = asset_id || this.state.fee_asset_id; - this._getFees(this.state.assets, account, trxInfo); - const options = { - ...trxInfo, - accountID: account.get("id"), - feeID - }; - if ( - JSON.stringify(this.state.last_fee_check_params) !== - JSON.stringify(options) - ) { - checkFeeStatusAsync(options) - .then(({fee, hasPoolBalance}) => { - this.setState({ - fee_amount: fee.getAmount({real: true}), - fee_asset_id: fee.asset_id, - error: !hasPoolBalance, - last_fee_check_params: options - }); - if (onChange) { - onChange(fee); + return { + fee, + hasPoolBalance + }; + } catch (err) { + if (setState) { + this.setState({ + calculatedFeeAmount: 0, + error: { + key: "unknown" } - this.setState({ - assets: this._getAvailableAssets(account), - fee_amount: fee.getAmount({real: true}), - fee_asset_id: fee.asset_id - }); - }) - .catch(err => { - console.warn(err); }); + } + console.error(err); + throw err; } } - componentWillReceiveProps(np, ns) { - const {fee_amount, fee_asset_id} = this.state; - const trxInfoChanged = !this.__are_equal_shallow( - np.trxInfo, - this.props.trxInfo - ); - const account_changed = + shouldComponentUpdate(np, ns) { + const accountChanged = np.account && this.props.account && np.account.get("id") !== this.props.account.get("id"); - const needsFeeCalculation = - trxInfoChanged || !fee_amount || account_changed; - if (needsFeeCalculation) { - this._updateFee(fee_asset_id, np.trxInfo, np.onChange, np.account); + const transactionChanged = + JSON.stringify(np.transaction) !== + JSON.stringify(this.props.transaction); + if (ns.assets) { + if (!this.state.assets) { + return true; + } + if (ns.assets.length !== this.state.assets.length) { + return true; + } + } + if (ns.feeAsset) { + if (!this.state.feeAsset) { + return true; + } + if (ns.feeAsset.get("id") !== this.state.feeAsset.get("id")) { + return true; + } } + return ( + accountChanged || + transactionChanged || + ns.calculatedFeeAmount !== this.state.calculatedFeeAmount || + ns.assetsLoading !== this.state.assetsLoading || + ns.isModalVisible !== this.state.isModalVisible || + ns.error !== this.state.error + ); } _getAsset() { - const {assets, fee_asset_id} = this.state; - return ChainStore.getAsset( - fee_asset_id - ? fee_asset_id - : assets.length === 1 - ? assets[0] - : "1.3.0" - ); + const {assets, feeAsset} = this.state; + return feeAsset + ? feeAsset + : assets && assets.length > 0 + ? assets[0] + : null; } - _getAvailableAssets(account = this.props.account) { - if (this.state.assets_fetched && this.state.assets.length > 0) { + _getSelectableAssets() { + return this.state.assets + ? this.state.assets + : [this._getAsset().get("symbol")]; + } + + async _syncAvailableAssets(opened, account = this.props.account) { + if (this.state.assets) { return this.state.assets; } - let fee_asset_types = []; - if (!(account && account.get("balances"))) { - return fee_asset_types; - } - const account_balances = account.get("balances").toJS(); - fee_asset_types = Object.keys(account_balances).sort(utils.sortID); - for (let key in account_balances) { - let balanceObject = ChainStore.getObject(account_balances[key]); - if (balanceObject && balanceObject.get("balance") === 0) { - if (fee_asset_types.includes(key)) { - fee_asset_types.splice(fee_asset_types.indexOf(key), 1); - } + this.setState({ + assetsLoading: true + }); + let possibleAssets = [this._getAsset().get("id")]; + const accountBalances = account.get("balances").toJS(); + const sortedKeys = Object.keys(accountBalances).sort(utils.sortID); + for (let i = 0, key; (key = sortedKeys[i]); i++) { + const balanceObject = await FetchChain( + "getObject", + accountBalances[key] + ); + const requiredForFee = await this._calculateFee(key); + if ( + balanceObject && + balanceObject.get("balance") >= + requiredForFee.fee.getAmount() && + !possibleAssets.includes(key) + ) { + possibleAssets.push(key); + possibleAssets = possibleAssets.sort(utils.sortID); + this.setState({ + assets: possibleAssets + }); } } this.setState({ - balances: account_balances, - assets: fee_asset_types, - assets_fetched: true + assetsLoading: false }); - this._updateFee( - this.state.fee_asset_id, - this.props.trxInfo, - this.props.onChange - ); - return fee_asset_types; } componentDidMount() { - this.onAssetChange(this.state.fee_asset_id); + this._calculateFee(); + } + + componentDidUpdate(prevProps) { + const {calculatedFeeAmount} = this.state; + const accountChanged = + this.props.account && + prevProps.account.get("id") !== this.props.account.get("id"); + const transactionChanged = + JSON.stringify(prevProps.transaction) !== + JSON.stringify(this.props.transaction); + const noFeeSetYet = !calculatedFeeAmount; + if (accountChanged) { + this.setState({assets: null}); + } + if (transactionChanged || accountChanged || noFeeSetYet) { + this._calculateFee(); + } } - onAssetChange(selected_asset) { - this.setState({fee_asset_id: selected_asset}); - this._updateFee( - selected_asset, - this.props.trxInfo, - this.props.onChange + componentWillReceiveProps(np, ns) { + // don't do async loading in componentWillReceiveProps + } + + async onAssetChange(selectedAssetId) { + const asset = await FetchChain("getAsset", selectedAssetId); + this.setState( + { + feeAsset: asset + }, + this._calculateFee.bind(this) ); } render() { const currentAsset = this._getAsset(); - const assets = - this.state.assets.length > 0 - ? this.state.assets - : [currentAsset.get("id") || "1.3.0"]; - - let value = this.state.error - ? counterpart.translate("transfer.errors.insufficient") - : this.state.fee_amount; + // noPoolBalanceShort + let feeInputString = this.state.error + ? counterpart.translate("transfer.errors." + this.state.error.key) + : this.state.calculatedFeeAmount; const label = this.props.label ? (
{counterpart.translate(this.props.label)} + {this.state.error && + this.state.error.tooltip && ( + +   + + )}
) : null; - const canChangeFeeParams = - !this.props.selectDisabled && this.props.account; + const canChangeFeeParams = !this.props.disabled && !!this.props.account; const changeDefaultButton = ( ); + const selectableAssets = this._getSelectableAssets(); + return (
- ({ - asset, - fee: this.state.fees[asset] - }))} - displayFees={true} - forceDefault={false} - current_asset={this.state.fee_asset_id} - onChange={this.onAssetChange.bind(this)} - close={() => { - this.setState({isModalVisible: false}); - }} - /> + {this.state.isModalVisible && ( + ({ + //asset, + //fee: this.state.fees[asset] + //})) + } + displayFees={true} + forceDefault={false} + current_asset={currentAsset.get("id")} + onChange={this.onAssetChange.bind(this)} + close={() => { + this.setState({isModalVisible: false}); + }} + /> + )}
); } @@ -284,34 +344,13 @@ class FeeAssetSelector extends React.Component { } } -FeeAssetSelector.propTypes = { - // a translation key for the input label - label: PropTypes.string, - // account which pays fee - account: PropTypes.any, - // handler for changed Fee (asset, or amount) - onChange: PropTypes.func, - tabIndex: PropTypes.number, - selectDisabled: PropTypes.bool, - settings: PropTypes.any, - // Object wih data required for fee calculation - trxInfo: PropTypes.any -}; - -FeeAssetSelector.defaultProps = { - disabled: true, - tabIndex: 0, - selectDisabled: false, - label: "transfer.fee", - account: null, - trxInfo: { - type: "transfer", - options: null, - data: {} - } -}; +FeeAssetSelector = debounceRender(FeeAssetSelector, 150, { + leading: false +}); -FeeAssetSelector = AssetWrapper(FeeAssetSelector); +FeeAssetSelector = AssetWrapper(FeeAssetSelector, { + propNames: ["defaultFeeAsset"] +}); export default connect( FeeAssetSelector, @@ -319,9 +358,11 @@ export default connect( listenTo() { return [SettingsStore]; }, - getProps(props) { + getProps() { return { - settings: SettingsStore.getState().settings + defaultFeeAsset: + SettingsStore.getState().settings.get("fee_asset") || + "1.3.0" }; } } diff --git a/app/lib/common/trxHelper.js b/app/lib/common/trxHelper.js index bad5142ea6..eea5df8e95 100644 --- a/app/lib/common/trxHelper.js +++ b/app/lib/common/trxHelper.js @@ -13,7 +13,11 @@ function estimateFeeAsync(type, options = null, data = {}) { return new Promise((res, rej) => { FetchChain("getObject", "2.0.0") .then(obj => { - res(estimateFee(type, options, obj, data)); + try { + res(estimateFee(type, options, obj, data)); + } catch (err) { + rej(err); + } }) .catch(rej); }); @@ -25,17 +29,19 @@ function checkFeePoolAsync({ options = null, data } = {}) { - return new Promise(res => { + return new Promise((res, rej) => { if (assetID === "1.3.0") { res(true); } else { Promise.all([ estimateFeeAsync(type, options, data), FetchChain("getObject", assetID.replace(/^1\./, "2.")) - ]).then(result => { - const [fee, dynamicObject] = result; - res(parseInt(dynamicObject.get("fee_pool"), 10) >= fee); - }); + ]) + .then(result => { + const [fee, dynamicObject] = result; + res(parseInt(dynamicObject.get("fee_pool"), 10) >= fee); + }) + .catch(rej); } }); } @@ -192,9 +198,9 @@ function checkFeeStatusAsync({ }, feeStatusTTL); }); }) - .catch(() => { + .catch(err => { asyncCache[key].queue.forEach(promise => { - promise.rej(); + promise.rej(err); }); }); });