182 lines
6.6 KiB
TypeScript
182 lines
6.6 KiB
TypeScript
/**
|
|
* test-wallet-store.ts — Test encrypted client-side wallet store.
|
|
*
|
|
* Mocks localStorage since we're running in Bun (not a browser).
|
|
*
|
|
* Usage:
|
|
* bun run scripts/test-wallet-store.ts
|
|
*/
|
|
|
|
// Mock localStorage before importing anything
|
|
const storage = new Map<string, string>();
|
|
(globalThis as any).localStorage = {
|
|
getItem: (k: string) => storage.get(k) ?? null,
|
|
setItem: (k: string, v: string) => storage.set(k, v),
|
|
removeItem: (k: string) => storage.delete(k),
|
|
clear: () => storage.clear(),
|
|
};
|
|
|
|
import { WalletStore } from '../src/encryptid/wallet-store';
|
|
import { EncryptIDKeyManager } from '../src/encryptid/key-derivation';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition: boolean, msg: string) {
|
|
if (condition) {
|
|
console.log(` ✓ ${msg}`);
|
|
passed++;
|
|
} else {
|
|
console.error(` ✗ ${msg}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('=== Wallet Store Tests ===\n');
|
|
|
|
// Set up key manager with a test PRF
|
|
const prfOutput = new ArrayBuffer(32);
|
|
new Uint8Array(prfOutput).set(Array.from({ length: 32 }, (_, i) => i + 1));
|
|
|
|
const km = new EncryptIDKeyManager();
|
|
await km.initFromPRF(prfOutput);
|
|
const keys = await km.getKeys();
|
|
|
|
const store = new WalletStore(keys.encryptionKey);
|
|
|
|
// Test 1: Empty store
|
|
console.log('[1] Empty store');
|
|
const empty = await store.list();
|
|
assert(empty.length === 0, 'No wallets initially');
|
|
const noDefault = await store.getDefault();
|
|
assert(noDefault === null, 'No default wallet');
|
|
|
|
// Test 2: Add first wallet (auto-default)
|
|
console.log('\n[2] Add first wallet');
|
|
const w1 = await store.add({
|
|
safeAddress: '0x1111111111111111111111111111111111111111',
|
|
chainId: 84532,
|
|
eoaAddress: keys.eoaAddress!,
|
|
label: 'Test Treasury',
|
|
});
|
|
assert(w1.id.length > 0, 'Has UUID');
|
|
assert(w1.safeAddress === '0x1111111111111111111111111111111111111111', 'Address correct');
|
|
assert(w1.chainId === 84532, 'Chain correct');
|
|
assert(w1.isDefault === true, 'First wallet is auto-default');
|
|
assert(w1.label === 'Test Treasury', 'Label correct');
|
|
assert(w1.addedAt > 0, 'Has timestamp');
|
|
|
|
// Test 3: Data is encrypted in localStorage
|
|
console.log('\n[3] Encrypted at rest');
|
|
const raw = storage.get('encryptid_wallets');
|
|
assert(raw !== undefined, 'Data exists in localStorage');
|
|
const blob = JSON.parse(raw!);
|
|
assert(typeof blob.c === 'string', 'Has ciphertext field');
|
|
assert(typeof blob.iv === 'string', 'Has IV field');
|
|
assert(!raw!.includes('Treasury'), 'Label NOT in plaintext');
|
|
assert(!raw!.includes('1111111'), 'Address NOT in plaintext');
|
|
|
|
// Test 4: Add second wallet
|
|
console.log('\n[4] Add second wallet');
|
|
const w2 = await store.add({
|
|
safeAddress: '0x2222222222222222222222222222222222222222',
|
|
chainId: 8453,
|
|
eoaAddress: keys.eoaAddress!,
|
|
label: 'Mainnet Safe',
|
|
});
|
|
assert(w2.isDefault === false, 'Second wallet is not default');
|
|
const all = await store.list();
|
|
assert(all.length === 2, 'Now have 2 wallets');
|
|
|
|
// Test 5: Get default
|
|
console.log('\n[5] Get default');
|
|
const def = await store.getDefault();
|
|
assert(def !== null, 'Has default');
|
|
assert(def!.id === w1.id, 'First wallet is still default');
|
|
|
|
// Test 6: Get by address + chain
|
|
console.log('\n[6] Get by address + chain');
|
|
const found = await store.get('0x2222222222222222222222222222222222222222', 8453);
|
|
assert(found !== null, 'Found by address+chain');
|
|
assert(found!.label === 'Mainnet Safe', 'Correct wallet');
|
|
const notFound = await store.get('0x2222222222222222222222222222222222222222', 1);
|
|
assert(notFound === null, 'Not found on wrong chain');
|
|
|
|
// Test 7: Update label and default
|
|
console.log('\n[7] Update');
|
|
const updated = await store.update(w2.id, { label: 'Base Mainnet', isDefault: true });
|
|
assert(updated !== null, 'Update succeeded');
|
|
assert(updated!.label === 'Base Mainnet', 'Label updated');
|
|
assert(updated!.isDefault === true, 'Now default');
|
|
const newDefault = await store.getDefault();
|
|
assert(newDefault!.id === w2.id, 'Default switched');
|
|
// Old default should be false now
|
|
const allAfter = await store.list();
|
|
const oldW1 = allAfter.find(w => w.id === w1.id);
|
|
assert(oldW1!.isDefault === false, 'Old default cleared');
|
|
|
|
// Test 8: Duplicate add = upsert
|
|
console.log('\n[8] Duplicate add (upsert)');
|
|
const w1Updated = await store.add({
|
|
safeAddress: '0x1111111111111111111111111111111111111111',
|
|
chainId: 84532,
|
|
eoaAddress: keys.eoaAddress!,
|
|
label: 'Renamed Treasury',
|
|
});
|
|
assert(w1Updated.label === 'Renamed Treasury', 'Label updated via upsert');
|
|
const afterUpsert = await store.list();
|
|
assert(afterUpsert.length === 2, 'Still 2 wallets (not 3)');
|
|
|
|
// Test 9: Persistence — new store instance reads encrypted data
|
|
console.log('\n[9] Persistence across instances');
|
|
const store2 = new WalletStore(keys.encryptionKey);
|
|
const restored = await store2.list();
|
|
assert(restored.length === 2, 'Restored 2 wallets');
|
|
assert(restored.some(w => w.label === 'Renamed Treasury'), 'Labels preserved');
|
|
assert(restored.some(w => w.label === 'Base Mainnet'), 'Both wallets present');
|
|
|
|
// Test 10: Wrong key can't decrypt
|
|
console.log('\n[10] Wrong key fails gracefully');
|
|
const km2 = new EncryptIDKeyManager();
|
|
const otherPrf = new ArrayBuffer(32);
|
|
new Uint8Array(otherPrf).set(Array.from({ length: 32 }, (_, i) => 255 - i));
|
|
await km2.initFromPRF(otherPrf);
|
|
const otherKeys = await km2.getKeys();
|
|
const storeWrongKey = new WalletStore(otherKeys.encryptionKey);
|
|
const wrongResult = await storeWrongKey.list();
|
|
assert(wrongResult.length === 0, 'Wrong key returns empty (graceful failure)');
|
|
km2.clear();
|
|
|
|
// Test 11: Remove wallet
|
|
console.log('\n[11] Remove');
|
|
const removed = await store.remove(w2.id);
|
|
assert(removed === true, 'Remove succeeded');
|
|
const afterRemove = await store.list();
|
|
assert(afterRemove.length === 1, 'Down to 1 wallet');
|
|
// Removed wallet was default, so remaining should be promoted
|
|
assert(afterRemove[0].isDefault === true, 'Remaining wallet promoted to default');
|
|
|
|
// Test 12: Remove non-existent
|
|
const removedAgain = await store.remove('nonexistent-id');
|
|
assert(removedAgain === false, 'Remove non-existent returns false');
|
|
|
|
// Test 13: Clear
|
|
console.log('\n[13] Clear');
|
|
await store.clear();
|
|
const afterClear = await store.list();
|
|
assert(afterClear.length === 0, 'Empty after clear');
|
|
assert(!storage.has('encryptid_wallets'), 'localStorage key removed');
|
|
|
|
// Cleanup
|
|
km.clear();
|
|
|
|
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Fatal:', err);
|
|
process.exit(1);
|
|
});
|