Skip to content

Commit 5f50434

Browse files
committed
chore: add tests for cash account key ring
1 parent 42960d3 commit 5f50434

File tree

1 file changed

+242
-0
lines changed

1 file changed

+242
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import {
2+
recoverPersonalSignature,
3+
recoverTypedSignature,
4+
SignTypedDataVersion,
5+
type MessageTypes,
6+
type TypedMessage,
7+
} from '@metamask/eth-sig-util';
8+
import { normalize } from '@metamask/eth-sig-util';
9+
import { assert, type Hex } from '@metamask/utils';
10+
11+
import { CashAccountKeyring } from './cash-account-keyring';
12+
13+
const sampleMnemonic =
14+
'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango';
15+
16+
const cashAccountHdPath = `m/44'/4392018'/0'/0`;
17+
18+
const getAddressAtIndex = async (
19+
keyring: CashAccountKeyring,
20+
index: number,
21+
): Promise<Hex> => {
22+
const accounts = await keyring.getAccounts();
23+
assert(accounts[index], `Account not found at index ${index}`);
24+
return accounts[index];
25+
};
26+
27+
describe('CashAccountKeyring', () => {
28+
describe('static properties', () => {
29+
it('has the correct type', () => {
30+
expect(CashAccountKeyring.type).toBe('Cash Account Keyring');
31+
});
32+
});
33+
34+
describe('#type', () => {
35+
it('returns the correct value', () => {
36+
const keyring = new CashAccountKeyring();
37+
expect(keyring.type).toBe('Cash Account Keyring');
38+
expect(keyring.type).toBe(CashAccountKeyring.type);
39+
});
40+
});
41+
42+
describe('#hdPath', () => {
43+
it('uses the cash account derivation path', () => {
44+
const keyring = new CashAccountKeyring();
45+
expect(keyring.hdPath).toBe(cashAccountHdPath);
46+
});
47+
});
48+
49+
describe('#deserialize', () => {
50+
it('derives accounts using the cash account hd path', async () => {
51+
const keyring = new CashAccountKeyring();
52+
await keyring.deserialize({
53+
mnemonic: sampleMnemonic,
54+
numberOfAccounts: 1,
55+
});
56+
57+
const serialized = await keyring.serialize();
58+
expect(serialized.hdPath).toBe(cashAccountHdPath);
59+
});
60+
61+
it('derives different addresses than the standard HD keyring path', async () => {
62+
const { HdKeyring } = await import('@metamask/eth-hd-keyring');
63+
64+
const cashKeyring = new CashAccountKeyring();
65+
await cashKeyring.deserialize({
66+
mnemonic: sampleMnemonic,
67+
numberOfAccounts: 1,
68+
});
69+
const cashAccounts = await cashKeyring.getAccounts();
70+
71+
const hdKeyring = new HdKeyring();
72+
await hdKeyring.deserialize({
73+
mnemonic: sampleMnemonic,
74+
numberOfAccounts: 1,
75+
});
76+
const hdAccounts = await hdKeyring.getAccounts();
77+
78+
expect(cashAccounts[0]).not.toBe(hdAccounts[0]);
79+
});
80+
81+
it('uses the cash account hd path when no hdPath option is provided', async () => {
82+
const keyring = new CashAccountKeyring();
83+
await keyring.deserialize({
84+
mnemonic: sampleMnemonic,
85+
numberOfAccounts: 1,
86+
});
87+
88+
const serialized = await keyring.serialize();
89+
expect(serialized.hdPath).toBe(cashAccountHdPath);
90+
});
91+
92+
it('respects an explicitly provided hdPath', async () => {
93+
const customPath = `m/44'/60'/0'/0`;
94+
const keyring = new CashAccountKeyring();
95+
await keyring.deserialize({
96+
mnemonic: sampleMnemonic,
97+
numberOfAccounts: 1,
98+
hdPath: customPath,
99+
});
100+
101+
const serialized = await keyring.serialize();
102+
expect(serialized.hdPath).toBe(customPath);
103+
});
104+
});
105+
106+
describe('#addAccounts', () => {
107+
it('creates accounts', async () => {
108+
const keyring = new CashAccountKeyring();
109+
await keyring.deserialize({
110+
mnemonic: sampleMnemonic,
111+
numberOfAccounts: 1,
112+
});
113+
114+
await keyring.addAccounts(1);
115+
const accounts = await keyring.getAccounts();
116+
expect(accounts).toHaveLength(2);
117+
});
118+
});
119+
120+
describe('#signPersonalMessage', () => {
121+
it('signs and the signature can be recovered', async () => {
122+
const keyring = new CashAccountKeyring();
123+
await keyring.deserialize({
124+
mnemonic: sampleMnemonic,
125+
numberOfAccounts: 1,
126+
});
127+
128+
const address = await getAddressAtIndex(keyring, 0);
129+
const message = '0x68656c6c6f20776f726c64';
130+
const signature = await keyring.signPersonalMessage(address, message);
131+
132+
const restored = recoverPersonalSignature({
133+
data: message,
134+
signature,
135+
});
136+
expect(restored).toStrictEqual(normalize(address));
137+
});
138+
});
139+
140+
describe('#signTypedData', () => {
141+
it('signs V1 typed data and the signature can be recovered', async () => {
142+
const keyring = new CashAccountKeyring();
143+
await keyring.deserialize({
144+
mnemonic: sampleMnemonic,
145+
numberOfAccounts: 1,
146+
});
147+
148+
const address = await getAddressAtIndex(keyring, 0);
149+
const typedData = [
150+
{
151+
type: 'string',
152+
name: 'message',
153+
value: 'Hi, Alice!',
154+
},
155+
];
156+
157+
const signature = await keyring.signTypedData(address, typedData);
158+
const restored = recoverTypedSignature({
159+
data: typedData,
160+
signature,
161+
version: SignTypedDataVersion.V1,
162+
});
163+
expect(restored).toStrictEqual(address);
164+
});
165+
166+
it('signs V3 typed data and the signature can be recovered', async () => {
167+
const keyring = new CashAccountKeyring();
168+
await keyring.deserialize({
169+
mnemonic: sampleMnemonic,
170+
numberOfAccounts: 1,
171+
});
172+
173+
const address = await getAddressAtIndex(keyring, 0);
174+
const typedData: TypedMessage<MessageTypes> = {
175+
types: {
176+
EIP712Domain: [],
177+
},
178+
domain: {},
179+
primaryType: 'EIP712Domain',
180+
message: {},
181+
};
182+
183+
const signature = await keyring.signTypedData(address, typedData, {
184+
version: SignTypedDataVersion.V3,
185+
});
186+
const restored = recoverTypedSignature({
187+
data: typedData,
188+
signature,
189+
version: SignTypedDataVersion.V3,
190+
});
191+
expect(restored).toStrictEqual(address);
192+
});
193+
});
194+
195+
describe('#removeAccount', () => {
196+
it('removes an existing account', async () => {
197+
const keyring = new CashAccountKeyring();
198+
await keyring.deserialize({
199+
mnemonic: sampleMnemonic,
200+
numberOfAccounts: 1,
201+
});
202+
203+
const accounts = await keyring.getAccounts();
204+
expect(accounts).toHaveLength(1);
205+
206+
keyring.removeAccount(accounts[0]!);
207+
const remaining = await keyring.getAccounts();
208+
expect(remaining).toHaveLength(0);
209+
});
210+
211+
it('throws when removing a non-existent account', () => {
212+
const keyring = new CashAccountKeyring();
213+
const fakeAddress = '0x0000000000000000000000000000000000000000';
214+
expect(() => keyring.removeAccount(fakeAddress)).toThrow(
215+
`Address ${fakeAddress} not found in this keyring`,
216+
);
217+
});
218+
});
219+
220+
describe('#serialize / #deserialize round-trip', () => {
221+
it('serializes what it deserializes', async () => {
222+
const keyring = new CashAccountKeyring();
223+
await keyring.deserialize({
224+
mnemonic: sampleMnemonic,
225+
numberOfAccounts: 2,
226+
});
227+
228+
const accounts = await keyring.getAccounts();
229+
const serialized = await keyring.serialize();
230+
231+
const restored = new CashAccountKeyring();
232+
await restored.deserialize(serialized);
233+
234+
const restoredAccounts = await restored.getAccounts();
235+
expect(restoredAccounts).toStrictEqual(accounts);
236+
237+
const restoredSerialized = await restored.serialize();
238+
expect(restoredSerialized.hdPath).toBe(cashAccountHdPath);
239+
expect(restoredSerialized.numberOfAccounts).toBe(2);
240+
});
241+
});
242+
});

0 commit comments

Comments
 (0)