Skip to content

Commit

Permalink
Merge pull request #3884 from LiskHQ/3736-add-use-max
Browse files Browse the repository at this point in the history
 Add "use maximum amount for voting" button to edit vote dialog- Closes #3736
  • Loading branch information
reyraa authored Oct 26, 2021
2 parents 4431247 + ae7977e commit b0afa16
Show file tree
Hide file tree
Showing 21 changed files with 321 additions and 123 deletions.
5 changes: 4 additions & 1 deletion i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"Apply filters": "Apply filters",
"At this moment there is a connection problem with the tweets feed": "At this moment there is a connection problem with the tweets feed",
"Auto sign out": "Auto sign out",
"Available balance": "Available balance",
"Awaiting slot": "Awaiting slot",
"BTC address": "BTC address",
"Back": "Back",
Expand Down Expand Up @@ -427,7 +428,7 @@
"Send": "Send",
"Send LSK and BTC": "Send LSK and BTC",
"Send a reclaim transaction": "Send a reclaim transaction",
"Send entire balance": "Send entire balance",
"Send maximum amount": "Send maximum amount",
"Send using second passphrase right away": "Send using second passphrase right away",
"Send {{amount}} {{token}}": "Send {{amount}} {{token}}",
"Send {{token}}": "Send {{token}}",
Expand Down Expand Up @@ -546,6 +547,7 @@
"Update now": "Update now",
"Update to your new account": "Update to your new account",
"Updates downloaded, application has to be restarted to apply the updates.": "Updates downloaded, application has to be restarted to apply the updates.",
"Use maximum amount": "Use maximum amount",
"Use your old passphrase ": "Use your old passphrase ",
"Using your recovery phrase this way should be avoided, and if you don’t need to access your funds now, we recommend waiting for full support of hardware wallets in Lisk Desktop 2.2.0.": "Using your recovery phrase this way should be avoided, and if you don’t need to access your funds now, we recommend waiting for full support of hardware wallets in Lisk Desktop 2.2.0.",
"Verify address": "Verify address",
Expand Down Expand Up @@ -594,6 +596,7 @@
"Written by": "Written by",
"YYYY": "YYYY",
"You are about to send your entire balance": "You are about to send your entire balance",
"You are about to vote almost your entire balance": "You are about to vote almost your entire balance",
"You are disconnected": "You are disconnected",
"You can also download, print and store safely your passphrase.": "You can also download, print and store safely your passphrase.",
"You can learn more": "You can learn more",
Expand Down
30 changes: 21 additions & 9 deletions src/components/screens/editVote/editVotes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import * as votingActions from '@actions';
import { mountWithRouterAndStore } from '@utils/testHelpers';
import EditVote from './index';

jest.mock('@api/transaction', () => ({
getTransactionFee: jest.fn().mockImplementation(() => Promise.resolve({ value: '0.046' })),
}));

jest.mock('@actions/voting', () => ({
voteEdited: jest.fn(),
}));

