Skip to content

Commit 0c809cf

Browse files
rhyslbwmkazlauskasmirceahaseganljagiela
authored andcommitted
LW-13371 Feature/sign message stake key support (#1970)
* refactor: correct sign message copy * feat: support stake key message signing * feat: display raw key in message signing result we were displaying only 'cose key' structure that wraps the raw key which does not for Glacier Drop * feat(core): limit addresses drawer and add scroll * test: fix message signing e2e * chore(core): fix scrollbars styles --------- Co-authored-by: Martynas Kazlauskas <martynas.kazlauskas@iohk.io> Co-authored-by: Mircea Hasegan <mircea.hasegan@iohk.io> Co-authored-by: Lukasz Jagiela <Lukasz.Jagiela@iohk.io>
1 parent 4acd826 commit 0c809cf

File tree

8 files changed

+110
-19
lines changed

8 files changed

+110
-19
lines changed

apps/browser-extension-wallet/src/views/browser-view/features/sign-message/SignMessageDrawer.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ export const SignMessageDrawer: React.FC = () => {
216216
className={styles.customTextArea}
217217
/>
218218
</div>
219+
<div className={styles.inputGroup}>
220+
<Flex justifyContent="space-between" alignItems="center">
221+
<Text.Body.Normal weight="$medium" data-testid={'result-message-raw-key-label'}>
222+
{t('core.signMessage.rawKey')}
223+
</Text.Body.Normal>
224+
{renderCopyToClipboard({
225+
text: signatureObject.rawKey,
226+
handleCopy,
227+
t,
228+
testId: 'raw-public-key-copy-to-clipboard-button'
229+
})}
230+
</Flex>
231+
<TextArea
232+
value={signatureObject.rawKey}
233+
dataTestId="sign-message-raw-key"
234+
rows={4}
235+
className={styles.customTextArea}
236+
/>
237+
</div>
219238
</div>
220239
);
221240

apps/browser-extension-wallet/src/views/browser-view/features/sign-message/useSignMessageState.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { useAnalyticsContext } from '@providers';
1212
import { useSecrets } from '@lace/core';
1313
import { parseError } from '@src/utils/parse-error';
1414

15+
type SignatureObject = Cip30DataSignature & { rawKey: Wallet.Crypto.Ed25519PublicKeyHex };
16+
1517
interface SignMessageState {
16-
usedAddresses: { address: string; id: number }[];
18+
usedAddresses: { address: string; id: number; type: 'payment' | 'stake' }[];
1719
isSigningInProgress: boolean;
18-
signatureObject: Cip30DataSignature | undefined;
20+
signatureObject: SignatureObject | undefined;
1921
error: string;
2022
errorObj?: Error;
2123
hardwareWalletError: string;
@@ -31,19 +33,20 @@ export const useSignMessageState = (): SignMessageState => {
3133

3234
const { inMemoryWallet, isHardwareWallet } = useWalletStore();
3335
const [isSigningInProgress, setIsSigningInProgress] = useState(false);
34-
const [signature, setSignature] = useState<Cip30DataSignature>();
36+
const [signatureObject, setSignatureObject] = useState<SignatureObject | undefined>();
3537
const [error, setError] = useState<string>('');
3638
const [errorObj, setErrorObj] = useState<Error | undefined>();
3739
const [hardwareWalletError, setHardwareWalletError] = useState<string>('');
3840

3941
const addresses = useObservable(inMemoryWallet?.addresses$);
42+
const rewardAccounts = useObservable(inMemoryWallet?.delegation.rewardAccounts$);
4043

4144
const resetSigningState = useCallback(() => {
4245
setIsSigningInProgress(false);
4346
setError('');
4447
setHardwareWalletError('');
4548
setErrorObj(undefined);
46-
setSignature(undefined);
49+
setSignatureObject(undefined);
4750
}, []);
4851

4952
const performSigning = useCallback(
@@ -57,17 +60,26 @@ export const useSignMessageState = (): SignMessageState => {
5760
isHardwareWallet
5861
? analytics.sendEventToPostHog(PostHogAction.SignMessageAskingHardwareWalletInteraction)
5962
: analytics.sendEventToPostHog(PostHogAction.SignMessageAskingForPassword);
63+
64+
// Determine if the address is a payment address or reward account
65+
const isRewardAccount = Wallet.Cardano.isRewardAccount(address);
66+
6067
const signatureGenerated = await withSignDataConfirmation(
6168
async () =>
6269
await inMemoryWallet.signData({
63-
signWith: address as Wallet.Cardano.PaymentAddress,
70+
signWith: isRewardAccount
71+
? (address as Wallet.Cardano.RewardAccount)
72+
: (address as Wallet.Cardano.PaymentAddress),
6473
payload
6574
}),
6675
!isHardwareWallet ? password : {},
6776
clearSecrets
6877
);
6978

70-
setSignature(signatureGenerated);
79+
setSignatureObject({
80+
...signatureGenerated,
81+
rawKey: Wallet.util.coseKeyToRaw(signatureGenerated.key)
82+
});
7183
} catch (signingError: unknown) {
7284
logger.error('Error signing message:', signingError);
7385
setErrorObj(parseError(signingError));
@@ -92,16 +104,25 @@ export const useSignMessageState = (): SignMessageState => {
92104
[isHardwareWallet, analytics, clearSecrets, inMemoryWallet, t]
93105
);
94106

95-
const usedAddresses =
96-
addresses?.map((address, index) => ({
107+
const usedAddresses = [
108+
// Payment addresses
109+
...(addresses?.map((address, index) => ({
97110
address: address.address.toString(),
98-
id: index
99-
})) || [];
111+
id: index,
112+
type: 'payment' as const
113+
})) || []),
114+
// Reward accounts (stake addresses)
115+
...(rewardAccounts?.map((rewardAccount, index) => ({
116+
address: rewardAccount.address,
117+
id: addresses?.length ? addresses.length + index : index,
118+
type: 'stake' as const
119+
})) || [])
120+
];
100121

101122
return {
102123
usedAddresses,
103124
isSigningInProgress,
104-
signatureObject: signature,
125+
signatureObject,
105126
error,
106127
errorObj,
107128
hardwareWalletError,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { COSEKey, Label, Int } from '@emurgo/cardano-message-signing-nodejs';
2+
import type { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
3+
import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto';
4+
5+
export const coseKeyToRaw = (coseKey: Cip30DataSignature['key']): Ed25519PublicKeyHex => {
6+
const parsedKey = COSEKey.from_bytes(Buffer.from(coseKey, 'hex'));
7+
// eslint-disable-next-line no-magic-numbers
8+
const rawKeyLabelX = Label.new_int(Int.new_i32(-2));
9+
const keyCbor = parsedKey.header(rawKeyLabelX);
10+
const keyBytes = keyCbor?.as_bytes();
11+
keyCbor?.free();
12+
rawKeyLabelX.free();
13+
parsedKey.free();
14+
if (!keyBytes) {
15+
throw new Error('expected COSE key with label "x" (-2)');
16+
}
17+
// eslint-disable-next-line new-cap
18+
return Ed25519PublicKeyHex(Buffer.from(keyBytes).toString('hex'));
19+
};

packages/cardano/src/wallet/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './drep';
77
export * from './voter';
88
export * from './derive-ed25519-key-hash-from-bip32-public-key';
99
export * from './is-nft';
10+
export * from './cose-key-to-raw';

packages/core/src/ui/components/WalletOwnAddressesDropdown/WalletOwnAddressDropdown.module.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import '../../styles/theme.scss';
2+
@import '../../../../../common/src/ui/styles/abstracts/mixins';
23

34
.overlay {
45
z-index: 10 !important;
@@ -36,6 +37,12 @@
3637
color: var(--text-color-primary, #000000) !important;
3738
}
3839

40+
.addressDropdownMenu {
41+
@include scroll-bar-style;
42+
max-height: 40vh;
43+
overflow: auto;
44+
}
45+
3946
:global(.ant-dropdown-menu) {
4047
background-color: var(--bg-color-body, #ffffff) !important;
4148
border: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey, #333333)) !important;

packages/core/src/ui/components/WalletOwnAddressesDropdown/WalletOwnAddressDropdown.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
99
export interface AddressSchema {
1010
id: number;
1111
address: string;
12+
type?: 'payment' | 'stake';
1213
}
1314

1415
export type WalletOwnAddressDropdownProps = {
@@ -40,10 +41,25 @@ export const WalletOwnAddressDropdown = ({
4041
}
4142
};
4243

43-
const items: MenuProps['items'] = addresses.map((address) => ({
44-
key: address.id.toString(),
45-
label: addEllipsis(address.address, FIRST_PART_ADDRESS_LENGTH, LAST_PART_ADDRESS_LENGTH)
46-
}));
44+
const getAddressTypeDisplay = (addressType?: 'payment' | 'stake'): string => {
45+
if (addressType === 'stake') {
46+
return ` (${t('core.signMessage.addressTypeStake')})`;
47+
}
48+
if (addressType === 'payment') {
49+
return ` (${t('core.signMessage.addressTypePayment')})`;
50+
}
51+
return '';
52+
};
53+
54+
const items: MenuProps['items'] = addresses.map((address) => {
55+
const shortenedAddress = addEllipsis(address.address, FIRST_PART_ADDRESS_LENGTH, LAST_PART_ADDRESS_LENGTH);
56+
const addressType = getAddressTypeDisplay(address.type);
57+
58+
return {
59+
key: address.id.toString(),
60+
label: `${shortenedAddress}${addressType}`
61+
};
62+
});
4763

4864
const menuProps: MenuProps = {
4965
items,
@@ -53,7 +69,11 @@ export const WalletOwnAddressDropdown = ({
5369
return (
5470
<Dropdown
5571
menu={menuProps}
56-
dropdownRender={(menus) => <div data-testid="address-dropdown-menu">{menus}</div>}
72+
dropdownRender={(menus) => (
73+
<div data-testid="address-dropdown-menu" className={styles.addressDropdownMenu}>
74+
{menus}
75+
</div>
76+
)}
5777
trigger={['click']}
5878
data-testid="address-menu"
5979
>

packages/e2e-tests/src/assert/settings/MessageSigningInputDrawerAssert.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ class MessageSigningInputDrawerAssert {
5454
async assertSeeSelectedAddress(expectedAddress: string) {
5555
await MessageSigningInputDrawer.selectAddressButton.waitForDisplayed();
5656
const actualAddress = await MessageSigningInputDrawer.selectAddressButton.getText();
57-
expect(actualAddress).to.equal(expectedAddress);
57+
const normalizedExpectedAddress = expectedAddress.replace(/\s*\((Payment|Stake)\)$/, '');
58+
expect(actualAddress).to.equal(normalizedExpectedAddress);
5859
}
5960
}
6061

packages/translation/src/lib/translations/core/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@
208208
"core.authorizeDapp.seeWalletUtxo": "See your wallet UTXOs",
209209
"core.authorizeDapp.title": "Allow this site to",
210210
"core.authorizeDapp.warning": "Only connect to trusted DApps",
211-
"core.signMessage.title": "Message signing",
211+
"core.signMessage.title": "Sign message",
212212
"core.signMessage.instructions": "Type or paste the message to be signed",
213-
"core.signMessage.subtitle": "Your message will be encrypted and can be used after that",
213+
"core.signMessage.subtitle": "Your message will be cryptographically signed to prove authenticity",
214214
"core.signMessage.addressLabel": "Address",
215215
"core.signMessage.selectAddress": "Select an address to use",
216216
"core.signMessage.messageLabel": "Message to sign",
@@ -231,12 +231,15 @@
231231
"core.signMessage.successTitle": "All done!",
232232
"core.signMessage.signature": "Signature",
233233
"core.signMessage.key": "Public Key",
234+
"core.signMessage.rawKey": "Public Key (Glacier Drop Compatible)",
234235
"core.signMessage.nextButton": "Next",
235236
"core.signMessage.closeButton": "Close",
236237
"core.signMessage.cancelButton": "Cancel",
237238
"core.signMessage.signButton": "Sign message",
238239
"core.signMessage.signWithHardwareWalletButton": "Sign message with hardware wallet",
239240
"core.signMessage.hardwareWalletNotConnected": "Make sure your hardware wallet is connected and unlocked.",
241+
"core.signMessage.addressTypePayment": "Payment",
242+
"core.signMessage.addressTypeStake": "Stake",
240243
"core.Burn.title": "Confirm transaction",
241244
"core.coinInputSelection.assetSelection": "Select tokens or NFTs",
242245
"core.coinInputSelection.nfts": "NFTs",

0 commit comments

Comments
 (0)