diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 3b8a8ec2..2a102a10 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -53,7 +53,7 @@ export default class { this.storage = new Storage(settings.storageSettings) this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb) - this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings) + this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb) this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 01a358ed..55efca02 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -1,4 +1,5 @@ import { bech32 } from 'bech32' +import crypto from 'crypto' import Storage from '../storage/index.js' import * as Types from '../../../proto/autogenerated/ts/types.js' import { MainSettings } from './settings.js' @@ -6,6 +7,10 @@ import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorag import { LightningHandler } from '../lnd/index.js' import { Application } from '../storage/entity/Application.js' import { getLogger } from '../helpers/logger.js' +import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js' +import { AddressPaidCb, InvoicePaidCb, PaidInvoice } from '../lnd/settings.js' +import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' +import { SendCoinsResponse } from '../../../proto/lnd/lightning.js' interface UserOperationInfo { serial_id: number paid_amount: number @@ -22,10 +27,14 @@ export default class { storage: Storage settings: MainSettings lnd: LightningHandler - constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings) { + addressPaidCb: AddressPaidCb + invoicePaidCb: InvoicePaidCb + constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { this.storage = storage this.settings = settings this.lnd = lnd + this.addressPaidCb = addressPaidCb + this.invoicePaidCb = invoicePaidCb } getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { @@ -116,7 +125,13 @@ export default class { amount: Number(decoded.numSatoshis) } } - async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication?: Application): Promise { + + async PayInvoiceInternal(app: Application, userId: string, invoice: UserReceivingInvoice, amount: number, action: Types.UserOperationType): Promise { + + this.invoicePaidCb(invoice.invoice, amount) + return { txId: "" } + } + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) if (decoded.numSatoshis !== 0 && req.amount !== 0) { throw new Error("invoice has value, do not provide amount the the request") @@ -125,55 +140,70 @@ export default class { throw new Error("invoice has no value, an amount must be provided in the request") } const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) - if (!linkedApplication) { - throw new Error("only application operations are supported") // TODO - make this check obsolete - } const isAppUserPayment = userId !== linkedApplication.owner.user_id const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) const totalAmountToDecrement = payAmount + serviceFee - - const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - await this.lockUserWithMinBalance(userId, totalAmountToDecrement + routingFeeLimit) - let payment - try { - payment = await this.lnd.PayInvoice(req.invoice, req.amount, routingFeeLimit) - await this.storage.userStorage.UnlockUser(userId) - } catch (err) { - await this.storage.userStorage.UnlockUser(userId) - throw err + const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) + let payment: PaidInvoice | null = null + if (!internalInvoice) { + const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) + await this.lockUserWithMinBalance(userId, totalAmountToDecrement + routingFeeLimit) + try { + payment = await this.lnd.PayInvoice(req.invoice, req.amount, routingFeeLimit) + await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + payment.feeSat) + await this.storage.userStorage.UnlockUser(userId) + } catch (err) { + await this.storage.userStorage.UnlockUser(userId) + throw err + } + } else { + await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement) + this.invoicePaidCb(req.invoice, payAmount) } - await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + Number(payment.feeSat)) if (isAppUserPayment && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee) } - await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, Number(payment.feeSat), serviceFee) + const routingFees = payment ? payment.feeSat : 0 + await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, routingFees, serviceFee) return { - preimage: payment.paymentPreimage, - amount_paid: Number(payment.valueSat) + preimage: payment ? payment.paymentPreimage : "", + amount_paid: payment ? Number(payment.valueSat) : payAmount } } - async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise { - const userId = ctx.user_id - const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const estimate = await this.lnd.EstimateChainFees(req.address, req.amoutSats, 1) - const vBytes = Math.ceil(Number(estimate.feeSat / estimate.satPerVbyte)) - const chainFees = vBytes * req.satsPerVByte - const total = req.amoutSats + chainFees - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, req.amoutSats, false) - await this.lockUserWithMinBalance(userId, total + serviceFee) - let payment - try { - payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte) - await this.storage.userStorage.UnlockUser(userId) - } catch (err) { - await this.storage.userStorage.UnlockUser(userId) - throw err + + async PayAddress(userId: string, req: Types.PayAddressRequest, linkedApplication: Application): Promise { + const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) + const isAppUserPayment = userId !== linkedApplication.owner.user_id + const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address) + let txId = "" + let chainFees = 0 + if (!internalAddress) { + const estimate = await this.lnd.EstimateChainFees(req.address, req.amoutSats, 1) + const vBytes = Math.ceil(Number(estimate.feeSat / estimate.satPerVbyte)) + chainFees = vBytes * req.satsPerVByte + const total = req.amoutSats + chainFees + await this.lockUserWithMinBalance(userId, total + serviceFee) + try { + const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte) + txId = payment.txid + await this.storage.userStorage.DecrementUserBalance(userId, total + serviceFee) + await this.storage.userStorage.UnlockUser(userId) + } catch (err) { + await this.storage.userStorage.UnlockUser(userId) + throw err + } + } else { + await this.storage.userStorage.DecrementUserBalance(userId, req.amoutSats + serviceFee) + this.addressPaidCb({ hash: crypto.randomBytes(32).toString("hex"), index: 0 }, req.address, req.amoutSats) } - await this.storage.userStorage.DecrementUserBalance(userId, total + serviceFee) - await this.storage.paymentStorage.AddUserTransactionPayment(userId, req.address, payment.txid, 0, req.amoutSats, chainFees, serviceFee) + + if (isAppUserPayment && serviceFee > 0) { + await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee) + } + await this.storage.paymentStorage.AddUserTransactionPayment(userId, req.address, txId, 0, req.amoutSats, chainFees, serviceFee) return { - txId: payment.txid + txId: txId } } @@ -211,8 +241,11 @@ export default class { async HandleLnurlWithdraw(k1: string, invoice: string): Promise { const key = await this.storage.paymentStorage.UseUserEphemeralKey(k1, 'withdraw') + if (!key.linkedApplication) { + throw new Error("found lnurl key entry with no linked application") + } try { - await this.PayInvoice(key.user.user_id, { invoice: invoice, amount: 0 }) + await this.PayInvoice(key.user.user_id, { invoice: invoice, amount: 0 }, key.linkedApplication) } catch (err: any) { console.error("error sending payment for lnurl withdraw to ", key.user.user_id, err) throw new Error("failed to pay invoice") @@ -330,18 +363,13 @@ export default class { } } - async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number, linkedApplication?: Application) { - if (!linkedApplication) { - throw new Error("only application operations are supported") // TODO - make this check obsolete - } + async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number, linkedApplication: Application): Promise { + let sentAmount = 0 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.balance_sats < amount) { - throw new Error("not enough balance to send user to user payment") - } - if (!linkedApplication) { - throw new Error("only application operations are supported") // TODO - make this check obsolete + throw new Error("not enough balance to send payment") } const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) @@ -352,7 +380,9 @@ export default class { if (isAppUserPayment && fee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee) } + sentAmount = toIncrement }) + return sentAmount } encodeLnurl(base: string) {