mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
103 lines
3.8 KiB
TypeScript
103 lines
3.8 KiB
TypeScript
import {
|
|
OWNER_ID,
|
|
PASSWORD,
|
|
deferred,
|
|
installLegacyEncryptedWallet,
|
|
installLegacyOwnerlessResidue,
|
|
mocks,
|
|
} from './wallet.lifecycle.testHarness';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { savedKeyExists, savedKeyOwnerId } from '@/lnurlcash/keys';
|
|
import { readNwcConnections } from '@/lnurlcash/nwc';
|
|
import { readPasskeySlots } from '@/lnurlcash/passkeys';
|
|
import { readTrustedMints } from '@/lnurlcash/trustedMints';
|
|
import { useNwcStore } from './nwc';
|
|
import { useWalletStore } from './wallet';
|
|
|
|
describe('serialized wallet activation', () => {
|
|
it('migrates a proven legacy owner before NWC can observe unlocked', async () => {
|
|
// Given an encrypted legacy wallet with ownerless authorization residue
|
|
await installLegacyEncryptedWallet();
|
|
installLegacyOwnerlessResidue();
|
|
const wallet = useWalletStore();
|
|
useNwcStore();
|
|
|
|
// When password proof unlocks the wallet
|
|
await wallet.unlock(PASSWORD);
|
|
|
|
// Then every legacy namespace belongs to the proven owner before startup
|
|
expect(savedKeyOwnerId()).toBe(OWNER_ID);
|
|
expect(readPasskeySlots()).toHaveLength(1);
|
|
expect(readNwcConnections(OWNER_ID)).toHaveLength(1);
|
|
expect(readTrustedMints(OWNER_ID)).toHaveLength(1);
|
|
expect(wallet.state).toBe('unlocked');
|
|
await vi.waitFor(() => expect(mocks.startService).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
it('serializes a queued create behind an interrupted forget', async () => {
|
|
// Given an unlocked wallet whose NWC drain is deferred
|
|
const wallet = useWalletStore();
|
|
const nwc = useNwcStore();
|
|
await wallet.create(PASSWORD);
|
|
const drain = deferred();
|
|
const stopSpy = vi.spyOn(nwc, 'stop').mockReturnValue(drain.promise);
|
|
|
|
// When forget and create are requested without waiting between them
|
|
const forgetting = wallet.forgetWallet();
|
|
const creating = wallet.create();
|
|
await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled());
|
|
|
|
// Then the successor cannot install until the old owner drain completes,
|
|
// and the session keeps its commit capability while the drain runs
|
|
expect(savedKeyExists()).toBe(true);
|
|
expect(wallet.state).toBe('unlocked');
|
|
drain.resolve();
|
|
await forgetting;
|
|
const phrase = await creating;
|
|
expect(phrase.split(' ')).toHaveLength(12);
|
|
expect(wallet.state).toBe('unlocked');
|
|
});
|
|
|
|
it('drains the active session before file restore reactivation', async () => {
|
|
// Given an unlocked wallet whose NWC drain is deferred
|
|
const wallet = useWalletStore();
|
|
const nwc = useNwcStore();
|
|
await wallet.create(PASSWORD);
|
|
const drain = deferred();
|
|
const stopSpy = vi.spyOn(nwc, 'stop').mockReturnValue(drain.promise);
|
|
|
|
// When a valid file restore starts
|
|
const restoring = wallet.restoreFromBackup({
|
|
type: 'sattle-backup',
|
|
version: 1,
|
|
createdAt: 1,
|
|
bearers: [],
|
|
});
|
|
await vi.waitFor(() => expect(stopSpy).toHaveBeenCalled());
|
|
|
|
// Then deactivation waits for the drain before the lifecycle is
|
|
// invalidated, and reactivation returns to unlocked afterward
|
|
expect(wallet.state).toBe('unlocked');
|
|
drain.resolve();
|
|
await restoring;
|
|
expect(wallet.state).toBe('unlocked');
|
|
});
|
|
|
|
it('serializes current-wallet Nostr restore through full reactivation', async () => {
|
|
// Given an unlocked wallet and a relay restore result
|
|
const wallet = useWalletStore();
|
|
useNwcStore();
|
|
await wallet.create(PASSWORD);
|
|
const ownerId = wallet.pubkey;
|
|
|
|
// When the active-wallet Nostr restore runs
|
|
await wallet.restoreCurrentFromNostr(['wss://relay.example']);
|
|
|
|
// Then the same owner is active only after the restore completed
|
|
expect(mocks.restoreFromNostr).toHaveBeenCalledTimes(1);
|
|
expect(wallet.state).toBe('unlocked');
|
|
expect(wallet.pubkey).toBe(ownerId);
|
|
});
|
|
});
|