fix: Build errors in inbox and gift wrap implementation

Fixed TypeScript compilation errors and updated API usage:

**Type Fixes:**
- Changed `Signer` to `ISigner` (applesauce-signers v5 API)
- Fixed InboxViewer to use proper state management methods
- Removed unused imports (useEffect, Badge, db)

**State Management:**
- Added `setPrivateMessagesEnabled()` to Logic
- Added `setAutoDecryptGiftWraps()` to Logic
- Exposed both methods in useGrimoire hook
- Updated InboxViewer to use new setters instead of setState

**Relay Pool API:**
- Replaced `pool.subscribe()` with `createTimelineLoader()`
- Updated gift-wrap-loader to use applesauce-loaders pattern
- Proper Observable subscription with next/error/complete

**Test Fixes:**
- Updated all test calls to match new `unwrapAndUnseal()` signature
- Removed unused `beforeEach` import
- Fixed all instances of 3-arg calls to 2-arg calls

All changes maintain the same functionality while conforming to the
correct applesauce v5 and TypeScript APIs.
This commit is contained in:
Claude
2026-01-15 21:29:34 +00:00
parent 1b61b59f85
commit 69ed479697
6 changed files with 106 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import {
Mail,
@@ -14,15 +14,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Label } from "./ui/label";
import { Switch } from "./ui/switch";
import { Badge } from "./ui/badge";
import giftWrapLoader from "@/services/gift-wrap-loader";
import db from "@/services/db";
import { toast } from "sonner";
import { use$ } from "applesauce-react/hooks";
import accounts from "@/services/accounts";
export function InboxViewer() {
const { state, setState } = useGrimoire();
const { state, setPrivateMessagesEnabled, setAutoDecryptGiftWraps } =
useGrimoire();
const activeAccount = use$(accounts.active$);
const [isDecrypting, setIsDecrypting] = useState(false);
const [decryptResult, setDecryptResult] = useState<{
@@ -54,10 +53,7 @@ export function InboxViewer() {
}, [activeAccount?.pubkey]);
const handleTogglePrivateMessages = (enabled: boolean) => {
setState((prev) => ({
...prev,
privateMessagesEnabled: enabled,
}));
setPrivateMessagesEnabled(enabled);
if (enabled) {
toast.success("Private messages enabled");
@@ -67,10 +63,7 @@ export function InboxViewer() {
};
const handleToggleAutoDecrypt = (enabled: boolean) => {
setState((prev) => ({
...prev,
autoDecryptGiftWraps: enabled,
}));
setAutoDecryptGiftWraps(enabled);
if (enabled) {
toast.success("Auto-decrypt enabled");

View File

@@ -526,3 +526,29 @@ export const clearActiveSpellbook = (state: GrimoireState): GrimoireState => {
activeSpellbook: undefined,
};
};
/**
* Enables or disables NIP-59 private messages (gift wraps).
*/
export const setPrivateMessagesEnabled = (
state: GrimoireState,
enabled: boolean,
): GrimoireState => {
return {
...state,
privateMessagesEnabled: enabled,
};
};
/**
* Enables or disables auto-decrypt for gift wraps.
*/
export const setAutoDecryptGiftWraps = (
state: GrimoireState,
enabled: boolean,
): GrimoireState => {
return {
...state,
autoDecryptGiftWraps: enabled,
};
};

View File

@@ -319,6 +319,20 @@ export const useGrimoire = () => {
[setState],
);
const setPrivateMessagesEnabled = useCallback(
(enabled: boolean) => {
setState((prev) => Logic.setPrivateMessagesEnabled(prev, enabled));
},
[setState],
);
const setAutoDecryptGiftWraps = useCallback(
(enabled: boolean) => {
setState((prev) => Logic.setAutoDecryptGiftWraps(prev, enabled));
},
[setState],
);
const loadSpellbook = useCallback(
(spellbook: ParsedSpellbook) => {
setState((prev) => SpellbookManager.loadSpellbook(prev, spellbook));
@@ -366,6 +380,8 @@ export const useGrimoire = () => {
updateWorkspaceLabel,
reorderWorkspaces,
setCompactModeKinds,
setPrivateMessagesEnabled,
setAutoDecryptGiftWraps,
loadSpellbook,
clearActiveSpellbook,
switchToTemporary,

View File

@@ -17,7 +17,8 @@
import { BehaviorSubject, Observable } from "rxjs";
import type { NostrEvent } from "@/types/nostr";
import type { Signer } from "applesauce-signers";
import type { ISigner } from "applesauce-signers";
import { createTimelineLoader } from "applesauce-loaders/loaders";
import pool from "./relay-pool";
import eventStore from "./event-store";
import { relayListCache } from "./relay-list-cache";
@@ -49,7 +50,7 @@ class GiftWrapLoader {
});
private subscription?: { unsubscribe: () => void };
private currentSigner?: Signer;
private currentSigner?: ISigner;
/**
* Observable state of the loader
@@ -74,7 +75,7 @@ class GiftWrapLoader {
*/
async enable(
recipientPubkey: string,
signer: Signer,
signer: ISigner,
autoDecrypt = false,
): Promise<void> {
// Stop any existing subscription
@@ -153,34 +154,42 @@ class GiftWrapLoader {
`[GiftWrapLoader] Syncing from ${inboxRelays.length} inbox relays`,
);
// Subscribe to kind 1059 events for this user
// Subscribe to kind 1059 events for this user using timeline loader
const filter = {
kinds: [1059],
kinds: [1059 as number],
"#p": [state.recipientPubkey],
// Optionally add since: to only get new messages
// since: state.lastSync ? Math.floor(state.lastSync / 1000) : undefined,
};
// Store subscription for cleanup
this.subscription = pool.subscribe(inboxRelays, [filter], {
onevent: async (event: NostrEvent) => {
await this.handleGiftWrap(event);
// Create timeline loader for gift wraps
const timeline = createTimelineLoader(pool, {
eventStore,
relays: inboxRelays,
filters: [filter],
});
// Subscribe to timeline
this.subscription = timeline.subscribe({
next: (event: NostrEvent) => {
void this.handleGiftWrap(event);
},
oneose: () => {
console.log("[GiftWrapLoader] EOSE received");
error: (error: Error) => {
console.error("[GiftWrapLoader] Timeline error:", error);
this.state$.next({
...this.state$.value,
loading: false,
errorCount: this.state$.value.errorCount + 1,
});
},
complete: () => {
console.log("[GiftWrapLoader] Timeline complete");
this.state$.next({
...this.state$.value,
loading: false,
lastSync: Date.now(),
});
},
onclose: (reason: string) => {
console.log(`[GiftWrapLoader] Subscription closed: ${reason}`);
this.state$.next({
...this.state$.value,
loading: false,
});
},
});
// Process any pending gift wraps from database

View File

@@ -2,13 +2,13 @@
* Tests for NIP-59 Gift Wrap Service
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { unwrapAndUnseal, GiftWrapError } from "./gift-wrap";
import type { NostrEvent } from "@/types/nostr";
import type { Signer } from "applesauce-signers";
import type { ISigner } from "applesauce-signers";
// Mock signer for testing
function createMockSigner(decryptResponses: Map<string, string>): Signer & {
function createMockSigner(decryptResponses: Map<string, string>): ISigner & {
nip44Decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
} {
return {
@@ -195,13 +195,13 @@ describe("unwrapAndUnseal", () => {
const signer = createMockSigner(decryptResponses);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow(GiftWrapError);
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
GiftWrapError,
);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow("Expected seal kind 13");
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
"Expected seal kind 13",
);
});
it("should reject seal with empty content", async () => {
@@ -231,13 +231,13 @@ describe("unwrapAndUnseal", () => {
const signer = createMockSigner(decryptResponses);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow(GiftWrapError);
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
GiftWrapError,
);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow("Seal content is empty");
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
"Seal content is empty",
);
});
it("should reject invalid rumor structure", async () => {
@@ -275,13 +275,13 @@ describe("unwrapAndUnseal", () => {
const signer = createMockSigner(decryptResponses);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow(GiftWrapError);
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
GiftWrapError,
);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow("Rumor missing content");
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
"Rumor missing content",
);
});
it("should handle decryption failures", async () => {
@@ -298,13 +298,13 @@ describe("unwrapAndUnseal", () => {
// No responses configured - decryption will fail
const signer = createMockSigner(new Map());
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow(GiftWrapError);
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
GiftWrapError,
);
await expect(
unwrapAndUnseal(giftWrap, "recipient-pubkey", signer),
).rejects.toThrow("Failed to decrypt");
await expect(unwrapAndUnseal(giftWrap, signer)).rejects.toThrow(
"Failed to decrypt",
);
});
});
});

View File

@@ -13,7 +13,7 @@
*/
import type { NostrEvent } from "@/types/nostr";
import type { Signer } from "applesauce-signers";
import type { ISigner } from "applesauce-signers";
import db, {
GiftWrapEnvelope,
DecryptedRumor,
@@ -117,7 +117,7 @@ function validateRumor(event: any): NostrEvent {
*/
async function unwrapGiftWrap(
giftWrap: NostrEvent,
signer: Signer,
signer: ISigner,
): Promise<NostrEvent> {
validateGiftWrap(giftWrap);
@@ -161,7 +161,7 @@ async function unwrapGiftWrap(
*/
async function unsealSeal(
seal: NostrEvent,
signer: Signer,
signer: ISigner,
): Promise<NostrEvent> {
validateSeal(seal);
@@ -208,7 +208,7 @@ async function unsealSeal(
*/
export async function unwrapAndUnseal(
giftWrap: NostrEvent,
signer: Signer,
signer: ISigner,
): Promise<{ seal: NostrEvent; rumor: NostrEvent }> {
// Step 1: Unwrap gift wrap to get seal
const seal = await unwrapGiftWrap(giftWrap, signer);
@@ -230,7 +230,7 @@ export async function unwrapAndUnseal(
export async function processGiftWrap(
giftWrap: NostrEvent,
recipientPubkey: string,
signer: Signer,
signer: ISigner,
): Promise<DecryptedRumor | null> {
// Check if already processed
const existing = await db.giftWraps.get(giftWrap.id);