diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx index 4e81fbef..01474f4d 100644 --- a/packages/app/src/Pages/LoginPage.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom"; import { useIntl, FormattedMessage } from "react-intl"; import { HexKey, Nip46Signer, NotEncrypted, PinEncrypted, PrivateKeySigner } from "@snort/system"; -import { bech32ToHex, getPublicKey, isHex, unwrap } from "SnortUtils"; +import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils"; import ZapButton from "Element/Event/ZapButton"; import useImgProxy from "Hooks/useImgProxy"; import Icon from "Icons/Icon"; @@ -20,6 +20,7 @@ import Copy from "Element/Copy"; import { delay } from "SnortUtils"; import { PinPrompt } from "Element/PinPrompt"; import useEventPublisher from "Hooks/useEventPublisher"; +import { isHex } from "@snort/shared"; declare global { interface Window { diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts index c2e50114..d153bc2e 100644 --- a/packages/app/src/SnortUtils/index.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -18,7 +18,7 @@ import { NostrLink, UserMetadata, } from "@snort/system"; -import { isOffline } from "@snort/shared"; +import { isHex, isOffline } from "@snort/shared"; import { Day } from "Const"; import AnimalName from "Element/User/AnimalName"; @@ -152,14 +152,6 @@ export function deepClone(obj: T) { } } -export function isHex(s: string) { - // 48-57 = 0-9 - // 65-90 = A-Z - // 97-122 = a-z - return [...s] - .map(v => v.charCodeAt(0)) - .every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122); -} /** * Simple debounce */ diff --git a/packages/app/src/Zapper.ts b/packages/app/src/Zapper.ts index c5113a67..0ec9e15e 100644 --- a/packages/app/src/Zapper.ts +++ b/packages/app/src/Zapper.ts @@ -1,7 +1,6 @@ -import { LNURL } from "@snort/shared"; +import { LNURL, isHex } from "@snort/shared"; import { EventPublisher, NostrEvent, NostrLink, SystemInterface } from "@snort/system"; import { generateRandomKey } from "Login"; -import { isHex } from "SnortUtils"; import { LNWallet, WalletInvoiceState } from "Wallet"; export interface ZapTarget { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 80d04af5..6976140d 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -227,3 +227,12 @@ export function throwIfOffline() { export function isOffline() { return !("navigator" in globalThis && globalThis.navigator.onLine); } + +export function isHex(s: string) { + // 48-57 = 0-9 + // 65-90 = A-Z + // 97-122 = a-z + return [...s] + .map(v => v.charCodeAt(0)) + .every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122); +} diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts index 599aec43..697be955 100644 --- a/packages/system/src/nostr-link.ts +++ b/packages/system/src/nostr-link.ts @@ -1,4 +1,4 @@ -import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared"; +import { bech32ToHex, hexToBech32, isHex, unwrap } from "@snort/shared"; import { decodeTLV, encodeTLV, @@ -19,15 +19,24 @@ export class NostrLink { readonly kind?: number, readonly author?: string, readonly relays?: Array, - ) {} + ) { + if (type !== NostrPrefix.Address && !isHex(id)) { + throw new Error("ID must be hex"); + } + } encode(type?: NostrPrefix): string { - // cant encode 'naddr' to 'note'/'nevent' because 'id' is not hex - let newType = this.type === NostrPrefix.Address ? this.type : type ?? this.type; - if (newType === NostrPrefix.Note || newType === NostrPrefix.PrivateKey || newType === NostrPrefix.PublicKey) { - return hexToBech32(newType, this.id); - } else { - return encodeTLV(newType, this.id, this.relays, this.kind, this.author); + try { + // cant encode 'naddr' to 'note'/'nevent' because 'id' is not hex + let newType = this.type === NostrPrefix.Address ? this.type : type ?? this.type; + if (newType === NostrPrefix.Note || newType === NostrPrefix.PrivateKey || newType === NostrPrefix.PublicKey) { + return hexToBech32(newType, this.id); + } else { + return encodeTLV(newType, this.id, this.relays, this.kind, this.author); + } + } catch (e) { + console.error("Invalid data", this, e); + throw e; } } @@ -201,7 +210,10 @@ export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): Nostr } export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink { - const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; + let entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; + + // trim any non-bech32 chars + entity = entity.match(/(n(?:pub|profile|event|ote|addr)1[acdefghjklmnpqrstuvwxyz023456789]+)/)?.[0] ?? entity; const isPrefix = (prefix: NostrPrefix) => { return entity.startsWith(prefix);