@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
299 lines • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const sdk_1 = require("@bsv/sdk");
const ShamirWalletManager_1 = require("../ShamirWalletManager");
const globals_1 = require("@jest/globals");
// Mock WABClient
const mockWABClient = {
startShareAuth: globals_1.jest.fn().mockResolvedValue({ success: true }),
storeShare: globals_1.jest
.fn()
.mockResolvedValue({ success: true, userId: 1 }),
retrieveShare: globals_1.jest.fn().mockResolvedValue({
success: true,
shareB: '1.mockedserversharefromwab.2.test'
}),
updateShare: globals_1.jest
.fn()
.mockResolvedValue({ success: true, shareVersion: 2 }),
deleteShamirUser: globals_1.jest.fn().mockResolvedValue({ success: true })
};
globals_1.jest.mock('../wab-client/WABClient', () => {
return {
WABClient: globals_1.jest.fn().mockImplementation(() => mockWABClient)
};
});
// Mock wallet builder
const mockWallet = {};
const mockWalletBuilder = globals_1.jest.fn(async (privateKey, privilegedKeyManager) => mockWallet);
describe('ShamirWalletManager', () => {
globals_1.jest.spyOn(console, 'warn').mockImplementation((...args) => { });
describe('Configuration', () => {
it('should use default 2-of-3 configuration', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
expect(manager.getThreshold()).toBe(2);
expect(manager.getTotalShares()).toBe(3);
});
it('should accept custom threshold configuration', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 3,
totalShares: 5
});
expect(manager.getThreshold()).toBe(3);
expect(manager.getTotalShares()).toBe(5);
});
it('should reject threshold less than 2', () => {
expect(() => {
new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 1,
totalShares: 3
});
}).toThrow('Threshold must be at least 2');
});
it('should reject totalShares less than 3', () => {
expect(() => {
new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 2,
totalShares: 2
});
}).toThrow('Total shares must be at least 3');
});
it('should reject configurations where user cannot recover independently (custodial)', () => {
// 3-of-4 means user gets 3 shares (4-1), which equals threshold - OK
expect(() => {
new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 3,
totalShares: 4
});
}).not.toThrow();
// 3-of-3 means user gets 2 shares (3-1), which is less than threshold 3 - NOT OK
expect(() => {
new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 3,
totalShares: 3
});
}).toThrow('User must have at least 3 shares to recover independently');
});
});
describe('Entropy Collection', () => {
it('should track entropy collection', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
expect(manager.hasEnoughEntropy()).toBe(false);
// Add a sample
const progress = manager.addMouseEntropy(100, 200);
// First sample should be accepted
expect(progress).not.toBeNull();
expect(manager.getEntropyProgress().collected).toBeGreaterThan(0);
});
it('should reset entropy', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
// Add at least one sample
manager.addMouseEntropy(100, 200);
expect(manager.getEntropyProgress().collected).toBeGreaterThan(0);
manager.resetEntropy();
expect(manager.getEntropyProgress().collected).toBe(0);
});
});
describe('User ID Hash', () => {
it('should allow setting user ID hash', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
expect(manager.getUserIdHash()).toBeUndefined();
manager.setUserIdHash('abc123');
expect(manager.getUserIdHash()).toBe('abc123');
});
});
describe('Share Validation', () => {
it('should validate share format with 4 parts', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
// This should fail because the share format is invalid
await expect(manager.recoverWithUserShares(['invalid-share', 'also-invalid'])).rejects.toThrow('Invalid share format');
});
it('should reject shares with threshold less than 2', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
await expect(manager.recoverWithUserShares(['1.data.1.check', '2.data.1.check'])).rejects.toThrow('Invalid share: threshold must be at least 2');
});
});
describe('Recovery', () => {
it('should require enough shares for recovery', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
// Only 1 share but threshold encoded in share is 2
await expect(manager.recoverWithUserShares(['1.somedata.2.check'])).rejects.toThrow('Need at least 2 shares to recover');
});
it('should require userIdHash for server share recovery', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
await expect(manager.recoverWithServerShare(['1.somedata.2.check'], { otp: '123456' })).rejects.toThrow('User ID hash not set');
});
});
describe('Wallet Building', () => {
it('should fail to build wallet without private key', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
await expect(manager.buildWallet()).rejects.toThrow('No private key available');
});
it('should fail to get wallet without building', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
expect(() => manager.getWallet()).toThrow('Wallet not built');
});
it('should report no private key initially', () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
expect(manager.hasPrivateKey()).toBe(false);
});
});
describe('Wallet Creation', () => {
it('should create wallet and return correct number of user shares', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 2,
totalShares: 3
});
// Collect enough entropy
for (let i = 0; i < 256; i++) {
manager.addMouseEntropy(Math.random() * 1000, Math.random() * 1000);
}
let capturedShares = [];
const result = await manager.createNewWallet({ otp: '123456' }, async (userShares, threshold, totalShares) => {
capturedShares = userShares;
expect(threshold).toBe(2);
expect(totalShares).toBe(3);
return true;
});
// Should get 2 user shares (totalShares - 1 for server)
expect(result.userShares.length).toBe(2);
expect(capturedShares.length).toBe(2);
expect(result.threshold).toBe(2);
expect(result.totalShares).toBe(3);
expect(result.userIdHash).toBeDefined();
expect(result.privateKey).toBeInstanceOf(sdk_1.PrivateKey);
expect(manager.hasPrivateKey()).toBe(true);
});
it('should return more user shares for higher totalShares', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder,
threshold: 3,
totalShares: 5
});
for (let i = 0; i < 256; i++) {
manager.addMouseEntropy(Math.random() * 1000, Math.random() * 1000);
}
const result = await manager.createNewWallet({ otp: '123456' }, async (userShares) => {
expect(userShares.length).toBe(4); // 5 - 1 = 4 user shares
return true;
});
expect(result.userShares.length).toBe(4);
});
});
describe('Account Deletion', () => {
it('should require userIdHash for deletion', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
await expect(manager.deleteAccount({ otp: '123456' })).rejects.toThrow('User ID hash not set');
});
it('should clear state after deletion', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
// Collect entropy and create wallet
for (let i = 0; i < 256; i++) {
manager.addMouseEntropy(Math.random() * 1000, Math.random() * 1000);
}
await manager.createNewWallet({ otp: '123456' }, async () => true);
expect(manager.hasPrivateKey()).toBe(true);
expect(manager.getUserIdHash()).toBeDefined();
// Delete
await manager.deleteAccount({ otp: '654321' });
expect(manager.hasPrivateKey()).toBe(false);
expect(manager.getUserIdHash()).toBeUndefined();
});
});
describe('Key Rotation', () => {
it('should require userIdHash for rotation', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
// Collect entropy
for (let i = 0; i < 256; i++) {
manager.addMouseEntropy(Math.random() * 1000, Math.random() * 1000);
}
await expect(manager.rotateKeys({ otp: '123456' }, async () => true)).rejects.toThrow('User ID hash not set');
});
it('should require entropy for rotation', async () => {
const manager = new ShamirWalletManager_1.ShamirWalletManager({
wabServerUrl: 'https://test.example.com',
authMethodType: 'DevConsole',
walletBuilder: mockWalletBuilder
});
manager.setUserIdHash('abc123');
await expect(manager.rotateKeys({ otp: '123456' }, async () => true)).rejects.toThrow('Collect entropy before key rotation');
});
});
});
//# sourceMappingURL=ShamirWalletManager.test.js.map