Files
gashboard/apps/web/src/services/signer.ts
Dorian c56a47e2c4 feat(web): cyberpunk vue 3 dashboard with primal/amber/extension login
Frontend in @gashboard/web (Vue 3 + Vite + Pinia + TS, ~1.2k LoC):

Auth flow:
  - Two signing paths: NIP-07 browser extension (Alby/nos2x/Primal
    extension via window.nostr) and NIP-46 remote signer (Primal
    app, Amber, nsecbunker via bunker:// URI).
  - applesauce-signers lazy-loaded only on bunker login so users
    with NIP-07 don't pay the cost.
  - NIP-98 event built client-side, posted to /api/auth/login,
    JWT persisted in localStorage. Pinia auth store handles
    login/logout/state restore on reload.

Dashboard (composes the live /api/datum/stats poll, 5s):
  - PoolHero — combined hashrate as the headline number,
    block height, subscribed count, accepted/rejected shares.
  - LotteryWidget — rotating self-deprecating odds copy
    ("you're 0.3× as likely to find a block as get hit by
    lightning today"). Uses ~720 EH/s as the network-hashrate
    constant (TODO: fetch live).
  - ShareTicker — SVG sparkline of the last 60 polls.
  - MinerCard ×N — nickname (QU4CK/P1XEL/N4N0/M1N1), live
    hashrate, last share, lifetime tickets, reject %, status
    glow (green hashing / amber stale / red idle), affectionate
    roast subtitle per ASIC type.
  - BlockCelebration — full-screen overlay with celebration
    copy. Dormant for now (Datum's lastBlockFoundAt isn't
    surfaced yet); preview via window.gashboardCelebrate().

Cyberpunk theme:
  - Pure CSS vars, no Tailwind. Dark bg, neon cyan/magenta
    accents, monospace, glow shadows.
  - Optional CRT scanlines toggle (persists to localStorage).
  - Mobile-aware grid breakpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:59:00 +01:00

69 lines
2.2 KiB
TypeScript

// Two signing paths: NIP-07 browser extension and NIP-46 remote signer
// (covers Primal app + Amber via bunker:// URI). The active signer is held
// in module state — there's only one logged-in user at a time.
import type { Event as NostrEvent, EventTemplate } from "nostr-tools";
export type SignerKind = "extension" | "bunker";
export type Signer = {
kind: SignerKind;
getPublicKey(): Promise<string>;
signEvent(template: EventTemplate): Promise<NostrEvent>;
};
let activeSigner: Signer | null = null;
export function getActiveSigner(): Signer | null {
return activeSigner;
}
export function clearSigner(): void {
activeSigner = null;
}
export async function loginWithExtension(): Promise<string> {
if (!window.nostr) {
throw new Error("No NIP-07 extension found. Try Alby, nos2x, or Primal extension.");
}
const ext = window.nostr;
const pubkey = await ext.getPublicKey();
activeSigner = {
kind: "extension",
getPublicKey: async () => pubkey,
signEvent: async (template: EventTemplate) =>
(await ext.signEvent({
kind: template.kind,
created_at: template.created_at,
tags: template.tags,
content: template.content,
})) as NostrEvent,
};
return pubkey;
}
export async function loginWithBunker(bunkerUri: string): Promise<string> {
// Lazy-load applesauce-signers — only pulled when bunker login is attempted
// so the dashboard works for users who only use NIP-07.
const mod = await import("applesauce-signers");
const Ctor =
(mod as { NostrConnectSigner?: any }).NostrConnectSigner ??
((mod as { default?: { NostrConnectSigner?: any } }).default?.NostrConnectSigner);
if (!Ctor) {
throw new Error("applesauce-signers does not expose NostrConnectSigner");
}
const fromUri = Ctor.fromBunkerURI ?? Ctor.fromURI;
if (!fromUri) {
throw new Error("NostrConnectSigner has no fromBunkerURI / fromURI helper");
}
const signer = await fromUri.call(Ctor, bunkerUri);
const pubkey: string = await signer.getPublicKey();
activeSigner = {
kind: "bunker",
getPublicKey: async () => pubkey,
signEvent: async (template: EventTemplate) =>
(await signer.signEvent(template)) as NostrEvent,
};
return pubkey;
}