Browse Source

unisigned events

imwald
Silberengel 4 weeks ago
parent
commit
d3bee0bd57
  1. 86
      src/components/NoteOptions/EditOrCloneEventDialog.tsx
  2. 19
      src/constants.ts
  3. 32
      src/providers/NostrProvider/index.tsx

86
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -18,7 +18,13 @@ import Highlight from '@/components/Note/Highlight'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle' import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle' import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle'
import ClientTag from '@/components/ClientTag' import ClientTag from '@/components/ClientTag'
import { ExtendedKind } from '@/constants' import {
ExtendedKind,
MAX_SIGNED_CUSTOM_EVENT_KIND,
UNSIGNED_EXPERIMENTAL_KIND_MAX,
UNSIGNED_EXPERIMENTAL_KIND_MIN,
isUnsignedExperimentalKind
} from '@/constants'
import { applyImwaldAttributionTags } from '@/lib/draft-event' import { applyImwaldAttributionTags } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event' import { createFakeEvent } from '@/lib/event'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -32,7 +38,7 @@ import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import type { TDraftEvent } from '@/types' import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Plus, Trash2 } from 'lucide-react' import { AlertTriangle, Plus, Trash2 } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -54,15 +60,15 @@ function tagsFromRows(rows: string[][]): string[][] {
return out return out
} }
const MAX_CUSTOM_EVENT_KIND = 40000 /** Integer kind in [0, MAX_SIGNED_CUSTOM_EVENT_KIND] or unsigned experimental range; null if invalid / empty. */
/** Integer kind in [0, 40000], or null if invalid / empty. */
function parseEventKindInput(s: string): number | null { function parseEventKindInput(s: string): number | null {
const trimmed = s.trim() const trimmed = s.trim()
if (trimmed === '') return null if (trimmed === '') return null
const n = Number(trimmed) const n = Number(trimmed)
if (!Number.isInteger(n) || n < 0 || n > MAX_CUSTOM_EVENT_KIND) return null if (!Number.isInteger(n) || n < 0) return null
return n if (n <= MAX_SIGNED_CUSTOM_EVENT_KIND) return n
if (isUnsignedExperimentalKind(n)) return n
return null
} }
function StaticEventPreview({ event, className }: { event: Event; className?: string }) { function StaticEventPreview({ event, className }: { event: Event; className?: string }) {
@ -179,7 +185,14 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
const buildDraftJson = useCallback(() => { const buildDraftJson = useCallback(() => {
if (isCreate && parsedCreateKind === null) { if (isCreate && parsedCreateKind === null) {
return t('Enter a valid event kind (integer 0–40000).') return t(
'Enter a valid event kind: integer 0–{{maxSigned}}, or {{unsignedMin}}–{{unsignedMax}} (unsigned experiment).',
{
maxSigned: MAX_SIGNED_CUSTOM_EVENT_KIND,
unsignedMin: UNSIGNED_EXPERIMENTAL_KIND_MIN,
unsignedMax: UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)
} }
const k = isCreate ? parsedCreateKind! : sourceEvent!.kind const k = isCreate ? parsedCreateKind! : sourceEvent!.kind
const base: TDraftEvent = { const base: TDraftEvent = {
@ -191,13 +204,18 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
const withAttribution = applyImwaldAttributionTags(base, { const withAttribution = applyImwaldAttributionTags(base, {
addClientTag: storage.getAddClientTag() addClientTag: storage.getAddClientTag()
}) })
const unsignedNote = isUnsignedExperimentalKind(withAttribution.kind)
? t(
'Unsigned experimental kind: `sig` will be empty at publish; `id` is still the standard event hash. Not accepted by normal relays. Relays that allow this should authenticate you (e.g. NIP-42 AUTH) before writes.'
)
: t('id and sig are assigned when you publish')
const draft = { const draft = {
pubkey: pubkey ?? t('Log in to publish'), pubkey: pubkey ?? t('Log in to publish'),
kind: withAttribution.kind, kind: withAttribution.kind,
content: withAttribution.content, content: withAttribution.content,
tags: withAttribution.tags, tags: withAttribution.tags,
created_at: t('Set when you publish'), created_at: t('Set when you publish'),
_note: t('id and sig are assigned when you publish') _note: unsignedNote
} }
return JSON.stringify(draft, null, 2) return JSON.stringify(draft, null, 2)
}, [isCreate, parsedCreateKind, sourceEvent, pubkey, content, normalizedTags, t]) }, [isCreate, parsedCreateKind, sourceEvent, pubkey, content, normalizedTags, t])
@ -242,7 +260,16 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
if (isCreate) { if (isCreate) {
const k = parseEventKindInput(createKindInput) const k = parseEventKindInput(createKindInput)
if (k === null) { if (k === null) {
showPublishingError(t('Kind must be an integer from 0 to 40000.')) showPublishingError(
t(
'Kind must be an integer from 0 to {{maxSigned}}, or from {{unsignedMin}} to {{unsignedMax}} (unsigned experiment).',
{
maxSigned: MAX_SIGNED_CUSTOM_EVENT_KIND,
unsignedMin: UNSIGNED_EXPERIMENTAL_KIND_MIN,
unsignedMax: UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)
)
return return
} }
} }
@ -339,15 +366,41 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
<Input <Input
type="number" type="number"
min={0} min={0}
max={MAX_CUSTOM_EVENT_KIND} max={UNSIGNED_EXPERIMENTAL_KIND_MAX}
step={1} step={1}
value={createKindInput} value={createKindInput}
onChange={(e) => setCreateKindInput(e.target.value)} onChange={(e) => setCreateKindInput(e.target.value)}
className="font-mono text-sm" className="font-mono text-sm"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('Integer from 0 to 40000')} {t(
'Signed: 0–{{maxSigned}}. Unsigned experiment (empty sig): {{unsignedMin}}–{{unsignedMax}}.',
{
maxSigned: MAX_SIGNED_CUSTOM_EVENT_KIND,
unsignedMin: UNSIGNED_EXPERIMENTAL_KIND_MIN,
unsignedMax: UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)}
</p> </p>
{parsedCreateKind !== null && isUnsignedExperimentalKind(parsedCreateKind) ? (
<div
role="alert"
className="flex gap-2 rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm text-amber-950 dark:text-amber-100"
>
<AlertTriangle
className="h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400"
aria-hidden
/>
<div>
<p className="font-medium">{t('Unsigned experimental kind')}</p>
<p className="mt-1 text-xs leading-relaxed opacity-90">
{t(
'This kind is published with an empty signature. Normal Nostr relays will reject it, and these events are not portable on the open network. Only use relays that explicitly support this experiment and authenticate you (for example with NIP-42 AUTH) before accepting writes.'
)}
</p>
</div>
</div>
) : null}
</> </>
) : ( ) : (
<Input <Input
@ -445,7 +498,14 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
</> </>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('Enter a valid event kind (integer 0–40000).')} {t(
'Enter a valid event kind: 0–{{maxSigned}}, or {{unsignedMin}}–{{unsignedMax}}.',
{
maxSigned: MAX_SIGNED_CUSTOM_EVENT_KIND,
unsignedMin: UNSIGNED_EXPERIMENTAL_KIND_MIN,
unsignedMax: UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)}
</p> </p>
)} )}
</div> </div>

19
src/constants.ts

@ -457,6 +457,25 @@ export const ExtendedKind = {
GIT_RELEASE: 1642 GIT_RELEASE: 1642
} }
/**
* Relay-local experiment: event `id` is the standard Nostr hash, but `sig` is empty.
* Not verifiable on the public relay network; relays that accept writes should require NIP-42 AUTH first.
*/
export const UNSIGNED_EXPERIMENTAL_KIND_MIN = 69999
export const UNSIGNED_EXPERIMENTAL_KIND_MAX = 130000
export const UNSIGNED_EXPERIMENTAL_RELAY_URLS = [
'wss://nostr.land',
'wss://theforest.gitcitadel.eu',
]
export function isUnsignedExperimentalKind(kind: number): boolean {
return kind >= UNSIGNED_EXPERIMENTAL_KIND_MIN && kind <= UNSIGNED_EXPERIMENTAL_KIND_MAX
}
/** Max kind for signed “custom event” notes in the generic composer (below the unsigned experimental range). */
export const MAX_SIGNED_CUSTOM_EVENT_KIND = 40000
/** /**
* Kinds subscribed on `#e` / `#a` for the OP in {@link useQuoteEvents} (thread backlinks shard), * Kinds subscribed on `#e` / `#a` for the OP in {@link useQuoteEvents} (thread backlinks shard),
* alongside kind-1 `#q` quotes. Covers highlights, long-form, NIP-32 labels, NIP-56 reports, * alongside kind-1 `#q` quotes. Covers highlights, long-form, NIP-32 labels, NIP-56 reports,

32
src/providers/NostrProvider/index.tsx

@ -8,7 +8,10 @@ import {
ExtendedKind, ExtendedKind,
PROFILE_FETCH_RELAY_URLS, PROFILE_FETCH_RELAY_URLS,
PROFILE_RELAY_URLS, PROFILE_RELAY_URLS,
SEARCHABLE_RELAY_URLS SEARCHABLE_RELAY_URLS,
UNSIGNED_EXPERIMENTAL_KIND_MAX,
UNSIGNED_EXPERIMENTAL_KIND_MIN,
isUnsignedExperimentalKind
} from '@/constants' } from '@/constants'
import { import {
applyImwaldAttributionTags, applyImwaldAttributionTags,
@ -43,7 +46,7 @@ import {
} from '@/types' } from '@/types'
import { hexToBytes } from '@noble/hashes/utils' import { hexToBytes } from '@noble/hashes/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event, kinds, VerifiedEvent, validateEvent } from 'nostr-tools' import { Event, kinds, VerifiedEvent, getEventHash, validateEvent } from 'nostr-tools'
import * as nip19 from 'nostr-tools/nip19' import * as nip19 from 'nostr-tools/nip19'
import * as nip49 from 'nostr-tools/nip49' import * as nip49 from 'nostr-tools/nip49'
import { NostrContext } from '@/providers/nostr-context' import { NostrContext } from '@/providers/nostr-context'
@ -1261,8 +1264,29 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const normalizeOpts = { addClientTag: options.addClientTag } const normalizeOpts = { addClientTag: options.addClientTag }
const draft = normalizeDraftEventTags(draftEvent, normalizeOpts) const draft = normalizeDraftEventTags(draftEvent, normalizeOpts)
let event: VerifiedEvent let event: Event
if (minPow > 0) { if (isUnsignedExperimentalKind(draft.kind)) {
if (minPow > 0) {
throw new Error(
t('Proof of work is not supported for unsigned experimental kinds ({{min}}–{{max}}).', {
min: UNSIGNED_EXPERIMENTAL_KIND_MIN,
max: UNSIGNED_EXPERIMENTAL_KIND_MAX
})
)
}
const unsignedTemplate = {
kind: draft.kind,
content: draft.content,
tags: draft.tags,
created_at: draft.created_at,
pubkey: account.pubkey
}
if (!validateEvent(unsignedTemplate)) {
throw new Error(t('Invalid event fields'))
}
const id = getEventHash(unsignedTemplate)
event = { ...unsignedTemplate, id, sig: '' }
} else if (minPow > 0) {
const unsignedEvent = await minePow({ ...draft, pubkey: account.pubkey }, minPow) const unsignedEvent = await minePow({ ...draft, pubkey: account.pubkey }, minPow)
event = await signEvent(unsignedEvent, normalizeOpts) event = await signEvent(unsignedEvent, normalizeOpts)
} else { } else {

Loading…
Cancel
Save