Try to make applesauce work
This commit is contained in:
@@ -15,12 +15,14 @@
|
||||
"@welshman/util": "^0.8.16",
|
||||
"applesauce-accounts": "^6.0.0",
|
||||
"applesauce-actions": "^6.1.0",
|
||||
"applesauce-common": "^6.1.0",
|
||||
"applesauce-core": "^6.1.0",
|
||||
"applesauce-factory": "^4.0.0",
|
||||
"applesauce-relay": "^6.0.3",
|
||||
"applesauce-signers": "^6.0.1",
|
||||
"applesauce-solidjs": "^4.0.0",
|
||||
"idb": "^8.0.3",
|
||||
"rxjs": "^7.8.2",
|
||||
"solid-js": "^1.9.13",
|
||||
"solid-toast": "^0.5.0"
|
||||
},
|
||||
|
||||
Generated
+6
@@ -26,6 +26,9 @@ importers:
|
||||
applesauce-actions:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(typescript@6.0.3)
|
||||
applesauce-common:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(typescript@6.0.3)
|
||||
applesauce-core:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(typescript@6.0.3)
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
idb:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
rxjs:
|
||||
specifier: ^7.8.2
|
||||
version: 7.8.2
|
||||
solid-js:
|
||||
specifier: ^1.9.13
|
||||
version: 1.9.13
|
||||
|
||||
+3
-8
@@ -8,17 +8,12 @@ import { ProposeQuorum } from "./components/forms/DkgForms"
|
||||
import { ProposeResharing } from "./components/forms/ResharingForms"
|
||||
import { ProposeSign } from "./components/forms/SigningForms"
|
||||
import Avatar from "./components/Avatar"
|
||||
import { fetchProfile, getDisplayName, useProfileEvent } from "./lib/profiles"
|
||||
import { getDisplayName } from "./lib/profiles"
|
||||
import { useActivePubkey, useProfileEvent } from "./hooks"
|
||||
|
||||
function SidebarContent(props: { onNew: () => void }) {
|
||||
const pubkey = () => account()?.pubkey ?? ""
|
||||
const pubkey = useActivePubkey()
|
||||
const shortKey = () => `${pubkey().slice(0, 8)}…${pubkey().slice(-4)}`
|
||||
|
||||
createEffect(() => {
|
||||
const pk = account()?.pubkey
|
||||
if (pk) { fetchProfile(pk) }
|
||||
})
|
||||
|
||||
const profileEvent = useProfileEvent(pubkey)
|
||||
const displayName = () => getDisplayName(profileEvent(), shortKey())
|
||||
|
||||
|
||||
@@ -1,50 +1,27 @@
|
||||
import { createSignal, createEffect, onCleanup, For, Show } from "solid-js"
|
||||
import { createSignal, For, Show } from "solid-js"
|
||||
import { now } from "@welshman/lib"
|
||||
import { publish, eventStore } from "../nostr"
|
||||
import { account } from "../store"
|
||||
import {
|
||||
parseRelayList,
|
||||
parseOutboxRelays,
|
||||
INDEXER_RELAYS,
|
||||
} from "../lib/relays"
|
||||
import { useProfileEvent, getDisplayName } from "../lib/profiles"
|
||||
import { INDEXER_RELAYS } from "../lib/relays"
|
||||
import { getDisplayName } from "../lib/profiles"
|
||||
import { useActivePubkey, useProfileEvent, useMessagingRelays, useOutboxRelays } from "../hooks"
|
||||
import Avatar from "./Avatar"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
|
||||
export default function AccountPage() {
|
||||
const pubkey = () => account()?.pubkey ?? ""
|
||||
const pubkey = useActivePubkey()
|
||||
const profileEvent = useProfileEvent(pubkey)
|
||||
const messagingRelays = useMessagingRelays(pubkey)
|
||||
const outboxRelays = useOutboxRelays(pubkey)
|
||||
const displayName = () => getDisplayName(profileEvent(), "")
|
||||
|
||||
const [relays, setRelays] = createSignal<string[]>([])
|
||||
const [outboxEvent, setOutboxEvent] = createSignal<NostrEvent | undefined>()
|
||||
const [newRelay, setNewRelay] = createSignal("")
|
||||
const [publishing, setPublishing] = createSignal(false)
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const [addError, setAddError] = createSignal("")
|
||||
|
||||
// Sync relay list from kind 10050 in EventStore
|
||||
createEffect(() => {
|
||||
const pk = pubkey()
|
||||
if (!pk) { setRelays([]); return }
|
||||
const sub = eventStore.replaceable(10050, pk).subscribe(e => {
|
||||
if (e) setRelays(parseRelayList(e))
|
||||
})
|
||||
onCleanup(() => sub.unsubscribe())
|
||||
})
|
||||
|
||||
// Track kind 10002 (outbox relay list) for publishing target
|
||||
createEffect(() => {
|
||||
const pk = pubkey()
|
||||
if (!pk) { setOutboxEvent(undefined); return }
|
||||
const sub = eventStore.replaceable(10002, pk).subscribe(e => setOutboxEvent(e ?? undefined))
|
||||
onCleanup(() => sub.unsubscribe())
|
||||
})
|
||||
|
||||
async function publishRelays(updated: string[]): Promise<void> {
|
||||
const acc = account()
|
||||
if (!acc) return
|
||||
|
||||
setPublishing(true)
|
||||
try {
|
||||
const signed = await acc.signEvent({
|
||||
@@ -53,11 +30,8 @@ export default function AccountPage() {
|
||||
tags: updated.map(r => ["relay", r]),
|
||||
created_at: now(),
|
||||
})
|
||||
|
||||
const outbox = outboxEvent()
|
||||
const targetRelays = outbox ? parseOutboxRelays(outbox) : INDEXER_RELAYS
|
||||
|
||||
await publish(targetRelays, signed)
|
||||
const targets = outboxRelays().length ? outboxRelays() : INDEXER_RELAYS
|
||||
await publish(targets, signed)
|
||||
eventStore.add(signed)
|
||||
} finally {
|
||||
setPublishing(false)
|
||||
@@ -67,27 +41,21 @@ export default function AccountPage() {
|
||||
async function addRelay() {
|
||||
setAddError("")
|
||||
const relay = newRelay().trim()
|
||||
|
||||
if (!relay) return
|
||||
if (!relay.startsWith("wss://") && !relay.startsWith("ws://")) {
|
||||
setAddError("Relay URL must start with wss:// or ws://")
|
||||
return
|
||||
}
|
||||
if (relays().includes(relay)) {
|
||||
if (messagingRelays().includes(relay)) {
|
||||
setAddError("Already in your relay list")
|
||||
return
|
||||
}
|
||||
|
||||
const updated = [...relays(), relay]
|
||||
setRelays(updated)
|
||||
setNewRelay("")
|
||||
await publishRelays(updated)
|
||||
await publishRelays([...messagingRelays(), relay])
|
||||
}
|
||||
|
||||
async function removeRelay(relay: string) {
|
||||
const updated = relays().filter(r => r !== relay)
|
||||
setRelays(updated)
|
||||
await publishRelays(updated)
|
||||
await publishRelays(messagingRelays().filter(r => r !== relay))
|
||||
}
|
||||
|
||||
async function copyPubkey() {
|
||||
@@ -136,18 +104,18 @@ export default function AccountPage() {
|
||||
|
||||
{/* Inbox relay list */}
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-sm font-semibold text-gray-700 dark:text-neutral-300">Inbox Relays (kind 10050)</h2>
|
||||
<h2 class="text-sm font-semibold text-gray-700 dark:text-neutral-300">Messaging Relays (kind 10050)</h2>
|
||||
|
||||
<Show
|
||||
when={relays().length > 0}
|
||||
when={messagingRelays().length > 0}
|
||||
fallback={
|
||||
<p class="text-sm text-gray-400 dark:text-neutral-500 italic">
|
||||
No inbox relays published yet.
|
||||
No messaging relays published yet.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<ul class="flex flex-col divide-y divide-gray-100 dark:divide-neutral-700 border border-gray-200 dark:border-neutral-700 rounded-lg overflow-hidden">
|
||||
<For each={relays()}>
|
||||
<For each={messagingRelays()}>
|
||||
{(relay) => (
|
||||
<li class="flex items-center gap-2 px-3 py-2.5 bg-white dark:bg-neutral-800">
|
||||
<span class="flex-1 text-sm font-mono text-gray-800 dark:text-neutral-200 truncate">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSignal, Show, createEffect } from "solid-js"
|
||||
import { getDisplayName, getProfilePicture, useProfileEvent } from "../lib/profiles"
|
||||
import { getDisplayName, getProfilePicture } from "../lib/profiles"
|
||||
import { useProfileEvent } from "../hooks"
|
||||
|
||||
export default function Avatar(props: { pubkey: string; size?: string }) {
|
||||
const [imgError, setImgError] = createSignal(false)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createSignal, createEffect, For, Show, onMount, onCleanup } from "solid-js"
|
||||
import { normalizeToPubkey } from "applesauce-core/helpers"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import { fetchProfile, searchProfiles, getDisplayName, useProfileEvent } from "../lib/profiles"
|
||||
import { fetchProfile, searchProfiles, getDisplayName } from "../lib/profiles"
|
||||
import { useProfileEvent } from "../hooks"
|
||||
import Avatar from "./Avatar"
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createSignal, createEffect, Show } from "solid-js"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import type { DkgSession } from "../../models"
|
||||
import PubkeyInput from "../PubkeyInput"
|
||||
import { account } from "../../store"
|
||||
import { validateInboxRelays } from "../../lib/relays"
|
||||
import { validateMessagingRelays } from "../../lib/relays"
|
||||
import { useActivePubkey, useAutoThreshold } from "../../hooks"
|
||||
|
||||
const inputClass =
|
||||
"w-full px-3 py-2 text-sm border border-gray-200 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -14,13 +14,9 @@ const secondaryButtonClass =
|
||||
"px-4 py-2 text-sm font-medium rounded-lg border border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-neutral-300 hover:bg-gray-50 dark:hover:bg-neutral-700 transition-colors"
|
||||
|
||||
export function ProposeQuorum(props: { onClose: () => void }) {
|
||||
const selfPubkey = () => account()?.pubkey ?? ""
|
||||
const selfPubkey = useActivePubkey()
|
||||
const [members, setMembers] = createSignal<string[]>(selfPubkey() ? [selfPubkey()] : [])
|
||||
const [threshold, setThreshold] = createSignal(1)
|
||||
|
||||
createEffect(() => {
|
||||
setThreshold(Math.max(1, Math.floor(members().length * 2 / 3)))
|
||||
})
|
||||
const [threshold, setThreshold] = useAutoThreshold(members)
|
||||
const [error, setError] = createSignal("")
|
||||
const [submitting, setSubmitting] = createSignal(false)
|
||||
|
||||
@@ -41,7 +37,7 @@ export function ProposeQuorum(props: { onClose: () => void }) {
|
||||
|
||||
setError("")
|
||||
setSubmitting(true)
|
||||
const relayError = await validateInboxRelays(pubkeys)
|
||||
const relayError = await validateMessagingRelays(pubkeys)
|
||||
setSubmitting(false)
|
||||
|
||||
if (relayError) {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { createSignal, createEffect, Show } from "solid-js"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import type { ResharingSession } from "../../models"
|
||||
import PubkeyInput from "../PubkeyInput"
|
||||
import { validateInboxRelays } from "../../lib/relays"
|
||||
import { validateMessagingRelays } from "../../lib/relays"
|
||||
import { useAutoThreshold } from "../../hooks"
|
||||
|
||||
export function ProposeResharing(props: { quorumPubkey: string; onClose: () => void }) {
|
||||
const [newMembers, setNewMembers] = createSignal<string[]>([])
|
||||
const [newThreshold, setNewThreshold] = createSignal(0)
|
||||
|
||||
createEffect(() => {
|
||||
setNewThreshold(Math.max(1, Math.floor(newMembers().length * 2 / 3)))
|
||||
})
|
||||
const [newThreshold, setNewThreshold] = useAutoThreshold(newMembers)
|
||||
const [error, setError] = createSignal("")
|
||||
const [submitting, setSubmitting] = createSignal(false)
|
||||
|
||||
@@ -30,7 +27,7 @@ export function ProposeResharing(props: { quorumPubkey: string; onClose: () => v
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
const relayError = await validateInboxRelays(members)
|
||||
const relayError = await validateMessagingRelays(members)
|
||||
setSubmitting(false)
|
||||
|
||||
if (relayError) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import type { SigningSession } from "../../models"
|
||||
import { quora } from "../../store"
|
||||
import { validateInboxRelays } from "../../lib/relays"
|
||||
import { validateMessagingRelays } from "../../lib/relays"
|
||||
|
||||
export function ProposeSign(props: { quorumPubkey: string; onClose: () => void }) {
|
||||
const [message, setMessage] = createSignal("")
|
||||
@@ -19,7 +19,7 @@ export function ProposeSign(props: { quorumPubkey: string; onClose: () => void }
|
||||
|
||||
setError("")
|
||||
setSubmitting(true)
|
||||
const relayError = await validateInboxRelays(memberPubkeys())
|
||||
const relayError = await validateMessagingRelays(memberPubkeys())
|
||||
setSubmitting(false)
|
||||
|
||||
if (relayError) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import { account } from "./store"
|
||||
import { eventStore } from "./nostr"
|
||||
import { parseRelayList, parseOutboxRelays } from "./lib/relays"
|
||||
|
||||
export function useActivePubkey(): () => string {
|
||||
return () => account()?.pubkey ?? ""
|
||||
}
|
||||
|
||||
export function useAutoThreshold(members: () => string[]): [() => number, (n: number) => void] {
|
||||
const [threshold, setThreshold] = createSignal(1)
|
||||
createEffect(() => setThreshold(Math.max(1, Math.floor(members().length * 2 / 3))))
|
||||
return [threshold, setThreshold]
|
||||
}
|
||||
|
||||
function useReplaceable(kind: number, pubkey: () => string): () => NostrEvent | undefined {
|
||||
const [event, setEvent] = createSignal<NostrEvent | undefined>()
|
||||
createEffect(() => {
|
||||
const pk = pubkey()
|
||||
if (!pk) { setEvent(undefined); return }
|
||||
const sub = eventStore.replaceable(kind, pk).subscribe(e => setEvent(e ?? undefined))
|
||||
onCleanup(() => sub.unsubscribe())
|
||||
})
|
||||
return event
|
||||
}
|
||||
|
||||
export function useProfileEvent(pubkey: () => string): () => NostrEvent | undefined {
|
||||
return useReplaceable(0, pubkey)
|
||||
}
|
||||
|
||||
export function useMessagingRelays(pubkey: () => string): () => string[] {
|
||||
const event = useReplaceable(10050, pubkey)
|
||||
return () => { const e = event(); return e ? parseRelayList(e) : [] }
|
||||
}
|
||||
|
||||
export function useOutboxRelays(pubkey: () => string): () => string[] {
|
||||
const event = useReplaceable(10002, pubkey)
|
||||
return () => { const e = event(); return e ? parseOutboxRelays(e) : [] }
|
||||
}
|
||||
|
||||
+3
-16
@@ -1,16 +1,14 @@
|
||||
import { createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import { getDisplayName, getProfilePicture } from "applesauce-core/helpers"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import { completeOnEose, onlyEvents, storeEvents } from "applesauce-relay"
|
||||
import { pool, eventStore } from "../nostr"
|
||||
import { SEARCH_RELAYS, getOutboxRelays } from "./relays"
|
||||
import { SEARCH_RELAYS, loadOutboxRelays } from "./relays"
|
||||
|
||||
export { getDisplayName, getProfilePicture }
|
||||
|
||||
export function fetchProfile(pubkey: string): void {
|
||||
if (!pubkey || eventStore.hasReplaceable(0, pubkey)) return
|
||||
|
||||
getOutboxRelays(pubkey).then(relays => {
|
||||
if (!pubkey) return
|
||||
loadOutboxRelays(pubkey).then(relays => {
|
||||
if (!relays.length) return
|
||||
pool.subscription(relays, { kinds: [0], authors: [pubkey], limit: 1 })
|
||||
.pipe(storeEvents(eventStore), completeOnEose())
|
||||
@@ -29,14 +27,3 @@ export function searchProfiles(query: string, onResult: (event: NostrEvent) => v
|
||||
const timeout = setTimeout(() => sub.unsubscribe(), 4000)
|
||||
return () => { clearTimeout(timeout); sub.unsubscribe() }
|
||||
}
|
||||
|
||||
export function useProfileEvent(pubkey: () => string): () => NostrEvent | undefined {
|
||||
const [event, setEvent] = createSignal<NostrEvent | undefined>()
|
||||
createEffect(() => {
|
||||
const pk = pubkey()
|
||||
if (!pk) { setEvent(undefined); return }
|
||||
const sub = eventStore.replaceable(0, pk).subscribe(e => setEvent(e ?? undefined))
|
||||
onCleanup(() => sub.unsubscribe())
|
||||
})
|
||||
return event
|
||||
}
|
||||
|
||||
+31
-36
@@ -1,8 +1,11 @@
|
||||
import { completeOnEose, onlyEvents, storeEvents } from "applesauce-relay"
|
||||
import { lastValueFrom } from "applesauce-core/observable"
|
||||
import { timer, takeUntil } from "rxjs"
|
||||
import { pool, eventStore } from "../nostr"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
|
||||
const RELAY_TIMEOUT_MS = 10_000
|
||||
|
||||
export const SEARCH_RELAYS: string[] = (import.meta.env.VITE_SEARCH_RELAYS ?? "")
|
||||
.split(",")
|
||||
.map((r: string) => r.trim())
|
||||
@@ -24,32 +27,29 @@ export function parseOutboxRelays(event: NostrEvent): string[] {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export async function getOutboxRelays(pubkey: string): Promise<string[]> {
|
||||
if (!eventStore.hasReplaceable(10002, pubkey) && INDEXER_RELAYS.length) {
|
||||
await lastValueFrom(
|
||||
pool.subscription(INDEXER_RELAYS, { kinds: [10002], authors: [pubkey], limit: 1 })
|
||||
.pipe(storeEvents(eventStore), completeOnEose()),
|
||||
).catch(() => {})
|
||||
}
|
||||
const event = eventStore.getReplaceable(10002, pubkey)
|
||||
// Fetch kind 10002 from the indexer for a single pubkey and return its write relays.
|
||||
export async function loadOutboxRelays(pubkey: string): Promise<string[]> {
|
||||
if (!INDEXER_RELAYS.length) return []
|
||||
const event = await lastValueFrom<NostrEvent>(
|
||||
pool.subscription(INDEXER_RELAYS, { kinds: [10002], authors: [pubkey], limit: 1 })
|
||||
.pipe(storeEvents(eventStore), completeOnEose(), onlyEvents(), takeUntil(timer(RELAY_TIMEOUT_MS))) as any,
|
||||
).catch((): NostrEvent | undefined => undefined)
|
||||
return event ? parseOutboxRelays(event) : []
|
||||
}
|
||||
|
||||
export function fetchRelayLists(pubkeys: string[]): void {
|
||||
const toFetch = pubkeys.filter(pk => pk && !eventStore.hasReplaceable(10002, pk))
|
||||
if (!toFetch.length || !INDEXER_RELAYS.length) return
|
||||
|
||||
pool.subscription(INDEXER_RELAYS, { kinds: [10002], authors: toFetch })
|
||||
// Batch-fetch kind 10002 for multiple pubkeys (fire-and-forget).
|
||||
export function loadRelayLists(pubkeys: string[]): void {
|
||||
if (!pubkeys.length || !INDEXER_RELAYS.length) return
|
||||
pool.subscription(INDEXER_RELAYS, { kinds: [10002], authors: pubkeys })
|
||||
.pipe(storeEvents(eventStore), completeOnEose())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
export function fetchInboxLists(pubkeys: string[]): void {
|
||||
const toFetch = pubkeys.filter(pk => pk && !eventStore.hasReplaceable(10050, pk))
|
||||
if (!toFetch.length) return
|
||||
|
||||
for (const pubkey of toFetch) {
|
||||
getOutboxRelays(pubkey).then(relays => {
|
||||
// Batch-fetch kind 10050 for multiple pubkeys via their outbox relays (fire-and-forget).
|
||||
export function loadMessagingLists(pubkeys: string[]): void {
|
||||
if (!pubkeys.length) return
|
||||
for (const pubkey of pubkeys) {
|
||||
loadOutboxRelays(pubkey).then(relays => {
|
||||
if (!relays.length) return
|
||||
pool.subscription(relays, { kinds: [10050], authors: [pubkey], limit: 1 })
|
||||
.pipe(storeEvents(eventStore), completeOnEose())
|
||||
@@ -58,29 +58,24 @@ export function fetchInboxLists(pubkeys: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateInboxRelays(pubkeys: string[]): Promise<string | null> {
|
||||
// Validate that all pubkeys have a published kind 10050 with at least one relay.
|
||||
export async function validateMessagingRelays(pubkeys: string[]): Promise<string | null> {
|
||||
if (!pubkeys.length) return null
|
||||
if (!INDEXER_RELAYS.length) {
|
||||
return "No indexer relays configured — cannot verify member inbox relay lists"
|
||||
return "No indexer relays configured — cannot verify member messaging relay lists"
|
||||
}
|
||||
|
||||
await Promise.all(pubkeys.map(async pubkey => {
|
||||
if (eventStore.hasReplaceable(10050, pubkey)) return
|
||||
|
||||
const relays = await getOutboxRelays(pubkey)
|
||||
if (!relays.length) return
|
||||
|
||||
await lastValueFrom(
|
||||
const results = await Promise.all(pubkeys.map(async pubkey => {
|
||||
const relays = await loadOutboxRelays(pubkey)
|
||||
if (!relays.length) return false
|
||||
const event = await lastValueFrom<NostrEvent>(
|
||||
pool.subscription(relays, { kinds: [10050], authors: [pubkey], limit: 1 })
|
||||
.pipe(storeEvents(eventStore), completeOnEose()),
|
||||
).catch(() => {})
|
||||
.pipe(storeEvents(eventStore), completeOnEose(), onlyEvents(), takeUntil(timer(RELAY_TIMEOUT_MS))) as any,
|
||||
).catch((): NostrEvent | undefined => undefined)
|
||||
return !!event && parseRelayList(event).length > 0
|
||||
}))
|
||||
|
||||
const missing = pubkeys.filter(pk => {
|
||||
const event = eventStore.getReplaceable(10050, pk)
|
||||
return !event || !parseRelayList(event).length
|
||||
})
|
||||
|
||||
const missing = pubkeys.filter((_, i) => !results[i])
|
||||
if (!missing.length) return null
|
||||
return `No inbox relays found for: ${missing.map(pk => pk.slice(0, 8) + "…").join(", ")}`
|
||||
return `No messaging relays found for: ${missing.map(pk => pk.slice(0, 8) + "…").join(", ")}`
|
||||
}
|
||||
|
||||
+21
-50
@@ -1,37 +1,21 @@
|
||||
import { RelayPool, onlyEvents } from "applesauce-relay"
|
||||
import { EventStore } from "applesauce-core"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import { unlockGiftWrap } from "applesauce-common/helpers/gift-wrap"
|
||||
import type { ISigner } from "applesauce-signers"
|
||||
import { parseJson } from "@welshman/lib"
|
||||
import { storeEvent, getAllEvents, demoteOldEvents } from "./storage"
|
||||
import { demoteOldEvents, storeEvent } from "./storage"
|
||||
|
||||
export const eventStore = new EventStore()
|
||||
|
||||
// Rumors are unsigned inner events — disable sig verification so they store cleanly.
|
||||
eventStore.verifyEvent = undefined
|
||||
// Maintain storage health on startup (fire-and-forget)
|
||||
demoteOldEvents()
|
||||
|
||||
// On startup: run demotion pass then load all stored protocol events into memory.
|
||||
;(async () => {
|
||||
await demoteOldEvents()
|
||||
const events = await getAllEvents()
|
||||
for (const event of events) {
|
||||
eventStore.add(event)
|
||||
}
|
||||
})()
|
||||
|
||||
export function addEvent(event: NostrEvent): void {
|
||||
eventStore.add(event)
|
||||
storeEvent(event)
|
||||
}
|
||||
|
||||
// Shape of the object returned by Observable.subscribe()
|
||||
type Subscription = { unsubscribe(): void }
|
||||
|
||||
export const pool = new RelayPool()
|
||||
|
||||
export const INBOX_RELAYS: string[] = []
|
||||
export const MESSAGING_RELAYS: string[] = []
|
||||
|
||||
// Protocol event kinds produced by protocol.ts (inner rumor kinds)
|
||||
const PROTOCOL_KIND_MIN = 7050
|
||||
const PROTOCOL_KIND_MAX = 7061
|
||||
|
||||
@@ -44,44 +28,31 @@ export function subscribeInbox(
|
||||
throw new Error("Signer does not support NIP-44 encryption")
|
||||
}
|
||||
|
||||
const subscription = pool
|
||||
return pool
|
||||
.subscription(relays, { kinds: [1059], "#p": [pubkey], "#t": ["b7ed"] })
|
||||
.pipe(onlyEvents())
|
||||
.subscribe({
|
||||
next: async (wrap: NostrEvent) => {
|
||||
// Gift wraps are properly signed — add to EventStore as-is.
|
||||
eventStore.add(wrap)
|
||||
|
||||
let rumor
|
||||
try {
|
||||
const sealJson = await signer.nip44!.decrypt(wrap.pubkey, wrap.content)
|
||||
const seal = parseJson<NostrEvent>(sealJson)
|
||||
if (!seal) {
|
||||
return
|
||||
}
|
||||
|
||||
const rumorJson = await signer.nip44!.decrypt(seal.pubkey, seal.content)
|
||||
const rumor = parseJson<NostrEvent>(rumorJson)
|
||||
if (!rumor) {
|
||||
return
|
||||
}
|
||||
|
||||
// NIP-59: the rumor's claimed author must match the seal's authenticated author.
|
||||
// The seal is MAC-authenticated; the rumor's pubkey field is untrusted plaintext.
|
||||
// Without this check a malicious sealer can forge any rumor.pubkey,
|
||||
// which would let them impersonate a FROST participant.
|
||||
if (seal.pubkey !== rumor.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (rumor.kind < PROTOCOL_KIND_MIN || rumor.kind > PROTOCOL_KIND_MAX) {
|
||||
return
|
||||
}
|
||||
|
||||
addEvent(rumor as NostrEvent)
|
||||
// NIP-59 unwrap: decrypts seal → rumor, verifies seal.pubkey === rumor.pubkey.
|
||||
// Seal and rumor are cached in applesauce's internal gift-wrap memory,
|
||||
// intentionally isolated from the main EventStore.
|
||||
rumor = await unlockGiftWrap(wrap, signer)
|
||||
} catch {
|
||||
// Silently skip events that fail to decrypt or parse
|
||||
return
|
||||
}
|
||||
|
||||
if (rumor.kind < PROTOCOL_KIND_MIN || rumor.kind > PROTOCOL_KIND_MAX) return
|
||||
|
||||
// Persist the rumor for log display and session rebuilding.
|
||||
// Rumors are unsigned (no sig); storage serialises to JSON and never verifies.
|
||||
storeEvent(rumor as unknown as NostrEvent)
|
||||
},
|
||||
})
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
export async function publish(relays: string[], event: NostrEvent): Promise<void> {
|
||||
|
||||
+7
-7
@@ -5,9 +5,9 @@ import { registerCommonAccountTypes } from "applesauce-accounts/accounts/common"
|
||||
import type { IAccount } from "applesauce-accounts"
|
||||
import { parseJson } from "@welshman/lib"
|
||||
import { toast } from "solid-toast"
|
||||
import { subscribeInbox, INBOX_RELAYS } from "./nostr"
|
||||
import { subscribeInbox, MESSAGING_RELAYS } from "./nostr"
|
||||
import type { QuorumRecord, DkgSession, ResharingSession, SigningSession } from "./models"
|
||||
import { fetchRelayLists, fetchInboxLists } from "./lib/relays"
|
||||
import { loadRelayLists, loadMessagingLists } from "./lib/relays"
|
||||
import { fetchProfile } from "./lib/profiles"
|
||||
|
||||
// Shape of the object returned by Observable.subscribe()
|
||||
@@ -98,7 +98,7 @@ createEffect(() => {
|
||||
}
|
||||
|
||||
try {
|
||||
inboxSub = subscribeInbox(INBOX_RELAYS, acc.pubkey, acc)
|
||||
inboxSub = subscribeInbox(MESSAGING_RELAYS, acc.pubkey, acc)
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to subscribe to inbox")
|
||||
}
|
||||
@@ -108,8 +108,8 @@ createEffect(() => {
|
||||
createEffect(() => {
|
||||
const pk = account()?.pubkey
|
||||
if (pk) {
|
||||
fetchRelayLists([pk])
|
||||
fetchInboxLists([pk])
|
||||
loadRelayLists([pk])
|
||||
loadMessagingLists([pk])
|
||||
fetchProfile(pk)
|
||||
}
|
||||
})
|
||||
@@ -118,8 +118,8 @@ createEffect(() => {
|
||||
createEffect(() => {
|
||||
const pubkeys = quora.flatMap(q => q.members.map(m => m.pubkey))
|
||||
if (pubkeys.length > 0) {
|
||||
fetchRelayLists(pubkeys)
|
||||
fetchInboxLists(pubkeys)
|
||||
loadRelayLists(pubkeys)
|
||||
loadMessagingLists(pubkeys)
|
||||
for (const pk of pubkeys) { fetchProfile(pk) }
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user