Skip to content

@snort/shared Examples

Real-world usage of ExternalStore, TLV utilities, LNURL, work queues, and FeedCache from the Snort app.

ExternalStore for global state with React integration

Extend ExternalStore to create singleton state stores, then consume with useSyncExternalStore:

WalletStore — managing wallet configurations:

typescript
// Wallet/index.ts
export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
  #configs: Array<WalletConfig>

  save() {
    const json = JSON.stringify(this.#configs)
    window.localStorage.setItem("wallet-config", json)
    this.notifyChange()
  }

  takeSnapshot(): WalletStoreSnapshot {
    return {
      configs: [...this.#configs],
      config: this.#configs.find(a => a.active),
      wallet: this.get(),
    }
  }
}

// Consumed via React:
export function useWallet() {
  const wallet = useSyncExternalStore<WalletStoreSnapshot>(
    h => Wallets.hook(h),
    () => Wallets.snapshot(),
  )
}

NoteCreatorStore — note creation UI state with selector support:

typescript
// State/NoteCreator.ts
class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
  #updateFn = (fn: (v: NoteCreatorDataSnapshot) => void) => {
    fn(this.#data)
    this.notifyChange(this.#data)  // pass data to skip snapshot for perf
  }

  takeSnapshot(): NoteCreatorDataSnapshot {
    const sn = { ...this.#data, reset: this.#resetFn, update: this.#updateFn }
    return sn as NoteCreatorDataSnapshot
  }
}

// Consumed with selector support:
export function useNoteCreator<T extends object = NoteCreatorDataSnapshot>(
  selector?: (v: NoteCreatorDataSnapshot) => T,
) {
  return useSyncExternalStoreWithSelector<NoteCreatorDataSnapshot, T>(
    c => NoteCreatorState.hook(c),
    () => NoteCreatorState.snapshot(),
    undefined,
    selector || defaultSelector,
  )
}

ToasterSlots — toast notification stack:

typescript
// Toaster/Toaster.tsx
class ToasterSlots extends ExternalStore<Array<ToastNotification>> {
  #stack: Array<ToastNotification> = []

  push(n: ToastNotification) {
    n.expire ??= unixNow() + 10
    this.#stack.push(n)
    this.notifyChange()
  }

  takeSnapshot(): ToastNotification[] {
    return [...this.#stack]
  }
}
export const Toastore = new ToasterSlots()

TLV encoding for chat IDs

typescript
// chat/nip17.ts
function computeChatId(u: UnwrappedGift, pk: string): string | undefined {
  const pTags = [...]
    .filter((v, i, a) => a.indexOf(v) === i)
    .sort()
    .filter(a => a !== pk)

  return encodeTLVEntries(
    "nchat17",
    ...pTags.map(v => ({
      value: v,
      type: TLVEntryType.Author,
      length: v.length,
    }) as TLVEntry),
  )
}

// Decoding:
const participants = decodeTLV(id)
  .filter(v => v.type === TLVEntryType.Author)
  .map(v => ({ type: "pubkey" as const, id: v.value as string }))

bech32ToHex / hexToBech32 for entity ID conversion

typescript
// Utils/index.ts
export function parseId(id: string) {
  const hrp = ["note", "npub", "nsec"]
  try {
    if (hrp.some(a => id.startsWith(a))) {
      return bech32ToHex(id)
    }
  } catch (_e) {}
  return id
}

export function eventLink(hex: string, relays?: Array<string> | string) {
  const encoded = relays
    ? encodeTLV(NostrPrefix.Event, hexToBytes(hex), Array.isArray(relays) ? relays : [relays])
    : hexToBech32(NostrPrefix.Note, hex)
  return `/${encoded}`
}

LNURL for ZapPool payouts

typescript
// ZapPoolController.ts
async payout(wallet: LNWallet) {
  for (const x of this.#store.values()) {
    const profile = await ProfilesCache.get(x.pubkey)
    const svc = new LNURL(profile.lud16 || profile.lud06 || "")
    await svc.load()
    const invoice = await svc.getInvoice(amtSend, `SnortZapPool: ${x.split}%`)
    if (invoice.pr) {
      const result = await wallet.payInvoice(invoice.pr)
    }
  }
}

processWorkQueue / barrierQueue for serialized async operations

typescript
// ZapperQueue.tsx
export const ZapperQueue: Array<WorkQueueItem> = []
processWorkQueue(ZapperQueue)

// FooterZapButton.tsx
await barrierQueue(ZapperQueue, async () => {
  const zapper = new Zapper(system, publisher)
  const result = await zapper.send(wallet, targets, amount)
})

FeedCache as a base class for refresh feeds

typescript
// RefreshFeedCache.ts
export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> {
  abstract buildSub(session: LoginSession, rb: RequestBuilder): void
  abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pubKey: string, pub?: EventPublisher): void

  protected newest(filter?: (e: TWithCreated<T>) => boolean) {
    let ret = 0
    this.cache.forEach(v => {
      if (!filter || filter(v)) {
        ret = v.created_at > ret ? v.created_at : ret
      }
    })
    return ret
  }
}