first commit
Made-with: Cursor
This commit is contained in:
159
frontend/lib/nostr.ts
Normal file
159
frontend/lib/nostr.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { generateSecretKey, getPublicKey as getPubKeyFromSecret } from "nostr-tools/pure";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: {
|
||||
getPublicKey(): Promise<string>;
|
||||
signEvent(event: any): Promise<any>;
|
||||
getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type BunkerSignerInterface = {
|
||||
getPublicKey(): Promise<string>;
|
||||
signEvent(event: any): Promise<any>;
|
||||
close(): Promise<void>;
|
||||
};
|
||||
|
||||
export function hasNostrExtension(): boolean {
|
||||
return typeof window !== "undefined" && !!window.nostr;
|
||||
}
|
||||
|
||||
export async function getPublicKey(): Promise<string> {
|
||||
if (!window.nostr) throw new Error("No Nostr extension found");
|
||||
return window.nostr.getPublicKey();
|
||||
}
|
||||
|
||||
export async function signEvent(event: any): Promise<any> {
|
||||
if (!window.nostr) throw new Error("No Nostr extension found");
|
||||
return window.nostr.signEvent(event);
|
||||
}
|
||||
|
||||
export function createAuthEvent(pubkey: string, challenge: string) {
|
||||
return {
|
||||
kind: 22242,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["relay", ""],
|
||||
["challenge", challenge],
|
||||
],
|
||||
content: "",
|
||||
pubkey,
|
||||
};
|
||||
}
|
||||
|
||||
export function shortenPubkey(pubkey: string): string {
|
||||
if (!pubkey) return "";
|
||||
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
|
||||
}
|
||||
|
||||
export interface NostrProfile {
|
||||
name?: string;
|
||||
picture?: string;
|
||||
about?: string;
|
||||
nip05?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band",
|
||||
];
|
||||
|
||||
export async function publishEvent(signedEvent: any): Promise<void> {
|
||||
const { SimplePool } = await import("nostr-tools/pool");
|
||||
let relayUrls: string[] = DEFAULT_RELAYS;
|
||||
try {
|
||||
if (window.nostr?.getRelays) {
|
||||
const ext = await window.nostr.getRelays();
|
||||
const write = Object.entries(ext)
|
||||
.filter(([, p]) => (p as any).write)
|
||||
.map(([url]) => url);
|
||||
if (write.length > 0) relayUrls = write;
|
||||
}
|
||||
} catch {}
|
||||
const pool = new SimplePool();
|
||||
try {
|
||||
await Promise.allSettled(pool.publish(relayUrls, signedEvent));
|
||||
} finally {
|
||||
pool.close(relayUrls);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchNostrProfile(
|
||||
pubkey: string,
|
||||
relayUrls: string[] = DEFAULT_RELAYS
|
||||
): Promise<NostrProfile> {
|
||||
const { SimplePool } = await import("nostr-tools/pool");
|
||||
const pool = new SimplePool();
|
||||
|
||||
try {
|
||||
const event = await pool.get(relayUrls, {
|
||||
kinds: [0],
|
||||
authors: [pubkey],
|
||||
});
|
||||
|
||||
if (!event?.content) return {};
|
||||
|
||||
const meta = JSON.parse(event.content);
|
||||
return {
|
||||
name: meta.name || meta.display_name,
|
||||
displayName: meta.display_name,
|
||||
picture: meta.picture,
|
||||
about: meta.about,
|
||||
nip05: meta.nip05,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
} finally {
|
||||
pool.close(relayUrls);
|
||||
}
|
||||
}
|
||||
|
||||
// NIP-46: External signer (bunker:// URI)
|
||||
export async function createBunkerSigner(
|
||||
input: string
|
||||
): Promise<{ signer: BunkerSignerInterface; pubkey: string }> {
|
||||
const { BunkerSigner, parseBunkerInput } = await import("nostr-tools/nip46");
|
||||
const bp = await parseBunkerInput(input);
|
||||
if (!bp) throw new Error("Invalid bunker URI or NIP-05 identifier");
|
||||
const clientSecretKey = generateSecretKey();
|
||||
const signer = BunkerSigner.fromBunker(clientSecretKey, bp);
|
||||
await signer.connect();
|
||||
const pubkey = await signer.getPublicKey();
|
||||
return { signer, pubkey };
|
||||
}
|
||||
|
||||
// NIP-46: Generate a nostrconnect:// URI for QR display
|
||||
export async function generateNostrConnectSetup(
|
||||
relayUrls: string[] = DEFAULT_RELAYS.slice(0, 2)
|
||||
): Promise<{ uri: string; clientSecretKey: Uint8Array }> {
|
||||
const { createNostrConnectURI } = await import("nostr-tools/nip46");
|
||||
const clientSecretKey = generateSecretKey();
|
||||
const clientPubkey = getPubKeyFromSecret(clientSecretKey);
|
||||
const secretBytes = crypto.getRandomValues(new Uint8Array(8));
|
||||
const secret = Array.from(secretBytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
const uri = createNostrConnectURI({
|
||||
clientPubkey,
|
||||
relays: relayUrls,
|
||||
secret,
|
||||
name: "Belgian Bitcoin Embassy",
|
||||
});
|
||||
return { uri, clientSecretKey };
|
||||
}
|
||||
|
||||
// NIP-46: Wait for a remote signer to connect via nostrconnect:// URI
|
||||
export async function waitForNostrConnectSigner(
|
||||
clientSecretKey: Uint8Array,
|
||||
uri: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<{ signer: BunkerSignerInterface; pubkey: string }> {
|
||||
const { BunkerSigner } = await import("nostr-tools/nip46");
|
||||
const signer = await BunkerSigner.fromURI(clientSecretKey, uri, {}, signal);
|
||||
const pubkey = await signer.getPublicKey();
|
||||
return { signer, pubkey };
|
||||
}
|
||||
Reference in New Issue
Block a user