diff --git a/.gitignore b/.gitignore index ab360761..dbe0467d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ temp/ .env build/ db.sqlite -.key/ \ No newline at end of file +.key/ +logs \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index a0fa62f1..9e774225 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,11 +1,14 @@ import { ServerOptions } from "../proto/autogenerated/ts/express_server"; import { AdminContext } from "../proto/autogenerated/ts/types"; import Main from './services/main' +import { getLogger } from './services/helpers/logger.js' const serverOptions = (mainHandler: Main): ServerOptions => { + const log = getLogger({}) return { + logger: { log, error: err => log("ERROR", err) }, AdminAuthGuard: adminAuth, - AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(authHeader) } }, - UserAuthGuard: async (authHeader) => { return { user_id: mainHandler.userManager.DecodeUserToken(authHeader) } }, + AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(stripBearer(authHeader)) } }, + UserAuthGuard: async (authHeader) => { return { user_id: mainHandler.userManager.DecodeUserToken(stripBearer(authHeader)) } }, GuestAuthGuard: async (_) => ({}), encryptCallback: async (_, b) => b, decryptCallback: async (_, b) => b, @@ -13,6 +16,16 @@ const serverOptions = (mainHandler: Main): ServerOptions => { } } +const stripBearer = (header?: string) => { + if (!header) { + return "" + } + if (header.startsWith("Bearer ")) { + return header.substring("Bearer ".length) + } + return header +} + const adminAuth = async (header: string | undefined): Promise => { const AdminToken = process.env.ADMIN_TOKEN if (!AdminToken) { diff --git a/src/services/helpers/logger.ts b/src/services/helpers/logger.ts new file mode 100644 index 00000000..754489a1 --- /dev/null +++ b/src/services/helpers/logger.ts @@ -0,0 +1,43 @@ +import fs from 'fs' +type LoggerParams = { appName?: string, userId?: string } +export type PubLogger = (...message: (string | number | object)[]) => void +type Writer = (message: string) => void +try { + fs.mkdirSync("logs") +} catch { } +const z = (n: number) => n < 10 ? `0${n}` : `${n}` +const openWriter = (fileName: string): Writer => { + const logStream = fs.createWriteStream(`logs/${fileName}`, { flags: 'a' }); + return (message) => { + logStream.write(message + "\n") + } +} +const rootWriter = openWriter("ROOT.log") +export const getLogger = (params: LoggerParams): PubLogger => { + const writers: Writer[] = [] + if (params.appName) { + writers.push(openWriter(`apps/${params.appName}.log`)) + } + if (params.userId) { + writers.push(openWriter(`users/${params.userId}.log`)) + } + if (writers.length === 0) { + writers.push(rootWriter) + } + + return (...message) => { + const now = new Date() + const timestamp = `${now.getFullYear()}-${z(now.getMonth())}-${z(now.getDate())} ${z(now.getHours())}:${z(now.getMinutes())}:${z(now.getSeconds())}` + const toLog = [timestamp] + if (params.appName) { + toLog.push(params.appName) + } + if (params.userId) { + toLog.push(params.userId) + } + const parsed = message.map(m => typeof m === 'object' ? JSON.stringify(m) : m) + const final = `${toLog.join(" ")} >> ${parsed.join(" ")}` + console.log(final) + writers.forEach(w => w(final)) + } +} diff --git a/src/services/lnd/index.ts b/src/services/lnd/index.ts index ca9a8cbb..78831c36 100644 --- a/src/services/lnd/index.ts +++ b/src/services/lnd/index.ts @@ -4,6 +4,7 @@ import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../h import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NodeInfo, PaidInvoice } from './settings.js' import LND from './lnd.js' import MockLnd from './mock.js' +import { getLogger } from '../helpers/logger.js' export const LoadLndSettingsFromEnv = (test = false): LndSettings => { const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS") const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH") @@ -32,10 +33,10 @@ export interface LightningHandler { export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => { if (settings.mockLnd) { - console.log("registering mock lnd handler") + getLogger({})("registering mock lnd handler") return new MockLnd(settings, addressPaidCb, invoicePaidCb) } else { - console.log("registering prod lnd handler") + getLogger({})("registering prod lnd handler") return new LND(settings, addressPaidCb, invoicePaidCb) } } \ No newline at end of file diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 7087e3ab..0441126f 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -13,8 +13,9 @@ import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; import { SendCoinsReq } from './sendCoinsReq.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js'; +import { getLogger } from '../helpers/logger.js'; const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) - +const deadLndRetrySeconds = 5 export default class { lightning: LightningClient invoices: InvoicesClient @@ -26,6 +27,7 @@ export default class { abortController = new AbortController() addressPaidCb: AddressPaidCb invoicePaidCb: InvoicePaidCb + log = getLogger({}) constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { this.settings = settings this.addressPaidCb = addressPaidCb @@ -49,8 +51,6 @@ export default class { this.lightning = new LightningClient(transport) this.invoices = new InvoicesClient(transport) this.router = new RouterClient(transport) - this.SubscribeAddressPaid() - this.SubscribeInvoicePaid() } SetMockInvoiceAsPaid(invoice: string, amount: number): Promise { throw new Error("SetMockInvoiceAsPaid only available in mock mode") @@ -58,7 +58,11 @@ export default class { Stop() { this.abortController.abort() } - async Warmup() { this.ready = true } + async Warmup() { + this.SubscribeAddressPaid() + this.SubscribeInvoicePaid() + this.ready = true + } async GetInfo(): Promise { const res = await this.lightning.getInfo({}, DeadLineMetadata()) @@ -71,12 +75,27 @@ export default class { } const info = await this.GetInfo() if (!info.syncedToChain || !info.syncedToGraph) { - throw new Error("not ready") + throw new Error("not synced") } } - checkReady(): void { - if (!this.ready) throw new Error("lnd not ready, warmup required before usage") + + RestartStreams() { + if (!this.ready) { + return + } + this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") + const interval = setInterval(async () => { + try { + await this.Health() + this.log("LND is back online") + clearInterval(interval) + this.Warmup() + } catch (err) { + this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") + } + }, deadLndRetrySeconds * 1000) } + SubscribeAddressPaid(): void { const stream = this.lightning.subscribeTransactions({ account: "", @@ -84,19 +103,24 @@ export default class { startHeight: this.latestKnownBlockHeigh, }, { abort: this.abortController.signal }) stream.responses.onMessage(tx => { + if (tx.blockHeight > this.latestKnownBlockHeigh) { this.latestKnownBlockHeigh = tx.blockHeight } if (tx.numConfirmations > 0) { tx.outputDetails.forEach(output => { if (output.isOurAddress) { + this.log("received chan TX", Number(output.amount), "sats") this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount)) } }) } }) stream.responses.onError(error => { - // TODO... + this.log("Error with onchain tx stream") + }) + stream.responses.onComplete(() => { + this.log("onchain tx stream closed") }) } @@ -107,16 +131,22 @@ export default class { }, { abort: this.abortController.signal }) stream.responses.onMessage(invoice => { if (invoice.state === Invoice_InvoiceState.SETTLED) { + this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats") this.latestKnownSettleIndex = Number(invoice.settleIndex) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat)) } }) stream.responses.onError(error => { - // TODO... + this.log("Error with invoice stream") + }) + stream.responses.onComplete(() => { + this.log("invoice stream closed") + this.RestartStreams() }) } + async NewAddress(addressType: Types.AddressType): Promise { - this.checkReady() + await this.Health() let lndAddressType: AddressType switch (addressType) { case Types.AddressType.NESTED_PUBKEY_HASH: @@ -136,7 +166,7 @@ export default class { } async NewInvoice(value: number, memo: string, expiry: number): Promise { - this.checkReady() + await this.Health() const encoder = new TextEncoder() const ecoded = encoder.encode(memo) const hashed = crypto.createHash('sha256').update(ecoded).digest(); @@ -158,7 +188,7 @@ export default class { } async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise { - this.checkReady() + await this.Health() const abortController = new AbortController() const req = PayInvoiceReq(invoice, amount, feeLimit) const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) @@ -167,12 +197,13 @@ export default class { rej(error) }) stream.responses.onMessage(payment => { - console.log(payment) switch (payment.status) { case Payment_PaymentStatus.FAILED: + this.log("invoice payment failed", payment.failureReason) rej(PaymentFailureReason[payment.failureReason]) return case Payment_PaymentStatus.SUCCEEDED: + this.log("invoice payment succeded", Number(payment.valueSat)) res({ feeSat: Number(payment.feeSat), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage }) } }) @@ -180,7 +211,7 @@ export default class { } async EstimateChainFees(address: string, amount: number, targetConf: number): Promise { - this.checkReady() + await this.Health() const res = await this.lightning.estimateFee({ addrToAmount: { [address]: BigInt(amount) }, minConfs: 1, @@ -191,14 +222,15 @@ export default class { } async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise { - this.checkReady() + await this.Health() const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) + this.log("sent chain TX for", amount, "sats") return res.response } async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise { - this.checkReady() + await this.Health() const abortController = new AbortController() const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats) const stream = this.lightning.openChannel(req, { abort: abortController.signal }) diff --git a/src/services/lnd/mock.ts b/src/services/lnd/mock.ts index 0641cf9d..6f19ef7f 100644 --- a/src/services/lnd/mock.ts +++ b/src/services/lnd/mock.ts @@ -13,6 +13,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; import { SendCoinsReq } from './sendCoinsReq.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js'; +import { getLogger } from '../helpers/logger.js'; export default class { invoicesAwaiting: Record = {} @@ -86,10 +87,11 @@ export default class { } async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise { - console.log('payng', invoice) + const log = getLogger({}) + log('payng', invoice) await new Promise(res => setTimeout(res, 200)) const amt = this.decodeOutboundInvoice(invoice) - console.log('paid', invoice) + log('paid', invoice) return { feeSat: 1, paymentPreimage: "all_good", valueSat: amt || amount } } diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index aa3922b0..8af7fe4b 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -6,6 +6,7 @@ import { MainSettings } from './settings.js' import PaymentManager from './paymentManager.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js' +import { getLogger } from '../helpers/logger.js' export default class { storage: Storage settings: MainSettings @@ -35,7 +36,8 @@ export default class { } async SetMockAppUserBalance(appId: string, req: Types.SetMockAppUserBalanceRequest) { - const user = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.user_identifier, 0) + const app = await this.storage.applicationStorage.GetApplication(appId) + const { user } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.user_identifier, 0) await this.paymentManager.SetMockUserBalance(user.user.user_id, req.amount) } @@ -47,6 +49,8 @@ export default class { async AddApp(req: Types.AuthAppRequest): Promise { const app = await this.storage.applicationStorage.AddApplication(req.name) + getLogger({ appName: app.name })("app created") + return { app: { id: app.app_id, @@ -79,11 +83,16 @@ export default class { } async AddAppUser(appId: string, req: Types.AddAppUserRequest): Promise { + const app = await this.storage.applicationStorage.GetApplication(appId) + const log = getLogger({ appName: app.name }) let u: ApplicationUser if (req.fail_if_exists) { - u = await this.storage.applicationStorage.AddApplicationUser(appId, req.identifier, req.balance) + u = await this.storage.applicationStorage.AddApplicationUser(app, req.identifier, req.balance) + log(u.identifier, u.user.user_id, "user created") } else { - u = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.identifier, req.balance) + const { user, created } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.identifier, req.balance) + u = user + if (created) log(u.identifier, u.user.user_id, "user created") } return { identifier: u.identifier, @@ -97,26 +106,29 @@ export default class { async AddAppInvoice(appId: string, req: Types.AddAppInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) - const payer = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.payer_identifier, 0) + const { user: payer } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.payer_identifier, 0) const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app } - return this.paymentManager.NewInvoice(app.owner.user_id, req.invoice_req, opts) + const invoice = await this.paymentManager.NewInvoice(app.owner.user_id, req.invoice_req, opts) + getLogger({ appName: app.name })("app invoice created to be paid by", payer.identifier) + return invoice } async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) - const receiver = await this.storage.applicationStorage.GetApplicationUser(appId, req.receiver_identifier) - const payer = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.payer_identifier, 0) + const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) + const { user: payer } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.payer_identifier, 0) const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) + getLogger({ appName: app.name })(receiver.identifier, "invoice created to be paid by", payer.identifier) return { invoice: appUserInvoice.invoice } } async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise { - const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) + 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) - console.log(max, user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats @@ -126,25 +138,29 @@ export default class { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) - const appUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) - return this.paymentManager.PayInvoice(appUser.user.user_id, req, app) + const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) + const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) + getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") + return paid } async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise { - const fromUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.from_user_identifier) - const toUser = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.to_user_identifier, 0) const app = await this.storage.applicationStorage.GetApplication(appId) + const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier) + const { user: toUser } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.to_user_identifier, 0) await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, toUser.user.user_id, req.amount, app) + getLogger({ appName: app.name })(toUser.identifier, "received internal payment by", fromUser.identifier, "of", req.amount, "sats") } async SendAppUserToAppPayment(appId: string, req: Types.SendAppUserToAppPaymentRequest): Promise { - const fromUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.from_user_identifier) const app = await this.storage.applicationStorage.GetApplication(appId) + const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier) await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, app.owner.user_id, req.amount, app) + getLogger({ appName: app.name })("app received internal payment by", fromUser.identifier, "of", req.amount, "sats") } async GetAppUserLNURLInfo(appId: string, req: Types.GetAppUserLNURLInfoRequest): Promise { - const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) const app = await this.storage.applicationStorage.GetApplication(appId) + const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) return this.paymentManager.GetLnurlPayInfoFromUser(user.user.user_id, app, req.base_url_override) } } \ No newline at end of file diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 10958c7e..136df8ff 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -9,6 +9,7 @@ import PaymentManager from './paymentManager.js' import { MainSettings } from './settings.js' import NewLightningHandler, { LoadLndSettingsFromEnv, LightningHandler } from "../lnd/index.js" import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js" +import { getLogger, PubLogger } from "../helpers/logger.js" export const LoadMainSettingsFromEnv = (test = false): MainSettings => { return { lndSettings: LoadLndSettingsFromEnv(test), @@ -75,8 +76,9 @@ export default class { this.storage.StartTransaction(async tx => { const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) if (!userInvoice || userInvoice.paid_at_unix > 0) { return } + const log = getLogger({}) if (!userInvoice.linkedApplication) { - console.error("an invoice was paid, that has no linked application") + log("ERROR", "an invoice was paid, that has no linked application") return } const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id @@ -92,22 +94,23 @@ export default class { if (isAppUserPayment && fee > 0) { await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, tx) } - await this.triggerPaidCallback(userInvoice.callbackUrl) - } catch { - //TODO + + await this.triggerPaidCallback(log, userInvoice.callbackUrl) + log("paid invoice processed successfully") + } catch (err: any) { + log("ERROR", "cannot process paid invoice", err.message || "") } }) } - async triggerPaidCallback(url: string) { - console.log(url) + async triggerPaidCallback(log: PubLogger, url: string) { if (!url) { return } try { await fetch(url + "&ok=true") } catch (err: any) { - console.log("error sending cb", err) + log("error sending paid callback for invoice", err.message || "") } } } \ No newline at end of file diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 1b82feb5..ca94eff7 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -5,6 +5,7 @@ import { MainSettings } from './settings.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { LightningHandler } from '../lnd/index.js' import { Application } from '../storage/entity/Application.js' +import { getLogger } from '../helpers/logger.js' interface UserOperationInfo { serial_id: number paid_amount: number @@ -59,7 +60,7 @@ export default class { if (!this.settings.lndSettings.mockLnd) { throw new Error("mock disabled, cannot set invoice as paid") } - console.log("setting mock balance...") + getLogger({})("setting mock balance...") await this.storage.userStorage.UpdateUser(userId, { balance_sats: balance }) } @@ -81,7 +82,6 @@ export default class { } async lockUserWithMinBalance(userId: string, minBalance: number) { - console.log("locking", userId) return this.storage.StartTransaction(async tx => { const user = await this.storage.userStorage.GetUser(userId, tx) if (user.locked) { diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index cd6797dc..e598d248 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -138,7 +138,6 @@ export default (mainHandler: Main): Types.ServerMethods => { payer_identifier_CustomCheck: id => id !== '', }) if (err != null) throw new Error(err.message) - console.log(req) return mainHandler.applicationManager.AddAppInvoice(ctx.app_id, req) }, AddAppUserInvoice: async (ctx, req) => { diff --git a/src/services/storage/applicationStorage.ts b/src/services/storage/applicationStorage.ts index b5f9d0c1..67f8372b 100644 --- a/src/services/storage/applicationStorage.ts +++ b/src/services/storage/applicationStorage.ts @@ -46,38 +46,38 @@ export default class { return found } - async AddApplicationUser(appId: string, userIdentifier: string, balance: number) { + async AddApplicationUser(application: Application, userIdentifier: string, balance: number) { return this.DB.transaction(async tx => { const user = await this.userStorage.AddUser(balance, tx) const repo = tx.getRepository(ApplicationUser) const appUser = repo.create({ user: user, - application: await this.GetApplication(appId), + application, identifier: userIdentifier, }) return repo.save(appUser) }) } - GetApplicationUserIfExists(appId: string, userIdentifier: string, entityManager = this.DB): Promise { - return entityManager.getRepository(ApplicationUser).findOne({ where: { identifier: userIdentifier, application: { app_id: appId } } }) + GetApplicationUserIfExists(application: Application, userIdentifier: string, entityManager = this.DB): Promise { + return entityManager.getRepository(ApplicationUser).findOne({ where: { identifier: userIdentifier, application: application } }) } - async GetOrCreateApplicationUser(appId: string, userIdentifier: string, balance: number, entityManager = this.DB): Promise { - const found = await this.GetApplicationUserIfExists(appId, userIdentifier, entityManager) - if (found) { - return found + async GetOrCreateApplicationUser(application: Application, userIdentifier: string, balance: number, entityManager = this.DB): Promise<{ user: ApplicationUser, created: boolean }> { + const user = await this.GetApplicationUserIfExists(application, userIdentifier, entityManager) + if (user) { + return { user, created: false } } - return this.AddApplicationUser(appId, userIdentifier, balance) + return { user: await this.AddApplicationUser(application, userIdentifier, balance), created: true } } - async GetApplicationUser(appId: string, userIdentifier: string, entityManager = this.DB): Promise { - const found = await this.GetApplicationUserIfExists(appId, userIdentifier, entityManager) + async GetApplicationUser(application: Application, userIdentifier: string, entityManager = this.DB): Promise { + const found = await this.GetApplicationUserIfExists(application, userIdentifier, entityManager) if (!found) { throw new Error(`application user not found`) } - if (found.application.app_id !== appId) { + if (found.application.app_id !== application.app_id) { throw new Error("requested user does not belong to requestor application") } return found diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index eac8e49a..544331ae 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -69,17 +69,14 @@ export default class { doTransaction(exec: TX) { if (this.pendingTx) { - throw new Error("cannot start transaction") + throw new Error("cannot start DB transaction") } this.pendingTx = true - console.log("starting tx") return this.DB.transaction(async tx => { try { await exec(tx) - console.log("tx done") this.ExecNextInQueue() } catch (err) { - console.log("tx err") this.ExecNextInQueue() throw err } diff --git a/src/services/storage/userStorage.ts b/src/services/storage/userStorage.ts index a011df82..006843ff 100644 --- a/src/services/storage/userStorage.ts +++ b/src/services/storage/userStorage.ts @@ -3,6 +3,7 @@ import { DataSource, EntityManager } from "typeorm" import { User } from './entity/User.js'; import { UserBasicAuth } from './entity/UserBasicAuth.js'; import { UserNostrAuth } from './entity/UserNostrAuth.js'; +import { getLogger } from '../helpers/logger.js'; export default class { DB: DataSource | EntityManager constructor(DB: DataSource | EntityManager) { @@ -12,7 +13,7 @@ export default class { if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') { throw new Error("balance migration is not allowed") } - console.log("Adding user with balance", balance) + getLogger({})("Adding user with balance", balance) const newUser = entityManager.getRepository(User).create({ user_id: crypto.randomBytes(32).toString('hex'), balance_sats: balance @@ -75,7 +76,6 @@ export default class { } } async UnlockUser(userId: string, entityManager = this.DB) { - console.log("unlocking", userId) const res = await entityManager.getRepository(User).update({ user_id: userId }, { locked: false }) @@ -84,12 +84,14 @@ export default class { } } async IncrementUserBalance(userId: string, increment: number, entityManager = this.DB) { + const user = await this.GetUser(userId, entityManager) const res = await entityManager.getRepository(User).increment({ user_id: userId, }, "balance_sats", increment) if (!res.affected) { throw new Error("unaffected balance increment for " + userId) // TODO: fix logs doxing } + getLogger({ userId: userId })("incremented balance from", user.balance_sats, "sats, by", increment, "sats") } async DecrementUserBalance(userId: string, decrement: number, entityManager = this.DB) { const user = await this.GetUser(userId, entityManager) @@ -102,6 +104,7 @@ export default class { if (!res.affected) { throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing } + getLogger({ userId: userId })("decremented balance from", user.balance_sats, "sats, by", decrement, "sats") } async UpdateUser(userId: string, update: Partial, entityManager = this.DB) {