stacker.news/lib/nostr.js

204 lines
6.3 KiB
JavaScript
Raw Normal View History

import { bech32 } from 'bech32'
Nostr crossposting all item types (#779) * crosspost-item * crosspost old items, update with nEventId * Updating noteId encoding, cleaning up a little * Fixing item-info condition, cleaning up * Linting * Add createdAt variable back * Change instances of eventId to noteId * Adding upsertNoteId mutation * Cleaning up updateItem, using toasts to communivate success/failure in crosspost-item * Linting * Move crosspost to share button, make sure only OP can crosspost * Lint * Simplify conditions * user might have no nostr extension installed Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com> * change upsertNoteId to updateNoteID for resolver and mutations, change isOp to mine, remove unused noteId params * Basic setup for crossposting poll / link items * post rebase fixes and Bounty and job crossposts * Job crossposting working * adding back accidentally removed import * Lint / rebase * Outsource as much crossposting logic from discussion-form into use-crossposter as possible * Fix incorrect property for user relays, fix itemId param in updateNoteId * Fix toast messages / error cases in use-crossposter * Update item forms to for updated use-crossposter hook * CrosspostDropdownItem in share updated to accomodate use-crossposter update * Encode paramaterized replacable event id's in naddress format with nostr-tools, bounty to follw nip-99 spec * Increase timeout on relay connection / cleaning up * No longer crossposting job * Add blastr, fix crosspost button in item-info for polls/discussions, finish removing job crosspostr code * Fix toaster error, create reusable crossposterror function to surface toaster * Cleaning up / comments / linting * Update copy * Simplify CrosspostdropdownItem, keep replies from being crossposted * Moved query for missing item fields when crossposting to use-crossposter hook * Remove unneeded param in CrosspostDropdownItem, lint * Small fixes post rebase * Remove unused import * fix nostr-tools version, fix package-lock.json * Update components/item-info.js Co-authored-by: ekzyis <ek@stacker.news> * Remove unused param, determine poll item type from pollCost field, add mutiny strfry relay to defaults * Update toaster implementations, use no-cache for item query, restructure crosspostItem to use await with try catch * crosspost info modal that lives under adv-post-form now has dynamic crossposting info * Move determineItemType into handleEventCreation, mover item/event handing outside of do ... while loop * Lint * Reconcile skip method with onCancel function in toaster * Handle failedRelays being undefined * determine item type from router.query.type if available otherwise use item fields * Initiliaze failerRelays as undefined but handle error explicitly * Lint * Fix crosspost default value for link, poll, bounty forms --------- Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com> Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-22 01:18:36 +00:00
import { nip19 } from 'nostr-tools'
import NDK, { NDKEvent, NDKNip46Signer, NDKRelaySet, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk'
2023-02-08 19:38:04 +00:00
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
export const NOSTR_MAX_RELAY_NUM = 20
export const NOSTR_ZAPPLE_PAY_NPUB = 'npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan'
export const DEFAULT_CROSSPOSTING_RELAYS = [
'wss://nostrue.com/',
'wss://relay.damus.io/',
'wss://relay.nostr.band/',
'wss://relay.snort.social/',
Nostr crossposting all item types (#779) * crosspost-item * crosspost old items, update with nEventId * Updating noteId encoding, cleaning up a little * Fixing item-info condition, cleaning up * Linting * Add createdAt variable back * Change instances of eventId to noteId * Adding upsertNoteId mutation * Cleaning up updateItem, using toasts to communivate success/failure in crosspost-item * Linting * Move crosspost to share button, make sure only OP can crosspost * Lint * Simplify conditions * user might have no nostr extension installed Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com> * change upsertNoteId to updateNoteID for resolver and mutations, change isOp to mine, remove unused noteId params * Basic setup for crossposting poll / link items * post rebase fixes and Bounty and job crossposts * Job crossposting working * adding back accidentally removed import * Lint / rebase * Outsource as much crossposting logic from discussion-form into use-crossposter as possible * Fix incorrect property for user relays, fix itemId param in updateNoteId * Fix toast messages / error cases in use-crossposter * Update item forms to for updated use-crossposter hook * CrosspostDropdownItem in share updated to accomodate use-crossposter update * Encode paramaterized replacable event id's in naddress format with nostr-tools, bounty to follw nip-99 spec * Increase timeout on relay connection / cleaning up * No longer crossposting job * Add blastr, fix crosspost button in item-info for polls/discussions, finish removing job crosspostr code * Fix toaster error, create reusable crossposterror function to surface toaster * Cleaning up / comments / linting * Update copy * Simplify CrosspostdropdownItem, keep replies from being crossposted * Moved query for missing item fields when crossposting to use-crossposter hook * Remove unneeded param in CrosspostDropdownItem, lint * Small fixes post rebase * Remove unused import * fix nostr-tools version, fix package-lock.json * Update components/item-info.js Co-authored-by: ekzyis <ek@stacker.news> * Remove unused param, determine poll item type from pollCost field, add mutiny strfry relay to defaults * Update toaster implementations, use no-cache for item query, restructure crosspostItem to use await with try catch * crosspost info modal that lives under adv-post-form now has dynamic crossposting info * Move determineItemType into handleEventCreation, mover item/event handing outside of do ... while loop * Lint * Reconcile skip method with onCancel function in toaster * Handle failedRelays being undefined * determine item type from router.query.type if available otherwise use item fields * Initiliaze failerRelays as undefined but handle error explicitly * Lint * Fix crosspost default value for link, poll, bounty forms --------- Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com> Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-22 01:18:36 +00:00
'wss://nostr21.com/',
'wss://nostr.mutinywallet.com/',
'wss://relay.mutinywallet.com/'
]
export const RELAYS_BLACKLIST = []
/* eslint-disable camelcase */
/**
* @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 default class Nostr {
/**
* @type {NDK}
*/
_ndk = null
static globalInstance = null
constructor ({ privKey, defaultSigner, relays, nip46token, 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, nip46token }),
...ndkOptions
})
}
/**
* @type {NDK}
*/
static get () {
if (!Nostr.globalInstance) {
Nostr.globalInstance = new Nostr()
}
return Nostr.globalInstance
}
/**
* @type {NDK}
*/
get ndk () {
return this._ndk
}
/**
*
* @param {Object} param0
* @param {string} [args.privKey] - private key to use for signing
* @param {string} [args.nip46token] - NIP-46 token to use for signing
* @param {boolean} [args.supportNip07] - whether to use NIP-07 signer if available
* @returns {NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer | null} - a signer instance
*/
getSigner ({ privKey, nip46token, supportNip07 = true } = {}) {
if (privKey) return new NDKPrivateKeySigner(privKey)
if (nip46token) return new NDKNip46SignerURLPatch(this.ndk, nip46token)
if (supportNip07 && typeof window !== 'undefined' && window?.nostr) return new NDKNip07Signer()
return null
}
/**
* @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
}
/**
* @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 })
const successfulRelays = []
const failedRelays = []
const relaySet = NDKRelaySet.fromRelayUrls(relays, this.ndk, true)
Support receiving with NWC (#1310) * Add NWC receives * Refactor sendPayment+createInvoice with nwcCall function * Update badge * Add method support checks * Add timeout to NWC test invoice * Fix NWC isConfigured state All NWC fields are marked as optional but NWC should only be considered configured if one of them is set. * Fix relay.fetch() throws 'crypto is not defined' in node nip04.encrypt() was failing in worker because 'crypto is not defined'. Updating to nostr-tools v2.7.2 fixed that. However, now crypto.randomUUID() in relay.fetch() was throwing 'crypto is not defined'. Importing crypto from 'crypto' fixed that. However, with the import, randomUUID() does not work so I switched to randomBytes(). Running relay.fetch() now works in browser and node. * recv must not support pay_invoice * Fix Relay connection check * this.url was undefined * error was an object * Fix additional isConfigured check runs always It was meant to only catch false positives, not turn negatives into false positives. * Rename testConnectServer to testCreateInvoice * Rename testConnectClient to testSendPayment * Only run testSendPayment if send is configured The return value of testSendPayment was used before but it only returned something for LNC. And for LNC, we only wanted to save the transformation during validation, so it was not needed. * Always use withTimeout in NWC test functions * Fix fragment name * Use get_info command exclusively * Check permissions more efficiently * Log NWC request-response flow * Fix variable name * Call ws.send after listener is added * Fix websocket not closed after timeout * Also check that pay_keysend etc. are not supported * fix lnc session key save --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
2024-08-21 15:13:27 +00:00
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 {
signer ??= this.getSigner({ supportNip07: true })
const { event: signedEvent, successfulRelays, failedRelays } = await this.publish({ created_at, content, tags, kind }, { relays, signer, timeout })
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 }
}
}
/**
* Close all relay connections
*/
close () {
const pool = this.ndk.pool
for (const relay of pool.urls()) {
pool.removeRelay(relay)
}
}
}
export function hexToBech32 (hex, prefix = 'npub') {
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
}
export function nostrZapDetails (zap) {
let { pubkey, content, tags } = zap
let npub = hexToBech32(pubkey)
if (npub === NOSTR_ZAPPLE_PAY_NPUB) {
const znpub = content.match(/^From: nostr:(npub1[02-9ac-hj-np-z]+)$/)?.[1]
if (znpub) {
npub = znpub
// zapple pay does not support user content
content = null
}
}
const event = tags.filter(t => t?.length >= 2 && t[0] === 'e')?.[0]?.[1]
const note = event ? hexToBech32(event, 'note') : null
return { npub, content, note }
}
// workaround NDK url parsing issue (see https://github.com/stackernews/stacker.news/pull/1636)
class NDKNip46SignerURLPatch extends NDKNip46Signer {
connectionTokenInit (connectionToken) {
connectionToken = connectionToken.replace('bunker://', 'http://')
return super.connectionTokenInit(connectionToken)
}
}