describe('EditVote', () => {
const genesis = 'lskdxc4ta5j43jp9ro3f8zqbxta9fn6jwzjucw7yt';
const delegate = 'lskehj8am9afxdz8arztqajy52acnoubkzvmo9cjy';
const propsWithoutSearch = {
t: str => str,
history: {
Expand All @@ -22,21 +28,26 @@ describe('EditVote', () => {
history: {
push: jest.fn(),
location: {
search: '?address=987665L&modal=editVote',
search: `?address=${delegate}&modal=editVote`,
},
},
};
const noVote = {};
const withVotes = {
'123456L': { confirmed: 1e9, unconfirmed: 1e9 },
'987665L': { confirmed: 1e9, unconfirmed: 1e9 },
[genesis]: { confirmed: 1e9, unconfirmed: 1e9 },
[delegate]: { confirmed: 1e9, unconfirmed: 1e9 },
};
const state = {
account: {
passphrase: 'test',
info: {
LSK: { summary: { address: '123456L', balance: 10004674000 } },
BTC: { summary: { address: '123456L', balance: 0 } },
LSK: { summary: { address: genesis, balance: 10004674000 } },
BTC: { summary: { address: genesis, balance: 0 } },
},
},
settings: {
token: {
active: 'LSK',
},
},
};
Expand All @@ -63,7 +74,7 @@ describe('EditVote', () => {
);
wrapper.find('.remove-vote').at(0).simulate('click');
expect(votingActions.voteEdited).toHaveBeenCalledWith([{
address: '123456L',
address: genesis,
amount: 0,
}]);
});
Expand All @@ -74,7 +85,7 @@ describe('EditVote', () => {
);
wrapper.find('.remove-vote').at(0).simulate('click');
expect(votingActions.voteEdited).toHaveBeenCalledWith([{
address: '987665L',
address: delegate,
amount: 0,
}]);
});
Expand All @@ -91,7 +102,7 @@ describe('EditVote', () => {
});
wrapper.find('.confirm').at(0).simulate('click');
expect(votingActions.voteEdited).toHaveBeenCalledWith([{
address: '123456L',
address: genesis,
amount: 2e9,
}]);
});
Expand All @@ -107,6 +118,7 @@ describe('EditVote', () => {
name: 'vote',
},
});
wrapper.update();
act(() => { jest.advanceTimersByTime(300); });
wrapper.update();
amountField = wrapper.find('input[name="vote"]').at(0);
Expand All @@ -127,7 +139,7 @@ describe('EditVote', () => {
});
wrapper.find('.confirm').at(0).simulate('click');
expect(votingActions.voteEdited).toHaveBeenCalledWith([{
address: '987665L',
address: delegate,
amount: 2e9,
}]);
});
Expand Down
64 changes: 64 additions & 0 deletions src/components/screens/editVote/getMaxAmount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getTransactionFee } from '@api/transaction';
import {
VOTE_AMOUNT_STEP,
MIN_ACCOUNT_BALANCE,
MODULE_ASSETS_NAME_ID_MAP,
} from '@constants';
import { toRawLsk } from '@utils/lsk';
import { normalizeVotesForTx, getNumberOfSignatures } from '@shared/transactionPriority';

/**
* Calculates the maximum vote amount possible. It
* Takes the current votes, minimum account balance and
* transaction fee into account.
*
* @param {object} account - Lisk account info from the Redux store
* @param {object} network - Network info from the Redux store
* @param {object} transaction - Raw transaction object
* @param {object} voting - List of votes from the Redux store
* @returns {Number} - Maximum possible vote amount
*/
const getMaxAmount = async (account, network, voting, address) => {
const balance = account.summary?.balance ?? 0;
const totalUnconfirmedVotes = Object.values(voting)
.filter(vote => vote.confirmed < vote.unconfirmed)
.map(vote => vote.unconfirmed - vote.confirmed)
.reduce((total, amount) => (total + amount), 0);

const maxVoteAmount = Math.floor(
(balance - totalUnconfirmedVotes - MIN_ACCOUNT_BALANCE) / 1e9,
) * 1e9;

const transaction = {
fee: 1e6,
votes: normalizeVotesForTx({
...voting,
[address]: {
confirmed: voting[address] ? voting[address].confirmed : 0,
unconfirmed: maxVoteAmount,
},
}),
nonce: account.sequence?.nonce,
senderPublicKey: account.summary?.publicKey,
moduleAssetId: MODULE_ASSETS_NAME_ID_MAP.voteDelegate,
};

const maxAmountFee = await getTransactionFee({
token: 'LSK',
account,
network,
transaction,
selectedPriority: { title: 'Normal', value: 0, selectedIndex: 0 }, // Always set to LOW
numberOfSignatures: getNumberOfSignatures(account),
}, 'LSK');

// If the "sum of vote amounts + fee + dust" exceeds balance
// return 10 LSK less, since votes must be multiplications of 10 LSK.
if ((maxVoteAmount + toRawLsk(maxAmountFee.value)) <= (
balance - totalUnconfirmedVotes - MIN_ACCOUNT_BALANCE)) {
return maxVoteAmount;
}
return maxVoteAmount - VOTE_AMOUNT_STEP;
};

export default getMaxAmount;
87 changes: 87 additions & 0 deletions src/components/screens/editVote/getMaxAmount.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import getMaxAmount from './getMaxAmount';
import accounts from '../../../../test/constants/accounts';

