Skip to content

Commit

Permalink
Merge pull request #2453 from LiskHQ/2445-implement-signSendTransacti…
Browse files Browse the repository at this point in the history
…on-and-signVoteTransaction-utils

Implement and use signSendTransaction and signVoteTransaction utils - closes #2445
  • Loading branch information
Osvaldo Vega Agüero committed Sep 18, 2019
2 parents 5a56739 + 40549e4 commit d737513
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 105 deletions.
8 changes: 7 additions & 1 deletion libs/hwManager/communication.js
Expand Up @@ -47,7 +47,13 @@ const getPublicKey = async (data) => {
* @param {object} data.tx -> Object with all transaction information
*/
const signTransaction = async (data) => {
const response = await executeCommand(IPC_MESSAGES.SIGN_TRANSACTION, data);
const response = await executeCommand(
IPC_MESSAGES.HW_COMMAND,
{
action: IPC_MESSAGES.SIGN_TRANSACTION,
data,
},
);
return response;
};

Expand Down
5 changes: 3 additions & 2 deletions src/actions/transactions.js
Expand Up @@ -11,7 +11,8 @@ import { passphraseUsed } from './account';
import { getTimeOffset } from '../utils/hacks';
import { sendWithHW } from '../utils/api/hwWallet';
import { loginType } from '../constants/hwConstants';
import { transactions as transactionsAPI, hardwareWallet as hwAPI } from '../utils/api';
import { transactions as transactionsAPI } from '../utils/api';
import { signSendTransaction } from '../utils/hwManager';

// ========================================= //
// ACTION CREATORS
Expand Down Expand Up @@ -257,7 +258,7 @@ export const transactionCreated = data => async (dispatch, getState) => {
{ ...data, timeOffset, network },
createTransactionType.transaction,
))
: await to(hwAPI.create(account, data));
: await to(signSendTransaction(account, data));

