nmanage backend flow

This commit is contained in:
boufni95 2025-06-30 17:47:21 +00:00
parent acb09396ec
commit 668a5bbac5
17 changed files with 303 additions and 247 deletions

8
package-lock.json generated
View file

@ -13,7 +13,7 @@
"@protobuf-ts/grpc-transport": "^2.9.4",
"@protobuf-ts/plugin": "^2.5.0",
"@protobuf-ts/runtime": "^2.5.0",
"@shocknet/clink-sdk": "^1.0.4",
"@shocknet/clink-sdk": "^1.1.2",
"@stablelib/xchacha20": "^1.0.1",
"@types/express": "^4.17.21",
"@types/node": "^17.0.31",
@ -591,9 +591,9 @@
}
},
"node_modules/@shocknet/clink-sdk": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.0.4.tgz",
"integrity": "sha512-ekkfJpP+YPry4/5+V+3JPx9zOVEjCDOWW7AHzfOLyVGnLuIR6jEBjDkg7avM2f3BVvFKSl4l0mkS9ImK9lX0eQ==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.1.2.tgz",
"integrity": "sha512-nICsXlLZRIs6E+wy3PfQccIidmQ/D7uSHfHfqrJzNJFOUH2+XGDkApB9TQU1eTrNgD/BHxm9tSZkEmG0it7I3w==",
"license": "ISC",
"dependencies": {
"@noble/hashes": "^1.8.0",

View file

@ -31,7 +31,7 @@
"@protobuf-ts/grpc-transport": "^2.9.4",
"@protobuf-ts/plugin": "^2.5.0",
"@protobuf-ts/runtime": "^2.5.0",
"@shocknet/clink-sdk": "^1.0.4",
"@shocknet/clink-sdk": "^1.1.2",
"@stablelib/xchacha20": "^1.0.1",
"@types/express": "^4.17.21",
"@types/node": "^17.0.31",

View file

@ -2,7 +2,6 @@ import 'dotenv/config'
import NewServer from '../proto/autogenerated/ts/express_server.js'
import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js';
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'
import nostrMiddleware from './nostrMiddleware.js'
import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js';
@ -22,7 +21,7 @@ const start = async () => {
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler)
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = mainSettings.nostrRelaySettings
log("initializing nostr middleware")
const { Send } = nostrMiddleware(serverMethods, mainHandler,
{ ...nostrSettings, apps, clients: [liquidityProviderInfo] },

View file

@ -2,7 +2,6 @@ import 'dotenv/config'
import NewServer from '../proto/autogenerated/ts/express_server.js'
import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js';
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'
import nostrMiddleware from './nostrMiddleware.js'
import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js';
@ -22,10 +21,9 @@ const start = async () => {
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler)
const nostrSettings = LoadNosrtSettingsFromEnv()
log("initializing nostr middleware")
const { Send, Stop, Ping } = nostrMiddleware(serverMethods, mainHandler,
{ ...nostrSettings, apps, clients: [liquidityProviderInfo] },
{ ...mainSettings.nostrRelaySettings, apps, clients: [liquidityProviderInfo] },
(e, p) => mainHandler.liquidityProvider.onEvent(e, p)
)
exitHandler(() => { Stop(); mainHandler.Stop() })
@ -33,9 +31,9 @@ const start = async () => {
mainHandler.attachNostrSend(Send)
mainHandler.attachNostrProcessPing(Ping)
mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays })
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: mainSettings.nostrRelaySettings.relays })
if (wizard) {
wizard.AddConnectInfo(appNprofile, nostrSettings.relays)
wizard.AddConnectInfo(appNprofile, mainSettings.nostrRelaySettings.relays)
}
adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler))

View file