const account = {
...accounts.genesis,
summary: {
...accounts.genesis.summary,
balance: 100.106e8,
},
};
const network = {
network: {
networks: {
LSK: {
networkIdentifier: '15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c',
moduleAssets: [
{
id: '2:0',
name: 'token:transfer',
},
{
id: '4:0',
name: 'keys:registerMultisignatureGroup',
},
{
id: '5:0',
name: 'dpos:registerDelegate',
},
{
id: '5:1',
name: 'dpos:voteDelegate',
},
{
id: '5:2',
name: 'dpos:unlockToken',
},
{
id: '5:3',
name: 'dpos:reportDelegateMisbehavior',
},
{
id: '1000:0',
name: 'legacyAccount:reclaimLSK',
},
],
serviceUrl: 'https://testnet-service.lisk.com',
},
},
name: 'testnet',
},
};
const voting = {
voting: {
[accounts.genesis.summary.address]: {
confirmed: 20e8,
unconfirmed: 20e8,
username: 'genesis',
},
},
};

jest.mock('@api/transaction', () => ({
getTransactionFee: jest.fn().mockImplementation(() => Promise.resolve({ value: '0.046' })),
}));

jest.mock('@actions/voting', () => ({
voteEdited: jest.fn(),
}));

describe('getMaxAmount', () => {
it('Returns 10n LSK if: balance >= (10n LSK + fee + dust)', async () => {
const result = await getMaxAmount(account, network, voting, accounts.genesis.summary.address);
expect(result).toBe(1e10);
});

it('Returns (n-1) * 10 LSK if: 10n LSK < balance < (10n LSK + fee + dust)', async () => {
const acc = {
...accounts.genesis,
summary: {
...accounts.genesis.summary,
balance: 1e10,
},
};
const result = await getMaxAmount(acc, network, voting, accounts.genesis.summary.address);
expect(result).toBe(9e9);
});
});
28 changes: 18 additions & 10 deletions src/components/screens/editVote/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom';
import { withTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -19,7 +19,7 @@ import LiskAmount from '@shared/liskAmount';
import Converter from '@shared/converter';
import { PrimaryButton, WarningButton } from '@toolbox/buttons';
import useVoteAmountField from './useVoteAmountField';

import getMaxAmount from './getMaxAmount';
import styles from './editVote.css';

const getTitles = t => ({
Expand All @@ -38,13 +38,18 @@ const AddVote = ({
history, t,
}) => {
const dispatch = useDispatch();
const { account, network, voting } = useSelector(state => state);
const host = useSelector(state => state.account.info.LSK.summary.address);
const address = selectSearchParamValue(history.location.search, 'address');
const existingVote = useSelector(state => state.voting[address || host]);
const activeToken = tokenMap.LSK.key;
const balance = useSelector(selectAccountBalance);
const [voteAmount, setVoteAmount] = useVoteAmountField(existingVote ? fromRawLsk(existingVote.unconfirmed) : '', balance);
const [voteAmount, setVoteAmount] = useVoteAmountField(existingVote ? fromRawLsk(existingVote.unconfirmed) : '');
const mode = existingVote ? 'edit' : 'add';
const [maxAmount, setMaxAmount] = useState(0);
useEffect(() => {
getMaxAmount(account.info.LSK, network, voting, address || host)
.then(setMaxAmount);
}, [account, voting]);

const confirm = () => {
dispatch(voteEdited([{
Expand Down Expand Up @@ -77,10 +82,10 @@ const AddVote = ({
<span>{titles.description}</span>
</BoxInfoText>
<BoxInfoText className={styles.accountInfo}>
<p className={styles.balanceTitle}>Available balance</p>
<p className={styles.balanceTitle}>{t('Available balance')}</p>
<div className={styles.balanceDetails}>
<span className={styles.lskValue}>
<LiskAmount val={balance} token={activeToken} />
<LiskAmount val={balance} token={tokenMap.LSK.key} />
</span>
<Converter
className={styles.fiatValue}
Expand All @@ -92,11 +97,14 @@ const AddVote = ({
<label className={styles.fieldGroup}>
<AmountField
amount={voteAmount}
setAmountField={setVoteAmount}
title={t('Vote amount (LSK)')}
inputPlaceHolder={t('Insert vote amount')}
name="vote"
onChange={setVoteAmount}
maxAmount={{ value: maxAmount || balance }}
displayConverter
label={t('Vote amount (LSK)')}
placeholder={t('Insert vote amount')}
useMaxLabel={t('Use maximum amount')}
useMaxWarning={t('You are about to vote almost your entire balance')}
name="vote"
/>
</label>
</BoxContent>
Expand Down
Loading

0 comments on commit b0afa16

Please sign in to comment.