if (error) {
return dispatch({
Expand Down
54 changes: 25 additions & 29 deletions src/utils/api/delegates.js
@@ -1,13 +1,12 @@
import { to } from 'await-to-js';
import Lisk from '@liskhq/lisk-client';
import i18next from 'i18next';
import { getBlocks } from './blocks';
import { getTransactions } from './transactions';
import { loadDelegateCache, updateDelegateCache } from '../delegates';
import { loginType } from '../../constants/hwConstants';
import { splitVotesIntoRounds } from '../voting';
import { voteWithHW } from './hwWallet';
import transactionTypes from '../../constants/transactionTypes';
import { signVoteTransaction } from '../hwManager';

export const getDelegates = (liskAPIClient, options) => liskAPIClient.delegates.get(options);

Expand Down Expand Up @@ -79,29 +78,22 @@ export const getDelegateByName = (liskAPIClient, name) => new Promise(async (res
});

const voteWithPassphrase = (
liskAPIClient,
passphrase,
votes,
unvotes,
secondPassphrase,
timeOffset,
) => (
Promise.all(splitVotesIntoRounds({
votes: [...votes],
unvotes: [...unvotes],
}).map(({ votes, unvotes }) => { // eslint-disable-line no-shadow
const transaction = Lisk.transaction.castVotes({
) => (Promise.all(splitVotesIntoRounds({ votes: [...votes], unvotes: [...unvotes] })
// eslint-disable-next-line no-shadow
.map(({ votes, unvotes }) => (Lisk.transaction.castVotes(
{
votes,
unvotes,
passphrase,
secondPassphrase,
timeOffset,
});
return new Promise((resolve, reject) => {
liskAPIClient.transactions.broadcast(transaction)
.then(() => resolve(transaction)).catch(reject);
});
}))
},
))))
);

export const castVotes = async ({
Expand All @@ -112,20 +104,24 @@ export const castVotes = async ({
secondPassphrase,
timeOffset,
}) => {
switch (account.loginType) {
case loginType.normal:
return voteWithPassphrase(
liskAPIClient, account.passphrase,
votedList, unvotedList, secondPassphrase, timeOffset,
);
case loginType.ledger:
case loginType.trezor:
return voteWithHW(liskAPIClient, account, votedList, unvotedList);
default:
return new Promise((resolve, reject) => {
reject(i18next.t('Login Type not recognized.'));
});
}
const [error, signedTransactions] = account.loginType === loginType.normal
? await to(voteWithPassphrase(
account.passphrase,
votedList,
unvotedList,
secondPassphrase,
timeOffset,
))
: await to(signVoteTransaction(account, votedList, unvotedList));

if (error) return error;
return Promise.all(signedTransactions.map(transaction => (
new Promise((resolve, reject) => {
liskAPIClient.transactions.broadcast(transaction)
.then(() => resolve(transaction))
.catch(reject);
})
)));
};

export const getVotes = (liskAPIClient, { address }) =>
Expand Down
134 changes: 72 additions & 62 deletions src/utils/api/delegates.test.js
Expand Up @@ -14,15 +14,15 @@ import {
import { loginType } from '../../constants/hwConstants';
import accounts from '../../../test/constants/accounts';
import delegates from '../../../test/constants/delegates';
import * as hwWallet from './hwWallet';
import * as hwManager from '../hwManager';

describe('Utils: Delegate', () => {
let liskAPIClientMockDelegates;
let liskAPIClientMockVotes;
let liskAPIClientMockTransations;
let liskTransactionsCastVotesStub;
let liskTransactionsRegisterDelegateStub;
let voteWithHWStub;
let signVoteTransaction;
const timeOffset = 0;

const liskAPIClient = {
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('Utils: Delegate', () => {
liskAPIClientMockVotes = sinon.mock(liskAPIClient.votes);
liskAPIClientMockTransations = sinon.stub(liskAPIClient.transactions, 'broadcast').returnsPromise().resolves({ id: '1234' });
sinon.stub(liskAPIClient.transactions, 'get').returnsPromise();
voteWithHWStub = sinon.stub(hwWallet, 'voteWithHW');
signVoteTransaction = sinon.stub(hwManager, 'signVoteTransaction').returnsPromise().resolves([{ id: '1234' }]);
});

afterEach(() => {
Expand All @@ -67,7 +67,7 @@ describe('Utils: Delegate', () => {
liskTransactionsRegisterDelegateStub.restore();

liskAPIClient.transactions.get.restore();
voteWithHWStub.restore();
signVoteTransaction.restore();
localStorage.clear();
});

Expand Down Expand Up @@ -187,64 +187,6 @@ describe('Utils: Delegate', () => {
});
});

describe('castVotes', () => {
it('should call castVotes and broadcast transaction', () => {
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);

castVotes({
liskAPIClient,
account: {
...accounts.genesis,
loginType: loginType.normal,
},
votedList: votes,
unvotedList: unvotes,
secondPassphrase,
timeOffset,
});
expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);

castVotes({
liskAPIClient,
account: {
...accounts.genesis,
loginType: loginType.ledger,
},
votedList: votes,
unvotedList: unvotes,
secondPassphrase,
timeOffset,
});
expect(voteWithHWStub).to.have.been.calledWith();
});

it('should call return error if account.loginType is not recognized', () => (
expect(castVotes({
liskAPIClient,
account: {
...accounts.genesis,
loginType: 'something unknown',
},
})).to.be.rejectedWith('Login Type not recognized.')
));
});

describe('getVotes', () => {
it('should get votes for an address with no parameters', () => {
const address = '123L';
Expand Down Expand Up @@ -289,4 +231,72 @@ describe('Utils: Delegate', () => {
expect(liskAPIClient.transactions.broadcast).to.have.been.calledWith(transaction);
});
});

describe('castVotes', () => {
it('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('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);
});
});
});
74 changes: 64 additions & 10 deletions src/utils/hwManager.js
@@ -1,5 +1,5 @@
// istanbul ignore file
// TODO include unit test
import { castVotes, utils } from '@liskhq/lisk-transactions';
import i18next from 'i18next';
import { getAccount } from './api/lsk/account';
import {
getPublicKey,
Expand All @@ -8,6 +8,8 @@ import {
subscribeToDeviceDisonnceted,
subscribeToDevicesList,
} from '../../libs/hwManager/communication';
import { createSendTX } from './rawTransactionWrapper';
import { splitVotesIntoRounds } from './voting';

/**
* getAccountsFromDevice - Function.
Expand All @@ -32,20 +34,72 @@ const getAccountsFromDevice = async ({ device: { deviceId }, networkConfig }) =>
* signSendTransaction - Function.
* This function is used for sign a send transaction.
*/
const signSendTransaction = () => {
// TODO implement logic for this function
signTransaction();
throw new Error('not umplemented');
// eslint-disable-next-line max-statements
const signSendTransaction = async (account, data) => {
const transactionObject = createSendTX(
account.info.LSK.publicKey,
data.recipientId,
data.amount,
data.data,
);

const transaction = {
deviceId: account.hwInfo.deviceId,
index: account.hwInfo.derivationIndex,
tx: transactionObject,
};

try {
const signature = await signTransaction(transaction);
const signedTransaction = { ...transactionObject, signature };
const result = { ...signedTransaction, id: utils.getTransactionId(signedTransaction) };
return result;
} catch (error) {
throw new Error(error);
}
};

/**
* signVoteTransaction - Function.
* This function is used for sign a vote transaction.
*/
const signVoteTransaction = () => {
// TODO implement logic for this function
signTransaction();
throw new Error('not umplemented');
const signVoteTransaction = async (
account,
votedList,
unvotedList,
) => {
const signedTransactions = [];
const votesChunks = splitVotesIntoRounds({ votes: [...votedList], unvotes: [...unvotedList] });

try {
for (let i = 0; i < votesChunks.length; i++) {
const transactionObject = {
...castVotes(votesChunks[i]),
senderPublicKey: account.publicKey,
recipientId: account.address,
};

// 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,
id: utils.getTransactionId({ ...transactionObject, signature }),
});
}

return signedTransactions;
} catch (error) {
throw new Error(i18next.t(
'The transaction has been canceled on your {{model}}',
{ model: account.hwInfo.deviceModel },
));
}
};

export {
Expand Down

0 comments on commit d737513

Please sign in to comment.