@ -5,7 +5,7 @@ import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js";
import { NdebitData, NofferData } from "@shocknet/clink-sdk";
import { ManagementManager } from "./services/main/managementManager.js";
import { NmanageRequest } from "./services/main/managementManager.js";
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend, Ping: () => Promise<void> } => {
const log = getLogger({})
@ -39,14 +39,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
logger: { log: console.log, error: err => log(ERROR, err) },
})
let nostr: Nostr;
if (!mainHandler.managementManager) {
throw new Error("management manager not initialized on main handler")
}
const managementManager = mainHandler.managementManager;
nostr = new Nostr(nostrSettings, mainHandler.utils, event => {
const nostr = new Nostr(nostrSettings, mainHandler.utils, event => {
let j: NostrRequest
try {
j = JSON.parse(event.content)
@ -64,7 +57,8 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
mainHandler.debitManager.handleNip68Debit(debitReq, event)
return
} else if (event.kind === 21003) {
managementManager.handleRequest(event);
const nmanageReq = j as NmanageRequest
mainHandler.managementManager.handleRequest(nmanageReq, event);
return;
}
if (!j.rpcName) {
@ -80,7 +74,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
}, event.startAtNano, event.startAtMs)
})
return { Stop: () => nostr.Stop, Send: (...args: Parameters<NostrSend>) => nostr.Send(...args), Ping: () => nostr.Ping() }
return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args), Ping: () => nostr.Ping() }
}

View file

