* 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:
Riccardo Balbo 2024-12-13 20:28:36 +01:00 committed by GitHub
parent 52734940a3
commit d73f6323ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 452 additions and 281 deletions

View File

@ -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 {

View File

@ -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
ws.onerror = (err) => {
console.error('websocket error:', err.message)
this.error = err.message
}
this.ws = ws
this.url = relayUrl
this.error = 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
})
}
static async connect (url, { timeout } = {}) {
const relay = new Relay(url)
await relay.waitUntilConnected({ timeout })
return relay
/**
* @type {NDK}
*/
get ndk () {
return this._ndk
}
get connected () {
return this.ws.readyState === WebSocket.OPEN
/**
*
* @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
}
get closed () {
return this.ws.readyState === WebSocket.CLOSING || this.ws.readyState === WebSocket.CLOSED
/**
* @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
}
async waitUntilConnected ({ timeout } = {}) {
let interval
/**
* @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 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)
const successfulRelays = []
const failedRelays = []
const relaySet = NDKRelaySet.fromRelayUrls(relays, this.ndk, true)
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)
} else {
reject(new Error(reason || `event rejected: ${eventId}`))
}
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')
}
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 }
}
}

View File

@ -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
}

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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({

View File

@ -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)
}