fees
This commit is contained in:
parent
9555b7be51
commit
80933b1ecf
6 changed files with 95 additions and 41 deletions
20
env.example
20
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
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue