diff --git a/package.json b/package.json index b0d56c9b..c61d12d4 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,14 @@ "description": "", "main": "index.js", "scripts": { - "test": " tsc && node build/src/tests/testRunner.js", - "start": "tsc && node build/src/index.js", + "clean": "rimraf build", + "test": "npm run clean && tsc && node build/src/tests/testRunner.js", + "start": "npm run clean && tsc && node build/src/index.js", "start:ci": "git reset --hard && git pull && npm run start", "build_autogenerated": "cd proto && rimraf autogenerated && protoc -I ./service --pub_out=. service/*", "build_lnd_client_1": "cd proto && protoc -I ./others --plugin=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_out=./lnd --ts_proto_opt=esModuleInterop=true others/* ", "build_lnd_client": "cd proto && rimraf lnd/* && npx protoc --ts_out ./lnd --ts_opt long_type_string --proto_path others others/* ", - "typeorm": "typeorm-ts-node-commonjs", - "aa": "tsc && cd build && node src/services/nostr/index.js" + "typeorm": "typeorm-ts-node-commonjs" }, "repository": { "type": "git", diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 64614ac1..c6462fcb 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -30,46 +30,4 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett }, event.startAtNano, event.startAtMs) }) return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) } -} - -/* -export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings): Nostr => { - // TODO: - move to codegen - const nostr = new Nostr(nostrSettings, - async (event) => { - if (!nostrSettings.allowedPubs.includes(event.pub)) { - console.log("nostr pub not allowed") - return - } - let nostrUser = await mainHandler.storage.FindNostrUser(event.pub) - if (!nostrUser) { - nostrUser = await mainHandler.storage.AddNostrUser(event.pub) - } - let j: EventRequest - try { - j = JSON.parse(event.content) - } catch { - console.error("invalid json event received", event.content) - return - } - if (handledRequests.includes(j.requestId)) { - console.log("request already handled") - return - } - handledRequests.push(j.requestId) - switch (j.method) { - case '/api/user/chain/new': - const error = Types.NewAddressRequestValidate(j.body) - if (error !== null) { - console.error("invalid request from", event.pub, j)// TODO: dont dox - return // TODO: respond - } - if (!serverMethods.NewAddress) { - throw new Error("unimplemented NewInvoice") - } - const res = await serverMethods.NewAddress({ user_id: nostrUser.user.user_id }, j.body) - nostr.Send(event.pub, JSON.stringify({ ...res, requestId: j.requestId })) - } - }) - return nostr -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 9bcf2de1..94a47461 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -189,7 +189,6 @@ export default class { this.log("paying external invoice", invoice) const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice) - console.log("decremented") const pendingPayment = await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, payAmount, linkedApplication) try { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit) @@ -523,9 +522,8 @@ export default class { } } - async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number, linkedApplication: Application): Promise { - let sentAmount = 0 - await this.storage.StartTransaction(async tx => { + async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number, linkedApplication: Application): Promise<{ amount: number, fees: number }> { + const payment = await this.storage.StartTransaction(async tx => { const fromUser = await this.storage.userStorage.GetUser(fromUserId, tx) const toUser = await this.storage.userStorage.GetUser(toUserId, tx) if (fromUser.locked || toUser.locked) { @@ -536,21 +534,21 @@ export default class { } const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) - const toIncrement = amount - fee - const paymentEntry = await this.storage.paymentStorage.CreateUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) - await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, `${toUserId}:${paymentEntry.serial_id}`, tx) - await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, `${fromUserId}:${paymentEntry.serial_id}`, tx) - await this.storage.paymentStorage.SaveUserToUserPayment(paymentEntry, tx) + const toDecrement = amount + fee + const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) + await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx) + await this.storage.userStorage.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${paymentEntry.serial_id}`, tx) + await this.storage.paymentStorage.SetPendingUserToUserPaymentAsPaid(paymentEntry.serial_id, tx) if (isAppUserPayment && fee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee, 'fees', tx) } - sentAmount = toIncrement + return paymentEntry }) const fromUser = await this.storage.userStorage.GetUser(fromUserId) const toUser = await this.storage.userStorage.GetUser(toUserId) - this.storage.eventsLog.LogEvent({ type: 'u2u_sender', userId: fromUserId, appId: linkedApplication.app_id, appUserId: "", balance: fromUser.balance_sats, data: toUserId, amount: amount }) + this.storage.eventsLog.LogEvent({ type: 'u2u_sender', userId: fromUserId, appId: linkedApplication.app_id, appUserId: "", balance: fromUser.balance_sats, data: toUserId, amount: payment.paid_amount + payment.service_fees }) this.storage.eventsLog.LogEvent({ type: 'u2u_receiver', userId: toUserId, appId: linkedApplication.app_id, appUserId: "", balance: toUser.balance_sats, data: fromUserId, amount: amount }) - return sentAmount + return { amount: payment.paid_amount, fees: payment.service_fees } } async CheckNewlyConfirmedTxs(height: number) { diff --git a/src/services/main/sanityChecker.ts b/src/services/main/sanityChecker.ts index 8b94c80c..c6951673 100644 --- a/src/services/main/sanityChecker.ts +++ b/src/services/main/sanityChecker.ts @@ -2,6 +2,7 @@ import Storage from '../storage/index.js' import { LightningHandler } from "../lnd/index.js" import { LoggedEvent } from '../storage/eventsLog.js' import { Invoice, Payment } from '../../../proto/lnd/lightning'; +import { getLogger } from '../helpers/logger.js'; const LN_INVOICE_REGEX = /^(lightning:)?(lnbc|lntb)[0-9a-zA-Z]+$/; const BITCOIN_ADDRESS_REGEX = /^(bitcoin:)?([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-zA-HJ-NP-Z0-9]{39,59})$/; type UniqueDecrementReasons = 'ban' @@ -19,6 +20,7 @@ export default class SanityChecker { incrementSources: Record = {} decrementSources: Record = {} decrementEvents: Record = {} + log = getLogger({ appName: "SanityChecker" }) users: Record = {} constructor(storage: Storage, lnd: LightningHandler) { this.storage = storage @@ -201,7 +203,6 @@ export default class SanityChecker { throw new Error("payment failled, should not refund routing fees " + invoice) } if (entry.refund !== amt) { - console.log(entry.refund, amt) throw new Error("refund amount mismatch for routing fee refund " + invoice) } } @@ -251,9 +252,8 @@ export default class SanityChecker { checkUserEntry(e: LoggedEvent, u: { ts: number, updatedBalance: number } | undefined) { const newEntry = { ts: e.timestampMs, updatedBalance: e.balance + e.amount * (e.type === 'balance_decrement' ? -1 : 1) } - console.log(e) if (!u) { - console.log(e.userId, "balance starts at", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") + this.log(e.userId, "balance starts at", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") return newEntry } if (e.timestampMs < u.ts) { @@ -262,7 +262,7 @@ export default class SanityChecker { if (e.balance !== u.updatedBalance) { throw new Error("inconsistent balance update got: " + e.balance + " expected " + u.updatedBalance) } - console.log(e.userId, "balance updates from", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") + this.log(e.userId, "balance updates from", e.balance, "sats and moves by", e.amount * (e.type === 'balance_decrement' ? -1 : 1), "sats, resulting in", newEntry.updatedBalance, "sats") return newEntry } } \ No newline at end of file diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index 6bb1fce2..772738f7 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -56,13 +56,13 @@ process.on("message", (message: ChildProcessRequest) => { sendToNostr(message.appId, message.data, message.relays) break default: - console.error("unknown nostr request", message) + getLogger({ appName: "nostrMiddleware" })("ERROR", "unknown nostr request", message) break } }) const initSubprocessHandler = (settings: NostrSettings) => { if (subProcessHandler) { - console.error("nostr settings ignored since handler already exists") + getLogger({ appName: "nostrMiddleware" })("ERROR", "nostr settings ignored since handler already exists") return } subProcessHandler = new Handler(settings, event => { @@ -74,7 +74,7 @@ const initSubprocessHandler = (settings: NostrSettings) => { } const sendToNostr: NostrSend = (appId, data, relays) => { if (!subProcessHandler) { - console.error("nostr was not initialized") + getLogger({ appName: "nostrMiddleware" })("ERROR", "nostr was not initialized") return } subProcessHandler.Send(appId, data, relays) @@ -87,9 +87,10 @@ export default class Handler { subs: Sub[] = [] apps: Record = {} eventCallback: (event: NostrEvent) => void + log = getLogger({ appName: "nostrMiddleware" }) constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) { this.settings = settings - console.log( + this.log( { ...settings, apps: settings.apps.map(app => { @@ -151,7 +152,7 @@ export default class Handler { } const eventId = e.id if (handledEvents.includes(eventId)) { - console.log("event already handled") + this.log("event already handled") return } handledEvents.push(eventId) diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 8038102b..1510dc9e 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -41,7 +41,7 @@ export default class { return { executedMigrations, executedMetricsMigrations }; } - StartTransaction(exec: TX, description?: string) { + StartTransaction(exec: TX, description?: string) { return this.txQueue.PushToQueue({ exec, dbTx: true, description }) } } \ No newline at end of file diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 7a0c249e..f2ce7992 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -283,18 +283,19 @@ export default class { return found } - async CreateUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, dbTx: DataSource | EntityManager) { - return dbTx.getRepository(UserToUserPayment).create({ + async AddPendingUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, dbTx: DataSource | EntityManager) { + const entry = dbTx.getRepository(UserToUserPayment).create({ from_user: await this.userStorage.GetUser(fromUserId, dbTx), to_user: await this.userStorage.GetUser(toUserId, dbTx), - paid_at_unix: Math.floor(Date.now() / 1000), + paid_at_unix: 0, paid_amount: amount, service_fees: fee, linkedApplication }) + return dbTx.getRepository(UserToUserPayment).save(entry) } - async SaveUserToUserPayment(payment: UserToUserPayment, dbTx: DataSource | EntityManager) { - return dbTx.getRepository(UserToUserPayment).save(payment) + async SetPendingUserToUserPaymentAsPaid(serialId: number, dbTx: DataSource | EntityManager) { + dbTx.getRepository(UserToUserPayment).update(serialId, { paid_at_unix: Math.floor(Date.now() / 1000) }) } GetUserToUserReceivedPayments(userId: string, fromIndex: number, take = 50, entityManager = this.DB) { diff --git a/src/tests/externalPayment.spec.ts b/src/tests/externalPayment.spec.ts index 28253d0d..551cdd57 100644 --- a/src/tests/externalPayment.spec.ts +++ b/src/tests/externalPayment.spec.ts @@ -1,15 +1,17 @@ import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { Describe, expect, expectThrowsAsync, runSanityCheck, safelySetUserBalance, SetupTest, TestBase } from './testBase.js' export const ignore = false - +export const dev = false export default async (T: TestBase) => { await safelySetUserBalance(T, T.user1, 2000) await testSuccessfulExternalPayment(T) + await testFailedExternalPayment(T) await runSanityCheck(T) } const testSuccessfulExternalPayment = async (T: TestBase) => { + T.d("starting testSuccessfulExternalPayment") const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry) expect(invoice.payRequest).to.startWith("lnbcrt5u") @@ -27,3 +29,19 @@ const testSuccessfulExternalPayment = async (T: TestBase) => { } +const testFailedExternalPayment = async (T: TestBase) => { + T.d("starting testFailedExternalPayment") + const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) + const invoice = await T.externalAccessToOtherLnd.NewInvoice(1500, "test", defaultInvoiceExpiry) + expect(invoice.payRequest).to.startWith("lnbcrt15u") + T.d("generated 1500 sats invoice for external node") + + await expectThrowsAsync(T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application), "not enough balance to decrement") + T.d("payment failed as expected, with the expected error message") + const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) + expect(u1.balance_sats).to.be.equal(1496) + T.d("user1 balance is still 1496") + const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) + expect(owner.balance_sats).to.be.equal(3) + T.d("app balance is still 3 sats") +} \ No newline at end of file diff --git a/src/tests/internalPayment.spec.ts b/src/tests/internalPayment.spec.ts index 562a502d..8c4bef53 100644 --- a/src/tests/internalPayment.spec.ts +++ b/src/tests/internalPayment.spec.ts @@ -10,6 +10,7 @@ export default async (T: TestBase) => { } const testSuccessfulInternalPayment = async (T: TestBase) => { + T.d("starting testSuccessfulInternalPayment") const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const invoice = await T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 1000, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry }) expect(invoice.invoice).to.startWith("lnbcrt10u") @@ -29,6 +30,7 @@ const testSuccessfulInternalPayment = async (T: TestBase) => { } const testFailedInternalPayment = async (T: TestBase) => { + T.d("starting testFailedInternalPayment") const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const invoice = await T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 1000, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry }) expect(invoice.invoice).to.startWith("lnbcrt10u") diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index 4c44f21a..a7e935c9 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -12,18 +12,15 @@ export default async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => { + T.d("starting testSpamExternalPayment") const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const invoices = await Promise.all(new Array(10).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) T.d("generated 10 500 sats invoices for external node") const res = await Promise.all(invoices.map(async (invoice, i) => { try { - T.d("trying to pay invoice " + i) const result = await T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application) - T.d("payment succeeded " + i) return { success: true, result } } catch (e: any) { - T.d("payment failed " + i) - console.log(e, i) return { success: false, err: e } } })) diff --git a/src/tests/spamMixedPayments.spec.ts b/src/tests/spamMixedPayments.spec.ts index 2bcf4ab9..b271369f 100644 --- a/src/tests/spamMixedPayments.spec.ts +++ b/src/tests/spamMixedPayments.spec.ts @@ -13,6 +13,7 @@ export default async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => { + T.d("starting testSpamExternalPayment") const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const invoicesForExternal = await Promise.all(new Array(5).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) const invoicesForUser2 = await Promise.all(new Array(5).fill(0).map(() => T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 500, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry }))) @@ -20,13 +21,9 @@ const testSpamExternalPayment = async (T: TestBase) => { T.d("generated 10 500 sats mixed invoices between external node and user 2") const res = await Promise.all(invoices.map(async (invoice, i) => { try { - T.d("trying to pay invoice " + i) const result = await T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice, amount: 0 }, application) - T.d("payment succeeded " + i) return { success: true, result } } catch (e: any) { - T.d("payment failed " + i) - console.log(e, i) return { success: false, err: e } } })) diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index e7595e0b..a33f8004 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -70,6 +70,7 @@ export const teardown = async (T: TestBase) => { T.main.lnd.Stop() T.externalAccessToMainLnd.Stop() T.externalAccessToOtherLnd.Stop() + T.externalAccessToThirdLnd.Stop() console.log("teardown") } diff --git a/src/tests/testRunner.ts b/src/tests/testRunner.ts index 576ba351..d68ba31f 100644 --- a/src/tests/testRunner.ts +++ b/src/tests/testRunner.ts @@ -17,6 +17,7 @@ const start = async () => { const module = await import(`./${file.slice("build/src/tests/".length)}`) as TestModule modules.push({ module, file }) if (module.dev) { + console.log("dev module found", file) if (devModule !== -1) { console.error(redConsole, "there are multiple dev modules", resetConsole) return @@ -63,6 +64,9 @@ const runTestFile = async (fileName: string, mod: TestModule) => { d(e, true) await teardown(T) } + if (mod.dev) { + d("dev mod is not allowed to in CI, failing for precaution", true) + } } const getDescribe = (fileName: string): Describe => { diff --git a/src/tests/userToUserPayment.spec.ts b/src/tests/userToUserPayment.spec.ts new file mode 100644 index 00000000..d2decd0c --- /dev/null +++ b/src/tests/userToUserPayment.spec.ts @@ -0,0 +1,35 @@ +import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' +import { Describe, expect, expectThrowsAsync, runSanityCheck, safelySetUserBalance, SetupTest, TestBase } from './testBase.js' +export const ignore = false +export const dev = true +export default async (T: TestBase) => { + await safelySetUserBalance(T, T.user1, 2000) + await testSuccessfulU2UPayment(T) + await testFailedInternalPayment(T) + await runSanityCheck(T) +} + +const testSuccessfulU2UPayment = async (T: TestBase) => { + T.d("starting testSuccessfulU2UPayment") + const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) + const sentAmt = await T.main.paymentManager.SendUserToUserPayment(T.user1.userId, T.user2.userId, 1000, application) + expect(sentAmt.amount).to.be.equal(1000) + T.d("paid 1000 sats u2u from user1 to user2") + const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) + const u2 = await T.main.storage.userStorage.GetUser(T.user2.userId) + const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) + expect(u2.balance_sats).to.be.equal(1000) + T.d("user2 balance is 1000") + expect(u1.balance_sats).to.be.equal(994) + T.d("user1 balance is 994 cuz he paid 6 sats fee") + expect(owner.balance_sats).to.be.equal(6) + T.d("app balance is 6 sats") +} + +const testFailedInternalPayment = async (T: TestBase) => { + T.d("starting testFailedInternalPayment") + const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) + await expectThrowsAsync(T.main.paymentManager.SendUserToUserPayment(T.user1.userId, T.user2.userId, 1000, application), "not enough balance to send payment") + T.d("payment failed as expected, with the expected error message") +} +