nmanage backend flow
This commit is contained in:
parent
acb09396ec
commit
668a5bbac5
17 changed files with 303 additions and 247 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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] },
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private async getOffer(nmanageReq: NmanageGetOffer, requestorPub: string): Promise<Result<UserOffer>> {
|
||||
const offer = await this.validateOfferAccess(nmanageReq.offer.id, requestorPub)
|
||||
if (!offer.success) {
|
||||
return offer
|
||||
}
|
||||
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;
|
||||
return { success: true, result: offer.result }
|
||||
}
|
||||
|
||||
if (offerToDelete.managing_app_pubkey !== app.publicKey) {
|
||||
console.error(`App ${app.publicKey} not authorized to delete offer ${offerIdToDelete}`);
|
||||
return;
|
||||
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 }
|
||||
}
|
||||
|
||||
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 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 }
|
||||
}
|
||||
}
|
||||
|
||||
private sendSuccessResponse(recipient: string, message: string, app: {publicKey: string, appId: string}) {
|
||||
const responseEvent = {
|
||||
kind: 21003,
|
||||
pubkey: app.publicKey,
|
||||
const newNmanageResponse = (content: string, event: NostrEvent): UnsignedEvent => {
|
||||
return {
|
||||
content,
|
||||
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 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 } });
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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] }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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[]) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 } })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue