This commit is contained in:
hatim boufnichel 2024-04-05 19:42:54 +02:00
parent 229cfa7a95
commit 0b08fde708
14 changed files with 95 additions and 83 deletions

View file

@ -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",

View file

@ -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
}*/
}

View file

@ -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<number> {
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) {

View file

@ -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<string, boolean> = {}
decrementSources: Record<string, boolean> = {}
decrementEvents: Record<string, { userId: string, refund: number, failure: boolean }> = {}
log = getLogger({ appName: "SanityChecker" })
users: Record<string, { ts: number, updatedBalance: number }> = {}
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
}
}

View file

@ -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<string, AppInfo> = {}
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)

View file

@ -41,7 +41,7 @@ export default class {
return { executedMigrations, executedMetricsMigrations };
}
StartTransaction(exec: TX<void>, description?: string) {
StartTransaction<T>(exec: TX<T>, description?: string) {
return this.txQueue.PushToQueue({ exec, dbTx: true, description })
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 => {

View file

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