Compare commits
6 Commits
3608d133d7
...
a495c421ce
Author | SHA1 | Date | |
---|---|---|---|
|
a495c421ce | ||
|
2ff839f3a5 | ||
|
3264601dc6 | ||
|
06b661625c | ||
|
ccbc28322e | ||
|
df7baf4d7c |
@ -230,6 +230,7 @@ _Due to Rule 3, make sure that you mark your PR as a draft when you create it an
|
|||||||
|
|
||||||
| tag | multiplier |
|
| tag | multiplier |
|
||||||
| ----------------- | ---------- |
|
| ----------------- | ---------- |
|
||||||
|
| `priority:low` | 0.5 |
|
||||||
| `priority:medium` | 1.5 |
|
| `priority:medium` | 1.5 |
|
||||||
| `priority:high` | 2 |
|
| `priority:high` | 2 |
|
||||||
| `priority:urgent` | 3 |
|
| `priority:urgent` | 3 |
|
||||||
|
@ -158,6 +158,8 @@ export async function retryPaidAction (actionType, args, context) {
|
|||||||
const { models, me } = context
|
const { models, me } = context
|
||||||
const { invoiceId } = args
|
const { invoiceId } = args
|
||||||
|
|
||||||
|
console.log('retryPaidAction', actionType, args)
|
||||||
|
|
||||||
const action = paidActions[actionType]
|
const action = paidActions[actionType]
|
||||||
if (!action) {
|
if (!action) {
|
||||||
throw new Error(`retryPaidAction - invalid action type ${actionType}`)
|
throw new Error(`retryPaidAction - invalid action type ${actionType}`)
|
||||||
|
@ -202,7 +202,10 @@ export const nsfwClause = showNsfw => showNsfw ? '' : HIDE_NSFW_CLAUSE
|
|||||||
|
|
||||||
const subClause = (sub, num, table, me, showNsfw) => {
|
const subClause = (sub, num, table, me, showNsfw) => {
|
||||||
// Intentionally show nsfw posts (i.e. no nsfw clause) when viewing a specific nsfw sub
|
// Intentionally show nsfw posts (i.e. no nsfw clause) when viewing a specific nsfw sub
|
||||||
if (sub) { return `${table ? `"${table}".` : ''}"subName" = $${num}::CITEXT` }
|
if (sub) {
|
||||||
|
const tables = [...new Set(['Item', table])].map(t => `"${t}".`)
|
||||||
|
return `(${tables.map(t => `${t}"subName" = $${num}::CITEXT`).join(' OR ')})`
|
||||||
|
}
|
||||||
|
|
||||||
if (!me) { return HIDE_NSFW_CLAUSE }
|
if (!me) { return HIDE_NSFW_CLAUSE }
|
||||||
|
|
||||||
@ -346,7 +349,6 @@ export default {
|
|||||||
${whereClause(
|
${whereClause(
|
||||||
`"${table}"."userId" = $3`,
|
`"${table}"."userId" = $3`,
|
||||||
activeOrMine(me),
|
activeOrMine(me),
|
||||||
await filterClause(me, models, type),
|
|
||||||
nsfwClause(showNsfw),
|
nsfwClause(showNsfw),
|
||||||
typeClause(type),
|
typeClause(type),
|
||||||
whenClause(when || 'forever', table))}
|
whenClause(when || 'forever', table))}
|
||||||
|
@ -8,7 +8,7 @@ import { useRouter } from 'next/router'
|
|||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import BackIcon from '@/svgs/arrow-left-line.svg'
|
import BackIcon from '@/svgs/arrow-left-line.svg'
|
||||||
import styles from './lightning-auth.module.css'
|
import styles from './lightning-auth.module.css'
|
||||||
import { callWithTimeout } from '@/lib/nostr'
|
import { callWithTimeout } from '@/lib/time'
|
||||||
|
|
||||||
function ExtensionError ({ message, details }) {
|
function ExtensionError ({ message, details }) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { DEFAULT_CROSSPOSTING_RELAYS, crosspost, callWithTimeout } from '@/lib/nostr'
|
import { DEFAULT_CROSSPOSTING_RELAYS, crosspost } from '@/lib/nostr'
|
||||||
|
import { callWithTimeout } from '@/lib/time'
|
||||||
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
|
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
|
||||||
import { SETTINGS } from '@/fragments/users'
|
import { SETTINGS } from '@/fragments/users'
|
||||||
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
|
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
|
||||||
|
204
lib/nostr.js
204
lib/nostr.js
@ -1,5 +1,7 @@
|
|||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import WebSocket from 'isomorphic-ws'
|
||||||
|
import { callWithTimeout, withTimeout } from '@/lib/time'
|
||||||
|
|
||||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||||
@ -15,6 +17,149 @@ export const DEFAULT_CROSSPOSTING_RELAYS = [
|
|||||||
'wss://relay.mutinywallet.com/'
|
'wss://relay.mutinywallet.com/'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export class Relay {
|
||||||
|
constructor (relayUrl) {
|
||||||
|
const ws = new WebSocket(relayUrl)
|
||||||
|
|
||||||
|
ws.onmessage = function (msg) {
|
||||||
|
const [type, notice] = JSON.parse(msg.data)
|
||||||
|
if (type === 'NOTICE') {
|
||||||
|
console.log('relay notice:', notice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = function (err) {
|
||||||
|
console.error('websocket error: ' + err)
|
||||||
|
this.error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws = ws
|
||||||
|
}
|
||||||
|
|
||||||
|
static async connect (url, { timeout } = {}) {
|
||||||
|
const relay = new Relay(url)
|
||||||
|
await relay.waitUntilConnected({ timeout })
|
||||||
|
return relay
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected () {
|
||||||
|
return this.ws.readyState === WebSocket.OPEN
|
||||||
|
}
|
||||||
|
|
||||||
|
get closed () {
|
||||||
|
return this.ws.readyState === WebSocket.CLOSING || this.ws.readyState === WebSocket.CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitUntilConnected ({ timeout } = {}) {
|
||||||
|
let interval
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await withTimeout(checkPromise, timeout)
|
||||||
|
} finally {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
ws.send(JSON.stringify(['EVENT', event]))
|
||||||
|
|
||||||
|
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}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.addEventListener('message', listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
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.randomUUID()
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(['REQ', id, ...filter]))
|
||||||
|
|
||||||
|
const events = []
|
||||||
|
let eose = false
|
||||||
|
|
||||||
|
listener = function onmessage (msg) {
|
||||||
|
const [type, eventId, event] = JSON.parse(msg.data)
|
||||||
|
|
||||||
|
if (eventId !== 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await withTimeout(ackPromise, timeout)
|
||||||
|
} finally {
|
||||||
|
ws.removeEventListener('message', listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function hexToBech32 (hex, prefix = 'npub') {
|
export function hexToBech32 (hex, prefix = 'npub') {
|
||||||
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
||||||
}
|
}
|
||||||
@ -36,51 +181,10 @@ export function nostrZapDetails (zap) {
|
|||||||
return { npub, content, note }
|
return { npub, content, note }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishNostrEvent (signedEvent, relay) {
|
async function publishNostrEvent (signedEvent, relayUrl) {
|
||||||
return new Promise((resolve, reject) => {
|
const timeout = 3000
|
||||||
const timeout = 3000
|
const relay = await Relay.connect(relayUrl, { timeout })
|
||||||
const wsRelay = new window.WebSocket(relay)
|
await relay.publish(signedEvent, { timeout })
|
||||||
let timer
|
|
||||||
let isMessageSentSuccessfully = false
|
|
||||||
|
|
||||||
function timedout () {
|
|
||||||
clearTimeout(timer)
|
|
||||||
wsRelay.close()
|
|
||||||
reject(new Error(`relay timeout for ${relay}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
timer = setTimeout(timedout, timeout)
|
|
||||||
|
|
||||||
wsRelay.onopen = function () {
|
|
||||||
clearTimeout(timer)
|
|
||||||
timer = setTimeout(timedout, timeout)
|
|
||||||
wsRelay.send(JSON.stringify(['EVENT', signedEvent]))
|
|
||||||
}
|
|
||||||
|
|
||||||
wsRelay.onmessage = function (msg) {
|
|
||||||
const m = JSON.parse(msg.data)
|
|
||||||
if (m[0] === 'OK') {
|
|
||||||
isMessageSentSuccessfully = true
|
|
||||||
clearTimeout(timer)
|
|
||||||
wsRelay.close()
|
|
||||||
console.log('Successfully sent event to', relay)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wsRelay.onerror = function (error) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
console.log(error)
|
|
||||||
reject(new Error(`relay error: Failed to send to ${relay}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
wsRelay.onclose = function () {
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (!isMessageSentSuccessfully) {
|
|
||||||
reject(new Error(`relay error: Failed to send to ${relay}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
||||||
@ -118,13 +222,3 @@ export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
|||||||
return { error }
|
return { error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function callWithTimeout (targetFunction, timeoutMs) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
Promise.race([
|
|
||||||
targetFunction(),
|
|
||||||
new Promise((resolve, reject) => setTimeout(() => reject(new Error('timeouted after ' + timeoutMs + ' ms waiting for extension')), timeoutMs))
|
|
||||||
]).then(resolve)
|
|
||||||
.catch(reject)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
17
lib/time.js
17
lib/time.js
@ -127,3 +127,20 @@ function tzOffset (tz) {
|
|||||||
const targetOffsetHours = (date.getTime() - targetDate.getTime()) / 1000 / 60 / 60
|
const targetOffsetHours = (date.getTime() - targetDate.getTime()) / 1000 / 60 / 60
|
||||||
return targetOffsetHours
|
return targetOffsetHours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function timeoutPromise (timeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// if no timeout is specified, never settle
|
||||||
|
if (!timeout) return
|
||||||
|
|
||||||
|
setTimeout(() => reject(new Error('timeout')), timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withTimeout (promise, timeout) {
|
||||||
|
return await Promise.race([promise, timeoutPromise(timeout)])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callWithTimeout (fn, timeout) {
|
||||||
|
return await Promise.race([fn(), timeoutPromise(timeout)])
|
||||||
|
}
|
||||||
|
@ -91,7 +91,7 @@ export function middleware (request) {
|
|||||||
// Using nonces and strict-dynamic deploys a strict CSP.
|
// Using nonces and strict-dynamic deploys a strict CSP.
|
||||||
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
|
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
|
||||||
// Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline
|
// Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline
|
||||||
`script-src 'self' 'unsafe-inline' 'nonce-${nonce}' 'strict-dynamic' https:` + devScriptSrc,
|
`script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' 'nonce-${nonce}' 'strict-dynamic' https:` + devScriptSrc,
|
||||||
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
||||||
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -41,6 +41,7 @@
|
|||||||
"graphql-scalar": "^0.1.0",
|
"graphql-scalar": "^0.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ln-service": "^57.1.3",
|
"ln-service": "^57.1.3",
|
||||||
"macaroon": "^3.0.4",
|
"macaroon": "^3.0.4",
|
||||||
"mathjs": "^11.11.2",
|
"mathjs": "^11.11.2",
|
||||||
@ -10548,6 +10549,14 @@
|
|||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/isomorphic-ws": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/isstream": {
|
"node_modules/isstream": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
"graphql-scalar": "^0.1.0",
|
"graphql-scalar": "^0.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ln-service": "^57.1.3",
|
"ln-service": "^57.1.3",
|
||||||
"macaroon": "^3.0.4",
|
"macaroon": "^3.0.4",
|
||||||
"mathjs": "^11.11.2",
|
"mathjs": "^11.11.2",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { parseNwcUrl } from '@/lib/url'
|
import { parseNwcUrl } from '@/lib/url'
|
||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
import { finalizeEvent, nip04 } from 'nostr-tools'
|
||||||
|
import { Relay } from '@/lib/nostr'
|
||||||
|
|
||||||
export * from 'wallets/nwc'
|
export * from 'wallets/nwc'
|
||||||
|
|
||||||
@ -7,112 +8,67 @@ export async function testConnectClient ({ nwcUrl }, { logger }) {
|
|||||||
const { relayUrl, walletPubkey } = parseNwcUrl(nwcUrl)
|
const { relayUrl, walletPubkey } = parseNwcUrl(nwcUrl)
|
||||||
|
|
||||||
logger.info(`requesting info event from ${relayUrl}`)
|
logger.info(`requesting info event from ${relayUrl}`)
|
||||||
const relay = await Relay
|
|
||||||
.connect(relayUrl)
|
const relay = await Relay.connect(relayUrl)
|
||||||
.catch(() => {
|
|
||||||
// NOTE: passed error is undefined for some reason
|
|
||||||
const msg = `failed to connect to ${relayUrl}`
|
|
||||||
logger.error(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
})
|
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
logger.ok(`connected to ${relayUrl}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
const [info] = await relay.fetch([{
|
||||||
let found = false
|
kinds: [13194],
|
||||||
const sub = relay.subscribe([
|
authors: [walletPubkey]
|
||||||
{
|
}])
|
||||||
kinds: [13194],
|
|
||||||
authors: [walletPubkey]
|
if (info) {
|
||||||
}
|
logger.ok(`received info event from ${relayUrl}`)
|
||||||
], {
|
} else {
|
||||||
onevent (event) {
|
throw new Error('info event not found')
|
||||||
found = true
|
}
|
||||||
logger.ok(`received info event from ${relayUrl}`)
|
|
||||||
resolve(event)
|
|
||||||
},
|
|
||||||
onclose (reason) {
|
|
||||||
if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
|
|
||||||
// only log if not closed by us (caller)
|
|
||||||
const msg = 'connection closed: ' + (reason || 'unknown reason')
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oneose () {
|
|
||||||
if (!found) {
|
|
||||||
const msg = 'EOSE received without info event'
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
sub?.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
// For some reason, this throws 'WebSocket is already in CLOSING or CLOSED state'
|
relay?.close()
|
||||||
// even though relay connection is still open here
|
logger.info(`closed connection to ${relayUrl}`)
|
||||||
relay?.close()?.catch()
|
|
||||||
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPayment (bolt11, { nwcUrl }, { logger }) {
|
export async function sendPayment (bolt11, { nwcUrl }, { logger }) {
|
||||||
const { relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)
|
const { relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)
|
||||||
|
|
||||||
const relay = await Relay.connect(relayUrl).catch(() => {
|
const relay = await Relay.connect(relayUrl)
|
||||||
// NOTE: passed error is undefined for some reason
|
|
||||||
throw new Error(`failed to connect to ${relayUrl}`)
|
|
||||||
})
|
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
logger.ok(`connected to ${relayUrl}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ret = await new Promise(function (resolve, reject) {
|
const payload = {
|
||||||
(async function () {
|
method: 'pay_invoice',
|
||||||
const payload = {
|
params: { invoice: bolt11 }
|
||||||
method: 'pay_invoice',
|
}
|
||||||
params: { invoice: bolt11 }
|
const encrypted = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
||||||
}
|
|
||||||
const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
|
||||||
|
|
||||||
const request = finalizeEvent({
|
const request = finalizeEvent({
|
||||||
kind: 23194,
|
kind: 23194,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [['p', walletPubkey]],
|
tags: [['p', walletPubkey]],
|
||||||
content
|
content: encrypted
|
||||||
}, secret)
|
}, secret)
|
||||||
await relay.publish(request)
|
await relay.publish(request)
|
||||||
|
|
||||||
const filter = {
|
const [response] = await relay.fetch([{
|
||||||
kinds: [23195],
|
kinds: [23195],
|
||||||
authors: [walletPubkey],
|
authors: [walletPubkey],
|
||||||
'#e': [request.id]
|
'#e': [request.id]
|
||||||
}
|
}])
|
||||||
relay.subscribe([filter], {
|
|
||||||
async onevent (response) {
|
if (!response) {
|
||||||
try {
|
throw new Error('no response')
|
||||||
const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
|
}
|
||||||
if (content.error) return reject(new Error(content.error.message))
|
|
||||||
if (content.result) return resolve({ preimage: content.result.preimage })
|
const decrypted = await nip04.decrypt(secret, walletPubkey, response.content)
|
||||||
} catch (err) {
|
const content = JSON.parse(decrypted)
|
||||||
return reject(err)
|
|
||||||
}
|
if (content.error) throw new Error(content.error.message)
|
||||||
},
|
if (content.result) return { preimage: content.result.preimage }
|
||||||
onclose (reason) {
|
|
||||||
if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
|
throw new Error('invalid response')
|
||||||
// only log if not closed by us (caller)
|
|
||||||
const msg = 'connection closed: ' + (reason || 'unknown reason')
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})().catch(reject)
|
|
||||||
})
|
|
||||||
return ret
|
|
||||||
} finally {
|
} finally {
|
||||||
// For some reason, this throws 'WebSocket is already in CLOSING or CLOSED state'
|
relay?.close()
|
||||||
// even though relay connection is still open here
|
logger.info(`closed connection to ${relayUrl}`)
|
||||||
relay?.close()?.catch()
|
|
||||||
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { getInvoice } from 'ln-service'
|
import { getInvoice } from 'ln-service'
|
||||||
import { Relay, signId, calculateId, getPublicKey } from 'nostr'
|
import { signId, calculateId, getPublicKey } from 'nostr'
|
||||||
|
import { Relay } from '@/lib/nostr'
|
||||||
|
|
||||||
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
||||||
|
|
||||||
@ -50,31 +51,12 @@ export async function nip57 ({ data: { hash }, boss, lnd, models }) {
|
|||||||
|
|
||||||
console.log('zap note', e, relays)
|
console.log('zap note', e, relays)
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
relays.map(r => new Promise((resolve, reject) => {
|
relays.map(async r => {
|
||||||
const timeout = 1000
|
const timeout = 1000
|
||||||
const relay = Relay(r)
|
const relay = await Relay.connect(r, { timeout })
|
||||||
|
await relay.publish(e, { timeout })
|
||||||
function timedout () {
|
})
|
||||||
relay.close()
|
)
|
||||||
console.log('failed to send to', r)
|
|
||||||
reject(new Error('relay timeout'))
|
|
||||||
}
|
|
||||||
|
|
||||||
let timer = setTimeout(timedout, timeout)
|
|
||||||
|
|
||||||
relay.on('open', () => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
timer = setTimeout(timedout, timeout)
|
|
||||||
relay.send(['EVENT', e])
|
|
||||||
})
|
|
||||||
|
|
||||||
relay.on('ok', () => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
relay.close()
|
|
||||||
console.log('sent zap to', r)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})))
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
@ -239,9 +239,14 @@ async function subscribeToWithdrawals (args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
export async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
||||||
|
// get the withdrawl if pending or it's an invoiceForward
|
||||||
const dbWdrwl = await models.withdrawl.findFirst({
|
const dbWdrwl = await models.withdrawl.findFirst({
|
||||||
where: {
|
where: {
|
||||||
hash
|
hash,
|
||||||
|
OR: [
|
||||||
|
{ status: null },
|
||||||
|
{ invoiceForward: { some: { } } }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
wallet: true,
|
wallet: true,
|
||||||
@ -253,16 +258,9 @@ export async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!dbWdrwl) {
|
|
||||||
// [WARNING] LND paid an invoice that wasn't created via the SN GraphQL API.
|
|
||||||
// >>> an adversary might be draining our funds right now <<<
|
|
||||||
console.error('unexpected outgoing payment detected:', hash)
|
|
||||||
// TODO: log this in Slack
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// already recorded and no invoiceForward to handle
|
// nothing to do if the withdrawl is already recorded and it isn't an invoiceForward
|
||||||
if (dbWdrwl.status && dbWdrwl.invoiceForward.length === 0) return
|
if (!dbWdrwl) return
|
||||||
|
|
||||||
let wdrwl
|
let wdrwl
|
||||||
let notSent = false
|
let notSent = false
|
||||||
@ -395,7 +393,9 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss,
|
|||||||
|
|
||||||
// if this is an actionType we need to cancel conditionally
|
// if this is an actionType we need to cancel conditionally
|
||||||
if (dbInv.actionType) {
|
if (dbInv.actionType) {
|
||||||
return await paidActionCanceling({ data: { invoiceId: dbInv.id }, models, lnd, boss })
|
await paidActionCanceling({ data: { invoiceId: dbInv.id }, models, lnd, boss })
|
||||||
|
await checkInvoice({ data: { hash }, models, lnd, ...args })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await cancelHodlInvoice({ id: hash, lnd })
|
await cancelHodlInvoice({ id: hash, lnd })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user