|
| 1 | +import { Messenger } from '@metamask/messenger'; |
| 2 | +import { Json } from '@metamask/utils'; |
| 3 | +import { webcrypto } from 'crypto'; |
| 4 | + |
| 5 | +import MockEncryptor from '../../keyring-controller/tests/mocks/mockEncryptor'; |
| 6 | +import * as initializationModule from './initialization/initialization'; |
| 7 | +import { importSecretRecoveryPhrase } from './utilities'; |
| 8 | +import { Wallet } from './Wallet'; |
| 9 | + |
| 10 | +const TEST_SRP = 'test test test test test test test test test test test ball'; |
| 11 | +const TEST_PASSWORD = 'testpass'; |
| 12 | + |
| 13 | +async function setupWallet(): Promise<Wallet> { |
| 14 | + const wallet = new Wallet({}); |
| 15 | + |
| 16 | + await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_SRP); |
| 17 | + |
| 18 | + return wallet; |
| 19 | +} |
| 20 | + |
| 21 | +describe('Wallet', () => { |
| 22 | + beforeAll(() => { |
| 23 | + // We can remove this once we drop Node 18 |
| 24 | + // eslint-disable-next-line n/no-unsupported-features/node-builtins |
| 25 | + globalThis.crypto ??= webcrypto as typeof globalThis.crypto; |
| 26 | + |
| 27 | + // eslint-disable-next-line no-restricted-syntax |
| 28 | + if (!('CryptoKey' in globalThis)) { |
| 29 | + Object.defineProperty(globalThis, 'CryptoKey', { |
| 30 | + value: webcrypto.CryptoKey, |
| 31 | + }); |
| 32 | + } |
| 33 | + }); |
| 34 | + |
| 35 | + it('exposes state', async () => { |
| 36 | + const wallet = await setupWallet(); |
| 37 | + const { state } = wallet; |
| 38 | + |
| 39 | + expect(state.KeyringController).toStrictEqual({ |
| 40 | + isUnlocked: true, |
| 41 | + keyrings: expect.any(Array), |
| 42 | + encryptionKey: expect.any(String), |
| 43 | + encryptionSalt: expect.any(String), |
| 44 | + vault: expect.any(String), |
| 45 | + }); |
| 46 | + }); |
| 47 | + |
| 48 | + it('exposes instances', async () => { |
| 49 | + const wallet = await setupWallet(); |
| 50 | + |
| 51 | + expect(wallet.getInstance('KeyringController').state).toStrictEqual({ |
| 52 | + isUnlocked: true, |
| 53 | + keyrings: expect.any(Array), |
| 54 | + encryptionKey: expect.any(String), |
| 55 | + encryptionSalt: expect.any(String), |
| 56 | + vault: expect.any(String), |
| 57 | + }); |
| 58 | + }); |
| 59 | + |
| 60 | + it('supports passing instance options', async () => { |
| 61 | + const wallet = new Wallet({ |
| 62 | + instanceOptions: { |
| 63 | + keyringController: { |
| 64 | + encryptor: new MockEncryptor(), |
| 65 | + }, |
| 66 | + }, |
| 67 | + }); |
| 68 | + |
| 69 | + await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_SRP); |
| 70 | + |
| 71 | + const { state } = wallet; |
| 72 | + |
| 73 | + const vault = JSON.parse(state.KeyringController.vault as string); |
| 74 | + |
| 75 | + expect(vault).toStrictEqual({ |
| 76 | + data: expect.any(String), |
| 77 | + iv: 'iv', |
| 78 | + salt: 'salt', |
| 79 | + }); |
| 80 | + }); |
| 81 | + |
| 82 | + it('supports passing additional initialization configurations', async () => { |
| 83 | + class DummyController { |
| 84 | + state = { foo: 'bar' }; |
| 85 | + } |
| 86 | + |
| 87 | + class DummyService {} |
| 88 | + |
| 89 | + const wallet = new Wallet({ |
| 90 | + initializationConfigurations: [ |
| 91 | + { |
| 92 | + name: 'KeyringController', |
| 93 | + getMessenger: (): Messenger<string> => |
| 94 | + new Messenger({ namespace: 'KeyringController' }), |
| 95 | + init: (): DummyController => new DummyController(), |
| 96 | + }, |
| 97 | + { |
| 98 | + name: 'TestService', |
| 99 | + getMessenger: (): Messenger<string> => |
| 100 | + new Messenger({ namespace: 'TestService' }), |
| 101 | + init: (): DummyService => new DummyService(), |
| 102 | + }, |
| 103 | + ], |
| 104 | + }); |
| 105 | + const { state } = wallet; |
| 106 | + |
| 107 | + expect(state.KeyringController).toStrictEqual({ |
| 108 | + foo: 'bar', |
| 109 | + }); |
| 110 | + |
| 111 | + expect((state as Record<string, Json>).TestService).toBeUndefined(); |
| 112 | + }); |
| 113 | + |
| 114 | + it('exposes controllerMetadata for each initialized controller', async () => { |
| 115 | + const wallet = await setupWallet(); |
| 116 | + |
| 117 | + const names = Object.keys(wallet.controllerMetadata); |
| 118 | + expect(names).toStrictEqual(Object.keys(wallet.state)); |
| 119 | + for (const name of names) { |
| 120 | + expect(wallet.controllerMetadata[name]).toBeDefined(); |
| 121 | + } |
| 122 | + }); |
| 123 | + |
| 124 | + it('omits instances without a metadata property from controllerMetadata', async () => { |
| 125 | + const fakeMetadata = { |
| 126 | + foo: { persist: true, includeInDebugSnapshot: false }, |
| 127 | + }; |
| 128 | + jest.spyOn(initializationModule, 'initialize').mockReturnValueOnce({ |
| 129 | + // @ts-expect-error Mock data. |
| 130 | + WithMeta: { state: {}, metadata: fakeMetadata }, |
| 131 | + NoMeta: { state: {} }, |
| 132 | + }); |
| 133 | + |
| 134 | + const wallet = new Wallet({}); |
| 135 | + |
| 136 | + expect(wallet.controllerMetadata).toStrictEqual({ |
| 137 | + WithMeta: fakeMetadata, |
| 138 | + }); |
| 139 | + expect(Object.keys(wallet.state)).toStrictEqual(['WithMeta', 'NoMeta']); |
| 140 | + }); |
| 141 | + |
| 142 | + it('disallows modifying the messenger', async () => { |
| 143 | + const wallet = await setupWallet(); |
| 144 | + |
| 145 | + expect(() => { |
| 146 | + wallet.messenger = new Messenger({ namespace: 'Root' }); |
| 147 | + }).toThrow('The messenger cannot be directly mutated.'); |
| 148 | + }); |
| 149 | + |
| 150 | + it('disallows modifying the state', async () => { |
| 151 | + const wallet = await setupWallet(); |
| 152 | + |
| 153 | + expect(() => { |
| 154 | + wallet.state = { KeyringController: { isUnlocked: false, keyrings: [] } }; |
| 155 | + }).toThrow('Wallet state cannot be directly mutated.'); |
| 156 | + }); |
| 157 | + |
| 158 | + it('disallows modifying the controller metadata', async () => { |
| 159 | + const wallet = await setupWallet(); |
| 160 | + |
| 161 | + expect(() => { |
| 162 | + wallet.controllerMetadata = {}; |
| 163 | + }).toThrow('The controller metadata cannot be directly mutated.'); |
| 164 | + }); |
| 165 | + |
| 166 | + it('calls destroy on instances exactly once', async () => { |
| 167 | + const wallet = await setupWallet(); |
| 168 | + |
| 169 | + const keyringController = wallet.getInstance('KeyringController'); |
| 170 | + |
| 171 | + const spy = jest.spyOn(keyringController, 'destroy'); |
| 172 | + |
| 173 | + await wallet.destroy(); |
| 174 | + await wallet.destroy(); |
| 175 | + |
| 176 | + expect(spy).toHaveBeenCalledTimes(1); |
| 177 | + }); |
| 178 | + |
| 179 | + describe('KeyringController', () => { |
| 180 | + it('can unlock and populate accounts', async () => { |
| 181 | + const wallet = await setupWallet(); |
| 182 | + const { messenger } = wallet; |
| 183 | + |
| 184 | + expect( |
| 185 | + await messenger.call('KeyringController:getAccounts'), |
| 186 | + ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); |
| 187 | + }); |
| 188 | + |
| 189 | + it('can unlock a persisted vault', async () => { |
| 190 | + const vault = |
| 191 | + '{"data":"iOD5pIcPeRZYQ4WdEMsNYoZ3xBxWBafIU8Cr4nD0X4zBvrOA06tGen3sKQ/ValasXSweLnzH9Fk2frkPYmqeJWBtTNYFwdHPe7P970ThZwreSXN1Sqrx9Ad+YzmIN0y89Yg3KrUodPWaRgIZmgWbfDon6ADPgeEDkX0/GAEYET39O7Rx/gL+rcaTpxnpHPTgHiLbhRHWGsS3z+JVomSqoLAO5XVvrJWenO6R3Nzm62BaJaSPrf/pwstZqhSvxTq8hnQf7aR81hWfwYTxNBVG7TC/dniSQ8K5So6PvUN5nzAqvtzzHT2TagOuxQkX88Zi17P8os21jNmNdA90IGYroD+b/mppyRIgRYWtAUQZH9ji36atEuFupszbg8Qw1iaL3EQyUogC30Cpj9ko5bbqhYgqmFHF0J/kflhPHKuO6d4tgSmhYpTumydQRjxaPnlghIS5YI4W+7p9HVBpb+c6IPUz9y/x3Ngbp+ukJwOnXt2U/eZhXrJzi2z1x/nzPg4fzDJoM7k=","iv":"yrZsyC7dso/q7pQ48YX3vw==","keyMetadata":{"algorithm":"PBKDF2","params":{"iterations":600000}},"salt":"s7nIrMWK1lcZVjfdmES1DBML8Uz4ja2fpm8zUz1lWI0="}'; |
| 192 | + |
| 193 | + const wallet = new Wallet({ |
| 194 | + state: { |
| 195 | + KeyringController: { |
| 196 | + vault, |
| 197 | + }, |
| 198 | + }, |
| 199 | + }); |
| 200 | + |
| 201 | + await wallet.messenger.call( |
| 202 | + 'KeyringController:submitPassword', |
| 203 | + TEST_PASSWORD, |
| 204 | + ); |
| 205 | + |
| 206 | + expect(wallet.state.KeyringController).toStrictEqual({ |
| 207 | + isUnlocked: true, |
| 208 | + keyrings: expect.any(Array), |
| 209 | + encryptionKey: expect.any(String), |
| 210 | + encryptionSalt: expect.any(String), |
| 211 | + vault: expect.any(String), |
| 212 | + }); |
| 213 | + }); |
| 214 | + |
| 215 | + it('can lock', async () => { |
| 216 | + const wallet = await setupWallet(); |
| 217 | + const { messenger } = wallet; |
| 218 | + |
| 219 | + await messenger.call('KeyringController:setLocked'); |
| 220 | + |
| 221 | + expect(wallet.state.KeyringController).toStrictEqual({ |
| 222 | + isUnlocked: false, |
| 223 | + keyrings: [], |
| 224 | + vault: expect.any(String), |
| 225 | + }); |
| 226 | + }); |
| 227 | + }); |
| 228 | +}); |
0 commit comments