Add NIP-47 (Nostr Wallet Connect) support
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
Implements NWC protocol alongside the existing CLINK/Ndebit system, allowing any NWC-compatible wallet (Alby, Amethyst, Damus, etc.) to connect to a Lightning Pub node. Supported NIP-47 methods: pay_invoice, make_invoice, get_balance, get_info, lookup_invoice, list_transactions. New files: - NwcConnection entity with per-connection spending limits - NwcStorage for connection CRUD operations - NwcManager for NIP-47 request handling and connection management - Database migration for nwc_connection table Modified files: - nostrPool: subscribe to kind 23194 events - nostrMiddleware: route kind 23194 to NwcManager - main/index: wire NwcManager, publish kind 13194 info events - storage: register NwcConnection entity and NwcStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ef53408485
commit
6037dbe600
10 changed files with 473 additions and 4 deletions
|
|
@ -79,6 +79,13 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
|
||||||
const nmanageReq = j as NmanageRequest
|
const nmanageReq = j as NmanageRequest
|
||||||
mainHandler.managementManager.handleRequest(nmanageReq, event);
|
mainHandler.managementManager.handleRequest(nmanageReq, event);
|
||||||
return;
|
return;
|
||||||
|
} else if (event.kind === 23194) {
|
||||||
|
if (event.relayConstraint === 'provider') {
|
||||||
|
log("got NWC request on provider only relay, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainHandler.nwcManager.handleNwcRequest(event.content, event)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (!j.rpcName) {
|
if (!j.rpcName) {
|
||||||
if (event.relayConstraint === 'service') {
|
if (event.relayConstraint === 'service') {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { OfferManager } from "./offerManager.js"
|
||||||
import { parse } from "uri-template"
|
import { parse } from "uri-template"
|
||||||
import webRTC from "../webRTC/index.js"
|
import webRTC from "../webRTC/index.js"
|
||||||
import { ManagementManager } from "./managementManager.js"
|
import { ManagementManager } from "./managementManager.js"
|
||||||
|
import { NwcManager } from "./nwcManager.js"
|
||||||
import { Agent } from "https"
|
import { Agent } from "https"
|
||||||
import { NotificationsManager } from "./notificationsManager.js"
|
import { NotificationsManager } from "./notificationsManager.js"
|
||||||
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
|
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
|
||||||
|
|
@ -58,6 +59,7 @@ export default class {
|
||||||
debitManager: DebitManager
|
debitManager: DebitManager
|
||||||
offerManager: OfferManager
|
offerManager: OfferManager
|
||||||
managementManager: ManagementManager
|
managementManager: ManagementManager
|
||||||
|
nwcManager: NwcManager
|
||||||
utils: Utils
|
utils: Utils
|
||||||
rugPullTracker: RugPullTracker
|
rugPullTracker: RugPullTracker
|
||||||
unlocker: Unlocker
|
unlocker: Unlocker
|
||||||
|
|
@ -89,6 +91,7 @@ export default class {
|
||||||
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
|
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
|
||||||
this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
|
this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
|
||||||
this.managementManager = new ManagementManager(this.storage, this.settings)
|
this.managementManager = new ManagementManager(this.storage, this.settings)
|
||||||
|
this.nwcManager = new NwcManager(this.storage, this.settings, this.lnd, this.applicationManager)
|
||||||
this.notificationsManager = new NotificationsManager(this.settings)
|
this.notificationsManager = new NotificationsManager(this.settings)
|
||||||
//this.webRTC = new webRTC(this.storage, this.utils)
|
//this.webRTC = new webRTC(this.storage, this.utils)
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +107,9 @@ export default class {
|
||||||
StartBeacons() {
|
StartBeacons() {
|
||||||
this.applicationManager.StartAppsServiceBeacon((app, fees) => {
|
this.applicationManager.StartAppsServiceBeacon((app, fees) => {
|
||||||
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees })
|
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees })
|
||||||
|
if (app.nostr_public_key) {
|
||||||
|
this.nwcManager.publishNwcInfo(app.app_id, app.nostr_public_key)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,6 +541,9 @@ export default class {
|
||||||
const fees = this.paymentManager.GetFees()
|
const fees = this.paymentManager.GetFees()
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees })
|
await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees })
|
||||||
|
if (app.nostr_public_key) {
|
||||||
|
this.nwcManager.publishNwcInfo(app.app_id, app.nostr_public_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName]
|
const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName]
|
||||||
|
|
|
||||||
346
src/services/main/nwcManager.ts
Normal file
346
src/services/main/nwcManager.ts
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
import { generateSecretKey, getPublicKey, UnsignedEvent } from 'nostr-tools'
|
||||||
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
import ApplicationManager from "./applicationManager.js"
|
||||||
|
import Storage from '../storage/index.js'
|
||||||
|
import LND from "../lnd/lnd.js"
|
||||||
|
import { ERROR, getLogger } from "../helpers/logger.js"
|
||||||
|
import { NostrEvent } from '../nostr/nostrPool.js'
|
||||||
|
import SettingsManager from "./settingsManager.js"
|
||||||
|
|
||||||
|
type NwcRequest = {
|
||||||
|
method: string
|
||||||
|
params: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
type NwcResponse = {
|
||||||
|
result_type: string
|
||||||
|
error?: { code: string, message: string }
|
||||||
|
result?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const NWC_REQUEST_KIND = 23194
|
||||||
|
const NWC_RESPONSE_KIND = 23195
|
||||||
|
const NWC_INFO_KIND = 13194
|
||||||
|
|
||||||
|
const SUPPORTED_METHODS = [
|
||||||
|
'pay_invoice',
|
||||||
|
'make_invoice',
|
||||||
|
'get_balance',
|
||||||
|
'get_info',
|
||||||
|
'lookup_invoice',
|
||||||
|
'list_transactions',
|
||||||
|
]
|
||||||
|
|
||||||
|
const NWC_ERRORS = {
|
||||||
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
RESTRICTED: 'RESTRICTED',
|
||||||
|
PAYMENT_FAILED: 'PAYMENT_FAILED',
|
||||||
|
QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',
|
||||||
|
INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
|
||||||
|
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
INTERNAL: 'INTERNAL',
|
||||||
|
OTHER: 'OTHER',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const newNwcResponse = (content: string, event: { pub: string, id: string }): UnsignedEvent => {
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: NWC_RESPONSE_KIND,
|
||||||
|
pubkey: "",
|
||||||
|
tags: [
|
||||||
|
['p', event.pub],
|
||||||
|
['e', event.id],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NwcManager {
|
||||||
|
applicationManager: ApplicationManager
|
||||||
|
storage: Storage
|
||||||
|
settings: SettingsManager
|
||||||
|
lnd: LND
|
||||||
|
logger = getLogger({ component: 'NwcManager' })
|
||||||
|
|
||||||
|
constructor(storage: Storage, settings: SettingsManager, lnd: LND, applicationManager: ApplicationManager) {
|
||||||
|
this.storage = storage
|
||||||
|
this.settings = settings
|
||||||
|
this.lnd = lnd
|
||||||
|
this.applicationManager = applicationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNwcRequest = async (content: string, event: NostrEvent) => {
|
||||||
|
if (!this.storage.NostrSender().IsReady()) {
|
||||||
|
this.logger(ERROR, "Nostr sender not ready, dropping NWC request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request: NwcRequest
|
||||||
|
try {
|
||||||
|
request = JSON.parse(content)
|
||||||
|
} catch {
|
||||||
|
this.logger(ERROR, "invalid NWC request JSON")
|
||||||
|
this.sendError(event, 'unknown', NWC_ERRORS.OTHER, 'Invalid request JSON')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { method, params } = request
|
||||||
|
if (!method) {
|
||||||
|
this.sendError(event, 'unknown', NWC_ERRORS.OTHER, 'Missing method')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await this.storage.nwcStorage.GetConnection(event.appId, event.pub)
|
||||||
|
if (!connection) {
|
||||||
|
this.logger("NWC request from unknown client pubkey:", event.pub)
|
||||||
|
this.sendError(event, method, NWC_ERRORS.UNAUTHORIZED, 'Unknown connection')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.expires_at > 0 && connection.expires_at < Math.floor(Date.now() / 1000)) {
|
||||||
|
this.logger("NWC connection expired for client:", event.pub)
|
||||||
|
this.sendError(event, method, NWC_ERRORS.UNAUTHORIZED, 'Connection expired')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.permissions && connection.permissions.length > 0 && !connection.permissions.includes(method)) {
|
||||||
|
this.sendError(event, method, NWC_ERRORS.RESTRICTED, `Method ${method} not permitted`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case 'pay_invoice':
|
||||||
|
await this.handlePayInvoice(event, params, connection.app_user_id, connection.max_amount, connection.total_spent)
|
||||||
|
break
|
||||||
|
case 'make_invoice':
|
||||||
|
await this.handleMakeInvoice(event, params, connection.app_user_id)
|
||||||
|
break
|
||||||
|
case 'get_balance':
|
||||||
|
await this.handleGetBalance(event, connection.app_user_id)
|
||||||
|
break
|
||||||
|
case 'get_info':
|
||||||
|
await this.handleGetInfo(event)
|
||||||
|
break
|
||||||
|
case 'lookup_invoice':
|
||||||
|
await this.handleLookupInvoice(event, params)
|
||||||
|
break
|
||||||
|
case 'list_transactions':
|
||||||
|
await this.handleListTransactions(event, params, connection.app_user_id)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.sendError(event, method, NWC_ERRORS.NOT_IMPLEMENTED, `Method ${method} not supported`)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger(ERROR, `NWC ${method} failed:`, e.message || e)
|
||||||
|
this.sendError(event, method, NWC_ERRORS.INTERNAL, e.message || 'Internal error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePayInvoice = async (event: NostrEvent, params: Record<string, any>, appUserId: string, maxAmount: number, totalSpent: number) => {
|
||||||
|
const { invoice } = params
|
||||||
|
if (!invoice) {
|
||||||
|
this.sendError(event, 'pay_invoice', NWC_ERRORS.OTHER, 'Missing invoice parameter')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAmount > 0) {
|
||||||
|
const decoded = await this.lnd.DecodeInvoice(invoice)
|
||||||
|
const amountSats = decoded.numSatoshis
|
||||||
|
if (amountSats > 0 && (totalSpent + amountSats) > maxAmount) {
|
||||||
|
this.sendError(event, 'pay_invoice', NWC_ERRORS.QUOTA_EXCEEDED, 'Spending limit exceeded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paid = await this.applicationManager.PayAppUserInvoice(event.appId, {
|
||||||
|
amount: 0,
|
||||||
|
invoice,
|
||||||
|
user_identifier: appUserId,
|
||||||
|
debit_npub: event.pub,
|
||||||
|
})
|
||||||
|
await this.storage.nwcStorage.IncrementTotalSpent(event.appId, event.pub, paid.amount_paid + paid.service_fee)
|
||||||
|
this.sendResult(event, 'pay_invoice', { preimage: paid.preimage })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.sendError(event, 'pay_invoice', NWC_ERRORS.PAYMENT_FAILED, e.message || 'Payment failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMakeInvoice = async (event: NostrEvent, params: Record<string, any>, appUserId: string) => {
|
||||||
|
const amountMsats = params.amount
|
||||||
|
if (amountMsats === undefined || amountMsats === null) {
|
||||||
|
this.sendError(event, 'make_invoice', NWC_ERRORS.OTHER, 'Missing amount parameter')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const amountSats = Math.floor(amountMsats / 1000)
|
||||||
|
const description = params.description || ''
|
||||||
|
const expiry = params.expiry || undefined
|
||||||
|
|
||||||
|
const result = await this.applicationManager.AddAppUserInvoice(event.appId, {
|
||||||
|
receiver_identifier: appUserId,
|
||||||
|
payer_identifier: '',
|
||||||
|
http_callback_url: '',
|
||||||
|
invoice_req: {
|
||||||
|
amountSats,
|
||||||
|
memo: description,
|
||||||
|
expiry,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sendResult(event, 'make_invoice', {
|
||||||
|
type: 'incoming',
|
||||||
|
invoice: result.invoice,
|
||||||
|
description,
|
||||||
|
description_hash: '',
|
||||||
|
preimage: '',
|
||||||
|
payment_hash: '',
|
||||||
|
amount: amountMsats,
|
||||||
|
fees_paid: 0,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
expires_at: Math.floor(Date.now() / 1000) + (expiry || 3600),
|
||||||
|
metadata: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetBalance = async (event: NostrEvent, appUserId: string) => {
|
||||||
|
const app = await this.storage.applicationStorage.GetApplication(event.appId)
|
||||||
|
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, appUserId)
|
||||||
|
const balanceMsats = appUser.user.balance_sats * 1000
|
||||||
|
this.sendResult(event, 'get_balance', { balance: balanceMsats })
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetInfo = async (event: NostrEvent) => {
|
||||||
|
const info = await this.lnd.GetInfo()
|
||||||
|
this.sendResult(event, 'get_info', {
|
||||||
|
alias: info.alias,
|
||||||
|
color: '',
|
||||||
|
pubkey: info.identityPubkey,
|
||||||
|
network: 'mainnet',
|
||||||
|
block_height: info.blockHeight,
|
||||||
|
block_hash: info.blockHash,
|
||||||
|
methods: SUPPORTED_METHODS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLookupInvoice = async (event: NostrEvent, params: Record<string, any>) => {
|
||||||
|
const { invoice, payment_hash } = params
|
||||||
|
if (!invoice && !payment_hash) {
|
||||||
|
this.sendError(event, 'lookup_invoice', NWC_ERRORS.OTHER, 'Missing invoice or payment_hash parameter')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoice) {
|
||||||
|
const found = await this.storage.paymentStorage.GetInvoiceOwner(invoice)
|
||||||
|
if (!found) {
|
||||||
|
this.sendError(event, 'lookup_invoice', NWC_ERRORS.NOT_FOUND, 'Invoice not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.sendResult(event, 'lookup_invoice', {
|
||||||
|
type: 'incoming',
|
||||||
|
invoice: found.invoice,
|
||||||
|
description: '',
|
||||||
|
description_hash: '',
|
||||||
|
preimage: '',
|
||||||
|
payment_hash: '',
|
||||||
|
amount: found.paid_amount * 1000,
|
||||||
|
fees_paid: 0,
|
||||||
|
created_at: Math.floor(found.created_at.getTime() / 1000),
|
||||||
|
settled_at: found.paid_at_unix > 0 ? found.paid_at_unix : undefined,
|
||||||
|
metadata: {},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.sendError(event, 'lookup_invoice', NWC_ERRORS.NOT_IMPLEMENTED, 'Lookup by payment_hash not supported')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleListTransactions = async (event: NostrEvent, params: Record<string, any>, appUserId: string) => {
|
||||||
|
const app = await this.storage.applicationStorage.GetApplication(event.appId)
|
||||||
|
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, appUserId)
|
||||||
|
const from = params.from || 0
|
||||||
|
const limit = Math.min(params.limit || 50, 50)
|
||||||
|
|
||||||
|
const invoices = await this.storage.paymentStorage.GetUserInvoicesFlaggedAsPaid(appUser.user.serial_id, 0, from, limit)
|
||||||
|
const transactions = invoices.map(inv => ({
|
||||||
|
type: 'incoming' as const,
|
||||||
|
invoice: inv.invoice,
|
||||||
|
description: '',
|
||||||
|
description_hash: '',
|
||||||
|
preimage: '',
|
||||||
|
payment_hash: '',
|
||||||
|
amount: inv.paid_amount * 1000,
|
||||||
|
fees_paid: 0,
|
||||||
|
created_at: Math.floor(inv.created_at.getTime() / 1000),
|
||||||
|
settled_at: inv.paid_at_unix > 0 ? inv.paid_at_unix : undefined,
|
||||||
|
metadata: {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.sendResult(event, 'list_transactions', { transactions })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connection management methods ---
|
||||||
|
|
||||||
|
createConnection = async (appId: string, appUserId: string, permissions?: string[], options?: { maxAmount?: number, expiresAt?: number }) => {
|
||||||
|
const secretBytes = generateSecretKey()
|
||||||
|
const clientPubkey = getPublicKey(secretBytes)
|
||||||
|
const secret = bytesToHex(secretBytes)
|
||||||
|
|
||||||
|
await this.storage.nwcStorage.AddConnection({
|
||||||
|
app_id: appId,
|
||||||
|
app_user_id: appUserId,
|
||||||
|
client_pubkey: clientPubkey,
|
||||||
|
permissions,
|
||||||
|
max_amount: options?.maxAmount,
|
||||||
|
expires_at: options?.expiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = await this.storage.applicationStorage.GetApplication(appId)
|
||||||
|
const relays = this.settings.getSettings().nostrRelaySettings.relays
|
||||||
|
const relay = relays[0] || ''
|
||||||
|
const uri = `nostr+walletconnect://${app.nostr_public_key}?relay=${encodeURIComponent(relay)}&secret=${secret}`
|
||||||
|
return { uri, clientPubkey, secret }
|
||||||
|
}
|
||||||
|
|
||||||
|
listConnections = async (appUserId: string) => {
|
||||||
|
return this.storage.nwcStorage.GetUserConnections(appUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeConnection = async (appId: string, clientPubkey: string) => {
|
||||||
|
return this.storage.nwcStorage.DeleteConnectionByPubkey(appId, clientPubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Kind 13194 info event ---
|
||||||
|
|
||||||
|
publishNwcInfo = (appId: string, appPubkey: string) => {
|
||||||
|
const content = SUPPORTED_METHODS.join(' ')
|
||||||
|
const event: UnsignedEvent = {
|
||||||
|
content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: NWC_INFO_KIND,
|
||||||
|
pubkey: appPubkey,
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
this.storage.NostrSender().Send({ type: 'app', appId }, { type: 'event', event })
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Response helpers ---
|
||||||
|
|
||||||
|
private sendResult = (event: NostrEvent, resultType: string, result: Record<string, any>) => {
|
||||||
|
const response: NwcResponse = { result_type: resultType, result }
|
||||||
|
const e = newNwcResponse(JSON.stringify(response), event)
|
||||||
|
this.storage.NostrSender().Send(
|
||||||
|
{ type: 'app', appId: event.appId },
|
||||||
|
{ type: 'event', event: e, encrypt: { toPub: event.pub } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendError = (event: NostrEvent, resultType: string, code: string, message: string) => {
|
||||||
|
const response: NwcResponse = { result_type: resultType, error: { code, message } }
|
||||||
|
const e = newNwcResponse(JSON.stringify(response), event)
|
||||||
|
this.storage.NostrSender().Send(
|
||||||
|
{ type: 'app', appId: event.appId },
|
||||||
|
{ type: 'event', event: e, encrypt: { toPub: event.pub } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ const splitContent = (content: string, maxLength: number) => {
|
||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
const actionKinds = [21000, 21001, 21002, 21003]
|
const actionKinds = [21000, 21001, 21002, 21003, 23194]
|
||||||
const beaconKind = 30078
|
const beaconKind = 30078
|
||||||
const appTag = "Lightning.Pub"
|
const appTag = "Lightning.Pub"
|
||||||
export class NostrPool {
|
export class NostrPool {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { LndNodeInfo } from "../entity/LndNodeInfo.js"
|
||||||
import { TrackedProvider } from "../entity/TrackedProvider.js"
|
import { TrackedProvider } from "../entity/TrackedProvider.js"
|
||||||
import { InviteToken } from "../entity/InviteToken.js"
|
import { InviteToken } from "../entity/InviteToken.js"
|
||||||
import { DebitAccess } from "../entity/DebitAccess.js"
|
import { DebitAccess } from "../entity/DebitAccess.js"
|
||||||
|
import { NwcConnection } from "../entity/NwcConnection.js"
|
||||||
import { RootOperation } from "../entity/RootOperation.js"
|
import { RootOperation } from "../entity/RootOperation.js"
|
||||||
import { UserOffer } from "../entity/UserOffer.js"
|
import { UserOffer } from "../entity/UserOffer.js"
|
||||||
import { ManagementGrant } from "../entity/ManagementGrant.js"
|
import { ManagementGrant } from "../entity/ManagementGrant.js"
|
||||||
|
|
@ -71,6 +72,7 @@ export const MainDbEntities = {
|
||||||
'TrackedProvider': TrackedProvider,
|
'TrackedProvider': TrackedProvider,
|
||||||
'InviteToken': InviteToken,
|
'InviteToken': InviteToken,
|
||||||
'DebitAccess': DebitAccess,
|
'DebitAccess': DebitAccess,
|
||||||
|
'NwcConnection': NwcConnection,
|
||||||
'UserOffer': UserOffer,
|
'UserOffer': UserOffer,
|
||||||
'Product': Product,
|
'Product': Product,
|
||||||
'ManagementGrant': ManagementGrant,
|
'ManagementGrant': ManagementGrant,
|
||||||
|
|
|
||||||
37
src/services/storage/entity/NwcConnection.ts
Normal file
37
src/services/storage/entity/NwcConnection.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn, UpdateDateColumn } from "typeorm"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index("unique_nwc_connection", ["app_id", "client_pubkey"], { unique: true })
|
||||||
|
@Index("idx_nwc_app_user", ["app_user_id"])
|
||||||
|
export class NwcConnection {
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
serial_id: number
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
app_id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
app_user_id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
client_pubkey: string
|
||||||
|
|
||||||
|
@Column({ type: 'simple-json', default: null, nullable: true })
|
||||||
|
permissions: string[] | null
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
max_amount: number
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
expires_at: number
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
total_spent: number
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
created_at: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updated_at: Date
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import MetricsEventStorage from "./tlv/metricsEventStorage.js";
|
||||||
import EventsLogManager from "./eventsLog.js";
|
import EventsLogManager from "./eventsLog.js";
|
||||||
import { LiquidityStorage } from "./liquidityStorage.js";
|
import { LiquidityStorage } from "./liquidityStorage.js";
|
||||||
import DebitStorage from "./debitStorage.js"
|
import DebitStorage from "./debitStorage.js"
|
||||||
|
import NwcStorage from "./nwcStorage.js"
|
||||||
import OfferStorage from "./offerStorage.js"
|
import OfferStorage from "./offerStorage.js"
|
||||||
import { ManagementStorage } from "./managementStorage.js";
|
import { ManagementStorage } from "./managementStorage.js";
|
||||||
import { StorageInterface, TX } from "./db/storageInterface.js";
|
import { StorageInterface, TX } from "./db/storageInterface.js";
|
||||||
|
|
@ -78,6 +79,7 @@ export default class {
|
||||||
metricsEventStorage: MetricsEventStorage
|
metricsEventStorage: MetricsEventStorage
|
||||||
liquidityStorage: LiquidityStorage
|
liquidityStorage: LiquidityStorage
|
||||||
debitStorage: DebitStorage
|
debitStorage: DebitStorage
|
||||||
|
nwcStorage: NwcStorage
|
||||||
offerStorage: OfferStorage
|
offerStorage: OfferStorage
|
||||||
managementStorage: ManagementStorage
|
managementStorage: ManagementStorage
|
||||||
eventsLog: EventsLogManager
|
eventsLog: EventsLogManager
|
||||||
|
|
@ -103,6 +105,7 @@ export default class {
|
||||||
this.metricsEventStorage = new MetricsEventStorage(this.settings, this.utils.tlvStorageFactory)
|
this.metricsEventStorage = new MetricsEventStorage(this.settings, this.utils.tlvStorageFactory)
|
||||||
this.liquidityStorage = new LiquidityStorage(this.dbs)
|
this.liquidityStorage = new LiquidityStorage(this.dbs)
|
||||||
this.debitStorage = new DebitStorage(this.dbs)
|
this.debitStorage = new DebitStorage(this.dbs)
|
||||||
|
this.nwcStorage = new NwcStorage(this.dbs)
|
||||||
this.offerStorage = new OfferStorage(this.dbs)
|
this.offerStorage = new OfferStorage(this.dbs)
|
||||||
this.managementStorage = new ManagementStorage(this.dbs);
|
this.managementStorage = new ManagementStorage(this.dbs);
|
||||||
try { if (this.settings.dataDir) fs.mkdirSync(this.settings.dataDir) } catch (e) { }
|
try { if (this.settings.dataDir) fs.mkdirSync(this.settings.dataDir) } catch (e) { }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class NwcConnection1770000000000 implements MigrationInterface {
|
||||||
|
name = 'NwcConnection1770000000000'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "nwc_connection" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_id" varchar NOT NULL, "app_user_id" varchar NOT NULL, "client_pubkey" varchar NOT NULL, "permissions" text, "max_amount" integer NOT NULL DEFAULT (0), "expires_at" integer NOT NULL DEFAULT (0), "total_spent" integer NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "unique_nwc_connection" ON "nwc_connection" ("app_id", "client_pubkey") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "idx_nwc_app_user" ON "nwc_connection" ("app_user_id") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "idx_nwc_app_user"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "unique_nwc_connection"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "nwc_connection"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_prov
|
||||||
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
|
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
|
||||||
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
|
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
|
||||||
import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js'
|
import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js'
|
||||||
|
import { NwcConnection1770000000000 } from './1770000000000-nwc_connection.js'
|
||||||
import { ApplicationUserTopicId1770038768784 } from './1770038768784-application_user_topic_id.js'
|
import { ApplicationUserTopicId1770038768784 } from './1770038768784-application_user_topic_id.js'
|
||||||
import { SwapTimestamps1771347307798 } from './1771347307798-swap_timestamps.js'
|
import { SwapTimestamps1771347307798 } from './1771347307798-swap_timestamps.js'
|
||||||
import { TxSwapTimestamps1771878683383 } from './1771878683383-tx_swap_timestamps.js'
|
import { TxSwapTimestamps1771878683383 } from './1771878683383-tx_swap_timestamps.js'
|
||||||
|
|
@ -43,15 +44,13 @@ import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
|
||||||
import { RootOpPending1771524665409 } from './1771524665409-root_op_pending.js'
|
import { RootOpPending1771524665409 } from './1771524665409-root_op_pending.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
|
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
|
||||||
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
|
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
|
||||||
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
|
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
|
||||||
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
|
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
|
||||||
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
||||||
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
|
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
|
||||||
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798,
|
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, NwcConnection1770000000000, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798,
|
||||||
TxSwapTimestamps1771878683383, RefundSwapInfo1773082318982]
|
TxSwapTimestamps1771878683383, RefundSwapInfo1773082318982]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
49
src/services/storage/nwcStorage.ts
Normal file
49
src/services/storage/nwcStorage.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { NwcConnection } from "./entity/NwcConnection.js";
|
||||||
|
import { StorageInterface } from "./db/storageInterface.js";
|
||||||
|
|
||||||
|
type ConnectionToAdd = {
|
||||||
|
app_id: string
|
||||||
|
app_user_id: string
|
||||||
|
client_pubkey: string
|
||||||
|
permissions?: string[]
|
||||||
|
max_amount?: number
|
||||||
|
expires_at?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
dbs: StorageInterface
|
||||||
|
constructor(dbs: StorageInterface) {
|
||||||
|
this.dbs = dbs
|
||||||
|
}
|
||||||
|
|
||||||
|
async AddConnection(connection: ConnectionToAdd) {
|
||||||
|
return this.dbs.CreateAndSave<NwcConnection>('NwcConnection', {
|
||||||
|
app_id: connection.app_id,
|
||||||
|
app_user_id: connection.app_user_id,
|
||||||
|
client_pubkey: connection.client_pubkey,
|
||||||
|
permissions: connection.permissions || null,
|
||||||
|
max_amount: connection.max_amount || 0,
|
||||||
|
expires_at: connection.expires_at || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetConnection(appId: string, clientPubkey: string, txId?: string) {
|
||||||
|
return this.dbs.FindOne<NwcConnection>('NwcConnection', { where: { app_id: appId, client_pubkey: clientPubkey } }, txId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetUserConnections(appUserId: string, txId?: string) {
|
||||||
|
return this.dbs.Find<NwcConnection>('NwcConnection', { where: { app_user_id: appUserId } }, txId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async DeleteConnection(serialId: number, txId?: string) {
|
||||||
|
return this.dbs.Delete<NwcConnection>('NwcConnection', { serial_id: serialId }, txId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async DeleteConnectionByPubkey(appId: string, clientPubkey: string, txId?: string) {
|
||||||
|
return this.dbs.Delete<NwcConnection>('NwcConnection', { app_id: appId, client_pubkey: clientPubkey }, txId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async IncrementTotalSpent(appId: string, clientPubkey: string, amount: number, txId?: string) {
|
||||||
|
return this.dbs.Increment<NwcConnection>('NwcConnection', { app_id: appId, client_pubkey: clientPubkey }, 'total_spent', amount, txId)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue