Add detailed planning documents for multi-account, multi-login method support in Grimoire: - MULTI_ACCOUNT_LOGIN_PLAN.md: Complete implementation plan covering read-only accounts (npub/nip-05/hex/nprofile), NIP-07 browser extensions, NIP-46 remote signers (bunker URLs), and NIP-55 Android signers. Includes current state analysis, architecture design, login flows, commands specification, UI components, and 4-phase implementation roadmap. - MULTI_ACCOUNT_IMPLEMENTATION_PHASES.md: Step-by-step implementation guide with specific files to create/modify for each phase, testing checklists, and timeline estimates. Breaks down into Phase 1 (read-only accounts), Phase 2 (management UI), Phase 3 (NIP-46), and Phase 4 (NIP-55). - MULTI_ACCOUNT_ARCHITECTURE.md: Visual architecture diagrams and data flow documentation. Covers system architecture, state management, persistence layer, component hierarchy, security boundaries, and performance considerations. The plan integrates with existing applesauce-accounts AccountManager and applesauce-signers library, with special focus on NIP-46 relay pool integration for remote signing.
11 KiB
Multi-Account Implementation Phases
Quick reference for implementing multi-account support in Grimoire.
Phase 1: Read-Only Accounts (Week 1)
Files to Create
-
src/lib/account-types.ts- Account class implementationsexport class ReadOnlyAccount implements Account { // Factory methods: fromNpub, fromNip05, fromHex, fromNprofile } -
src/lib/login-parser.ts- Input detection and parsingexport function detectLoginInputType(input: string): LoginInputType export async function createAccountFromInput(input: string) -
src/components/LoginDialog.tsx- Login method selection UI- Method buttons (Extension, Read-Only, Remote, Android)
- Smart input field with auto-detection
- Error handling
Files to Modify
-
src/services/accounts.ts- Register new account typesaccountManager.registerAccountType("readonly", ReadOnlyAccount); -
src/types/man.ts- Add login commandlogin: { appId: "login-dialog", argParser: parseLoginCommand, // ... } -
src/components/nostr/user-menu.tsx- Show all accounts- Subscribe to
accounts.accounts$for all accounts - Add click handler to switch accounts
- Add "Add Account" button
- Subscribe to
Testing Tasks
login npub1...creates read-only accountlogin alice@nostr.comresolves NIP-05 and creates accountlogin <hex>creates read-only accountlogin nprofile1...creates account with relay hints- Account switching updates active state
- Accounts persist across page reload
Phase 2: Account Management UI (Week 2)
Files to Create
-
src/components/AccountManager.tsx- Full account management window- List all accounts with type badges
- Switch, remove, edit label actions
- Connection status for NIP-46
- Add account button
-
src/components/ui/account-badge.tsx- Account type badge component- Extension icon (🔌)
- Read-only icon (👁️)
- Remote signer icon (🔗)
- Android icon (🤖)
Files to Modify
-
src/types/man.ts- Add accounts and logout commandsaccounts: { appId: "account-manager", // ... }, logout: { argParser: parseLogoutCommand, // ... } -
src/core/state.ts- Add account metadata fieldsactiveAccount?: { // ... existing fields accountType: 'readonly' | 'extension' | 'remote' | 'android'; label?: string; } -
src/hooks/useAccountSync.ts- Sync account type and label
Testing Tasks
/accountsopens management window- Can switch accounts from manager
- Can remove accounts (with confirmation)
- Can edit account labels
- Account type badges display correctly
/logoutremoves active account
Phase 3: NIP-46 Remote Signer (Week 3)
Files to Create
src/lib/bunker-url.ts- Bunker URL parsing utilitiesexport function parseBunkerUrl(url: string) export function isValidBunkerUrl(url: string): boolean
Files to Modify
-
src/lib/account-types.ts- Add RemoteSignerAccountexport class RemoteSignerAccount implements Account { signer: Nip46Signer; // Connection lifecycle management static async fromBunkerUrl(url: string) } -
src/services/accounts.ts- Initialize NIP-46 connectionsfunction initializeRemoteSigners() { // Connect all NIP-46 signers on app start } -
src/lib/login-parser.ts- Handle bunker URLscase 'bunker': return await RemoteSignerAccount.fromBunkerUrl(input); -
src/components/LoginDialog.tsx- Show connection status- Loading indicator during connection
- Success/error feedback
- Relay status
-
src/components/AccountManager.tsx- Connection indicators- 🟢 Connected
- 🔴 Disconnected
- 🟡 Connecting
Integration Tasks
- Hook Nip46Signer with singleton relay pool
- Monitor connection status via observables
- Auto-reconnect on disconnect
- Handle connection errors gracefully
- Clean up relay subscriptions on account removal
Testing Tasks
login bunker://...connects to remote signer- Can sign events with remote signer
- Connection status updates in real-time
- Reconnects after page reload
- Handles relay failures gracefully
- Cleans up connections on logout
Phase 4: NIP-55 Android Signer (Future)
Research Phase
- Study NIP-55 specification
- Find reference implementations
- Test with Android signer apps
- Design intent/deep link flow
Files to Create
src/lib/account-types.ts- AndroidSignerAccountsrc/lib/nip55-handler.ts- Android intent handlingsrc/components/AndroidSignerSetup.tsx- QR code / deep link UI
Testing Tasks
- Generate signing request intent
- Handle response from Android app
- Sign events via Android signer
- Handle errors and timeouts
Critical Implementation Notes
1. Relay Pool Integration (NIP-46)
NIP-46 signers need to communicate with the relay pool. Two approaches:
Option A: Separate Pool (Recommended)
// Each Nip46Signer maintains its own relay connections
const signer = new Nip46Signer({
remotePubkey,
relays,
// Creates internal relay connections
});
Option B: Shared Pool
import pool from "@/services/relay-pool";
const signer = new Nip46Signer({
remotePubkey,
relays,
pool, // Pass singleton pool
});
Decision: Check applesauce-signers API - use Option B if supported, otherwise Option A.
2. Account Serialization
When saving to localStorage, NIP-46 accounts need special handling:
// Save
toJSON() {
return {
id: this.id,
pubkey: this.pubkey,
metadata: {
type: 'remote',
relays: this.metadata.relays,
remotePubkey: this.metadata.remotePubkey,
// DON'T save signer instance or connection secrets
}
};
}
// Load
static fromJSON(data: any) {
// Recreate signer from metadata
const signer = new Nip46Signer({
remotePubkey: data.metadata.remotePubkey,
relays: data.metadata.relays,
});
const account = new RemoteSignerAccount(/* ... */);
// Connect asynchronously after creation
account.connect().catch(console.error);
return account;
}
3. Error Handling Patterns
Read-Only Signing Attempt:
async function publishNote(content: string) {
const account = accountManager.active;
if (!account?.signer) {
toast.error("Cannot sign", {
description: "This is a read-only account. Add a signing account to publish.",
action: {
label: "Add Account",
onClick: () => openLoginDialog()
}
});
return;
}
// Proceed with signing
}
NIP-46 Connection Failure:
async function connectRemoteSigner(account: RemoteSignerAccount) {
try {
await account.signer.connect();
toast.success("Connected to remote signer");
} catch (error) {
toast.error("Connection failed", {
description: error.message,
action: {
label: "Retry",
onClick: () => connectRemoteSigner(account)
}
});
}
}
4. Account Sync Hook Enhancement
// src/hooks/useAccountSync.ts
export function useAccountSync() {
const { setActiveAccount } = useGrimoire();
const eventStore = useEventStore();
useEffect(() => {
const sub = accountManager.active$.subscribe(async (account) => {
if (!account) {
setActiveAccount(undefined);
return;
}
// Get account type from metadata
const accountType = account.metadata?.type || 'extension';
// Load relays from relay list cache or NIP-65
const relays = await loadRelaysForPubkey(account.pubkey, eventStore);
setActiveAccount({
pubkey: account.pubkey,
relays,
accountType,
label: account.metadata?.label,
});
});
return () => sub.unsubscribe();
}, [setActiveAccount, eventStore]);
}
Testing Strategy
Unit Tests
Create test files alongside implementation:
src/lib/account-types.test.tssrc/lib/login-parser.test.tssrc/lib/bunker-url.test.ts
Integration Tests
Test account lifecycle:
describe("Account Management", () => {
it("should add read-only account", async () => {
const account = await createAccountFromInput("npub1...");
accountManager.addAccount(account);
expect(accountManager.accounts.length).toBe(1);
});
it("should switch accounts", () => {
const account1 = /* ... */;
const account2 = /* ... */;
accountManager.addAccount(account1);
accountManager.addAccount(account2);
accountManager.setActive(account2);
expect(accountManager.active).toBe(account2);
});
});
Manual Testing Checklist
Phase 1:
- Login with npub works
- Login with NIP-05 works
- Login with hex works
- Login with nprofile works
- Can switch between accounts
- Accounts persist after reload
- Read-only accounts cannot sign
Phase 2:
- Account manager shows all accounts
- Can remove accounts
- Can edit labels
- Type badges display correctly
- Active account highlighted
Phase 3:
- Bunker URL parsing works
- Remote signer connects
- Can sign with remote signer
- Connection status accurate
- Reconnects after reload
- Handles disconnects gracefully
Quick Start Guide
To implement Phase 1 today:
-
Create account types:
# Create the file touch src/lib/account-types.ts # Implement ReadOnlyAccount class -
Create login parser:
touch src/lib/login-parser.ts # Implement detectLoginInputType and createAccountFromInput -
Add login command:
// In src/types/man.ts login: { description: "Add a new account", argParser: async (args) => { const input = args.join(' ').trim(); if (!input) return { showDialog: true }; const account = await createAccountFromInput(input); return { account }; }, // ... } -
Test it:
npm run dev # In app: login npub1...
Estimated Timeline
- Phase 1: 3-5 days (read-only accounts + basic switching)
- Phase 2: 2-3 days (account management UI)
- Phase 3: 5-7 days (NIP-46 integration + testing)
- Phase 4: TBD (future)
Total: ~2 weeks for full multi-account support with NIP-46.
Next Steps
- Start with Phase 1: Read-only accounts
- Create PR: Get feedback on architecture
- Iterate: Based on testing and feedback
- Phase 2: Add management UI
- Phase 3: NIP-46 integration
- Polish: UX improvements and edge cases
Let's build this step by step! 🚀