@ -4,7 +4,6 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js'
import ApplicationManager from './applicationManager.js'
import { LoadNosrtSettingsFromEnv } from '../nostr/index.js'
import { OfferPriceType, ndebitEncode, nofferEncode } from '@shocknet/clink-sdk'
export default class {
@ -66,7 +65,7 @@ export default class {
if (!appUser) {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
}
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = this.settings.nostrRelaySettings
return {
userId: ctx.user_id,
balance: user.balance_sats,

View file

@ -8,7 +8,6 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import { PubLogger, getLogger } from '../helpers/logger.js'
import crypto from 'crypto'
import { Application } from '../storage/entity/Application.js'
import { LoadNosrtSettingsFromEnv } from '../nostr/index.js'
import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { nofferEncode, ndebitEncode, OfferPriceType } from '@shocknet/clink-sdk'
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
@ -151,7 +150,7 @@ export default class {
u = user
if (created) log(u.identifier, u.user.user_id, "user created")
}
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = this.settings.nostrRelaySettings
return {
identifier: u.identifier,
info: {
@ -205,7 +204,7 @@ export default class {
const app = await this.storage.applicationStorage.GetApplication(appId)
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = this.settings.nostrRelaySettings
return {
max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats,

View file

@ -26,7 +26,6 @@ import { DebitManager } from "./debitManager.js"
import { OfferManager } from "./offerManager.js"
import webRTC from "../webRTC/index.js"
import { ManagementManager } from "./managementManager.js"
import NostrSubprocess, { LoadNosrtSettingsFromEnv } from "../nostr/index.js"
type UserOperationsSub = {
id: string
@ -53,7 +52,7 @@ export default class {
liquidityProvider: LiquidityProvider
debitManager: DebitManager
offerManager: OfferManager
managementManager?: ManagementManager
managementManager: ManagementManager
utils: Utils
rugPullTracker: RugPullTracker
unlocker: Unlocker
@ -79,7 +78,7 @@ export default class {
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
this.offerManager = new OfferManager(this.storage, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
this.managementManager = new ManagementManager(this.nostrSend, this.storage)
this.managementManager = new ManagementManager(this.storage, this.offerManager)
//this.webRTC = new webRTC(this.storage, this.utils)
}
@ -102,6 +101,7 @@ export default class {
this.liquidityProvider.attachNostrSend(f)
this.debitManager.attachNostrSend(f)
this.offerManager.attachNostrSend(f)
this.managementManager.attachNostrSend(f)
this.utils.attachNostrSend(f)
//this.webRTC.attachNostrSend(f)
}
@ -344,10 +344,6 @@ export default class {
log({ unsigned: event })
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined)
}
async Start() {
// ... existing code ...
}
}

View file

@ -4,198 +4,259 @@ import { UserOffer } from "../storage/entity/UserOffer.js";
import { ManagementGrant } from "../storage/entity/ManagementGrant.js";
import { NostrEvent, NostrSend } from "../nostr/handler.js";
import Storage from "../storage/index.js";
import { OfferManager } from "./offerManager.js";
import * as Types from "../../../proto/autogenerated/ts/types.js";
import { MainSettings } from "./settings.js";
import { nofferEncode, OfferPointer, OfferPriceType, NmanageRequest, NmanageResponse, NmanageCreateOffer, NmanageUpdateOffer, NmanageDeleteOffer, NmanageGetOffer, NmanageListOffers, OfferData, OfferFields } from "@shocknet/clink-sdk";
import { UnsignedEvent } from "nostr-tools";
type Result<T> = { success: true, result: T } | { success: false, error: string, code: number }
export class ManagementManager {
private nostrSend: NostrSend;
private storage: Storage;
private settings: MainSettings;
constructor(nostrSend: NostrSend, storage: Storage) {
this.nostrSend = nostrSend;
constructor(storage: Storage, settings: MainSettings) {
this.storage = storage;
this.settings = settings;
}
/**
* Handles an incoming CLINK Manage request
* @param event The raw Nostr event
*/
public async handleRequest(event: NostrEvent) {
let app;
attachNostrSend(f: NostrSend) {
this.nostrSend = f
}
public async handleRequest(nmanageReq: NmanageRequest, event: NostrEvent): Promise<void> {
try {
app = await this.storage.applicationStorage.GetApplication(event.appId);
} catch {
console.error(`App with id ${event.appId} not found`);
return;
const r = await this.doNmanage(nmanageReq, event)
let e: UnsignedEvent
if (!r.success) {
e = newNmanageResponse(JSON.stringify({ code: r.code, error: codeToMessage(r.code) }), event)
} else {
e = newNmanageResponse(JSON.stringify(r.result), event)
}
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
} catch (err) {
const e = newNmanageResponse(JSON.stringify({ code: 2, error: codeToMessage(2) }), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
}
if (!this._validateRequest(event)) {
return;
}
if (!app.nostr_public_key) {
console.error(`App with id ${event.appId} has no nostr public key configured.`);
return;
}
const grant = await this._checkGrant(event, app.nostr_public_key);
if (!grant) {
this.sendErrorResponse(event.pubkey, "Permission denied.", { publicKey: app.nostr_public_key, appId: app.app_id });
return;
}
const appInfo = { publicKey: app.nostr_public_key, appId: app.app_id };
await this._performAction(event, appInfo);
}
private _validateRequest(event: NostrEvent): boolean {
// TODO: NIP-44 validation or similar
return true;
}
private async _checkGrant(event: NostrEvent, appPubkey: string): Promise<ManagementGrant | null> {
const userIdTag = event.tags.find((t: string[]) => t[0] === 'p');
if (!userIdTag) {
return null;
}
const userId = userIdTag[1];
const grant = await this.storage.managementStorage.getGrant(userId, appPubkey);
if (!grant || (grant.expires_at && grant.expires_at.getTime() < Date.now())) {
return null;
}
return grant;
}
private async _performAction(event: NostrEvent, app: { publicKey: string; appId: string }) {
const actionTag = event.tags.find((t: string[]) => t[0] === 'a');
if (!actionTag) {
console.error("No action specified in event");
return;
}
const action = actionTag[1];
private async doNmanage(nmanageReq: NmanageRequest, event: NostrEvent): Promise<Result<NmanageResponse>> {
const action = nmanageReq.action
switch (action) {
case "create":
await this._createOffer(event, app);
break;
const createResult = await this.createOffer(nmanageReq)
return this.getNmanageResponse(event.appId, createResult)
case "update":
await this._updateOffer(event, app);
break;
const updateResult = await this.updateOffer(nmanageReq, event.pub);
return this.getNmanageResponse(event.appId, updateResult)
case "delete":
await this._deleteOffer(event, app);
break;
const deleteResult = await this.deleteOffer(nmanageReq, event.pub);
return this.getNmanageResponse(event.appId, deleteResult)
case "get":
const getResult = await this.getOffer(nmanageReq, event.pub);
return this.getNmanageResponse(event.appId, getResult)
case "list":
const listResult = await this.listOffers(nmanageReq, event.pub);
return this.getNmanageResponse(event.appId, listResult)
default:
console.error(`Unknown action: ${action}`);
this.sendErrorResponse(event.pubkey, `Unknown action: ${action}`, app);
return { success: false, error: `Unknown action: ${action}`, code: 1 }
}
}
private async _createOffer(event: NostrEvent, app: { publicKey: string; appId: string }) {
const createDetailsTag = event.tags.find((t: string[]) => t[0] === 'd');
if (!createDetailsTag || !createDetailsTag[1]) {
console.error("No details provided for create action");
return;
private getOfferData(offer: UserOffer, appPub: string): OfferData {
const pointer: OfferPointer = {
offer: offer.offer_id,
pubkey: appPub,
relay: this.settings.nostrRelaySettings.relays[0],
priceType: offer.price_sats > 0 ? OfferPriceType.Fixed : OfferPriceType.Spontaneous,
price: offer.price_sats,
}
const userId = event.tags.find((t: string[]) => t[0] === 'p')![1];
try {
const offerData = JSON.parse(createDetailsTag[1]);
const offerRepo = getRepository(UserOffer);
const newOffer = offerRepo.create({
...offerData,
user_id: userId,
managing_app_pubkey: app.publicKey
});
await offerRepo.save(newOffer);
this.sendSuccessResponse(event.pubkey, "Offer created successfully", app);
} catch (e) {
console.error("Failed to parse or save offer data", e);
this.sendErrorResponse(event.pubkey, "Failed to create offer", app);
return {
id: offer.offer_id,
label: offer.label,
price_sats: offer.price_sats,
callback_url: offer.callback_url,
payer_data: Object.keys(offer.expected_data || {}),
noffer: nofferEncode(pointer),
}
}
private async _updateOffer(event: NostrEvent, app: { publicKey: string; appId: string }) {
const updateTags = event.tags.filter((t: string[]) => t[0] === 'd');
if (updateTags.length < 2) {
console.error("Insufficient details for update action");
return;
private async getNmanageResponse(appId: string, result: Result<UserOffer | UserOffer[] | void>): Promise<Result<NmanageResponse>> {
if (!result.success) {
return result
}
const offerIdToUpdate = updateTags[0][1];
const updateData = JSON.parse(updateTags[1][1]);
const offerRepo = getRepository(UserOffer);
try {
const existingOffer = await offerRepo.findOne({where: { offer_id: offerIdToUpdate }});
if (!existingOffer) {
console.error(`Offer ${offerIdToUpdate} not found`);
return;
const args = result.result
const app = await this.storage.applicationStorage.GetApplication(appId)
if (args && Array.isArray(args)) {
return {
success: true, result: {
res: 'ok', resource: 'offer', details: args.map(offer => this.getOfferData(offer, app.nostr_public_key!))
}
}
if (existingOffer.managing_app_pubkey !== app.publicKey) {
console.error(`App ${app.publicKey} not authorized to update offer ${offerIdToUpdate}`);
return;
}
if (!args) {
return { success: true, result: { res: 'ok', resource: 'offer' } }
}
return {
success: true, result: {
res: 'ok', resource: 'offer', details: this.getOfferData(args, app.nostr_public_key!)
}
offerRepo.merge(existingOffer, updateData);
await offerRepo.save(existingOffer);
this.sendSuccessResponse(event.pubkey, "Offer updated successfully", app);
} catch (e) {
console.error("Failed to update offer data", e);
this.sendErrorResponse(event.pubkey, "Failed to update offer", app);
}
}
private async _deleteOffer(event: NostrEvent, app: { publicKey: string; appId: string }) {
const deleteDetailsTag = event.tags.find((t: string[]) => t[0] === 'd');
if (!deleteDetailsTag || !deleteDetailsTag[1]) {
console.error("No details provided for delete action");
return;
}
const offerIdToDelete = deleteDetailsTag[1];
const offerRepo = getRepository(UserOffer);
try {
const offerToDelete = await offerRepo.findOne({where: { offer_id: offerIdToDelete }});
if (!offerToDelete) {
console.error(`Offer ${offerIdToDelete} not found`);
return;
}
if (offerToDelete.managing_app_pubkey !== app.publicKey) {
console.error(`App ${app.publicKey} not authorized to delete offer ${offerIdToDelete}`);
return;
}
await offerRepo.remove(offerToDelete);
this.sendSuccessResponse(event.pubkey, "Offer deleted successfully", app);
} catch (e) {
console.error("Failed to delete offer", e);
this.sendErrorResponse(event.pubkey, "Failed to delete offer", app);
private async getOffer(nmanageReq: NmanageGetOffer, requestorPub: string): Promise<Result<UserOffer>> {
const offer = await this.validateOfferAccess(nmanageReq.offer.id, requestorPub)
if (!offer.success) {
return offer
}
return { success: true, result: offer.result }
}
private sendSuccessResponse(recipient: string, message: string, app: {publicKey: string, appId: string}) {
const responseEvent = {
kind: 21003,
pubkey: app.publicKey,
created_at: Math.floor(Date.now() / 1000),
content: JSON.stringify({ status: "success", message }),
tags: [['p', recipient]],
};
this.nostrSend({ type: 'app', appId: app.appId }, { type: 'event', event: responseEvent, encrypt: { toPub: recipient } });
private async listOffers(nmanageReq: NmanageListOffers, requestorPub: string): Promise<Result<UserOffer[]>> {
const appUserId = nmanageReq.pointer
if (!appUserId) {
return { success: false, error: 'No pointer provided', code: 1 }
}
const grantResult = await this.validateGrantAccess(appUserId, requestorPub)
if (!grantResult.success) {
return grantResult
}
const offers = await this.storage.offerStorage.getManagedUserOffers(appUserId, requestorPub)
return { success: true, result: offers }
}
private sendErrorResponse(recipient: string, message: string, app: {publicKey: string, appId: string}) {
const responseEvent = {
kind: 21003,
pubkey: app.publicKey,
created_at: Math.floor(Date.now() / 1000),
content: JSON.stringify({ status: "error", message }),
tags: [['p', recipient]],
};
this.nostrSend({ type: 'app', appId: app.appId }, { type: 'event', event: responseEvent, encrypt: { toPub: recipient } });
private validateOfferFields(fields: OfferFields): Result<void> {
if (!fields.label || typeof fields.label !== 'string') {
return { success: false, error: 'Label is required', code: 1 }
}
if (fields.price_sats && typeof fields.price_sats !== 'number') {
return { success: false, error: 'Price must be a number', code: 1 }
}
if (fields.callback_url && typeof fields.callback_url !== 'string') {
return { success: false, error: 'Callback URL must be a string', code: 1 }
}
if (fields.payer_data && !Array.isArray(fields.payer_data)) {
return { success: false, error: 'Payer data must be an array', code: 1 }
}
return { success: true, result: undefined }
}
private async createOffer(nmanageReq: NmanageCreateOffer): Promise<Result<UserOffer>> {
const appUserId = nmanageReq.pointer
if (!appUserId) {
return { success: false, error: 'No pointer provided', code: 1 }
}
const grantResult = await this.validateGrantAccess(appUserId, appUserId)
if (!grantResult.success) {
return grantResult
}
const validateResult = this.validateOfferFields(nmanageReq.offer.fields)
if (!validateResult.success) {
return validateResult
}
const dataMap: Record<string, Types.OfferDataType> = {}
nmanageReq.offer.fields.payer_data.forEach(data => {
dataMap[data] = Types.OfferDataType.DATA_STRING
})
const offer = await this.storage.offerStorage.AddUserOffer(appUserId, {
label: nmanageReq.offer.fields.label,
callback_url: nmanageReq.offer.fields.callback_url,
price_sats: nmanageReq.offer.fields.price_sats,
expected_data: dataMap,
})
return { success: true, result: offer }
}
private async validateGrantAccess(appUserId: string, requestorPub: string): Promise<Result<void>> {
const grant = await this.storage.managementStorage.getGrant(appUserId, requestorPub)
if (!grant) {
// TODO request from user
return { success: false, error: 'No grant found', code: 1 }
}
if (grant.expires_at_unix < Date.now()) {
return { success: false, error: 'Grant expired', code: 3 }
}
return { success: true, result: undefined }
}
private async validateOfferAccess(offerId: string, requestorPub: string): Promise<Result<UserOffer>> {
const offer = await this.storage.offerStorage.GetOffer(offerId)
if (!offer) {
return { success: false, error: 'Offer not found', code: 1 }
}
if (offer.management_pubkey !== requestorPub) {
return { success: false, error: 'App not authorized to update offer', code: 1 }
}
const grantResult = await this.validateGrantAccess(offer.app_user_id, requestorPub)
if (!grantResult.success) {
return grantResult
}
return { success: true, result: offer }
}
private async updateOffer(nmanageReq: NmanageUpdateOffer, requestorPub: string): Promise<Result<UserOffer>> {
const offer = await this.validateOfferAccess(nmanageReq.offer.id, requestorPub)
if (!offer.success) {
return offer
}
const validateResult = this.validateOfferFields(nmanageReq.offer.fields)
if (!validateResult.success) {
return validateResult
}
const dataMap: Record<string, Types.OfferDataType> = {}
for (const data of nmanageReq.offer.fields.payer_data || []) {
if (typeof data !== 'string') {
return { success: false, error: 'Payer data must be a string', code: 1 }
}
dataMap[data] = Types.OfferDataType.DATA_STRING
}
await this.storage.offerStorage.UpdateUserOffer(offer.result.app_user_id, nmanageReq.offer.id, {
label: nmanageReq.offer.fields.label,
callback_url: nmanageReq.offer.fields.callback_url,
price_sats: nmanageReq.offer.fields.price_sats,
expected_data: dataMap,
})
const updatedOffer = await this.storage.offerStorage.GetOffer(nmanageReq.offer.id)
if (!updatedOffer) {
return { success: false, error: 'Offer not found', code: 2 }
}
return { success: true, result: updatedOffer }
}
private async deleteOffer(nmanageReq: NmanageDeleteOffer, requestorPub: string): Promise<Result<void>> {
const offerResult = await this.validateOfferAccess(nmanageReq.offer.id, requestorPub)
if (!offerResult.success) {
return offerResult
}
await this.storage.offerStorage.DeleteUserOffer(offerResult.result.app_user_id, offerResult.result.offer_id)
return { success: true, result: undefined }
}
}
const newNmanageResponse = (content: string, event: NostrEvent): UnsignedEvent => {
return {
content,
created_at: Math.floor(Date.now() / 1000),
kind: 21003,
pubkey: "",
tags: [
['p', event.pub],
['e', event.id],
],
}
}
const codeToMessage = (code: number, reason = "") => {
switch (code) {
case 1: return 'Request Denied'
case 2: return 'Temporary Failure: ' + reason
case 3: return 'Expired Request'
case 4: return 'Rate Limited'
case 5: return 'Invalid Field or Value'
case 6: return 'Invalid Request: ' + reason
default: throw new Error("unknown error code" + code)
}
}

View file

@ -7,9 +7,9 @@ import { ERROR, getLogger } from "../helpers/logger.js";
import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js';
import { UnsignedEvent } from 'nostr-tools';
import { UserOffer } from '../storage/entity/UserOffer.js';
import { LoadNosrtSettingsFromEnv } from '../nostr/index.js';
import { LiquidityManager } from "./liquidityManager.js"
import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk';
import { MainSettings } from "./settings.js";
const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => {
if (offer.expected_data) {
@ -38,15 +38,16 @@ export class OfferManager {
_nostrSend: NostrSend | null = null
settings: MainSettings
applicationManager: ApplicationManager
productManager: ProductManager
storage: Storage
lnd: LND
liquidityManager: LiquidityManager
logger = getLogger({ component: 'DebitManager' })
constructor(storage: Storage, lnd: LND, applicationManager: ApplicationManager, productManager: ProductManager, liquidityManager: LiquidityManager) {
constructor(storage: Storage, settings: MainSettings, lnd: LND, applicationManager: ApplicationManager, productManager: ProductManager, liquidityManager: LiquidityManager) {
this.storage = storage
this.settings = settings
this.lnd = lnd
this.applicationManager = applicationManager
this.productManager = productManager
@ -113,7 +114,7 @@ export class OfferManager {
if (!offer) {
throw new Error("Offer not found")
}
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = this.settings.nostrRelaySettings
return mapToOfferConfig(ctx.app_user_id, offer, { pubkey: app.npub, relay: nostrSettings.relays[0] })
}
@ -131,7 +132,7 @@ export class OfferManager {
if (toAppend) {
offers.push(toAppend)
}
const nostrSettings = LoadNosrtSettingsFromEnv()
const nostrSettings = this.settings.nostrRelaySettings
return {
offers: offers.map(o => mapToOfferConfig(ctx.app_user_id, o, { pubkey: app.npub, relay: nostrSettings.relays[0] }))
}

View file

@ -7,12 +7,14 @@ import { getLogger } from '../helpers/logger.js'
import fs from 'fs'
import crypto from 'crypto';
import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
import { LoadNosrtRelaySettingsFromEnv, NostrRelaySettings } from '../nostr/handler.js'
export type MainSettings = {
storageSettings: StorageSettings,
lndSettings: LndSettings,
watchDogSettings: WatchdogSettings,
liquiditySettings: LiquiditySettings,
nostrRelaySettings: NostrRelaySettings,
jwtSecret: string
walletPasswordPath: string
walletSecretPath: string
@ -49,12 +51,13 @@ export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettin
export const LoadMainSettingsFromEnv = (): MainSettings => {
const storageSettings = LoadStorageSettingsFromEnv()
const outgoingAppUserInvoiceFeeBps = EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0)
const nostrRelaySettings = LoadNosrtRelaySettingsFromEnv()
return {
watchDogSettings: LoadWatchdogSettingsFromEnv(),
lndSettings: LoadLndSettingsFromEnv(),
storageSettings: storageSettings,
liquiditySettings: LoadLiquiditySettingsFromEnv(),
nostrRelaySettings: nostrRelaySettings,
jwtSecret: loadJwtSecret(storageSettings.dataDir),
walletSecretPath: process.env.WALLET_SECRET_PATH || getDataPath(storageSettings.dataDir, ".wallet_secret"),
walletPasswordPath: process.env.WALLET_PASSWORD_PATH || getDataPath(storageSettings.dataDir, ".wallet_password"),

View file

@ -7,6 +7,7 @@ import { ERROR, getLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js'
import { EnvCanBeInteger, } from '../helpers/envParser.js'
const { nprofileEncode } = nip19
const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
@ -26,16 +27,34 @@ export type NostrSettings = {
clients: ClientInfo[]
maxEventContentLength: number
}
export type NostrEvent = Event & {
/** Identifier of the application as defined in NostrSettings.apps */
appId: string;
/** High-resolution timer capture when processing began (BigInt serialized as string to keep JSON friendly) */
startAtNano: string;
/** wall-clock millis when processing began */
startAtMs: number;
/** Convenience duplicate of the sender pubkey (e.pubkey) kept for backwards-compat */
pub: string;
};
export type NostrRelaySettings = {
relays: string[],
maxEventContentLength: number
}
const getEnvOrDefault = (name: string, defaultValue: string): string => {
return process.env[name] || defaultValue;
}
export const LoadNosrtRelaySettingsFromEnv = (test = false): NostrRelaySettings => {
const relaysEnv = getEnvOrDefault("NOSTR_RELAYS", "wss://relay.lightning.pub");
const maxEventContentLength = EnvCanBeInteger("NOSTR_MAX_EVENT_CONTENT_LENGTH", 40000)
return {
relays: relaysEnv.split(' '),
maxEventContentLength
}
}
export type NostrEvent = {
id: string
pub: string
content: string
appId: string
startAtNano: string
startAtMs: number
kind: number
}
type SettingsRequest = {
type: 'settings'
@ -217,15 +236,7 @@ export default class Handler {
return
}
const nostrEvent: NostrEvent = {
...e,
content,
appId: app.appId,
startAtNano,
startAtMs,
pub: e.pubkey,
}
this.eventCallback(nostrEvent)
this.eventCallback({ id: eventId, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs, kind: e.kind })
}
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {

View file

@ -4,18 +4,9 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S
import { Utils } from '../helpers/utilsWrapper.js'
type EventCallback = (event: NostrEvent) => void
const getEnvOrDefault = (name: string, defaultValue: string): string => {
return process.env[name] || defaultValue;
}
export const LoadNosrtSettingsFromEnv = (test = false) => {
const relaysEnv = getEnvOrDefault("NOSTR_RELAYS", "wss://relay.lightning.pub");
const maxEventContentLength = EnvCanBeInteger("NOSTR_MAX_EVENT_CONTENT_LENGTH", 40000)
return {
relays: relaysEnv.split(' '),
maxEventContentLength
}
}
export default class NostrSubprocess {
settings: NostrSettings

View file

@ -1,24 +1,23 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from "typeorm";
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
@Entity("management_grants")
export class ManagementGrant {
@PrimaryGeneratedColumn("uuid")
id: string;
@PrimaryGeneratedColumn()
serial_id: number
@Column()
user_id: string;
@ManyToOne(() => User)
@JoinColumn({ name: "user_id", referencedColumnName: "user_id" })
user: User;
app_user_id: string
@Column()
app_pubkey: string;
@CreateDateColumn()
created_at: Date;
@Column()
expires_at_unix: number
@Column({ type: 'timestamp', nullable: true })
expires_at: Date;
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -13,8 +13,8 @@ export class UserOffer {
@Column({ unique: true, nullable: false })
offer_id: string
@Column({ type: "text", nullable: true })
managing_app_pubkey: string | null
@Column({ default: "" })
management_pubkey: string
@Column()
label: string

View file

@ -7,11 +7,11 @@ export class ManagementStorage {
this.dbs = dbs;
}
getGrant(user_id: string, app_pubkey: string) {
return this.dbs.FindOne<ManagementGrant>('ManagementGrant' as any, { where: { user_id, app_pubkey } });
getGrant(appUserId: string, appPubkey: string) {
return this.dbs.FindOne<ManagementGrant>('ManagementGrant', { where: { app_pubkey: appPubkey, app_user_id: appUserId } });
}
async addGrant(user_id: string, app_pubkey: string, expires_at?: Date) {
return this.dbs.CreateAndSave<ManagementGrant>('ManagementGrant' as any, { user_id, app_pubkey, expires_at });
async addGrant(appUserId: string, appPubkey: string, expires_at_unix: number) {
return this.dbs.CreateAndSave<ManagementGrant>('ManagementGrant', { app_user_id: appUserId, app_pubkey: appPubkey, expires_at_unix });
}
}

View file

@ -34,6 +34,11 @@ export default class {
async GetUserOffers(app_user_id: string): Promise<UserOffer[]> {
return this.dbs.Find<UserOffer>('UserOffer', { where: { app_user_id } })
}
async getManagedUserOffers(app_user_id: string, management_pubkey: string): Promise<UserOffer[]> {
return this.dbs.Find<UserOffer>('UserOffer', { where: { app_user_id, management_pubkey } })
}
async GetUserOffer(app_user_id: string, offer_id: string): Promise<UserOffer | null> {
return this.dbs.FindOne<UserOffer>('UserOffer', { where: { app_user_id, offer_id } })
}