Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@floating-ui/dom": "^1.7.6",
"@noble/hashes": "^2.2.0",
"@rust-nostr/nostr-sdk": "^0.44.0",
"@tabler/icons-svelte": "^3.41.1",
"@tabler/icons-webfont": "^3.41.1",
Expand Down
26 changes: 26 additions & 0 deletions web/src/lib/Nip05.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Thin wrapper around `nostr-tools` NIP-05 lookups that routes
* Namecoin `.bit` identifiers to the chain resolver instead of HTTPS.
*
* All existing call sites use the same `{ pubkey, relays? }` shape, so
* the wrapper returns the same `ProfilePointer | null` contract. When
* the identifier is not a `.bit`-style name, the call falls through
* to `nip05.queryProfile` from `nostr-tools` unchanged.
*/
import { nip05 } from 'nostr-tools';
import type { ProfilePointer } from 'nostr-tools/nip19';
import { isDotBitIdentifier, queryProfile as queryNamecoin } from './namecoin';

/**
* Resolve a NIP-05 identifier. `.bit` (Namecoin) identifiers are
* resolved over the Namecoin chain; everything else falls through to
* nostr-tools.
*/
export async function queryProfile(identifier: string): Promise<ProfilePointer | null> {
if (isDotBitIdentifier(identifier)) {
const result = await queryNamecoin(identifier);
if (!result) return null;
return { pubkey: result.pubkey, relays: result.relays ?? [] };
}
return nip05.queryProfile(identifier);
}
4 changes: 2 additions & 2 deletions web/src/lib/components/NostrAddress.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { nip05 } from 'nostr-tools';
import { queryProfile } from '$lib/Nip05';
import type { Metadata } from '$lib/Items';
import IconRosetteDiscountCheck from '@tabler/icons-svelte/icons/rosette-discount-check';
import IconAlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
Expand All @@ -17,7 +17,7 @@
{#if normalizedNip05}
<div class="nip05">
<span>{normalizedNip05}</span>
{#await nip05.queryProfile(normalizedNip05) then pointer}
{#await queryProfile(normalizedNip05) then pointer}
{#if pointer === null}
<IconAlertTriangle color="orange" />
<span class="label">{$_('profile.nip05.unknown')}</span>
Expand Down
52 changes: 52 additions & 0 deletions web/src/lib/namecoin/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Session-level in-memory cache for Namecoin `.bit` NIP-05 resolutions.
* Survives across components within a single browser session and dedupes
* concurrent lookups. No TTL — entries live for the lifetime of the
* runtime; on a hard navigation the cache is rebuilt.
*/
import type { NamecoinResolveResult } from './nip05';

const POS_CACHE = new Map<string, NamecoinResolveResult>();
const NEG_CACHE = new Set<string>();
const INFLIGHT = new Map<string, Promise<NamecoinResolveResult | null>>();

function normalizeKey(identifier: string): string {
return identifier.trim().toLowerCase();
}

export function getCached(identifier: string): NamecoinResolveResult | null | undefined {
const key = normalizeKey(identifier);
if (POS_CACHE.has(key)) return POS_CACHE.get(key)!;
if (NEG_CACHE.has(key)) return null;
return undefined;
}

export function setCached(identifier: string, result: NamecoinResolveResult | null): void {
const key = normalizeKey(identifier);
if (result) {
POS_CACHE.set(key, result);
NEG_CACHE.delete(key);
} else {
NEG_CACHE.add(key);
POS_CACHE.delete(key);
}
}

export function getInflight(identifier: string): Promise<NamecoinResolveResult | null> | undefined {
return INFLIGHT.get(normalizeKey(identifier));
}

export function setInflight(identifier: string, p: Promise<NamecoinResolveResult | null>): void {
INFLIGHT.set(normalizeKey(identifier), p);
}

export function deleteInflight(identifier: string): void {
INFLIGHT.delete(normalizeKey(identifier));
}

/** Test-only helper. Resets cache state. */
export function _resetCache(): void {
POS_CACHE.clear();
NEG_CACHE.clear();
INFLIGHT.clear();
}
Loading