Merge pull request #618 from shocknet/feature/restarts

Feature/restarts
This commit is contained in:
CapDog 2023-07-19 16:21:32 -04:00 committed by GitHub
commit d926db41bd
13 changed files with 176 additions and 66 deletions

3
.gitignore vendored
View file

@ -7,4 +7,5 @@ temp/
.env .env
build/ build/
db.sqlite db.sqlite
.key/ .key/
logs

View file

@ -1,11 +1,14 @@
import { ServerOptions } from "../proto/autogenerated/ts/express_server"; import { ServerOptions } from "../proto/autogenerated/ts/express_server";
import { AdminContext } from "../proto/autogenerated/ts/types"; import { AdminContext } from "../proto/autogenerated/ts/types";
import Main from './services/main' import Main from './services/main'
import { getLogger } from './services/helpers/logger.js'
const serverOptions = (mainHandler: Main): ServerOptions => { const serverOptions = (mainHandler: Main): ServerOptions => {
const log = getLogger({})
return { return {
logger: { log, error: err => log("ERROR", err) },
AdminAuthGuard: adminAuth, AdminAuthGuard: adminAuth,
AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(authHeader) } }, AppAuthGuard: async (authHeader) => { return { app_id: mainHandler.applicationManager.DecodeAppToken(stripBearer(authHeader)) } },
UserAuthGuard: async (authHeader) => { return { user_id: mainHandler.userManager.DecodeUserToken(authHeader) } }, UserAuthGuard: async (authHeader) => { return { user_id: mainHandler.userManager.DecodeUserToken(stripBearer(authHeader)) } },
GuestAuthGuard: async (_) => ({}), GuestAuthGuard: async (_) => ({}),
encryptCallback: async (_, b) => b, encryptCallback: async (_, b) => b,
decryptCallback: 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<AdminContext> => { const adminAuth = async (header: string | undefined): Promise<AdminContext> => {
const AdminToken = process.env.ADMIN_TOKEN const AdminToken = process.env.ADMIN_TOKEN
if (!AdminToken) { if (!AdminToken) {

View file

@ -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))
}
}

View file

@ -4,6 +4,7 @@ import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../h
import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NodeInfo, PaidInvoice } from './settings.js' import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NodeInfo, PaidInvoice } from './settings.js'
import LND from './lnd.js' import LND from './lnd.js'
import MockLnd from './mock.js' import MockLnd from './mock.js'
import { getLogger } from '../helpers/logger.js'
export const LoadLndSettingsFromEnv = (test = false): LndSettings => { export const LoadLndSettingsFromEnv = (test = false): LndSettings => {
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS") const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH") const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
@ -32,10 +33,10 @@ export interface LightningHandler {
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => { export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => {
if (settings.mockLnd) { if (settings.mockLnd) {
console.log("registering mock lnd handler") getLogger({})("registering mock lnd handler")
return new MockLnd(settings, addressPaidCb, invoicePaidCb) return new MockLnd(settings, addressPaidCb, invoicePaidCb)
} else { } else {
console.log("registering prod lnd handler") getLogger({})("registering prod lnd handler")
return new LND(settings, addressPaidCb, invoicePaidCb) return new LND(settings, addressPaidCb, invoicePaidCb)
} }
} }

View file

@ -13,8 +13,9 @@ import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js'; import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.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 DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
@ -26,6 +27,7 @@ export default class {
abortController = new AbortController() abortController = new AbortController()
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
log = getLogger({})
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.settings = settings this.settings = settings
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
@ -49,8 +51,6 @@ export default class {
this.lightning = new LightningClient(transport) this.lightning = new LightningClient(transport)
this.invoices = new InvoicesClient(transport) this.invoices = new InvoicesClient(transport)
this.router = new RouterClient(transport) this.router = new RouterClient(transport)
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
} }
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> { SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
throw new Error("SetMockInvoiceAsPaid only available in mock mode") throw new Error("SetMockInvoiceAsPaid only available in mock mode")
@ -58,7 +58,11 @@ export default class {
Stop() { Stop() {
this.abortController.abort() this.abortController.abort()
} }
async Warmup() { this.ready = true } async Warmup() {
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
this.ready = true
}
async GetInfo(): Promise<NodeInfo> { async GetInfo(): Promise<NodeInfo> {
const res = await this.lightning.getInfo({}, DeadLineMetadata()) const res = await this.lightning.getInfo({}, DeadLineMetadata())
@ -71,12 +75,27 @@ export default class {
} }
const info = await this.GetInfo() const info = await this.GetInfo()
if (!info.syncedToChain || !info.syncedToGraph) { 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 { SubscribeAddressPaid(): void {
const stream = this.lightning.subscribeTransactions({ const stream = this.lightning.subscribeTransactions({
account: "", account: "",
@ -84,19 +103,24 @@ export default class {
startHeight: this.latestKnownBlockHeigh, startHeight: this.latestKnownBlockHeigh,
}, { abort: this.abortController.signal }) }, { abort: this.abortController.signal })
stream.responses.onMessage(tx => { stream.responses.onMessage(tx => {
if (tx.blockHeight > this.latestKnownBlockHeigh) { if (tx.blockHeight > this.latestKnownBlockHeigh) {
this.latestKnownBlockHeigh = tx.blockHeight this.latestKnownBlockHeigh = tx.blockHeight
} }
if (tx.numConfirmations > 0) { if (tx.numConfirmations > 0) {
tx.outputDetails.forEach(output => { tx.outputDetails.forEach(output => {
if (output.isOurAddress) { 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)) this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount))
} }
}) })
} }
}) })
stream.responses.onError(error => { 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 }) }, { abort: this.abortController.signal })
stream.responses.onMessage(invoice => { stream.responses.onMessage(invoice => {
if (invoice.state === Invoice_InvoiceState.SETTLED) { if (invoice.state === Invoice_InvoiceState.SETTLED) {
this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats")
this.latestKnownSettleIndex = Number(invoice.settleIndex) this.latestKnownSettleIndex = Number(invoice.settleIndex)
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat)) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat))
} }
}) })
stream.responses.onError(error => { 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<NewAddressResponse> { async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
this.checkReady() await this.Health()
let lndAddressType: AddressType let lndAddressType: AddressType
switch (addressType) { switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH: case Types.AddressType.NESTED_PUBKEY_HASH:
@ -136,7 +166,7 @@ export default class {
} }
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> { async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
this.checkReady() await this.Health()
const encoder = new TextEncoder() const encoder = new TextEncoder()
const ecoded = encoder.encode(memo) const ecoded = encoder.encode(memo)
const hashed = crypto.createHash('sha256').update(ecoded).digest(); const hashed = crypto.createHash('sha256').update(ecoded).digest();
@ -158,7 +188,7 @@ export default class {
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
this.checkReady() await this.Health()
const abortController = new AbortController() const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit) const req = PayInvoiceReq(invoice, amount, feeLimit)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
@ -167,12 +197,13 @@ export default class {
rej(error) rej(error)
}) })
stream.responses.onMessage(payment => { stream.responses.onMessage(payment => {
console.log(payment)
switch (payment.status) { switch (payment.status) {
case Payment_PaymentStatus.FAILED: case Payment_PaymentStatus.FAILED:
this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason]) rej(PaymentFailureReason[payment.failureReason])
return return
case Payment_PaymentStatus.SUCCEEDED: case Payment_PaymentStatus.SUCCEEDED:
this.log("invoice payment succeded", Number(payment.valueSat))
res({ feeSat: Number(payment.feeSat), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage }) 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<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
this.checkReady() await this.Health()
const res = await this.lightning.estimateFee({ const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) }, addrToAmount: { [address]: BigInt(amount) },
minConfs: 1, minConfs: 1,
@ -191,14 +222,15 @@ export default class {
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> { async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
this.checkReady() await this.Health()
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
this.log("sent chain TX for", amount, "sats")
return res.response return res.response
} }
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> { async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
this.checkReady() await this.Health()
const abortController = new AbortController() const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats) const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
const stream = this.lightning.openChannel(req, { abort: abortController.signal }) const stream = this.lightning.openChannel(req, { abort: abortController.signal })

View file

@ -13,6 +13,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js'; import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js';
import { getLogger } from '../helpers/logger.js';
export default class { export default class {
invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }> = {} invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }> = {}
@ -86,10 +87,11 @@ export default class {
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
console.log('payng', invoice) const log = getLogger({})
log('payng', invoice)
await new Promise(res => setTimeout(res, 200)) await new Promise(res => setTimeout(res, 200))
const amt = this.decodeOutboundInvoice(invoice) const amt = this.decodeOutboundInvoice(invoice)
console.log('paid', invoice) log('paid', invoice)
return { feeSat: 1, paymentPreimage: "all_good", valueSat: amt || amount } return { feeSat: 1, paymentPreimage: "all_good", valueSat: amt || amount }
} }

View file

@ -6,6 +6,7 @@ import { MainSettings } from './settings.js'
import PaymentManager from './paymentManager.js' import PaymentManager from './paymentManager.js'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import { getLogger } from '../helpers/logger.js'
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: MainSettings
@ -35,7 +36,8 @@ export default class {
} }
async SetMockAppUserBalance(appId: string, req: Types.SetMockAppUserBalanceRequest) { 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) await this.paymentManager.SetMockUserBalance(user.user.user_id, req.amount)
} }
@ -47,6 +49,8 @@ export default class {
async AddApp(req: Types.AuthAppRequest): Promise<Types.AuthApp> { async AddApp(req: Types.AuthAppRequest): Promise<Types.AuthApp> {
const app = await this.storage.applicationStorage.AddApplication(req.name) const app = await this.storage.applicationStorage.AddApplication(req.name)
getLogger({ appName: app.name })("app created")
return { return {
app: { app: {
id: app.app_id, id: app.app_id,
@ -79,11 +83,16 @@ export default class {
} }
async AddAppUser(appId: string, req: Types.AddAppUserRequest): Promise<Types.AppUser> { async AddAppUser(appId: string, req: Types.AddAppUserRequest): Promise<Types.AppUser> {
const app = await this.storage.applicationStorage.GetApplication(appId)
const log = getLogger({ appName: app.name })
let u: ApplicationUser let u: ApplicationUser
if (req.fail_if_exists) { 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 { } 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 { return {
identifier: u.identifier, identifier: u.identifier,
@ -97,26 +106,29 @@ export default class {
async AddAppInvoice(appId: string, req: Types.AddAppInvoiceRequest): Promise<Types.NewInvoiceResponse> { async AddAppInvoice(appId: string, req: Types.AddAppInvoiceRequest): Promise<Types.NewInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) 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 } 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<Types.NewInvoiceResponse> { async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest): Promise<Types.NewInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const receiver = await this.storage.applicationStorage.GetApplicationUser(appId, req.receiver_identifier) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier)
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 } 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) 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 { return {
invoice: appUserInvoice.invoice invoice: appUserInvoice.invoice
} }
} }
async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> {
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) const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
console.log(max, user.user.balance_sats)
return { return {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats 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<Types.PayAppUserInvoiceResponse> { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayAppUserInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
return this.paymentManager.PayInvoice(appUser.user.user_id, req, app) 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<void> { async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> {
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 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) 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<void> { async SendAppUserToAppPayment(appId: string, req: Types.SendAppUserToAppPaymentRequest): Promise<void> {
const fromUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.from_user_identifier)
const app = await this.storage.applicationStorage.GetApplication(appId) 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) 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<Types.LnurlPayInfoResponse> { async GetAppUserLNURLInfo(appId: string, req: Types.GetAppUserLNURLInfoRequest): Promise<Types.LnurlPayInfoResponse> {
const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier)
const app = await this.storage.applicationStorage.GetApplication(appId) 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) return this.paymentManager.GetLnurlPayInfoFromUser(user.user.user_id, app, req.base_url_override)
} }
} }

View file

@ -9,6 +9,7 @@ import PaymentManager from './paymentManager.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import NewLightningHandler, { LoadLndSettingsFromEnv, LightningHandler } from "../lnd/index.js" import NewLightningHandler, { LoadLndSettingsFromEnv, LightningHandler } from "../lnd/index.js"
import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js" import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js"
import { getLogger, PubLogger } from "../helpers/logger.js"
export const LoadMainSettingsFromEnv = (test = false): MainSettings => { export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
return { return {
lndSettings: LoadLndSettingsFromEnv(test), lndSettings: LoadLndSettingsFromEnv(test),
@ -75,8 +76,9 @@ export default class {
this.storage.StartTransaction(async tx => { this.storage.StartTransaction(async tx => {
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx)
if (!userInvoice || userInvoice.paid_at_unix > 0) { return } if (!userInvoice || userInvoice.paid_at_unix > 0) { return }
const log = getLogger({})
if (!userInvoice.linkedApplication) { 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 return
} }
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id
@ -92,22 +94,23 @@ export default class {
if (isAppUserPayment && fee > 0) { if (isAppUserPayment && fee > 0) {
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, tx) await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, tx)
} }
await this.triggerPaidCallback(userInvoice.callbackUrl)
} catch { await this.triggerPaidCallback(log, userInvoice.callbackUrl)
//TODO log("paid invoice processed successfully")
} catch (err: any) {
log("ERROR", "cannot process paid invoice", err.message || "")
} }
}) })
} }
async triggerPaidCallback(url: string) { async triggerPaidCallback(log: PubLogger, url: string) {
console.log(url)
if (!url) { if (!url) {
return return
} }
try { try {
await fetch(url + "&ok=true") await fetch(url + "&ok=true")
} catch (err: any) { } catch (err: any) {
console.log("error sending cb", err) log("error sending paid callback for invoice", err.message || "")
} }
} }
} }

View file

@ -5,6 +5,7 @@ import { MainSettings } from './settings.js'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { LightningHandler } from '../lnd/index.js' import { LightningHandler } from '../lnd/index.js'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { getLogger } from '../helpers/logger.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -59,7 +60,7 @@ export default class {
if (!this.settings.lndSettings.mockLnd) { if (!this.settings.lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid") 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 }) await this.storage.userStorage.UpdateUser(userId, { balance_sats: balance })
} }
@ -81,7 +82,6 @@ export default class {
} }
async lockUserWithMinBalance(userId: string, minBalance: number) { async lockUserWithMinBalance(userId: string, minBalance: number) {
console.log("locking", userId)
return this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
const user = await this.storage.userStorage.GetUser(userId, tx) const user = await this.storage.userStorage.GetUser(userId, tx)
if (user.locked) { if (user.locked) {

View file

@ -138,7 +138,6 @@ export default (mainHandler: Main): Types.ServerMethods => {
payer_identifier_CustomCheck: id => id !== '', payer_identifier_CustomCheck: id => id !== '',
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
console.log(req)
return mainHandler.applicationManager.AddAppInvoice(ctx.app_id, req) return mainHandler.applicationManager.AddAppInvoice(ctx.app_id, req)
}, },
AddAppUserInvoice: async (ctx, req) => { AddAppUserInvoice: async (ctx, req) => {

View file

@ -46,38 +46,38 @@ export default class {
return found return found
} }
async AddApplicationUser(appId: string, userIdentifier: string, balance: number) { async AddApplicationUser(application: Application, userIdentifier: string, balance: number) {
return this.DB.transaction(async tx => { return this.DB.transaction(async tx => {
const user = await this.userStorage.AddUser(balance, tx) const user = await this.userStorage.AddUser(balance, tx)
const repo = tx.getRepository(ApplicationUser) const repo = tx.getRepository(ApplicationUser)
const appUser = repo.create({ const appUser = repo.create({
user: user, user: user,
application: await this.GetApplication(appId), application,
identifier: userIdentifier, identifier: userIdentifier,
}) })
return repo.save(appUser) return repo.save(appUser)
}) })
} }
GetApplicationUserIfExists(appId: string, userIdentifier: string, entityManager = this.DB): Promise<ApplicationUser | null> { GetApplicationUserIfExists(application: Application, userIdentifier: string, entityManager = this.DB): Promise<ApplicationUser | null> {
return entityManager.getRepository(ApplicationUser).findOne({ where: { identifier: userIdentifier, application: { app_id: appId } } }) return entityManager.getRepository(ApplicationUser).findOne({ where: { identifier: userIdentifier, application: application } })
} }
async GetOrCreateApplicationUser(appId: string, userIdentifier: string, balance: number, entityManager = this.DB): Promise<ApplicationUser> { async GetOrCreateApplicationUser(application: Application, userIdentifier: string, balance: number, entityManager = this.DB): Promise<{ user: ApplicationUser, created: boolean }> {
const found = await this.GetApplicationUserIfExists(appId, userIdentifier, entityManager) const user = await this.GetApplicationUserIfExists(application, userIdentifier, entityManager)
if (found) { if (user) {
return found 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<ApplicationUser> { async GetApplicationUser(application: Application, userIdentifier: string, entityManager = this.DB): Promise<ApplicationUser> {
const found = await this.GetApplicationUserIfExists(appId, userIdentifier, entityManager) const found = await this.GetApplicationUserIfExists(application, userIdentifier, entityManager)
if (!found) { if (!found) {
throw new Error(`application user not 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") throw new Error("requested user does not belong to requestor application")
} }
return found return found

View file

@ -69,17 +69,14 @@ export default class {
doTransaction(exec: TX) { doTransaction(exec: TX) {
if (this.pendingTx) { if (this.pendingTx) {
throw new Error("cannot start transaction") throw new Error("cannot start DB transaction")
} }
this.pendingTx = true this.pendingTx = true
console.log("starting tx")
return this.DB.transaction(async tx => { return this.DB.transaction(async tx => {
try { try {
await exec(tx) await exec(tx)
console.log("tx done")
this.ExecNextInQueue() this.ExecNextInQueue()
} catch (err) { } catch (err) {
console.log("tx err")
this.ExecNextInQueue() this.ExecNextInQueue()
throw err throw err
} }

View file

@ -3,6 +3,7 @@ import { DataSource, EntityManager } from "typeorm"
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { UserBasicAuth } from './entity/UserBasicAuth.js'; import { UserBasicAuth } from './entity/UserBasicAuth.js';
import { UserNostrAuth } from './entity/UserNostrAuth.js'; import { UserNostrAuth } from './entity/UserNostrAuth.js';
import { getLogger } from '../helpers/logger.js';
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
constructor(DB: DataSource | EntityManager) { constructor(DB: DataSource | EntityManager) {
@ -12,7 +13,7 @@ export default class {
if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') { if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') {
throw new Error("balance migration is not allowed") 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({ const newUser = entityManager.getRepository(User).create({
user_id: crypto.randomBytes(32).toString('hex'), user_id: crypto.randomBytes(32).toString('hex'),
balance_sats: balance balance_sats: balance
@ -75,7 +76,6 @@ export default class {
} }
} }
async UnlockUser(userId: string, entityManager = this.DB) { async UnlockUser(userId: string, entityManager = this.DB) {
console.log("unlocking", userId)
const res = await entityManager.getRepository(User).update({ const res = await entityManager.getRepository(User).update({
user_id: userId user_id: userId
}, { locked: false }) }, { locked: false })
@ -84,12 +84,14 @@ export default class {
} }
} }
async IncrementUserBalance(userId: string, increment: number, entityManager = this.DB) { async IncrementUserBalance(userId: string, increment: number, entityManager = this.DB) {
const user = await this.GetUser(userId, entityManager)
const res = await entityManager.getRepository(User).increment({ const res = await entityManager.getRepository(User).increment({
user_id: userId, user_id: userId,
}, "balance_sats", increment) }, "balance_sats", increment)
if (!res.affected) { if (!res.affected) {
throw new Error("unaffected balance increment for " + userId) // TODO: fix logs doxing 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) { async DecrementUserBalance(userId: string, decrement: number, entityManager = this.DB) {
const user = await this.GetUser(userId, entityManager) const user = await this.GetUser(userId, entityManager)
@ -102,6 +104,7 @@ export default class {
if (!res.affected) { if (!res.affected) {
throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing 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<User>, entityManager = this.DB) { async UpdateUser(userId: string, update: Partial<User>, entityManager = this.DB) {