Skip to content

Commit 3e41979

Browse files
authored
Merge pull request #5570 from LiskHQ/5569-claim-message-signing
Implement WalletConnect claim message signing and feedback
2 parents cb48cb8 + c4c61d9 commit 3e41979

File tree

14 files changed

+199
-35
lines changed

14 files changed

+199
-35
lines changed

libs/wcm/constants/permissions.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const SIGNING_METHODS = {
22
SIGN_TRANSACTION: { key: 'sign_transaction', title: 'Signature request' },
33
SIGN_MESSAGE: { key: 'sign_message', title: 'Sign message' },
4+
SIGN_RAW_MESSAGE: { key: 'sign_raw_message', title: 'Sign raw message' },
45
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
"socket.io-client": "4.7.0",
179179
"stream-browserify": "3.0.0",
180180
"swiper": "8.4.2",
181+
"tweetnacl": "1.0.3",
181182
"usb": "2.9.0",
182183
"yup": "0.32.11"
183184
},

setup/react/app/MainRouter.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Route, Switch } from 'react-router-dom';
44
import { addSearchParamsToUrl, removeSearchParamsFromUrl } from 'src/utils/searchParams';
55
import { useEvents } from '@libs/wcm/hooks/useEvents';
66
import { EVENTS } from '@libs/wcm/constants/lifeCycle';
7+
import { SIGNING_METHODS } from '@libs/wcm/constants/permissions';
78
import routesMap from 'src/routes/routesMap';
89
import NotFound from '@common/components/NotFound';
910
import CustomRoute from '@common/components/customRoute';
@@ -28,7 +29,7 @@ const MainRouter = ({ history }) => {
2829
if (event.name === EVENTS.SESSION_REQUEST) {
2930
const method = event.meta?.params?.request?.method;
3031

31-
if (method === 'sign_message') {
32+
if (method === SIGNING_METHODS.SIGN_MESSAGE || method === SIGNING_METHODS.SIGN_RAW_MESSAGE) {
3233
showRequestModal('requestSignMessageDialog', event);
3334
} else {
3435
showRequestModal('requestView', event);

src/locales/en/common.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@
330330
"Invalid amount": "Invalid amount",
331331
"Invalid dates": "Invalid dates",
332332
"Invalid network name": "Invalid network name",
333-
"Invalid transaction initiated from another application/network.": "Invalid transaction initiated from another application/network.",
333+
"Invalid signature request initiated from another application/network.": "Invalid signature request initiated from another application/network.",
334334
"Invalid websocket URL": "Invalid websocket URL",
335335
"Is the problem persisting?": "Is the problem persisting?",
336336
"Keep it safe as it is the only way to access your wallet.": "Keep it safe as it is the only way to access your wallet.",

src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/RequestSignMessageConfirmation/index.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@ import classNames from 'classnames';
33
import { useTranslation } from 'react-i18next';
44
import { useCurrentAccount } from '@account/hooks';
55
import { useDispatch } from 'react-redux';
6-
import { signMessage } from '@message/store/action';
6+
import { signMessage, signClaimMessage } from '@message/store/action';
77
import CopyToClipboard from '@common/components/copyToClipboard';
88
import { PrimaryButton } from '@theme/buttons';
99
import styles from './RequestSignMessageConfirmation.css';
1010

11-
export function RequestSignMessageConfirmation({ nextStep, address, message }) {
11+
export function RequestSignMessageConfirmation({ nextStep, address, message, portalMessage }) {
1212
const { t } = useTranslation();
1313
const [currentAccount] = useCurrentAccount();
1414
const dispatch = useDispatch();
1515

1616
/* istanbul ignore next */
1717
const onClick = () => {
18-
nextStep({
19-
message,
20-
actionFunction: (formProps, _, privateKey) =>
21-
dispatch(signMessage({ message, nextStep, privateKey, currentAccount })),
22-
});
18+
if (message) {
19+
nextStep({
20+
message,
21+
actionFunction: (formProps, _, privateKey) =>
22+
dispatch(signMessage({ message, nextStep, privateKey, currentAccount })),
23+
});
24+
} else {
25+
nextStep({
26+
message: portalMessage,
27+
actionFunction: (formProps, _, privateKey) =>
28+
dispatch(signClaimMessage({ portalMessage, nextStep, privateKey, currentAccount })),
29+
});
30+
}
2331
};
2432

2533
return (
@@ -39,7 +47,7 @@ export function RequestSignMessageConfirmation({ nextStep, address, message }) {
3947
}}
4048
/>
4149
<p className={styles.label}>{t('Message')}</p>
42-
<div className={styles.messageBox}>{message}</div>
50+
<div className={styles.messageBox}>{message ?? portalMessage}</div>
4351
<PrimaryButton className={classNames(styles.btnContinue, 'continue-btn')} onClick={onClick}>
4452
{t('Continue')}
4553
</PrimaryButton>

src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/RequestSignMessageConfirmation/index.test.js

+26-6
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,43 @@ const props = {
99
address: 'address',
1010
message: 'message',
1111
};
12-
13-
beforeEach(() => {
14-
render(<RequestSignMessageConfirmation {...props} />);
15-
});
12+
const portalProps = {
13+
...props,
14+
message: undefined,
15+
portalMessage:
16+
'0xe4dbb94d0f19e47b0cff8206bebc1fcf8d892325ab851e1a5bdab954711d926e000000000000000000',
17+
};
1618

1719
describe('RequestSignMessageConfirmation', () => {
18-
it('should render properly', async () => {
20+
it('should render properly when message is passed', async () => {
21+
render(<RequestSignMessageConfirmation {...props} />);
1922
expect(screen.getByText('This request was initiated from another application.')).toBeTruthy();
2023
expect(screen.getByText(props.address)).toBeTruthy();
2124
expect(screen.getByText(props.message)).toBeTruthy();
2225
});
2326

24-
it('should call nextStep when button is clicked', async () => {
27+
it('should render properly when portal message is passed', async () => {
28+
render(<RequestSignMessageConfirmation {...portalProps} />);
29+
expect(screen.getByText('This request was initiated from another application.')).toBeTruthy();
30+
expect(screen.getByText(portalProps.address)).toBeTruthy();
31+
expect(screen.getByText(portalProps.portalMessage)).toBeTruthy();
32+
});
33+
34+
it('should call nextStep with message when button is clicked', async () => {
35+
render(<RequestSignMessageConfirmation {...props} />);
2536
fireEvent.click(screen.getByRole('button'));
2637
expect(props.nextStep).toHaveBeenCalledWith({
2738
message: props.message,
2839
actionFunction: expect.any(Function),
2940
});
3041
});
42+
43+
it('should call nextStep when button is clicked', async () => {
44+
render(<RequestSignMessageConfirmation {...portalProps} />);
45+
fireEvent.click(screen.getByRole('button'));
46+
expect(portalProps.nextStep).toHaveBeenCalledWith({
47+
message: portalProps.portalMessage,
48+
actionFunction: expect.any(Function),
49+
});
50+
});
3151
});

src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/index.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SignedMessage from '@message/components/signedMessage';
1616
import { RequestSignMessageConfirmation } from '@blockchainApplication/connection/components/RequestSignMessageDialog/RequestSignMessageConfirmation';
1717
import { USER_REJECT_ERROR } from '@libs/wcm/utils/jsonRPCFormat';
1818
import styles from './RequestSignMessageDialog.css';
19-
import RequestSummary from '../RequestSummary';
19+
import RequestSummary, { getTitle } from '../RequestSummary';
2020

2121
// eslint-disable-next-line max-statements
2222
const RequestSignMessageDialog = () => {
@@ -31,7 +31,8 @@ const RequestSignMessageDialog = () => {
3131

3232
const { peer, requiredNamespaces } = sessionRequest || {};
3333
const event = events?.find((e) => e.name === EVENTS.SESSION_REQUEST);
34-
const { message, address } = event?.meta?.params?.request?.params || {};
34+
const { method, params: { message, address, portalMessage } = {} } =
35+
event?.meta?.params?.request || {};
3536
const { icons, name, url } = peer?.metadata || {};
3637

3738
/* istanbul ignore next */
@@ -73,7 +74,7 @@ const RequestSignMessageDialog = () => {
7374
{!isPasswordStep && !isErrorView && (
7475
<BlockchainAppDetailsHeader
7576
className={styles.blockchainAppDetailsHeaderProp}
76-
headerText={multiStepPosition === 2 ? t('Signed message') : t('Sign message')}
77+
headerText={method ? getTitle(method, t) : 'Signed message'}
7778
application={{
7879
data: {
7980
name,
@@ -93,7 +94,7 @@ const RequestSignMessageDialog = () => {
9394
})}
9495
onChange={onMultiStepChange}
9596
>
96-
<RequestSummary history={history} message={message} />
97+
<RequestSummary history={history} message={message} portalMessage={portalMessage} />
9798
<RequestSignMessageConfirmation message={message} address={address} />
9899
<TxSignatureCollector
99100
type="message"

src/modules/blockchainApplication/connection/components/RequestSummary/index.js

+23-8
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import { toTransactionJSON } from '@transaction/utils/encoding';
1818
import { useBlockchainApplicationMeta } from '@blockchainApplication/manage/hooks/queries/useBlockchainApplicationMeta';
1919
import { convertFromBaseDenom } from '@token/fungible/utils/helpers';
2020
import { joinModuleAndCommand } from '@transaction/utils/moduleCommand';
21-
import { signMessage } from '@message/store/action';
21+
import { signMessage, signClaimMessage } from '@message/store/action';
2222
import { addSearchParamsToUrl, removeSearchParamsFromUrl } from 'src/utils/searchParams';
23+
import { sizeOfString } from 'src/utils/helpers';
2324
import { validator } from '@liskhq/lisk-client';
2425
import { useSession } from '@libs/wcm/hooks/useSession';
2526
import { useEvents } from '@libs/wcm/hooks/useEvents';
@@ -36,12 +37,12 @@ import { ReactComponent as SwitchIcon } from '../../../../../../setup/react/asse
3637
import EmptyState from './EmptyState';
3738
import styles from './requestSummary.css';
3839

39-
const getTitle = (key, t) =>
40+
export const getTitle = (key, t) =>
4041
Object.values(SIGNING_METHODS).find((item) => item.key === key)?.title ?? t('Method not found.');
4142
const defaultToken = { symbol: 'LSK' };
4243

4344
// eslint-disable-next-line max-statements
44-
const RequestSummary = ({ nextStep, history, message }) => {
45+
const RequestSummary = ({ nextStep, history, message, portalMessage }) => {
4546
const { t } = useTranslation();
4647
const { getAccountByAddress, accounts } = useAccounts();
4748
const [currentAccount, setCurrentAccount] = useCurrentAccount();
@@ -86,6 +87,12 @@ const RequestSummary = ({ nextStep, history, message }) => {
8687
actionFunction: (formProps, _, privateKey) =>
8788
reduxDispatch(signMessage({ message, nextStep, privateKey, currentAccount })),
8889
});
90+
} else if (portalMessage) {
91+
nextStep({
92+
portalMessage,
93+
actionFunction: (formProps, _, privateKey) =>
94+
reduxDispatch(signClaimMessage({ portalMessage, nextStep, privateKey, currentAccount })),
95+
});
8996
} else {
9097
const moduleCommand = joinModuleAndCommand(transaction);
9198
const transactionJSON = toTransactionJSON(transaction, request?.request?.params.schema);
@@ -110,6 +117,7 @@ const RequestSummary = ({ nextStep, history, message }) => {
110117
});
111118
}
112119
};
120+
113121
const rejectHandler = async () => {
114122
await respond({ payload: USER_REJECT_ERROR });
115123
removeSearchParamsFromUrl(history, ['modal', 'status', 'name', 'action']);
@@ -144,14 +152,19 @@ const RequestSummary = ({ nextStep, history, message }) => {
144152
const { payload, schema, address: publicKey } = request.request.params;
145153
let transactionObj;
146154

147-
if (!message) {
155+
// Validate portal message
156+
if (portalMessage && sizeOfString(portalMessage) === 84) {
157+
setErrorMessage('');
158+
} else if (portalMessage && sizeOfString(portalMessage) !== 84) {
159+
setErrorMessage('Claim message of invalid size received.');
160+
} else if (!message) {
148161
validator.validator.validateSchema(schema);
149162
transactionObj = decodeTransaction(Buffer.from(payload, 'hex'), schema);
150163
validator.validator.validate(schema, transactionObj.params);
151164
setTransaction(transactionObj);
152165
}
153-
154-
const senderPublicKey = !message ? transactionObj.senderPublicKey : publicKey;
166+
const senderPublicKey =
167+
!message && !portalMessage ? transactionObj.senderPublicKey : publicKey;
155168
const address = extractAddressFromPublicKey(senderPublicKey);
156169
const account = getAccountByAddress(address);
157170
setSenderAccount({
@@ -203,7 +216,7 @@ const RequestSummary = ({ nextStep, history, message }) => {
203216

204217
return (
205218
<div className={`${styles.wrapper}`}>
206-
{!message && (
219+
{!message && !portalMessage && (
207220
<BlockchainAppDetailsHeader
208221
headerText={getTitle(request?.request?.method, t)}
209222
application={application}
@@ -221,7 +234,9 @@ const RequestSummary = ({ nextStep, history, message }) => {
221234
</span>
222235
) : (
223236
<div className={styles.invalidTransactionTextContainer}>
224-
<span>{t('Invalid transaction initiated from another application/network.')}</span>
237+
<span>
238+
{t('Invalid signature request initiated from another application/network.')}
239+
</span>
225240
<span className={styles.errorMessage}>{errorMessage}</span>
226241
</div>
227242
)}

src/modules/blockchainApplication/connection/components/RequestSummary/index.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ describe('RequestSummary', () => {
220220
});
221221

222222
expect(
223-
screen.getByText('Invalid transaction initiated from another application/network.')
223+
screen.getByText('Invalid signature request initiated from another application/network.')
224224
).toBeTruthy();
225225
expect(
226226
screen.getByText(

src/modules/message/components/signedMessage/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const SignedMessage = ({ signature, error, onPrev, reset }) => {
8282
return (
8383
<Success
8484
t={t}
85-
signature={signature}
85+
signature={typeof signature === 'string' ? signature : JSON.stringify(signature, null, '\t')}
8686
copied={copied}
8787
copy={copy}
8888
history={history}

src/modules/message/store/action.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { to } from 'await-to-js';
22
import { cryptography } from '@liskhq/lisk-client';
33
import { signMessageUsingHW } from '@wallet/utils/signMessage';
4-
import { signMessageWithPrivateKey } from '../utils/signMessageWithPrivateKey';
4+
import {
5+
signMessageWithPrivateKey,
6+
signClaimMessageWithPrivateKey,
7+
} from '../utils/signMessageWithPrivateKey';
58

69
export const getUnsignedNonProtocolMessage = (message) =>
710
Buffer.concat([
@@ -39,3 +42,21 @@ export const signMessage =
3942

4043
return nextStep({ signature, message });
4144
};
45+
46+
export const signClaimMessage =
47+
({ nextStep, portalMessage, privateKey, currentAccount }) =>
48+
async () => {
49+
const signature = signClaimMessageWithPrivateKey({
50+
message: portalMessage,
51+
privateKey,
52+
});
53+
const portalSignature = {
54+
data: {
55+
pubKey: currentAccount.metadata.pubkey,
56+
r: `0x${signature.substring(0, 64)}`,
57+
s: `0x${signature.substring(64)}`,
58+
},
59+
};
60+
61+
return nextStep({ signature: portalSignature, portalMessage });
62+
};

src/modules/message/store/action.test.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { cryptography } from '@liskhq/lisk-client';
22
import { mockHWAccounts } from '@hardwareWallet/__fixtures__';
33
import * as signMessageUtil from '@wallet/utils/signMessage';
4-
import { signMessage } from './action';
4+
import * as signMessageWithPrivateKeyUtils from '../utils/signMessageWithPrivateKey';
5+
import { signMessage, signClaimMessage } from './action';
56

67
jest.spyOn(cryptography.ed, 'signAndPrintMessage');
78
jest.spyOn(cryptography.ed, 'printSignedMessage');
89
jest.spyOn(signMessageUtil, 'signMessageUsingHW');
10+
jest.spyOn(signMessageWithPrivateKeyUtils, 'signClaimMessageWithPrivateKey');
11+
12+
const privateKey =
13+
'314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588';
14+
const nextStep = jest.fn();
915

1016
describe('balanceReclaimed', () => {
1117
const mockCurrentAccount = mockHWAccounts[0];
1218
const message = 'test-message';
13-
const nextStep = jest.fn();
14-
const privateKey =
15-
'314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588';
1619
const signature =
1720
'68937004b6720d7e1902ef05a577e6d9f9ab2756286b1f2ae918f8a0e5153c15e4f410916076f750b708f8979be2430e4cfc7ebb523ae1905d2ea1f5d24ce700';
1821
const defaultPrintedMessage = `
@@ -68,3 +71,35 @@ describe('balanceReclaimed', () => {
6871
});
6972
});
7073
});
74+
75+
describe('signClaimMessage', () => {
76+
const mockCurrentAccount = {
77+
metadata: {
78+
pubkey: '5bb1138c01b7762318f5e8a8799573077caadb1c7333a5c631773a2ade4bbdb5',
79+
},
80+
};
81+
const portalMessage =
82+
'0xe4dbb94d0f19e47b0cff8206bebc1fcf8d892325ab851e1a5bdab954711d926e000000000000000000';
83+
afterEach(() => jest.clearAllMocks());
84+
85+
it('should call next step with signature', async () => {
86+
const claimResult =
87+
'15e546e6df7a17960c00c80cb42a3968ca004f2d8efd044cb2bb14e83ba173b02fc4c40ad47b0eca722f3022d5d82874fad25a7c0264d8a31e20f17741a4e602';
88+
signMessageWithPrivateKeyUtils.signClaimMessageWithPrivateKey.mockReturnValue(claimResult);
89+
90+
const signedClaim = {
91+
data: {
92+
pubKey: '5bb1138c01b7762318f5e8a8799573077caadb1c7333a5c631773a2ade4bbdb5',
93+
r: '0x15e546e6df7a17960c00c80cb42a3968ca004f2d8efd044cb2bb14e83ba173b0',
94+
s: '0x2fc4c40ad47b0eca722f3022d5d82874fad25a7c0264d8a31e20f17741a4e602',
95+
},
96+
};
97+
await signClaimMessage({
98+
nextStep,
99+
portalMessage,
100+
privateKey,
101+
currentAccount: mockCurrentAccount,
102+
})();
103+
expect(nextStep).toHaveBeenCalledWith({ signature: signedClaim, portalMessage });
104+
});
105+
});
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { cryptography } from '@liskhq/lisk-client';
2+
import { sign } from 'tweetnacl';
23

34
export const signMessageWithPrivateKey = ({ message, privateKey }) => {
45
const result = cryptography.ed.signAndPrintMessage(message, Buffer.from(privateKey, 'hex'));
56

67
return result;
78
};
9+
10+
export const signClaimMessageWithPrivateKey = ({ message, privateKey }) => {
11+
const result = Buffer.from(
12+
sign.detached(Buffer.from(message.substring(2), 'hex'), Buffer.from(privateKey, 'hex'))
13+
).toString('hex');
14+
15+
return result;
16+
};

0 commit comments

Comments
 (0)