• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / lace / 9413237952

08 Feb 2024 01:46PM UTC coverage: 52.934% (-0.9%) from 53.839%
9413237952

push

github

f98d1b
rhyslbw
chore: bump version

1989 of 4673 branches covered (42.56%)

Branch coverage included in aggregate %.

4487 of 7561 relevant lines covered (59.34%)

56.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.09
/apps/browser-extension-wallet/src/hooks/useWalletManager.ts
1
import { useCallback } from 'react';
26✔
2
import dayjs from 'dayjs';
26✔
3
import isEqual from 'lodash/isEqual';
26✔
4
import { Wallet } from '@lace/cardano';
26✔
5
import { useWalletStore } from '@stores';
26✔
6
import { useCardanoWalletManagerContext } from '@providers/CardanoWalletManager';
26✔
7
import { useAppSettingsContext } from '@providers/AppSettings';
26✔
8
import { useBackgroundServiceAPIContext } from '@providers/BackgroundServiceAPI';
26✔
9
import { AddressBookSchema, addressBookSchema, NftFoldersSchema, nftFoldersSchema, useDbState } from '@src/lib/storage';
26✔
10
import {
26✔
11
  deleteFromLocalStorage,
12
  getValueFromLocalStorage,
13
  clearLocalStorage,
14
  saveValueInLocalStorage
15
} from '@src/utils/local-storage';
16
import { config } from '@src/config';
26✔
17
import { getWalletFromStorage } from '@src/utils/get-wallet-from-storage';
26✔
18
import { getUserIdService } from '@providers/AnalyticsProvider/getUserIdService';
26✔
19
import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/matomo/config';
26✔
20
import { ILocalStorage } from '@src/types';
21

22
const { AVAILABLE_CHAINS, CHAIN } = config();
26✔
23

24
export interface CreateWallet {
25
  name: string;
26
  mnemonic: string[];
27
  password: string;
28
  chainId: Wallet.Cardano.ChainId;
29
}
30

31
export interface CreateWalletData {
32
  wallet: Wallet.CardanoWallet;
33
  encryptedKeyAgents: Uint8Array;
34
  name: string;
35
}
36

37
export interface SetWallet {
38
  walletInstance: CreateWalletData;
39
  chainName?: Wallet.ChainName;
40
  mnemonicVerificationFrequency?: string;
41
}
42

43
export interface CreateHardwareWallet {
44
  accountIndex?: number;
45
  name: string;
46
  deviceConnection: Wallet.DeviceConnection;
47
  chainId: Wallet.Cardano.ChainId;
48
  connectedDevice: Wallet.HardwareWallets;
49
}
50

51
export interface UseWalletManager {
52
  lockWallet: () => void;
53
  unlockWallet: (password: string) => Promise<Wallet.KeyAgentsByChain | void>;
54
  loadWallet: (callback?: (result: boolean) => void) => Promise<void>;
55
  createWallet: (args: CreateWallet) => Promise<CreateWalletData>;
56
  setWallet: (args: SetWallet) => Promise<void>;
57
  getPassword: () => Promise<Uint8Array>;
58
  createHardwareWallet: (args: CreateHardwareWallet) => Promise<Wallet.CardanoWalletByChain>;
59
  connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise<Wallet.DeviceConnection>;
60
  saveHardwareWallet: (wallet: Wallet.CardanoWalletByChain, chainName?: Wallet.ChainName) => Promise<void>;
61
  deleteWallet: (isForgotPasswordFlow?: boolean) => Promise<void>;
62
  executeWithPassword: <T>(password: string, promiseFn: () => Promise<T>, cleanPassword?: boolean) => Promise<T>;
63
  clearPassword: () => void;
64
  switchNetwork: (chainName: Wallet.ChainName) => Promise<void>;
65
  updateAddresses: (args: {
66
    addresses: Wallet.KeyManagement.GroupedAddress[];
67
    currentChainName: Wallet.ChainName;
68
  }) => Promise<void>;
69
}
70

71
/** Connects a hardware wallet device */
72
export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Promise<Wallet.DeviceConnection> =>
26✔
73
  await Wallet.connectDevice(model);
2✔
74

75
const encryptKeyAgents = ({
26✔
76
  keyAgentsByChain,
77
  password
78
}: {
79
  keyAgentsByChain: Wallet.KeyAgentsByChain;
80
  password: string;
81
}) =>
82
  // Encrypt key agents with password for lock/unlock feature
83
  Wallet.KeyManagement.emip3encrypt(Buffer.from(JSON.stringify(keyAgentsByChain)), Buffer.from(password));
