diff --git a/src/components/screens/dashboard/newsFeed/newsParser.js b/src/components/screens/dashboard/newsFeed/newsParser.js index 0057766126..1bddb0129e 100644 --- a/src/components/screens/dashboard/newsFeed/newsParser.js +++ b/src/components/screens/dashboard/newsFeed/newsParser.js @@ -6,9 +6,9 @@ const NewsParser = ({ children }) => { const REGEX_USER = /\B(@[a-zA-Z0-9_]+)/g; // regex for @users const REGEX_HASHTAG = /\B(#[A-Za-z0-9-_]+)/g; // regex for #hashtags const textWithHashtag = reactStringReplace(Html5Entities.decode(children), REGEX_HASHTAG, - (hashtag, i) => ( + (hashtag, index, offset) => ( { {hashtag} )); - const textWithUsers = reactStringReplace(textWithHashtag, REGEX_USER, (user, i) => ( + const textWithUsers = reactStringReplace(textWithHashtag, REGEX_USER, (user, index, offset) => ( ( -
+
{accountsList.map((data) => ( { - // if (activeTab === 'active') console.log('lastBlock', lastBlock); if (state === undefined) { return ( - diff --git a/src/components/screens/multiSignature/form/form.js b/src/components/screens/multiSignature/form/form.js index 7dd1b63e3d..6c86f81c73 100644 --- a/src/components/screens/multiSignature/form/form.js +++ b/src/components/screens/multiSignature/form/form.js @@ -31,7 +31,8 @@ const validators = [ message: t => t('Either change the optional member to mandatory or define more optional members.'), }, { - pattern: (mandatory, optional) => mandatory.length === 0 && optional.length > 0, + pattern: (mandatory, optional, signatures) => + mandatory.length === 0 && optional.length === signatures, message: t => t('All members can not be optional. Consider changing them to mandatory.'), }, { diff --git a/src/components/screens/multiSignature/index.js b/src/components/screens/multiSignature/index.js index 7ec9020d2e..77ef83d2f5 100644 --- a/src/components/screens/multiSignature/index.js +++ b/src/components/screens/multiSignature/index.js @@ -2,9 +2,10 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; -import MultiStep from '../../shared/multiStep'; -import { removeSearchParamsFromUrl } from '../../../utils/searchParams'; -import Dialog from '../../toolbox/dialog/dialog'; +import TransactionSignature from '@shared/transactionSignature'; +import MultiStep from '@shared/multiStep'; +import { removeSearchParamsFromUrl } from '@utils/searchParams'; +import Dialog from '@toolbox/dialog/dialog'; import Form from './form'; import Summary from './summary'; @@ -25,6 +26,7 @@ const MultiSignature = ({ history }) => { >
+ diff --git a/src/components/screens/multiSignature/summary/index.js b/src/components/screens/multiSignature/summary/index.js index fc9e013aec..04279dba93 100644 --- a/src/components/screens/multiSignature/summary/index.js +++ b/src/components/screens/multiSignature/summary/index.js @@ -9,8 +9,6 @@ import Summary from './summary'; const mapStateToProps = state => ({ account: getActiveTokenAccount(state), - signedTransaction: state.transactions.signedTransaction, - txSignatureError: state.transactions.txSignatureError, }); const mapDispatchToProps = { diff --git a/src/components/screens/multiSignature/summary/summary.js b/src/components/screens/multiSignature/summary/summary.js index 323c0c6662..1d3afbe833 100644 --- a/src/components/screens/multiSignature/summary/summary.js +++ b/src/components/screens/multiSignature/summary/summary.js @@ -1,7 +1,6 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { MODULE_ASSETS_NAME_ID_MAP } from '@constants'; import TransactionInfo from '@shared/transactionInfo'; -import { isEmpty } from '@utils/helpers'; import Box from '@toolbox/box'; import BoxContent from '@toolbox/box/content'; import BoxFooter from '@toolbox/box/footer'; @@ -22,16 +21,22 @@ const Summary = ({ prevStep, nextStep, multisigGroupRegistered, - signedTransaction, - txSignatureError, }) => { // eslint-disable-next-line max-statements const onConfirm = () => { - multisigGroupRegistered({ - fee, - mandatoryKeys, - optionalKeys, - numberOfSignatures, + nextStep({ + rawTransaction: { + fee: String(fee), + mandatoryKeys, + optionalKeys, + numberOfSignatures, + }, + actionFunction: multisigGroupRegistered, + statusInfo: { + mandatoryKeys, + optionalKeys, + numberOfSignatures, + }, }); }; @@ -39,22 +44,6 @@ const Summary = ({ prevStep({ mandatoryKeys, optionalKeys, numberOfSignatures }); }; - useEffect(() => { - // success - if (!isEmpty(signedTransaction)) { - nextStep({ - transactionInfo: signedTransaction, - }); - } - }, [signedTransaction]); - - useEffect(() => { - // error - if (txSignatureError) { - nextStep({ error: txSignatureError }); - } - }, [txSignatureError]); - return (
diff --git a/src/components/screens/multiSignature/summary/summary.test.js b/src/components/screens/multiSignature/summary/summary.test.js index a15509b70d..55b2f299c3 100644 --- a/src/components/screens/multiSignature/summary/summary.test.js +++ b/src/components/screens/multiSignature/summary/summary.test.js @@ -3,7 +3,6 @@ import { mount } from 'enzyme'; import * as hwManagerAPI from '@utils/hwManager'; import Summary from './summary'; import accounts from '../../../../../test/constants/accounts'; -import flushPromises from '../../../../../test/unit-test-utils/flushPromises'; const mockTransaction = { fee: 0.02, @@ -56,8 +55,20 @@ describe('Multisignature summary component', () => { it('Should call props.nextStep', async () => { wrapper.find('button.confirm').simulate('click'); - await flushPromises(); - expect(props.multisigGroupRegistered).toHaveBeenCalledWith(mockTransaction); + expect(props.nextStep).toHaveBeenCalledWith({ + rawTransaction: { + fee: String(props.fee), + mandatoryKeys: props.mandatoryKeys, + optionalKeys: props.optionalKeys, + numberOfSignatures: props.numberOfSignatures, + }, + actionFunction: props.multisigGroupRegistered, + statusInfo: { + mandatoryKeys: props.mandatoryKeys, + optionalKeys: props.optionalKeys, + numberOfSignatures: props.numberOfSignatures, + }, + }); }); it('Should call props.prevStep', () => { diff --git a/src/components/screens/wallet/explorer.js b/src/components/screens/wallet/explorer.js index 12656d5c59..045112bcf8 100644 --- a/src/components/screens/wallet/explorer.js +++ b/src/components/screens/wallet/explorer.js @@ -42,24 +42,24 @@ const Wallet = ({ pending={[]} activeToken={activeToken} discreetMode={discreetMode} - tabName={t('Transactions')} - tabId="transactions" + name={t('Transactions')} + id="transactions" address={address} /> {activeToken !== 'BTC' ? ( ) : null} {isDelegate ? ( ) diff --git a/src/components/screens/wallet/index.js b/src/components/screens/wallet/index.js index 01ea1fffc7..07eb332b86 100644 --- a/src/components/screens/wallet/index.js +++ b/src/components/screens/wallet/index.js @@ -59,24 +59,24 @@ const Wallet = ({ t, history }) => { confirmedLength={confirmed.length} activeToken={activeToken} discreetMode={discreetMode} - tabName={t('Transactions')} - tabId="Transactions" + name={t('Transactions')} + id="Transactions" address={address} /> {activeToken !== 'BTC' ? ( ) : null} {isDelegate ? ( ) @@ -85,8 +85,8 @@ const Wallet = ({ t, history }) => { ? ( ) : null} */} diff --git a/src/components/shared/transactionResult/transactionResult.js b/src/components/shared/transactionResult/transactionResult.js index 2985ed331b..80cca68bb4 100644 --- a/src/components/shared/transactionResult/transactionResult.js +++ b/src/components/shared/transactionResult/transactionResult.js @@ -12,6 +12,7 @@ const TransactionResult = (props) => { && props.status.code !== txStatusTypes.broadcastSuccess && ( props.transactions.signedTransaction.signatures.length > 1 + || props.status.code === txStatusTypes.multisigSignaturePartialSuccess || props.account.summary.isMultisignature || props.account.summary.publicKey !== props.transactions.signedTransaction.senderPublicKey.toString('hex') ); diff --git a/src/components/shared/transactionSummary/footer.js b/src/components/shared/transactionSummary/footer.js index b82e689e00..f99e5754c0 100644 --- a/src/components/shared/transactionSummary/footer.js +++ b/src/components/shared/transactionSummary/footer.js @@ -70,7 +70,9 @@ const Footer = ({ }) => { const isMultisignature = !!account.keys?.numberOfSignatures; const hasSecondPass = account.keys?.numberOfSignatures === 2 - && account.keys.mandatoryKeys.length === 2 && account.keys.optionalKeys.length === 0; + && account.keys.mandatoryKeys.length === 2 + && account.keys.optionalKeys.length === 0 + && !account.hwInfo; const [inputStatus, setInputStatus] = useState(hasSecondPass ? 'hidden' : 'notRequired'); return ( diff --git a/src/components/toolbox/tabsContainer/tabsContainer.js b/src/components/toolbox/tabsContainer/tabsContainer.js index 31fd90b780..c9a8b7529e 100644 --- a/src/components/toolbox/tabsContainer/tabsContainer.js +++ b/src/components/toolbox/tabsContainer/tabsContainer.js @@ -19,7 +19,7 @@ class TabsContainer extends React.Component { // eslint-disable-next-line class-methods-use-this filterChildren(children) { const _children = (Array.isArray(children) && children.filter(c => c)) || [children]; - return _children.filter(tab => !!tab.props.tabId); + return _children.filter(tab => !!tab.props.id); } shouldComponentUpdate(nextProps, nextState) { @@ -29,7 +29,7 @@ class TabsContainer extends React.Component { /* istanbul ignore next */ if (nextTabs.length !== currentTabs.length) { - const activeTab = (nextTabs.length > 1 && (this.props.activeTab || nextTabs[0].props.tabId)) || ''; + const activeTab = (nextTabs.length > 1 && (this.props.activeTab || nextTabs[0].props.id)) || ''; this.setState({ activeTab }); return false; } @@ -47,7 +47,7 @@ class TabsContainer extends React.Component { this.setState({ activeTab: (React.Children.count(children) > 1 - && (tab || this.props.activeTab || children[0].props.tabId)) + && (tab || this.props.activeTab || children[0].props.id)) || '', }); } @@ -60,9 +60,9 @@ class TabsContainer extends React.Component {
({ - name: tab.props.tabName, - value: tab.props.tabName, - id: tab.props.tabId, + name: tab.props.name, + value: tab.props.name, + id: tab.props.id, }))} active={activeTab} /> @@ -70,7 +70,7 @@ class TabsContainer extends React.Component { {React.Children.map(children, tab => ( React.isValidElement(tab) && ( -
+
{ tab }
) diff --git a/src/components/toolbox/tabsContainer/tabsContainer.test.js b/src/components/toolbox/tabsContainer/tabsContainer.test.js index 1abb028d30..b16fd3c970 100644 --- a/src/components/toolbox/tabsContainer/tabsContainer.test.js +++ b/src/components/toolbox/tabsContainer/tabsContainer.test.js @@ -4,7 +4,7 @@ import TabsContainer from './tabsContainer'; describe.skip('TabsContainer Component', () => { let wrapper; - const children = [0, 1, 2].map((tab, key) =>
{`tab-${tab}`}
); + const children = [0, 1, 2].map((tab, key) =>
{`tab-${tab}`}
); beforeEach(() => { wrapper = shallow( @@ -13,14 +13,14 @@ describe.skip('TabsContainer Component', () => { }); it('Should act as noop if only one children present', () => { - wrapper = shallow(
); + wrapper = shallow(
); expect(wrapper).toMatchSelector('div'); }); it('Should act as noop if children updates to only one', () => { wrapper = shallow({children}); wrapper.setProps({ - children:
, + children:
, }); wrapper.update(); expect(wrapper).toMatchSelector('div'); @@ -28,8 +28,8 @@ describe.skip('TabsContainer Component', () => { it('Should update tabs if children updates', () => { wrapper = shallow( -
-
+
+
); wrapper.setProps({ children }); wrapper.update(); @@ -38,8 +38,8 @@ describe.skip('TabsContainer Component', () => { expect(wrapper.find('.contentHolder > div').at(0)).toHaveClassName('active'); wrapper = shallow( -
-
+
+
); wrapper.setProps({ children, activeTab: 'tab-1' }); wrapper.update(); @@ -55,13 +55,13 @@ describe.skip('TabsContainer Component', () => { expect(wrapper.find('.contentHolder > div').at(2)).toHaveClassName('active'); }); - it('Should render tabs for each child that has tabName props', () => { + it('Should render tabs for each child that has name props', () => { expect(wrapper).toContainMatchingElements(3, 'li'); expect(wrapper).toContainMatchingElements(3, '.contentHolder > div'); }); it('Should change selected tab onClick', () => { - wrapper.find('.tabs li').at(1).simulate('click', { target: { dataset: { tabname: 'tab-1' } } }); + wrapper.find('.tabs li').at(1).simulate('click', { target: { dataset: { name: 'tab-1' } } }); expect(wrapper.find('.tabs li').at(1)).toHaveClassName('active'); expect(wrapper.find('.contentHolder > div').at(1)).toHaveClassName('active'); }); diff --git a/src/store/actions/transactions.js b/src/store/actions/transactions.js index 414bd77340..e9033df11e 100644 --- a/src/store/actions/transactions.js +++ b/src/store/actions/transactions.js @@ -2,11 +2,12 @@ import to from 'await-to-js'; import { actionTypes, tokenMap, MODULE_ASSETS_NAME_ID_MAP, DEFAULT_LIMIT, + signatureCollectionStatus, } from '@constants'; import { isEmpty } from '@utils/helpers'; import { getTransactions, create, broadcast } from '@api/transaction'; -import { selectActiveTokenAccount, selectNetworkIdentifier } from '@store/selectors'; -import { signTransaction, transformTransaction } from '@utils/transaction'; +import { signMultisigTransaction, transformTransaction } from '@utils/transaction'; +import { extractKeyPair } from '@utils/account'; import { getTransactionSignatureStatus } from '@screens/signMultiSignTransaction/helpers'; import { timerReset } from './account'; import { loadingStarted, loadingFinished } from './loading'; @@ -137,16 +138,26 @@ export const transactionDoubleSigned = () => async (dispatch, getState) => { const { transactions, network, account, settings, } = getState(); - const networkIdentifier = selectNetworkIdentifier({ network }); - const activeAccount = selectActiveTokenAccount({ account, settings }); - const [signedTx, err] = signTransaction( - transformTransaction(transactions.signedTransaction), - account.secondPassphrase, - networkIdentifier, + const keyPair = extractKeyPair({ + passphrase: account.secondPassphrase, + enableCustomDerivationPath: false, + }); + const activeAccount = { + ...account.info[settings.token.active], + passphrase: account.secondPassphrase, + summary: { + ...account.info[settings.token.active].summary, + ...keyPair, + }, + }; + const transformedTx = transformTransaction(transactions.signedTransaction); + const [signedTx, err] = await signMultisigTransaction( + transformedTx, + activeAccount, { data: activeAccount, }, - false, + signatureCollectionStatus.partiallySigned, network, ); @@ -221,11 +232,10 @@ export const transactionBroadcasted = transaction => */ export const multisigTransactionSigned = ({ rawTransaction, sender, -}) => (dispatch, getState) => { +}) => async (dispatch, getState) => { const { network, account, } = getState(); - const networkIdentifier = selectNetworkIdentifier({ network }); const activeAccount = { ...account.info.LSK, passphrase: account.passphrase, @@ -234,10 +244,9 @@ export const multisigTransactionSigned = ({ // @todo move isTransactionFullySigned to a generic location const txStatus = getTransactionSignatureStatus(sender.data, rawTransaction); - const [tx, error] = signTransaction( + const [tx, error] = await signMultisigTransaction( rawTransaction, - activeAccount.passphrase, - networkIdentifier, + activeAccount, sender, txStatus, network, diff --git a/src/store/actions/transactions.test.js b/src/store/actions/transactions.test.js index 179b4acac4..7379901ccc 100644 --- a/src/store/actions/transactions.test.js +++ b/src/store/actions/transactions.test.js @@ -218,23 +218,14 @@ describe('actions: transactions', () => { }; const transactionError = new Error('Transaction create error'); loginTypes.passphrase.code = 1; - hwManagerApi.signTransactionByHW.mockRejectedValue(transactionError); + jest.spyOn(hwManagerApi, 'signTransactionByHW') + .mockRejectedValue(transactionError); const expectedAction = { type: actionTypes.transactionSignError, data: transactionError, }; - const { network } = getStateWithHW(); - // Act await transactionCreated(data)(dispatch, getStateWithHW); - - // Assert - expect(hwManagerApi.signTransactionByHW).toHaveBeenCalledWith( - activeAccount, - Buffer.from(network.networks.LSK.networkIdentifier, 'hex'), - expect.anything(), - expect.anything(), - ); expect(dispatch).toHaveBeenCalledWith(expectedAction); }); }); @@ -271,10 +262,11 @@ describe('actions: transactions', () => { expect(dispatch).toHaveBeenCalledWith(expectedAction); }); - it('should create an action to store signature error', async () => { + it.skip('should create an action to store signature error', async () => { // Prepare the store - const error = { message: 'error signing tx' }; - jest.spyOn(transactionUtils, 'signTransaction').mockImplementation(() => [{}, error]); + const error = new Error('error signing tx'); + jest.spyOn(transactionUtils, 'sign') + .mockImplementation(() => error); // Consume the utility await transactionDoubleSigned()(dispatch, getStateWithTx); @@ -384,10 +376,10 @@ describe('actions: transactions', () => { sender: { data: accounts.multiSig }, }; - it('should create an action to store double signed tx', () => { + it('should create an action to store double signed tx', async () => { // Consume the utility - jest.spyOn(transactionUtils, 'signTransaction').mockImplementation(() => [{ id: 1 }, undefined]); - multisigTransactionSigned(params)(dispatch, getStateWithTx); + jest.spyOn(transactionUtils, 'signMultisigTransaction').mockImplementation(() => [{ id: 1 }, undefined]); + await multisigTransactionSigned(params)(dispatch, getStateWithTx); // Prepare expectations const expectedAction = { @@ -399,13 +391,13 @@ describe('actions: transactions', () => { expect(dispatch).toHaveBeenCalledWith(expectedAction); }); - it('should create an action to store signature error', () => { + it('should create an action to store signature error', async () => { // Prepare the store const error = { message: 'error signing tx' }; - jest.spyOn(transactionUtils, 'signTransaction').mockImplementation(() => [undefined, error]); + jest.spyOn(transactionUtils, 'signMultisigTransaction').mockImplementation(() => [undefined, error]); // Consume the utility - multisigTransactionSigned(params)(dispatch, getStateWithTx); + await multisigTransactionSigned(params)(dispatch, getStateWithTx); // Prepare expectations const expectedAction = { diff --git a/src/utils/api/transaction/lsk.js b/src/utils/api/transaction/lsk.js index 05493edc1c..867315fdb0 100644 --- a/src/utils/api/transaction/lsk.js +++ b/src/utils/api/transaction/lsk.js @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ -import { transactions, cryptography } from '@liskhq/lisk-client'; -import { to } from 'await-to-js'; +import { transactions } from '@liskhq/lisk-client'; import { tokenMap, @@ -11,9 +10,8 @@ import { BASE_FEES, } from '@constants'; import { joinModuleAndAssetIds } from '@utils/moduleAssets'; -import { signTransactionByHW } from '@utils/hwManager'; import { - createTransactionObject, convertStringToBinary, transformTransaction, flattenTransaction, + createTransactionObject, sign, } from '@utils/transaction'; import { validateAddress } from '../../validators'; import http from '../http'; @@ -297,94 +295,6 @@ export const getTransactionFee = async ({ }; }; -/** - * Computes transaction id - * @param {object} transaction - * @returns {Promise} returns transaction id for a given transaction object - */ -export const computeTransactionId = ({ transaction, network }) => { - const moduleAssetId = joinModuleAndAssetIds({ - moduleID: transaction.moduleID, - assetID: transaction.assetID, - }); - const schema = network.networks.LSK.moduleAssetSchemas[moduleAssetId]; - const transactionBytes = transactions.getBytes(schema, transaction); - const id = cryptography.hash(transactionBytes); - - return id; -}; - -const signMultisigUsingPrivateKey = ( - schema, transaction, networkIdentifier, keys, privateKey, - isMultiSignatureRegistration, publicKey, moduleAssetId, rawTransaction, -) => { - let signedTransaction = transactions.signMultiSignatureTransactionWithPrivateKey( - schema, - transaction, - networkIdentifier, - Buffer.from(privateKey, 'hex'), - { - optionalKeys: keys.optionalKeys.map(convertStringToBinary), - mandatoryKeys: keys.mandatoryKeys.map(convertStringToBinary), - }, - isMultiSignatureRegistration, - ); - - const transactionKeys = { - mandatoryKeys: rawTransaction.mandatoryKeys ?? [], - optionalKeys: rawTransaction.optionalKeys ?? [], - }; - - const needsDoubleSign = [ - ...transactionKeys.mandatoryKeys, - ...transactionKeys.optionalKeys, - ].includes(publicKey); - - if (isMultiSignatureRegistration && needsDoubleSign) { - const transformedTransaction = transformTransaction(signedTransaction); - const flattenedTransaction = flattenTransaction(transformedTransaction); - const tx = createTransactionObject(flattenedTransaction, moduleAssetId); - const transactionKeysInBinary = { - mandatoryKeys: transactionKeys.mandatoryKeys.map(convertStringToBinary), - optionalKeys: transactionKeys.optionalKeys.map(convertStringToBinary), - }; - - signedTransaction = transactions.signMultiSignatureTransactionWithPrivateKey( - schema, - tx, - networkIdentifier, - Buffer.from(privateKey, 'hex'), - transactionKeysInBinary, - isMultiSignatureRegistration, - ); - } - - return signedTransaction; -}; - -const signUsingPrivateKey = (schema, transaction, networkIdentifier, privateKey) => - transactions.signTransactionWithPrivateKey( - schema, - transaction, - networkIdentifier, - Buffer.from(privateKey, 'hex'), - ); - -const signUsingHW = async (schema, transaction, account, networkIdentifier, network) => { - const signingBytes = transactions.getSigningBytes(schema, transaction); - const [error, signedTransaction] = await to(signTransactionByHW( - account, - networkIdentifier, - transaction, - signingBytes, - )); - if (error) { - throw error; - } - const id = computeTransactionId({ transaction: signedTransaction, network }); - return { ...signedTransaction, id }; -}; - /** * creates a new transaction * @@ -420,17 +330,13 @@ export const create = async ({ const isMultiSignatureRegistration = moduleAssetId === MODULE_ASSETS_NAME_ID_MAP.registerMultisignatureGroup; - if (isMultisignature || isMultiSignatureRegistration) { - return signMultisigUsingPrivateKey( - schema, transaction, networkIdentifier, keys, privateKey, - isMultiSignatureRegistration, publicKey, moduleAssetId, rawTransaction, - ); - } - if (account.hwInfo) { - const signedTx = await signUsingHW(schema, transaction, account, networkIdentifier, network); - return signedTx; - } - return signUsingPrivateKey(schema, transaction, networkIdentifier, privateKey); + const result = await sign( + account, schema, transaction, network, networkIdentifier, + isMultisignature, isMultiSignatureRegistration, keys, publicKey, + moduleAssetId, rawTransaction, privateKey, + ); + + return result; }; /** diff --git a/src/utils/hwManager.js b/src/utils/hwManager.js index 946d6e9373..ae2caf6194 100644 --- a/src/utils/hwManager.js +++ b/src/utils/hwManager.js @@ -1,5 +1,4 @@ // eslint-disable-next-line import/no-unresolved -// import Lisk from '@liskhq/lisk-client'; import i18next from 'i18next'; import { getAccount } from './api/account'; import { @@ -33,6 +32,59 @@ const getAccountsFromDevice = async ({ device: { deviceId }, network }) => { return accounts; }; +const isKeyMatch = (aPublicKey, signerPublicKey) => (Buffer.isBuffer(aPublicKey) + ? aPublicKey.equals(signerPublicKey) + : Buffer.from(aPublicKey, 'hex').equals(signerPublicKey)); + +/** + * updateTransactionSignatures - Function. + * This function updates transaction object to include the signatures at correct index. + * The below logic is copied from Lisk SDK https://github.com/LiskHQ/lisk-sdk/blob/2593d1fe70154a9209b713994a252c494cad7123/elements/lisk-transactions/src/sign.ts#L228-L297 + */ +/* eslint-disable max-statements */ +const updateTransactionSignatures = ( + account, + transactionObject, + signature, + keys, +) => { + const isMultiSignatureRegistration = transactionObject.moduleID === 4; + const signerPublicKey = Buffer.from(account.summary.publicKey, 'hex'); + if (Buffer.isBuffer(transactionObject.senderPublicKey) + && signerPublicKey.equals(transactionObject.senderPublicKey) + ) { + transactionObject.signatures[0] = signature; + } + + const { mandatoryKeys, optionalKeys } = keys; + if (mandatoryKeys.length + optionalKeys.length > 0) { + const mandatoryKeyIndex = mandatoryKeys.findIndex( + aPublicKey => isKeyMatch(aPublicKey, signerPublicKey), + ); + const optionalKeyIndex = optionalKeys.findIndex( + aPublicKey => isKeyMatch(aPublicKey, signerPublicKey), + ); + const signatureOffset = isMultiSignatureRegistration ? 1 : 0; + if (mandatoryKeyIndex !== -1) { + transactionObject.signatures[mandatoryKeyIndex + signatureOffset] = signature; + } + if (optionalKeyIndex !== -1) { + const index = mandatoryKeys.length + optionalKeyIndex + signatureOffset; + transactionObject.signatures[index] = signature; + } + const numberOfSignatures = signatureOffset + mandatoryKeys.length + optionalKeys.length; + for (let i = 0; i < numberOfSignatures; i += 1) { + if (Array.isArray(transactionObject.signatures) + && transactionObject.signatures[i] === undefined) { + transactionObject.signatures[i] = Buffer.alloc(0); + } + } + } + + return transactionObject; +}; +/* eslint-disable max-statements */ + /** * signTransactionByHW - Function. * This function is used for sign a send hardware wallet transaction. @@ -42,8 +94,9 @@ const signTransactionByHW = async ( networkIdentifier, transactionObject, transactionBytes, + keys, ) => { - const transaction = { + const data = { deviceId: account.hwInfo.deviceId, index: account.hwInfo.derivationIndex, networkIdentifier, @@ -51,14 +104,8 @@ const signTransactionByHW = async ( }; try { - const signature = await signTransaction(transaction); - if (Array.isArray(transactionObject.signatures)) { - transactionObject.signatures.push(signature); - } else { - Object.assign(transactionObject, { signatures: [signature] }); - } - - return transactionObject; + const signature = await signTransaction(data); + return updateTransactionSignatures(account, transactionObject, signature, keys); } catch (error) { throw new Error(error); } diff --git a/src/utils/hwManager.test.js b/src/utils/hwManager.test.js index 692c56d6d4..b700ba533b 100644 --- a/src/utils/hwManager.test.js +++ b/src/utils/hwManager.test.js @@ -43,11 +43,9 @@ describe('hwManager util', () => { describe('signTransactionByHW', () => { it('should return a transaction object with the proper signature', async () => { const account = { - info: { - LSK: { - address: 'lskbgyrx3v76jxowgkgthu9yaf3dr29wqxbtxz8yp', - publicKey: 'fd061b9146691f3c56504be051175d5b76d1b1d0179c5c4370e18534c5882122', - }, + summary: { + address: 'lskbgyrx3v76jxowgkgthu9yaf3dr29wqxbtxz8yp', + publicKey: 'fd061b9146691f3c56504be051175d5b76d1b1d0179c5c4370e18534c5882122', }, hwInfo: { deviceId: '060E803263E985C022CA2C9B', @@ -66,6 +64,12 @@ describe('hwManager util', () => { assetID: 0, nonce: '1', senderAddress: 'lskdxc4ta5j43jp9ro3f8zqbxta9fn6jwzjucw7yt', + signatures: [], + }; + + const keys = { + mandatoryKeys: [Buffer.from(account.summary.publicKey, 'hex'), Buffer.from(accounts.genesis.summary.publicKey, 'hex')], + optionalKeys: [], }; const networkIdentifier = Buffer.from('15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c', 'hex'); @@ -76,6 +80,7 @@ describe('hwManager util', () => { networkIdentifier, transactionObject, transactionBytes, + keys, ); expect(signedTransaction.signatures[0]).toEqual(signature); diff --git a/src/utils/transaction.js b/src/utils/transaction.js index 0c1c6d0563..08808ef963 100644 --- a/src/utils/transaction.js +++ b/src/utils/transaction.js @@ -1,5 +1,7 @@ /* eslint-disable max-lines */ -import { transactions } from '@liskhq/lisk-client'; +import { transactions, cryptography } from '@liskhq/lisk-client'; +import { to } from 'await-to-js'; +import { signTransactionByHW } from '@utils/hwManager'; import { DEFAULT_NUMBER_OF_SIGNATURES, MODULE_ASSETS_NAME_ID_MAP, @@ -13,6 +15,7 @@ import { } from '@utils/account'; import { transformStringDateToUnixTimestamp } from '@utils/datetime'; import { toRawLsk } from '@utils/lsk'; +import { isEmpty } from '@utils/helpers'; import { splitModuleAndAssetIds, joinModuleAndAssetIds } from '@utils/moduleAssets'; const { @@ -224,26 +227,26 @@ const flattenTransaction = ({ moduleAssetId, asset, ...rest }) => { }; switch (moduleAssetId) { - case MODULE_ASSETS_NAME_ID_MAP.transfer: { + case transfer: { transaction.recipientAddress = asset.recipient.address; transaction.amount = asset.amount; transaction.data = asset.data; break; } - case MODULE_ASSETS_NAME_ID_MAP.voteDelegate: + case voteDelegate: transaction.votes = asset.votes; break; - case MODULE_ASSETS_NAME_ID_MAP.registerDelegate: + case registerDelegate: transaction.username = asset.username; break; - case MODULE_ASSETS_NAME_ID_MAP.unlockToken: + case unlockToken: transaction.unlockObjects = asset.unlockObjects; break; - case MODULE_ASSETS_NAME_ID_MAP.registerMultisignatureGroup: { + case registerMultisignatureGroup: { transaction.numberOfSignatures = asset.numberOfSignatures; transaction.mandatoryKeys = asset.mandatoryKeys; transaction.optionalKeys = asset.optionalKeys; @@ -405,6 +408,144 @@ export const removeExcessSignatures = (signatures, mandatoryKeysNo, hasSenderSig return trimmedSignatures; }; +/** + * Computes transaction id + * @param {object} transaction + * @returns {Promise} returns transaction id for a given transaction object + */ +export const computeTransactionId = ({ transaction, network }) => { + const moduleAssetId = joinModuleAndAssetIds({ + moduleID: transaction.moduleID, + assetID: transaction.assetID, + }); + const schema = network.networks.LSK.moduleAssetSchemas[moduleAssetId]; + const transactionBytes = transactions.getBytes(schema, transaction); + const id = cryptography.hash(transactionBytes); + + return id; +}; + +const signMultisigUsingPrivateKey = ( + schema, transaction, networkIdentifier, keys, privateKey, + isMultiSignatureRegistration, publicKey, rawTransaction, +) => { + /** + * Use Lisk Element to Sign with Private Key + */ + const signedTransaction = transactions.signMultiSignatureTransactionWithPrivateKey( + schema, + transaction, + networkIdentifier, + Buffer.from(privateKey, 'hex'), + { + optionalKeys: keys.optionalKeys.map(convertStringToBinary), + mandatoryKeys: keys.mandatoryKeys.map(convertStringToBinary), + }, + isMultiSignatureRegistration, + ); + + /** + * Define keys. Since we are creating the tx + * The keys only exist for MultisigReg + */ + const transactionKeys = { + mandatoryKeys: rawTransaction.mandatoryKeys ?? [], + optionalKeys: rawTransaction.optionalKeys ?? [], + }; + + /** + * Check if the tx is multisigReg + */ + const members = [ + ...transactionKeys.mandatoryKeys.sort(), + ...transactionKeys.optionalKeys.sort(), + ]; + const senderIndex = members.indexOf(publicKey); + const isSender = rawTransaction.senderPublicKey === publicKey; + + if (isMultiSignatureRegistration && isSender && senderIndex > -1) { + const signatures = Array.from(Array(members.length + 1).keys()).map((index) => { + if (signedTransaction.signatures[index]) return signedTransaction.signatures[index]; + if (index === senderIndex + 1) return signedTransaction.signatures[0]; + return Buffer.from(''); + }); + signedTransaction.signatures = signatures; + } + + return signedTransaction; +}; + +const signUsingPrivateKey = (schema, transaction, networkIdentifier, privateKey) => + transactions.signTransactionWithPrivateKey( + schema, + transaction, + networkIdentifier, + Buffer.from(privateKey, 'hex'), + ); + +// eslint-disable-next-line max-statements +const signUsingHW = async ( + schema, transaction, account, networkIdentifier, network, keys, rawTransaction, + isMultiSignatureRegistration, +) => { + const signingBytes = transactions.getSigningBytes(schema, transaction); + const [error, signedTransaction] = await to(signTransactionByHW( + account, + networkIdentifier, + transaction, + signingBytes, + keys, + )); + if (error) { + throw error; + } + + const transactionKeys = { + mandatoryKeys: rawTransaction.mandatoryKeys ?? [], + optionalKeys: rawTransaction.optionalKeys ?? [], + }; + + const members = [ + ...transactionKeys.mandatoryKeys.sort(), + ...transactionKeys.optionalKeys.sort(), + ]; + const senderIndex = members.indexOf(account.summary.publicKey); + const isSender = rawTransaction.senderPublicKey === account.summary.publicKey; + + if (isMultiSignatureRegistration && isSender && senderIndex > -1) { + const signatures = Array.from(Array(members.length + 1).keys()).map((index) => { + if (signedTransaction.signatures[index]) return signedTransaction.signatures[index]; + if (index === senderIndex + 1) return signedTransaction.signatures[0]; + return Buffer.from(''); + }); + signedTransaction.signatures = signatures; + } + + const id = computeTransactionId({ transaction: signedTransaction, network }); + return { ...signedTransaction, id }; +}; + +export const sign = async ( + account, schema, transaction, network, networkIdentifier, + isMultisignature, isMultiSignatureRegistration, keys, publicKey, + moduleAssetId, rawTransaction, privateKey, +) => { + if (!isEmpty(account.hwInfo)) { + const signedTx = await signUsingHW( + schema, transaction, account, networkIdentifier, network, keys, rawTransaction, + isMultiSignatureRegistration, + ); + return signedTx; + } + if (isMultisignature || isMultiSignatureRegistration) { + return signMultisigUsingPrivateKey( + schema, transaction, networkIdentifier, keys, privateKey, + isMultiSignatureRegistration, publicKey, rawTransaction, + ); + } + return signUsingPrivateKey(schema, transaction, networkIdentifier, privateKey); +}; + /** * Signs a given multisignature tx with a given passphrase * @@ -418,57 +559,54 @@ export const removeExcessSignatures = (signatures, mandatoryKeysNo, hasSenderSig * @returns [Object, Object] - Signed transaction and err */ // eslint-disable-next-line max-statements -const signTransaction = ( +const signMultisigTransaction = async ( transaction, - passphrase, - networkIdentifier, + account, senderAccount, txStatus, network, ) => { - let signedTransaction; - let err; - - const isGroupRegistration = transaction.moduleAssetId - === MODULE_ASSETS_NAME_ID_MAP.registerMultisignatureGroup; + /** + * Define keys. + * Since the sender is different, the keys are defined based on that + */ + const isGroupRegistration = transaction.moduleAssetId === registerMultisignatureGroup; + const schema = network.networks.LSK.moduleAssetSchemas[transaction.moduleAssetId]; + const networkIdentifier = Buffer.from(network.networks.LSK.networkIdentifier, 'hex'); const { mandatoryKeys, optionalKeys } = getKeys({ senderAccount: senderAccount.data, transaction, isGroupRegistration, }); - - const flatTransaction = flattenTransaction(transaction); - const transactionObject = createTransactionObject(flatTransaction, transaction.moduleAssetId); const keys = { mandatoryKeys: mandatoryKeys.map(key => Buffer.from(key, 'hex')), optionalKeys: optionalKeys.map(key => Buffer.from(key, 'hex')), }; - const includeSender = transaction.moduleAssetId - === MODULE_ASSETS_NAME_ID_MAP.registerMultisignatureGroup; + /** + * To do so, we have to flatten, then create txObject + */ + const flatTransaction = flattenTransaction(transaction); + const transactionObject = createTransactionObject(flatTransaction, transaction.moduleAssetId); + + /** + * remove excess optional signatures + */ + if (txStatus === signatureCollectionStatus.occupiedByOptionals) { + transactionObject.signatures = removeExcessSignatures( + transactionObject.signatures, keys.mandatoryKeys.length, isGroupRegistration, + ); + } try { - // remove excess optionals - if (txStatus === signatureCollectionStatus.occupiedByOptionals) { - transactionObject.signatures = removeExcessSignatures( - transactionObject.signatures, keys.mandatoryKeys.length, includeSender, - ); - } - - signedTransaction = transactions.signMultiSignatureTransaction( - network.networks.LSK.moduleAssetSchemas[transaction.moduleAssetId], - transactionObject, - Buffer.from(networkIdentifier, 'hex'), - passphrase, - keys, - includeSender, + const result = await sign( + account, schema, transactionObject, network, networkIdentifier, + !!senderAccount.data, isGroupRegistration, keys, account.summary.publicKey, + transaction.moduleAssetId, flatTransaction, account.summary.privateKey, ); + return [result]; } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - err = e; + return [null, e]; } - - return [signedTransaction, err]; }; /** @@ -482,7 +620,7 @@ const signTransaction = ( * @returns {number} the number of signatures required */ const getNumberOfSignatures = (account, transaction) => { - if (transaction?.moduleAssetId === MODULE_ASSETS_NAME_ID_MAP.registerMultisignatureGroup) { + if (transaction?.moduleAssetId === registerMultisignatureGroup) { return transaction.optionalKeys.length + transaction.mandatoryKeys.length + 1; } if (account?.summary?.isMultisignature) { @@ -500,6 +638,6 @@ export { containsTransactionType, createTransactionObject, normalizeTransactionParams, - signTransaction, + signMultisigTransaction, getNumberOfSignatures, }; diff --git a/test/cypress/features/common/common.js b/test/cypress/features/common/common.js index c487c8dfa2..c009870d9e 100644 --- a/test/cypress/features/common/common.js +++ b/test/cypress/features/common/common.js @@ -6,7 +6,6 @@ const txConfirmationTimeout = 15000; Given(/^Network switcher is (enabled|disabled)$/, function (status) { const showNetwork = status === 'enabled'; - console.log('showNetwork', showNetwork); window.localStorage.setItem('settings', JSON.stringify({ ...settings, 'showNetwork': showNetwork })); }); diff --git a/test/cypress/features/search.feature b/test/cypress/features/search.feature index eb7d2088a6..bd8ad77fd3 100644 --- a/test/cypress/features/search.feature +++ b/test/cypress/features/search.feature @@ -20,8 +20,9 @@ Feature: Search Scenario: Search for non-existent account Given Network is set to testnet Given I login as genesis on testnet - When I click on searchIcon + When I wait 3 seconds + And I click on searchIcon And I search for delegate 43th3j4bt324 - And I wait 3 seconds + And I wait 2 seconds Then I should see no results