NDK (#1590)
* ndk * fix: remove duplicated zap note event template * don't init Nip07 signer by default * Update wallets/nwc/server.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * nwc protocol parsing workaround * WebSocket polyfill for worker * increase nwc timeout * remove NDKNip46Signer type * fix type annotation * move eslint-disable camelcase to the top * pass event args to the constructor * fix error handling * Update wallets/nwc/index.js Co-authored-by: ekzyis <ek@stacker.news> * Fix type annotation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									52734940a3
								
							
						
					
					
						commit
						d73f6323ff
					
				@ -1,8 +1,7 @@
 | 
			
		||||
import { useCallback } from 'react'
 | 
			
		||||
import { useToast } from './toast'
 | 
			
		||||
import { Button } from 'react-bootstrap'
 | 
			
		||||
import { DEFAULT_CROSSPOSTING_RELAYS, crosspost } from '@/lib/nostr'
 | 
			
		||||
import { callWithTimeout } from '@/lib/time'
 | 
			
		||||
import Nostr, { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
 | 
			
		||||
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
 | 
			
		||||
import { SETTINGS } from '@/fragments/users'
 | 
			
		||||
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
 | 
			
		||||
@ -204,7 +203,7 @@ export default function useCrossposter () {
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
      try {
 | 
			
		||||
        const result = await crosspost(event, failedRelays || relays)
 | 
			
		||||
        const result = await Nostr.crosspost(event, { relays: failedRelays || relays })
 | 
			
		||||
 | 
			
		||||
        if (result.error) {
 | 
			
		||||
          failedRelays = []
 | 
			
		||||
@ -239,13 +238,6 @@ export default function useCrossposter () {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleCrosspost = useCallback(async (itemId) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const pubkey = await callWithTimeout(() => window.nostr.getPublicKey(), 10000)
 | 
			
		||||
      if (!pubkey) throw new Error('failed to get pubkey')
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw new Error(`Nostr extension error: ${e.message}`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let noteId
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										292
									
								
								lib/nostr.js
									
									
									
									
									
								
							
							
						
						
									
										292
									
								
								lib/nostr.js
									
									
									
									
									
								
							@ -1,8 +1,6 @@
 | 
			
		||||
import { bech32 } from 'bech32'
 | 
			
		||||
import { nip19 } from 'nostr-tools'
 | 
			
		||||
import WebSocket from 'isomorphic-ws'
 | 
			
		||||
import { callWithTimeout, withTimeout } from '@/lib/time'
 | 
			
		||||
import crypto from 'crypto'
 | 
			
		||||
import NDK, { NDKEvent, NDKRelaySet, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk'
 | 
			
		||||
 | 
			
		||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
 | 
			
		||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
 | 
			
		||||
@ -17,155 +15,147 @@ export const DEFAULT_CROSSPOSTING_RELAYS = [
 | 
			
		||||
  'wss://nostr.mutinywallet.com/',
 | 
			
		||||
  'wss://relay.mutinywallet.com/'
 | 
			
		||||
]
 | 
			
		||||
export const RELAYS_BLACKLIST = []
 | 
			
		||||
 | 
			
		||||
export class Relay {
 | 
			
		||||
  constructor (relayUrl) {
 | 
			
		||||
    const ws = new WebSocket(relayUrl)
 | 
			
		||||
/* eslint-disable camelcase */
 | 
			
		||||
 | 
			
		||||
    ws.onmessage = (msg) => {
 | 
			
		||||
      const [type, notice] = JSON.parse(msg.data)
 | 
			
		||||
      if (type === 'NOTICE') {
 | 
			
		||||
        console.log('relay notice:', notice)
 | 
			
		||||
      }
 | 
			
		||||
/**
 | 
			
		||||
 * @import {NDKSigner} from '@nostr-dev-kit/ndk'
 | 
			
		||||
 * @import { NDK } from '@nostr-dev-kit/ndk'
 | 
			
		||||
 * @import {NDKNwc} from '@nostr-dev-kit/ndk'
 | 
			
		||||
 * @typedef {Object} Nostr
 | 
			
		||||
 * @property {NDK} ndk
 | 
			
		||||
 * @property {function(string, {logger: Object}): Promise<NDKNwc>} nwc
 | 
			
		||||
 * @property {function(Object, {privKey: string, signer: NDKSigner}): Promise<NDKEvent>} sign
 | 
			
		||||
 * @property {function(Object, {relays: Array<string>, privKey: string, signer: NDKSigner}): Promise<NDKEvent>} publish
 | 
			
		||||
 */
 | 
			
		||||
export class Nostr {
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {NDK}
 | 
			
		||||
   */
 | 
			
		||||
  _ndk = null
 | 
			
		||||
 | 
			
		||||
  constructor ({ privKey, defaultSigner, relays, supportNip07 = false, ...ndkOptions } = {}) {
 | 
			
		||||
    this._ndk = new NDK({
 | 
			
		||||
      explicitRelayUrls: relays,
 | 
			
		||||
      blacklistRelayUrls: RELAYS_BLACKLIST,
 | 
			
		||||
      autoConnectUserRelays: false,
 | 
			
		||||
      autoFetchUserMutelist: false,
 | 
			
		||||
      clientName: 'stacker.news',
 | 
			
		||||
      signer: defaultSigner ?? this.getSigner({ privKey, supportNip07 }),
 | 
			
		||||
      ...ndkOptions
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    ws.onerror = (err) => {
 | 
			
		||||
      console.error('websocket error:', err.message)
 | 
			
		||||
      this.error = err.message
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {NDK}
 | 
			
		||||
   */
 | 
			
		||||
  get ndk () {
 | 
			
		||||
    return this._ndk
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    this.ws = ws
 | 
			
		||||
    this.url = relayUrl
 | 
			
		||||
    this.error = null
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Object} args
 | 
			
		||||
   * @param {string} [args.privKey] - private key to use for signing
 | 
			
		||||
   * @param {boolean} [args.supportNip07] - whether to use NIP-07 signer if available
 | 
			
		||||
   * @returns {NDKPrivateKeySigner | NDKNip07Signer | null} - a signer instance
 | 
			
		||||
   */
 | 
			
		||||
  getSigner ({ privKey, supportNip07 = true } = {}) {
 | 
			
		||||
    if (privKey) return new NDKPrivateKeySigner(privKey)
 | 
			
		||||
    if (supportNip07 && typeof window !== 'undefined' && window?.nostr) return new NDKNip07Signer()
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async connect (url, { timeout } = {}) {
 | 
			
		||||
    const relay = new Relay(url)
 | 
			
		||||
    await relay.waitUntilConnected({ timeout })
 | 
			
		||||
    return relay
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Object} rawEvent
 | 
			
		||||
   * @param {number} rawEvent.kind
 | 
			
		||||
   * @param {number} rawEvent.created_at
 | 
			
		||||
   * @param {string} rawEvent.content
 | 
			
		||||
   * @param {Array<Array<string>>} rawEvent.tags
 | 
			
		||||
   * @param {Object} context
 | 
			
		||||
   * @param {string} context.privKey
 | 
			
		||||
   * @param {NDKSigner} context.signer
 | 
			
		||||
   * @returns {Promise<NDKEvent>}
 | 
			
		||||
   */
 | 
			
		||||
  async sign ({ kind, created_at, content, tags }, { signer } = {}) {
 | 
			
		||||
    const event = new NDKEvent(this.ndk, {
 | 
			
		||||
      kind,
 | 
			
		||||
      created_at,
 | 
			
		||||
      content,
 | 
			
		||||
      tags
 | 
			
		||||
    })
 | 
			
		||||
    signer ??= this.ndk.signer
 | 
			
		||||
    if (!signer) throw new Error('no way to sign this event, please provide a signer or private key')
 | 
			
		||||
    await event.sign(signer)
 | 
			
		||||
    return event
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get connected () {
 | 
			
		||||
    return this.ws.readyState === WebSocket.OPEN
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Object} rawEvent
 | 
			
		||||
   * @param {number} rawEvent.kind
 | 
			
		||||
   * @param {number} rawEvent.created_at
 | 
			
		||||
   * @param {string} rawEvent.content
 | 
			
		||||
   * @param {Array<Array<string>>} rawEvent.tags
 | 
			
		||||
   * @param {Object} context
 | 
			
		||||
   * @param {Array<string>} context.relays
 | 
			
		||||
   * @param {string} context.privKey
 | 
			
		||||
   * @param {NDKSigner} context.signer
 | 
			
		||||
   * @param {number} context.timeout
 | 
			
		||||
   * @returns {Promise<NDKEvent>}
 | 
			
		||||
   */
 | 
			
		||||
  async publish ({ created_at, content, tags = [], kind }, { relays, signer, timeout } = {}) {
 | 
			
		||||
    const event = await this.sign({ kind, created_at, content, tags }, { signer })
 | 
			
		||||
 | 
			
		||||
  get closed () {
 | 
			
		||||
    return this.ws.readyState === WebSocket.CLOSING || this.ws.readyState === WebSocket.CLOSED
 | 
			
		||||
  }
 | 
			
		||||
    const successfulRelays = []
 | 
			
		||||
    const failedRelays = []
 | 
			
		||||
 | 
			
		||||
  async waitUntilConnected ({ timeout } = {}) {
 | 
			
		||||
    let interval
 | 
			
		||||
    const relaySet = NDKRelaySet.fromRelayUrls(relays, this.ndk, true)
 | 
			
		||||
 | 
			
		||||
    const checkPromise = new Promise((resolve, reject) => {
 | 
			
		||||
      interval = setInterval(() => {
 | 
			
		||||
        if (this.connected) {
 | 
			
		||||
          resolve()
 | 
			
		||||
        }
 | 
			
		||||
        if (this.closed) {
 | 
			
		||||
          reject(new Error(`failed to connect to ${this.url}: ` + this.error))
 | 
			
		||||
        }
 | 
			
		||||
      }, 100)
 | 
			
		||||
    event.on('relay:publish:failed', (relay, error) => {
 | 
			
		||||
      failedRelays.push({ relay: relay.url, error })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    for (const relay of (await relaySet.publish(event, timeout))) {
 | 
			
		||||
      successfulRelays.push(relay.url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      event,
 | 
			
		||||
      successfulRelays,
 | 
			
		||||
      failedRelays
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async crosspost ({ created_at, content, tags = [], kind }, { relays = DEFAULT_CROSSPOSTING_RELAYS, signer, timeout } = {}) {
 | 
			
		||||
    try {
 | 
			
		||||
      return await withTimeout(checkPromise, timeout)
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      this.close()
 | 
			
		||||
      throw err
 | 
			
		||||
    } finally {
 | 
			
		||||
      clearInterval(interval)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
      signer ??= this.getSigner({ supportNip07: true })
 | 
			
		||||
      const { event: signedEvent, successfulRelays, failedRelays } = await this.publish({ created_at, content, tags, kind }, { relays, signer, timeout })
 | 
			
		||||
 | 
			
		||||
  close () {
 | 
			
		||||
    const state = this.ws.readyState
 | 
			
		||||
    if (state !== WebSocket.CLOSING && state !== WebSocket.CLOSED) {
 | 
			
		||||
      this.ws.close()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async publish (event, { timeout } = {}) {
 | 
			
		||||
    const ws = this.ws
 | 
			
		||||
 | 
			
		||||
    let listener
 | 
			
		||||
    const ackPromise = new Promise((resolve, reject) => {
 | 
			
		||||
      listener = function onmessage (msg) {
 | 
			
		||||
        const [type, eventId, accepted, reason] = JSON.parse(msg.data)
 | 
			
		||||
 | 
			
		||||
        if (type !== 'OK' || eventId !== event.id) return
 | 
			
		||||
 | 
			
		||||
        if (accepted) {
 | 
			
		||||
          resolve(eventId)
 | 
			
		||||
      let noteId = null
 | 
			
		||||
      if (signedEvent.kind !== 1) {
 | 
			
		||||
        noteId = await nip19.naddrEncode({
 | 
			
		||||
          kind: signedEvent.kind,
 | 
			
		||||
          pubkey: signedEvent.pubkey,
 | 
			
		||||
          identifier: signedEvent.tags[0][1]
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
          reject(new Error(reason || `event rejected: ${eventId}`))
 | 
			
		||||
        }
 | 
			
		||||
        noteId = hexToBech32(signedEvent.id, 'note')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ws.addEventListener('message', listener)
 | 
			
		||||
 | 
			
		||||
      ws.send(JSON.stringify(['EVENT', event]))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      return await withTimeout(ackPromise, timeout)
 | 
			
		||||
    } finally {
 | 
			
		||||
      ws.removeEventListener('message', listener)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async fetch (filter, { timeout } = {}) {
 | 
			
		||||
    const ws = this.ws
 | 
			
		||||
 | 
			
		||||
    let listener
 | 
			
		||||
    const ackPromise = new Promise((resolve, reject) => {
 | 
			
		||||
      const id = crypto.randomBytes(16).toString('hex')
 | 
			
		||||
 | 
			
		||||
      const events = []
 | 
			
		||||
      let eose = false
 | 
			
		||||
 | 
			
		||||
      listener = function onmessage (msg) {
 | 
			
		||||
        const [type, subId, event] = JSON.parse(msg.data)
 | 
			
		||||
 | 
			
		||||
        if (subId !== id) return
 | 
			
		||||
 | 
			
		||||
        if (type === 'EVENT') {
 | 
			
		||||
          events.push(event)
 | 
			
		||||
          if (eose) {
 | 
			
		||||
            // EOSE was already received:
 | 
			
		||||
            // return first event after EOSE
 | 
			
		||||
            resolve(events)
 | 
			
		||||
          }
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (type === 'CLOSED') {
 | 
			
		||||
          return resolve(events)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (type === 'EOSE') {
 | 
			
		||||
          eose = true
 | 
			
		||||
          if (events.length > 0) {
 | 
			
		||||
            // we already received events before EOSE:
 | 
			
		||||
            // return all events before EOSE
 | 
			
		||||
            ws.send(JSON.stringify(['CLOSE', id]))
 | 
			
		||||
            return resolve(events)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ws.addEventListener('message', listener)
 | 
			
		||||
 | 
			
		||||
      ws.send(JSON.stringify(['REQ', id, ...filter]))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      return await withTimeout(ackPromise, timeout)
 | 
			
		||||
    } finally {
 | 
			
		||||
      ws.removeEventListener('message', listener)
 | 
			
		||||
      return { successfulRelays, failedRelays, noteId }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Crosspost error:', error)
 | 
			
		||||
      return { error }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type {Nostr}
 | 
			
		||||
 */
 | 
			
		||||
export default new Nostr()
 | 
			
		||||
 | 
			
		||||
export function hexToBech32 (hex, prefix = 'npub') {
 | 
			
		||||
  return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
 | 
			
		||||
}
 | 
			
		||||
@ -186,49 +176,3 @@ export function nostrZapDetails (zap) {
 | 
			
		||||
 | 
			
		||||
  return { npub, content, note }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function publishNostrEvent (signedEvent, relayUrl) {
 | 
			
		||||
  const timeout = 3000
 | 
			
		||||
  const relay = await Relay.connect(relayUrl, { timeout })
 | 
			
		||||
  try {
 | 
			
		||||
    await relay.publish(signedEvent, { timeout })
 | 
			
		||||
  } finally {
 | 
			
		||||
    relay.close()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
 | 
			
		||||
  try {
 | 
			
		||||
    const signedEvent = await callWithTimeout(() => window.nostr.signEvent(event), 10000)
 | 
			
		||||
    if (!signedEvent) throw new Error('failed to sign event')
 | 
			
		||||
 | 
			
		||||
    const promises = relays.map(r => publishNostrEvent(signedEvent, r))
 | 
			
		||||
    const results = await Promise.allSettled(promises)
 | 
			
		||||
    const successfulRelays = []
 | 
			
		||||
    const failedRelays = []
 | 
			
		||||
 | 
			
		||||
    results.forEach((result, index) => {
 | 
			
		||||
      if (result.status === 'fulfilled') {
 | 
			
		||||
        successfulRelays.push(relays[index])
 | 
			
		||||
      } else {
 | 
			
		||||
        failedRelays.push({ relay: relays[index], error: result.reason })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    let noteId = null
 | 
			
		||||
    if (signedEvent.kind !== 1) {
 | 
			
		||||
      noteId = await nip19.naddrEncode({
 | 
			
		||||
        kind: signedEvent.kind,
 | 
			
		||||
        pubkey: signedEvent.pubkey,
 | 
			
		||||
        identifier: signedEvent.tags[0][1]
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      noteId = hexToBech32(signedEvent.id, 'note')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { successfulRelays, failedRelays, noteId }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Crosspost error:', error)
 | 
			
		||||
    return { error }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -203,12 +203,12 @@ export function parseNwcUrl (walletConnectUrl) {
 | 
			
		||||
  const params = {}
 | 
			
		||||
  params.walletPubkey = url.host
 | 
			
		||||
  const secret = url.searchParams.get('secret')
 | 
			
		||||
  const relayUrl = url.searchParams.get('relay')
 | 
			
		||||
  const relayUrls = url.searchParams.getAll('relay')
 | 
			
		||||
  if (secret) {
 | 
			
		||||
    params.secret = secret
 | 
			
		||||
  }
 | 
			
		||||
  if (relayUrl) {
 | 
			
		||||
    params.relayUrl = relayUrl
 | 
			
		||||
  if (relayUrls) {
 | 
			
		||||
    params.relayUrls = relayUrls
 | 
			
		||||
  }
 | 
			
		||||
  return params
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -147,15 +147,15 @@ addMethod(string, 'nwcUrl', function () {
 | 
			
		||||
      // inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
 | 
			
		||||
      try {
 | 
			
		||||
        string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validateSync(nwcUrl)
 | 
			
		||||
        let relayUrl, walletPubkey, secret
 | 
			
		||||
        let relayUrls, walletPubkey, secret
 | 
			
		||||
        try {
 | 
			
		||||
          ({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl))
 | 
			
		||||
          ({ relayUrls, walletPubkey, secret } = parseNwcUrl(nwcUrl))
 | 
			
		||||
        } catch {
 | 
			
		||||
          // invalid URL error. handle as if pubkey validation failed to not confuse user.
 | 
			
		||||
          throw new Error('pubkey must be 64 hex chars')
 | 
			
		||||
        }
 | 
			
		||||
        string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey)
 | 
			
		||||
        string().required('relay url required').trim().wss('relay must use wss://').validateSync(relayUrl)
 | 
			
		||||
        array().of(string().required('relay url required').trim().wss('relay must use wss://')).min(1, 'at least one relay required').validateSync(relayUrls)
 | 
			
		||||
        string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret)
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        return context.createError({ message: err.message })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										273
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										273
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -15,6 +15,7 @@
 | 
			
		||||
        "@graphql-tools/schema": "^10.0.6",
 | 
			
		||||
        "@lightninglabs/lnc-web": "^0.3.2-alpha",
 | 
			
		||||
        "@noble/curves": "^1.6.0",
 | 
			
		||||
        "@nostr-dev-kit/ndk": "^2.10.5",
 | 
			
		||||
        "@opensearch-project/opensearch": "^2.12.0",
 | 
			
		||||
        "@prisma/client": "^5.20.0",
 | 
			
		||||
        "@slack/web-api": "^7.6.0",
 | 
			
		||||
@ -4371,6 +4372,15 @@
 | 
			
		||||
        "url": "https://paulmillr.com/funding/"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@noble/secp256k1": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://paulmillr.com/funding/"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@nodelib/fs.scandir": {
 | 
			
		||||
      "version": "2.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
			
		||||
@ -4406,6 +4416,49 @@
 | 
			
		||||
        "node": ">= 8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@nostr-dev-kit/ndk": {
 | 
			
		||||
      "version": "2.10.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.5.tgz",
 | 
			
		||||
      "integrity": "sha512-QEnarJL9BGCxeenSIE9jxNSDyYQYjzD30YL3sVJ9cNybNZX8tl/I1/vhEUeRRMBz/qjROLtt0M2RV68rZ205tg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@noble/curves": "^1.6.0",
 | 
			
		||||
        "@noble/hashes": "^1.5.0",
 | 
			
		||||
        "@noble/secp256k1": "^2.1.0",
 | 
			
		||||
        "@scure/base": "^1.1.9",
 | 
			
		||||
        "debug": "^4.3.6",
 | 
			
		||||
        "light-bolt11-decoder": "^3.2.0",
 | 
			
		||||
        "nostr-tools": "^2.7.1",
 | 
			
		||||
        "tseep": "^1.2.2",
 | 
			
		||||
        "typescript-lru-cache": "^2.0.0",
 | 
			
		||||
        "utf8-buffer": "^1.0.0",
 | 
			
		||||
        "websocket-polyfill": "^0.0.3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^14.21.3 || >=16"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://paulmillr.com/funding/"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@nostr-dev-kit/ndk/node_modules/@scure/base": {
 | 
			
		||||
      "version": "1.1.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
 | 
			
		||||
      "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://paulmillr.com/funding/"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@opensearch-project/opensearch": {
 | 
			
		||||
      "version": "2.12.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.12.0.tgz",
 | 
			
		||||
@ -7310,6 +7363,19 @@
 | 
			
		||||
        "node": ">=4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bufferutil": {
 | 
			
		||||
      "version": "4.0.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
 | 
			
		||||
      "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "node-gyp-build": "^4.3.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.14.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/builtins": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
 | 
			
		||||
@ -8089,6 +8155,19 @@
 | 
			
		||||
        "node": ">= 10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "es5-ext": "^0.10.64",
 | 
			
		||||
        "type": "^2.7.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-array": {
 | 
			
		||||
      "version": "3.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
 | 
			
		||||
@ -8968,6 +9047,46 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/ljharb"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/es5-ext": {
 | 
			
		||||
      "version": "0.10.64",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
 | 
			
		||||
      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "es6-iterator": "^2.0.3",
 | 
			
		||||
        "es6-symbol": "^3.1.3",
 | 
			
		||||
        "esniff": "^2.0.1",
 | 
			
		||||
        "next-tick": "^1.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/es6-iterator": {
 | 
			
		||||
      "version": "2.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d": "1",
 | 
			
		||||
        "es5-ext": "^0.10.35",
 | 
			
		||||
        "es6-symbol": "^3.1.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/es6-symbol": {
 | 
			
		||||
      "version": "3.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d": "^1.0.2",
 | 
			
		||||
        "ext": "^1.7.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/esbuild": {
 | 
			
		||||
      "version": "0.23.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
 | 
			
		||||
@ -9581,6 +9700,21 @@
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/esniff": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d": "^1.0.1",
 | 
			
		||||
        "es5-ext": "^0.10.62",
 | 
			
		||||
        "event-emitter": "^0.3.5",
 | 
			
		||||
        "type": "^2.7.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/espree": {
 | 
			
		||||
      "version": "10.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
 | 
			
		||||
@ -9675,6 +9809,16 @@
 | 
			
		||||
        "node": ">= 0.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/event-emitter": {
 | 
			
		||||
      "version": "0.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
 | 
			
		||||
      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d": "1",
 | 
			
		||||
        "es5-ext": "~0.10.14"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/eventemitter3": {
 | 
			
		||||
      "version": "4.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
 | 
			
		||||
@ -9829,6 +9973,15 @@
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ext": {
 | 
			
		||||
      "version": "1.7.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
 | 
			
		||||
      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "type": "^2.7.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/extend": {
 | 
			
		||||
      "version": "3.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
 | 
			
		||||
@ -14154,6 +14307,15 @@
 | 
			
		||||
        "node": ">= 0.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/light-bolt11-decoder": {
 | 
			
		||||
      "version": "3.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@scure/base": "1.1.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lightning": {
 | 
			
		||||
      "version": "10.22.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lightning/-/lightning-10.22.0.tgz",
 | 
			
		||||
@ -15606,6 +15768,12 @@
 | 
			
		||||
        "react-dom": ">=16.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/next-tick": {
 | 
			
		||||
      "version": "1.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
 | 
			
		||||
      "license": "ISC"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/no-case": {
 | 
			
		||||
      "version": "3.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
 | 
			
		||||
@ -19408,11 +19576,23 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tsdef/-/tsdef-0.0.14.tgz",
 | 
			
		||||
      "integrity": "sha512-UjMD4XKRWWFlFBfwKVQmGFT5YzW/ZaF8x6KpCDf92u9wgKeha/go3FU0e5WqDjXsCOdfiavCkfwfVHNDxRDGMA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tseep": {
 | 
			
		||||
      "version": "1.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tslib": {
 | 
			
		||||
      "version": "2.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tstl": {
 | 
			
		||||
      "version": "2.5.16",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
 | 
			
		||||
      "integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tsx": {
 | 
			
		||||
      "version": "4.19.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz",
 | 
			
		||||
@ -19452,6 +19632,12 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/type": {
 | 
			
		||||
      "version": "2.7.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
 | 
			
		||||
      "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
 | 
			
		||||
      "license": "ISC"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/type-check": {
 | 
			
		||||
      "version": "0.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
 | 
			
		||||
@ -19574,11 +19760,26 @@
 | 
			
		||||
        "node": ">= 18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/typedarray-to-buffer": {
 | 
			
		||||
      "version": "3.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "is-typedarray": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/typeforce": {
 | 
			
		||||
      "version": "1.18.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
 | 
			
		||||
      "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/typescript-lru-cache": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/uint8array-tools": {
 | 
			
		||||
      "version": "0.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz",
 | 
			
		||||
@ -19997,6 +20198,28 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/utf-8-validate": {
 | 
			
		||||
      "version": "5.0.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
 | 
			
		||||
      "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "node-gyp-build": "^4.3.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.14.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/utf8-buffer": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/util": {
 | 
			
		||||
      "version": "0.12.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
 | 
			
		||||
@ -20320,6 +20543,47 @@
 | 
			
		||||
        "npm": ">=3.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/websocket": {
 | 
			
		||||
      "version": "1.0.35",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
 | 
			
		||||
      "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "bufferutil": "^4.0.1",
 | 
			
		||||
        "debug": "^2.2.0",
 | 
			
		||||
        "es5-ext": "^0.10.63",
 | 
			
		||||
        "typedarray-to-buffer": "^3.1.5",
 | 
			
		||||
        "utf-8-validate": "^5.0.2",
 | 
			
		||||
        "yaeti": "^0.0.6"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/websocket-polyfill": {
 | 
			
		||||
      "version": "0.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tstl": "^2.0.7",
 | 
			
		||||
        "websocket": "^1.0.28"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/websocket/node_modules/debug": {
 | 
			
		||||
      "version": "2.6.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
 | 
			
		||||
      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "ms": "2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/websocket/node_modules/ms": {
 | 
			
		||||
      "version": "2.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/whatwg-encoding": {
 | 
			
		||||
      "version": "3.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
 | 
			
		||||
@ -20896,6 +21160,15 @@
 | 
			
		||||
        "node": ">=10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/yaeti": {
 | 
			
		||||
      "version": "0.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.32"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/yallist": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@
 | 
			
		||||
    "@graphql-tools/schema": "^10.0.6",
 | 
			
		||||
    "@lightninglabs/lnc-web": "^0.3.2-alpha",
 | 
			
		||||
    "@noble/curves": "^1.6.0",
 | 
			
		||||
    "@nostr-dev-kit/ndk": "^2.10.5",
 | 
			
		||||
    "@opensearch-project/opensearch": "^2.12.0",
 | 
			
		||||
    "@prisma/client": "^5.20.0",
 | 
			
		||||
    "@slack/web-api": "^7.6.0",
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
 | 
			
		||||
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
 | 
			
		||||
export * from '@/wallets/nwc'
 | 
			
		||||
 | 
			
		||||
export async function testSendPayment ({ nwcUrl }, { logger }) {
 | 
			
		||||
@ -11,11 +11,7 @@ export async function testSendPayment ({ nwcUrl }, { logger }) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function sendPayment (bolt11, { nwcUrl }, { logger }) {
 | 
			
		||||
  const result = await nwcCall({
 | 
			
		||||
    nwcUrl,
 | 
			
		||||
    method: 'pay_invoice',
 | 
			
		||||
    params: { invoice: bolt11 }
 | 
			
		||||
  },
 | 
			
		||||
  { logger })
 | 
			
		||||
  const nwc = await getNwc(nwcUrl)
 | 
			
		||||
  const result = await nwcTryRun(() => nwc.payInvoice(bolt11))
 | 
			
		||||
  return result.preimage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Relay } from '@/lib/nostr'
 | 
			
		||||
import { parseNwcUrl } from '@/lib/url'
 | 
			
		||||
import Nostr from '@/lib/nostr'
 | 
			
		||||
import { string } from '@/lib/yup'
 | 
			
		||||
import { finalizeEvent, nip04, verifyEvent } from 'nostr-tools'
 | 
			
		||||
import { parseNwcUrl } from '@/lib/url'
 | 
			
		||||
import { NDKNwc } from '@nostr-dev-kit/ndk'
 | 
			
		||||
 | 
			
		||||
export const name = 'nwc'
 | 
			
		||||
export const walletType = 'NWC'
 | 
			
		||||
@ -33,61 +33,38 @@ export const card = {
 | 
			
		||||
  subtitle: 'use Nostr Wallet Connect for payments'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } = {}) {
 | 
			
		||||
  const { relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)
 | 
			
		||||
 | 
			
		||||
  const relay = await Relay.connect(relayUrl, { timeout })
 | 
			
		||||
  logger?.ok(`connected to ${relayUrl}`)
 | 
			
		||||
export async function getNwc (nwcUrl, { timeout = 5e4 } = {}) {
 | 
			
		||||
  const ndk = Nostr.ndk
 | 
			
		||||
  const { walletPubkey, secret, relayUrls } = parseNwcUrl(nwcUrl)
 | 
			
		||||
  const nwc = new NDKNwc({
 | 
			
		||||
    ndk,
 | 
			
		||||
    pubkey: walletPubkey,
 | 
			
		||||
    relayUrls,
 | 
			
		||||
    secret
 | 
			
		||||
  })
 | 
			
		||||
  await nwc.blockUntilReady(timeout)
 | 
			
		||||
  return nwc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Run a nwc function and throw if it errors
 | 
			
		||||
 * (workaround to handle ambiguous NDK error handling)
 | 
			
		||||
 * @param {function} fun - the nwc function to run
 | 
			
		||||
 * @returns - the result of the nwc function
 | 
			
		||||
 */
 | 
			
		||||
export async function nwcTryRun (fun) {
 | 
			
		||||
  try {
 | 
			
		||||
    const payload = { method, params }
 | 
			
		||||
    const encrypted = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
 | 
			
		||||
 | 
			
		||||
    const request = finalizeEvent({
 | 
			
		||||
      kind: 23194,
 | 
			
		||||
      created_at: Math.floor(Date.now() / 1000),
 | 
			
		||||
      tags: [['p', walletPubkey]],
 | 
			
		||||
      content: encrypted
 | 
			
		||||
    }, secret)
 | 
			
		||||
 | 
			
		||||
    // we need to subscribe to the response before publishing the request
 | 
			
		||||
    // since NWC events are ephemeral (20000 <= kind < 30000)
 | 
			
		||||
    const subscription = relay.fetch([{
 | 
			
		||||
      kinds: [23195],
 | 
			
		||||
      authors: [walletPubkey],
 | 
			
		||||
      '#e': [request.id]
 | 
			
		||||
    }], { timeout })
 | 
			
		||||
 | 
			
		||||
    await relay.publish(request, { timeout })
 | 
			
		||||
 | 
			
		||||
    logger?.info(`published ${method} request`)
 | 
			
		||||
 | 
			
		||||
    logger?.info(`waiting for ${method} response ...`)
 | 
			
		||||
 | 
			
		||||
    const [response] = await subscription
 | 
			
		||||
 | 
			
		||||
    if (!response) {
 | 
			
		||||
      throw new Error(`no ${method} response`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger?.ok(`${method} response received`)
 | 
			
		||||
 | 
			
		||||
    if (!verifyEvent(response)) throw new Error(`invalid ${method} response: failed to verify`)
 | 
			
		||||
 | 
			
		||||
    const decrypted = await nip04.decrypt(secret, walletPubkey, response.content)
 | 
			
		||||
    const content = JSON.parse(decrypted)
 | 
			
		||||
 | 
			
		||||
    if (content.error) throw new Error(content.error.message)
 | 
			
		||||
    if (content.result) return content.result
 | 
			
		||||
 | 
			
		||||
    throw new Error(`invalid ${method} response: missing error or result`)
 | 
			
		||||
  } finally {
 | 
			
		||||
    relay?.close()
 | 
			
		||||
    logger?.info(`closed connection to ${relayUrl}`)
 | 
			
		||||
    const { error, result } = await fun()
 | 
			
		||||
    if (error) throw new Error(error.message || error.code)
 | 
			
		||||
    return result
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if (e.error) throw new Error(e.error.message || e.error.code)
 | 
			
		||||
    throw e
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function supportedMethods (nwcUrl, { logger, timeout } = {}) {
 | 
			
		||||
  const result = await nwcCall({ nwcUrl, method: 'get_info' }, { logger, timeout })
 | 
			
		||||
  const nwc = await getNwc(nwcUrl, { timeout })
 | 
			
		||||
  const result = await nwcTryRun(() => nwc.getInfo())
 | 
			
		||||
  return result.methods
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { withTimeout } from '@/lib/time'
 | 
			
		||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
 | 
			
		||||
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
 | 
			
		||||
export * from '@/wallets/nwc'
 | 
			
		||||
 | 
			
		||||
export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
 | 
			
		||||
@ -23,17 +23,8 @@ export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
 | 
			
		||||
  return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }, { logger }), timeout)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function createInvoice (
 | 
			
		||||
  { msats, description, expiry },
 | 
			
		||||
  { nwcUrlRecv }, { logger }) {
 | 
			
		||||
  const result = await nwcCall({
 | 
			
		||||
    nwcUrl: nwcUrlRecv,
 | 
			
		||||
    method: 'make_invoice',
 | 
			
		||||
    params: {
 | 
			
		||||
      amount: msats,
 | 
			
		||||
      description,
 | 
			
		||||
      expiry
 | 
			
		||||
    }
 | 
			
		||||
  }, { logger })
 | 
			
		||||
export async function createInvoice ({ msats, description, expiry }, { nwcUrlRecv }, { logger }) {
 | 
			
		||||
  const nwc = await getNwc(nwcUrlRecv)
 | 
			
		||||
  const result = await nwcTryRun(() => nwc.sendReq('make_invoice', { amount: msats, description, expiry }))
 | 
			
		||||
  return result.invoice
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,12 @@ import { expireBoost } from './expireBoost'
 | 
			
		||||
import { payingActionConfirmed, payingActionFailed } from './payingAction'
 | 
			
		||||
import { autoDropBolt11s } from './autoDropBolt11'
 | 
			
		||||
 | 
			
		||||
// WebSocket polyfill
 | 
			
		||||
import ws from 'isomorphic-ws'
 | 
			
		||||
if (typeof WebSocket === 'undefined') {
 | 
			
		||||
  global.WebSocket = ws
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function work () {
 | 
			
		||||
  const boss = new PgBoss(process.env.DATABASE_URL)
 | 
			
		||||
  const models = createPrisma({
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import { signId, calculateId, getPublicKey } from 'nostr'
 | 
			
		||||
import { Relay } from '@/lib/nostr'
 | 
			
		||||
import Nostr from '@/lib/nostr'
 | 
			
		||||
 | 
			
		||||
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
 | 
			
		||||
 | 
			
		||||
@ -40,26 +39,18 @@ export async function nip57 ({ data: { hash }, boss, lnd, models }) {
 | 
			
		||||
 | 
			
		||||
    const e = {
 | 
			
		||||
      kind: 9735,
 | 
			
		||||
      pubkey: getPublicKey(process.env.NOSTR_PRIVATE_KEY),
 | 
			
		||||
      created_at: Math.floor(new Date(inv.confirmedAt).getTime() / 1000),
 | 
			
		||||
      content: '',
 | 
			
		||||
      tags
 | 
			
		||||
    }
 | 
			
		||||
    e.id = await calculateId(e)
 | 
			
		||||
    e.sig = await signId(process.env.NOSTR_PRIVATE_KEY, e.id)
 | 
			
		||||
 | 
			
		||||
    console.log('zap note', e, relays)
 | 
			
		||||
    await Promise.allSettled(
 | 
			
		||||
      relays.map(async r => {
 | 
			
		||||
        const timeout = 1000
 | 
			
		||||
        const relay = await Relay.connect(r, { timeout })
 | 
			
		||||
        try {
 | 
			
		||||
          await relay.publish(e, { timeout })
 | 
			
		||||
        } finally {
 | 
			
		||||
          relay.close()
 | 
			
		||||
        }
 | 
			
		||||
    const signer = Nostr.getSigner({ privKey: process.env.NOSTR_PRIVATE_KEY })
 | 
			
		||||
    await Nostr.publish(e, {
 | 
			
		||||
      relays,
 | 
			
		||||
      signer,
 | 
			
		||||
      timeout: 1000
 | 
			
		||||
    })
 | 
			
		||||
    )
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.log(e)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user