2✔
84

85
export const useWalletManager = (): UseWalletManager => {
26✔
86
  const cardanoWalletManager = useCardanoWalletManagerContext();
186✔
87
  const {
88
    walletLock,
89
    setWalletLock,
90
    keyAgentData,
91
    setKeyAgentData,
92
    setCardanoWallet,
93
    resetWalletLock,
94
    setCurrentChain,
95
    setCardanoCoin,
96
    environmentName,
97
    walletManagerUi,
98
    setAddressesDiscoveryCompleted
99
  } = useWalletStore();
186✔
100
  const [settings, updateAppSettings] = useAppSettingsContext();
186✔
101
  const {
102
    utils: { clearTable: clearAddressBook }
103
  } = useDbState<AddressBookSchema, AddressBookSchema>([], addressBookSchema);
186✔
104
  const {
105
    utils: { clearTable: clearNftsFolders }
106
  } = useDbState<NftFoldersSchema, NftFoldersSchema>([], nftFoldersSchema);
186✔
107
  const backgroundService = useBackgroundServiceAPIContext();
186✔
108
  const userIdService = getUserIdService();
186✔
109

110
  const storeMnemonicInBackgroundScript = useCallback(
186✔
111
    async (mnemonic: string[], password: string) => {
2✔
112
      const walletEncrypted = await Wallet.KeyManagement.emip3encrypt(
2✔
113
        Buffer.from(Wallet.KeyManagement.util.joinMnemonicWords(mnemonic)),
114
        Buffer.from(password)
115
      );
116
      await backgroundService.setBackgroundStorage({ mnemonic: JSON.stringify(walletEncrypted) });
2✔
117
    },
118
    [backgroundService]
119
  );
120

121
  /**
122
   * Called by the wallet when needed to decrypt private key.
123
   *
124
   * Input password must be set before a function that needs it is executed (e.g. finalizeTx()),
125
   * and should be cleared afterwards
126
   */
127
  const getPassword: () => Promise<Uint8Array> = useCallback(
186✔
128
    async () => backgroundService.getWalletPassword(),
2✔
129
    [backgroundService]
130
  );
131

132
  /**
133
   * Sets the wallet password and clears it right after running the promise unless `cleanPassword` is `true`
134
   */
135
  const executeWithPassword = useCallback(
186✔
136
    async <T>(password: string, promiseFn: () => Promise<T>, cleanPassword = true): Promise<T> => {
4✔
137
      try {
4✔
138
        backgroundService.setWalletPassword(Buffer.from(password));
4✔
139
        return await promiseFn();
4✔
140
      } finally {
141
        // Delete the password so we don't keep it in state. `cleanPassword` flag is needed for cip30 use
142
        if (cleanPassword) backgroundService.setWalletPassword();
4✔
143
      }
144
    },
145
    [backgroundService]
146
  );
147

148
  /**
149
   * Clears the wallet password
150
   */
151
  const clearPassword = useCallback(() => {
186✔
152
    backgroundService.setWalletPassword();
×
153
  }, [backgroundService]);
154

155
  /**
156
   * Deletes wallet info in storage, which should be stored encrypted with the wallet password as lock
157
   */
158
  const lockWallet = useCallback(async (): Promise<void> => {
186✔
159
    if (!walletLock) return;
6✔
160
    // Deletes key agent data from storage and clears states
161
    await backgroundService.clearBackgroundStorage({ keys: ['keyAgentsByChain'] });
4✔
162
    deleteFromLocalStorage('keyAgentData');
4✔
163

164
    setKeyAgentData();
4✔
165
    setCardanoWallet();
4✔
166
    setAddressesDiscoveryCompleted(false);
4✔
167
  }, [walletLock, backgroundService, setKeyAgentData, setCardanoWallet, setAddressesDiscoveryCompleted]);
168

169
  /**
170
   * Recovers wallet info from encrypted lock using the wallet password
171
   */
172
  const unlockWallet = useCallback(
186✔
173
    async (password: string): Promise<Wallet.KeyAgentsByChain | void> => {
4✔
174
      if (!walletLock) return;
4✔
175
      const walletDecrypted = await Wallet.KeyManagement.emip3decrypt(walletLock, Buffer.from(password));
2✔
176

177
      // eslint-disable-next-line consistent-return
178
      return JSON.parse(walletDecrypted.toString());
2✔
179
    },
180
    [walletLock]
181
  );
182

183
  /**
184
   * Loads wallet from key agent serialized data in storage
185
   */
186
  const loadWallet = useCallback(
186✔
187
    async (callback?: (result: boolean) => void) => {
8✔
188
      // Wallet info for current network
189
      if (!keyAgentData) return;
8✔
190
      const walletName = getWalletFromStorage()?.name;
6✔
191
      if (!walletName || !walletManagerUi) return;
6✔
192

193
      const wallet = await cardanoWalletManager.restoreWallet(
2✔
194
        walletManagerUi,
195
        walletName,
196
        keyAgentData,
197
        getPassword,
198
        environmentName,
199
        undefined,
200
        callback
201
      );
202
      setCardanoWallet(wallet);
2✔
203
    },
204
    [keyAgentData, cardanoWalletManager, walletManagerUi, getPassword, environmentName, setCardanoWallet]
205
  );
206

207
  /**
208
   * Creates or restores a new wallet with the cardano-js-sdk
209
   * and saves it in browser storage with the data to lock/unlock it
210
   */
211
  const createWallet = useCallback(
186✔
212
    async ({ mnemonic, name, password, chainId }: CreateWallet): Promise<CreateWalletData> => {
2✔
213
      const { keyAgentsByChain, ...wallet } = await executeWithPassword(password, () =>
2✔
214
        cardanoWalletManager.createCardanoWallet(walletManagerUi, name, mnemonic, getPassword, chainId)
2✔
215
      );
216

217
      // Encrypt key agents with password for lock/unlock feature
218
      const encryptedKeyAgents = await encryptKeyAgents({ keyAgentsByChain, password });
2✔
219

220
      // Save in storage
221
      await storeMnemonicInBackgroundScript(mnemonic, password);
2✔
222
      await backgroundService.setBackgroundStorage({ keyAgentsByChain });
2✔
223
      saveValueInLocalStorage({ key: 'lock', value: encryptedKeyAgents });
2✔
224
      saveValueInLocalStorage({ key: 'wallet', value: { name } });
2✔
225
      saveValueInLocalStorage({ key: 'keyAgentData', value: wallet.keyAgent.serializableData });
2✔
226

227
      return { wallet, encryptedKeyAgents, name };
2✔
228
    },
229
    [
230
      backgroundService,
231
      walletManagerUi,
232
      executeWithPassword,
233
      storeMnemonicInBackgroundScript,
234
      getPassword,
235
      cardanoWalletManager
236
    ]
237
  );
238

239
  const setWallet = useCallback(
186✔
240
    async ({ walletInstance, mnemonicVerificationFrequency = '', chainName = CHAIN }: SetWallet): Promise<void> => {
4✔
241
      updateAppSettings({
4✔
242
        chainName,
243
        mnemonicVerificationFrequency,
244
        lastMnemonicVerification: dayjs().valueOf().toString()
245
      });
246

247
      // Set wallet states
248
      setWalletLock(walletInstance.encryptedKeyAgents);
4✔
249
      setCardanoWallet(walletInstance.wallet);
4✔
250
      setKeyAgentData(walletInstance.wallet.keyAgent.serializableData);
4✔
251
      setCurrentChain(chainName);
4✔
252
    },
253
    [updateAppSettings, setWalletLock, setCardanoWallet, setKeyAgentData, setCurrentChain]
254
  );
255

256
  /**
257
   * Creates a Ledger hardware wallet
258
   * and saves it in browser storage with the data to lock/unlock it
259
   */
260
  const createHardwareWallet = useCallback(
186✔
261
    async ({
262
      accountIndex = 0,
1✔
263
      deviceConnection,
264
      name,
265
      chainId,
266
      connectedDevice
267
    }: CreateHardwareWallet): Promise<Wallet.CardanoWalletByChain> =>
4✔
268
      cardanoWalletManager.createHardwareWallet(walletManagerUi, {
4✔
269
        accountIndex,
270
        deviceConnection,
271
        name,
272
        activeChainId: chainId,
273
        connectedDevice
274
      }),
275
    [walletManagerUi, cardanoWalletManager]
276
  );
277

278
  /**
279
   * Saves hardware wallet in storage and updates wallet store
280
   */
281
  const saveHardwareWallet = useCallback(
186✔
282
    async (wallet: Wallet.CardanoWalletByChain, chainName = CHAIN): Promise<void> => {
4✔
283
      const { keyAgentsByChain, ...cardanoWalletData } = wallet;
4✔
284

285
      // Save in storage
286
      await backgroundService.setBackgroundStorage({ keyAgentsByChain });
4✔
287
      saveValueInLocalStorage({ key: 'wallet', value: { name: cardanoWalletData.name } });
4✔
288
      saveValueInLocalStorage({ key: 'keyAgentData', value: cardanoWalletData.keyAgent.serializableData });
4✔
289

290
      updateAppSettings({
4✔
291
        chainName,
292
        // Doesn't make sense for hardware wallets
293
        mnemonicVerificationFrequency: ''
294
      });
295

296
      // Set wallet states
297
      // eslint-disable-next-line unicorn/no-null
298
      setWalletLock(null); // Lock is not available for hardware wallets
4✔
299
      setCardanoWallet(cardanoWalletData);
4✔
300
      setKeyAgentData(cardanoWalletData.keyAgent.serializableData);
4✔
301
      setCurrentChain(chainName);
4✔
302
    },
303
    [backgroundService, updateAppSettings, setWalletLock, setCardanoWallet, setKeyAgentData, setCurrentChain]
304
  );
305

306
  /**
307
   * Deletes Wallet from memory, all info from browser storage and destroys all stores
308
   */
309
  const deleteWallet = useCallback(
186✔
310
    async (isForgotPasswordFlow = false): Promise<void> => {
2✔
311
      await Wallet.shutdownWallet(walletManagerUi);
2✔
312
      deleteFromLocalStorage('appSettings');
2✔
313
      deleteFromLocalStorage('showDappBetaModal');
2✔
314
      deleteFromLocalStorage('lastStaking');
2✔
315
      deleteFromLocalStorage('userInfo');
2✔
316
      deleteFromLocalStorage('keyAgentData');
2✔
317
      await backgroundService.clearBackgroundStorage({
2✔
318
        except: ['fiatPrices', 'userId', 'usePersistentUserId', 'experimentsConfiguration']
319
      });
320
      setKeyAgentData();
2✔
321
      resetWalletLock();
2✔
322
      setCardanoWallet();
2✔
323
      setCurrentChain(CHAIN);
2✔
324

325
      const commonLocalStorageKeysToKeep: (keyof ILocalStorage)[] = [
2✔
326
        'currency',
327
        'lock',
328
        'mode',
329
        'hideBalance',
330
        'isForgotPasswordFlow',
331
        'multidelegationFirstVisit',
332
        'multidelegationFirstVisitSincePortfolioPersistence'
333
      ];
334

335
      if (isForgotPasswordFlow) {
2!
336
        const additionalKeysToKeep: (keyof ILocalStorage)[] = ['wallet', ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY];
×
337
        clearLocalStorage({
×
338
          except: [...commonLocalStorageKeysToKeep, ...additionalKeysToKeep]
339
        });
340
        return;
×
341
      }
342

343
      clearLocalStorage({ except: commonLocalStorageKeysToKeep });
2✔
344
      await userIdService.clearId();
2✔
345
      clearAddressBook();
2✔
346
      clearNftsFolders();
2✔
347
    },
348
    [
349
      walletManagerUi,
350
      setKeyAgentData,
351
      resetWalletLock,
352
      setCardanoWallet,
353
      backgroundService,
354
      setCurrentChain,
355
      userIdService,
356
      clearAddressBook,
357
      clearNftsFolders
358
    ]
359
  );
360

361
  /**
362
   * Deactivates current wallet and activates it again with the new network
363
   */
364
  const switchNetwork = useCallback(
186✔
365
    async (chainName: Wallet.ChainName): Promise<void> => {
20✔
366
      if (!walletManagerUi) return;
20✔
367
      const chainId = Wallet.Cardano.ChainIds[chainName];
18✔
368
      console.info('Switching chain to', chainName, AVAILABLE_CHAINS);
18✔
369
      if (!chainId || !AVAILABLE_CHAINS.includes(chainName)) throw new Error('Chain not supported');
18✔
370

371
      const backgroundStorage = await backgroundService.getBackgroundStorage();
12✔
372
      const keyAgentsByChain: Wallet.KeyAgentsByChain = backgroundStorage.keyAgentsByChain;
12✔
373
      const walletName = getWalletFromStorage()?.name;
12!
374

375
      if (!keyAgentsByChain[chainName] || !walletName) throw new Error('Wallet data for chosen chain not found');
12✔
376
      const { keyAgentData: newKeyAgent } = keyAgentsByChain[chainName];
8✔
377
      if (!newKeyAgent) throw new Error('Wallet data for chosen chain is empty');
8✔
378

379
      const { asyncKeyAgent, keyAgent, wallet } = await Wallet.restoreWalletFromKeyAgent(
6✔
380
        walletManagerUi,
381
        walletName,
382
        newKeyAgent,
383
        getPassword,
384
        chainName,
385
        false
386
      );
387

388
      await Wallet.switchKeyAgents(walletManagerUi, walletName, asyncKeyAgent, chainName);
6✔
389

390
      setAddressesDiscoveryCompleted(false);
6✔
391
      updateAppSettings({ ...settings, chainName });
6✔
392
      saveValueInLocalStorage({ key: 'wallet', value: { name: walletName } });
6✔
393
      saveValueInLocalStorage({ key: 'keyAgentData', value: newKeyAgent });
6✔
394

395
      setCardanoWallet({
6✔
396
        asyncKeyAgent,
397
        keyAgent,
398
        name: walletName,
399
        wallet
400
      });
401
      setCurrentChain(chainName);
6✔
402
      setCardanoCoin(chainId);
6✔
403
      setKeyAgentData(newKeyAgent);
6✔
404
    },
405
    [
406
      backgroundService,
407
      walletManagerUi,
408
      getPassword,
409
      setAddressesDiscoveryCompleted,
410
      updateAppSettings,
411
      settings,
412
      setCardanoWallet,
413
      setCurrentChain,
414
      setCardanoCoin,
415
      setKeyAgentData
416
    ]
417
  );
418

419
  const updateAddresses = useCallback<UseWalletManager['updateAddresses']>(
186✔
420
    async ({ addresses, currentChainName }) => {
×
421
      const currentChainId = Wallet.Cardano.ChainIds[currentChainName];
×
422
      const currentKeyAgentData = getValueFromLocalStorage('keyAgentData');
×
423
      if (!currentKeyAgentData) return;
×
424

425
      const networkOfKeyAgentAsIntended = isEqual(currentKeyAgentData.chainId, currentChainId);
×
426
      const networkOfAddressesEqualsNetworkOfKeyAgent = addresses.every(
×
427
        (address) => address.networkId === currentKeyAgentData.chainId.networkId
×
428
      );
429
      const keyAgentInLocalStorageIsTheOneWeExpect =
430
        networkOfKeyAgentAsIntended && networkOfAddressesEqualsNetworkOfKeyAgent;
×
431
      const addressesUpToDate = isEqual(currentKeyAgentData.knownAddresses, addresses);
×
432
      if (!keyAgentInLocalStorageIsTheOneWeExpect) return;
×
433

434
      if (addressesUpToDate) {
×
435
        setAddressesDiscoveryCompleted(true);
×
436
        return;
×
437
      }
438

439
      let newKeyAgentData;
440
      if (keyAgentInLocalStorageIsTheOneWeExpect) {
×
441
        newKeyAgentData = {
×
442
          ...currentKeyAgentData,
443
          knownAddresses: addresses
444
        };
445
        saveValueInLocalStorage({
×
446
          key: 'keyAgentData',
447
          value: newKeyAgentData
448
        });
449
      }
450

451
      const backgroundStorage = await backgroundService.getBackgroundStorage();
×
452
      const { name } = getValueFromLocalStorage('wallet');
×
453

454
      if (!backgroundStorage) throw new Error("Couldn't access background storage");
×
455
      const { keyAgentsByChain } = backgroundStorage;
×
456

457
      newKeyAgentData ??= {
×
458
        ...keyAgentsByChain[currentChainName].keyAgentData,
459
        knownAddresses: addresses
460
      };
461

462
      keyAgentsByChain[currentChainName].keyAgentData = newKeyAgentData;
×
463
      await backgroundService.setBackgroundStorage({ keyAgentsByChain });
×
464

465
      // TODO: update walletLock so after unlocking the wallet the discovery does not get triggered again
466

467
      const newKeyAgent = await Wallet.createKeyAgent(walletManagerUi, newKeyAgentData, getPassword);
×
468
      setCardanoWallet({
×
469
        asyncKeyAgent: Wallet.KeyManagement.util.createAsyncKeyAgent(newKeyAgent),
470
        keyAgent: newKeyAgent,
471
        name,
472
        wallet: walletManagerUi.wallet
473
      });
474
      setKeyAgentData(newKeyAgentData);
×
475
      setAddressesDiscoveryCompleted(true);
×
476
    },
477
    [backgroundService, getPassword, setAddressesDiscoveryCompleted, setCardanoWallet, setKeyAgentData, walletManagerUi]
478
  );
479

480
  return {
186✔
481
    lockWallet,
482
    unlockWallet,
483
    loadWallet,
484
    createWallet,
485
    setWallet,
486
    getPassword,
487
    createHardwareWallet,
488
    connectHardwareWallet,
489
    saveHardwareWallet,
490
    deleteWallet,
491
    executeWithPassword,
492
    clearPassword,
493
    switchNetwork,
494
    updateAddresses
495
  };
496
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc