diff --git a/package.json b/package.json index cbb67fba9..acfd3a9d3 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "private": true, "main": "./electron/index.js", "dependencies": { + "big.js": "^5.2.1", "bluebird": "^3.5.1", "centrifuge": "^1.4.6", "chart.js": "^2.7.2", @@ -90,6 +91,7 @@ "release": "electron-builder --config electron-builder.json" }, "devDependencies": { + "@types/big.js": "^4.0.5", "@types/bluebird": "^3.5.18", "@types/chart.js": "^2.7.22", "@types/classnames": "^2.2.3", @@ -147,4 +149,4 @@ "typescript": "^2.8.3", "typescript-react-intl": "^0.1.7" } -} \ No newline at end of file +} diff --git a/public/locales/ru-RU.json b/public/locales/ru-RU.json index 2843934eb..493fab4f0 100644 --- a/public/locales/ru-RU.json +++ b/public/locales/ru-RU.json @@ -63,6 +63,7 @@ "confirm": "Подтвердить", "contract": "Смарт-контракт", "contract.exec": "Вызвать контракт", + "disabled": "Отключено", "editor.block.create": "Создать блок", "editor.close.confirm": "Вы действительно хотите закрыть '{name}' без сохранения изменений?", "editor.close.all": "Закрыть все вкладки", @@ -188,6 +189,9 @@ "tx.error.E_LIMITFORSIGN.desc": "Параметры вызова содержат данные слишком большого объёма", "tx.error.E_OFFLINE.desc": "Не удалось установить подключение к серверу", "tx.error.E_SERVER.desc": "{error}", + "tx.spending.limit.desc": "Вызываемые транзакции, не смогут израсходовать больше валюты, чем указано здесь", + "tx.spending.limit.long": "Лимит списания транзакций", + "tx.spending.limit.short": "Лимит", "validation.field.invalid": "Поле содержит некорректные данные", "validation.required": "Данное поле необходимо заполнить", "validation.minlength": "Значение слишком короткое", diff --git a/src/app/components/Animation/Dropdown.tsx b/src/app/components/Animation/Dropdown.tsx index 12a41c755..b18e2ea7b 100644 --- a/src/app/components/Animation/Dropdown.tsx +++ b/src/app/components/Animation/Dropdown.tsx @@ -53,7 +53,7 @@ const containerAnimationDef = { padding: '0 50px 50px', margin: '0 -50px', overflow: 'hidden', - zIndex: 500 + zIndex: 590 } }; diff --git a/src/app/components/Button/index.tsx b/src/app/components/Button/index.tsx index 94a93090a..71fcdc2e8 100644 --- a/src/app/components/Button/index.tsx +++ b/src/app/components/Button/index.tsx @@ -26,6 +26,7 @@ export interface IButtonProps { disabled?: boolean; pending?: boolean; className?: string; + style?: React.CSSProperties; onClick?: React.MouseEventHandler; } @@ -35,6 +36,7 @@ const Button: React.SFC = props => ( onClick={props.onClick} disabled={props.disabled} className={props.className} + style={props.style} > {props.children} diff --git a/src/app/components/DropdownButton/index.tsx b/src/app/components/DropdownButton/index.tsx index 4af07adef..bc1c46f7d 100644 --- a/src/app/components/DropdownButton/index.tsx +++ b/src/app/components/DropdownButton/index.tsx @@ -34,6 +34,7 @@ const StyledDropdown = themed.div` &.dropdown-active button.dropdown-toggle { background: rgba(0,0,0,0.15); + z-index: 610; } button.dropdown-toggle { @@ -43,7 +44,7 @@ const StyledDropdown = themed.div` padding: 0 10px; text-align: center; position: relative; - z-index: 1000; + z-index: 500; .dropdown-badge { position: absolute; diff --git a/src/app/components/Main/Toolbar/DropdownToolButton.tsx b/src/app/components/Main/Toolbar/DropdownToolButton.tsx index 7d082d1e1..a5e56f352 100644 --- a/src/app/components/Main/Toolbar/DropdownToolButton.tsx +++ b/src/app/components/Main/Toolbar/DropdownToolButton.tsx @@ -32,6 +32,7 @@ export interface IDropdownToolButtonProps { disabled?: boolean; right?: boolean; leftMost?: boolean; + width?: number; content: React.ReactNode; } @@ -39,7 +40,7 @@ const DropdownToolButton: React.SFC = props => (
  • = props => (
  • ); -const StyledDropdownToolButton = styled(DropdownToolButton) ` +const StyledDropdownToolButton = styled(DropdownToolButton)` display: inline-block; vertical-align: top; diff --git a/src/app/components/Main/TxMenu.tsx b/src/app/components/Main/TxMenu.tsx new file mode 100644 index 000000000..d365082a1 --- /dev/null +++ b/src/app/components/Main/TxMenu.tsx @@ -0,0 +1,107 @@ +// MIT License +// +// Copyright (c) 2016-2018 GenesisKernel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import React from 'react'; +import propTypes from 'prop-types'; +import { Row, Col } from 'react-bootstrap'; +import { FormattedMessage } from 'react-intl'; + +import DropdownToolButton from './Toolbar/DropdownToolButton'; +import Validation from '../Validation'; +import { CloseDropdownButton } from 'components/DropdownButton'; + +export interface ITxMenuProps { + maxSum: string; + onEdit: (value: string) => any; +} + +interface ITxMenuContext { + closeDropdown: () => void; +} + +const TxMenuContent: React.SFC = (props, context: ITxMenuContext) => ( +
    +
    + +
    +
    + +
    +
    + { props.onEdit(payload.maxSum === '' ? null : payload.maxSum); context.closeDropdown(); }}> + + + + + + props.onEdit(null)} className="btn btn-block btn-default"> + + + + + + + + + + +
    +
    +); + +TxMenuContent.contextTypes = { + closeDropdown: propTypes.func.isRequired +}; + +const TxMenu: React.SFC = (props) => ( + + )} + > + + : + + + {props.maxSum ? + ( + {props.maxSum} + ) : + ( + + + + ) + } + + +); + +export default TxMenu; \ No newline at end of file diff --git a/src/app/components/Main/index.tsx b/src/app/components/Main/index.tsx index 953628cd0..70ef342d2 100644 --- a/src/app/components/Main/index.tsx +++ b/src/app/components/Main/index.tsx @@ -38,6 +38,7 @@ import ToolButton from 'components/Main/Toolbar/ToolButton'; import EditorToolbar from 'containers/Main/Toolbar/EditorToolbar'; import ToolIndicator from 'components/Main/Toolbar/ToolIndicator'; import LoadingBar from './LoadingBar'; +import TxMenu from 'containers/Main/Toolbar/TxMenu'; // import TransactionsMenu from './TransactionsMenu'; const StyledWrapper = themed.div` @@ -169,6 +170,7 @@ class Main extends React.Component { titleDesc={} /> )} + {'editor' === this.props.section ? ( diff --git a/src/app/components/Validation/ValidatedControl.tsx b/src/app/components/Validation/ValidatedControl.tsx index 451bee0e2..f2e5d7a19 100644 --- a/src/app/components/Validation/ValidatedControl.tsx +++ b/src/app/components/Validation/ValidatedControl.tsx @@ -103,6 +103,7 @@ export default class ValidatedControl extends React.Component implements IValidatedControl { + constructor(props: IValidatedDecimalProps) { + super(props); + + this.state = { + value: (props.value || props.defaultValue || '') as string + }; + } + + componentDidMount() { + if (this.context.form) { + (this.context.form as ValidatedForm)._registerElement(this); + } + } + + componentWillUnmount() { + if (this.context.form) { + (this.context.form as ValidatedForm)._unregisterElement(this); + } + } + + componentWillReceiveProps(props: IValidatedDecimalProps) { + if (this.props.value !== props.value) { + this.setState({ + value: props.value === null ? '' : props.value as string + }); + (this.context.form as ValidatedForm).updateState(props.name, props.value); + } + } + + getValue() { + return this.state.value; + } + + validateValue(value: string) { + if (value === '') { + return true; + } + + value = value.replace(',', '.'); + + if (!/^(0|[1-9]+[0-9]*)\.?\d*$/.test(value)) { + return false; + } + + const dotCount = (value.match(/\./g) || []).length; + + if (dotCount > 1) { + return false; + } + + if (dotCount === 1 && value.split('.')[1].length > MONEY_POWER) { // check precision + return false; + } + + return true; + } + + onChange = (e: React.ChangeEvent) => { + const value = (e.target as any).value.replace(',', '.'); + + if (!this.validateValue(value)) { + return; + } + + this.setState({ + value + }); + + if (this.props.onChange) { + this.props.onChange(e); + } + + (this.context.form as ValidatedForm).emitUpdate(this.props.name, value); + } + + onBlur = (e: React.FocusEvent) => { + if (this.context.form) { + (this.context.form as ValidatedForm).updateState(this.props.name); + } + + if (this.props.onBlur) { + this.props.onBlur(e); + } + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +(ValidatedDecimal as React.ComponentClass).contextTypes = { + form: propTypes.instanceOf(ValidatedForm) +}; \ No newline at end of file diff --git a/src/app/components/Validation/Validators.tsx b/src/app/components/Validation/Validators.tsx index 7a5834928..ca4a5ea35 100644 --- a/src/app/components/Validation/Validators.tsx +++ b/src/app/components/Validation/Validators.tsx @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Big from 'big.js'; + export class Validator { public name: string; public params?: any; @@ -60,6 +62,35 @@ export const required = new Validator({ } }); +export const min: IValidatorGenerator = (minValue: number | string) => { + return new Validator({ + name: 'min', + validate: (value) => { + if ('string' !== typeof value) { + throw new Error(`Unrecognized value type "${typeof value}"`); + } + + // Do not affect empty strings. 'required' must do this job + return value.length === 0 || Big(minValue).lte(Big(value)); + + } + }); +}; + +export const max: IValidatorGenerator = (maxValue: number | string) => { + return new Validator({ + name: 'max', + validate: (value) => { + if ('string' !== typeof value) { + throw new Error(`Unrecognized value type "${typeof value}"`); + } + + // Do not affect empty strings. 'required' must do this job + return value.length === 0 || Big(maxValue).gte(Big(value)); + } + }); +}; + export const minlength: IValidatorGenerator = (count: number | string) => { return new Validator({ name: 'minlength', diff --git a/src/app/components/Validation/index.ts b/src/app/components/Validation/index.ts index 10d3a1918..f1fe7870b 100644 --- a/src/app/components/Validation/index.ts +++ b/src/app/components/Validation/index.ts @@ -32,6 +32,7 @@ import ValidatedForm from './ValidatedForm'; import ValidatedFormGroup from './ValidatedFormGroup'; import ValidatedRadioGroup from './ValidatedRadioGroup'; import ValidatedSubmit from './ValidatedSubmit'; +import ValidatedDecimal from './ValidatedDecimal'; import * as validators from './Validators'; export default { @@ -47,7 +48,8 @@ export default { ValidatedFormGroup, ValidatedRadioGroup, ValidatedSubmit, - ValidationMessage + ValidationMessage, + ValidatedDecimal }, validators }; \ No newline at end of file diff --git a/src/app/containers/Main/Toolbar/TxMenu.tsx b/src/app/containers/Main/Toolbar/TxMenu.tsx new file mode 100644 index 000000000..3b0b2ae07 --- /dev/null +++ b/src/app/containers/Main/Toolbar/TxMenu.tsx @@ -0,0 +1,55 @@ +// MIT License +// +// Copyright (c) 2016-2018 GenesisKernel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import React from 'react'; +import { IRootState } from 'modules'; +import { connect } from 'react-redux'; +import { updateSettings } from 'modules/auth/actions'; +import TxMenu from 'components/Main/TxMenu'; + +export interface ITxMenuContainerProps { + +} + +interface ITxMenuContainerState { + maxSum: string; +} + +interface ITxMenuContainerDispatch { + onEdit: (maxSum: string) => void; +} + +const TxMenuContainer: React.SFC = props => ( + +); + +const mapStateToProps = (state: IRootState) => ({ + maxSum: state.auth.wallet && state.auth.wallet.settings && state.auth.wallet.settings.maxSum +}); + +const mapDispatchToProps = { + onEdit: (maxSum: string) => updateSettings({ + maxSum + }) +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TxMenuContainer); \ No newline at end of file diff --git a/src/app/lib/en-US.json b/src/app/lib/en-US.json index 240124f6f..c525639e5 100644 --- a/src/app/lib/en-US.json +++ b/src/app/lib/en-US.json @@ -63,6 +63,7 @@ "confirm": "Confirm", "contract": "Smart contract", "contract.exec": "Execute contract", + "disabled": "Disabled", "editor.block.create": "Create block", "editor.close.confirm": "Do you really want to close '{name}' without saving changes?", "editor.close.all": "Close all tabs", @@ -188,6 +189,9 @@ "tx.error.E_LIMITFORSIGN.desc": "Transaction params are too heavy to execute", "tx.error.E_OFFLINE.desc": "Failed to connect to the server", "tx.error.E_SERVER.desc": "{error}", + "tx.spending.limit.desc": "Transactions executed on your behalf will not be able to spend more tokens than you specify here", + "tx.spending.limit.long": "Transaction spending limit", + "tx.spending.limit.short": "Limit", "validation.field.invalid": "This field contains invalid data", "validation.required": "This field is mandatory", "validation.minlength": "Value is too short", diff --git a/src/app/lib/tx/contract/index.ts b/src/app/lib/tx/contract/index.ts index 8a92390a6..414df7a2f 100644 --- a/src/app/lib/tx/contract/index.ts +++ b/src/app/lib/tx/contract/index.ts @@ -32,6 +32,7 @@ export interface IContractContext { id: number; schema: ISchema; ecosystemID: number; + maxSum?: string; fields: { [name: string]: IContractParam; }; @@ -42,6 +43,21 @@ export interface IContractParam { value: object; } +export interface IContractBuffer { + Header: { + ID: number; + Time: number; + EcosystemID: number; + KeyID: Int64BE; + NetworkID: number; + PublicKey: ArrayBuffer; + }; + MaxSum?: string; + Params: { + [name: string]: any; + }; +} + export default class Contract { private _context: IContractContext; private _keyID: Int64BE; @@ -97,23 +113,22 @@ export default class Contract { params[name] = this._fields[name].get(); }); - const txBuffer = msgpack.encode( - { - Header: { - ID: this._context.id, - Time: this._time, - EcosystemID: this._context.ecosystemID, - KeyID: this._keyID, - NetworkID: this._context.schema.network, - PublicKey: this._publicKey - }, - Params: params + const data: IContractBuffer = { + Header: { + ID: this._context.id, + Time: this._time, + EcosystemID: this._context.ecosystemID, + KeyID: this._keyID, + NetworkID: this._context.schema.network, + PublicKey: this._publicKey }, - { - codec - } - ); + Params: params + }; + + if (this._context.maxSum) { + data.MaxSum = convert.toMoney(this._context.maxSum); + } - return txBuffer; + return msgpack.encode(data, { codec }); } } \ No newline at end of file diff --git a/src/app/modules/auth/actions.ts b/src/app/modules/auth/actions.ts index 84e927d2f..9bd116892 100644 --- a/src/app/modules/auth/actions.ts +++ b/src/app/modules/auth/actions.ts @@ -21,7 +21,7 @@ // SOFTWARE. import actionCreatorFactory from 'typescript-fsa'; -import { IWallet, ILoginCall, IRole, ISession } from 'genesis/auth'; +import { IWallet, ILoginCall, IRole, ISession, IWalletSettings } from 'genesis/auth'; import { ICreateWalletCall, IImportWalletCall } from 'genesis/auth'; const actionCreator = actionCreatorFactory('auth'); @@ -37,4 +37,5 @@ export const selectWallet = actionCreator('SELECT_WALLET'); export const selectRole = actionCreator.async('SELECT_ROLE'); export const authorize = actionCreator('AUTHORIZE'); export const deauthorize = actionCreator('DEAUTHORIZE'); -export const changePassword = actionCreator.async('CHANGE_PASSWORD'); \ No newline at end of file +export const changePassword = actionCreator.async('CHANGE_PASSWORD'); +export const updateSettings = actionCreator>('UPDATE_SETTINGS'); \ No newline at end of file diff --git a/src/app/modules/auth/epics/createWalletEpic.ts b/src/app/modules/auth/epics/createWalletEpic.ts index eea493f95..2995792af 100644 --- a/src/app/modules/auth/epics/createWalletEpic.ts +++ b/src/app/modules/auth/epics/createWalletEpic.ts @@ -53,7 +53,8 @@ const createWalletEpic: Epic = (action$, store, { api }) => action$.ofAction(cre address: payload.address, ecosystem: '1', ecosystemName: null, - username: payload.key_id + username: payload.key_id, + settings: {} } }), navigate('/') diff --git a/src/app/modules/auth/epics/importWalletEpic.ts b/src/app/modules/auth/epics/importWalletEpic.ts index afddc48ff..8416478de 100644 --- a/src/app/modules/auth/epics/importWalletEpic.ts +++ b/src/app/modules/auth/epics/importWalletEpic.ts @@ -63,7 +63,8 @@ const importWalletEpic: Epic = (action$, store, { api }) => action$.ofAction(imp ecosystem: wallet.ecosystem_id, ecosystemName: null, username: wallet.key_id, - avatar: null + avatar: null, + settings: {} })); if (wallets.length) { diff --git a/src/app/modules/auth/epics/inviteEcosystemEpic.ts b/src/app/modules/auth/epics/inviteEcosystemEpic.ts index 1ead1446b..62116000b 100644 --- a/src/app/modules/auth/epics/inviteEcosystemEpic.ts +++ b/src/app/modules/auth/epics/inviteEcosystemEpic.ts @@ -48,7 +48,8 @@ const inviteEcosystemEpic: Epic = (action$, store) => action$.ofAction(inviteEco address: wallet.address, ecosystem: action.payload.ecosystem, ecosystemName: null, - username: null + username: null, + settings: {} }), ); diff --git a/src/app/modules/auth/epics/loginEpic.ts b/src/app/modules/auth/epics/loginEpic.ts index 5a98b798b..aa642f894 100644 --- a/src/app/modules/auth/epics/loginEpic.ts +++ b/src/app/modules/auth/epics/loginEpic.ts @@ -85,12 +85,8 @@ const loginEpic: Epic = (action$, store, { api }) => action$.ofAction(login.star params: action.payload, result: { wallet: { - id: wallet.key_id, - encKey: action.payload.wallet.encKey, - address: wallet.address, - ecosystem: action.payload.wallet.ecosystem, - ecosystemName, - username: wallet.username + ...action.payload.wallet, + ecosystemName }, roles: wallet.roles && wallet.roles.map(role => ({ id: role.role_id, diff --git a/src/app/modules/auth/reducer.ts b/src/app/modules/auth/reducer.ts index f48a10b3a..b4de92615 100644 --- a/src/app/modules/auth/reducer.ts +++ b/src/app/modules/auth/reducer.ts @@ -39,6 +39,7 @@ import authorizeHandler from './reducers/authorizeHandler'; import deauthorizeHandler from './reducers/deauthorizeHandler'; import generateSeedDoneHandler from './reducers/generateSeedDoneHandler'; import selectRoleDoneHandler from './reducers/selectRoleDoneHandler'; +import updateSettingsHandler from './reducers/updateSettingsHandler'; export type State = { readonly loadedSeed: string; @@ -90,4 +91,5 @@ export default reducerWithInitialState(initialState) .case(actions.selectRole.done, selectRoleDoneHandler) .case(actions.authorize, authorizeHandler) .case(actions.deauthorize, deauthorizeHandler) - .case(actions.generateSeed.done, generateSeedDoneHandler); \ No newline at end of file + .case(actions.generateSeed.done, generateSeedDoneHandler) + .case(actions.updateSettings, updateSettingsHandler); \ No newline at end of file diff --git a/src/app/modules/auth/reducers/updateSettingsHandler.ts b/src/app/modules/auth/reducers/updateSettingsHandler.ts new file mode 100644 index 000000000..f008b2bd1 --- /dev/null +++ b/src/app/modules/auth/reducers/updateSettingsHandler.ts @@ -0,0 +1,38 @@ +// MIT License +// +// Copyright (c) 2016-2018 GenesisKernel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { State } from '../reducer'; +import { updateSettings } from '../actions'; +import { Reducer } from 'modules'; + +const updateSettingsHandler: Reducer = (state, payload) => ({ + ...state, + wallet: { + ...state.wallet, + settings: { + ...state.wallet.settings, + ...payload + } + } +}); + +export default updateSettingsHandler; \ No newline at end of file diff --git a/src/app/modules/engine/epics/initializeEpic.ts b/src/app/modules/engine/epics/initializeEpic.ts index 87aa1ded6..2366246a7 100644 --- a/src/app/modules/engine/epics/initializeEpic.ts +++ b/src/app/modules/engine/epics/initializeEpic.ts @@ -122,7 +122,8 @@ const initializeEpic: Epic = (action$, store, { api, defaultKey, defaultPassword address: result.login.address, ecosystem: result.login.ecosystem_id, ecosystemName: null, - username: null + username: null, + settings: {} })), Observable.empty() ), diff --git a/src/app/modules/storage/epic.ts b/src/app/modules/storage/epic.ts index 7255068fe..ce92cd807 100644 --- a/src/app/modules/storage/epic.ts +++ b/src/app/modules/storage/epic.ts @@ -26,11 +26,13 @@ import saveWalletOnEcosystemInitEpic from './epics/saveWalletOnEcosystemInitEpic import saveWalletOnSelectEpic from './epics/saveWalletOnSelectEpic'; import saveWalletOnImportEpic from './epics/saveWalletOnImportEpic'; import saveWalletOnCreateEpic from './epics/saveWalletOnCreateEpic'; +import saveWalletOnSettingsEpic from './epics/saveWalletOnSettingsEpic'; export default combineEpics( saveWalletOnCreateEpic, saveWalletOnEcosystemInitEpic, saveWalletOnImportEpic, saveWalletOnLoginEpic, - saveWalletOnSelectEpic + saveWalletOnSelectEpic, + saveWalletOnSettingsEpic ); \ No newline at end of file diff --git a/src/app/modules/storage/epics/saveWalletOnSettingsEpic.ts b/src/app/modules/storage/epics/saveWalletOnSettingsEpic.ts new file mode 100644 index 000000000..c2eebf2c4 --- /dev/null +++ b/src/app/modules/storage/epics/saveWalletOnSettingsEpic.ts @@ -0,0 +1,35 @@ +// MIT License +// +// Copyright (c) 2016-2018 GenesisKernel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { IRootState } from 'modules'; +import { saveWallet } from '../actions'; +import { updateSettings } from 'modules/auth/actions'; + +const saveWalletOnSettingsEpic: Epic = + (action$, store) => action$.ofAction(updateSettings) + .map(action => + saveWallet(store.getState().auth.wallet) + ); + +export default saveWalletOnSettingsEpic; \ No newline at end of file diff --git a/src/app/modules/storage/reducers/saveWalletHandler.ts b/src/app/modules/storage/reducers/saveWalletHandler.ts index de51689d6..e2eb552d5 100644 --- a/src/app/modules/storage/reducers/saveWalletHandler.ts +++ b/src/app/modules/storage/reducers/saveWalletHandler.ts @@ -23,13 +23,24 @@ import { State } from '../reducer'; import { saveWallet } from '../actions'; import { Reducer } from 'modules'; +import { IWallet } from 'genesis/auth'; -const saveWalletHandler: Reducer = (state, payload) => ({ - ...state, - wallets: [ - ...state.wallets.filter(l => l.id !== payload.id || l.ecosystem !== payload.ecosystem), - payload - ] -}); +const saveWalletHandler: Reducer = (state, payload) => { + const wallet: Partial = state.wallets.find(l => l.id === payload.id && l.ecosystem === payload.ecosystem) || {}; + return { + ...state, + wallets: [ + ...state.wallets.filter(l => l.id !== payload.id || l.ecosystem !== payload.ecosystem), + { + ...wallet, + ...payload, + settings: { + ...wallet.settings, + ...payload.settings + } + } + ] + }; +}; export default saveWalletHandler; \ No newline at end of file diff --git a/src/app/modules/tx/epics/newEcosystemEpic.ts b/src/app/modules/tx/epics/newEcosystemEpic.ts index 4c08faa9a..8c783a783 100644 --- a/src/app/modules/tx/epics/newEcosystemEpic.ts +++ b/src/app/modules/tx/epics/newEcosystemEpic.ts @@ -39,7 +39,8 @@ const newEcosystemEpic: Epic = (action$, store) => action$.o address: wallet.address, username: null, ecosystem, - ecosystemName: String(result.params.Name.value) || ecosystem + ecosystemName: String(result.params.Name.value) || ecosystem, + settings: {} }); })); diff --git a/src/app/modules/tx/epics/txExecEpic.ts b/src/app/modules/tx/epics/txExecEpic.ts index 4750c438c..bc0d4aa8b 100644 --- a/src/app/modules/tx/epics/txExecEpic.ts +++ b/src/app/modules/tx/epics/txExecEpic.ts @@ -80,8 +80,8 @@ export const txExecEpic: Epic = (action$, store, { api }) => action$.ofAction(tx id: proto.id, schema: defaultSchema, ecosystemID: parseInt(state.auth.wallet && state.auth.wallet.ecosystem || '1', 10), - fields: txParams - + fields: txParams, + maxSum: state.auth.wallet.settings && state.auth.wallet.settings.maxSum }).sign(privateKey)).map(signature => ({ ...signature, name: proto.name, diff --git a/src/defs/auth.d.ts b/src/defs/auth.d.ts index 64903b889..8e203714a 100644 --- a/src/defs/auth.d.ts +++ b/src/defs/auth.d.ts @@ -28,6 +28,11 @@ declare module 'genesis/auth' { ecosystem: string; ecosystemName: string; username: string; + settings: IWalletSettings; + } + + interface IWalletSettings { + maxSum?: string; } interface ISaveEncKeyCall { diff --git a/yarn.lock b/yarn.lock index 3df691f3a..1abe1dc30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,6 +47,10 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@types/big.js@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-4.0.5.tgz#62c61697646269e39191f24e55e8272f05f21fc0" + "@types/bluebird@^3.5.18": version "3.5.18" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.18.tgz#6a60435d4663e290f3709898a4f75014f279c4d6" @@ -635,7 +639,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.4.0: +ansi-escapes@^1.1.0, ansi-escapes@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= @@ -1587,6 +1591,14 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" +babel-polyfill@6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" + dependencies: + babel-runtime "^6.22.0" + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + babel-preset-env@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" @@ -1788,6 +1800,12 @@ big.js@^3.1.3: resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" integrity sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg= +big.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.1.tgz#0abb06f3acd00509e8385ebe20b1b41419612cb9" + dependencies: + opencollective "^1.0.3" + binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" @@ -2294,6 +2312,10 @@ change-emitter@^0.1.2: resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU= +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + chart.js@^2.7.2: version "2.7.2" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714" @@ -4097,6 +4119,14 @@ extend@~3.0.0, extend@~3.0.1: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= +external-editor@^2.0.1: + version "2.2.0" + resolved "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + external-editor@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" @@ -5285,6 +5315,24 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inquirer@3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.0.6.tgz#e04aaa9d05b7a3cb9b0f407d04375f0447190347" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.1" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^2.0.0" + strip-ansi "^3.0.0" + through "^2.3.6" + inquirer@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -7205,7 +7253,7 @@ minimist@0.0.8, minimist@~0.0.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -7389,6 +7437,13 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +node-fetch@1.6.3: + version "1.6.3" + resolved "http://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -7757,6 +7812,17 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +opencollective@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/opencollective/-/opencollective-1.0.3.tgz#aee6372bc28144583690c3ca8daecfc120dd0ef1" + dependencies: + babel-polyfill "6.23.0" + chalk "1.1.3" + inquirer "3.0.6" + minimist "1.2.0" + node-fetch "1.6.3" + opn "4.0.2" + opn@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" @@ -7832,7 +7898,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -9558,6 +9624,10 @@ regenerate@^1.2.1: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" integrity sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA= +regenerator-runtime@^0.10.0: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + regenerator-runtime@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" @@ -9972,6 +10042,10 @@ rx-lite@*, rx-lite@^4.0.8: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + rxjs@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" @@ -11024,6 +11098,12 @@ tmp@^0.0.31: dependencies: os-tmpdir "~1.0.1" +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"