diff --git a/env.example b/env.example index 9d09f9ce..6e2a814a 100644 --- a/env.example +++ b/env.example @@ -3,14 +3,22 @@ LND_CERT_PATH=C:\Users\user\.polar\networks\2\volumes\lnd\alice\tls.cert LND_MACAROON_PATH=C:\Users\user\.polar\networks\2\volumes\lnd\alice\data\chain\bitcoin\regtest\admin.macaroon DATABASE_FILE=db.sqlite JWT_SECRET=bigsecrethere -LIMIT_FEE_RATE_MILLISATS=6 -LIMIT_FEE_FIXED_SATS=100 -SERVICE_FEE_INCOMING_TX_PERCENT=0 -SERVICE_FEE_OUTGOING_TX_PERCENT=0 -SERVICE_FEE_INCOMING_INVOICE_PERCENT=0 -SERVICE_FEE_OUTGOING_INVOICE_PERCENT=0 +OUTBOUND_MAX_FEE_BPS=60 +OUTBOUND_MAX_FEE_EXTRA_SATS=100 +INCOMING_CHAIN_FEE_ROOT_BPS=0 +OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets +INCOMING_INVOICE_FEE_ROOT_BPS=0 +OUTGOING_INVOICE_FEE_ROOT_BPS=60 #this is applied only to withdrawals from application wallets +INCOMING_INVOICE_FEE_USER_BPS=0 #defined by app this is just default +OUTGOING_INVOICE_FEE_USER_BPS=60 #defined by app this is just default +TX_FEE_INTERNAL_ROOT_BPS=60 #this is applied only to withdrawls from application wallets +TX_FEE_INTERNAL_USER_BPS=60 #defined by app this is just default NOSTR_PUBLIC_KEY= NOSTR_PRIVATE_KEY= NOSTR_RELAYS=wss://nostr-relay.wlvs.space NOSTR_ALLOWED_PUBS= SERVICE_URL=http://localhost:8080 +ADMIN_TOKEN= +ALLOW_BALANCE_MIGRATION=false +PORT=8080 +MOCK_LND=false \ No newline at end of file diff --git a/src/services/lnd/index.ts b/src/services/lnd/index.ts index 63440ecd..ca9a8cbb 100644 --- a/src/services/lnd/index.ts +++ b/src/services/lnd/index.ts @@ -8,8 +8,8 @@ export const LoadLndSettingsFromEnv = (test = false): LndSettings => { const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS") const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH") const lndMacaroonPath = EnvMustBeNonEmptyString("LND_MACAROON_PATH") - const feeRateLimit = EnvMustBeInteger("LIMIT_FEE_RATE_MILLISATS") / 1000 - const feeFixedLimit = EnvMustBeInteger("LIMIT_FEE_FIXED_SATS") + const feeRateLimit = EnvMustBeInteger("OUTBOUND_MAX_FEE_BPS") / 10000 + const feeFixedLimit = EnvMustBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS") const mockLnd = EnvCanBeBoolean("MOCK_LND") return { lndAddr, lndCertPath, lndMacaroonPath, feeRateLimit, feeFixedLimit, mockLnd } } diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index e0395222..dbaa3bc1 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -79,7 +79,7 @@ export default class { userId: u.user.user_id, balance: u.user.balance_sats }, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) + max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) } } @@ -91,9 +91,10 @@ export default class { } 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 opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user } + 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) return { invoice: appUserInvoice.invoice @@ -102,7 +103,7 @@ export default class { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise { const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) - const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) + 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: { @@ -127,7 +128,7 @@ export default class { 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) - await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, app.owner.user_id, req.amount) + await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, app.owner.user_id, req.amount, app) } async GetAppUserLNURLInfo(appId: string, req: Types.GetAppUserLNURLInfoRequest): Promise { const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) diff --git a/src/services/main/index.ts b/src/services/main/index.ts index cc9818ba..10958c7e 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -14,11 +14,14 @@ export const LoadMainSettingsFromEnv = (test = false): MainSettings => { lndSettings: LoadLndSettingsFromEnv(test), storageSettings: LoadStorageSettingsFromEnv(test), jwtSecret: EnvMustBeNonEmptyString("JWT_SECRET"), - incomingTxFee: EnvMustBeInteger("SERVICE_FEE_INCOMING_TX_PERCENT") / 100, - outgoingTxFee: EnvMustBeInteger("SERVICE_FEE_OUTGOING_TX_PERCENT") / 100, - incomingInvoiceFee: EnvMustBeInteger("SERVICE_FEE_INCOMING_INVOICE_PERCENT") / 100, - outgoingInvoiceFee: EnvMustBeInteger("SERVICE_FEE_OUTGOING_INVOICE_PERCENT") / 100, - userToUserFee: EnvMustBeInteger("SERVICE_FEE_USER_TO_USER_PERCENT") / 100, + incomingTxFee: EnvMustBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS") / 10000, + outgoingTxFee: EnvMustBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS") / 10000, + incomingAppInvoiceFee: EnvMustBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS") / 10000, + outgoingAppInvoiceFee: EnvMustBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS") / 10000, + incomingAppUserInvoiceFee: EnvMustBeInteger("INCOMING_INVOICE_FEE_USER_BPS") / 10000, + outgoingAppUserInvoiceFee: EnvMustBeInteger("OUTGOING_INVOICE_FEE_USER_BPS") / 10000, + userToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_USER_BPS") / 10000, + appToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_ROOT_BPS") / 10000, serviceUrl: EnvMustBeNonEmptyString("SERVICE_URL"), servicePort: EnvMustBeInteger("PORT") } @@ -57,7 +60,7 @@ export default class { this.storage.StartTransaction(async tx => { const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) if (!userAddress) { return } - const fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount) + const fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, false) try { // This call will fail if the transaction is already registered const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, tx) @@ -72,12 +75,21 @@ 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 fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount) + if (!userInvoice.linkedApplication) { + console.error("an invoice was paid, that has no linked application") + return + } + const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id + let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) + if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) { + fee = 0 + } try { // This call will fail if the invoice is already registered await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, tx) + await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, tx) - if (userInvoice.linkedApplication) { + if (isAppUserPayment && fee > 0) { await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, tx) } await this.triggerPaidCallback(userInvoice.callbackUrl) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index ff86c278..1e8de448 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -22,18 +22,27 @@ export default class { this.settings = settings this.lnd = lnd } - getServiceFee(action: Types.UserOperationType, amount: number): number { + getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { switch (action) { case Types.UserOperationType.INCOMING_TX: return Math.ceil(this.settings.incomingTxFee * amount) case Types.UserOperationType.OUTGOING_TX: return Math.ceil(this.settings.outgoingTxFee * amount) case Types.UserOperationType.INCOMING_INVOICE: - return Math.ceil(this.settings.incomingInvoiceFee * amount) + if (appUser) { + return Math.ceil(this.settings.incomingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.incomingAppInvoiceFee * amount) case Types.UserOperationType.OUTGOING_INVOICE: - return Math.ceil(this.settings.outgoingInvoiceFee * amount) + if (appUser) { + return Math.ceil(this.settings.outgoingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.outgoingAppInvoiceFee * amount) case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: - return Math.ceil(this.settings.userToUserFee * amount) + if (appUser) { + return Math.ceil(this.settings.userToUserFee * amount) + } + return Math.ceil(this.settings.appToUserFee * amount) default: throw new Error("Unknown service action type") } @@ -86,8 +95,13 @@ export default class { }) } - GetMaxPayableInvoice(balance: number): number { - const maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.outgoingInvoiceFee))) + GetMaxPayableInvoice(balance: number, appUser: boolean): number { + let maxWithinServiceFee = 0 + if (appUser) { + maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.outgoingAppUserInvoiceFee))) + } else { + maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.outgoingAppInvoiceFee))) + } return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { @@ -105,17 +119,19 @@ 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) - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount) + 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) const payment = await this.lnd.PayInvoice(req.invoice, req.amount, routingFeeLimit) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + Number(payment.feeSat)) - if (linkedApplication) { + if (isAppUserPayment && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee) - } else { - //const appOwner = await this.storage.applicationStorage.IsApplicationOwner(userId) } await this.storage.userStorage.UnlockUser(userId) await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, Number(payment.feeSat), serviceFee) @@ -125,12 +141,18 @@ export default class { } } - async PayAddress(userId: string, req: Types.PayAddressRequest): Promise { + async PayAddress(userId: string, req: Types.PayAddressRequest, linkedApplication?: Application): Promise { const estimate = await this.lnd.EstimateChainFees(req.address, req.amoutSats, req.targetConf) const satPerVByte = Number(estimate.satPerVbyte) const chainFees = Number(estimate.feeSat) const total = req.amoutSats + chainFees - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, req.amoutSats) + if (!linkedApplication) { + throw new Error("only application operations are supported") // TODO - make this check obsolete + } + if (userId !== linkedApplication.owner.user_id) { + throw new Error("chain operations only supported for applications") + } + const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, req.amoutSats, false) await this.lockUserWithMinBalance(userId, total + serviceFee) const payment = await this.lnd.PayAddress(req.address, req.amoutSats, satPerVByte) await this.storage.userStorage.DecrementUserBalance(userId, total + serviceFee) @@ -154,7 +176,8 @@ export default class { } async GetLnurlWithdrawInfo(balanceCheckK1: string): Promise { - const key = await this.storage.paymentStorage.UseUserEphemeralKey(balanceCheckK1, 'balanceCheck') + throw new Error("LNURL withdraw currenlty not supported for non application users") + /*const key = await this.storage.paymentStorage.UseUserEphemeralKey(balanceCheckK1, 'balanceCheck') const maxWithdrawable = this.GetMaxPayableInvoice(key.user.balance_sats) const callbackK1 = await this.storage.paymentStorage.AddUserEphemeralKey(key.user.user_id, 'withdraw') const newBalanceCheckK1 = await this.storage.paymentStorage.AddUserEphemeralKey(key.user.user_id, 'balanceCheck') @@ -165,10 +188,10 @@ export default class { defaultDescription: "lnurl withdraw from lightning.pub", k1: callbackK1.key, maxWithdrawable: maxWithdrawable * 1000, - minWithdrawable: 0, + minWithdrawable: 10000, balanceCheck: this.balanceCheckUrl(newBalanceCheckK1.key), payLink: `${this.settings.serviceUrl}/api/guest/lnurl_pay/info?k1=${payInfoK1.key}`, - } + }*/ } async HandleLnurlWithdraw(k1: string, invoice: string): Promise { @@ -188,7 +211,7 @@ export default class { tag: 'payRequest', callback: `${url}?k1=${payK1.key}`, maxSendable: 10000000000, - minSendable: 0, + minSendable: 10000, metadata: defaultLnurlPayMetadata } } @@ -200,7 +223,7 @@ export default class { tag: 'payRequest', callback: `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payK1.key}`, maxSendable: 10000000, - minSendable: 0, + minSendable: 10000, metadata: defaultLnurlPayMetadata } } @@ -263,18 +286,25 @@ 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 + } 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") } - const fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount) + if (!linkedApplication) { + throw new Error("only application operations are supported") // TODO - make this check obsolete + } + 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 await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, tx) await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, tx) await this.storage.paymentStorage.AddUserToUserPayment(fromUserId, toUserId, amount, fee) - if (linkedApplication) { + if (isAppUserPayment && fee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee) } }) diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 1c3354c3..1be670f2 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -6,9 +6,12 @@ export type MainSettings = { jwtSecret: string incomingTxFee: number outgoingTxFee: number - incomingInvoiceFee: number - outgoingInvoiceFee: number + incomingAppInvoiceFee: number + incomingAppUserInvoiceFee: number + outgoingAppInvoiceFee: number + outgoingAppUserInvoiceFee: number userToUserFee: number + appToUserFee: number serviceUrl: string servicePort: number