160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
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 };
|
|
}
|