UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

527 lines 29.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const sdk_1 = require("@bsv/sdk"); const sdk_2 = require("../sdk"); const CWIStyleWalletManager_1 = require("../CWIStyleWalletManager"); const globals_1 = require("@jest/globals"); globals_1.jest.useFakeTimers(); // ------------------------------------------------------------------------------------------ // Mocks and Utilities // ------------------------------------------------------------------------------------------ /** A utility to create an Outpoint string for test usage. */ function makeOutpoint(txid, vout) { return `${txid}:${vout}`; } /** * A mock underlying WalletInterface to verify that proxy methods: * 1. Are not callable if not authenticated * 2. Are disallowed if originator is admin * 3. Forward to the real method if conditions pass */ const mockUnderlyingWallet = { getPublicKey: globals_1.jest.fn(), revealCounterpartyKeyLinkage: globals_1.jest.fn(), revealSpecificKeyLinkage: globals_1.jest.fn(), encrypt: globals_1.jest.fn(), decrypt: globals_1.jest.fn(), createHmac: globals_1.jest.fn(), verifyHmac: globals_1.jest.fn(), createSignature: globals_1.jest.fn(), verifySignature: globals_1.jest.fn(), createAction: globals_1.jest.fn(), signAction: globals_1.jest.fn(), abortAction: globals_1.jest.fn(), listActions: globals_1.jest.fn(), internalizeAction: globals_1.jest.fn(), listOutputs: globals_1.jest.fn(), relinquishOutput: globals_1.jest.fn(), acquireCertificate: globals_1.jest.fn(), listCertificates: globals_1.jest.fn(), proveCertificate: globals_1.jest.fn(), relinquishCertificate: globals_1.jest.fn(), discoverByIdentityKey: globals_1.jest.fn(), discoverByAttributes: globals_1.jest.fn(), isAuthenticated: globals_1.jest.fn(), waitForAuthentication: globals_1.jest.fn(), getHeight: globals_1.jest.fn(), getHeaderForHeight: globals_1.jest.fn(), getNetwork: globals_1.jest.fn(), getVersion: globals_1.jest.fn() }; /** * A mock function that simulates building an underlying wallet. */ const mockWalletBuilder = globals_1.jest.fn(async (primaryKey, privilegedKeyManager) => { // Return our mock underlying wallet object. return mockUnderlyingWallet; }); /** * A mock UMPTokenInteractor implementation. * We can track whether buildAndSend is called with the right arguments, etc. */ const mockUMPTokenInteractor = { findByPresentationKeyHash: globals_1.jest.fn(async (hash) => undefined), findByRecoveryKeyHash: globals_1.jest.fn(async (hash) => undefined), buildAndSend: globals_1.jest.fn(async (wallet, admin, token, oldToken) => 'abcd.0') }; /** * A mock "recoveryKeySaver" that claims it always saved the key successfully. */ const mockRecoveryKeySaver = globals_1.jest.fn(async (key) => true); /** * A mock "passwordRetriever" that we can customize to return a specific password * or throw if needed. */ const mockPasswordRetriever = globals_1.jest.fn(async () => 'test-password'); const XOR = (n1, n2) => { if (n1.length !== n2.length) { throw new Error('lengths mismatch'); } const r = new Array(n1.length); for (let i = 0; i < n1.length; i++) { r[i] = n1[i] ^ n2[i]; } return r; }; // Generate some globals const presentationKey = (0, sdk_1.Random)(32); const recoveryKey = (0, sdk_1.Random)(32); const passwordSalt = (0, sdk_1.Random)(32); const passwordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray('test-password', 'utf8'), passwordSalt, CWIStyleWalletManager_1.PBKDF2_NUM_ROUNDS, 32, 'sha512'); const primaryKey = (0, sdk_1.Random)(32); const privilegedKey = (0, sdk_1.Random)(32); /** * A helper function to create a minimal valid UMP token. * This can be used to mock a stored token for existing users. */ async function createMockUMPToken() { const presentationPassword = new sdk_1.SymmetricKey(XOR(presentationKey, passwordKey)); const presentationRecovery = new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey)); const recoveryPassword = new sdk_1.SymmetricKey(XOR(recoveryKey, passwordKey)); const primaryPassword = new sdk_1.SymmetricKey(XOR(primaryKey, passwordKey)); const tempPrivilegedKeyManager = new sdk_2.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(privilegedKey)); return { passwordSalt, passwordPresentationPrimary: presentationPassword.encrypt(primaryKey), passwordRecoveryPrimary: recoveryPassword.encrypt(primaryKey), presentationRecoveryPrimary: presentationRecovery.encrypt(primaryKey), passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey), presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey), presentationHash: sdk_1.Hash.sha256(presentationKey), recoveryHash: sdk_1.Hash.sha256(recoveryKey), presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({ plaintext: presentationKey, protocolID: [2, 'admin key wrapping'], keyID: '1' })).ciphertext, passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({ plaintext: passwordKey, protocolID: [2, 'admin key wrapping'], keyID: '1' })).ciphertext, recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({ plaintext: recoveryKey, protocolID: [2, 'admin key wrapping'], keyID: '1' })).ciphertext, currentOutpoint: 'abcd:0' }; } describe('CWIStyleWalletManager Tests', () => { let manager; beforeEach(() => { // Reset all mock calls globals_1.jest.clearAllMocks(); // We create a new manager for each test, with no initial snapshot manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', // admin originator mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever // no state snapshot ); }); // ---------------------------------------------------------------------------------------- // Private method tests (just to ensure coverage). // We'll call them via (manager as any).somePrivateMethod(...) if needed. // ---------------------------------------------------------------------------------------- test('XOR function: verifies correctness', () => { const fnXOR = manager.XOR; const a = [0x00, 0xff, 0xaa]; const b = [0xff, 0xff, 0x55]; const result = fnXOR(a, b); // 0x00 ^ 0xFF = 0xFF // 0xFF ^ 0xFF = 0x00 // 0xAA ^ 0x55 = 0xFF expect(result).toEqual([0xff, 0x00, 0xff]); }); // ---------------------------------------------------------------------------------------- // Authentication flows // ---------------------------------------------------------------------------------------- describe('New user flow: presentation + password', () => { test('Successfully creates a new token and calls buildAndSend', async () => { // New wallet funder is a mock function const newWalletFunder = globals_1.jest.fn(() => { }); manager.newWalletFunder = newWalletFunder; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); // Provide a presentation key await manager.providePresentationKey(presentationKey); expect(manager.authenticationFlow).toBe('new-user'); // Provide a password mockPasswordRetriever.mockResolvedValueOnce('dummy-password'); await manager.providePassword('dummy-password'); // The wallet should now be built, so manager is authenticated expect(manager.authenticated).toBe(true); // Recovery key saver should have been called expect(mockRecoveryKeySaver).toHaveBeenCalledTimes(1); // The underlying wallet builder should have been called exactly once expect(mockWalletBuilder).toHaveBeenCalledTimes(1); // The manager should have called buildAndSend on the interactor expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(1); const buildArgs = mockUMPTokenInteractor.buildAndSend.mock.calls[0]; // [0] => the wallet, [1] => adminOriginator, [2] => newToken, [3] => oldToken expect(buildArgs[1]).toBe('admin.walletvendor.com'); expect(buildArgs[2]).toHaveProperty('presentationHash'); expect(buildArgs[3]).toBeUndefined(); // Because it's a new user (no old token) expect(newWalletFunder).toHaveBeenCalled(); // New wallet funder should have been called }); test('Throws if user tries to provide recovery key during new-user flow', async () => { // Mark it as new user flow by no token found ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(Array.from({ length: 32 }, () => 1)); await expect(manager.provideRecoveryKey(Array.from({ length: 32 }, () => 2))).rejects.toThrow('Do not submit recovery key in new-user flow'); }); }); describe('Existing user flow: presentation + password', () => { test('Decryption of primary key and building the wallet', async () => { // Provide a mock UMP token const mockToken = await createMockUMPToken(); mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(mockToken); // Provide presentation await manager.providePresentationKey(presentationKey); expect(manager.authenticationFlow).toBe('existing-user'); // Provide password // The manager's internal code will do PBKDF2 with the password + token.passwordSalt // Then XOR that with the presentation key for decryption. await manager.providePassword('test-password'); // Check that manager is authenticated expect(manager.authenticated).toBe(true); // Underlying wallet is built expect(mockWalletBuilder).toHaveBeenCalledTimes(1); }); }); describe('Existing user flow: presentation + recovery key', () => { beforeEach(async () => { manager.authenticationMode = 'presentation-key-and-recovery-key'; manager.authenticationFlow = 'existing-user'; }); test('Successfully decrypts with presentation+recovery', async () => { // Provide a mock UMP token const mockToken = await createMockUMPToken(); mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(mockToken); await manager.providePresentationKey(presentationKey); // Provide the recovery key. // In "presentation-key-and-recovery-key" mode, the manager won't need the password at all. await manager.provideRecoveryKey(recoveryKey); expect(manager.authenticated).toBe(true); expect(mockWalletBuilder).toHaveBeenCalledTimes(1); }); test('Throws if presentation key not provided first', async () => { const recoveryKey = Array.from({ length: 32 }, () => 8); await expect(manager.provideRecoveryKey(recoveryKey)).rejects.toThrow('Provide the presentation key first'); }); }); describe('Existing user flow: recovery key + password', () => { beforeEach(async () => { manager.authenticationMode = 'recovery-key-and-password'; manager.authenticationFlow = 'existing-user'; }); test('Works with correct keys, sets mode as existing-user', async () => { const mockToken = await createMockUMPToken(); mockUMPTokenInteractor.findByRecoveryKeyHash.mockResolvedValueOnce(mockToken); // Provide recovery key await manager.provideRecoveryKey(recoveryKey); // Provide password await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); expect(mockWalletBuilder).toHaveBeenCalledTimes(1); }); test('Throws if no token found by recovery key hash', async () => { ; mockUMPTokenInteractor.findByRecoveryKeyHash.mockResolvedValueOnce(undefined); await expect(manager.provideRecoveryKey(recoveryKey)).rejects.toThrow('No user found with this recovery key'); }); }); // ---------------------------------------------------------------------------------------- // Snapshots // ---------------------------------------------------------------------------------------- describe('saveSnapshot / loadSnapshot', () => { test('Saves a snapshot and can load it into a fresh manager instance', async () => { // We'll do a new user flow so that manager is authenticated with a real token. ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); const presKey = Array.from({ length: 32 }, () => 0xa1); await manager.providePresentationKey(presKey); await manager.providePassword('mypassword'); // triggers creation of new user const snapshot = manager.saveSnapshot(); expect(Array.isArray(snapshot)).toBe(true); expect(snapshot.length).toBeGreaterThan(64); // 32 bytes + encrypted data // Now create a fresh manager: const freshManager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever); // Not authenticated yet await expect(() => freshManager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated'); // Load the snapshot await freshManager.loadSnapshot(snapshot); // The fresh manager is now authenticated (underlying wallet will be built). await expect(freshManager.getPublicKey({ identityKey: true })).resolves.not.toThrow(); // It calls walletBuilder again expect(mockWalletBuilder).toHaveBeenCalledTimes(2); // once for the old manager, once for the fresh }); test('Throws error if saving snapshot while no primary key or token set', async () => { // Manager is not yet authenticated expect(() => manager.saveSnapshot()).toThrow('No root primary key or current UMP token set'); }); test('Throws if snapshot is corrupt or cannot be decrypted', async () => { // Attempt to load an invalid snapshot await expect(() => manager.loadSnapshot([1, 2, 3])).rejects.toThrow('Failed to load snapshot'); }); }); // ---------------------------------------------------------------------------------------- // Changing Keys // ---------------------------------------------------------------------------------------- describe('Change Password', () => { test('Requires authentication and updates the UMP token on-chain', async () => { ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password'); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); await manager.changePassword('new-pass'); expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(2); }); test('Throws if not authenticated', async () => { await expect(manager.changePassword('test-password')).rejects.toThrow('Not authenticated or missing required data.'); }); }); describe('Change Recovery Key', () => { test('Prompts to save the new key, updates the token', async () => { ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password'); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); mockUMPTokenInteractor.buildAndSend.mockResolvedValueOnce(makeOutpoint('rcv1', 0)); await manager.changeRecoveryKey(); // The user is prompted to store the new key expect(mockRecoveryKeySaver).toHaveBeenCalledTimes(2); // once when user created, once after changed // The UMP token is updated expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(2); }); test('Throws if not authenticated', async () => { await expect(manager.changeRecoveryKey()).rejects.toThrow('Not authenticated or missing required data.'); }); }); describe('Change Presentation Key', () => { test('Requires authentication, re-publishes the token, old token consumed', async () => { ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password'); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); mockUMPTokenInteractor.buildAndSend.mockResolvedValueOnce(makeOutpoint('rcv1', 0)); const newPresKey = Array.from({ length: 32 }, () => 0xee); await manager.changePresentationKey(newPresKey); expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(2); }); }); describe('Profile management', () => { test('addProfile adds a new profile and updates the UMP token', async () => { ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); const initialProfiles = manager.listProfiles(); expect(initialProfiles).toHaveLength(1); expect(initialProfiles[0].name).toBe('default'); const getFactorSpy = globals_1.jest.spyOn(manager, 'getFactor').mockImplementation(async () => (0, sdk_1.Random)(32)); mockUMPTokenInteractor.buildAndSend.mockClear(); const newProfileId = await manager.addProfile('Work'); expect(Array.isArray(newProfileId)).toBe(true); expect(newProfileId.length).toBe(16); const updatedProfiles = manager.listProfiles(); expect(updatedProfiles).toHaveLength(2); const workProfile = updatedProfiles.find(p => p.name === 'Work'); expect(workProfile).toBeDefined(); expect(workProfile.active).toBe(false); expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(1); getFactorSpy.mockRestore(); }); test('syncUMPToken refreshes UMP token and profiles from overlay when newer token exists', async () => { ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); const originalToken = manager.currentUMPToken; const rootPrimaryKey = manager.rootPrimaryKey; const extraProfile = { name: 'overlay-profile', id: (0, sdk_1.Random)(16), primaryPad: (0, sdk_1.Random)(32), privilegedPad: (0, sdk_1.Random)(32), createdAt: Math.floor(Date.now() / 1000) }; const profilesJson = JSON.stringify([extraProfile]); const profilesBytes = sdk_1.Utils.toArray(profilesJson, 'utf8'); const profilesEncrypted = new sdk_1.SymmetricKey(rootPrimaryKey).encrypt(profilesBytes); const updatedToken = { ...originalToken, currentOutpoint: makeOutpoint('overlay-tx', 0), profilesEncrypted }; const saveSnapshotSpy = globals_1.jest.spyOn(manager, 'saveSnapshot'); mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(updatedToken); const result = await manager.syncUMPToken(); expect(result).toBe(true); expect(saveSnapshotSpy).toHaveBeenCalled(); saveSnapshotSpy.mockRestore(); const profiles = manager.listProfiles(); expect(profiles.some(p => p.name === 'overlay-profile')).toBe(true); }); }); test('Destroy callback clears sensitive data', async () => { // authenticate as new user ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(Array.from({ length: 32 }, () => 12)); await manager.providePassword('some-pass'); // manager is authenticated expect(manager.authenticated).toBe(true); // Destroy manager.destroy(); expect(manager.authenticated).toBe(false); // And we can confirm that manager won't allow calls await expect(() => manager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated'); }); // ---------------------------------------------------------------------------------------- // Proxies / originator checks // ---------------------------------------------------------------------------------------- describe('Proxy method calls', () => { beforeEach(async () => { // authenticate ; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); }); test('Throws if user is not authenticated', async () => { // force de-auth ; manager.authenticated = false; await expect(() => manager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated.'); }); test('Throws if originator is adminOriginator', async () => { await expect(manager.getPublicKey({ identityKey: true }, 'admin.walletvendor.com')).rejects.toThrow('External applications are not allowed to use the admin originator.'); }); test('Passes if user is authenticated and originator is not admin', async () => { await manager.getPublicKey({ identityKey: true }, 'example.com'); expect(mockUnderlyingWallet.getPublicKey).toHaveBeenCalledTimes(1); }); test('All proxied methods call underlying with correct arguments', async () => { // We'll do a quick spot-check of a few methods: await manager.encrypt({ plaintext: [1, 2, 3], protocolID: [1, 'tests'], keyID: '1' }, 'mydomain.com'); expect(mockUnderlyingWallet.encrypt).toHaveBeenCalledWith({ plaintext: [1, 2, 3], protocolID: [1, 'tests'], keyID: '1' }, 'mydomain.com'); // TODO: Test all other proxied methods }); test('isAuthenticated() rejects if originator is admin, resolves otherwise', async () => { // If admin tries: await expect(manager.isAuthenticated({}, 'admin.walletvendor.com')).rejects.toThrow('External applications are not allowed to use the admin originator.'); // If normal domain: const result = await manager.isAuthenticated({}, 'normal.com'); expect(result).toEqual({ authenticated: true }); }); test('waitForAuthentication() eventually resolves', async () => { // Already authenticated from beforeEach. So it should immediately return. await manager.waitForAuthentication({}, 'normal.com'); expect(mockUnderlyingWallet.waitForAuthentication).toHaveBeenCalledTimes(1); }); }); describe('Additional Tests for Password Retriever Callback, Privileged Key Expiry, and UMP Token Serialization', () => { let manager; beforeEach(() => { globals_1.jest.clearAllMocks(); manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever); }); test('serializeUMPToken and deserializeUMPToken correctly round-trip a UMP token', async () => { const token = await createMockUMPToken(); // We need a token with a currentOutpoint for serialization. expect(token.currentOutpoint).toBeDefined(); const serializeFn = manager.serializeUMPToken; const deserializeFn = manager.deserializeUMPToken; const serialized = serializeFn(token); expect(Array.isArray(serialized)).toBe(true); expect(serialized.length).toBeGreaterThan(0); const deserialized = deserializeFn(serialized); expect(deserialized).toEqual(token); }); test('Password retriever callback: the test function is passed and returns a boolean', async () => { let capturedTestFn = null; const customPasswordRetriever = globals_1.jest.fn(async (reason, testFn) => { capturedTestFn = testFn; // In a real scenario the test function would validate a candidate. // For our test we simply return the correct password. return 'test-password'; }); manager.passwordRetriever = customPasswordRetriever; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); expect(manager.authenticated).toBe(true); // Clear the privileged key so the callback gets ran globals_1.jest.advanceTimersByTime(121000); // Let's trigger a privileged operation await manager.changePassword('test-password'); // trigger some privileged operation... expect(customPasswordRetriever).toHaveBeenCalled(); expect(capturedTestFn).not.toBeNull(); // Since the internal test function is defined inline, we simply check that its output is a boolean. // (Its logic uses the outer scope and may not use its argument correctly, but we verify that it at least returns a boolean.) const testResult = capturedTestFn('any-input'); expect(typeof testResult).toBe('boolean'); expect(capturedTestFn('any-input')).toBe(false); expect(capturedTestFn('test-password')).toBe(true); }); test('Privileged key expiry: each call to decrypt via the privileged manager invokes passwordRetriever', async () => { // In a new-user flow, buildUnderlying is called without a privilegedKey, // so any later use of the privileged manager will trigger a password prompt. const customPasswordRetriever = globals_1.jest.fn(async (reason, testFn) => { return 'test-password'; }); manager.passwordRetriever = customPasswordRetriever; mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined); await manager.providePresentationKey(presentationKey); await manager.providePassword('test-password'); // Clear any calls recorded during authentication. customPasswordRetriever.mockClear(); // Call the underlying privileged key manager’s decrypt twice. // (For example, we use the ciphertext from one of the token’s encrypted fields.) await manager.rootPrivilegedKeyManager.decrypt({ ciphertext: manager.currentUMPToken.passwordKeyEncrypted, protocolID: [2, 'admin key wrapping'], keyID: '1' }); // Key expires after 2 minutes globals_1.jest.advanceTimersByTime(121000); await manager.rootPrivilegedKeyManager.decrypt({ ciphertext: manager.currentUMPToken.passwordKeyEncrypted, protocolID: [2, 'admin key wrapping'], keyID: '1' }); // Since no ephemeral privileged key was provided when building the underlying wallet, // each call to decrypt should have resulted in a call to passwordRetriever. expect(customPasswordRetriever).toHaveBeenCalledTimes(2); }); }); }); //# sourceMappingURL=CWIStyleWalletManager.test.js.map