import type NDK from "@nostr-dev-kit/ndk"; import { type NDKEncryptionScheme, type NDKSigner, NDKUser, type NostrEvent } from "@nostr-dev-kit/ndk"; import { registerSigner } from "@nostr-dev-kit/ndk"; import * as Nip55 from "expo-nip55"; export type NDKNip55Permission = Nip55.Permission; export class NDKNip55Signer implements NDKSigner { private _pubkey: string; private _user?: NDKUser; public packageName: string; private ndk?: NDK; public permissions: NDKNip55Permission[]; constructor(packageName: string, ndk?: NDK, permissions: NDKNip55Permission[] = []) { this.packageName = packageName; this.ndk = ndk; this.permissions = permissions; } /** * Blocks until the signer is ready and returns the associated NDKUser. * @returns A promise that resolves to the NDKUser instance. */ async blockUntilReady(): Promise { if (this._user) return this._user; await Nip55.setPackageName(this.packageName); const data = await Nip55.getPublicKey(this.packageName, this.permissions); if (!data) throw new Error("No signer available found"); this._user = new NDKUser({ npub: data.npub }); this._pubkey = this._user.pubkey; return this._user; } /** * Getter for the user property. * @returns A promise that resolves to the NDKUser instance. */ async user(): Promise { return this.blockUntilReady(); } public get userSync(): NDKUser { if (!this._user) throw new Error("User not ready"); return this._user; } public get pubkey(): string { if (!this._pubkey) throw new Error("Pubkey not ready"); return this._pubkey; } /** * Signs the given Nostr event. * @param event - The Nostr event to be signed. * @returns A promise that resolves to the signature of the signed event. */ async sign(event: NostrEvent): Promise { const result = await Nip55.signEvent(this.packageName, JSON.stringify(event), event.id, this._pubkey); return result.signature; } /** * Determine the types of encryption (by nip) that this signer can perform. * Implementing classes SHOULD return a value even for legacy (only nip04) third party signers. * @nip Optionally returns an array with single supported nip or empty, to check for truthy or falsy. * @return A promised list of any (or none) of these strings ['nip04', 'nip44'] */ async encryptionEnabled?(scheme?: NDKEncryptionScheme): Promise { if (scheme) return [scheme]; return Promise.resolve(["nip04", "nip44"]); } /** * Encrypts the given Nostr event for the given recipient. * Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined * @param recipient - The recipient (pubkey or conversationKey) of the encrypted value. * @param value - The value to be encrypted. * @param nip - which NIP is being implemented ('nip04', 'nip44') */ async encrypt(recipient: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise { const randomId = Math.random().toString(36).substring(2, 15); if (scheme === "nip04") { const result = (await Nip55.nip04Encrypt( this.packageName, value, randomId, recipient.pubkey, this._pubkey, )) as unknown as { result: string }; return result.result; } else { const result = (await Nip55.nip44Encrypt( this.packageName, value, randomId, recipient.pubkey, this._pubkey, )) as unknown as { result: string }; return result.result; } } /** * Decrypts the given value. * Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined * @param sender - The sender (pubkey or conversationKey) of the encrypted value * @param value - The value to be decrypted * @param scheme - which NIP is being implemented ('nip04', 'nip44', 'nip49') */ async decrypt(sender: NDKUser, value: string, scheme?: NDKEncryptionScheme): Promise { const randomId = Math.random().toString(36).substring(2, 15); if (scheme === "nip04") { const result = (await Nip55.nip04Decrypt( this.packageName, value, randomId, this.pubkey, sender.pubkey, )) as unknown as { result: string }; return result.result; } else { const result = (await Nip55.nip44Decrypt( this.packageName, value, randomId, this.pubkey, sender.pubkey, )) as unknown as { result: string }; return result.result; } } /** * Serializes the signer's package name and pubkey. * @returns A JSON string containing the type, package name, and pubkey. */ public toPayload(): string { if (!this._pubkey) { throw new Error("NIP-55 signer not ready for serialization (missing pubkey)"); } const payload = { type: "nip55", payload: { packageName: this.packageName, pubkey: this._pubkey, // Include pubkey for potential re-initialization }, }; return JSON.stringify(payload); } /** * Deserializes the signer from a payload string. * @param payloadString The JSON string obtained from toPayload(). * @param ndk Optional NDK instance. * @returns An instance of NDKNip55Signer. */ public static async fromPayload(payloadString: string, ndk?: NDK): Promise { const parsed = JSON.parse(payloadString); if (parsed.type !== "nip55") { throw new Error(`Invalid payload type: expected 'nip55', got ${parsed.type}`); } const payload = parsed.payload; if (!payload || typeof payload !== "object" || !payload.packageName) { throw new Error("Invalid payload content for nip55 signer"); } const signer = new NDKNip55Signer(payload.packageName, ndk); // We can optionally pre-set the pubkey if available in the payload // to potentially speed up the initial blockUntilReady, though it will verify anyway. if (payload.pubkey) { signer._pubkey = payload.pubkey; } return signer; } } // Register this signer type with the core registry *outside* the class definition // This ensures ndkSignerFromPayload can deserialize it. registerSigner("nip55", NDKNip55Signer);