This commit is contained in:
hatim 2023-05-12 19:53:57 +02:00
parent 9555b7be51
commit 80933b1ecf
6 changed files with 95 additions and 41 deletions

View file

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

View file

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

View file

@ -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<Types.NewInvoiceResponse> {
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<Types.AppUser> {
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<void> {
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<Types.LnurlPayInfoResponse> {
const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier)

View file

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

View file

@ -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:
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<Types.DecodeInvoiceResponse> {
@ -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<Types.PayAddressResponse> {
async PayAddress(userId: string, req: Types.PayAddressRequest, linkedApplication?: Application): Promise<Types.PayAddressResponse> {
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<Types.LnurlWithdrawInfoResponse> {
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<void> {
@ -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)
}
})

View file

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