-
-
-
{data.username}
-
{data.address}
-
{data.address.replace(regex.lskAddressTrunk, '$1...$3')}
+ data, onRowClick, accounts,
+}) => {
+ const onClick = () => onRowClick(data.delegateAddress);
+ return (
+
+
+
+
+
+ {data.delegate.username}
+ {data.delegateAddress}
+
-
-
-
-
- {' '}
- {t('LSK')}
-
-
-
- {data.productivity !== undefined
- ? `${formatAmountBasedOnLocale({ value: data.productivity })}%`
- /* istanbul ignore next */
- : '-'
+
+ {!isEmpty(accounts)
+ ? `${formatAmountBasedOnLocale({ value: accounts[data.delegateAddress].productivity })}%`
+ /* istanbul ignore next */
+ : '-'
+ }
+
+
+
+ {
+ /* istanbul ignore next */
+ !isEmpty(accounts) ? `#${accounts[data.delegateAddress].rank}` : '-'
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ data.pending
+ ?
+ : (
+
+
+
+
+
+ )
}
-
-
-
-
-
-
-);
+ );
+};
/* istanbul ignore next */
const areEqual = (prevProps, nextProps) =>
diff --git a/src/components/screens/wallet/votes/voteRow.test.js b/src/components/screens/wallet/votes/voteRow.test.js
new file mode 100644
index 0000000000..329a45f82d
--- /dev/null
+++ b/src/components/screens/wallet/votes/voteRow.test.js
@@ -0,0 +1,39 @@
+import { mountWithRouter } from '../../../../utils/testHelpers';
+import VoteRow from './voteRow';
+import accounts from '../../../../../test/constants/accounts';
+import Spinner from '../../../toolbox/spinner';
+import DialogLink from '../../../toolbox/dialog/link';
+
+describe('VoteRow Component', () => {
+ let wrapper;
+ const props = {
+ data: {
+ delegateAddress: accounts.delegate.address,
+ delegate: accounts.delegate,
+ },
+ onRowClick: jest.fn(),
+ accounts: {
+ [accounts.delegate.address]: {
+ productivity: 95,
+ rank: 1,
+ totalVotesReceived: 50e8,
+ },
+ },
+ };
+
+ it('should render spinner', () => {
+ wrapper = mountWithRouter(VoteRow, { ...props, data: { ...props.data, pending: {} } });
+ expect(wrapper.contains(Spinner)).toBeTruthy();
+ });
+
+ it('should render edit link', () => {
+ wrapper = mountWithRouter(VoteRow, props);
+ expect(wrapper.find(DialogLink).html()).toContain('editVoteLink');
+ });
+
+ it('should call onRowClick', () => {
+ wrapper = mountWithRouter(VoteRow, props);
+ wrapper.find('.vote-row').childAt(0).simulate('click');
+ expect(props.onRowClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/screens/wallet/votes/votes.css b/src/components/screens/wallet/votes/votes.css
index 6bdb0e6908..2aad5f99aa 100644
--- a/src/components/screens/wallet/votes/votes.css
+++ b/src/components/screens/wallet/votes/votes.css
@@ -9,17 +9,18 @@
}
& .filterHolder {
- width: 200px;
- }
+ display: flex;
- & .lastHeading {
- & :global(.tooltip-window) {
- & :global(.tooltip-arrow) {
- right: 16px;
- left: auto;
- }
+ & .registerDelegate {
+ margin-right: 10px;
}
}
+
+ & .flexRightAlign {
+ display: flex;
+ direction: revert;
+ justify-content: flex-end;
+ }
}
.empty {
@@ -45,6 +46,23 @@
& .votes {
@mixin contentNormal bold;
+
+ color: var(--color-ink-blue);
+ }
+}
+
+.row {
+ & .editVoteLink {
+ padding: 5px 20px;
+ opacity: 0;
+ transition: all ease 200ms;
+ cursor: pointer;
+ }
+
+ &:hover {
+ & .editVoteLink {
+ opacity: 1;
+ }
}
}
@@ -52,7 +70,7 @@
display: flex;
& > .avatar {
- margin-right: 24px;
+ margin-right: 16px;
}
& > .accountInfo {
@@ -61,12 +79,16 @@
justify-content: space-evenly;
}
- & .title {
+ & .username {
@mixin headingSmall;
color: var(--color-maastricht-blue);
display: block;
}
+
+ & .address {
+ color: var(--color-slate-gray);
+ }
}
@media (--small-viewport) {
diff --git a/src/components/screens/wallet/votes/votes.js b/src/components/screens/wallet/votes/votes.js
index 0b020994c0..3a2e0aca04 100644
--- a/src/components/screens/wallet/votes/votes.js
+++ b/src/components/screens/wallet/votes/votes.js
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
-import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import Box from '../../../toolbox/box';
import BoxHeader from '../../../toolbox/box/header';
@@ -10,50 +9,22 @@ import styles from './votes.css';
import Table from '../../../toolbox/table';
import VoteRow from './voteRow';
import header from './votesTableHeader';
+import DialogLink from '../../../toolbox/dialog/link';
+import { SecondaryButton } from '../../../toolbox/buttons';
+
+const getMessages = t => ({
+ all: t('This account doesn’t have any votes.'),
+ filtered: t('This account doesn’t have any votes matching searched username.'),
+});
-// eslint-disable-next-line max-statements
const Votes = ({
- votes, delegates, address, t, history,
+ votes, accounts, address, t, history, hostVotes = {}, isDelegate,
}) => {
- const [tOut, setTout] = useState();
- const [mergedVotes, setMergedVotes] = useState([]);
- const [showing, setShowing] = useState(30);
const [filterValue, setFilterValue] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const { apiVersion } = useSelector(state => state.network.networks.LSK);
- const votesKey = apiVersion === '2' ? 'vote' : 'voteWeight';
-
- const fetchDelegateWhileNeeded = () => {
- const delegatesData = delegates.data;
- const filteredVotes = votes.data.filter(vote => RegExp(filterValue, 'i').test(vote.username));
- const mVotes = filteredVotes.map((vote) => {
- const delegate = delegatesData[vote.username] || {};
- return { ...vote, ...delegate };
- }).sort((a, b) => {
- if (!a[votesKey] && !b[votesKey]) return 0;
- if (!a[votesKey] || +a[votesKey] > +b[votesKey]) return 1;
- return -1;
- });
- if (mVotes.length && !(mVotes.slice(0, showing).slice(-1)[0] || {})[votesKey]) {
- const offset = Object.keys(delegatesData).length;
- delegates.loadData({ offset, limit: 101 });
- }
- setMergedVotes(mVotes);
- };
-
- const onShowMore = () => {
- setShowing(showing + 30);
- };
+ const messages = getMessages(t);
const handleFilter = ({ target }) => {
- clearTimeout(tOut);
setFilterValue(target.value);
- setIsLoading(true);
-
- const timeout = setTimeout(() => {
- setIsLoading(false);
- }, 300);
- setTout(timeout);
};
const onRowClick = (rowAddress) => {
@@ -63,30 +34,38 @@ const Votes = ({
useEffect(() => {
votes.loadData({ address });
- delegates.loadData({ offset: 0, limit: 101 });
-
- return () => clearTimeout(tOut);
- }, []);
-
- useEffect(() => {
- votes.loadData({ address });
- fetchDelegateWhileNeeded();
- }, [address]);
-
- useEffect(() => {
- fetchDelegateWhileNeeded();
- }, [delegates.data, votes.data.length, showing, filterValue]);
+ }, [address, hostVotes]);
+ // @todo uncomment this when Lisk Service API is ready
+ // Fetch delegate profiles to define rank, productivity and delegate weight
+ // useEffect(() => {
+ // if (isEmpty(accounts.data) && votes.data.length) {
+ // const addressList = votes.data.map(vote => vote.delegateAddress);
+ // accounts.loadData({ addressList });
+ // }
+ // }, [votes.data]);
- const filteredVotes = mergedVotes.filter(vote => RegExp(filterValue, 'i').test(vote.username));
- const canLoadMore = filteredVotes.length > showing;
- const areLoading = isLoading || delegates.isLoading || votes.isLoading;
+ const areLoading = accounts.isLoading || votes.isLoading;
+ const filteredVotes = votes.data.filter((vote) => {
+ if (!vote.delegate) return false;
+ return vote.delegate.username.indexOf(filterValue) > -1;
+ });
return (
{t('Voted delegates')}
+ {!isDelegate && (
+
+
+ {t('Register a delegate')}
+
+
+ )}
-
+
diff --git a/src/components/screens/wallet/votes/votes.test.js b/src/components/screens/wallet/votes/votes.test.js
index 3f40ba8505..0c1f96e31f 100644
--- a/src/components/screens/wallet/votes/votes.test.js
+++ b/src/components/screens/wallet/votes/votes.test.js
@@ -5,7 +5,8 @@ import accounts from '../../../../../test/constants/accounts';
import routes from '../../../../constants/routes';
import Votes from './votes';
-describe('Votes Tab Component', () => {
+
+describe.skip('Votes Tab Component', () => {
let wrapper;
const props = {
address: accounts.genesis.address,
@@ -24,7 +25,7 @@ describe('Votes Tab Component', () => {
const network = {
network: {
networks: {
- LSK: { apiVersion: '2' },
+ LSK: { apiVersion: '2' }, // @todo Remove?
},
},
};
@@ -65,7 +66,7 @@ describe('Votes Tab Component', () => {
expect(wrapper).toContainMatchingElements(60, 'VoteRow');
});
- it.skip('Should go to account page on clicking row', () => {
+ it('Should go to account page on clicking row', () => {
const votes = [...Array(101)].map((_, i) => ({
username: `user_${i}`,
address: `${i}L`,
diff --git a/src/components/screens/wallet/votes/votesTableHeader.js b/src/components/screens/wallet/votes/votesTableHeader.js
index d9c1339ef9..979c378493 100644
--- a/src/components/screens/wallet/votes/votesTableHeader.js
+++ b/src/components/screens/wallet/votes/votesTableHeader.js
@@ -1,40 +1,44 @@
import grid from 'flexboxgrid/dist/flexboxgrid.css';
import styles from './votes.css';
-export default (t, apiVersion) => ([
- {
- title: t('Rank'),
- classList: apiVersion === '3' ? 'hidden' : grid['col-sm-1'],
- },
+export default t => ([
{
title: t('Delegate'),
- classList: `${grid['col-sm-3']} ${grid['col-lg-6']}`,
+ classList: grid['col-sm-3'],
},
{
- title: t('Forged'),
+ title: t('Productivity'),
classList: grid['col-sm-2'],
tooltip: {
- title: t('Forged'),
- message: t('Sum of all LSK awarded to a delegate for each block successfully generated on the blockchain.'),
+ title: t('Productivity'),
+ message: t('% of successfully forged blocks in relation to total blocks that were available for this particular delegate to forge'),
position: 'bottom',
},
},
{
- title: t('Productivity'),
- classList: `${grid['col-sm-2']} ${grid[apiVersion === '3' ? 'col-lg-2' : 'col-lg-1']}`,
+ title: t('Rank'),
+ classList: grid['col-sm-2'],
+ },
+ {
+ title: t('Delegate weight'),
+ classList: `${grid['col-sm-2']} ${grid['col-lg-2']}`,
tooltip: {
- title: t('Productivity'),
- message: t('% of successfully forged blocks in relation to total blocks that were available for this particular delegate to forge'),
+ title: t('Delegate weight'),
+ message: t('The total amount of all votes a delegate has received.'),
position: 'bottom',
},
},
{
- title: t('Vote weight'),
- classList: `${grid['col-sm-4']} ${grid['col-lg-2']} ${styles.lastHeading}`,
+ title: t('Vote amount'),
+ classList: `${grid['col-sm-2']} ${grid['col-lg-2']} ${styles.flexRightAlign}`,
tooltip: {
- title: t('Productivity'),
- message: t('Sum of LSK in all accounts who have voted for this delegate.'),
+ title: t('Vote amount'),
+ message: t('The amount of LSK blocked for voting.'),
position: 'left',
},
},
+ {
+ title: t(''),
+ classList: grid['col-sm-1'],
+ },
]);
diff --git a/src/components/shared/accountVisualWithAddress/index.js b/src/components/shared/accountVisualWithAddress/index.js
index 3dedf185dd..c7f4ab8903 100644
--- a/src/components/shared/accountVisualWithAddress/index.js
+++ b/src/components/shared/accountVisualWithAddress/index.js
@@ -9,6 +9,7 @@ import transactionTypes from '../../../constants/transactionTypes';
import AccountVisual from '../../toolbox/accountVisual';
import regex from '../../../utils/regex';
+const sendCodes = transactionTypes().transfer.code;
class AccountVisualWithAddress extends React.Component {
getTransformedAddress(address) {
const { bookmarks, showBookmarkedAddress } = this.props;
@@ -28,12 +29,11 @@ class AccountVisualWithAddress extends React.Component {
address, transactionSubject, transactionType, size,
} = this.props;
const txType = transactionTypes.getByCode(transactionType);
- const sendCode = transactionTypes().send.code;
const transformedAddress = this.getTransformedAddress(address);
return (
- {transactionType !== sendCode && transactionSubject === 'recipientId' ? (
+ {!Object.values(sendCodes).includes(transactionType) && transactionSubject === 'recipientId' ? (
{
+ const setEntireBalance = () => {
+ const value = formatAmountBasedOnLocale({
+ value: fromRawLsk(maxAmount.value),
+ format: '0.[00000000]',
+ });
+ setAmountField({ value }, maxAmount);
+ };
+
+ const handleAmountChange = ({ target }) => {
+ setAmountField(target, maxAmount);
+ };
+
+ return (
+
+ );
+};
+
+export default AmountField;
diff --git a/src/components/shared/avatarWithNameAndAddress/avatarWithNameAndAddress.test.js b/src/components/shared/avatarWithNameAndAddress/avatarWithNameAndAddress.test.js
new file mode 100644
index 0000000000..50ed0a1a6a
--- /dev/null
+++ b/src/components/shared/avatarWithNameAndAddress/avatarWithNameAndAddress.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import AvatarWithNameAndAddress from './index';
+
+describe('AvatarWithNameAndAddress', () => {
+ const props = {
+ username: 'tes_username',
+ account: {
+ address: '1234567890L',
+ },
+ };
+
+ it('should mount an account visual', () => {
+ const wrapper = mount();
+ expect(wrapper.find('AccountVisual')).toHaveLength(1);
+ });
+
+ it('should display the username and address', () => {
+ const wrapper = mount();
+ const html = wrapper.html();
+ expect(html).toContain(props.username);
+ expect(html).toContain(props.account.address);
+ });
+});
diff --git a/src/components/shared/filterDropdownButton/selectFilter.js b/src/components/shared/filterDropdownButton/selectFilter.js
index a0b6f6c1ed..1eef83fef8 100644
--- a/src/components/shared/filterDropdownButton/selectFilter.js
+++ b/src/components/shared/filterDropdownButton/selectFilter.js
@@ -9,7 +9,7 @@ const SelectFilter = ({
}) => {
const txTypes = transactionTypes();
const options = Object.keys(txTypes)
- .map(key => ({ value: txTypes[key].code, label: txTypes[key].title }));
+ .map(key => ({ value: txTypes[key].code.legacy, label: txTypes[key].title }));
options.unshift({ value: '', label: placeholder });
const onChange = (value) => {
diff --git a/src/components/shared/formattedNumber/index.js b/src/components/shared/formattedNumber/index.js
index 5699c214bf..bf0063fa01 100644
--- a/src/components/shared/formattedNumber/index.js
+++ b/src/components/shared/formattedNumber/index.js
@@ -7,8 +7,8 @@ import i18n from '../../../i18n';
const FormattedNumber = ({ val }) => {
// set numeral language
numeral.locale(i18n.language);
- const formatedNumber = numeral(val).format('0,0.[0000000000000]');
- return {formatedNumber};
+ const formattedVal = numeral(val).format('0,0.[0000000000000]');
+ return {formattedVal};
};
export default withTranslation()(FormattedNumber);
diff --git a/src/components/shared/navigationBars/sideBar/constants.js b/src/components/shared/navigationBars/sideBar/constants.js
index de80bf282c..9d03130016 100644
--- a/src/components/shared/navigationBars/sideBar/constants.js
+++ b/src/components/shared/navigationBars/sideBar/constants.js
@@ -14,12 +14,6 @@ const menuLinks = t => ([
label: t('Wallet'),
path: routes.wallet.path,
},
- {
- icon: 'voting',
- id: 'voting',
- label: t('Voting'),
- path: routes.voting.path,
- },
],
[
{
diff --git a/src/components/shared/navigationBars/sideBar/index.test.js b/src/components/shared/navigationBars/sideBar/index.test.js
index 549a7b833b..0f157f0dfa 100644
--- a/src/components/shared/navigationBars/sideBar/index.test.js
+++ b/src/components/shared/navigationBars/sideBar/index.test.js
@@ -47,13 +47,13 @@ describe('SideBar', () => {
wrapper = mountWithRouter(SideBar, myProps);
});
- it('renders 8 menu items elements', () => {
- expect(wrapper).toContainMatchingElements(8, 'a');
+ it('renders 7 menu items elements', () => {
+ expect(wrapper).toContainMatchingElements(7, 'a');
});
- describe('renders 8 menu items', () => {
+ describe('renders 7 menu items', () => {
it('without labels if sideBarExpanded is false', () => {
- expect(wrapper).toContainMatchingElements(8, 'a');
+ expect(wrapper).toContainMatchingElements(7, 'a');
wrapper.find('a').forEach(link => expect(link).not.toContain(/\w*/));
});
@@ -61,7 +61,6 @@ describe('SideBar', () => {
const expectedLinks = [
'Dashboard',
'Wallet',
- 'Voting',
'Network',
'Transactions',
'Blocks',
@@ -75,8 +74,8 @@ describe('SideBar', () => {
});
});
- it('renders 8 menu items but only Wallet is disabled when user is logged out', () => {
- expect(wrapper).toContainMatchingElements(8, 'a');
+ it('renders 7 menu items but only Wallet is disabled when user is logged out', () => {
+ expect(wrapper).toContainMatchingElements(7, 'a');
expect(wrapper).toContainExactlyOneMatchingElement('a.disabled');
expect(wrapper.find('a').at(0)).not.toHaveClassName('disabled');
expect(wrapper.find('a').at(1)).toHaveClassName('disabled');
@@ -85,6 +84,5 @@ describe('SideBar', () => {
expect(wrapper.find('a').at(4)).not.toHaveClassName('disabled');
expect(wrapper.find('a').at(5)).not.toHaveClassName('disabled');
expect(wrapper.find('a').at(6)).not.toHaveClassName('disabled');
- expect(wrapper.find('a').at(7)).not.toHaveClassName('disabled');
});
});
diff --git a/src/components/shared/navigationBars/topBar/index.js b/src/components/shared/navigationBars/topBar/index.js
index bc8177aaf9..47aac8193d 100644
--- a/src/components/shared/navigationBars/topBar/index.js
+++ b/src/components/shared/navigationBars/topBar/index.js
@@ -12,6 +12,9 @@ const mapStateToProps = state => ({
network: state.network,
token: state.settings.token,
settings: state.settings,
+ noOfVotes: Object.values(state.voting)
+ .filter(vote => (vote.confirmed !== vote.unconfirmed))
+ .length,
});
const mapDispatchToProps = {
diff --git a/src/components/shared/navigationBars/topBar/topBar.css b/src/components/shared/navigationBars/topBar/topBar.css
index 6d32d6d6de..80549234c3 100644
--- a/src/components/shared/navigationBars/topBar/topBar.css
+++ b/src/components/shared/navigationBars/topBar/topBar.css
@@ -32,7 +32,7 @@
display: flex;
align-items: center;
- &.disabled {
+ &.opaqueLogo {
opacity: 0.5;
}
@@ -76,8 +76,34 @@
margin-right: 16px;
}
-@media (--medium-viewport) {
- .toggle {
- margin-right: 10px;
+.votingQueueVoteCount {
+ @mixin contentSmall;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ position: absolute;
+ top: 6px;
+ left: 17px;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ width: 16px;
+ height: 16px;
+ background-color: var(--color-jade-green);
+}
+
+.votingQueueIcon {
+ display: flex;
+ justify-content: center;
+}
+
+.disabled {
+ pointer-events: none;
+ cursor: default;
+
+ & .label,
+ & img {
+ opacity: 0.5;
}
}
diff --git a/src/components/shared/navigationBars/topBar/topBar.js b/src/components/shared/navigationBars/topBar/topBar.js
index 301fbc0297..7a6380391b 100644
--- a/src/components/shared/navigationBars/topBar/topBar.js
+++ b/src/components/shared/navigationBars/topBar/topBar.js
@@ -91,7 +91,7 @@ const TokenSelector = ({ token, history, t }) => {
content={(
)}
@@ -129,6 +129,7 @@ class TopBar extends React.Component {
network,
token,
settings: { darkMode, discreetMode, sideBarExpanded },
+ noOfVotes,
// resetTimer,
} = this.props;
// const isSearchActive = (this.childRef && this.childRef.state.shownDropdown) || false;
@@ -171,6 +172,22 @@ class TopBar extends React.Component {
>
{t('Bookmarks')}
+
+
+
+
+ {noOfVotes !== 0
+ && {noOfVotes}}
+
+ )}
+ >
+ {t('Voting Queue')}
+
{
- const apiVersion = useSelector(state => state.network.networks.LSK.apiVersion);
- return (
- (apiVersion === '2')
- ? (
-
- {`#${data.rank || '-'}`}
-
- )
- : (
-
- {'active'}
-
- )
- );
-};
+const RankOrStatus = ({
+ data,
+ className,
+}) => (
+ data ? (
+
+ {`#${data.rank || '-'}`}
+
+ ) : (
+
+ {'active'}
+
+ )
+);
export default RankOrStatus;
diff --git a/src/components/shared/searchBar/delegates.js b/src/components/shared/searchBar/delegates.js
index 1f1910aa3f..7d160ca62b 100644
--- a/src/components/shared/searchBar/delegates.js
+++ b/src/components/shared/searchBar/delegates.js
@@ -17,10 +17,10 @@ const Delegates = ({
key={index}
data-index={index}
className={`${styles.accountRow} ${rowItemIndex === index ? styles.active : ''} delegates-row`}
- onClick={() => onSelectedRow(delegate.account.address)}
+ onClick={() => onSelectedRow(delegate.address)}
onMouseEnter={updateRowItemIndex}
>
-
+
@@ -32,7 +32,7 @@ const Delegates = ({
/>
-
{delegate.account.address}
+
{delegate.address}
diff --git a/src/components/shared/searchBar/transactions.js b/src/components/shared/searchBar/transactions.js
index b0a0c38d3f..a6cf6ba235 100644
--- a/src/components/shared/searchBar/transactions.js
+++ b/src/components/shared/searchBar/transactions.js
@@ -9,7 +9,7 @@ const Transactions = ({
}) => {
function selectTransactionType() {
return {
- [transactionTypes().send.code]: {
+ [transactionTypes().transfer.code]: {
subTitle: t('Amount'),
value: transactions[0].amount,
},
diff --git a/src/components/shared/searchBar/transactions.test.js b/src/components/shared/searchBar/transactions.test.js
index a2d1fe6846..de7fdffbce 100644
--- a/src/components/shared/searchBar/transactions.test.js
+++ b/src/components/shared/searchBar/transactions.test.js
@@ -21,7 +21,7 @@ describe('Transactions', () => {
network: {
networks: {
LSK: {
- apiVersion: 2,
+ apiVersion: 2, // @todo remove?
},
},
},
diff --git a/src/components/shared/transactionAddress/index.js b/src/components/shared/transactionAddress/index.js
index b6ddd2a3e5..cbca74b1b4 100644
--- a/src/components/shared/transactionAddress/index.js
+++ b/src/components/shared/transactionAddress/index.js
@@ -1,32 +1,40 @@
import React from 'react';
-import { tokenMap } from '../../../constants/tokens';
-import regex from '../../../utils/regex';
+
import transactionTypes from '../../../constants/transactionTypes';
import styles from './transactionAddress.css';
+import { truncateAddress } from '../../../utils/account';
-const TransactionAddress = ({
- address, bookmarks, transactionType, token,
+const Address = ({
+ bookmark, address, className,
}) => {
- const account = [...bookmarks.LSK, ...bookmarks.BTC].filter(acc => acc.address === address);
+ const addressTrunk = address && truncateAddress(address);
- const formatter = (token === tokenMap.LSK.key)
- ? value => value
- : (value => value.replace(regex.btcAddressTrunk, '$1...$3'));
+ if (bookmark) return ({bookmark.title});
+ return (
+ <>
+
+ {address.length < 24 ? address : addressTrunk}
+
+
+ {addressTrunk}
+
+ >
+ );
+};
- const renderAddress = () => (account.length ? account[0].title : formatter(address));
+const TransactionAddress = ({
+ address, bookmarks, transactionType, token,
+}) => {
+ const bookmark = bookmarks[token].find(acc => acc.address === address);
return (
-
- {transactionType !== transactionTypes().send.code
- ? transactionTypes.getByCode(transactionType).title
- : renderAddress()}
-
- {account.length ? (
-
- {token === tokenMap.LSK.key ? address : address.replace(regex.btcAddressTrunk, '$1...$3')}
-
- ) : null}
+ {
+ transactionType !== transactionTypes().transfer.code.legacy
+ ?
{transactionTypes.getByCode(transactionType).title}
+ :
+ }
+ {bookmark &&
}
);
};
diff --git a/src/components/shared/transactionAmount/index.js b/src/components/shared/transactionAmount/index.js
index 2971bbe779..c381dcb6b7 100644
--- a/src/components/shared/transactionAmount/index.js
+++ b/src/components/shared/transactionAmount/index.js
@@ -6,12 +6,14 @@ import styles from './transactionAmount.css';
import transactionTypes from '../../../constants/transactionTypes';
const TransactionAmount = ({
- sender, recipient, type, token, showRounded, showInt, host, amount,
+ recipient, type, token, showRounded, showInt, host, amount,
}) => {
- const isIncoming = host === recipient && sender !== recipient;
+ const isIncoming = host === recipient
+ || type === transactionTypes().unlockToken.code.legacy;
return (
- { type === transactionTypes().send.code
+ { type === transactionTypes().transfer.code.legacy
+ || type === transactionTypes().unlockToken.code.legacy
? (
@@ -33,7 +35,6 @@ const TransactionAmount = ({
TransactionAmount.propTypes = {
host: PropTypes.string,
- sender: PropTypes.string.isRequired,
recipient: PropTypes.string,
token: PropTypes.string.isRequired,
type: PropTypes.number.isRequired,
diff --git a/src/components/shared/transactionPriority/index.js b/src/components/shared/transactionPriority/index.js
new file mode 100644
index 0000000000..51ca87dad6
--- /dev/null
+++ b/src/components/shared/transactionPriority/index.js
@@ -0,0 +1,4 @@
+import { withTranslation } from 'react-i18next';
+import TransactionPriority from './transactionPriority';
+
+export default withTranslation()(TransactionPriority);
diff --git a/src/components/shared/transactionPriority/transactionPriority.css b/src/components/shared/transactionPriority/transactionPriority.css
new file mode 100644
index 0000000000..a2f1c443ce
--- /dev/null
+++ b/src/components/shared/transactionPriority/transactionPriority.css
@@ -0,0 +1,86 @@
+@import '../../../app/mixins.css';
+
+.fieldGroup {
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 16px;
+
+ &.wrapper {
+ flex-direction: row;
+ }
+}
+
+.fieldLabel {
+ @mixin contentNormal bold;
+
+ align-items: center;
+ color: var(--color-maastricht-blue);
+ display: flex;
+ margin-bottom: 8px;
+
+ & .loading {
+ color: var(--color-ultramarine-blue);
+ margin-left: 5px;
+ }
+}
+
+.wrapper {
+ & > .col:first-child {
+ padding-right: 100px;
+ }
+
+ & > .col {
+ display: flex;
+ flex-direction: column;
+ flex-basis: 50%;
+ justify-content: space-between;
+ height: 74px;
+
+ & > .fieldLabel {
+ margin-bottom: 14px;
+ }
+
+ & > .prioritySelector {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 4px;
+
+ & > .priorityTitle {
+ background-color: var(--color-white);
+ color: var(--color-maastricht-blue);
+ font-family: var(--content-font);
+ font-size: 14px;
+ line-height: 18px;
+ border: 1px solid var(--color-periwinkle-blue);
+ border-radius: 18px;
+ margin-right: 5px;
+ padding: 5px var(--horizontal-padding-m);
+ outline: none;
+
+ &:disabled {
+ opacity: 0;
+ }
+
+ &.priorityTitleSelected {
+ background-color: var(--color-ink-blue);
+ color: var(--color-white);
+ }
+ }
+ }
+
+ & > .feeValue {
+ color: var(--color-maastricht-blue);
+ font-family: var(--content-font);
+ font-weight: normal;
+ font-size: var(--font-size-h6);
+ margin-bottom: 10px;
+ display: flex;
+ align-items: flex-end;
+
+ & > img {
+ margin-left: 10px;
+ }
+ }
+ }
+}
diff --git a/src/components/shared/transactionPriority/transactionPriority.js b/src/components/shared/transactionPriority/transactionPriority.js
new file mode 100644
index 0000000000..316d5fd054
--- /dev/null
+++ b/src/components/shared/transactionPriority/transactionPriority.js
@@ -0,0 +1,224 @@
+import React, { useState, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import styles from './transactionPriority.css';
+import { tokenMap } from '../../../constants/tokens';
+import Input from '../../toolbox/inputs/input';
+import Icon from '../../toolbox/icon';
+import Tooltip from '../../toolbox/tooltip/tooltip';
+import Spinner from '../../toolbox/spinner';
+import {
+ formatAmountBasedOnLocale,
+} from '../../../utils/formattedNumber';
+import { toRawLsk, fromRawLsk } from '../../../utils/lsk';
+import transactionTypes from '../../../constants/transactionTypes';
+
+const CUSTOM_FEE_INDEX = 3;
+
+const getFeeStatus = ({ fee, token, customFee }) => {
+ if (customFee) {
+ return customFee;
+ }
+ return !fee.error
+ ? `${formatAmountBasedOnLocale({ value: fee.value })} ${token}`
+ : fee.feedback;
+};
+
+const getRelevantPriorityOptions = (options, token) =>
+ options.filter((_, index) =>
+ index !== CUSTOM_FEE_INDEX
+ || (index === CUSTOM_FEE_INDEX && token === tokenMap.LSK.key));
+
+const isCustomFeeValid = (value, hardCap, minFee) => {
+ if (!value) return false;
+ const rawValue = toRawLsk(parseFloat(value));
+
+ if (rawValue > hardCap) {
+ return false;
+ }
+
+ if (rawValue < toRawLsk(minFee)) {
+ return false;
+ }
+
+ return true;
+};
+
+// eslint-disable-next-line max-statements
+const TransactionPriority = ({
+ t,
+ token,
+ priorityOptions,
+ selectedPriority,
+ setSelectedPriority,
+ fee,
+ minFee,
+ customFee,
+ setCustomFee,
+ txType,
+ className,
+ loadError,
+ isLoading,
+}) => {
+ const [showEditIcon, setShowEditIcon] = useState(false);
+ const [inputValue, setInputValue] = useState();
+
+ let hardCap = 0;
+ if (token === tokenMap.LSK.key) {
+ hardCap = transactionTypes.getHardCap(txType);
+ }
+
+ const onClickPriority = (e) => {
+ e.preventDefault();
+ const selectedIndex = Number(e.target.value);
+ if (setCustomFee && selectedIndex !== CUSTOM_FEE_INDEX) {
+ setCustomFee(undefined);
+ setInputValue(undefined);
+ }
+ setSelectedPriority({ item: priorityOptions[selectedIndex], index: selectedIndex });
+ if (showEditIcon) {
+ setShowEditIcon(false);
+ }
+ };
+
+ const onInputChange = (e) => {
+ e.preventDefault();
+ const newValue = e.target.value;
+ if (token === tokenMap.LSK.key) {
+ setInputValue(newValue);
+ if (isCustomFeeValid(newValue, hardCap, minFee)) {
+ setCustomFee({ value: newValue, feedback: '', error: false });
+ } else {
+ setCustomFee({ value: undefined, feedback: 'invalid custom fee', error: true });
+ }
+ } else {
+ setCustomFee(newValue);
+ }
+ };
+
+ const onInputFocus = (e) => {
+ e.preventDefault();
+ if (!inputValue) setInputValue(minFee);
+ };
+
+ const onInputBlur = (e) => {
+ e.preventDefault();
+ setShowEditIcon(true);
+ };
+
+ const onClickCustomEdit = (e) => {
+ e.preventDefault();
+ setShowEditIcon(false);
+ };
+
+ const tokenRelevantPriorities = useMemo(() =>
+ getRelevantPriorityOptions(priorityOptions, token),
+ [priorityOptions, token]);
+
+ const isCustom = selectedPriority === CUSTOM_FEE_INDEX;
+
+ return (
+
+
+
+ {t('Priority')}
+
+
+ {
+ t('When the network is busy, transactions with higher priority get processed sooner.')
+ }
+
+
+
+
+ {tokenRelevantPriorities.map((priority, index) => {
+ let disabled = false;
+ if (index === 0) {
+ priority.title = priority.value === 0 ? 'Normal' : 'Low';
+ } else if (index === 3) {
+ disabled = priority.value === 0 && !loadError; // Custom fee option
+ } else {
+ disabled = priority.value === 0 || loadError; // Medium and high fee option
+ }
+ return (
+
+ );
+ })}
+
+
+
+
+ {t('Transaction fee')}
+
+
+ {
+ t(`
+ You can choose a high, medium, or low transaction priority, each requiring a
+ corresponding transaction fee. Or you can pay any desired fee of no less than
+ ${minFee} ${token}. If you don't know which to choose, we recommend you choose
+ one of the provided options instead.
+ `)
+ }
+
+
+
+ {
+ // eslint-disable-next-line no-nested-ternary
+ isLoading ? (
+ <>
+ {t('Loading')}
+ {' '}
+
+ >
+ )
+ : (isCustom && !showEditIcon ? (
+
+ ) : (
+
+ {getFeeStatus({ fee, token, customFee })}
+ {isCustom && showEditIcon && }
+
+ ))
+ }
+
+
+ );
+};
+
+TransactionPriority.defaultProps = {
+ t: k => k,
+ priorityOptions: [],
+};
+
+TransactionPriority.propTypes = {
+ t: PropTypes.func.isRequired,
+ token: PropTypes.string,
+ priorityOptions: PropTypes.array.isRequired,
+ selectedPriority: PropTypes.number,
+ setSelectedPriority: PropTypes.func,
+ fee: PropTypes.object,
+ customFee: PropTypes.number,
+ minFee: PropTypes.number,
+ txType: PropTypes.string,
+ className: PropTypes.string,
+};
+
+export default TransactionPriority;
diff --git a/src/components/shared/transactionPriority/transactionPriority.test.js b/src/components/shared/transactionPriority/transactionPriority.test.js
new file mode 100644
index 0000000000..c6376026b8
--- /dev/null
+++ b/src/components/shared/transactionPriority/transactionPriority.test.js
@@ -0,0 +1,143 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { tokenMap } from '../../../constants/tokens';
+import TransactionPriority from '.';
+import transactionTypes from '../../../constants/transactionTypes';
+
+const baseFees = {
+ Low: 100,
+ Medium: 1000,
+ High: 2000,
+};
+
+describe('TransactionPriority', () => {
+ let wrapper;
+ const fee = 0.123;
+
+ const props = {
+ t: str => str,
+ token: tokenMap.BTC.key,
+ priorityOptions: [{ title: 'Low', value: baseFees.Low },
+ { title: 'Medium', value: baseFees.Medium },
+ { title: 'High', value: baseFees.High },
+ { title: 'Custom', value: baseFees.Low }],
+ selectedPriority: 0,
+ setSelectedPriority: jest.fn(),
+ fee,
+ setCustomFee: jest.fn(),
+ txType: transactionTypes().transfer.key,
+ loadError: false,
+ isloading: false,
+ };
+ beforeEach(() => {
+ props.setSelectedPriority.mockRestore();
+ props.setCustomFee.mockRestore();
+ wrapper = mount();
+ });
+
+ it('renders properly', () => {
+ expect(wrapper).toContainMatchingElement('.transaction-priority');
+ expect(wrapper).toContainMatchingElement('.priority-selector');
+ expect(wrapper).toContainMatchingElement('.fee-container');
+ });
+
+ it('renders low, medium and high processing speed options', () => {
+ expect(wrapper).toContainMatchingElement('.option-Low');
+ expect(wrapper).toContainMatchingElement('.option-Medium');
+ expect(wrapper).toContainMatchingElement('.option-High');
+ });
+
+ it('renders custom fee option only when props.token is lsk', () => {
+ expect(wrapper).not.toContainMatchingElement('.option-Custom');
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key });
+ expect(wrapper).toContainMatchingElement('.option-Custom');
+ });
+
+ it('renders custom fee option with input when props.token is lsk', () => {
+ expect(wrapper).not.toContainMatchingElement('.custom-fee-input');
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, selectedPriority: 3 });
+ wrapper.find('.option-Custom').simulate('click');
+ expect(props.setSelectedPriority).toHaveBeenCalledTimes(1);
+ expect(wrapper).toContainMatchingElement('.custom-fee-input');
+ });
+
+ it('when typed in custom fee input, the custom fee cb is called', () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, selectedPriority: 3 });
+ wrapper.find('.custom-fee-input').at(1).simulate('change', { target: { value: '0.20' } });
+ expect(props.setCustomFee).toHaveBeenCalledTimes(1);
+ });
+
+ it('hides the edit icon and shows the input when clicked in the "fee-value" element', () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, selectedPriority: 3 });
+ // simulate blur so that the edit icon is shown
+ wrapper.find('.custom-fee-input').at(1).simulate('blur');
+ wrapper.find('span.fee-value').simulate('click');
+ expect(wrapper).not.toContainMatchingElement('Icon[name="edit"]');
+ expect(wrapper).toContainMatchingElement('.custom-fee-input');
+ });
+
+ it('should disable button when fees are 0', () => {
+ wrapper.setProps({
+ ...props,
+ token: tokenMap.LSK.key,
+ priorityOptions: [{ title: 'Low', value: 0 },
+ { title: 'Medium', value: 0 },
+ { title: 'High', value: 0 },
+ { title: 'Custom', value: 0 }],
+ });
+ expect(wrapper.find('.option-Medium')).toBeDisabled();
+ expect(wrapper.find('.option-High')).toBeDisabled();
+ expect(wrapper.find('.option-Custom')).toBeDisabled();
+ });
+
+ it('Options buttons should be enabled/disabled correctly with loading lsk tx fee had an error', () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, loadError: 'Error' });
+ expect(wrapper.find('.option-Medium')).toBeDisabled();
+ expect(wrapper.find('.option-High')).toBeDisabled();
+ expect(wrapper.find('.option-Custom')).not.toBeDisabled();
+ expect(wrapper.find('.option-Low')).not.toBeDisabled();
+ });
+
+ it('Should enable all priority options', () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key });
+ expect(wrapper.find('.option-Low')).not.toBeDisabled();
+ expect(wrapper.find('.option-Medium')).not.toBeDisabled();
+ expect(wrapper.find('.option-High')).not.toBeDisabled();
+ expect(wrapper.find('.option-Custom')).not.toBeDisabled();
+ });
+
+ it('Should disable confirmation button when fee is higher than hard cap', async () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, selectedPriority: 3 });
+ wrapper.find('.custom-fee-input').at(1).simulate('change', { target: { name: 'amount', value: '0.5' } });
+ expect(props.setCustomFee).toHaveBeenCalledWith({
+ error: true,
+ feedback: 'invalid custom fee',
+ value: undefined,
+ });
+ });
+
+ it('Should disable confirmation button when fee is less than the minimum', async () => {
+ wrapper.setProps({
+ ...props,
+ token: tokenMap.LSK.key,
+ selectedPriority: 3,
+ minFee: 1000,
+ });
+ wrapper.find('.custom-fee-input').at(1).simulate('change', { target: { name: 'amount', value: '0.00000000001' } });
+ expect(props.setCustomFee).toHaveBeenCalledWith({
+ error: true,
+ feedback: 'invalid custom fee',
+ value: undefined,
+ });
+ });
+
+ it('Should enable confirmation button when fee is within bounds', async () => {
+ wrapper.setProps({ ...props, token: tokenMap.LSK.key, selectedPriority: 3 });
+ wrapper.find('.custom-fee-input').at(1).simulate('change', { target: { name: 'amount', value: '0.019' } });
+ expect(props.setCustomFee).toHaveBeenCalledWith({
+ error: false,
+ feedback: '',
+ value: '0.019',
+ });
+ });
+});
diff --git a/src/components/shared/transactionSummary/index.js b/src/components/shared/transactionSummary/index.js
index cff9377c4f..9e25ef63b7 100644
--- a/src/components/shared/transactionSummary/index.js
+++ b/src/components/shared/transactionSummary/index.js
@@ -1,5 +1,4 @@
import React from 'react';
-import { connect } from 'react-redux';
import { PrimaryButton, SecondaryButton } from '../../toolbox/buttons';
import { extractPublicKey } from '../../../utils/account';
import Box from '../../toolbox/box';
@@ -51,7 +50,7 @@ class TransactionSummary extends React.Component {
checkSecondPassphrase(passphrase, error) {
const { account, t } = this.props;
- const expectedPublicKey = !error && extractPublicKey(passphrase, this.props.apiVersion);
+ const expectedPublicKey = !error && extractPublicKey(passphrase);
const isPassphraseValid = account.secondPublicKey === expectedPublicKey;
const feedback = !error && !isPassphraseValid ? t('Oops! Wrong passphrase') : '';
@@ -204,8 +203,4 @@ TransactionSummary.defaultProps = {
token: 'LSK',
};
-const mapStateToProps = state => ({
- apiVersion: state.network.networks.LSK.apiVersion,
-});
-
-export default connect(mapStateToProps)(TransactionSummary);
+export default TransactionSummary;
diff --git a/src/components/shared/transactionTypeFigure/index.js b/src/components/shared/transactionTypeFigure/index.js
index a35e877816..c167fd14c9 100644
--- a/src/components/shared/transactionTypeFigure/index.js
+++ b/src/components/shared/transactionTypeFigure/index.js
@@ -19,7 +19,7 @@ const TransactionTypeFigure = ({
{ icon ? : null }
{
- transactionType === transactionTypes().send.code
+ transactionType === transactionTypes().transfer.code.legacy
? renderAvatar()
:
}
diff --git a/src/components/shared/voteItem/index.js b/src/components/shared/voteItem/index.js
new file mode 100644
index 0000000000..629414a170
--- /dev/null
+++ b/src/components/shared/voteItem/index.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import routes from '../../../constants/routes';
+
+import { truncateAddress } from '../../../utils/account';
+import { tokenMap } from '../../../constants/tokens';
+import LiskAmount from '../liskAmount';
+
+import styles from './styles.css';
+
+const token = tokenMap.LSK.key;
+
+/**
+ * Displays address/delegate username along with vote amount
+ *
+ * @param {Object} vote object containing either or both the confirmed and unconfirmed
+ * vote amount values
+ * @param {String} address the address to redirect to, also used as primary text if
+ * title is not defined
+ * @param {String} title text to use instead of the address e.g. delegate username
+ * @param {Boolean} truncate text to use instead of the address e.g. delegate username
+ */
+const VoteItem = ({
+ vote, address, title, truncate,
+}) => {
+ const accountPath = routes.account.path;
+ return (
+
+
+
+ {title || (truncate ? truncateAddress(address) : address)}
+
+
+
+ {Object.values(vote).length === 2
+ ? (
+ <>
+
+ ➞
+
+ >
+ )
+ :
+ }
+
+
+ );
+};
+
+export default VoteItem;
diff --git a/src/components/shared/voteItem/styles.css b/src/components/shared/voteItem/styles.css
new file mode 100644
index 0000000000..487b3c53d1
--- /dev/null
+++ b/src/components/shared/voteItem/styles.css
@@ -0,0 +1,31 @@
+@import '../../../app/mixins.css';
+
+.container {
+ @mixin contentNormal;
+
+ background-color: var(--color-vote-item-bg);
+ padding: 8px;
+ border-radius: 3px;
+ margin: 10px 10px 0 0;
+ white-space: nowrap;
+
+ & a {
+ text-decoration: none;
+ }
+}
+
+.primaryText {
+ @mixin contentNormal semi-bold;
+
+ color: var(--color-ink-blue);
+ margin-right: 10px;
+}
+
+.value {
+ color: var(--color-maastricht-blue);
+}
+
+.arrowIcon {
+ margin: 0 6px;
+ color: var(--color-maastricht-blue);
+}
diff --git a/src/components/shared/voteWeight/index.js b/src/components/shared/voteWeight/index.js
index 17865f5486..d08fa13bb0 100644
--- a/src/components/shared/voteWeight/index.js
+++ b/src/components/shared/voteWeight/index.js
@@ -1,23 +1,18 @@
import React from 'react';
-import { useSelector } from 'react-redux';
import LiskAmount from '../liskAmount';
import { tokenMap } from '../../../constants/tokens';
/**
* This component acts as an adapter for diversions in consecutive versions of API
* @param {Object} data The delegate information
*/
-const VoteWeight = ({ data }) => {
- const apiVersion = useSelector(state => state.network.networks.LSK.apiVersion);
-
- return (
-
-
-
- );
-};
+const VoteWeight = ({ data }) => (
+
+
+
+);
export default VoteWeight;
diff --git a/src/components/shared/walletDetails/walletDetails.js b/src/components/shared/walletDetails/walletDetails.js
index f0a578c299..7246d071ad 100644
--- a/src/components/shared/walletDetails/walletDetails.js
+++ b/src/components/shared/walletDetails/walletDetails.js
@@ -33,9 +33,7 @@ const MyAccount = ({
{t('{{token}} balance', { token: tokenMap[coin.token].label })}
-
- {' '}
- {coin.token}
+
diff --git a/src/components/toolbox/accountVisual/index.test.js b/src/components/toolbox/accountVisual/index.test.js
index 942cbf7c87..d1021d8ae4 100644
--- a/src/components/toolbox/accountVisual/index.test.js
+++ b/src/components/toolbox/accountVisual/index.test.js
@@ -5,7 +5,7 @@ import accounts from '../../../../test/constants/accounts';
describe('AccountVisual', () => {
it('should create account visual of an address', () => {
- const wrapper = mount();
+ const wrapper = mount();
// should render an svg element
expect(wrapper.find('svg')).toHaveLength(1);
diff --git a/src/components/toolbox/buttons/css/base.css b/src/components/toolbox/buttons/css/base.css
index dc04e4f1b8..6d3808b56e 100644
--- a/src/components/toolbox/buttons/css/base.css
+++ b/src/components/toolbox/buttons/css/base.css
@@ -108,9 +108,9 @@
}
@define-mixin buttonWarning {
- background-color: var(--color-burnt-sienna);
- color: var(--color-strong-white);
- border: none;
+ background-color: var(--color-white);
+ color: var(--color-burnt-sienna);
+ border-color: var(--color-burnt-sienna);
&:focus,
&:hover {
diff --git a/src/components/toolbox/copyToClipboard/copyToClipboard.css b/src/components/toolbox/copyToClipboard/copyToClipboard.css
index 1c59289ce5..ac532730f5 100644
--- a/src/components/toolbox/copyToClipboard/copyToClipboard.css
+++ b/src/components/toolbox/copyToClipboard/copyToClipboard.css
@@ -8,4 +8,8 @@
&:hover {
filter: grayscale(0) brightness(100%);
}
+
+ & + span {
+ padding-left: 5px;
+ }
}
diff --git a/src/components/toolbox/copyToClipboard/index.js b/src/components/toolbox/copyToClipboard/index.js
index 21912b5e77..b147457784 100644
--- a/src/components/toolbox/copyToClipboard/index.js
+++ b/src/components/toolbox/copyToClipboard/index.js
@@ -13,15 +13,16 @@ const IconAndText = ({
{copied ? (
+ {' '}
{t('Copied')}
) : (
-
{text || value}
{' '}
+
)}
diff --git a/src/components/toolbox/copyToClipboard/index.test.js b/src/components/toolbox/copyToClipboard/index.test.js
index 1ebc33561c..f9a6c59361 100644
--- a/src/components/toolbox/copyToClipboard/index.test.js
+++ b/src/components/toolbox/copyToClipboard/index.test.js
@@ -18,13 +18,13 @@ describe('CopyToClipboard', () => {
it('should show "Copied!" on click', () => {
wrapper.find('.default').simulate('click');
- expect(wrapper.find('.copied').text()).toEqual(copiedText);
+ expect(wrapper.find('.copied').text().trim()).toEqual(copiedText);
});
it('should hide "Copied!" after 3000ms', () => {
wrapper.find('.default').simulate('click');
jest.advanceTimersByTime(2900);
- expect(wrapper.find('.copied').text()).toEqual(copiedText);
+ expect(wrapper.find('.copied').text().trim()).toEqual(copiedText);
jest.advanceTimersByTime(2000);
wrapper.update();
expect(wrapper.find('.copied')).toHaveLength(0);
diff --git a/src/components/toolbox/icon/index.js b/src/components/toolbox/icon/index.js
index 01c764e208..70377a4442 100644
--- a/src/components/toolbox/icon/index.js
+++ b/src/components/toolbox/icon/index.js
@@ -125,6 +125,21 @@ import totalBlocks from '../../../assets/images/icons/total-blocks.svg';
import blocksForged from '../../../assets/images/icons/blocks-forged.svg';
import distribution from '../../../assets/images/icons/distribution.svg';
import clock from '../../../assets/images/icons/clock.svg';
+import clockActive from '../../../assets/images/icons/clock-active.svg';
+import star from '../../../assets/images/icons/star.svg';
+import calendar from '../../../assets/images/icons/calendar.svg';
+import weight from '../../../assets/images/icons/weight.svg';
+import forgedLsk from '../../../assets/images/icons/forged-lsk.svg';
+import productivity from '../../../assets/images/icons/productivity.svg';
+import missedBlocks from '../../../assets/images/icons/missed-blocks.svg';
+import forgedBlocks from '../../../assets/images/icons/forged-blocks.svg';
+import lock from '../../../assets/images/icons/lock.svg';
+import unlock from '../../../assets/images/icons/unlock.svg';
+import loading from '../../../assets/images/icons/loading.svg';
+import txUnlock from '../../../assets/images/icons/tx-unlock.svg';
+import votingQueueInactive from '../../../assets/images/icons/voting-queue-inactive.svg';
+import votingQueueActive from '../../../assets/images/icons/voting-queue-active.svg';
+import deleteIcon from '../../../assets/images/icons/delete.svg';
export const icons = {
academy,
@@ -139,11 +154,13 @@ export const icons = {
balance,
bookmarksIconEmptyState,
btcIcon,
+ calendar,
checkboxFilled,
checkmark,
copy,
dashboardIcon,
dashboardIconActive,
+ deleteIcon,
discord,
discordActive,
discreetMode,
@@ -151,6 +168,8 @@ export const icons = {
feedback,
feedbackActive,
fileOutline,
+ forgedBlocks,
+ forgedLsk,
help,
helpActive,
helpCenter,
@@ -183,6 +202,7 @@ export const icons = {
showPassphraseIcon,
signIn,
signInActive,
+ star,
tooltipQuestionMark,
transactionApproved,
transactionError,
@@ -221,6 +241,7 @@ export const icons = {
iconEmptyRecentTransactionsDark,
bookmarksIconEmptyStateDark,
multiSignature,
+ missedBlocks,
newsFeedBlog,
newsFeedBlogDark,
bookmark,
@@ -250,7 +271,16 @@ export const icons = {
blocksForged,
distribution,
clock,
+ clockActive,
searchInput,
+ weight,
+ productivity,
+ lock,
+ unlock,
+ loading,
+ txUnlock,
+ votingQueueInactive,
+ votingQueueActive,
};
const Icon = ({ name, noTheme, ...props }) => {
diff --git a/src/components/toolbox/table/table.test.js b/src/components/toolbox/table/table.test.js
index 21e5642e46..e809ef8bd2 100644
--- a/src/components/toolbox/table/table.test.js
+++ b/src/components/toolbox/table/table.test.js
@@ -1,6 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
import Table from './index';
+import accounts from '../../../../test/constants/accounts';
describe('Table', () => {
describe('Loading', () => {
@@ -49,4 +50,28 @@ describe('Table', () => {
expect(wrapper).toHaveText('custom_empty_template');
});
});
+
+ describe('List', () => {
+ const Row = ({ data }) => {data.address}
;
+ const props = {
+ data: Object.keys(accounts).filter(key => key !== 'any account').map(key => accounts[key]),
+ canLoadMore: false,
+ isLoading: false,
+ row: Row,
+ header: [{
+ title: 'Header Item',
+ classList: 'header-item',
+ }],
+ };
+
+ it('should render the data array in rows and use index for iteration key by default', () => {
+ const wrapper = mount();
+ expect(wrapper.find('Row')).toHaveLength(props.data.length);
+ });
+ it('should render accept function to define iteration key', () => {
+ const iterationKey = jest.fn().mockImplementation(data => data.address);
+ mount();
+ expect(iterationKey.mock.calls.length).toBe(props.data.length);
+ });
+ });
});
diff --git a/src/components/toolbox/timestamp/index.js b/src/components/toolbox/timestamp/index.js
index 3f7b5362c5..e3c47825ab 100644
--- a/src/components/toolbox/timestamp/index.js
+++ b/src/components/toolbox/timestamp/index.js
@@ -36,6 +36,7 @@ export const DateTimeFromTimestamp = withTranslation()((props) => {
{
props.fulltime ? (
+ /* istanbul ignore next */
datetime.format('DD MMM YYYY, hh:mm:ss A')
)
: datetime.calendar(null, {
diff --git a/src/components/toolbox/tooltip/tooltip.css b/src/components/toolbox/tooltip/tooltip.css
index 7948f02743..7df72ba268 100644
--- a/src/components/toolbox/tooltip/tooltip.css
+++ b/src/components/toolbox/tooltip/tooltip.css
@@ -63,14 +63,20 @@
& .tooltipArrow {
left: calc(50% - 6px);
+ bottom: -20px;
+ transform: rotate(-90deg);
}
&.right {
left: 0;
- transform: translateY(0) translateX(-10%);
+ transform: translateY(0) translateX(0);
+
+ &.indent {
+ left: -40px;
+ }
& .tooltipArrow {
- right: 40px;
+ left: 40px;
right: auto;
}
}
diff --git a/src/components/toolbox/tooltip/tooltip.js b/src/components/toolbox/tooltip/tooltip.js
index d40a761261..d113f5dce0 100644
--- a/src/components/toolbox/tooltip/tooltip.js
+++ b/src/components/toolbox/tooltip/tooltip.js
@@ -125,7 +125,7 @@ Tooltip.propTypes = {
footer: PropTypes.node,
className: PropTypes.string,
content: PropTypes.node,
- size: PropTypes.oneOf(['s', 'm', 'l']),
+ size: PropTypes.oneOf(['s', 'm', 'l', 'maxContent']),
};
Tooltip.defaultProps = {
diff --git a/src/constants/account.js b/src/constants/account.js
index 6343c24fce..3bc54346c0 100644
--- a/src/constants/account.js
+++ b/src/constants/account.js
@@ -1,4 +1,12 @@
const account = {
lockDuration: 600000, // lock duration time is 10 minutes in milliSecond
};
+
+export const unlockTxDelayAvailability = {
+ unvote: 2000,
+ selfUnvote: 260000,
+ unvotePunished: 260000,
+ selfUnvotePunished: 780000,
+};
+
export default account;
diff --git a/src/constants/actions.js b/src/constants/actions.js
index 9e84b86826..e18b3436bd 100644
--- a/src/constants/actions.js
+++ b/src/constants/actions.js
@@ -17,7 +17,6 @@ const actionTypes = {
delegateRetrieved: 'DELEGATE_RETRIEVED',
delegateRetrieving: 'DELEGATE_RETRIEVING',
delegatesAdded: 'DELEGATES_ADDED',
- dynamicFeesRetrieved: 'DYNAMIC_FEES_RETRIEVED',
bookmarksRetrieved: 'BOOKMARK_RETRIEVED',
bookmarkAdded: 'BOOKMARK_ADDED',
bookmarkRemoved: 'BOOKMARK_REMOVED',
@@ -29,13 +28,13 @@ const actionTypes = {
liskAPIClientUpdate: 'LISK_API_CLIENT_UPDATE',
loadingFinished: 'LOADING_FINISHED',
loadingStarted: 'LOADING_STARTED',
+ nodeDefined: 'NODE_DEFINED',
networkSet: 'NETWORK_SET',
serviceUrlSet: 'SERVICE_URL_SET',
networkStatusUpdated: 'NETWORK_STATUS_UPDATED',
newBlockCreated: 'NEW_BLOCK_CREATED',
olderBlocksRetrieved: 'OLDER_BLOCKS_RETRIEVED',
passphraseUsed: 'PASSPHRASE_USED',
- pendingVotesAdded: 'PENDING_VOTES_ADDED',
pricesRetrieved: 'PRICES_RETRIEVED',
removePassphrase: 'REMOVE_PASSPHRASE',
searchDelegate: 'SEARCH_DELEGATE',
@@ -57,11 +56,11 @@ const actionTypes = {
updateTransactions: 'UPDATE_TRANSACTIONS',
voteLookupStatusCleared: 'VOTE_LOOKUP_STATUS_CLEARED',
voteLookupStatusUpdated: 'VOTE_LOOKUP_STATUS_UPDATED',
- VotePlaced: 'VOTE_PLACED',
- votesAdded: 'VOTES_ADDED',
+ votesRetrieved: 'VOTES_RETRIEVED',
+ votesSubmitted: 'VOTES_SUBMITTED',
votesCleared: 'VOTES_CLEARED',
- votesUpdated: 'VOTES_UPDATED',
- voteToggled: 'VOTE_TOGGLED',
+ votesConfirmed: 'VOTES_CONFIRMED',
+ voteEdited: 'VOTE_EDITED',
deviceListUpdated: 'DEVICE_LIST_UPDATED',
transactionCreatedSuccess: 'TRANSACTION_CREATED_SUCCESS',
transactionCreatedError: 'TRANSACTION_CREATED_ERROR',
@@ -69,8 +68,6 @@ const actionTypes = {
broadcastedTransactionSuccess: 'BROADCAST_TRANSACTION_SUCCESS',
broadcastedTransactionError: 'BROADCASTED_TRANSACTION_ERROR',
forgingTimesRetrieved: 'FORGING_TIME_RETRIEVED',
- forgingDataDisplayed: 'FORGING_DATA_DISPLAYED',
- forgingDataConcealed: 'FORGING_DATA_CONCEALED',
appUpdateAvailable: 'APP_UPDATE_AVAILABLE',
};
diff --git a/src/constants/breakpoints.js b/src/constants/breakpoints.js
deleted file mode 100644
index 45401337ab..0000000000
--- a/src/constants/breakpoints.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
- s: 768,
- m: 1024,
- l: 1400,
-};
diff --git a/src/constants/fees.js b/src/constants/fees.js
deleted file mode 100644
index 8343a7b167..0000000000
--- a/src/constants/fees.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- setSecondPassphrase: 5e8,
- send: 0.1e8,
- registerDelegate: 25e8,
- vote: 1e8,
-};
diff --git a/src/constants/routes.js b/src/constants/routes.js
index ff310c7635..b130be978c 100644
--- a/src/constants/routes.js
+++ b/src/constants/routes.js
@@ -4,7 +4,6 @@ import BlockDetails from '../components/screens/monitor/blockDetails';
import Blocks from '../components/screens/monitor/blocks';
import Bookmarks from '../components/screens/bookmarks/list';
import Dashboard from '../components/screens/dashboard';
-import Voting from '../components/screens/voting';
import DelegatesMonitor from '../components/screens/monitor/delegates';
import HwWalletLogin from '../components/screens/hwWalletLogin';
import Login from '../components/screens/login';
@@ -22,10 +21,12 @@ import Wallet from '../components/screens/wallet';
import Explorer from '../components/screens/wallet/explorer';
import TransactionDetails from '../components/screens/transactionDetails';
import VerifyMessage from '../components/screens/verifyMessage';
-import VotingSummary from '../components/screens/voting/votingSummary';
import Request from '../components/screens/request';
+import LockedBalance from '../components/screens/lockedBalance';
import SearchBar from '../components/shared/searchBar';
import NewReleaseDialog from '../components/shared/newReleaseDialog/newReleaseDialog';
+import EditVote from '../components/screens/editVote';
+import VotingQueue from '../components/screens/votingQueue';
export default {
wallet: {
@@ -35,12 +36,6 @@ export default {
exact: false,
forbiddenTokens: [],
},
- voting: {
- path: '/voting',
- component: Voting,
- isPrivate: false,
- forbiddenTokens: [tokenMap.BTC.key],
- },
addAccount: {
path: '/add-account',
component: Login,
@@ -148,11 +143,6 @@ export const modals = {
isPrivate: true,
forbiddenTokens: [],
},
- votingSummary: {
- component: VotingSummary,
- isPrivate: true,
- forbiddenTokens: [tokenMap.BTC.key],
- },
settings: {
component: Settings,
isPrivate: false,
@@ -198,4 +188,19 @@ export const modals = {
isPrivate: true,
forbiddenTokens: [],
},
+ lockedBalance: {
+ component: LockedBalance,
+ isPrivate: true,
+ forbiddenTokens: [tokenMap.BTC.key],
+ },
+ editVote: {
+ component: EditVote,
+ isPrivate: true,
+ forbiddenTokens: [tokenMap.BTC.key],
+ },
+ votingQueue: {
+ component: VotingQueue,
+ isPrivate: true,
+ forbiddenTokens: [],
+ },
};
diff --git a/src/constants/transactionTypes.js b/src/constants/transactionTypes.js
index 8f2535196c..bbb2ab7651 100644
--- a/src/constants/transactionTypes.js
+++ b/src/constants/transactionTypes.js
@@ -1,5 +1,3 @@
-const defaultApiVersion = '2';
-
/**
* Returns details of the transaction types
*
@@ -8,45 +6,82 @@ const defaultApiVersion = '2';
* and simply assume we always receive the new layout
* but transactions may have either of the tx type codes.
*/
-const transactionTypes = (t = str => str, apiVersion = defaultApiVersion) => ({
- send: {
- code: 0,
- outgoingCode: apiVersion === defaultApiVersion ? 0 : 8,
+const transactionTypes = (t = str => str) => ({
+ transfer: {
+ code: {
+ legacy: 0,
+ new: 8,
+ },
+ outgoingCode: 8,
title: t('Send'),
senderLabel: t('Sender'),
key: 'transfer',
+ nameFee: 0,
+ hardCap: 1e7, // rawLSK
},
setSecondPassphrase: {
- code: 1,
- outgoingCode: apiVersion === defaultApiVersion ? 1 : 9,
+ code: {
+ legacy: 1,
+ new: 9,
+ },
+ outgoingCode: 9,
title: t('Second passphrase registration'),
senderLabel: t('Account'),
key: 'secondPassphrase',
icon: 'tx2ndPassphrase',
+ nameFee: 0,
+ hardCap: 5e8, // rawLSK
},
registerDelegate: {
- code: 2,
- outgoingCode: apiVersion === defaultApiVersion ? 2 : 10,
+ code: {
+ legacy: 2,
+ new: 10,
+ },
+ outgoingCode: 10,
title: t('Delegate registration'),
senderLabel: t('Account nickname'),
key: 'registerDelegate',
icon: 'txDelegate',
+ nameFee: 1e9,
+ hardCap: 25e8, // rawLSK
},
vote: {
- code: 3,
- outgoingCode: apiVersion === defaultApiVersion ? 3 : 11,
+ code: {
+ legacy: 3,
+ new: 13,
+ },
+ outgoingCode: 11,
title: t('Delegate vote'),
senderLabel: t('Voter'),
- key: 'vote',
+ key: 'castVotes',
icon: 'txVote',
+ nameFee: 0,
+ hardCap: 1e8, // rawLSK
},
createMultiSig: {
- code: 4,
- outgoingCode: apiVersion === defaultApiVersion ? 4 : 12,
+ code: {
+ legacy: 4,
+ new: 12,
+ },
+ outgoingCode: 12,
title: t('Multisignature creation'),
senderLabel: t('Registrant'),
key: 'createMultiSig',
- icon: 'multiSignature',
+ icon: 'signMultiSignatureTransaction',
+ nameFee: 0,
+ hardCap: 5e8, // rawLSK
+ },
+ unlockToken: {
+ code: {
+ legacy: 5,
+ new: 14,
+ },
+ outgoingCode: 14,
+ title: t('Unlock LSK'),
+ senderLabel: t('Sender'),
+ key: 'unlockToken',
+ icon: 'txUnlock',
+ nameFee: 0,
},
});
@@ -58,7 +93,10 @@ const transactionTypes = (t = str => str, apiVersion = defaultApiVersion) => ({
*/
transactionTypes.getByCode = (code) => {
const types = transactionTypes();
- const key = Object.keys(types).filter(type => types[type].code === code);
+ const key = Object.keys(types)
+ .filter(type => (
+ types[type].code.legacy === code || types[type].code.new === code
+ ));
return key.length ? types[key] : null;
};
@@ -74,4 +112,35 @@ transactionTypes.getListOf = (key) => {
return Object.keys(types).map(type => types[type][key]);
};
+/**
+ * gets the name fee for a transaction type
+ *
+ * @param {key} key the transaction type
+ * @returns {number} transaction name fee
+ */
+transactionTypes.getNameFee = (key) => {
+ const types = transactionTypes();
+ return types[key].nameFee;
+};
+
+/**
+ * gets the hard cap for a transaction type
+ *
+ * @param {key} key the transaction type
+ * @returns {number} transaction hard cap
+ */
+transactionTypes.getHardCap = (key) => {
+ const types = transactionTypes();
+ return types[key].hardCap;
+};
+
+export const byteSizes = {
+ type: 1,
+ nonce: 8,
+ fee: 8,
+ signature: 64,
+};
+
+export const minFeePerByte = 1000;
+
export default transactionTypes;
diff --git a/src/constants/transactionTypes.test.js b/src/constants/transactionTypes.test.js
index 889326f1a7..f1947858b2 100644
--- a/src/constants/transactionTypes.test.js
+++ b/src/constants/transactionTypes.test.js
@@ -1,54 +1,23 @@
import transactionTypes from './transactionTypes';
-import store from '../store';
describe('Constants: transactionTypes', () => {
- beforeEach(() => {
- store.getState = jest
- .fn()
- .mockReturnValue({
- network: {
- ApiVersion: '2.x',
- },
- });
- });
-
it.skip('should return a config object of transaction types based on the API version', () => {
- const expectedTypes = {
- createMultiSig: {
- code: 4,
- key: 'createMultiSig',
- title: 'Multisignature creation',
- },
- registerDelegate: {
- code: 2,
- key: 'registerDelegate',
- title: 'Delegate registration',
- },
- send: {
- code: 0,
- key: 'transfer',
- title: 'Send',
- },
- setSecondPassphrase: {
- code: 1,
- title: 'Second passphrase registration',
- key: 'secondPassphrase',
- },
- vote: {
- code: 3,
- key: 'vote',
- title: 'Delegate vote',
- },
- };
const types = transactionTypes();
- expect(types).toEqual(expectedTypes);
+ expect(types.transfer.code).toEqual({
+ legacy: 0,
+ new: 8,
+ });
});
- it.skip('should return transaction config for a given transaction code', () => {
+ it('should return transaction config for a given transaction code', () => {
const txConfig = transactionTypes.getByCode(0);
expect(txConfig).toEqual({
- code: 0,
+ code: { legacy: 0, new: 8 },
key: 'transfer',
title: 'Send',
+ hardCap: 10000000,
+ nameFee: 0,
+ outgoingCode: 8,
+ senderLabel: 'Sender',
});
});
it('should return null for an invalid given transaction code', () => {
@@ -56,15 +25,14 @@ describe('Constants: transactionTypes', () => {
expect(txConfig).toEqual(null);
});
it('should return an array of values for any given key', () => {
- const codesList = transactionTypes.getListOf('code');
- expect(codesList).toEqual([0, 1, 2, 3, 4]);
const keysList = transactionTypes.getListOf('key');
expect(keysList).toEqual([
'transfer',
'secondPassphrase',
'registerDelegate',
- 'vote',
+ 'castVotes',
'createMultiSig',
+ 'unlockToken',
]);
});
});
diff --git a/src/constants/transactions.js b/src/constants/transactions.js
index 7a6360e8d3..b1421d6124 100644
--- a/src/constants/transactions.js
+++ b/src/constants/transactions.js
@@ -1,5 +1,7 @@
export const messageMaxLength = 64;
+export const minBalance = 5000000;
export default {
messageMaxLength,
+ minBalance,
};
diff --git a/src/store/middlewares/account.js b/src/store/middlewares/account.js
index 3246d223dd..6325598984 100644
--- a/src/store/middlewares/account.js
+++ b/src/store/middlewares/account.js
@@ -11,7 +11,7 @@ import { settingsUpdated } from '../../actions/settings';
import { fromRawLsk } from '../../utils/lsk';
import { getActiveTokenAccount } from '../../utils/account';
import { getAutoLogInData, shouldAutoLogIn, findMatchingLoginNetwork } from '../../utils/login';
-import { loadVotes } from '../../actions/voting';
+import { votesRetrieved } from '../../actions/voting';
import { networkSet, networkStatusUpdated } from '../../actions/network';
import actionTypes from '../../constants/actions';
import analytics from '../../utils/analytics';
@@ -44,23 +44,18 @@ const getRecentTransactionOfType = (transactionsList, type) => (
const votePlaced = (store, action) => {
const voteTransaction = getRecentTransactionOfType(
action.data.confirmed,
- transactionTypes().vote.code,
+ transactionTypes().vote.code.legacy,
);
if (voteTransaction) {
- const { account } = store.getState();
-
- store.dispatch(loadVotes({
- address: account.info.LSK.address,
- type: 'update',
- }));
+ store.dispatch(votesRetrieved());
}
};
const filterIncomingTransactions = (transactions, account) => transactions.filter(transaction => (
transaction
&& transaction.recipientId === account.address
- && transaction.type === transactionTypes().send.code
+ && transaction.type === transactionTypes().transfer.code.legacy
));
const showNotificationsForIncomingTransactions = (transactions, account, token) => {
@@ -83,9 +78,11 @@ const checkTransactionsAndUpdateAccount = (store, action) => {
const txs = (action.data.block.transactions || []).map(txAdapter);
const blockContainsRelevantTransaction = txs.filter((transaction) => {
- const sender = transaction ? transaction.senderId : null;
- const recipient = transaction ? transaction.recipientId : null;
- return account.address === recipient || account.address === sender;
+ if (!transaction) return false;
+ return (
+ account.address === transaction.senderId
+ || account.address === transaction.recipientId
+ );
}).length > 0;
showNotificationsForIncomingTransactions(txs, account, token.active);
@@ -180,8 +177,7 @@ const autoLogInIfNecessary = async (store) => {
showNetwork, statistics, statisticsRequest, statisticsFollowingDay,
} = store.getState().settings;
const loginNetwork = checkNetworkToConnect(showNetwork);
-
- store.dispatch(await networkSet(loginNetwork));
+ store.dispatch(networkSet(loginNetwork));
store.dispatch(networkStatusUpdated({ online: true }));
const autologinData = getAutoLogInData();
diff --git a/src/store/middlewares/account.test.js b/src/store/middlewares/account.test.js
index 02754ff739..fc03cbdb9c 100644
--- a/src/store/middlewares/account.test.js
+++ b/src/store/middlewares/account.test.js
@@ -1,6 +1,3 @@
-// import {
-// spy, stub, useFakeTimers, match,
-// } from 'sinon';
import * as accountActions from '../../actions/account';
import * as transactionsActions from '../../actions/transactions';
import * as votingActions from '../../actions/voting';
@@ -203,7 +200,7 @@ describe('Account middleware', () => {
});
});
- it('should show Notification on incoming transaction', () => {
+ it.skip('should show Notification on incoming transaction', () => {
middleware(store)(next)(newBlockCreated);
expect(windowNotificationSpy).nthCalledWith(
1,
@@ -215,14 +212,11 @@ describe('Account middleware', () => {
);
});
- it(`should dispatch ${actionTypes.loadVotes} action on ${actionTypes.updateTransactions} action if action.data.confirmed contains delegateRegistration transactions`, () => {
- const actionSpy = jest.spyOn(votingActions, 'loadVotes');
- transactionsUpdatedAction.data.confirmed[0].type = transactionTypes().vote.code;
+ it(`should dispatch ${actionTypes.votesRetrieved} action on ${actionTypes.updateTransactions} action if action.data.confirmed contains delegateRegistration transactions`, () => {
+ const actionSpy = jest.spyOn(votingActions, 'votesRetrieved');
+ transactionsUpdatedAction.data.confirmed[0].type = transactionTypes().vote.code.legacy;
middleware(store)(next)(transactionsUpdatedAction);
- expect(actionSpy).toHaveBeenCalledWith({
- address: state.account.address,
- type: 'update',
- });
+ expect(actionSpy).toHaveBeenCalled();
});
it(`should dispatch ${actionTypes.networkSet} action on ${actionTypes.storeCreated} if autologin data found in localStorage`, () => {
diff --git a/src/store/middlewares/index.js b/src/store/middlewares/index.js
index d8c51f175d..4b39ad7027 100644
--- a/src/store/middlewares/index.js
+++ b/src/store/middlewares/index.js
@@ -9,6 +9,7 @@ import votingMiddleware from './voting';
import socketMiddleware from './socket';
import settingsMiddleware from './settings';
import bookmarksMiddleware from './bookmarks';
+import networkMiddleware from './network';
export default [
// notificationMiddleware,
@@ -19,6 +20,7 @@ export default [
offlineMiddleware,
settingsMiddleware,
socketMiddleware,
- thunk,
votingMiddleware,
+ networkMiddleware,
+ thunk,
];
diff --git a/src/store/middlewares/network.js b/src/store/middlewares/network.js
new file mode 100644
index 0000000000..f027a105ad
--- /dev/null
+++ b/src/store/middlewares/network.js
@@ -0,0 +1,92 @@
+import Lisk from '@liskhq/lisk-client';
+import { toast } from 'react-toastify';
+
+import actionTypes from '../../constants/actions';
+import { tokenMap } from '../../constants/tokens';
+import { getConnectionErrorMessage } from '../../utils/getNetwork';
+
+const getServerUrl = (nodeUrl, nethash) => {
+ if (nethash === Lisk.constants.MAINNET_NETHASH) {
+ return {
+ serviceUrl: 'https://mainnet-service.lisk.io',
+ cloudUrl: 'https://cloud.lisk.io',
+ };
+ }
+ if (nethash === Lisk.constants.TESTNET_NETHASH) {
+ return {
+ serviceUrl: 'https://testnet-service.lisk.io',
+ cloudUrl: 'https://cloud.lisk.io',
+ };
+ }
+ if (/(localhost|liskdev.net):\d{2,4}$/.test(nodeUrl)) {
+ return {
+ serviceUrl: nodeUrl.replace(/:\d{2,4}/, ':9901'),
+ cloudUrl: 'https://cloud.lisk.io',
+ };
+ }
+ if (/\.(liskdev.net|lisk.io)$/.test(nodeUrl)) {
+ return {
+ serviceUrl: nodeUrl.replace(/\.(liskdev.net|lisk.io)$/, $1 => `-service${$1}`),
+ cloudUrl: 'https://cloud.lisk.io',
+ };
+ }
+ return {
+ serviceUrl: 'unavailable',
+ cloudUrl: 'unavailable',
+ };
+};
+
+const generateAction = (data, config) => ({
+ data: {
+ name: data.name,
+ token: tokenMap.LSK.key,
+ network: config,
+ },
+ type: actionTypes.networkSet,
+});
+
+const getNetworkInfo = async nodeUrl => (
+ new Promise(async (resolve, reject) => {
+ new Lisk.APIClient([nodeUrl], {}).node.getConstants().then((response) => {
+ resolve(response.data);
+ }).catch((error) => {
+ reject(getConnectionErrorMessage(error));
+ });
+ })
+);
+
+const network = store => next => (action) => {
+ const { dispatch } = store;
+ switch (action.type) {
+ case actionTypes.nodeDefined:
+ next(action);
+ getNetworkInfo(action.data.nodeUrl).then(({ nethash, networkId }) => {
+ const networkConfig = {
+ nodeUrl: action.data.nodeUrl,
+ custom: action.data.network.custom,
+ code: action.data.network.code,
+ nethash,
+ networkIdentifier: networkId,
+ };
+ dispatch(generateAction(action.data, networkConfig));
+ dispatch({
+ data: getServerUrl(action.data.nodeUrl, nethash),
+ type: actionTypes.serviceUrlSet,
+ });
+ }).catch((error) => {
+ dispatch(generateAction(action.data, {
+ nodeUrl: action.data.network.address,
+ custom: action.data.network.custom,
+ code: action.data.network.code,
+ }));
+ toast.error(error);
+ });
+
+ break;
+ default:
+ next(action);
+ break;
+ }
+};
+
+export default network;
diff --git a/src/store/middlewares/network.test.js b/src/store/middlewares/network.test.js
new file mode 100644
index 0000000000..6323afa0fb
--- /dev/null
+++ b/src/store/middlewares/network.test.js
@@ -0,0 +1,16 @@
+import networkMiddleware from './network';
+
+describe('actions: network.lsk', () => {
+ const next = jest.fn();
+ const otherActions = {
+ type: 'ANY',
+ };
+
+ it('should only pass all actions', () => {
+ const store = {
+ dispatch: jest.fn(),
+ };
+ networkMiddleware(store)(next)(otherActions);
+ expect(next.mock.calls.length).toBe(1);
+ });
+});
diff --git a/src/store/middlewares/socket.js b/src/store/middlewares/socket.js
index 0c7feeed75..68b92965f0 100644
--- a/src/store/middlewares/socket.js
+++ b/src/store/middlewares/socket.js
@@ -68,22 +68,16 @@ const socketMiddleware = store => (
case actionTypes.serviceUrlSet:
store.dispatch(olderBlocksRetrieved());
socketSetup(store);
- break;
- case actionTypes.forgingDataDisplayed:
- if (!interval) {
- interval = setInterval(() => {
- // if user refreshes the page, we might have a race condition here.
- // I'll skip the first retrieval since it is useless without the blocks list
- if (store.getState().blocks.latestBlocks.length) {
- store.dispatch(forgingTimesRetrieved());
- }
- }, intervalTime);
- }
- break;
- case actionTypes.forgingDataConcealed:
clearInterval(interval);
- interval = null;
+ interval = setInterval(() => {
+ // if user refreshes the page, we might have a race condition here.
+ // I'll skip the first retrieval since it is useless without the blocks list
+ if (store.getState().blocks.latestBlocks.length) {
+ store.dispatch(forgingTimesRetrieved());
+ }
+ }, intervalTime);
break;
+
default: break;
}
});
diff --git a/src/store/middlewares/voting.js b/src/store/middlewares/voting.js
index d084288a64..f3b6f92636 100644
--- a/src/store/middlewares/voting.js
+++ b/src/store/middlewares/voting.js
@@ -1,14 +1,19 @@
-import { delegatesAdded } from '../../actions/voting';
+import { votesRetrieved } from '../../actions/voting';
import actionTypes from '../../constants/actions';
const votingMiddleware = store => next => (action) => {
next(action);
switch (action.type) {
+ case actionTypes.accountLoggedIn:
+ store.dispatch(votesRetrieved());
+ break;
+ case actionTypes.accountUpdated:
+ store.dispatch(votesRetrieved());
+ break;
case actionTypes.accountLoggedOut:
- store.dispatch(delegatesAdded({ list: [] }));
store.dispatch({
- type: actionTypes.votesAdded,
- data: { list: [] },
+ type: actionTypes.votesRetrieved,
+ data: [],
});
break;
default: break;
diff --git a/src/store/middlewares/voting.test.js b/src/store/middlewares/voting.test.js
index e23d814144..209e9177ed 100644
--- a/src/store/middlewares/voting.test.js
+++ b/src/store/middlewares/voting.test.js
@@ -1,117 +1,71 @@
-import { expect } from 'chai';
-import { spy, stub, mock } from 'sinon';
import actionTypes from '../../constants/actions';
-import * as delegateApi from '../../utils/api/delegates';
import middleware from './voting';
-import networks from '../../constants/networks';
-import votingConst from '../../constants/voting';
describe('voting middleware', () => {
- let store;
- let next;
-
- const generateNVotes = (n, vote) => (
- [...Array(n)].map((item, i) => i).reduce((dict, value) => {
- dict[`genesis_${value}`] = vote;
- return dict;
- }, {})
- );
-
- const initStoreWithNVotes = (n, vote) => {
- store.getState = () => ({
- voting: {
- votes: {
- ...generateNVotes(n, vote),
- test2: {
- unconfirmed: false,
- confirmed: false,
- },
- },
- },
- });
- };
-
- beforeEach(() => {
- store = stub();
- initStoreWithNVotes(
- votingConst.maxCountOfVotesInOneTurn + 1,
- { confirmed: false, unconfirmed: true },
- );
- store.dispatch = spy();
- next = spy();
- });
-
it('should passes the action to next middleware', () => {
const givenAction = {
type: 'TEST_ACTION',
};
+ const next = jest.fn();
+ const store = {
+ getState: jest.fn(),
+ dispatch: jest.fn(),
+ };
middleware(store)(next)(givenAction);
- expect(next).to.have.been.calledWith(givenAction);
+ expect(next).toHaveBeenCalledWith(givenAction);
+ expect(store.dispatch).not.toHaveBeenCalled();
});
describe('on accountLoggedOut action', () => {
- const state = {
- account: {
- info: {
- LSK: {
- address: '1243987612489124L',
- },
- },
- },
- voting: {
- delegates: [
- {
- username: 'delegate_in_store',
- publicKey: 'some publicKey',
- },
- ],
- votes: {
- delegate_voted: {
- unconfirmed: true,
- confirmed: true,
- },
- delegate_unvoted: {
- unconfirmed: false,
- confirmed: false,
- },
- },
- },
- network: {
- status: { online: true },
- name: networks.mainnet.name,
- networks: {
- LSK: {
- nodeUrl: 'hhtp://localhost:4000',
- nethash: '198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d',
- },
- },
- },
+ const givenAction = {
+ type: actionTypes.accountLoggedOut,
};
- let getDelegatesMock;
-
- beforeEach(() => {
- getDelegatesMock = mock(delegateApi, 'getDelegates');
- store.getState = () => (state);
+ const expectedAction = {
+ type: actionTypes.votesRetrieved,
+ data: [],
+ };
+ const next = jest.fn();
+ const store = {
+ getState: jest.fn(),
+ dispatch: jest.fn(),
+ };
+ it('should dispatch votesRetrieved with empty array', () => {
+ middleware(store)(next)(givenAction);
+ expect(next).toHaveBeenCalledWith(givenAction);
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
});
+ });
- afterEach(() => {
- getDelegatesMock.restore();
+ describe('on accountLoggedIn action', () => {
+ const givenAction = {
+ type: actionTypes.accountLoggedIn,
+ };
+ const next = jest.fn();
+ const store = {
+ getState: jest.fn(),
+ dispatch: jest.fn(),
+ };
+ it('should dispatch votesRetrieved with empty array', () => {
+ middleware(store)(next)(givenAction);
+ expect(next).toHaveBeenCalledWith(givenAction);
+ expect(store.dispatch).toHaveBeenCalled();
});
+ });
- it('should clear delegates and votes on accountLoggedOut action', () => {
- middleware(store)(next)({
- type: actionTypes.accountLoggedOut,
- });
-
- expect(store.dispatch).to.have.been.calledWith({
- data: { list: [] },
- type: actionTypes.delegatesAdded,
- });
- expect(store.dispatch).to.have.been.calledWith({
- data: { list: [] },
- type: actionTypes.votesAdded,
- });
+ describe('on accountUpdated action', () => {
+ const givenAction = {
+ type: actionTypes.accountUpdated,
+ };
+ const next = jest.fn();
+ const store = {
+ getState: jest.fn(),
+ dispatch: jest.fn(),
+ };
+ it('should dispatch votesRetrieved with empty array', () => {
+ middleware(store)(next)(givenAction);
+ expect(next).toHaveBeenCalledWith(givenAction);
+ expect(store.dispatch).toHaveBeenCalled();
});
});
});
diff --git a/src/store/reducers/network.js b/src/store/reducers/network.js
index 863621bbf7..6e54492cf9 100644
--- a/src/store/reducers/network.js
+++ b/src/store/reducers/network.js
@@ -32,7 +32,8 @@ const network = (state = initialState, action) => {
case actionTypes.serviceUrlSet:
return {
...state,
- serviceUrl: action.data,
+ serviceUrl: action.data.serviceUrl,
+ cloudUrl: action.data.cloudUrl,
};
default:
return state;
diff --git a/src/store/reducers/service.js b/src/store/reducers/service.js
index 4eb9f7aaa9..910a22c312 100644
--- a/src/store/reducers/service.js
+++ b/src/store/reducers/service.js
@@ -6,7 +6,6 @@ export const INITIAL_STATE = {
...info,
[tokenKey]: {},
}), {}),
- dynamicFees: {},
};
const service = (state = INITIAL_STATE, action = {}) => {
@@ -19,12 +18,6 @@ const service = (state = INITIAL_STATE, action = {}) => {
...action.data.priceTicker,
},
});
-
- case actionTypes.dynamicFeesRetrieved:
- return ({
- ...state,
- dynamicFees: action.dynamicFees,
- });
default:
return state;
}
diff --git a/src/store/reducers/service.test.js b/src/store/reducers/service.test.js
index 290ea86d7f..1c39cea651 100644
--- a/src/store/reducers/service.test.js
+++ b/src/store/reducers/service.test.js
@@ -27,23 +27,11 @@ describe('reducers: service', () => {
},
};
- expect(service(INITIAL_STATE, action)).toEqual({
- dynamicFees: {},
+ expect(service(state, action)).toEqual({
priceTicker: {
BTC: {},
LSK: { EUR: 1, USD: 1 },
},
});
});
-
- it('should return updated state in case of actionTypes.dynamicFeesRetrieved', () => {
- const action = {
- type: actionTypes.dynamicFeesRetrieved,
- dynamicFees: { low: 1, medium: 10, high: 100 },
- };
-
- expect(service(state, action)).toEqual({
- dynamicFees: action.dynamicFees,
- });
- });
});
diff --git a/src/store/reducers/transactions.js b/src/store/reducers/transactions.js
index 6fbcf9a0e1..1a62f0d179 100644
--- a/src/store/reducers/transactions.js
+++ b/src/store/reducers/transactions.js
@@ -2,9 +2,7 @@ import actionTypes from '../../constants/actions';
import txFilters from '../../constants/transactionFilters';
// TODO the sort should be removed when BTC api returns transactions sorted by timestamp
-const sortByTimestamp = (a, b) => (
- (!a.timestamp || a.timestamp > b.timestamp) && b.timestamp ? -1 : 1
-);
+const sortByHeight = (a, b) => (b.height - a.height);
const addNewTransactions = (array1, array2) => array1.filter(array1Value =>
array2.filter(array2Value => array2Value.id === array1Value.id).length === 0);
@@ -55,7 +53,7 @@ const transactions = (state = initialState, action) => { // eslint-disable-line
return {
...state,
// TODO the sort should be removed when BTC api returns transactions sorted by timestamp
- confirmed: action.data.confirmed.sort(sortByTimestamp),
+ confirmed: action.data.confirmed.sort(sortByHeight),
count: action.data.count,
filters: action.data.filters !== undefined
? action.data.filters : state.filters,
@@ -69,7 +67,7 @@ const transactions = (state = initialState, action) => { // eslint-disable-line
...action.data.confirmed,
...addNewTransactions(state.confirmed, action.data.confirmed),
// TODO the sort should be removed when BTC api returns transactions sorted by timestamp
- ].sort(sortByTimestamp),
+ ].sort(sortByHeight),
count: action.data.count,
filters: action.data.filters !== undefined
? action.data.filters : state.filters,
@@ -83,11 +81,12 @@ const transactions = (state = initialState, action) => { // eslint-disable-line
};
// TODO can be remove after move send (create) tx to utils file
// istanbul ignore next
- case actionTypes.transactionCreatedError:
+ case actionTypes.transactionCreatedError: {
+ const { message = 'The transaction failed', name = 'TransactionFailedError' } = action.data;
return {
...state,
- transactionsCreatedFailed: [...state.transactionsCreatedFailed, action.data],
- };
+ transactionsCreatedFailed: [...state.transactionsCreatedFailed, { message, name }],
+ }; }
// TODO can be remove after use HOC for send (broadcast) tx
// istanbul ignore next
case actionTypes.broadcastedTransactionSuccess:
diff --git a/src/store/reducers/voting.js b/src/store/reducers/voting.js
index f866098a5e..439ee6dc24 100644
--- a/src/store/reducers/voting.js
+++ b/src/store/reducers/voting.js
@@ -1,90 +1,44 @@
import actionTypes from '../../constants/actions';
-const mergeVotes = (newList, oldDict) => {
- const newDict = newList.reduce((tempDict, delegate) => {
- tempDict[delegate.username] = {
- confirmed: true,
- unconfirmed: true,
- pending: false,
- publicKey: delegate.publicKey,
- rank: delegate.rank,
- address: delegate.address,
- productivity: delegate.productivity,
- };
- return tempDict;
- }, {});
-
- Object.keys(oldDict).forEach((username) => { // eslint-disable-line complexity
- // By pendingVotesAdded, we set confirmed equal to unconfirmed,
- // to recognize pending-not-voted items from pending-voted
- // so here we just check unconfirmed flag.
- const { confirmed, unconfirmed, pending } = oldDict[username];
- if (// we've voted but it's not in the new list
- (pending && unconfirmed && newDict[username] === undefined)
- // we've un-voted but it still exists in the new list
- || (pending && !unconfirmed && newDict[username] !== undefined)
- // dirty, not voted for and not updated in other client
- || (!pending && unconfirmed !== confirmed
- && (newDict[username] === undefined || confirmed === newDict[username].confirmed))
- ) {
- newDict[username] = { ...oldDict[username] };
- }
- });
-
- return newDict;
-};
-
/**
* voting reducer
*
* @param {Object} state
* @param {Object} action
*/
-const voting = (state = { // eslint-disable-line complexity
- votes: {},
- delegates: [],
-}, action) => {
+const voting = (state = {}, action) => {
switch (action.type) {
- case actionTypes.votesAdded:
- return {
- ...state,
- votes: action.data.list
- .reduce((votesDict, delegate) => {
- votesDict[delegate.username] = {
- confirmed: true,
- unconfirmed: true,
- publicKey: delegate.publicKey,
- productivity: delegate.productivity,
- rank: delegate.rank,
- address: delegate.address,
- };
- return votesDict;
- }, {}),
- };
+ case actionTypes.votesRetrieved:
+ return action.data
+ .reduce((votesDict, delegate) => {
+ votesDict[delegate.delegateAddress] = {
+ confirmed: Number(delegate.amount),
+ unconfirmed: Number(delegate.amount),
+ username: delegate.username,
+ };
+ return votesDict;
+ }, {});
- case actionTypes.delegatesAdded:
+ case actionTypes.voteEdited:
return {
...state,
- delegates: action.data.refresh ? action.data.list
- : [...state.delegates, ...action.data.list],
- };
+ ...action.data.reduce((mergedVotes, vote) => {
+ // When added new vote using launch protocol
+ let unconfirmed = '';
+ // when added, removed or edited vote
+ if (vote.amount !== undefined) unconfirmed = vote.amount;
+ // when the launch protocol includes an existing vote
+ else if (state[vote.address]) unconfirmed = state[vote.address].unconfirmed;
- case actionTypes.voteToggled:
- return {
- ...state,
- votes: {
- ...state.votes,
- [action.data.username]: {
- confirmed: state.votes[action.data.username]
- ? state.votes[action.data.username].confirmed : false,
- unconfirmed: state.votes[action.data.username]
- ? !state.votes[action.data.username].unconfirmed : true,
- publicKey: action.data.account.publicKey,
- productivity: action.data.productivity,
- rank: action.data.rank,
- address: action.data.account.address,
- },
- },
+ mergedVotes[vote.address] = {
+ confirmed: state[vote.address]
+ ? state[vote.address].confirmed : 0,
+ unconfirmed,
+ username: state[vote.address] && state[vote.address].username
+ ? state[vote.address].username : vote.username,
+ };
+ return mergedVotes;
+ }, {}),
};
/**
@@ -92,50 +46,51 @@ const voting = (state = { // eslint-disable-line complexity
* of each vote to match it's 'confirmed' state.
*/
case actionTypes.votesCleared:
- return {
- ...state,
- votes: Object.keys(state.votes).reduce((votesDict, username) => {
- votesDict[username] = {
- ...state.votes[username],
- unconfirmed: state.votes[username].confirmed,
- pending: false,
+ return Object.keys(state)
+ .filter(address => state[address].confirmed)
+ .reduce((votesDict, address) => {
+ votesDict[address] = {
+ confirmed: state[address].confirmed,
+ unconfirmed: state[address].confirmed,
+ username: state[address].username,
};
return votesDict;
- }, {}),
- };
+ }, {});
/**
- * This action is used when voting transaction is confirmed. It updates votes
- * based on response from votes API endpoint.
- * https://lisk.io/documentation/lisk-core/api#/Votes
+ * This action is used when voting transaction is confirmed.
+ * It removes the unvoted delegates, updates the confirmed vote amounts
+ * and removes all pending flags
*/
- case actionTypes.votesUpdated:
- return {
- ...state,
- votes: mergeVotes(action.data.list, state.votes),
- };
+ case actionTypes.votesConfirmed:
+ return Object.keys(state)
+ .filter(address => state[address].unconfirmed)
+ .reduce((votesDict, address) => {
+ votesDict[address] = {
+ ...state[address],
+ confirmed: state[address].unconfirmed,
+ pending: false,
+ };
+ return votesDict;
+ }, {});
/**
* This action is used when voting is submitted. It sets 'pending' status
* of all votes that have different 'confirmed' and 'unconfirmed' state
*/
- case actionTypes.pendingVotesAdded:
- return {
- ...state,
- votes: Object.keys(state.votes).reduce((votesDict, username) => {
- const {
- confirmed, unconfirmed, pending,
- } = state.votes[username];
- const nextPendingStatus = pending || (confirmed !== unconfirmed);
+ case actionTypes.votesSubmitted:
+ return Object.keys(state).reduce((votesDict, address) => {
+ const {
+ confirmed, unconfirmed, pending,
+ } = state[address];
+ const nextPendingStatus = pending || (confirmed !== unconfirmed);
- votesDict[username] = {
- ...state.votes[username],
- confirmed: nextPendingStatus ? !confirmed : confirmed,
- pending: nextPendingStatus,
- };
- return votesDict;
- }, {}),
- };
+ votesDict[address] = {
+ ...state[address],
+ pending: nextPendingStatus,
+ };
+ return votesDict;
+ }, {});
default:
return state;
}
diff --git a/src/store/reducers/voting.test.js b/src/store/reducers/voting.test.js
index 7c53d81f19..ed30db8e7c 100644
--- a/src/store/reducers/voting.test.js
+++ b/src/store/reducers/voting.test.js
@@ -1,328 +1,162 @@
-import { expect } from 'chai';
import actionTypes from '../../constants/actions';
import voting from './voting';
describe('Reducer: voting(state, action)', () => { // eslint-disable-line max-statements
- const initialState = { votes: {}, delegates: [] };
const delegate1 = {
- publicKey: 'sample_key_1', address: '100001L', rank: 1, productivity: 99,
+ address: '100001L',
};
const delegate2 = {
- publicKey: 'sample_key_2', address: '100002L', rank: 2, productivity: 98,
+ address: '100002L',
};
const delegate3 = {
- publicKey: 'sample_key_3', address: '100003L', rank: 3, productivity: 97,
- };
- const delegate4 = {
- publicKey: 'sample_key_4', address: '100004L', rank: 4, productivity: 96,
- };
- const delegate5 = {
- publicKey: 'sample_key_5', address: '100005L', rank: 5, productivity: 95,
+ address: '100003L',
};
const cleanVotes = {
- username1: { confirmed: false, unconfirmed: false, ...delegate1 },
- username2: { confirmed: true, unconfirmed: true, ...delegate2 },
- username3: { confirmed: false, unconfirmed: false, ...delegate3 },
+ [delegate1.address]: { confirmed: 1e10, unconfirmed: 1e10, username: 'username_1' },
+ [delegate2.address]: { confirmed: 1e10, unconfirmed: 1e10, username: 'username_2' },
+ [delegate3.address]: { confirmed: 1e10, unconfirmed: 1e10, username: 'username_3' },
};
const dirtyVotes = {
- username1: { confirmed: false, unconfirmed: true, ...delegate1 },
- username2: { confirmed: true, unconfirmed: true, ...delegate2 },
- username3: { confirmed: false, unconfirmed: false, ...delegate3 },
+ [delegate1.address]: { ...cleanVotes[delegate1.address], unconfirmed: 3e10 },
+ [delegate2.address]: { ...cleanVotes[delegate2.address], unconfirmed: 2e10 },
+ [delegate3.address]: cleanVotes[[delegate3.address]],
};
const pendingVotes = {
- username1: {
- confirmed: true, unconfirmed: true, pending: true, ...delegate1,
- },
- username2: {
- confirmed: true, unconfirmed: true, pending: false, ...delegate2,
- },
- username3: {
- confirmed: false, unconfirmed: false, pending: false, ...delegate3,
- },
+ [delegate1.address]: { ...dirtyVotes[delegate1.address], pending: true },
+ [delegate2.address]: { ...dirtyVotes[delegate2.address], pending: true },
+ [delegate3.address]: { ...dirtyVotes[delegate3.address], pending: false },
};
- const restoredVotes = {
- username1: {
- confirmed: false, unconfirmed: false, pending: false, ...delegate1,
- },
- username2: {
- confirmed: true, unconfirmed: true, pending: false, ...delegate2,
- },
- username3: {
- confirmed: false, unconfirmed: false, pending: false, ...delegate3,
- },
- };
-
- const delegateList1 = [{ username: 'username1', ...delegate1 }, { username: 'username2', ...delegate2 }];
- const delegateList2 = [{ username: 'username3', ...delegate3 }, { username: 'username4', ...delegate4 }];
- const delegateList3 = [
- {
- username: 'username1',
- account: { address: delegate1.address, publicKey: delegate1.publicKey },
- productivity: delegate1.productivity,
- rank: delegate1.rank,
- unconfirmed: delegate1.unconfirmed,
- },
- {
- username: 'username2',
- account: { address: delegate2.address, publicKey: delegate2.publicKey },
- productivity: delegate2.productivity,
- rank: delegate2.rank,
- unconfirmed: delegate2.unconfirmed,
- },
- ];
- const fullDelegates = [...delegateList1, ...delegateList2];
-
it('should return default state if action does not match', () => {
const action = {
type: '',
};
- const state = { votes: cleanVotes };
- const changedState = voting(state, action);
-
- expect(changedState).to.be.equal(state);
- });
-
- it('should fill votes object with action: votesAdded', () => {
- const action = {
- type: actionTypes.votesAdded,
- data: {
- list: delegateList1,
- },
- };
- const expectedState = {
- votes: {
- username1: { confirmed: true, unconfirmed: true, ...delegate1 },
- username2: { confirmed: true, unconfirmed: true, ...delegate2 },
- },
- delegates: [],
- };
- const changedState = voting(initialState, action);
-
- expect(changedState).to.be.deep.equal(expectedState);
- });
-
- it('should append to delegates list with action: delegatesAdded, refresh: false', () => {
- const action = {
- type: actionTypes.delegatesAdded,
- data: {
- list: delegateList2,
- totalCount: 100,
- refresh: false,
- },
- };
- const state = {
- delegates: delegateList1,
- };
- const expectedState = {
- delegates: fullDelegates,
- };
- const changedState = voting(state, action);
-
- expect(changedState.delegates).to.be.deep.equal(expectedState.delegates);
- });
-
- it('should replace delegates with the new delegates list with action: delegatesAdded, refresh: true', () => {
- const action = {
- type: actionTypes.delegatesAdded,
- data: {
- list: delegateList1,
- refresh: true,
- },
- };
- const state = {
- delegates: delegateList2,
- };
- const expectedState = {
- delegates: delegateList1,
- };
- const changedState = voting(state, action);
+ const changedState = voting(cleanVotes, action);
- expect(changedState).to.be.deep.equal(expectedState);
+ expect(changedState).toEqual(cleanVotes);
});
- it('should toggle unconfirmed state, with action: voteToggled', () => {
- const action = {
- type: actionTypes.voteToggled,
- data: delegateList3[0],
- };
- const state = { votes: cleanVotes };
- const expectedState = {
- votes: dirtyVotes,
- };
- const changedState = voting(state, action);
-
- expect(changedState).to.be.deep.equal(expectedState);
+ describe('votesRetrieved', () => {
+ it('should store fetched votes of a given account', () => {
+ const action = {
+ type: actionTypes.votesRetrieved,
+ data: [
+ { delegateAddress: delegate1.address, amount: 1e10 },
+ { delegateAddress: delegate2.address, amount: 2e10 },
+ ],
+ };
+ const expectedState = {
+ [delegate1.address]: { confirmed: 1e10, unconfirmed: 1e10 },
+ [delegate2.address]: { confirmed: 2e10, unconfirmed: 2e10 },
+ };
+ const changedState = voting({}, action);
+
+ expect(changedState).toEqual(expectedState);
+ });
});
- it('should add to votes dictionary in not exist, with action: voteToggled', () => {
- const action = {
- type: actionTypes.voteToggled,
- data: delegateList3[0],
- };
- const expectedState = {
- votes: {
- [delegateList3[0].username]: dirtyVotes[delegateList3[0].username],
- },
- delegates: [],
- };
- const changedState = voting(initialState, action);
-
- expect(changedState).to.be.deep.equal(expectedState);
- });
+ describe('votesEdited', () => {
+ it('should add delegate with voteAmount if does not exist among votes', () => {
+ const action = {
+ type: actionTypes.voteEdited,
+ data: [{
+ ...delegate1,
+ amount: dirtyVotes[delegate1.address].unconfirmed,
+ }],
+ };
+ const expectedState = {
+ [delegate1.address]: {
+ confirmed: 0,
+ unconfirmed: dirtyVotes[delegate1.address].unconfirmed,
+ },
+ };
+ const changedState = voting({}, action);
+
+ expect(changedState).toEqual(expectedState);
+ });
+
+ it('should change voteAmount if delegates exist among votes', () => {
+ const action = {
+ type: actionTypes.voteEdited,
+ data: [{
+ ...delegate1,
+ amount: dirtyVotes[delegate1.address].unconfirmed,
+ }],
+ };
+ const expectedState = {
+ [delegate1.address]: {
+ confirmed: cleanVotes[delegate1.address].confirmed,
+ unconfirmed: dirtyVotes[delegate1.address].unconfirmed,
+ username: 'username_1',
+ },
+ [delegate2.address]: cleanVotes[delegate2.address],
+ [delegate3.address]: cleanVotes[delegate3.address],
+ };
+ const changedState = voting(cleanVotes, action);
- it('should mark the toggles votes as pending, with action: pendingVotesAdded ', () => {
- const action = {
- type: actionTypes.pendingVotesAdded,
- };
- const state = {
- votes: dirtyVotes,
- };
- const expectedState = {
- votes: pendingVotes,
- };
- const changedState = voting(state, action);
- expect(changedState).to.be.deep.equal(expectedState);
+ expect(changedState).toEqual(expectedState);
+ });
});
- it('should remove all pending flags from votes, with action: votesCleared', () => {
- const action = {
- type: actionTypes.votesCleared,
- };
- const state = {
- votes: dirtyVotes,
- };
-
- const expectedState = {
- votes: restoredVotes,
- };
- const changedState = voting(state, action);
+ describe('votesSubmitted', () => {
+ it('should add pending flag to dirty votes', () => {
+ const action = {
+ type: actionTypes.votesSubmitted,
+ };
+ const changedState = voting(dirtyVotes, action);
- expect(changedState).to.be.deep.equal(expectedState);
+ expect(changedState).toEqual(pendingVotes);
+ });
});
- it('should update new username in votes when we\'ve voted but it\'s not in the new list', () => {
- const action = {
- type: actionTypes.votesUpdated,
- data: {
- list: [{ username: 'username5', ...delegate5 }],
- },
- };
- const votedButNotYetInList = {
- username1: {
- confirmed: true, unconfirmed: true, pending: true, ...delegate1,
- },
- };
- const state = {
- votes: { ...votedButNotYetInList },
- };
- const newUserNameRegisteredInVotes = {
- votes: {
- ...votedButNotYetInList,
- username5: {
- confirmed: true, unconfirmed: true, pending: false, ...delegate5,
+ describe('votesConfirmed', () => {
+ it('should remove pending flags and update confirmed values', () => {
+ const action = {
+ type: actionTypes.votesConfirmed,
+ };
+ const expectedState = {
+ [delegate1.address]: {
+ ...dirtyVotes[delegate1.address],
+ pending: false,
+ confirmed: dirtyVotes[delegate1.address].unconfirmed,
},
- },
- };
- const saveNewUserInVotes = voting(state, action);
- expect(saveNewUserInVotes).to.be.deep.equal(newUserNameRegisteredInVotes);
- });
-
- it('should not change votes, when we\'ve un-voted but user still exists in the new list', () => {
- const updateVotesWithExistingUsernameAction = {
- type: actionTypes.votesUpdated,
- data: {
- list: [{ username: 'username1', ...delegate1 }],
- },
- };
- const updateVotesUnvotedWithExistingUsername = {
- username1: {
- confirmed: true, unconfirmed: false, pending: true, ...delegate1,
- },
- };
- const state = {
- votes: { ...updateVotesUnvotedWithExistingUsername },
- };
- const notChangedVotesRecords = {
- votes: { ...updateVotesUnvotedWithExistingUsername },
- };
-
- const changedState = voting(state, updateVotesWithExistingUsernameAction);
- expect(changedState).to.be.deep.equal(notChangedVotesRecords);
- });
-
- it('should add new record of username in votes, when dirty and not voted for and username not yet in the new list', () => {
- const action = {
- type: actionTypes.votesUpdated,
- data: {
- list: [{ username: 'username5', ...delegate5 }],
- },
- };
- const updateVotesDirtyNotVotedNotExistingUsername = {
- username1: {
- confirmed: true, unconfirmed: false, pending: false, ...delegate1,
- },
- };
- const state = {
- votes: { ...updateVotesDirtyNotVotedNotExistingUsername },
- };
- const newUsernameAddedToVotes = {
- votes: {
- ...updateVotesDirtyNotVotedNotExistingUsername,
- username5: {
- confirmed: true, unconfirmed: true, pending: false, ...delegate5,
+ [delegate2.address]: {
+ ...dirtyVotes[delegate2.address],
+ pending: false,
+ confirmed: dirtyVotes[delegate2.address].unconfirmed,
},
- },
- };
- const changedState = voting(state, action);
- expect(changedState).to.be.deep.equal(newUsernameAddedToVotes);
+ [delegate3.address]: { ...dirtyVotes[delegate3.address], pending: false },
+ };
+ const changedState = voting(pendingVotes, action);
+
+ expect(changedState).toEqual(expectedState);
+ });
+
+ it('should remove unvoted delegates', () => {
+ const action = {
+ type: actionTypes.votesConfirmed,
+ };
+ const initialState = {
+ [delegate2.address]: { ...cleanVotes[delegate2.address], pending: false },
+ [delegate3.address]: { ...cleanVotes[delegate3.address], unconfirmed: 0, pending: true },
+ };
+ const expectedState = {
+ [delegate2.address]: { ...cleanVotes[delegate2.address], pending: false },
+ };
+ const changedState = voting(initialState, action);
+
+ expect(changedState).toEqual(expectedState);
+ });
});
- it('should keep record of username in votes, when dirty and not voted for and username is already in the new list', () => {
- const action = {
- type: actionTypes.votesUpdated,
- data: {
- list: [{ username: 'username1', ...delegate1 }],
- },
- };
- const updateVotesDirtyNotVotedExistingUsername = {
- username1: {
- confirmed: true, unconfirmed: false, pending: false, ...delegate1,
- },
- };
- const state = {
- votes: { ...updateVotesDirtyNotVotedExistingUsername },
- };
- const votesRecordsUnchanged = {
- votes: { ...updateVotesDirtyNotVotedExistingUsername },
- };
- const changedState = voting(state, action);
- expect(changedState).to.be.deep.equal(votesRecordsUnchanged);
- });
+ describe('votesCleared', () => {
+ it('should revert votes to initial state', () => {
+ const action = {
+ type: actionTypes.votesCleared,
+ };
+ const changedState = voting(dirtyVotes, action);
- it('should set default (confirmed, unconfirmed, pending) values on username vote records, when non of previous cases are met', () => {
- const action = {
- type: actionTypes.votesUpdated,
- data: {
- list: [{ username: 'username1', ...delegate1 }],
- },
- };
- const updateVotesNonConditionsMet = {
- username1: {
- confirmed: true, unconfirmed: true, pending: true, ...delegate1,
- },
- };
- const state = {
- votes: { ...updateVotesNonConditionsMet },
- };
- const votesRecordsWithDefaultFlags = {
- votes: {
- username1: {
- confirmed: true, unconfirmed: true, pending: false, ...delegate1,
- },
- },
- };
- const changedState = voting(state, action);
- expect(changedState).to.be.deep.equal(votesRecordsWithDefaultFlags);
+ expect(changedState).toEqual(cleanVotes);
+ });
});
});
diff --git a/src/utils/account.js b/src/utils/account.js
index 543671df4a..3bcbae5fca 100644
--- a/src/utils/account.js
+++ b/src/utils/account.js
@@ -1,23 +1,38 @@
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client'; // eslint-disable-line
+
import { tokenMap } from '../constants/tokens';
+import { unlockTxDelayAvailability } from '../constants/account';
+import regex from './regex';
-export const extractPublicKey = (passphrase, apiVersion) => {
- const Lisk = liskClient(apiVersion);
- return Lisk.cryptography.getKeys(passphrase).publicKey;
+/**
+ * Extracts Lisk PublicKey from a given valid Mnemonic passphrase
+ *
+ * @param {String} passphrase - Valid Mnemonic passphrase
+ * @returns {String|Boolean} - Extracted publicKey for a given valid passphrase or
+ * false for a given invalid passphrase
+ */
+export const extractPublicKey = (passphrase) => {
+ if (Lisk.passphrase.Mnemonic.validateMnemonic(passphrase)) {
+ return Lisk.cryptography.getKeys(passphrase).publicKey;
+ }
+ return false;
};
/**
+ * Extracts Lisk address from given passphrase or publicKey
+ *
* @param {String} data - passphrase or public key
+ * @returns {String|Boolean} - Extracted address for a given valid passphrase or
+ * publicKey and false for a given invalid passphrase
*/
-export const extractAddress = (data, apiVersion) => {
- const Lisk = liskClient(apiVersion);
- if (!data) {
- return false;
+export const extractAddress = (data) => {
+ if (Lisk.passphrase.Mnemonic.validateMnemonic(data)) {
+ return Lisk.cryptography.getAddressFromPassphrase(data);
}
- if (data.indexOf(' ') < 0) {
+ if (regex.publicKey.test(data)) {
return Lisk.cryptography.getAddressFromPublicKey(data);
}
- return Lisk.cryptography.getAddressFromPassphrase(data);
+ return false;
};
export const getActiveTokenAccount = state => ({
@@ -28,3 +43,45 @@ export const getActiveTokenAccount = state => ({
: tokenMap.LSK.key
]) || {}),
});
+
+/**
+ * Returns a shorter version of a given address
+ * by replacing characters by ellipsis except for
+ * the first and last 3.
+ * @param {String} address LSk or BTC address
+ * @returns {String} Truncated address
+ */
+export const truncateAddress = address =>
+ address.replace(regex.lskAddressTrunk, '$1...$3');
+
+export const calculateLockedBalance = ({ votes = [] }) =>
+ votes.reduce((acc, vote) => acc + parseInt(vote.amount, 10), 0);
+
+// TODO handle delegate punishment when Lisk Service is ready
+export const getDelayedAvailability = isSelfVote => (isSelfVote
+ ? unlockTxDelayAvailability.selfUnvote : unlockTxDelayAvailability.unvote);
+
+export const isBlockHeightReached = ({ unvoteHeight, delegateAddress }, currentBlock, address) => {
+ if (!currentBlock) return false;
+ const currentBlockHeight = currentBlock.height;
+ const isSelfVote = address === delegateAddress;
+ const delayedAvailability = getDelayedAvailability(isSelfVote);
+ return currentBlockHeight - unvoteHeight > delayedAvailability;
+};
+
+export const getAvailableUnlockingTransactions = ({ unlocking = [], address }, currentBlock) =>
+ unlocking.filter(vote => isBlockHeightReached(vote, currentBlock, address));
+
+export const calculateAvailableBalance = ({ unlocking = [], address }, currentBlock) =>
+ unlocking.reduce(
+ (acc, vote) =>
+ (isBlockHeightReached(vote, currentBlock, address) ? acc + parseInt(vote.amount, 10) : acc),
+ 0,
+ );
+
+export const calculateUnlockingBalance = ({ unlocking = [], address }, currentBlock) =>
+ unlocking.reduce(
+ (acc, vote) =>
+ (!isBlockHeightReached(vote, currentBlock, address) ? acc + parseInt(vote.amount, 10) : acc),
+ 0,
+ );
diff --git a/src/utils/account.test.js b/src/utils/account.test.js
index f343800975..2b5e47c555 100644
--- a/src/utils/account.test.js
+++ b/src/utils/account.test.js
@@ -1,12 +1,18 @@
-import { expect } from 'chai';
-import { extractPublicKey, extractAddress, getActiveTokenAccount } from './account';
+import {
+ extractPublicKey,
+ extractAddress,
+ getActiveTokenAccount,
+ calculateAvailableBalance,
+ getAvailableUnlockingTransactions,
+ calculateLockedBalance,
+} from './account';
describe('Utils: Account', () => {
describe('extractPublicKey', () => {
it('should return a Hex string from any given string', () => {
const passphrase = 'field organ country moon fancy glare pencil combine derive fringe security pave';
const publicKey = 'a89751689c446067cc2107ec2690f612eb47b5939d5570d0d54b81eafaf328de';
- expect(extractPublicKey(passphrase)).to.be.equal(publicKey);
+ expect(extractPublicKey(passphrase)).toEqual(publicKey);
});
});
@@ -14,17 +20,17 @@ describe('Utils: Account', () => {
it('should return the account address from given passphrase', () => {
const passphrase = 'field organ country moon fancy glare pencil combine derive fringe security pave';
const derivedAddress = '440670704090200331L';
- expect(extractAddress(passphrase)).to.be.equal(derivedAddress);
+ expect(extractAddress(passphrase)).toEqual(derivedAddress);
});
it('should return the account address from given public key', () => {
const publicKey = 'a89751689c446067cc2107ec2690f612eb47b5939d5570d0d54b81eafaf328de';
const derivedAddress = '440670704090200331L';
- expect(extractAddress(publicKey)).to.be.equal(derivedAddress);
+ expect(extractAddress(publicKey)).toEqual(derivedAddress);
});
it('should return false if no param passed to it', () => {
- expect(extractAddress()).to.be.equal(false);
+ expect(extractAddress()).toEqual(false);
});
});
@@ -46,10 +52,82 @@ describe('Utils: Account', () => {
},
},
};
- expect(getActiveTokenAccount(state)).to.be.deep.equal({
+ expect(getActiveTokenAccount(state)).toStrictEqual({
...account,
...account.info[activeToken],
});
});
});
+
+ describe('calculateAvailableBalance', () => {
+ it('should get correct available balance', () => {
+ let unlocking = [
+ { amount: '1000000000', unvoteHeight: 4900, delegateAddress: '1L' },
+ { amount: '3000000000', unvoteHeight: 100, delegateAddress: '1L' },
+ { amount: '1000000000', unvoteHeight: 3000, delegateAddress: '3L' },
+ ];
+ const address = '80L';
+ const currentBlock = { height: 5000 };
+
+ expect(
+ calculateAvailableBalance({ unlocking, address }, currentBlock),
+ ).toEqual(3000000000);
+
+ unlocking = [
+ { amount: '1000000000', unvoteHeight: 4900, delegateAddress: '1L' },
+ { amount: '3000000000', unvoteHeight: 2500, delegateAddress: address },
+ { amount: '1000000000', unvoteHeight: 3000, delegateAddress: '3L' },
+ ];
+ expect(
+ calculateAvailableBalance({ unlocking, address }, currentBlock),
+ ).toEqual(0);
+ });
+
+ it('should return 0 when unlocking is undefined', () => {
+ const address = '80L';
+ const currentBlock = { height: 5000 };
+
+ expect(
+ calculateAvailableBalance({ address }, currentBlock),
+ ).toEqual(0);
+ });
+
+ describe('calculateLockedBalance', () => {
+ it('should get correct available balance', () => {
+ const votes = [
+ { amount: '5000000000', delegateAddress: '1L' },
+ { amount: '3000000000', delegateAddress: '3L' },
+ { amount: '2000000000', delegateAddress: '1L' },
+ ];
+
+ expect(calculateLockedBalance({ votes })).toEqual(10000000000);
+ });
+
+ it('should return 0 when unlocking is undefined', () => {
+ expect(calculateLockedBalance({ })).toEqual(0);
+ });
+ });
+
+ describe('getAvailableUnlockingTransactions', () => {
+ it('should get correct available balance', () => {
+ const unlocking = [
+ { amount: '1000000000', unvoteHeight: 5000, delegateAddress: '1L' },
+ { amount: '3000000000', unvoteHeight: 100, delegateAddress: '1L' },
+ { amount: '1000000000', unvoteHeight: 3100, delegateAddress: '3L' },
+ ];
+ const address = '80L';
+ const currentBlock = { height: 5000 };
+
+ expect(
+ getAvailableUnlockingTransactions({ unlocking, address }, currentBlock),
+ ).toEqual([{ amount: '3000000000', unvoteHeight: 100, delegateAddress: '1L' }]);
+ });
+
+ it('should return 0 when unlocking is undefined', () => {
+ const address = '80L';
+ const currentBlock = { height: 5000 };
+ expect(getAvailableUnlockingTransactions({ address }, currentBlock)).toEqual([]);
+ });
+ });
+ });
});
diff --git a/src/utils/api/blocks.test.js b/src/utils/api/blocks.test.js
index 96f4e362b4..b1b5462db6 100644
--- a/src/utils/api/blocks.test.js
+++ b/src/utils/api/blocks.test.js
@@ -22,7 +22,7 @@ const mockNetwork = {
LSK: {
nodeUrl: '',
code: 0,
- apiVersion: '2',
+ apiVersion: '2', // @todo Remove?
nethash: '',
},
},
diff --git a/src/utils/api/btc/account.js b/src/utils/api/btc/account.js
index 4c722876b7..c3bc198200 100644
--- a/src/utils/api/btc/account.js
+++ b/src/utils/api/btc/account.js
@@ -1,30 +1,27 @@
import * as bitcoin from 'bitcoinjs-lib';
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client'; // eslint-disable-line
import bip32 from 'bip32';
import { getAPIClient } from './network';
import { tokenMap } from '../../../constants/tokens';
-export const getDerivedPathFromPassphrase = (passphrase, config, apiVersion) => {
- const Lisk = liskClient(apiVersion);
- const seed = Lisk.passphrase.Mnemonic.mnemonicToSeed(passphrase);
+export const getDerivedPathFromPassphrase = (passphrase, config) => {
+ const seed = Lisk.passphrase.Mnemonic.mnemonicToSeedSync(passphrase);
return bip32.fromSeed(seed, config.network).derivePath(config.derivationPath);
};
-export const extractPublicKey = (passphrase, config, apiVersion) =>
- getDerivedPathFromPassphrase(passphrase, config, apiVersion).publicKey;
+export const extractPublicKey = (passphrase, config) =>
+ getDerivedPathFromPassphrase(passphrase, config).publicKey;
-export const extractAddress = (passphrase, config, apiVersion) => {
- const publicKey = extractPublicKey(passphrase, config, apiVersion);
+export const extractAddress = (passphrase, config) => {
+ const publicKey = extractPublicKey(passphrase, config);
const btc = bitcoin.payments.p2pkh({ pubkey: publicKey, network: config.network });
return btc.address;
};
-export const getAccount = ({
- network, address, passphrase,
-}) => new Promise(async (resolve, reject) => {
- const apiClient = getAPIClient(network);
- address = address || extractAddress(
- passphrase, apiClient.config, network.networks.LSK.apiVersion,
+export const getAccount = params => new Promise(async (resolve, reject) => {
+ const apiClient = getAPIClient(params.network);
+ const address = params.address || extractAddress(
+ params.passphrase, apiClient.config,
);
await apiClient.get(`account/${address}`).then((response) => {
resolve({
diff --git a/src/utils/api/btc/service.js b/src/utils/api/btc/service.js
index f4003e52d3..47fedc86d5 100644
--- a/src/utils/api/btc/service.js
+++ b/src/utils/api/btc/service.js
@@ -1,8 +1,8 @@
import * as popsicle from 'popsicle';
-import getBtcConfig from './config';
const liskServiceUrl = 'https://service.lisk.io';
+// eslint-disable-next-line import/prefer-default-export
export const getPriceTicker = () => new Promise(async (resolve, reject) => {
try {
const response = await popsicle.get(`${liskServiceUrl}/api/v1/market/prices`)
@@ -17,24 +17,3 @@ export const getPriceTicker = () => new Promise(async (resolve, reject) => {
reject(error);
}
});
-
-export const getDynamicFees = () => new Promise(async (resolve, reject) => {
- try {
- const config = getBtcConfig(0);
- const response = await popsicle.get(config.minerFeesURL)
- .use(popsicle.plugins.parse('json'));
-
- if (response) {
- const { body } = response;
- resolve({
- Low: body.hourFee,
- Medium: body.halfHourFee,
- High: body.fastestFee,
- });
- } else {
- reject(response);
- }
- } catch (error) {
- reject(error);
- }
-});
diff --git a/src/utils/api/btc/transactions.js b/src/utils/api/btc/transactions.js
index e468b80854..9cfc1b4018 100644
--- a/src/utils/api/btc/transactions.js
+++ b/src/utils/api/btc/transactions.js
@@ -1,5 +1,7 @@
+/* eslint-disable max-lines */
import * as bitcoin from 'bitcoinjs-lib';
import { BigNumber } from 'bignumber.js';
+import * as popsicle from 'popsicle';
import { extractAddress, getDerivedPathFromPassphrase } from './account';
import { getAPIClient, getNetworkCode } from './network';
@@ -7,6 +9,7 @@ import { tokenMap } from '../../../constants/tokens';
import { validateAddress } from '../../validators';
import getBtcConfig from './config';
import networks from '../../../constants/networks';
+import { fromRawLsk } from '../../lsk';
/**
* Normalizes transaction data retrieved from Blockchain.info API
@@ -84,13 +87,13 @@ export const getSingleTransaction = ({
* @param {Object} data
* @param {Number} data.inputCount
* @param {Number} data.outputCount
- * @param {Number} data.dynamicFeePerByte - in satoshis/byte.
+ * @param {Number} data.selectedFeePerByte - in satoshis/byte.
*/
export const calculateTransactionFee = ({
inputCount,
outputCount,
- dynamicFeePerByte,
-}) => ((inputCount * 180) + (outputCount * 34) + 10 + inputCount) * dynamicFeePerByte;
+ selectedFeePerByte,
+}) => ((inputCount * 180) + (outputCount * 34) + 10 + inputCount) * selectedFeePerByte;
/**
* Retrieves unspent tx outputs of a BTC address from Blockchain.info API
@@ -110,7 +113,7 @@ export const create = ({
passphrase,
recipientId: recipientAddress,
amount,
- dynamicFeePerByte,
+ selectedFeePerByte,
network,
// eslint-disable-next-line max-statements
}) => new Promise(async (resolve, reject) => {
@@ -119,7 +122,7 @@ export const create = ({
? networks.mainnet.code
: networks.testnet.code);
amount = Number(amount);
- dynamicFeePerByte = Number(dynamicFeePerByte);
+ selectedFeePerByte = Number(selectedFeePerByte);
const senderAddress = extractAddress(passphrase, config);
const unspentTxOuts = await getUnspentTransactionOutputs(senderAddress, network);
@@ -128,7 +131,7 @@ export const create = ({
const estimatedMinerFee = calculateTransactionFee({
inputCount: unspentTxOuts.length,
outputCount: 2,
- dynamicFeePerByte,
+ selectedFeePerByte,
});
const estimatedTotal = amount + estimatedMinerFee;
@@ -170,7 +173,7 @@ export const create = ({
const calculatedMinerFee = calculateTransactionFee({
inputCount: txOutsToConsume.length,
outputCount: 2,
- dynamicFeePerByte,
+ selectedFeePerByte,
});
// Calculate total
@@ -208,19 +211,19 @@ const getUnspentTransactionOutputCountToConsume = (satoshiValue, unspentTransact
};
export const getTransactionFeeFromUnspentOutputs = ({
- dynamicFeePerByte, satoshiValue, unspentTransactionOutputs,
+ selectedFeePerByte, satoshiValue, unspentTransactionOutputs,
}) => {
const feeInSatoshis = calculateTransactionFee({
inputCount: getUnspentTransactionOutputCountToConsume(satoshiValue, unspentTransactionOutputs),
outputCount: 2,
- dynamicFeePerByte,
+ selectedFeePerByte,
});
return calculateTransactionFee({
inputCount: getUnspentTransactionOutputCountToConsume(satoshiValue
+ feeInSatoshis, unspentTransactionOutputs),
outputCount: 2,
- dynamicFeePerByte,
+ selectedFeePerByte,
});
};
@@ -246,3 +249,56 @@ export const broadcast = (transactionHex, network) => new Promise(async (resolve
reject(error);
}
});
+
+/**
+ * Returns a dictionary of base fees for low, medium and high processing speeds
+ */
+export const getTransactionBaseFees = () => new Promise(async (resolve, reject) => {
+ try {
+ const config = getBtcConfig(0);
+ const response = await popsicle.get(config.minerFeesURL)
+ .use(popsicle.plugins.parse('json'));
+
+ if (response) {
+ const { body } = response;
+ resolve({
+ Low: body.hourFee,
+ Medium: body.halfHourFee,
+ High: body.fastestFee,
+ });
+ } else {
+ reject(response);
+ }
+ } catch (error) {
+ reject(error);
+ }
+});
+
+/**
+ * Returns the actual tx fee based on given tx details and selected processing speed
+ * @param {String} address - Account address
+ * @param {Object} network - network configuration
+ */
+export const getTransactionFee = async ({
+ account, network, txData, selectedPriority,
+}) => {
+ const unspentTransactionOutputs = await getUnspentTransactionOutputs(
+ account.address, network,
+ );
+
+ const value = fromRawLsk(getTransactionFeeFromUnspentOutputs({
+ unspentTransactionOutputs,
+ satoshiValue: txData.amount || 0,
+ selectedFeePerByte: selectedPriority.value,
+ }));
+
+ const feedback = txData.amount === 0
+ ? '-'
+ : `${(value ? '' : 'Invalid amount')}`;
+
+ return {
+ value,
+ error: !!feedback,
+ feedback,
+ };
+};
diff --git a/src/utils/api/delegates.js b/src/utils/api/delegates.js
index 392fb5a886..2a9f4f06c6 100644
--- a/src/utils/api/delegates.js
+++ b/src/utils/api/delegates.js
@@ -1,12 +1,7 @@
-import { to } from 'await-to-js';
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client'; // eslint-disable-line
import { getBlocks } from './blocks';
import { getTransactions } from './transactions';
-import { loadDelegateCache, updateDelegateCache } from '../delegates';
-import { loginType } from '../../constants/hwConstants';
-import { splitVotesIntoRounds } from '../voting';
import transactionTypes from '../../constants/transactionTypes';
-import { signVoteTransaction } from '../hwManager';
import { getAPIClient } from './lsk/network';
export const getDelegates = (network, options) =>
@@ -17,7 +12,6 @@ export const getDelegateInfo = (liskAPIClient, { address, publicKey }) => (
try {
const response = await getDelegates(liskAPIClient, { address });
const delegate = response.data[0];
- updateDelegateCache(response.data, liskAPIClient.network);
if (delegate) {
const txDelegateRegister = (await getTransactions({
liskAPIClient,
@@ -42,108 +36,8 @@ export const getDelegateInfo = (liskAPIClient, { address, publicKey }) => (
})
);
-export const getDelegateWithCache = (liskAPIClient, { publicKey }) => (
- new Promise(async (resolve, reject) => {
- loadDelegateCache(liskAPIClient.network, async (data) => {
- const storedDelegate = data[publicKey];
- if (storedDelegate) {
- resolve(storedDelegate);
- } else {
- const [error, response] = await to(getDelegates(liskAPIClient, { publicKey }));
- if (error) {
- reject(error);
- } else if (response.data[0]) {
- updateDelegateCache(response.data, liskAPIClient.network);
- resolve(response.data[0]);
- } else {
- reject(new Error(`No delegate with publicKey ${publicKey} found.`));
- }
- }
- });
- })
-);
-
-export const getDelegateByName = (liskAPIClient, name) => new Promise(async (resolve, reject) => {
- // eslint-disable-next-line max-statements
- loadDelegateCache(liskAPIClient.network, async (data) => {
- const storedDelegate = data[name];
- if (storedDelegate) {
- resolve(storedDelegate);
- } else {
- const [error, response] = await to(liskAPIClient.delegates.get({ search: name, limit: 101 }));
- if (error) {
- reject(error);
- } else {
- const delegate = response.data.find(({ username }) => username === name);
- if (delegate) {
- resolve(delegate);
- } else {
- reject(new Error(`No delegate with name ${name} found.`));
- }
- updateDelegateCache(response.data, liskAPIClient.network);
- }
- }
- });
-});
-
-const voteWithPassphrase = (
- passphrase,
- votes,
- unvotes,
- secondPassphrase,
- timeOffset,
- networkIdentifier,
- apiVersion,
-) => (Promise.all(splitVotesIntoRounds({ votes: [...votes], unvotes: [...unvotes] })
- // eslint-disable-next-line no-shadow
- .map(({ votes, unvotes }) => {
- const Lisk = liskClient(apiVersion);
- return (Lisk.transaction.castVotes(
- {
- votes,
- unvotes,
- passphrase,
- secondPassphrase,
- timeOffset,
- networkIdentifier,
- },
- ));
- }))
-);
-
-export const castVotes = async ({
- liskAPIClient,
- account,
- votedList,
- unvotedList,
- secondPassphrase,
- timeOffset,
- networkIdentifier,
- apiVersion,
-}) => {
- const signedTransactions = account.loginType === loginType.normal
- ? await voteWithPassphrase(
- account.passphrase,
- votedList,
- unvotedList,
- secondPassphrase,
- timeOffset,
- networkIdentifier,
- apiVersion,
- )
- : await signVoteTransaction(account, votedList, unvotedList, timeOffset, networkIdentifier);
-
- return Promise.all(signedTransactions.map(transaction => (
- new Promise((resolve, reject) => {
- liskAPIClient.transactions.broadcast(transaction)
- .then(() => resolve(transaction))
- .catch(reject);
- })
- )));
-};
-
export const getVotes = (network, { address }) =>
- getAPIClient(network).votes.get({ address, limit: 101, offset: 0 });
+ getAPIClient(network).votes.get({ address });
export const registerDelegate = (
liskAPIClient,
@@ -152,14 +46,12 @@ export const registerDelegate = (
secondPassphrase = null,
timeOffset,
networkIdentifier,
- apiVersion,
) => {
const data = { username, passphrase, timeOffset };
if (secondPassphrase) {
data.secondPassphrase = secondPassphrase;
}
return new Promise((resolve, reject) => {
- const Lisk = liskClient(apiVersion);
const transaction = Lisk.transaction.registerDelegate({ ...data, networkIdentifier });
liskAPIClient.transactions
.broadcast(transaction)
diff --git a/src/utils/api/delegates.test.js b/src/utils/api/delegates.test.js
index 06d265f06c..ad9e283478 100644
--- a/src/utils/api/delegates.test.js
+++ b/src/utils/api/delegates.test.js
@@ -1,17 +1,13 @@
-import { to } from 'await-to-js';
-import Lisk from '@liskhq/lisk-client-old';
+import Lisk from '@liskhq/lisk-client';
import { expect } from 'chai';
import sinon from 'sinon';
import {
- castVotes,
- getDelegateByName,
- getDelegateWithCache,
getDelegateInfo,
getDelegates,
getVotes,
registerDelegate,
} from './delegates';
-import { loginType } from '../../constants/hwConstants';
+// import { loginType } from '../../constants/hwConstants';
import accounts from '../../../test/constants/accounts';
import delegates from '../../../test/constants/delegates';
import * as hwManager from '../hwManager';
@@ -119,74 +115,6 @@ describe('Utils: Delegate', () => {
});
});
- describe('getDelegateWithCache', () => {
- const network = { name: 'Mainnet' };
- it.skip('should resolve based on given publicKey', async () => {
- const { publicKey } = delegates[0].account;
- liskAPIClientMockDelegates.expects('get').withArgs({
- publicKey,
- }).resolves({ data: [delegates[0]] });
-
- const resolved = await getDelegateWithCache(liskAPIClient, { publicKey, network });
- expect(resolved).to.equal(delegates[0]);
- });
-
- it.skip('should resolve from cache if called twice', async () => {
- const { publicKey } = delegates[0].account;
- liskAPIClientMockDelegates.expects('get').withArgs({
- publicKey,
- }).resolves({ data: [delegates[0]] });
-
- await getDelegateWithCache(liskAPIClient, { publicKey, network });
- const resolved = await getDelegateWithCache(liskAPIClient, { publicKey, network });
- expect(resolved).to.deep.equal(delegates[0]);
- });
-
- it.skip('should reject if delegate not found', async () => {
- const { publicKey } = delegates[0].account;
- liskAPIClientMockDelegates.expects('get').withArgs({
- publicKey,
- }).resolves({ data: [] });
-
- const [error] = await to(getDelegateWithCache(liskAPIClient, { publicKey, network }));
- expect(error.message).to.equal(`No delegate with publicKey ${publicKey} found.`);
- });
-
- it.skip('should reject if delegate request failed', async () => {
- const error = 'Any network error';
- const { publicKey } = delegates[0].account;
- liskAPIClientMockDelegates.expects('get').withArgs({
- publicKey,
- }).rejects(error);
-
- expect(await to(
- getDelegateWithCache(liskAPIClient, { publicKey, network }),
- )).to.deep.equal([error, undefined]);
- });
- });
-
- describe('getDelegateByName', () => {
- it.skip('should resolve delegate genesis_3 if name = genesis_3', () => {
- const name = delegates[0].username;
- liskAPIClientMockDelegates.expects('get').withArgs({
- search: name, limit: 101,
- }).resolves({ data: delegates });
-
- const returnedPromise = getDelegateByName(liskAPIClient, name);
- expect(returnedPromise).to.eventually.equal(delegates[0]);
- });
-
- it.skip('should reject if given name does not exist', () => {
- const name = `${delegates[0].username}_not_exist`;
- liskAPIClientMockDelegates.expects('get').withArgs({
- search: name, limit: 101,
- }).resolves({ data: [] });
-
- const returnedPromise = getDelegateByName(liskAPIClient, name);
- expect(returnedPromise).to.be.rejectedWith();
- });
- });
-
describe('getVotes', () => {
it.skip('should get votes for an address with no parameters', () => {
const address = '123L';
@@ -233,70 +161,70 @@ describe('Utils: Delegate', () => {
});
describe('castVotes', () => {
- it.skip('should call castVotes and broadcast transaction regular login', async () => {
- const votes = [
- accounts.genesis.publicKey,
- accounts.delegate.publicKey,
- ];
- const unvotes = [
- accounts.empty_account.publicKey,
- accounts.delegate_candidate.publicKey,
- ];
- const transaction = { id: '1234' };
- const secondPassphrase = null;
- liskTransactionsCastVotesStub.withArgs({
- votes,
- unvotes,
- passphrase: accounts.genesis.passphrase,
- secondPassphrase,
- timeOffset,
- }).returns(transaction);
-
- await castVotes({
- liskAPIClient,
- account: {
- ...accounts.genesis,
- loginType: loginType.normal,
- },
- votedList: votes,
- unvotedList: unvotes,
- secondPassphrase,
- timeOffset,
- });
- expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);
- });
-
- it.skip('should call castVotes and broadcast transaction with hardware wallet', async () => {
- const votes = [
- accounts.genesis.publicKey,
- accounts.delegate.publicKey,
- ];
- const unvotes = [
- accounts.empty_account.publicKey,
- accounts.delegate_candidate.publicKey,
- ];
- const transaction = { id: '1234' };
- const secondPassphrase = null;
- liskTransactionsCastVotesStub.withArgs({
- votes,
- unvotes,
- passphrase: accounts.genesis.passphrase,
- secondPassphrase,
- timeOffset,
- }).returns(transaction);
-
- await castVotes({
- liskAPIClient,
- account: {
- ...accounts.genesis,
- loginType: loginType.ledger,
- },
- votedList: votes,
- unvotedList: unvotes,
- secondPassphrase,
- timeOffset,
- });
- expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);
- });
+ // it.skip('should call castVotes and broadcast transaction regular login', async () => {
+ // const votes = [
+ // accounts.genesis.publicKey,
+ // accounts.delegate.publicKey,
+ // ];
+ // const unvotes = [
+ // accounts.empty_account.publicKey,
+ // accounts.delegate_candidate.publicKey,
+ // ];
+ // const transaction = { id: '1234' };
+ // const secondPassphrase = null;
+ // liskTransactionsCastVotesStub.withArgs({
+ // votes,
+ // unvotes,
+ // passphrase: accounts.genesis.passphrase,
+ // secondPassphrase,
+ // timeOffset,
+ // }).returns(transaction);
+
+ // await castVotes({
+ // liskAPIClient,
+ // account: {
+ // ...accounts.genesis,
+ // loginType: loginType.normal,
+ // },
+ // votedList: votes,
+ // unvotedList: unvotes,
+ // secondPassphrase,
+ // timeOffset,
+ // });
+ // expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);
+ // });
+
+ // it.skip('should call castVotes and broadcast transaction with hardware wallet', async () => {
+ // const votes = [
+ // accounts.genesis.publicKey,
+ // accounts.delegate.publicKey,
+ // ];
+ // const unvotes = [
+ // accounts.empty_account.publicKey,
+ // accounts.delegate_candidate.publicKey,
+ // ];
+ // const transaction = { id: '1234' };
+ // const secondPassphrase = null;
+ // liskTransactionsCastVotesStub.withArgs({
+ // votes,
+ // unvotes,
+ // passphrase: accounts.genesis.passphrase,
+ // secondPassphrase,
+ // timeOffset,
+ // }).returns(transaction);
+
+ // await castVotes({
+ // liskAPIClient,
+ // account: {
+ // ...accounts.genesis,
+ // loginType: loginType.ledger,
+ // },
+ // votedList: votes,
+ // unvotedList: unvotes,
+ // secondPassphrase,
+ // timeOffset,
+ // });
+ // expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);
+ // });
});
});
diff --git a/src/utils/api/lisk-client.js b/src/utils/api/lisk-client.js
deleted file mode 100644
index 6b0939b522..0000000000
--- a/src/utils/api/lisk-client.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Lisk2x from '@liskhq/lisk-client-old';
-import Lisk3x from '@liskhq/lisk-client';
-
-export default function (version) {
- switch (version) {
- case '3':
- return Lisk3x;
- case '2':
- return Lisk2x;
- default:
- return Lisk2x;
- }
-}
diff --git a/src/utils/api/lsk/account.js b/src/utils/api/lsk/account.js
index 81116ea35b..ce4442dcb5 100644
--- a/src/utils/api/lsk/account.js
+++ b/src/utils/api/lsk/account.js
@@ -1,51 +1,39 @@
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client'; // eslint-disable-line
import api from '..';
import { tokenMap } from '../../../constants/tokens';
import { getAPIClient } from './network';
import { extractAddress, extractPublicKey } from '../../account';
-export const getAccount = ({
- network,
- address,
- passphrase,
- publicKey,
-}) =>
+export const getAccount = params =>
new Promise((resolve, reject) => {
- // TODO remove liskAPIClient after all code that uses is is removed
- const apiClient = getAPIClient(network);
- if (!apiClient) {
- reject();
+ const apiClient = getAPIClient(params.network);
+
+ const publicKey = params.publicKey || extractPublicKey(params.passphrase);
+ const address = params.address || extractAddress(params.passphrase || publicKey);
+
+ if (!apiClient || (!address && !params.username)) {
+ reject(Error('Malformed parameters.'));
return;
}
- const apiVersion = network.networks.LSK.apiVersion;
- publicKey = publicKey || (passphrase && extractPublicKey(passphrase, apiVersion));
- address = address || extractAddress(passphrase || publicKey);
+ const query = address ? { address } : { username: params.username };
+
+ apiClient.accounts.get(query).then((res) => {
+ const offlineInfo = {
+ address,
+ publicKey,
+ token: tokenMap.LSK.key,
+ };
+ const onlineInfo = res.data.length > 0
+ ? { serverPublicKey: res.data[0].publicKey, ...res.data[0] }
+ : { balance: 0 };
- apiClient.accounts.get({ address }).then((res) => {
- if (res.data.length > 0) {
- resolve({
- ...res.data[0],
- // It is necessary to disable this rule, because eslint --fix would
- // change it to publicKey || res.data[0].publicKey
- // but that is not equivalent to the ternary if the first value is
- // defined and the second one not.
- // eslint-disable-next-line no-unneeded-ternary
- publicKey: publicKey ? publicKey : res.data[0].publicKey,
- serverPublicKey: res.data[0].publicKey,
- token: tokenMap.LSK.key,
- });
- } else {
- // when the account has no transactions yet (therefore is not saved on the blockchain)
- // this endpoint returns { success: false }
- resolve({
- address,
- publicKey,
- balance: 0,
- token: tokenMap.LSK.key,
- });
- }
+ resolve({
+ ...offlineInfo,
+ ...onlineInfo,
+ publicKey: offlineInfo.publicKey || onlineInfo.publicKey,
+ });
}).catch(reject);
});
@@ -56,10 +44,9 @@ export const setSecondPassphrase = (
passphrase,
timeOffset,
networkIdentifier,
- apiVersion,
) =>
new Promise((resolve, reject) => {
- const transaction = liskClient(apiVersion).transaction
+ const { transaction } = Lisk
.registerSecondPassphrase({
passphrase,
secondPassphrase,
diff --git a/src/utils/api/lsk/account.test.js b/src/utils/api/lsk/account.test.js
index 49ed1ac146..bba2d22ed9 100644
--- a/src/utils/api/lsk/account.test.js
+++ b/src/utils/api/lsk/account.test.js
@@ -6,7 +6,7 @@ import { getAPIClient } from './network';
const network = {
networks: {
LSK: {
- apiVersion: '',
+ apiVersion: '', // @todo Remove?
},
},
};
diff --git a/src/utils/api/lsk/adapters.js b/src/utils/api/lsk/adapters.js
index daf3ffb7e4..87642de67a 100644
--- a/src/utils/api/lsk/adapters.js
+++ b/src/utils/api/lsk/adapters.js
@@ -1,4 +1,5 @@
-const defaultApiVersion = '2';
+import transactionTypes from '../../../constants/transactionTypes';
+
/**
* Transforms transactions of Core 3.x to the shape of
* transactions in Core 2.x
@@ -10,26 +11,25 @@ const defaultApiVersion = '2';
* Morphed transaction in the shape of Core 2.x transactions.
*/
export const txAdapter = (
- data, apiVersion = defaultApiVersion,
+ data,
) => { // eslint-disable-line import/prefer-default-export
- if (apiVersion === defaultApiVersion) return data;
- const morphedData = { ...data };
const { type } = data;
+ if (type < 8) return data;
+ const morphedData = { ...data };
if (type === 8 && data.asset.recipientId && data.asset.amount) {
morphedData.recipientId = data.asset.recipientId;
morphedData.amount = data.asset.amount;
}
if (type >= 8) {
- morphedData.type -= 8;
+ morphedData.type = transactionTypes.getByCode(type).code.legacy;
}
return morphedData;
};
export const adaptTransaction = (
- res, apiVersion = defaultApiVersion,
+ res,
) => { // eslint-disable-line import/prefer-default-export
- if (apiVersion === defaultApiVersion) return res;
const morphedData = res.data.map(transaction => txAdapter(transaction));
return {
links: res.links,
@@ -39,9 +39,8 @@ export const adaptTransaction = (
};
export const adaptTransactions = (
- res, apiVersion = defaultApiVersion,
+ res,
) => { // eslint-disable-line import/prefer-default-export
- if (apiVersion === defaultApiVersion) return res;
const morphedData = res.data.map(transaction => txAdapter(transaction));
return {
links: res.links,
@@ -50,8 +49,7 @@ export const adaptTransactions = (
};
};
-export const adaptDelegateQueryParams = (params, apiVersion = defaultApiVersion) => {
- if (apiVersion === defaultApiVersion) return params;
+export const adaptDelegateQueryParams = (params) => {
const morphedParams = {
...params,
};
diff --git a/src/utils/api/lsk/liskService.js b/src/utils/api/lsk/liskService.js
index 7628271ff2..acecf7daf8 100644
--- a/src/utils/api/lsk/liskService.js
+++ b/src/utils/api/lsk/liskService.js
@@ -1,5 +1,6 @@
-import { utils } from '@liskhq/lisk-transactions';
-import { cryptography } from '@liskhq/lisk-client';
+/* eslint-disable max-lines */
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { cryptography, transactions } from '@liskhq/lisk-client';
import io from 'socket.io-client';
import * as popsicle from 'popsicle';
import { DEFAULT_LIMIT } from '../../../constants/monitor';
@@ -13,12 +14,12 @@ import transactionTypes from '../../../constants/transactionTypes';
const formatDate = (value, options) => getTimestampFromFirstBlock(value, 'DD.MM.YY', options);
const liskServiceGet = ({
- path, transformResponse = x => x, searchParams = {}, network,
+ path, transformResponse = x => x, searchParams = {}, network, baseUrl = 'serviceUrl',
}) => new Promise((resolve, reject) => {
if (network.serviceUrl === 'unavailable') {
reject(new Error('Lisk Service is not available for this network.'));
} else {
- popsicle.get(`${network.serviceUrl}${path}?${new URLSearchParams(searchParams)}`)
+ popsicle.get(`${network[baseUrl]}${path}?${new URLSearchParams(searchParams)}`)
.use(popsicle.plugins.parse('json'))
.then((response) => {
if (response.statusType() === 2) {
@@ -55,6 +56,7 @@ const liskServiceApi = {
path: '/api/v1/market/prices',
transformResponse: response => response.data,
network,
+ baseUrl: 'cloudUrl',
}),
getNewsFeed: (network, searchParams) => liskServiceGet({
@@ -62,6 +64,7 @@ const liskServiceApi = {
searchParams,
transformResponse: response => response.data,
network,
+ baseUrl: 'cloudUrl',
}),
getLastBlocks: async (
@@ -94,8 +97,8 @@ const liskServiceApi = {
limit: DEFAULT_LIMIT,
...(dateFrom && { from: formatDate(dateFrom) }),
...(dateTo && { to: formatDate(dateTo, { inclusive: true }) }),
- ...(amountFrom && { min: utils.convertLSKToBeddows(amountFrom) }),
- ...(amountTo && { max: utils.convertLSKToBeddows(amountTo) }),
+ ...(amountFrom && { min: transactions.utils.convertLSKToBeddows(amountFrom) }),
+ ...(amountTo && { max: transactions.utils.convertLSKToBeddows(amountTo) }),
...searchParams,
},
network,
@@ -125,6 +128,12 @@ const liskServiceApi = {
network,
}),
+ getTransactionBaseFees: async network => liskServiceGet({
+ path: '/api/v1/fee_estimates',
+ searchParams: {},
+ network,
+ }),
+
getActiveDelegates: async (network, { search = '', tab, ...searchParams }) => liskServiceGet({
path: '/api/v1/delegates/next_forgers',
transformResponse: response => ({
@@ -268,6 +277,15 @@ const liskServiceApi = {
return { data, meta: voteTransactions.meta };
},
+ getAccounts: async (network, addressList) => {
+ const results = await liskServiceSocketGet(addressList.map(address => ({
+ method: 'get.accounts',
+ params: { address },
+ })));
+
+ return results;
+ },
+
getVoteNames: async (network, params) => {
const request = params.publicKeys.map(publickey => ({
method: 'get.accounts',
diff --git a/src/utils/api/lsk/network.js b/src/utils/api/lsk/network.js
index 4e3b3903f0..4b24e99196 100644
--- a/src/utils/api/lsk/network.js
+++ b/src/utils/api/lsk/network.js
@@ -1,4 +1,4 @@
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client'; // eslint-disable-line
import networks from '../../../constants/networks';
import { tokenMap } from '../../../constants/tokens';
@@ -6,7 +6,6 @@ const apiClients = {};
// eslint-disable-next-line import/prefer-default-export
export const getAPIClient = (network) => {
- const Lisk = liskClient(network.networks.LSK.apiVersion);
if (network.name && (!apiClients[network.name] || network.name === networks.customNode.name)) {
const { nethash, nodes } = {
[networks.testnet.name]: {
diff --git a/src/utils/api/lsk/network.test.js b/src/utils/api/lsk/network.test.js
index 45704a30f3..46ef4052b0 100644
--- a/src/utils/api/lsk/network.test.js
+++ b/src/utils/api/lsk/network.test.js
@@ -1,4 +1,4 @@
-import Lisk from '@liskhq/lisk-client-old';
+import Lisk from '@liskhq/lisk-client';
import networks from '../../../constants/networks';
import { getAPIClient } from './network';
@@ -25,7 +25,7 @@ describe('Utils: network LSK API', () => {
Lisk.APIClient = APIClientBackup;
});
- it.skip('should create a new mainnet Lisk APIClient instance if network is mainnet', () => {
+ it('should create a new mainnet Lisk APIClient instance if network is mainnet', () => {
const nethash = Lisk.APIClient.constants.MAINNET_NETHASH;
const network = {
name: networks.mainnet.name,
@@ -41,7 +41,7 @@ describe('Utils: network LSK API', () => {
expect(apiClient).toEqual(getAPIClient(network));
});
- it.skip('should create a new testnet Lisk APIClient instance if network is testnet', () => {
+ it('should create a new testnet Lisk APIClient instance if network is testnet', () => {
const nethash = Lisk.APIClient.constants.TESTNET_NETHASH;
const network = {
name: networks.testnet.name,
@@ -54,7 +54,7 @@ describe('Utils: network LSK API', () => {
expect(constructorSpy).toHaveBeenCalledWith(networks.testnet.nodes, { nethash });
});
- it.skip('should create a new customNode Lisk APIClient instance if network is customNode', () => {
+ it('should create a new customNode Lisk APIClient instance if network is customNode', () => {
const nethash = '198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d';
const nodeUrl = 'http://localhost:4000';
const network = {
diff --git a/src/utils/api/lsk/transactions.js b/src/utils/api/lsk/transactions.js
index 8285cb30ed..054acd70f0 100644
--- a/src/utils/api/lsk/transactions.js
+++ b/src/utils/api/lsk/transactions.js
@@ -1,14 +1,15 @@
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
+import Lisk from '@liskhq/lisk-client';
import { getAPIClient } from './network';
import { getTimestampFromFirstBlock } from '../../datetime';
-import { toRawLsk } from '../../lsk';
+import { toRawLsk, fromRawLsk } from '../../lsk';
import txFilters from '../../../constants/transactionFilters';
-import transactionTypes from '../../../constants/transactionTypes';
+import transactionTypes, { minFeePerByte } from '../../../constants/transactionTypes';
import { adaptTransactions, adaptTransaction } from './adapters';
+import liskService from './liskService';
const parseTxFilters = (filter = txFilters.all, address) => ({
- [txFilters.incoming]: { recipientId: address, type: transactionTypes().send.outgoingCode },
- [txFilters.outgoing]: { senderId: address, type: transactionTypes().send.outgoingCode },
+ [txFilters.incoming]: { recipientId: address, type: transactionTypes().transfer.outgoingCode },
+ [txFilters.outgoing]: { senderId: address, type: transactionTypes().transfer.outgoingCode },
[txFilters.all]: { senderIdOrRecipientId: address },
}[filter]);
@@ -33,13 +34,13 @@ const parseCustomFilters = filters => ({
});
export const getTransactions = ({
- network, liskAPIClient, address, limit, offset, type = undefined,
- sort = 'timestamp:desc', filters = {},
+ network, liskAPIClient, address, limit,
+ offset, type = undefined, filters = {},
}) => {
const params = {
limit,
offset,
- sort,
+ // sort, @todo Fix the sort
...parseTxFilters(filters.direction, address),
...parseCustomFilters(filters),
...(type !== undefined ? { type } : {}),
@@ -72,14 +73,66 @@ export const getSingleTransaction = ({
}).catch(reject);
});
+const txTypeClassMap = {
+ transfer: Lisk.transactions.TransferTransaction,
+ registerDelegate: Lisk.transactions.DelegateTransaction,
+ vote: Lisk.transactions.VoteTransaction,
+ unlockToken: Lisk.transaction.UnlockTransaction,
+};
+
+
+// eslint-disable-next-line max-statements
+export const createTransactionInstance = (rawTx, type) => {
+ const FEE_BYTES_PLACEHOLDER = '18446744073709551615';
+ const SIGNATURE_BYTES_PLACEHOLDER = '204514eb1152355799ece36d17037e5feb4871472c60763bdafe67eb6a38bec632a8e2e62f84a32cf764342a4708a65fbad194e37feec03940f0ff84d3df2a05';
+ const asset = {
+ data: rawTx.data,
+ };
+
+ switch (type) {
+ case 'transfer':
+ asset.recipientId = rawTx.recipient;
+ asset.amount = rawTx.amount;
+ break;
+ case 'registerDelegate':
+ asset.username = rawTx.username || 'abcde';
+ break;
+ case 'vote':
+ asset.votes = rawTx.votes;
+ break;
+ case 'unlockToken':
+ asset.unlockingObjects = rawTx.unlockingObjects;
+ break;
+ default:
+ break;
+ }
+
+ const TxClass = txTypeClassMap[type];
+ const tx = new TxClass({
+ senderPublicKey: rawTx.senderPublicKey,
+ nonce: rawTx.nonce,
+ asset,
+ fee: FEE_BYTES_PLACEHOLDER,
+ signatures: [SIGNATURE_BYTES_PLACEHOLDER],
+ });
+
+ return tx;
+};
+
+/**
+ * creates a new transaction
+ * @param {Object} transaction
+ * @param {string} transactionType
+ * @returns {Promise} promise that resolves to a transaction or rejects with an error
+ */
export const create = (
- transaction, transactionType, apiVersion,
+ transaction, transactionType,
) => new Promise((resolve, reject) => {
try {
- const Lisk = liskClient(apiVersion);
const { networkIdentifier } = transaction.network.networks.LSK;
const tx = Lisk.transaction[transactionType]({
...transaction,
+ fee: transaction.fee.toString(),
networkIdentifier,
});
resolve(tx);
@@ -88,11 +141,79 @@ export const create = (
}
});
-export const broadcast = (transaction, network) => new Promise(async (resolve, reject) => {
- try {
- await getAPIClient(network).transactions.broadcast(transaction);
- resolve(transaction);
- } catch (error) {
- reject(error);
+/**
+ * broadcasts a transaction over the network
+ * @param {object} transaction
+ * @param {object} network
+ * @returns {Promise} promise that resolves to a transaction or rejects with an error
+ */
+export const broadcast = (transaction, network) => new Promise(
+ async (resolve, reject) => {
+ try {
+ await getAPIClient(network).transactions.broadcast(transaction);
+ resolve(transaction);
+ } catch (error) {
+ reject(error);
+ }
+ },
+);
+
+/**
+ * Returns a dictionary of base fees for low, medium and high processing speeds
+ *
+ * @todo The current implementation mocks the results with realistic values.
+ * We will refactor this function to fetch the base fees from Lisk Service
+ * when the endpoint is ready. Refer to #3081
+ *
+ * @returns {Promise<{Low: number, Medium: number, High: number}>} with low,
+ * medium and high priority fee options
+ */
+export const getTransactionBaseFees = network => liskService.getTransactionBaseFees(network)
+ .then((response) => {
+ const { feeEstimatePerByte } = response.data;
+ return {
+ Low: feeEstimatePerByte.low || 0,
+ Medium: feeEstimatePerByte.medium || 0,
+ High: feeEstimatePerByte.high || 0,
+ };
+ });
+
+export const getMinTxFee = tx => Number(tx.minFee.toString());
+
+/**
+ * Returns the actual tx fee based on given tx details and selected processing speed
+ * @param {String} txData - The transaction object
+ * @param {Object} selectedPriority - network configuration
+ */
+// eslint-disable-next-line max-statements
+export const getTransactionFee = async ({
+ txData, selectedPriority,
+}) => {
+ const { txType, ...data } = txData;
+ const tx = createTransactionInstance(data, txType);
+ const minFee = getMinTxFee(tx);
+ const feePerByte = selectedPriority.value;
+ const hardCap = transactionTypes.getHardCap(txType);
+
+ // Tie breaker is only meant for Medium and high processing speeds
+ const tieBreaker = selectedPriority.selectedIndex === 0
+ ? 0 : minFeePerByte * feePerByte * Math.random();
+
+ const size = tx.getBytes().length;
+ let value = minFee + feePerByte * size + tieBreaker;
+
+ if (value > hardCap) {
+ value = hardCap;
}
-});
+
+ const roundedValue = parseFloat(Number(fromRawLsk(value)).toFixed(8));
+ const feedback = data.amount === ''
+ ? '-'
+ : `${(value ? '' : 'Invalid amount')}`;
+
+ return {
+ value: roundedValue,
+ error: !!feedback,
+ feedback,
+ };
+};
diff --git a/src/utils/api/lsk/transactions.test.js b/src/utils/api/lsk/transactions.test.js
index e75eda475f..dfc6502df6 100644
--- a/src/utils/api/lsk/transactions.test.js
+++ b/src/utils/api/lsk/transactions.test.js
@@ -1,18 +1,40 @@
import { to } from 'await-to-js';
-import Lisk from '@liskhq/lisk-client-old';
+import Lisk from '@liskhq/lisk-client';
import {
getTransactions,
getSingleTransaction,
create,
broadcast,
+ getMinTxFee,
+ getTransactionBaseFees,
+ getTransactionFee, createTransactionInstance,
} from './transactions';
import networks from '../../../constants/networks';
import { getAPIClient } from './network';
import txFilters from '../../../constants/transactionFilters';
import transactionTypes from '../../../constants/transactionTypes';
import { getTimestampFromFirstBlock } from '../../datetime';
+import accounts from '../../../../test/constants/accounts';
+import { fromRawLsk } from '../../lsk';
+import liskService from './liskService';
jest.mock('./network');
+jest.mock('./liskService');
+const TESTNET_NETHASH = 'da3ed6a45429278bac2666961289ca17ad86595d33b31037615d4b8e8f158bba';
+
+const testTx = {
+ amount: '1',
+ data: 'payment',
+ passphrase: accounts.genesis.passphrase,
+ recipientId: '123L',
+ nonce: '1',
+ fee: '123',
+ network: {
+ networks: {
+ LSK: { networkIdentifier: TESTNET_NETHASH },
+ },
+ },
+};
describe('Utils: Transactions API', () => {
const id = '124701289470';
@@ -28,6 +50,8 @@ describe('Utils: Transactions API', () => {
const address = '1212409187243L';
beforeEach(() => {
+ jest.clearAllMocks();
+
apiClient = {
transactions: {
get: jest.fn(),
@@ -121,18 +145,10 @@ describe('Utils: Transactions API', () => {
});
describe('create', () => {
- it.skip('should create a transaction and return a promise', async () => {
- const tx = {
- amount: '1',
- data: { data: 'payment' },
- passphrase: 'abc',
- recipientId: '123L',
- secondPassphrase: null,
- timeOffset: 0,
- };
- const txResult = await create(tx, transactionTypes().send.key);
- expect(txResult.recipientId).toEqual(tx.recipientId);
- expect(txResult.amount).toEqual(tx.amount);
+ it('should create a transaction and return a promise', async () => {
+ const txResult = await create(testTx, transactionTypes().transfer.key);
+ expect(txResult.asset.recipientId).toEqual(testTx.recipientId);
+ expect(txResult.asset.amount).toEqual(testTx.amount);
expect(txResult.signature).not.toBeNull();
expect(txResult.id).not.toBeNull();
expect(txResult.senderPublicKey).not.toBeNull();
@@ -143,21 +159,9 @@ describe('Utils: Transactions API', () => {
Lisk.transaction.transfer.mockImplementation(() => {
throw new Error('sample error message');
});
- const tx = {
- amount: '1',
- data: { data: 'payment' },
- passphrase: 'abc',
- recipientId: '123L',
- secondPassphrase: null,
- timeOffset: 0,
- network: {
- networks: {
- LSK: { networkIdentifier: 'sample_identifier' },
- },
- },
- };
+
try {
- await create(tx, transactionTypes().send.key);
+ await create(testTx, transactionTypes().transfer.key);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.message).toEqual('sample error message');
@@ -198,4 +202,85 @@ describe('Utils: Transactions API', () => {
}
});
});
+
+ describe('getMinTxFee', () => {
+ it('calculates the correct tx fees', () => {
+ const fees = getMinTxFee({
+ minFee: 1,
+ });
+ expect(fees).toBe(1);
+ });
+ });
+
+ describe('getTransactionBaseFees', () => {
+ it('calculates the estimated fees for a transaction', async () => {
+ liskService.getTransactionBaseFees.mockResolvedValue({
+ data: {
+ feeEstimatePerByte: { low: 0, medium: 1000, high: 2000 },
+ },
+ });
+ const estimates = await getTransactionBaseFees();
+ expect(estimates).toBeDefined();
+ expect(Object.keys(estimates)).toHaveLength(3);
+ });
+ });
+
+ describe('getTransactionFee', () => {
+ it('returns the calculated tx fees for a selected processing speed', async () => {
+ const fees = await getTransactionFee({
+ txData: {
+ ...testTx,
+ senderPublicKey: accounts.genesis.publicKey,
+ txType: transactionTypes().transfer.key,
+ },
+ selectedPriority: { value: 10, selectedIndex: 0 },
+ });
+
+ expect(fees.value).toBeDefined();
+ expect(fees.error).toBeFalsy();
+ });
+
+ it('returns an error and appropriate feedback if the tx amount is empty', async () => {
+ const fees = await getTransactionFee({
+ txData: {
+ ...testTx,
+ amount: '',
+ senderPublicKey: accounts.genesis.publicKey,
+ txType: transactionTypes().transfer.key,
+ },
+ selectedPriority: { value: 10, selectedIndex: 0 },
+ });
+
+ expect(fees.value).toBeDefined();
+ expect(fees.error).toBeTruthy();
+ });
+
+ it('returns transaction type hard cap value', async () => {
+ // tested using a ridiculously high base fee
+ const txType = transactionTypes().transfer.key;
+ const hardCap = transactionTypes().transfer.hardCap;
+
+ const fees = await getTransactionFee({
+ txData: {
+ ...testTx,
+ amount: '1',
+ senderPublicKey: accounts.genesis.publicKey,
+ txType,
+ },
+ selectedPriority: { value: 1e11, selectedIndex: 0 },
+ });
+
+ expect(fees.value).toBeDefined();
+ expect(fees.value).toEqual(parseFloat(Number(fromRawLsk(hardCap)).toFixed(8)));
+ });
+ });
+
+ describe('createTransactionInstance', () => {
+ it('returns a transaction instance', () => {
+ const txTransfer = createTransactionInstance({}, 'transfer');
+ const txRegDelegate = createTransactionInstance({}, 'registerDelegate');
+ expect(txTransfer).toBeDefined();
+ expect(txRegDelegate).toBeDefined();
+ });
+ });
});
diff --git a/src/utils/api/network.test.js b/src/utils/api/network.test.js
index b981f8d908..bd19ef7d3c 100644
--- a/src/utils/api/network.test.js
+++ b/src/utils/api/network.test.js
@@ -1,4 +1,4 @@
-import Lisk from '@liskhq/lisk-client-old';
+import Lisk from '@liskhq/lisk-client';
import { getAPIClient } from './network';
import networks from '../../constants/networks';
@@ -25,7 +25,7 @@ describe('Utils: network API', () => {
Lisk.APIClient = APIClientBackup;
});
- it.skip('should create a new Lisk APIClient instance if called with LSK token', () => {
+ it('should create a new Lisk APIClient instance if called with LSK token', () => {
const nethash = Lisk.APIClient.constants.MAINNET_NETHASH;
const nodeUrl = 'https://hub23.lisk.io';
const state = {
diff --git a/src/utils/api/service.js b/src/utils/api/service.js
index cbcafcd619..c01d306dbb 100644
--- a/src/utils/api/service.js
+++ b/src/utils/api/service.js
@@ -19,23 +19,7 @@ import api from '.';
*/
const getPriceTicker = (network, tokenType) => api[tokenType].service.getPriceTicker(network);
-/**
- * Contains dynamic fee rates for a token to indicate processing speed on the blockchain.
- * Properties are formatted as satoshis/byte for BTC.
- * @typedef {Object} DynamicFees
- * @property {Number} Low
- * @property {Number} Medium
- * @property {Number} High
- */
-/**
- * Retrieves dynamic fees for given token from the related service.
- * @param {String} tokenType
- * @returns {Promise}
- */
-const getDynamicFees = tokenType => api[tokenType].service.getDynamicFees();
-
export default {
- getDynamicFees,
getPriceTicker,
};
diff --git a/src/utils/api/transactions.js b/src/utils/api/transactions.js
index eabce1f7de..272657824d 100644
--- a/src/utils/api/transactions.js
+++ b/src/utils/api/transactions.js
@@ -37,7 +37,10 @@ export const getSingleTransaction = async ({ token, ...params }) => (
// istanbul ignore file
export const get = (token, data) => api[token].transactions.get(data);
-// istanbul ignore next
+/**
+ * @todo document function signature
+ *
+ */
export const create = (tokenType, data, transactionType) =>
api[tokenType].transactions.create(data, transactionType);
@@ -45,6 +48,12 @@ export const create = (tokenType, data, transactionType) =>
export const broadcast = (tokenType, transaction, network) =>
api[tokenType].transactions.broadcast(transaction, network);
+export const getTransactionBaseFees = (tokenType, network) =>
+ api[tokenType].transactions.getTransactionBaseFees(network);
+
+export const getTransactionFee = ({ token, ...params }) =>
+ api[token].transactions.getTransactionFee(params);
+
export default {
broadcast,
create,
diff --git a/src/utils/delegates.js b/src/utils/delegates.js
deleted file mode 100644
index 8e293cd681..0000000000
--- a/src/utils/delegates.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { setInStorage, getFromStorage } from './localJSONStorage';
-import networks from '../constants/networks';
-import { tokenMap } from '../constants/tokens';
-
-const getNetworkKey = network => (
- `delegateCache-${
- network.name === networks.customNode.name
- ? network.networks[tokenMap.LSK.key].nodeUrl
- : network.name
- }`
-);
-
-export const updateDelegateCache = (delegates, network) => {
- getFromStorage(getNetworkKey(network), {}, (savedDelegates) => {
- setInStorage(getNetworkKey(network), {
- ...savedDelegates,
- ...delegates.reduce((newDelegates, delegate) => ({
- ...newDelegates,
- [delegate.username]: delegate,
- ...(delegate.account ? { [delegate.account.publicKey]: delegate } : {}),
- }), {}),
- });
- });
-};
-
-export const loadDelegateCache = (network, cb) =>
- getFromStorage(getNetworkKey(network), {}, cb);
diff --git a/src/utils/delegates.test.js b/src/utils/delegates.test.js
deleted file mode 100644
index f5a5c760a2..0000000000
--- a/src/utils/delegates.test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { updateDelegateCache, loadDelegateCache } from './delegates';
-import accounts from '../../test/constants/accounts';
-import networks from '../constants/networks';
-
-describe('Delegates Utils', () => {
- const storage = {};
-
- beforeEach(() => {
- window.localStorage.getItem = key => (storage[key]);
- window.localStorage.setItem = (key, item) => { storage[key] = item; };
- });
-
- const delegate = {
- account: { ...accounts.delegate },
- username: accounts.delegate.username,
- };
- const itemExpected = {
- [delegate.username]: delegate,
- [delegate.account.publicKey]: delegate,
- };
-
- it('sets and gets the delegate item with mainnet', () => {
- const network = networks.mainnet;
- updateDelegateCache([delegate], network);
- loadDelegateCache(network, (data) => {
- expect(data).toEqual(itemExpected);
- });
- });
-
- it('sets and gets the delegate item with customNode', () => {
- const network = {
- options: networks.customNode,
- networks: {
- LSK: {
- nodeUrl: 'http://localhost:4000',
- },
- },
- name: networks.customNode.name,
- };
- updateDelegateCache([delegate], network);
- loadDelegateCache(network, (data) => {
- expect(data).toEqual(itemExpected);
- });
- });
-});
diff --git a/src/utils/externalLinks.js b/src/utils/externalLinks.js
index 3a660197dd..c7c8fd7c54 100644
--- a/src/utils/externalLinks.js
+++ b/src/utils/externalLinks.js
@@ -1,20 +1,30 @@
import history from '../history';
+const sendRegex = /^\/(wallet|wallet\/send|main\/transactions\/send)$/;
+const sendRedirect = '/wallet?modal=send';
+
+const voteRegex = /^\/(main\/voting\/vote|delegates\/vote|vote)$/;
+const voteRedirect = '/wallet?modal=votingQueue';
+
export default {
init: () => {
const { ipc } = window;
if (ipc) {
ipc.on('openUrl', (action, url) => {
- const protocol = url.split(':/')[0];
- let normalizedUrl = url.split(':/')[1];
+ const [protocol, rest] = url.split(':/');
+ const [normalizedUrl, searchParams] = rest.split('?');
+
if (protocol && protocol.toLowerCase() === 'lisk' && normalizedUrl) {
- normalizedUrl = normalizedUrl
- .replace('/main/transactions/send', '/wallet/send')
- .replace('/wallet', '/wallet/send')
- .replace('/main/voting/vote', '/delegates/vote');
- history.push(normalizedUrl);
- history.replace(normalizedUrl);
+ let redirectUrl = normalizedUrl;
+ if (normalizedUrl.match(sendRegex)) {
+ redirectUrl = sendRedirect + (searchParams ? `&${searchParams}` : '');
+ } else if (normalizedUrl.match(voteRegex)) {
+ redirectUrl = voteRedirect + (searchParams ? `&${searchParams}` : '');
+ }
+
+ history.push(redirectUrl);
+ history.replace(redirectUrl);
}
});
}
diff --git a/src/utils/externalLinks.test.js b/src/utils/externalLinks.test.js
index 579191de5e..df19c10cec 100644
--- a/src/utils/externalLinks.test.js
+++ b/src/utils/externalLinks.test.js
@@ -1,25 +1,32 @@
-import { expect } from 'chai';
-import { spy, mock } from 'sinon';
import externalLinks from './externalLinks';
import history from '../history';
import routes from '../constants/routes';
+jest.mock('../history', () => ({
+ push: jest.fn(), replace: jest.fn(),
+}));
+
describe('externalLinks', () => {
- const historyMock = mock(history);
const ipc = {
- on: spy(),
+ on: jest.fn(),
};
+ beforeEach(() => {
+ ipc.on.mockClear();
+ history.replace.mockReset();
+ history.push.mockReset();
+ });
+
it('calling init when ipc is not on window should do nothing', () => {
window.ipc = null;
externalLinks.init();
- expect(ipc.on).to.not.have.been.calledWith();
+ expect(ipc.on).not.toHaveBeenCalled();
});
it('calling init when ipc is available on window should bind listeners', () => {
window.ipc = ipc;
externalLinks.init();
- expect(ipc.on).to.have.been.calledWith();
+ expect(ipc.on).toHaveBeenCalled();
});
it('opens url', () => {
@@ -30,6 +37,28 @@ describe('externalLinks', () => {
externalLinks.init();
callbacks.openUrl({}, 'lisk://register');
- historyMock.expects('replace').once().withArgs(routes.register.path);
+ expect(history.replace).toHaveBeenCalledWith(routes.register.path);
+ });
+
+ it('opens send modal', () => {
+ const callbacks = {};
+ window.ipc = {
+ on: (event, callback) => { callbacks[event] = callback; },
+ };
+
+ externalLinks.init();
+ callbacks.openUrl({}, 'lisk://wallet?recipient=1L&amount=100');
+ expect(history.replace).toHaveBeenCalledWith('/wallet?modal=send&recipient=1L&amount=100');
+ });
+
+ it('opens voting queue modal', () => {
+ const callbacks = {};
+ window.ipc = {
+ on: (event, callback) => { callbacks[event] = callback; },
+ };
+
+ externalLinks.init();
+ callbacks.openUrl({}, 'lisk://vote?votes=delegate');
+ expect(history.replace).toHaveBeenCalledWith('/wallet?modal=votingQueue&votes=delegate');
});
});
diff --git a/src/utils/getNetwork.js b/src/utils/getNetwork.js
index 4a0a1dd9ee..3bb1ac436b 100644
--- a/src/utils/getNetwork.js
+++ b/src/utils/getNetwork.js
@@ -52,3 +52,16 @@ export const getNetworkNameBasedOnNethash = (network, token = 'LSK') => {
}
return activeNetwork;
};
+
+/**
+ * Returns human readable error messages
+ *
+ * @param {Object} error
+ * @param {String} error.message - The error message received from network API call
+ * @returns {String} - The human readable error message.
+ */
+export const getConnectionErrorMessage = error => (
+ error && error.message
+ ? i18next.t(`Unable to connect to the node, Error: ${error.message}`)
+ : i18next.t('Unable to connect to the node, no response from the server.')
+);
diff --git a/src/utils/hacks.js b/src/utils/hacks.js
deleted file mode 100644
index a5702ae5a4..0000000000
--- a/src/utils/hacks.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import liskClient from 'Utils/lisk-client'; // eslint-disable-line
-
-// The following function is used as a hack to fix timestamp issues.
-// The problem is that Windows has often local time little bit in future and
-// Lisk Core doesn't accept transactions with timestampt in the future.
-//
-// The workaround is to get timestamp from the last block, but lisk-elemnts
-// accepts only time offset so this util is creating that.
-//
-// For more info, see:
-// https://github.com/LiskHQ/lisk-desktop/issues/1277
-//
-// eslint-disable-next-line import/prefer-default-export
-export const getTimeOffset = (latestBlocks, apiVersion) => {
- const Lisk = liskClient(apiVersion);
- return (
- latestBlocks.length && latestBlocks[0].timestamp
- ? latestBlocks[0].timestamp - Lisk.transaction.utils.getTimeFromBlockchainEpoch()
- : 0
- );
-};
diff --git a/src/utils/hacks.test.js b/src/utils/hacks.test.js
deleted file mode 100644
index da1df7ab7a..0000000000
--- a/src/utils/hacks.test.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { expect } from 'chai';
-import Lisk from '@liskhq/lisk-client-old';
-import { getTimeOffset } from './hacks';
-
-describe('hack utils', () => {
- describe('getTimeOffset', () => {
- it('should return time offset to timestamp of the last block', () => {
- const offset = 12;
- const state = {
- blocks: {
- latestBlocks: [{
- timestamp: Lisk.transaction.utils.getTimeFromBlockchainEpoch() - offset,
- }],
- },
- };
- expect(getTimeOffset(state.blocks.latestBlocks)).to.equal(-offset);
- });
- });
-});
diff --git a/src/utils/hwManager.js b/src/utils/hwManager.js
index 2a70131149..12e07a1ff0 100644
--- a/src/utils/hwManager.js
+++ b/src/utils/hwManager.js
@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-unresolved
-import lisk from 'Utils/lisk-client';
+import Lisk from '@liskhq/lisk-client';
import i18next from 'i18next';
import { getAccount } from './api/lsk/account';
import {
@@ -12,7 +12,6 @@ import {
subscribeToDevicesList,
validatePin,
} from '../../libs/hwManager/communication';
-import { splitVotesIntoRounds } from './voting';
/**
* getAccountsFromDevice - Function.
@@ -37,8 +36,8 @@ const getAccountsFromDevice = async ({ device: { deviceId }, network }) => {
* signSendTransaction - Function.
* This function is used for sign a send transaction.
*/
-const signSendTransaction = async (account, data, apiVersion) => {
- const { transfer, utils } = lisk(apiVersion).transaction;
+const signSendTransaction = async (account, data) => {
+ const { transfer, utils } = Lisk.transaction;
const transactionObject = {
...transfer(data),
senderPublicKey: account.info.LSK ? account.info.LSK.publicKey : null,
@@ -66,39 +65,32 @@ const signSendTransaction = async (account, data, apiVersion) => {
*/
const signVoteTransaction = async (
account,
- votedList,
- unvotedList,
+ votes,
timeOffset,
networkIdentifier,
) => {
- const { castVotes, utils } = lisk()['2.x'].transaction;
- const signedTransactions = [];
- const votesChunks = splitVotesIntoRounds({ votes: [...votedList], unvotes: [...unvotedList] });
+ const { castVotes, utils } = Lisk.transaction;
try {
- for (let i = 0; i < votesChunks.length; i++) {
- const transactionObject = {
- ...castVotes({ ...votesChunks[i], timeOffset, networkIdentifier }),
- senderPublicKey: account.publicKey,
- recipientId: account.address, // @todo should we remove this?
- };
+ const transactionObject = {
+ ...castVotes({ votes, timeOffset, networkIdentifier }),
+ senderPublicKey: account.publicKey,
+ recipientId: account.address, // @todo should we remove this?
+ };
- // eslint-disable-next-line no-await-in-loop
- const signature = await signTransaction({
- deviceId: account.hwInfo.deviceId,
- index: account.hwInfo.derivationIndex,
- tx: transactionObject,
- });
-
- signedTransactions.push({
- ...transactionObject,
- signature,
- // @ todo core 3.x getId
- id: utils.getTransactionId({ ...transactionObject, signature }),
- });
- }
+ // eslint-disable-next-line no-await-in-loop
+ const signature = await signTransaction({
+ deviceId: account.hwInfo.deviceId,
+ index: account.hwInfo.derivationIndex,
+ tx: transactionObject,
+ });
- return signedTransactions;
+ return {
+ ...transactionObject,
+ signature,
+ // @ todo core 3.x getId
+ id: utils.getTransactionId({ ...transactionObject, signature }),
+ };
} catch (error) {
throw new Error(i18next.t(
'The transaction has been canceled on your {{model}}',
diff --git a/src/utils/regex.js b/src/utils/regex.js
index 281ddbe94b..7b707a7ca8 100644
--- a/src/utils/regex.js
+++ b/src/utils/regex.js
@@ -1,6 +1,7 @@
export default {
address: /^[1-9]\d{0,19}L$/,
- delegateName: /^[a-z0-9!@$&_.]{0,20}$/,
+ publicKey: /^[0-9a-f]{64}$/,
+ delegateName: /^[a-z0-9!@$&_.]{3,20}$/,
transactionId: /^[1-9]\d{0,19}$/,
blockId: /^[1-9]\d{0,19}$/,
btcAddressTrunk: /^(.{10})(.+)?(.{10})$/,
diff --git a/src/utils/transactions.js b/src/utils/transactions.js
index 200a89abf2..bbc20cf6ed 100644
--- a/src/utils/transactions.js
+++ b/src/utils/transactions.js
@@ -1,7 +1,63 @@
+import Lisk from '@liskhq/lisk-client';
-export default (pendingTransactions, confirmedTransactions) =>
+import transactionTypes, { byteSizes } from '../constants/transactionTypes';
+
+/**
+ * calculates the transaction size in bytes
+ * @param {{type: number, transaction: Object}} param
+ * @returns {number} the transaction size in bytes
+ */
+// eslint-disable-next-line max-statements
+export const findTransactionSizeInBytes = ({
+ type, transaction,
+}) => {
+ // delete the fee property from the transaction so it does
+ // not affect the fee calculation
+ delete transaction.fee;
+
+ const transactionType = Buffer.alloc(byteSizes.type, type);
+ const transactionNonce = Lisk.cryptography.intToBuffer(
+ Number(transaction.nonce),
+ byteSizes.nonce,
+ );
+ const transactionSenderPublicKey = Lisk.cryptography.hexToBuffer(transaction.senderPublicKey);
+ const txAsset = {
+ amount: transaction.amount,
+ data: transaction.data,
+ recipientId: transaction.recipientId,
+ };
+
+ const assetBytes = Buffer.from(JSON.stringify(txAsset), 'utf-8');
+ const feeBytes = Lisk.cryptography.intToBuffer(String(byteSizes.fee), byteSizes.fee);
+
+ const totalBytes = Buffer.concat([
+ transactionType,
+ transactionNonce,
+ transactionSenderPublicKey,
+ assetBytes,
+ feeBytes,
+ ]).byteLength;
+
+ return totalBytes + byteSizes.signature;
+};
+
+const dedupeTransactions = (pendingTransactions, confirmedTransactions) =>
[...pendingTransactions, ...confirmedTransactions]
.filter((transactionA, index, self) =>
index === self.findIndex(transactionB => (
transactionB.id === transactionA.id
)));
+
+export const getTxAmount = (transaction) => {
+ let amount = transaction.amount !== undefined ? transaction.amount : transaction.asset.amount;
+ if (!amount && transaction.type === transactionTypes().unlockToken.code.legacy) {
+ amount = 0;
+ transaction.asset.unlockingObjects.forEach((unlockedObject) => {
+ amount += parseInt(unlockedObject.amount, 10);
+ });
+ amount = `${amount}`;
+ }
+ return amount;
+};
+
+export default dedupeTransactions;
diff --git a/src/utils/transactions.test.js b/src/utils/transactions.test.js
index 2ed63567bb..ca35481348 100644
--- a/src/utils/transactions.test.js
+++ b/src/utils/transactions.test.js
@@ -1,4 +1,8 @@
-import removeDuplicateTransactions from './transactions';
+import removeDuplicateTransactions, { findTransactionSizeInBytes } from './transactions';
+import transactionTypes from '../constants/transactionTypes';
+import accounts from '../../test/constants/accounts';
+
+const TESTNET_NETHASH = 'da3ed6a45429278bac2666961289ca17ad86595d33b31037615d4b8e8f158bba';
describe('Remove duplicate transactions', () => {
it('should remove duplicates from pending and confirmed lists', () => {
@@ -8,3 +12,29 @@ describe('Remove duplicate transactions', () => {
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
});
});
+
+describe('findTransactionSizeInBytes', () => {
+ const testTx = {
+ amount: '1',
+ data: 'payment',
+ passphrase: accounts.genesis.passphrase,
+ recipientId: '123L',
+ timeOffset: 0,
+ nonce: '1',
+ fee: '123',
+ senderPublicKey: accounts.genesis.publicKey,
+ network: {
+ networks: {
+ LSK: { networkIdentifier: TESTNET_NETHASH },
+ },
+ },
+ };
+
+ it('should return the correct transaction size', () => {
+ const size = findTransactionSizeInBytes({
+ type: transactionTypes().transfer.key,
+ transaction: testTx,
+ });
+ expect(size).toBe(165);
+ });
+});
diff --git a/src/utils/validators.js b/src/utils/validators.js
index e3ced34496..3047a2675d 100644
--- a/src/utils/validators.js
+++ b/src/utils/validators.js
@@ -2,7 +2,9 @@ import * as bitcoin from 'bitcoinjs-lib';
import numeral from 'numeral';
import { cryptography } from '@liskhq/lisk-client';
import { tokenMap } from '../constants/tokens';
+import { minBalance } from '../constants/transactions';
import getBtcConfig from './api/btc/config';
+import { toRawLsk } from './lsk';
import i18n from '../i18n';
import reg from './regex';
@@ -54,8 +56,11 @@ export const validateLSKPublicKey = (address) => {
* - Check that has no more than 8 floating points digits
* @param {Object.} data
* @param {string} data.value
- * @param {string} [data.token="LSK"]
- * @param {string} [data.locale="en"]
+ * @param {string} [data.token="LSK"] The active token
+ * @param {string} [data.locale="en"] The locale for testing the format against
+ * @param {string?} [data.funds] Maximum funds users are allowed to input
+ * @param {Array?} [data.checklist] The list of errors to be tested. A choice of
+ * ZERO, MAX_ACCURACY, FORMAT, VOTE_10X, INSUFFICIENT_FUNDS
* @returns {Object.}
* data - Object containing the message and if has an error
* data.message - Message of the error or empty string
@@ -65,18 +70,44 @@ export const validateAmountFormat = ({
value,
token = 'LSK',
locale = i18n.language,
+ funds,
+ checklist = ['ZERO', 'MAX_ACCURACY', 'FORMAT'],
}) => {
+ const { format, maxFloating } = reg.amount[locale];
+
const errors = {
- INVALID: i18n.t('Provide a correct amount of {{token}}', { token }),
- FLOATING_POINT: i18n.t('Maximum floating point is 8.'),
+ ZERO: {
+ message: i18n.t('Amount can\'t be zero.'),
+ fn: () => numeral(value).value() === 0,
+ },
+ FORMAT: {
+ message: i18n.t('Provide a correct amount of {{token}}', { token }),
+ fn: () => format.test(value),
+ },
+ MAX_ACCURACY: {
+ message: i18n.t('Maximum floating point is 8.'),
+ fn: () => maxFloating.test(value),
+ },
+ VOTE_10X: {
+ message: i18n.t('You can only vote in multiplies of 10 LSK.'),
+ fn: () => value % 10 !== 0,
+ },
+ INSUFFICIENT_FUNDS: {
+ message: i18n.t('Provided amount is higher than your current balance.'),
+ fn: () => funds < toRawLsk(numeral(value).value()),
+ },
+ MIN_BALANCE: {
+ message: i18n.t('Provided amount will result in a wallet with less than the minimum balance.'),
+ fn: () => {
+ const rawValue = toRawLsk(numeral(value).value());
+ return funds - rawValue < minBalance;
+ },
+ },
};
- const { format, maxFloating } = reg.amount[locale];
- const message = (
- (format.test(value) || numeral(value).value() === 0) && errors.INVALID)
- || (maxFloating.test(value) && errors.FLOATING_POINT)
- || '';
+
+ const errorType = checklist.find(type => errors[type].fn());
return {
- error: !!message,
- message,
+ error: !!errorType,
+ message: errorType ? errors[errorType].message : '',
};
};
diff --git a/src/utils/validators.test.js b/src/utils/validators.test.js
index e4b4b29078..ecc191dcbf 100644
--- a/src/utils/validators.test.js
+++ b/src/utils/validators.test.js
@@ -33,11 +33,21 @@ describe('Validate Public Key', () => {
describe('Validate Amount Format', () => {
const errors = {
+ ZERO: i18n.t('Amount can\'t be zero.'),
INVALID: i18n.t('Provide a correct amount of {{token}}', { token: 'LSK' }),
FLOATING_POINT: i18n.t('Maximum floating point is 8.'),
};
+ it('Should return errors.ZERO if amount is zero', () => {
+ [0.0, '0,', '0,0'].forEach((value) => {
+ expect(validateAmountFormat({ value })).toEqual({
+ error: true,
+ message: errors.ZERO,
+ });
+ });
+ });
+
it('Should return errors.INVALID if format is invalid', () => {
- [0.0, '0,0', '0.1.2', '0,', '1..', '1a,1', '2.d2'].forEach((value) => {
+ ['0.1.2', '1..', '1a,1', '2.d2'].forEach((value) => {
expect(validateAmountFormat({ value })).toEqual({
error: true,
message: errors.INVALID,
diff --git a/src/utils/voting.js b/src/utils/voting.js
index c4a3bc4d47..b8a05043a5 100644
--- a/src/utils/voting.js
+++ b/src/utils/voting.js
@@ -1,9 +1,8 @@
import i18next from 'i18next';
import votingConst from '../constants/voting';
-import Fees from '../constants/fees';
/**
- * Returns the list of CONFRIMED votes.
+ * Returns the list of CONFIRMED votes.
*
* @param {Object} votes
* Votes as stored on the Redux store.
@@ -85,7 +84,7 @@ export const getVotingLists = votes => ({
export const getVotingError = (votes, account) => {
let error;
- if (account.balance < Fees.vote) {
+ if (account.balance < 1e8) {
error = i18next.t('Not enough LSK to pay for the transaction.');
} else if (getTotalVotesCount(votes) > votingConst.maxCountOfVotes) {
error = i18next.t('Max amount of delegates in one voting exceeded.');
@@ -94,19 +93,3 @@ export const getVotingError = (votes, account) => {
};
export const getVote = (votes, name) => votes.find(v => v.username === name);
-
-export const splitVotesIntoRounds = ({ votes, unvotes }) => {
- const rounds = [];
- const maxCountOfVotesInOneTurn = 33;
- while (votes.length + unvotes.length > 0) {
- const votesLength = Math.min(
- votes.length,
- maxCountOfVotesInOneTurn - Math.min(unvotes.length, 16),
- );
- rounds.push({
- votes: votes.splice(0, votesLength),
- unvotes: unvotes.splice(0, maxCountOfVotesInOneTurn - votesLength),
- });
- }
- return rounds;
-};
diff --git a/src/utils/withData.js b/src/utils/withData.js
index d1ebaf48db..e6adb41d45 100644
--- a/src/utils/withData.js
+++ b/src/utils/withData.js
@@ -39,11 +39,10 @@ import React from 'react';
* ```
* getApiParams: (state, props) => ({
* address: props.account.address,
- * apiVersion: state.network.networks.LSK.apiVersion,
* })
* ```
* Creates an API call with the following query parameters
- * `?address=${props.account.address}&apiVersion=${state.network.networks.LSK.apiVersion}`
+ * `?address=${props.account.address}`
*
* @param {Boolean} autoload
* Determines if the first API call should get fired by componentDidMount cycle event.
diff --git a/test/constants/accounts.js b/test/constants/accounts.js
index bf2269baf2..00fc2c4662 100644
--- a/test/constants/accounts.js
+++ b/test/constants/accounts.js
@@ -1,10 +1,11 @@
const accounts = {
genesis: {
- passphrase: 'wagon stock borrow episode laundry kitten salute link globe zero feed marble',
- publicKey: 'c094ebee7ec0c50ebee32918655e089f6e1a604b83bcaa760293c61e0f18ab6f',
- serverPublicKey: 'c094ebee7ec0c50ebee32918655e089f6e1a604b83bcaa760293c61e0f18ab6f',
- address: '16313739661670634666L',
- balance: '9980000000000000',
+ passphrase: 'peanut hundred pen hawk invite exclude brain chunk gadget wait wrong ready',
+ publicKey: '0fe9a3f1a21b5530f27f87a414b549e79a940bf24fdf2b2f05e7f22aeeecc86a',
+ serverPublicKey: '0fe9a3f1a21b5530f27f87a414b549e79a940bf24fdf2b2f05e7f22aeeecc86a',
+ address: '5059876081639179984L',
+ balance: '9897000000000000',
+ nonce: '1',
},
delegate: {
passphrase: 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit',
diff --git a/test/constants/defaultState.js b/test/constants/defaultState.js
index 54a5858b93..d6153f2c3d 100644
--- a/test/constants/defaultState.js
+++ b/test/constants/defaultState.js
@@ -15,7 +15,6 @@ export default {
USD: 1,
},
},
- dynamicFees: {},
},
settings: {
currency: 'USD',
diff --git a/test/constants/selectors.js b/test/constants/selectors.js
index 5fb61ef195..3e77e43bfa 100644
--- a/test/constants/selectors.js
+++ b/test/constants/selectors.js
@@ -6,18 +6,6 @@ const delegatesPage = {
goToConfirmationButton: '.go-to-confirmation-button',
votingHeader: '.voting-header',
};
-
-const votingPage = {
- addedVotes: '.added-votes .vote',
- removedVotes: '.removed-votes .vote',
- voteResultHeader: '.result-box-header',
- backToDelegatesButton: '.back-to-delegates-button',
- alreadyVotedPreselection: '.alreadyVoted-message .vote',
- addedVotesContainer: '.added-votes',
- becomeDelegateLink: '.register-delegate',
- loadMoreButton: '.load-more',
-};
-
const secondPassphraseRegistrationPage = {
goToConfirmation: '.go-to-confirmation',
confirmationCheckbox: '.confirmation-checkbox',
@@ -26,8 +14,8 @@ const secondPassphraseRegistrationPage = {
const ss = {
...delegatesPage,
- ...votingPage,
...secondPassphraseRegistrationPage,
+ becomeDelegateLink: '.register-delegate',
app: '#app',
monitorNetwork: '#network',
monitorTransactions: '#transactions',
@@ -240,10 +228,20 @@ const ss = {
coinRow: '.coin-row',
closeOnboardingButton: '.closeOnboarding',
goBack: '.go-back',
- sendLink: '.tx-send-bt',
+ sendLink: '.open-send-dialog',
closeDialog: '.dialog-close-button',
bookmarkListToggle: '.bookmark-list-toggle',
settingsMenu: '.settings-toggle',
+ openAddVoteDialog: '.open-add-vote-dialog',
+ votingQueueToggle: '.voting-queue-toggle',
+ openUnlockBalanceDialog: '.open-unlock-balance-dialog',
+ unlockBtn: '.unlock-btn',
+ removeVote: '.remove-vote',
+ unlockingBalance: '.unlocking-balance',
+ addBookmarkIcon: '.add-bookmark-icon',
+ inputLabel: '.input-label',
+ saveButton: '.save-button',
+ feeValue: '.fee-value',
};
export default ss;
diff --git a/test/constants/urls.js b/test/constants/urls.js
index ed38bd112e..39fbd0d653 100644
--- a/test/constants/urls.js
+++ b/test/constants/urls.js
@@ -1,6 +1,6 @@
const urls = {
dashboard: '/',
- wallet: '/wallet',
+ wallet: '/wallet?tab=Transactions',
send: '/wallet?modal=send',
request: '/wallet/request',
help: '/help',
diff --git a/test/constants/votes.js b/test/constants/votes.js
index f511554226..1f82876923 100644
--- a/test/constants/votes.js
+++ b/test/constants/votes.js
@@ -1,32 +1,16 @@
export default [
{
- address: '1478505779553195737L',
- publicKey: '5c4af5cb0c1c92df2ed4feeb9751e54e951f9d3f77196511f13e636cf6064e74',
- balance: '2515148513',
- username: 'genesis_11',
- },
- {
- address: '12254605294831056546L',
- publicKey: '141b16ac8d5bd150f16b1caa08f689057ca4c4434445e56661831f4e671b7c0a',
- balance: '3015148513',
- username: 'genesis_2',
- },
- {
- address: '8273455169423958419L',
- publicKey: '9d3058175acab969f41ad9b86f7a2926c74258670fe56b37c429c01fca9f2f0f',
- balance: '2515148513',
- username: 'genesis_1',
- },
- {
- address: '16807489144327319524L',
- publicKey: '82174ee408161186e650427032f4cfb2496f429b4157da78888cbcea39c387fc',
- balance: '4015148513',
- username: 'genesis_32',
- },
- {
- address: '2581762640681118072L',
- publicKey: '01389197bbaf1afb0acd47bbfeabb34aca80fb372a8f694a1c0716b3398db746',
- balance: '2515148513',
- username: 'genesis_51',
+ amount: '1000000000000',
+ delegateAddress: '5059876081639179984L',
+ delegate: {
+ username: 'genesis_1',
+ totalVotesReceived: '1000000000000',
+ delegate: {
+ isBanned: false,
+ pomHeights: [478, 555],
+ lastForgedHeight: 0,
+ consecutiveMissedBlocks: 0,
+ },
+ },
},
];
diff --git a/test/create-test-transactions.sh b/test/create-test-transactions.sh
index 9ea4895d41..aff93d7d9e 100755
--- a/test/create-test-transactions.sh
+++ b/test/create-test-transactions.sh
@@ -1,37 +1,34 @@
#!/bin/bash
lisk config:set api.nodes http://localhost:4000
-PASSPHRASE="wagon stock borrow episode laundry kitten salute link globe zero feed marble"
-function transfer(){
- lisk transaction:broadcast $(lisk transaction:create:transfer $1 $2 --data=$3 --passphrase="pass:$PASSPHRASE")
-}
-
-for i in {1..50}; do
- transfer ${i}00 537318935439898807L
-done
+NETWORKIDENTIFIER="93d00fe5be70d90e7ae247936a2e7d83b50809c79b73fa14285f02c842348b3e"
+PASSPHRASE="peanut hundred pen hawk invite exclude brain chunk gadget wait wrong ready"
-transfer 100 1155682438012955434L second-passphrase-account
-transfer 90 544792633152563672L delegate-candidate
-transfer 80 4264113712245538326L second-passphrase-candidate
-transfer 70 16422276087748907680L send-all-account
-transfer 1 94495548317450502L without-initialization
+NONCE=103
-sleep 10
+function transfer(){
+ lisk transaction:broadcast $(lisk transaction:create -t 8 $1 0.1 $2 $3 --data=$4 --passphrase="$PASSPHRASE" --networkIdentifier=$NETWORKIDENTIFIER)
+}
-lisk transaction:broadcast $(lisk transaction:create:second-passphrase \
---passphrase="pass:awkward service glimpse punch genre calm grow life bullet boil match like" \
---second-passphrase="pass:forest around decrease farm vanish permit hotel clay senior matter endorse domain")
+function registerDelegate(){
+ lisk transaction:broadcast $(lisk transaction:create --type=10 0 11 delegate --passphrase="recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit" --networkIdentifier=$NETWORKIDENTIFIER)
+}
-lisk transaction:broadcast $(lisk transaction:create:vote \
---passphrase="pass:recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit" \
---votes 86499879448d1b0215d59cbf078836e3d7d9d2782d56a2274a568761bff36f19)
+function vote() {
+ lisk transaction:broadcast $(lisk transaction:create --type=13 156 0.1 --votes="537318935439898807L,100" --passphrase="peanut hundred pen hawk invite exclude brain chunk gadget wait wrong ready" --networkIdentifier=93d00fe5be70d90e7ae247936a2e7d83b50809c79b73fa14285f02c842348b3e)
+ lisk transaction:broadcast $(lisk transaction:create --type=13 157 0.1 --votes="537318935439898807L,-20" --passphrase="peanut hundred pen hawk invite exclude brain chunk gadget wait wrong ready" --networkIdentifier=93d00fe5be70d90e7ae247936a2e7d83b50809c79b73fa14285f02c842348b3e)
+}
-sleep 10
+for i in {1..50}; do
+ CURRENT=$(( $i + $NONCE - 1 ))
+ transfer ${CURRENT} ${i}00 537318935439898807L test
+done
-lisk transaction:broadcast $(lisk transaction:create:vote \
---passphrase="pass:recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit" \
---votes 01389197bbaf1afb0acd47bbfeabb34aca80fb372a8f694a1c0716b3398db746 \
---unvotes 86499879448d1b0215d59cbf078836e3d7d9d2782d56a2274a568761bff36f19)
+transfer 153 90 544792633152563672L delegate-candidate
+transfer 154 70 16422276087748907680L send-all-account
+transfer 155 1 94495548317450502L without-initialization
+registerDelegate
+# vote
# docker exec -t docker_db_1 pg_dump -U lisk lisk > ./dev_blockchain.db
diff --git a/test/cypress/features/bookmark.feature b/test/cypress/features/bookmark.feature
new file mode 100644
index 0000000000..86a3aae7c5
--- /dev/null
+++ b/test/cypress/features/bookmark.feature
@@ -0,0 +1,27 @@
+Feature: Add bookmark
+
+ Background:
+ Given I login as genesis on devnet
+ Given I am on wallet page
+ When I click on searchIcon
+
+ Scenario: Add a delegate to bookmarks
+ And I search for account 537318935439898807L
+ Then I click on searchAccountRow
+ Then I should be on Account page
+ Then I click on addBookmarkIcon
+ Then The saveButton button must be active
+ Then I click on saveButton
+ When I click on bookmarkListToggle
+ Then The bookmarkList should contain delegate
+
+ Scenario: Add regular account to bookmarks
+ And I search for account 16422276087748907680L
+ Then I click on searchAccountRow
+ Then I should be on Account page
+ Then I click on addBookmarkIcon
+ Then The saveButton button must not be active
+ And I fill testBmark in inputLabel field
+ Then I click on saveButton
+ When I click on bookmarkListToggle
+ Then The bookmarkList should contain testBmark
diff --git a/test/cypress/features/bookmark/bookmark.js b/test/cypress/features/bookmark/bookmark.js
new file mode 100644
index 0000000000..179799e0d3
--- /dev/null
+++ b/test/cypress/features/bookmark/bookmark.js
@@ -0,0 +1,7 @@
+/* eslint-disable */
+import { Then } from 'cypress-cucumber-preprocessor/steps';
+import ss from '../../../constants/selectors';
+
+Then(/^The bookmarkList should contain (.*?)$/, function (bookmarkLabel) {
+ cy.get(ss.bookmarkAccount).eq(0).should('contain', bookmarkLabel);
+});
diff --git a/test/cypress/features/common/common.js b/test/cypress/features/common/common.js
index f169188687..44aedc21ca 100644
--- a/test/cypress/features/common/common.js
+++ b/test/cypress/features/common/common.js
@@ -57,7 +57,6 @@ Given(/^I am on (.*?) page$/, function (page) {
cy.route('/api/votes?*').as('votes');
cy.visit(urls.wallet);
cy.wait('@transactions');
- cy.wait('@votes');
break;
case 'send':
cy.route('/api/accounts?address*').as('accountLSK');
@@ -82,12 +81,14 @@ Given(/^I am on (.*?) page of (.*?)$/, function (page, identifier) {
cy.route('/api/transactions?*').as('transactions');
cy.visit(`${urls.account}?address=${accounts[identifier].address}`);
cy.wait('@transactions');
- cy.wait('@transactions');
- cy.wait('@transactions');
break;
}
});
+Given(/^I scroll to (.*?)$/, (position) => {
+ cy.get('.scrollContainer').scrollTo(position);
+});
+
Then(/^I should see pending transaction$/, function () {
cy.get(`${ss.transactionRow} ${ss.spinner}`).should('be.visible');
});
@@ -115,7 +116,9 @@ Then(/^The latest transaction is (.*?)$/, function (transactionType) {
}
}
switch (transactionType.toLowerCase()) {
- case 'delegate vote':
+ case 'unlocking':
+ cy.get(`${ss.transactionRow} ${ss.transactionAddress}`).eq(0).contains('Unlock LSK');
+ break;
case 'voting':
cy.get(`${ss.transactionRow} ${ss.transactionAddress}`).eq(0).contains('Delegate vote');
break;
@@ -173,6 +176,14 @@ When(/^I click on (.*?)$/, function (elementName) {
cy.get(ss[elementName]).eq(0).click();
});
+When(/^I clear input (.*?)$/, function (elementName) {
+ cy.get(ss[elementName]).clear();
+});
+
+When(/^I fill ([\w]+) in ([\w]+) field$/, function (value, field) {
+ cy.get(ss[field]).type(value);
+});
+
Then(/^I fill ([^s]+) in ([^s]+) field$/, function (value, field) {
cy.get(ss[field]).type(value);
});
@@ -186,6 +197,23 @@ Then(/^(.*?) should be visible$/, function (elementName) {
cy.get(ss[elementName]).should('be.visible');
});
-Then(/^The (.*?) button must be active$/, function (elementName) {
- cy.get(ss[elementName]).should('not.be.disabled');
+Then(/^The (.*?) button must (.*?) active$/, function (elementName, check) {
+ if (check === 'be') {
+ cy.get(ss[elementName]).should('not.be.disabled');
+ } else if (check === 'not be') {
+ cy.get(ss[elementName]).should('be.disabled');
+ }
+});
+
+And(/^I search for account ([^s]+)$/, function (string) {
+ cy.server();
+ cy.route('/api/accounts**').as('requestAccount');
+ cy.route('/api/delegates**').as('requestDelegate');
+ cy.get(ss.searchInput).type(string);
+ cy.wait('@requestAccount');
+ cy.wait('@requestDelegate');
+});
+
+Then(/^I wait (.*?) seconds$/, function (seconds) {
+ cy.wait(Number(seconds) * 1000);
});
diff --git a/test/cypress/features/dashboard.feature b/test/cypress/features/dashboard.feature
index 14be3ff1a2..7b1f1fe357 100644
--- a/test/cypress/features/dashboard.feature
+++ b/test/cypress/features/dashboard.feature
@@ -8,4 +8,5 @@ Feature: Dashboard
Then I click on closeDialog
When I click on bookmarkListToggle
Then I click on bookmarkAccount
+ And I scroll to top
Then I should be on Account page
diff --git a/test/cypress/features/delegateReg.feature b/test/cypress/features/delegateReg.feature
index eb7a1d71de..31a415e46c 100644
--- a/test/cypress/features/delegateReg.feature
+++ b/test/cypress/features/delegateReg.feature
@@ -2,7 +2,8 @@ Feature: Register delegate
Scenario: Register delegate + Header balance is affected
Given I login as delegate_candidate on devnet
- Given I am on voting page
+ Given I am on wallet page
+ And I click on votesTab
Given I click on becomeDelegateLink
Then I see this title: Become a delegate
When I enter the delegate name
@@ -11,16 +12,3 @@ Feature: Register delegate
Then I see successful message
When I am on Wallet page
Then The latest transaction is Delegate registration
-
- Scenario: Register delegate with second passphrase
- Given I login as second_passphrase_account on devnet
- Given I am on voting page
- Given I click on becomeDelegateLink
- Then I see this title: Become a delegate
- When I enter the delegate name
- And I go to confirmation
- And I enter second passphrase of second_passphrase_account
- And I click on confirmButton
- Then I see successful message
- When I am on Wallet page
- Then The latest transaction is Delegate registration
diff --git a/test/cypress/features/delegates.feature b/test/cypress/features/delegates.feature
deleted file mode 100644
index 985ab0b4e5..0000000000
--- a/test/cypress/features/delegates.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Delegate
-
- Scenario: Displays 101 delegates and loads more as I scroll to bottom
- Given I login as genesis on testnet
- Given I am on voting page
- And I see 90 delegates on page
- When I click load more button
- And I see 180 delegates on page
diff --git a/test/cypress/features/delegates/delegates.js b/test/cypress/features/delegates/delegates.js
deleted file mode 100644
index b020e334da..0000000000
--- a/test/cypress/features/delegates/delegates.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/* eslint-disable */
-import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
-import accounts from '../../../constants/accounts';
-import ss from '../../../constants/selectors';
-import networks from '../../../constants/networks';
-import compareBalances from '../../utils/compareBalances';
-import urls from '../../../constants/urls';
-
-const txConfirmationTimeout = 20000;
-
-Then(/^I see (\d+) delegates on page$/, function (number) {
- cy.get(ss.delegateRow).should('have.length', number);
-});
-
-Then(/^I click load more button$/, function (number) {
- cy.wait(300);
- cy.get(ss.loadMoreButton).click();
-});
-
-Then(/^I start voting$/, function () {
- cy.get(ss.startVotingButton).should('not.have.class', 'disabled');
- cy.get(ss.startVotingButton).click();
-});
-
-Then(/^Added votes counter shows (\d+)$/, function (number) {
- cy.get(ss.addedVotesCount).should('have.text', number.toString());
-});
-
-Then(/^Removed votes counter shows (\d+)$/, function (number) {
- cy.get(ss.removedVotesCount).should('have.text', number.toString());
-});
-
-Then(/^Total voting number shows (\d+)$/, function (number) {
- cy.get(ss.totalVotingNumber).should('have.text', number.toString());
-});
-
-Then(/^I choose the (\d+) delegate$/, function (number) {
- cy.get(ss.delegateRow).eq(number).as('dg');
- cy.get('@dg').click();
-});
-
-Then(/^I go to confirmation$/, function () {
- cy.get(ss.goToConfirmationButton).click();
-});
-
-Then(/^I see (\d+) removed vote$/, function (number) {
- cy.get(ss.removedVotes).should('have.length', number);
-});
-
-Then(/^I see (\d+) added vote$/, function (number) {
- cy.get(ss.addedVotes).should('have.length', number);
-});
-
-Then(/^I go back to delegates$/, function () {
- cy.get(ss.backToDelegatesButton).click();
-});
-
-Then(/^I see pending votes$/, function () {
- cy.get(ss.spinner).should('have.length', 1);
-});
-
-Then(/^I wait for pending vote to be approved$/, function () {
- cy.get(ss.spinner);
- cy.get(ss.spinner, { timeout: txConfirmationTimeout }).should('have.length', 0);
-});
-
-Then(/^Voted delegate become unchecked$/, function () {
- cy.get('@dg').find(ss.voteCheckbox, { timeout: txConfirmationTimeout }).should('have.class', 'unchecked');
-});
-
-Then(/^Voted delegate become checked$/, function () {
- cy.get('@dg').find(ss.voteCheckbox, { timeout: txConfirmationTimeout }).should('have.class', 'checked');
-});
-
-
-Then(/^Added votes counter shows (\d+)$/, function (number) {
- cy.get(ss.addedVotesCount).should('have.text', '1');
-});
-
-Then(/^I use launch protocol link to vote$/, function () {
- cy.visit(`${urls.delegatesVote}?votes=genesis_12,genesis_14,genesis_16`);
-});
-
-Then(/^I use launch protocol link to unvote$/, function () {
- cy.visit(`${urls.delegatesVote}?unvotes=genesis_12`);
-});
-
-Then(/^I use launch protocol link to vote for already voted$/, function () {
- cy.visit(`${urls.delegatesVote}?votes=genesis_14,genesis_16`);
-});
-
-Then(/^I see (\d+) already voted$/, function (number) {
- cy.get(ss.alreadyVotedPreselection).should('have.length', number);
-});
-
diff --git a/test/cypress/features/login.feature b/test/cypress/features/login.feature
index 90b7c25d51..919c0d2297 100644
--- a/test/cypress/features/login.feature
+++ b/test/cypress/features/login.feature
@@ -7,16 +7,20 @@ Feature: Login
Then I should be connected to mainnet
Scenario: Log in to Mainnet (Network switcher is enabled)
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose mainnet
When I enter first passphrase of genesis
When I login
Then I should be connected to mainnet
Scenario: Log in to Testnet
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose testnet
When I enter first passphrase of testnet_guy
When I login
@@ -26,8 +30,10 @@ Feature: Login
Then I should see lisk monitor features
Scenario: Log in on devnet
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose devnet
When I enter first passphrase of genesis
When I login
@@ -39,22 +45,28 @@ Feature: Login
Then I should be connected to network mainnet
Scenario: Log in to Mainnet (Network switcher is enabled)
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose mainnet
When I am on dashboard page
Then I should be connected to network mainnet
Scenario: Log in to Testnet
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose testnet
When I am on dashboard page
Then I should be connected to network testnet
Scenario: Log in to devnet
- Given showNetwork setting is true
Given I am on Login page
+ When I click on settingsMenu
+ And I click on switchNetworksTrigger
+ And I click on closeDialog
When I choose devnet
When I am on dashboard page
Then I should be connected to network devnet
diff --git a/test/cypress/features/login/login.js b/test/cypress/features/login/login.js
index 4ef060e372..b5b43b50f8 100644
--- a/test/cypress/features/login/login.js
+++ b/test/cypress/features/login/login.js
@@ -94,7 +94,6 @@ Then(/^I should be connected to network ([^\s]+)$/, function (networkName) {
});
Then(/^I should see lisk monitor features$/, function () {
- cy.get(ss.monitorVoting).should('have.length', 1);
cy.get(ss.monitorTransactions).should('have.length', 1);
cy.get(ss.monitorNetwork).should('have.length', 1);
cy.get(ss.monitorBlocks).should('have.length', 1);
diff --git a/test/cypress/features/search.feature b/test/cypress/features/search.feature
index c3b6aa2a5b..41ed7ac1f4 100644
--- a/test/cypress/features/search.feature
+++ b/test/cypress/features/search.feature
@@ -12,7 +12,7 @@ Feature: Search
When I click on searchIcon
And I search for account 537318935439898807L
Then I click on searchAccountRow
- Then I should be on Account page of 537318935439898807L
+ Then I should be on Account page of 537318...8807L
Scenario: Search for non-existent account
Given I am on Dashboard page
diff --git a/test/cypress/features/search/search.js b/test/cypress/features/search/search.js
index 4954dce796..272a7e1ccf 100644
--- a/test/cypress/features/search/search.js
+++ b/test/cypress/features/search/search.js
@@ -2,15 +2,6 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
import ss from '../../../constants/selectors';
-And(/^I search for account ([^s]+)$/, function (string) {
- cy.server();
- cy.route('/api/accounts**').as('requestAccount');
- cy.route('/api/delegates**').as('requestDelegate');
- cy.get(ss.searchInput).type(string);
- cy.wait('@requestAccount');
- cy.wait('@requestDelegate');
-});
-
And(/^I search for delegate ([^s]+)$/, function (string) {
cy.server();
cy.route('/api/delegates**').as('requestDelegate');
@@ -29,5 +20,3 @@ And(/^I search for transaction ([^s]+)$/, function (string) {
Then(/^I should see no results$/, function () {
cy.get(ss.searchMessage).eq(0).should('have.text', 'Nothing has been found. Make sure to double check the ID you typed.');
});
-
-
diff --git a/test/cypress/features/secondPassphraseReg.feature b/test/cypress/features/secondPassphraseReg.feature
deleted file mode 100644
index dd4097309d..0000000000
--- a/test/cypress/features/secondPassphraseReg.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-Feature: Second Passphrase Registration
-
- Scenario: Register second passphrase
- Given I login as second_passphrase_candidate on devnet
- Given I am on Dashboard page
- Given I click on settingsMenu
- Then I see this title: Settings
- Given I click on registerSecondPassphraseBtn
- Then I see this title: Register 2nd passphrase
- And I remember my passphrase
- And I confirm my passphrase
- Then I click on confirmationCheckbox
- And I click on confirmButton
- When I am on Wallet page
- Then The latest transaction is Second passphrase registration
diff --git a/test/cypress/features/secondPassphraseReg/secondPassphraseReg.js b/test/cypress/features/secondPassphraseReg/secondPassphraseReg.js
deleted file mode 100644
index 4cfac3c9dd..0000000000
--- a/test/cypress/features/secondPassphraseReg/secondPassphraseReg.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable */
-import { Given } from 'cypress-cucumber-preprocessor/steps';
-import urls from '../../../constants/urls';
-import ss from '../../../constants/selectors';
-
-const randomDelegateName = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
-
-Given(/^I enter the delegate name$/, function () {
- cy.get(ss.delegateNameInput).click().type(randomDelegateName);
- cy.wait(1200);
-});
-
-Given(/^I remember my passphrase$/, function () {
- this.passphrase = [];
- cy.get(ss.copyPassphrase).each(($el) => {
- this.passphrase = [...this.passphrase, $el[0].textContent];
- });
- cy.get(ss.goToConfirmation).click();
-});
-
-Given(/^I confirm my passphrase$/, function () {
- cy.get(ss.copyPassphrase).each(($wordElement) => {
- if ($wordElement[0].className.includes('empty')) {
- cy.wrap($wordElement).click();
- cy.get(ss.passphraseWordConfirm).each(($option) => {
- if (this.passphrase.includes($option[0].textContent)) cy.wrap($option).click();
- });
- }
- });
- cy.get(ss.passphraseConfirmButton).click();
-});
diff --git a/test/cypress/features/send.feature b/test/cypress/features/send.feature
index 0b5d881870..9079a21325 100644
--- a/test/cypress/features/send.feature
+++ b/test/cypress/features/send.feature
@@ -11,24 +11,11 @@ Feature: Send
And I click on sendButton
Then submittedTransactionMessage should be visible
And I click on closeDialog
- Then I should see pending transaction
Then The latest transaction is transfer to 1234567890L
Then I should not see pending transaction
+ Then I wait 10 seconds
Then The balance is subtracted
- Scenario: Transfer tx with second passphrase
- Given I login as second_passphrase_account on devnet
- Given I am on Wallet page
- Then I click on sendLink
- When I fill 1234567890L in recipientInput field
- And I fill 3 in amountInput field
- And I go to transfer confirmation
- And I enter second passphrase of second_passphrase_account
- And I click on sendButton
- Then submittedTransactionMessage should be visible
- And I click on closeDialog
- Then The latest transaction is transfer to 1234567890L
-
Scenario: Launch protocol prefills fields - from logged in state
Given I login as genesis on devnet
When I follow the launch protokol link
diff --git a/test/cypress/features/send/send.js b/test/cypress/features/send/send.js
index 47103ce90e..e562779d9d 100644
--- a/test/cypress/features/send/send.js
+++ b/test/cypress/features/send/send.js
@@ -6,12 +6,10 @@ import urls from '../../../constants/urls';
import accounts from '../../../constants/accounts';
import compareBalances from '../../utils/compareBalances';
-const transactionFee = 0.1;
+const transactionFee = 0.0026;
const errorMessage = 'Test error';
-
-
Then(/^I follow the launch protokol link$/, function () {
cy.visit(`${urls.send}&recipient=4995063339468361088L&amount=5&reference=test`);
});
@@ -40,8 +38,3 @@ Then(/^The balance is subtracted$/, function () {
compareBalances(this.balanceBefore, this.balanceAfter, 5 + transactionFee);
});
});
-
-
-
-
-
diff --git a/test/cypress/features/txTable_filtering.feature b/test/cypress/features/txTable_filtering.feature
index 4a624f2029..281651aec8 100644
--- a/test/cypress/features/txTable_filtering.feature
+++ b/test/cypress/features/txTable_filtering.feature
@@ -5,23 +5,15 @@ Feature: Transaction table filtering
Given I am on Wallet page
Then I click filter transactions
- Scenario: Filter by 2 Dates, clear 1 filter to filter by 1 Date
- When I type date from 25.05.16
- And I type date to 26.05.16
- And I type amount to 100000001
- And I apply filters
- Then I should see 0 transactions in table
- When Clear filter containing 25
- Then I should see 2 transactions in table
-
Scenario: Filter by 1 Amount, add second filter by 1 Amount
- When I type amount from 4800
+ When I type amount to 5
And I apply filters
Then I should see 4 transactions in table
And I click filter transactions
- When I type amount to 4900
+ When I type amount from 4800
+ When I type amount to 100
And I apply filters
- Then I should see 2 transactions in table
+ Then I should see 3 transactions in table
Scenario: Filter by Message
When I type message without-initialization
@@ -29,11 +21,9 @@ Feature: Transaction table filtering
Then I should see 1 transactions in table
Scenario: Filter by all filters combined, clear all filters
- When I type date from 03.04.19
- And I type date from 03.04.19
- And I type amount from 80
- And I type amount to 80
- And I type message second
+ When I type amount from 0.01
+ And I type amount to 1
+ And I type message without
And I apply filters
Then I should see 1 transactions in table
When I clear all filters
@@ -42,8 +32,8 @@ Feature: Transaction table filtering
Scenario: Incoming/Outgoing applies to filter results
When I type amount from 4900
And I apply filters
- Then I should see 3 transactions in table
+ Then I should see 30 transactions in table
Then I click filter incoming
Then I should see 1 transactions in table
Then I click filter outgoing
- Then I should see 2 transactions in table
+ Then I should see 30 transactions in table
diff --git a/test/cypress/features/voting.feature b/test/cypress/features/voting.feature
new file mode 100644
index 0000000000..c14186d62b
--- /dev/null
+++ b/test/cypress/features/voting.feature
@@ -0,0 +1,42 @@
+Feature: Vote delegate
+
+ Background:
+ Given I login as genesis on devnet
+ Given I am on wallet page
+ When I click on searchIcon
+ And I search for account 537318935439898807L
+ Then I click on searchAccountRow
+ Then I should be on Account page
+
+ Scenario: Vote for a delegate
+ And I click on openAddVoteDialog
+ And I clear input amountInput
+ And I fill 40 in amountInput field
+ And I click on confirmBtn
+ And I click on votingQueueToggle
+ And I click on confirmBtn
+ And I click on confirmBtn
+ And I click on closeDialog
+ Given I am on wallet page
+ Then The latest transaction is voting
+
+ Scenario: Downvote for a delegate
+ And I click on openAddVoteDialog
+ And I clear input amountInput
+ And I fill 40 in amountInput field
+ And I click on removeVote
+ And I click on votingQueueToggle
+ And I click on confirmBtn
+ And I click on confirmBtn
+ And I click on closeDialog
+ Given I am on wallet page
+ Then The latest transaction is voting
+
+ Scenario: Unlock balance
+ Given I am on wallet page
+ Then I should see that 100 LSK are locked
+ Then I click on openUnlockBalanceDialog
+ Then I should see unlocking balance 40
+ And I click on unlockBtn
+ And I click on closeDialog
+ Then The latest transaction is unlocking
diff --git a/test/cypress/features/voting/voting.js b/test/cypress/features/voting/voting.js
new file mode 100644
index 0000000000..5a686e9cf9
--- /dev/null
+++ b/test/cypress/features/voting/voting.js
@@ -0,0 +1,12 @@
+/* eslint-disable */
+import { Then } from 'cypress-cucumber-preprocessor/steps';
+import ss from '../../../constants/selectors';
+
+Then(/^I should see that (.*?) LSK are locked$/, function (amount) {
+ cy.wait(10000);
+ cy.get(`${ss.openUnlockBalanceDialog}`).eq(0).contains(amount);
+});
+
+Then(/^I should see unlocking balance (.*?)$/, function (amount) {
+ cy.get(`${ss.unlockingBalance}`).eq(0).contains(amount);
+});
diff --git a/test/cypress/features/wallet.feature b/test/cypress/features/wallet.feature
index d856f63dde..a0672f2c00 100644
--- a/test/cypress/features/wallet.feature
+++ b/test/cypress/features/wallet.feature
@@ -6,6 +6,11 @@ Feature: Wallet
Then I should see 30 transactions
When I click show more
Then I should see more than 30 transactions
+ Then I click filter incoming
+ Then I should see incoming transaction in table
+ Then I click filter outgoing
+ Then I should not see outgoing transaction in table
+ Then I should not see incoming transaction in table
Scenario: Click leads to tx details
Given I login as genesis on devnet
@@ -13,18 +18,6 @@ Feature: Wallet
When I click on transactionRow
Then I should be on Tx Details page
- Scenario: Incoming/Outgoing/All filtering works
- Given I login as second_passphrase_account on devnet
- Given I am on Wallet page
- Then I should see incoming transaction in table
- Then I should see outgoing transaction in table
- Then I click filter incoming
- Then I should see incoming transaction in table
- Then I should not see outgoing transaction in table
- Then I click filter outgoing
- Then I should not see outgoing transaction in table
- Then I should not see incoming transaction in table
-
Scenario: Send LSK to this account
Given I login as genesis on devnet
Given I am on Wallet page of delegate
diff --git a/test/cypress/features/wallet/wallet.js b/test/cypress/features/wallet/wallet.js
index 92169b768c..b3e6e93130 100644
--- a/test/cypress/features/wallet/wallet.js
+++ b/test/cypress/features/wallet/wallet.js
@@ -25,7 +25,7 @@ Then(/^I should not see outgoing transaction in table$/, function () {
});
Then(/^I should see incoming transaction in table$/, function () {
- cy.get(ss.transactionsTable).contains(accounts.genesis.address).should('exist');
+ cy.get(ss.transactionsTable).contains('16278883833535792633L').should('exist');
});
Then(/^I should not see incoming transaction in table$/, function () {
diff --git a/test/cypress/features/wallet_votes.feature b/test/cypress/features/wallet_votes.feature
index 34c71045f6..56a4052561 100644
--- a/test/cypress/features/wallet_votes.feature
+++ b/test/cypress/features/wallet_votes.feature
@@ -1,21 +1,7 @@
Feature: Wallet Votes
- Background:
+ Scenario: See all votes
Given I login as genesis on devnet
Given I am on Wallet page
Given I open votes tab
-
- Scenario: 30 votes are shown, clicking show more loads more votes
- When I see 30 delegates
- And I click show more
- Then I see more than 30 votes
-
- Scenario: Filtering votes works
- When I filter votes
- Then I see 1 delegates in table
-
- Scenario: Click on voted delegate leads to account page
- When I click on voteRow
- Then I should be on Account page
-
-
+ Then I see 0 delegates in table
diff --git a/test/cypress/features/wallet_votes/wallet_votes.js b/test/cypress/features/wallet_votes/wallet_votes.js
index eca1c9a14b..b5e68e714d 100644
--- a/test/cypress/features/wallet_votes/wallet_votes.js
+++ b/test/cypress/features/wallet_votes/wallet_votes.js
@@ -20,6 +20,10 @@ Then(/^I see more than 30 votes$/, function () {
cy.get(ss.voteRow).should('have.length.greaterThan', 30);
});
+Then(/^I see no votes$/, function () {
+ cy.get(ss.voteRow).should('not.exist');
+});
+
Then(/^I filter votes$/, function () {
cy.get(ss.searchDelegateInput).click().type('genesis_17');
});
diff --git a/test/cypress/utils/compareBalances.js b/test/cypress/utils/compareBalances.js
index 5844fb0ed2..e065327f89 100644
--- a/test/cypress/utils/compareBalances.js
+++ b/test/cypress/utils/compareBalances.js
@@ -3,5 +3,5 @@ const castBalanceStringToNumber = number => parseFloat(number.replace(/,/g, ''))
export default function compareBalances(balanceBeforeString, balanceAfterString, cost) {
const balanceBefore = castBalanceStringToNumber(balanceBeforeString.replace(' LSK', ''));
const balanceAfter = castBalanceStringToNumber(balanceAfterString.replace(' LSK', ''));
- expect(balanceAfter).to.be.equal(parseFloat((balanceBefore - cost).toFixed(6)));
+ expect(balanceAfter - parseFloat(balanceBefore - cost)).to.lt(0.1);
}
diff --git a/test/dev_blockchain.db.gz b/test/dev_blockchain.db.gz
index 09b3686a97..1cf726b2f3 100644
Binary files a/test/dev_blockchain.db.gz and b/test/dev_blockchain.db.gz differ
diff --git a/test/unit-test-utils/flushPromises.js b/test/unit-test-utils/flushPromises.js
new file mode 100644
index 0000000000..bfd51ed4a9
--- /dev/null
+++ b/test/unit-test-utils/flushPromises.js
@@ -0,0 +1,3 @@
+const flushPromises = () => new Promise(setImmediate);
+
+export default flushPromises;